背调
This commit is contained in:
@@ -1,303 +1,394 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Created by PhpStorm.
|
|
||||||
* User: Administrator
|
|
||||||
* Date: 2026/6/2
|
|
||||||
* Time: 15:04
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace app\api\controller;
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use app\common\service\AuthorBackgroundService;
|
||||||
|
use think\Controller;
|
||||||
|
|
||||||
class Author
|
/**
|
||||||
|
* 作者背调 API(前后端分离,返回 JSON)
|
||||||
|
*
|
||||||
|
* 主接口:background_report / due_diligence(ORCID 必填)
|
||||||
|
* Scopus 相关接口委托 AuthorInfo 实现
|
||||||
|
*/
|
||||||
|
class Author extends Controller
|
||||||
{
|
{
|
||||||
|
/** @var AuthorBackgroundService */
|
||||||
|
private $bgService;
|
||||||
|
|
||||||
|
/** @var AuthorInfo */
|
||||||
|
private $authorInfo;
|
||||||
|
|
||||||
|
public function __construct(\think\Request $request = null)
|
||||||
|
{
|
||||||
|
parent::__construct($request);
|
||||||
|
$this->bgService = new AuthorBackgroundService();
|
||||||
|
$this->authorInfo = new AuthorInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作者背调 HTML 页面入口
|
||||||
|
*
|
||||||
|
* 1. 传了 ORCID → 直接生成报告
|
||||||
|
* 2. 未传 ORCID + 姓氏(机构选填)→ 仅按姓名搜 ORCID;1 条直接报告,多条显示选择列表
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
|
$formAction = $this->resolveFormAction();
|
||||||
|
$params = $this->resolveBackgroundParams();
|
||||||
|
$orcidNorm = $this->bgService->normalizeOrcid($params['orcid']);
|
||||||
|
|
||||||
|
if ($orcidNorm === ''
|
||||||
|
&& $params['last_name'] === ''
|
||||||
|
&& $params['first_name'] === ''
|
||||||
|
&& $params['institution'] === ''
|
||||||
|
) {
|
||||||
|
$this->assign('form_action', $formAction);
|
||||||
|
return $this->fetch('author/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 有 ORCID → 直接报告页
|
||||||
|
if ($orcidNorm !== '') {
|
||||||
|
return $this->renderReportPage($params, $formAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 无 ORCID → 姓氏必填,机构选填
|
||||||
|
if ($params['last_name'] === '') {
|
||||||
|
$this->assign([
|
||||||
|
'form_action' => $formAction,
|
||||||
|
'error_msg' => '未填 ORCID 时,请填写姓氏',
|
||||||
|
'last_name' => $params['last_name'],
|
||||||
|
'first_name' => $params['first_name'],
|
||||||
|
'institution' => $params['institution'],
|
||||||
|
]);
|
||||||
|
return $this->fetch('author/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 仅按姓名搜 ORCID(机构只做排序校验)
|
||||||
|
$search = $this->bgService->searchOrcidCandidates(
|
||||||
|
$params['last_name'],
|
||||||
|
$params['first_name'],
|
||||||
|
$params['institution']
|
||||||
|
);
|
||||||
|
$candidates = $search['candidates'] ?? [];
|
||||||
|
|
||||||
|
if (empty($candidates)) {
|
||||||
|
return $this->renderOrcidRequiredPage($params, $formAction, '已在 OpenAlex、ORCID 官网、Scopus 按姓名检索,未找到带 ORCID 的作者');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($candidates) > 1) {
|
||||||
|
$this->assignCandidateListView($candidates, $params, $formAction);
|
||||||
|
return $this->fetch('author/select_orcid');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect($this->buildReportEntryUrl($formAction, $params, $candidates[0]['orcid']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医学期刊作者背景调查报告(ORCID 必填)
|
||||||
|
*
|
||||||
|
* POST/GET 参数:
|
||||||
|
* orcid / orcid_id ORCID(必填)
|
||||||
|
* lastName / last_name 姓(选填,用于 PubMed 辅助检索与报告展示)
|
||||||
|
* firstName / first_name 名(选填)
|
||||||
|
* institution / affiliation 机构(选填)
|
||||||
|
*/
|
||||||
|
public function background_report()
|
||||||
|
{
|
||||||
|
@set_time_limit(120);
|
||||||
|
|
||||||
|
$params = $this->resolveBackgroundParams();
|
||||||
|
$result = $this->bgService->buildReport(
|
||||||
|
$params['orcid'],
|
||||||
|
$params['last_name'],
|
||||||
|
$params['first_name'],
|
||||||
|
$params['institution']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($result['ok'])) {
|
||||||
|
$code = !empty($result['need_select']) ? 2 : 0;
|
||||||
|
return json([
|
||||||
|
'code' => $code,
|
||||||
|
'msg' => $result['msg'] ?? '查询失败',
|
||||||
|
'data' => $result['data'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 1,
|
||||||
|
'msg' => 'success',
|
||||||
|
'data' => $result['data'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** camelCase 别名 */
|
||||||
|
public function backgroundReport()
|
||||||
|
{
|
||||||
|
return $this->background_report();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 background_report 相同(路由兼容) */
|
||||||
|
public function due_diligence()
|
||||||
|
{
|
||||||
|
return $this->background_report();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dueDiligence()
|
||||||
|
{
|
||||||
|
return $this->due_diligence();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAlex + Crossref 诚信扫描(不依赖 ORCID 必填)
|
||||||
|
*/
|
||||||
|
public function background_check()
|
||||||
|
{
|
||||||
|
return $this->authorInfo->background_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function backgroundCheck()
|
||||||
|
{
|
||||||
|
return $this->background_check();
|
||||||
|
}
|
||||||
|
|
||||||
public function get_hindex()
|
public function get_hindex()
|
||||||
{
|
{
|
||||||
$name = trim(input('get.name'));
|
return $this->authorInfo->get_hindex();
|
||||||
$affil = trim(input('get.affil'));
|
|
||||||
$debug = (int) input('get.debug', 0);
|
|
||||||
$cookieFile = tempnam(sys_get_temp_dir(), 'scopus_cookie_');
|
|
||||||
|
|
||||||
if (empty($name)) {
|
|
||||||
return json(['code' => 0, 'msg' => '请输入作者姓名']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1) 获取 freelookup 页面,用于拿到真实提交地址和隐藏字段。
|
|
||||||
$lookupUrl = 'https://www.scopus.com/freelookup/form/author.uri?zone=TopNavBar&origin=NO%20ORIGIN%20DEFINED';
|
|
||||||
$lookupRes = $this->httpRequest($lookupUrl, null, true, '', $cookieFile);
|
|
||||||
if (!$lookupRes['ok']) {
|
|
||||||
@unlink($cookieFile);
|
|
||||||
$ret = ['code' => 0, 'msg' => '访问 Scopus 失败:' . $lookupRes['msg']];
|
|
||||||
if ($debug === 1) {
|
|
||||||
$ret['debug'] = $this->buildDebugInfo($lookupRes['url'], $lookupRes['http_code'], $lookupRes['body']);
|
|
||||||
}
|
|
||||||
return json($ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formInfo = $this->extractScopusLookupForm($lookupRes['body']);
|
|
||||||
if (empty($formInfo['action'])) {
|
|
||||||
@unlink($cookieFile);
|
|
||||||
$ret = ['code' => 0, 'msg' => 'Scopus 页面结构已变化,未找到查询表单'];
|
|
||||||
if ($debug === 1) {
|
|
||||||
$ret['debug'] = $this->buildDebugInfo($lookupRes['url'], $lookupRes['http_code'], $lookupRes['body']);
|
|
||||||
}
|
|
||||||
return json($ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) 组装查询参数(姓名 + 机构),并携带隐藏字段提交。
|
|
||||||
$postData = $formInfo['hidden_fields'];
|
|
||||||
$postData['authLast'] = $name;
|
|
||||||
$postData['affil'] = $affil;
|
|
||||||
|
|
||||||
$searchRes = $this->httpRequest($formInfo['action'], $postData, true, $lookupUrl, $cookieFile);
|
|
||||||
if (!$searchRes['ok']) {
|
|
||||||
@unlink($cookieFile);
|
|
||||||
$ret = ['code' => 0, 'msg' => '查询 Scopus 失败:' . $searchRes['msg']];
|
|
||||||
if ($debug === 1) {
|
|
||||||
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
|
|
||||||
}
|
|
||||||
return json($ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
$blockMsg = $this->detectScopusBlocking($searchRes['body']);
|
|
||||||
if (!empty($blockMsg)) {
|
|
||||||
@unlink($cookieFile);
|
|
||||||
$ret = ['code' => 0, 'msg' => $blockMsg];
|
|
||||||
$fallback = $this->fallbackByOpenAlex($name, $affil);
|
|
||||||
if ($fallback !== null) {
|
|
||||||
$ret = array_merge($fallback, [
|
|
||||||
'msg' => $blockMsg . ',已自动降级 OpenAlex 结果'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if ($debug === 1) {
|
|
||||||
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
|
|
||||||
}
|
|
||||||
return json($ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) 从返回页提取 h-index(优先匹配“h-index”关键词附近数字)。
|
|
||||||
$hIndex = $this->extractHIndexFromHtml($searchRes['body']);
|
|
||||||
if ($hIndex === null) {
|
|
||||||
@unlink($cookieFile);
|
|
||||||
$ret = [
|
|
||||||
'code' => 0,
|
|
||||||
'msg' => '未从 Scopus 结果页解析到 H 指数(可能需要人工登录或页面结构调整)'
|
|
||||||
];
|
|
||||||
if ($debug === 1) {
|
|
||||||
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
|
|
||||||
}
|
|
||||||
return json($ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
@unlink($cookieFile);
|
|
||||||
|
|
||||||
$ret = [
|
|
||||||
'code' => 1,
|
|
||||||
'name' => $name,
|
|
||||||
'affil' => $affil,
|
|
||||||
'h_index_scopus' => $hIndex,
|
|
||||||
'source' => 'scopus_freelookup',
|
|
||||||
];
|
|
||||||
if ($debug === 1) {
|
|
||||||
$ret['debug'] = $this->buildDebugInfo($searchRes['url'], $searchRes['http_code'], $searchRes['body']);
|
|
||||||
}
|
|
||||||
return json($ret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function httpRequest($url, $postData = null, $followLocation = true, $referer = '', $cookieFile = '')
|
public function getHindex()
|
||||||
{
|
{
|
||||||
$ch = curl_init();
|
return $this->get_hindex();
|
||||||
$options = [
|
|
||||||
CURLOPT_URL => $url,
|
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
|
||||||
CURLOPT_SSL_VERIFYHOST => false,
|
|
||||||
CURLOPT_FOLLOWLOCATION => $followLocation,
|
|
||||||
CURLOPT_MAXREDIRS => 8,
|
|
||||||
CURLOPT_TIMEOUT => 30,
|
|
||||||
CURLOPT_CONNECTTIMEOUT => 15,
|
|
||||||
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
|
||||||
CURLOPT_ENCODING => '',
|
|
||||||
CURLOPT_HTTPHEADER => [
|
|
||||||
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!empty($referer)) {
|
|
||||||
$options[CURLOPT_REFERER] = $referer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($cookieFile)) {
|
|
||||||
$options[CURLOPT_COOKIEJAR] = $cookieFile;
|
|
||||||
$options[CURLOPT_COOKIEFILE] = $cookieFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($postData)) {
|
|
||||||
$options[CURLOPT_POST] = true;
|
|
||||||
$options[CURLOPT_POSTFIELDS] = http_build_query($postData);
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_setopt_array($ch, $options);
|
|
||||||
$body = curl_exec($ch);
|
|
||||||
$error = curl_error($ch);
|
|
||||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
$finalUrl = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($error) {
|
|
||||||
if (strpos($error, 'Maximum (') !== false && strpos($error, 'redirects followed') !== false) {
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'msg' => 'Scopus 跳转过多(可能触发登录/验证页面),请稍后重试或先在浏览器登录 Scopus',
|
|
||||||
'body' => '',
|
|
||||||
'http_code' => $httpCode,
|
|
||||||
'url' => $finalUrl
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return ['ok' => false, 'msg' => $error, 'body' => '', 'http_code' => $httpCode, 'url' => $finalUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($httpCode >= 400 || $httpCode === 0) {
|
|
||||||
return ['ok' => false, 'msg' => 'HTTP ' . $httpCode, 'body' => (string) $body, 'http_code' => $httpCode, 'url' => $finalUrl];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['ok' => true, 'msg' => '', 'body' => (string) $body, 'http_code' => $httpCode, 'url' => $finalUrl];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function detectScopusBlocking($html)
|
public function get_scopus_id()
|
||||||
{
|
{
|
||||||
if (empty($html)) {
|
return $this->authorInfo->get_scopus_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScopusId()
|
||||||
|
{
|
||||||
|
return $this->get_scopus_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check_scopus_cookie()
|
||||||
|
{
|
||||||
|
return $this->authorInfo->check_scopus_cookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkScopusCookie()
|
||||||
|
{
|
||||||
|
return $this->check_scopus_cookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save_scopus_cookie()
|
||||||
|
{
|
||||||
|
return $this->authorInfo->save_scopus_cookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveScopusCookie()
|
||||||
|
{
|
||||||
|
return $this->save_scopus_cookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login_scopus()
|
||||||
|
{
|
||||||
|
return $this->authorInfo->login_scopus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginScopus()
|
||||||
|
{
|
||||||
|
return $this->login_scopus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check_elsevier_api()
|
||||||
|
{
|
||||||
|
if (method_exists($this->authorInfo, 'check_elsevier_api')) {
|
||||||
|
return $this->authorInfo->check_elsevier_api();
|
||||||
|
}
|
||||||
|
return json(['code' => 0, 'msg' => 'check_elsevier_api not implemented']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkElsevierApi()
|
||||||
|
{
|
||||||
|
return $this->check_elsevier_api();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析背调查询参数(兼容多种命名)
|
||||||
|
*/
|
||||||
|
private function resolveBackgroundParams()
|
||||||
|
{
|
||||||
|
$pick = function (...$keys) {
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
$v = trim((string) input('param.' . $k, ''));
|
||||||
|
if ($v === '') {
|
||||||
|
$v = trim((string) input('post.' . $k, ''));
|
||||||
|
}
|
||||||
|
if ($v === '') {
|
||||||
|
$v = trim((string) input('get.' . $k, ''));
|
||||||
|
}
|
||||||
|
if ($v !== '') {
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
};
|
||||||
|
|
||||||
$text = strtolower(strip_tags($html));
|
|
||||||
if (strpos($text, 'sign in') !== false || strpos($text, 'institutional sign in') !== false) {
|
|
||||||
return 'Scopus 返回登录页,当前环境未授权访问作者详情页面';
|
|
||||||
}
|
|
||||||
if (strpos($text, 'captcha') !== false || strpos($text, 'are you a robot') !== false) {
|
|
||||||
return 'Scopus 触发了人机验证,当前接口无法自动通过';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildDebugInfo($finalUrl, $httpCode, $html)
|
|
||||||
{
|
|
||||||
$normalized = html_entity_decode(strip_tags((string) $html), ENT_QUOTES, 'UTF-8');
|
|
||||||
$normalized = preg_replace('/\s+/u', ' ', $normalized);
|
|
||||||
$snippet = mb_substr($normalized, 0, 300, 'UTF-8');
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'final_url' => (string) $finalUrl,
|
'orcid' => $pick('orcid', 'orcid_id'),
|
||||||
'http_code' => (int) $httpCode,
|
'last_name' => $pick('lastName', 'last_name', 'lastname', 'surname'),
|
||||||
'page_snippet' => $snippet,
|
'first_name' => $pick('firstName', 'first_name', 'firstname', 'given_name'),
|
||||||
'contains_signin' => stripos($normalized, 'sign in') !== false ? 1 : 0,
|
'institution' => $pick('institution', 'affiliation', 'affil', 'org'),
|
||||||
'contains_captcha' => stripos($normalized, 'captcha') !== false ? 1 : 0,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractScopusLookupForm($html)
|
private function resolveFormAction()
|
||||||
{
|
{
|
||||||
$ret = [
|
return rtrim($this->request->root(), '/') . '/api/author/index';
|
||||||
'action' => '',
|
|
||||||
'hidden_fields' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (empty($html)) {
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先定位包含 author 的 form,减少解析误匹配。
|
|
||||||
if (preg_match('/<form[^>]*action=["\']([^"\']+)["\'][^>]*>.*?<\/form>/is', $html, $formMatch)) {
|
|
||||||
$action = trim($formMatch[1]);
|
|
||||||
if (!preg_match('/^https?:\/\//i', $action)) {
|
|
||||||
$action = 'https://www.scopus.com' . (substr($action, 0, 1) === '/' ? '' : '/') . $action;
|
|
||||||
}
|
|
||||||
$ret['action'] = $action;
|
|
||||||
|
|
||||||
if (preg_match_all('/<input[^>]*type=["\']hidden["\'][^>]*>/is', $formMatch[0], $inputs)) {
|
|
||||||
foreach ($inputs[0] as $inputTag) {
|
|
||||||
if (preg_match('/name=["\']([^"\']+)["\']/i', $inputTag, $nameMatch)) {
|
|
||||||
$fieldName = trim($nameMatch[1]);
|
|
||||||
$fieldVal = '';
|
|
||||||
if (preg_match('/value=["\']([^"\']*)["\']/i', $inputTag, $valMatch)) {
|
|
||||||
$fieldVal = $valMatch[1];
|
|
||||||
}
|
|
||||||
$ret['hidden_fields'][$fieldName] = $fieldVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractHIndexFromHtml($html)
|
private function renderReportPage(array $params, $formAction)
|
||||||
{
|
{
|
||||||
if (empty($html)) {
|
$result = $this->bgService->buildReport(
|
||||||
return null;
|
$params['orcid'],
|
||||||
}
|
$params['last_name'],
|
||||||
|
$params['first_name'],
|
||||||
|
$params['institution']
|
||||||
|
);
|
||||||
|
|
||||||
$text = html_entity_decode(strip_tags($html), ENT_QUOTES, 'UTF-8');
|
if (empty($result['ok'])) {
|
||||||
$text = preg_replace('/\s+/u', ' ', $text);
|
$data = $result['data'] ?? [];
|
||||||
|
if (!empty($result['need_select'])) {
|
||||||
$patterns = [
|
$this->assignCandidateListView($data['candidates'] ?? [], $params, $formAction);
|
||||||
'/h[\-\s]?index[^0-9]{0,20}([0-9]{1,3})/iu',
|
return $this->fetch('author/select_orcid');
|
||||||
'/([0-9]{1,3})[^0-9]{0,20}h[\-\s]?index/iu',
|
|
||||||
];
|
|
||||||
foreach ($patterns as $pattern) {
|
|
||||||
if (preg_match($pattern, $text, $m)) {
|
|
||||||
return (int) $m[1];
|
|
||||||
}
|
}
|
||||||
|
if (!empty($data['orcid_required'])) {
|
||||||
|
return $this->renderOrcidRequiredPage($params, $formAction, $data['hint'] ?? '');
|
||||||
|
}
|
||||||
|
$this->assign([
|
||||||
|
'form_action' => $formAction,
|
||||||
|
'error_msg' => $result['msg'] ?? '查询失败',
|
||||||
|
]);
|
||||||
|
return $this->fetch('author/index');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
$this->assignReportView($result['data'], $formAction);
|
||||||
|
return $this->fetch('author/report');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function fallbackByOpenAlex($name, $affil)
|
private function renderOrcidRequiredPage(array $params, $formAction, $hint = '')
|
||||||
{
|
{
|
||||||
$search = urlencode($name);
|
$this->assign([
|
||||||
$url = "https://api.openalex.org/authors?search={$search}&limit=8";
|
'form_action' => $formAction,
|
||||||
$res = $this->httpRequest($url, null, true);
|
'submitted_name' => trim($params['first_name'] . ' ' . $params['last_name']),
|
||||||
if (!$res['ok']) {
|
'submitted_institution' => $params['institution'],
|
||||||
return null;
|
'last_name' => $params['last_name'],
|
||||||
}
|
'first_name' => $params['first_name'],
|
||||||
|
'institution' => $params['institution'],
|
||||||
$data = json_decode($res['body'], true);
|
'hint' => $hint,
|
||||||
$list = $data['results'] ?? [];
|
]);
|
||||||
if (empty($list)) {
|
return $this->fetch('author/orcid_required');
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetAffil = strtolower((string) $affil);
|
|
||||||
$match = null;
|
|
||||||
foreach ($list as $item) {
|
|
||||||
if (empty($targetAffil)) {
|
|
||||||
$match = $item;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$insts = $item['affiliations'] ?? [];
|
|
||||||
foreach ($insts as $inst) {
|
|
||||||
$instName = strtolower($inst['display_name'] ?? '');
|
|
||||||
if ($instName !== '' && strpos($instName, $targetAffil) !== false) {
|
|
||||||
$match = $item;
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($match === null) {
|
|
||||||
$match = $list[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'code' => 1,
|
|
||||||
'name' => $match['display_name'] ?? $name,
|
|
||||||
'affil' => !empty($match['affiliations'][0]['display_name']) ? $match['affiliations'][0]['display_name'] : $affil,
|
|
||||||
'h_index_scopus' => $match['summary_stats']['h_index_scopus'] ?? null,
|
|
||||||
'h_index_openalex' => $match['summary_stats']['h_index'] ?? null,
|
|
||||||
'source' => 'openalex_fallback',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private function buildReportEntryUrl($formAction, array $params, $orcid)
|
||||||
|
{
|
||||||
|
return $formAction . '?' . http_build_query(
|
||||||
|
array_filter([
|
||||||
|
'orcid' => $orcid,
|
||||||
|
'lastName' => $params['last_name'] ?? '',
|
||||||
|
'firstName' => $params['first_name'] ?? '',
|
||||||
|
'institution' => $params['institution'] ?? '',
|
||||||
|
], function ($v) {
|
||||||
|
return trim((string) $v) !== '';
|
||||||
|
}),
|
||||||
|
'',
|
||||||
|
'&',
|
||||||
|
PHP_QUERY_RFC3986
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignCandidateListView(array $candidates, array $params, $formAction)
|
||||||
|
{
|
||||||
|
foreach ($candidates as $idx => $item) {
|
||||||
|
$candidates[$idx]['report_url'] = $this->buildReportEntryUrl(
|
||||||
|
$formAction,
|
||||||
|
$params,
|
||||||
|
$item['orcid'] ?? ''
|
||||||
|
);
|
||||||
|
$candidates[$idx]['matched_class'] = !empty($item['institution_matched']) ? 'match' : '';
|
||||||
|
$name = trim((string) ($item['display_name'] ?? ''));
|
||||||
|
$candidates[$idx]['avatar_letter'] = $name !== ''
|
||||||
|
? mb_strtoupper(mb_substr($name, 0, 1))
|
||||||
|
: '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assign([
|
||||||
|
'form_action' => $formAction,
|
||||||
|
'candidates' => $candidates,
|
||||||
|
'candidate_count' => count($candidates),
|
||||||
|
'submitted_name' => trim(($params['first_name'] ?? '') . ' ' . ($params['last_name'] ?? '')),
|
||||||
|
'submitted_institution' => $params['institution'] ?? '',
|
||||||
|
'last_name' => $params['last_name'] ?? '',
|
||||||
|
'first_name' => $params['first_name'] ?? '',
|
||||||
|
'institution' => $params['institution'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignReportView(array $report, $formAction)
|
||||||
|
{
|
||||||
|
$dupPaperCount = 0;
|
||||||
|
$duplicates = $report['duplicates'] ?? [];
|
||||||
|
foreach ($duplicates as $idx => $dg) {
|
||||||
|
$duplicates[$idx]['paper_count'] = count($dg['papers'] ?? []);
|
||||||
|
$dupPaperCount += $duplicates[$idx]['paper_count'];
|
||||||
|
foreach ($duplicates[$idx]['papers'] as $pi => $dp) {
|
||||||
|
$src = strtolower((string) ($dp['source'] ?? 'orcid'));
|
||||||
|
$duplicates[$idx]['papers'][$pi]['source_class'] = in_array($src, ['orcid', 'pubmed'], true) ? $src : 'orcid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$report['duplicates'] = $duplicates;
|
||||||
|
|
||||||
|
$rw = $report['retraction_watch'] ?? [];
|
||||||
|
$items = $rw['items'] ?? [];
|
||||||
|
foreach ($items as $idx => $it) {
|
||||||
|
$title = !empty($it['author_title']) ? $it['author_title'] : ($it['title'] ?? '');
|
||||||
|
$items[$idx]['display_title'] = mb_substr($title, 0, 120);
|
||||||
|
$items[$idx]['reason_short'] = mb_substr((string) ($it['reason'] ?? ''), 0, 200);
|
||||||
|
$linkUrl = trim((string) ($it['url'] ?? ''));
|
||||||
|
if ($linkUrl === '') {
|
||||||
|
$linkUrl = 'https://retractionwatch.com/?s=' . rawurlencode((string) ($it['title'] ?? ''));
|
||||||
|
}
|
||||||
|
$items[$idx]['link_url'] = $linkUrl;
|
||||||
|
}
|
||||||
|
$report['retraction_watch']['items'] = $items;
|
||||||
|
|
||||||
|
$riskLevel = (string) ($report['conclusion']['risk_level'] ?? '');
|
||||||
|
$riskClass = 'risk-default';
|
||||||
|
if (strpos($riskLevel, '高风险') !== false) {
|
||||||
|
$riskClass = 'risk-high';
|
||||||
|
} elseif (strpos($riskLevel, '中风险') !== false) {
|
||||||
|
$riskClass = 'risk-mid';
|
||||||
|
} elseif (strpos($riskLevel, '低风险') !== false) {
|
||||||
|
$riskClass = 'risk-low';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assign([
|
||||||
|
'form_action' => $formAction,
|
||||||
|
'report' => $report,
|
||||||
|
'risk_class' => $riskClass,
|
||||||
|
'orcid_affiliations_text' => implode(';', $report['basic']['orcid_affiliations'] ?? []),
|
||||||
|
'openalex_institutions_text' => implode(';', $report['basic']['openalex_institutions'] ?? []),
|
||||||
|
'topics_text' => implode(';', $report['metrics']['topics'] ?? []),
|
||||||
|
'rw_match_total' => (int) ($rw['doi_match_count'] ?? 0)
|
||||||
|
+ (int) ($rw['name_match_count'] ?? 0)
|
||||||
|
+ (int) ($rw['name_loose_match_count'] ?? 0),
|
||||||
|
'dup_group_count' => count($duplicates),
|
||||||
|
'dup_paper_count' => $dupPaperCount,
|
||||||
|
'pubmed_list_count' => min(10, count($report['pubmed_papers'] ?? [])),
|
||||||
|
'orcid_section_num' => (($report['metrics']['pubmed_total'] ?? 0) > 0) ? '七' : '六',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
315
application/api/view/author/_styles.html
Normal file
315
application/api/view/author/_styles.html
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #2563eb;
|
||||||
|
--primary-dark: #1d4ed8;
|
||||||
|
--primary-light: #dbeafe;
|
||||||
|
--accent: #0d9488;
|
||||||
|
--accent-light: #ccfbf1;
|
||||||
|
--bg: #f1f5f9;
|
||||||
|
--bg-gradient: linear-gradient(135deg, #eef2ff 0%, #f0fdfa 50%, #f8fafc 100%);
|
||||||
|
--card: #ffffff;
|
||||||
|
--text: #1e293b;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--radius: 14px;
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--shadow: 0 4px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
--shadow-hover: 0 8px 32px rgba(37, 99, 235, 0.12);
|
||||||
|
--success: #059669;
|
||||||
|
--success-bg: #d1fae5;
|
||||||
|
--warn: #d97706;
|
||||||
|
--warn-bg: #fef3c7;
|
||||||
|
--danger: #dc2626;
|
||||||
|
--danger-bg: #fee2e2;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background: var(--bg-gradient);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 32px 20px 48px;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
a { color: var(--primary); text-decoration: none; transition: color .15s; }
|
||||||
|
a:hover { color: var(--primary-dark); }
|
||||||
|
.page { max-width: 920px; margin: 0 auto; }
|
||||||
|
.page-narrow { max-width: 560px; margin: 0 auto; }
|
||||||
|
.page-form { max-width: 480px; margin: 0 auto; }
|
||||||
|
|
||||||
|
/* 顶栏 */
|
||||||
|
.topbar {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
margin-bottom: 20px; font-size: 14px; color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.topbar a {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 8px 14px; background: var(--card); border-radius: 999px;
|
||||||
|
border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
.topbar a:hover { border-color: var(--primary-light); background: #f8fafc; }
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid rgba(255,255,255, .8);
|
||||||
|
padding: 32px 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
width: 56px; height: 56px; margin: 0 auto 14px;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 26px; box-shadow: 0 4px 14px rgba(37, 99, 235, .25);
|
||||||
|
}
|
||||||
|
.card-title { font-size: 22px; font-weight: 700; color: var(--text); margin: 0 0 6px; letter-spacing: -.02em; }
|
||||||
|
.card-subtitle { font-size: 14px; color: var(--text-muted); margin: 0; }
|
||||||
|
|
||||||
|
/* 提示框 */
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px; border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px; line-height: 1.65; margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.alert-info { background: var(--primary-light); color: #1e40af; border: 1px solid #bfdbfe; }
|
||||||
|
.alert-error { background: var(--danger-bg); color: #991b1b; border: 1px solid #fecaca; }
|
||||||
|
.alert-warn { background: var(--warn-bg); color: #92400e; border: 1px solid #fde68a; }
|
||||||
|
|
||||||
|
/* 表单 */
|
||||||
|
.form-group { margin-bottom: 18px; }
|
||||||
|
.form-label {
|
||||||
|
display: block; font-size: 13px; font-weight: 600;
|
||||||
|
color: var(--text); margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.form-label .opt { font-weight: 400; color: var(--text-muted); font-size: 12px; }
|
||||||
|
.form-input {
|
||||||
|
width: 100%; padding: 11px 14px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||||
|
font-size: 15px; color: var(--text);
|
||||||
|
background: #fafbfc; transition: border-color .15s, box-shadow .15s, background .15s;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none; border-color: var(--primary);
|
||||||
|
background: #fff; box-shadow: 0 0 0 3px rgba(37, 99, 235, .12);
|
||||||
|
}
|
||||||
|
.form-input::placeholder { color: #94a3b8; }
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%; margin-top: 8px; padding: 13px 20px;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||||
|
color: #fff; border: none; border-radius: var(--radius-sm);
|
||||||
|
font-size: 16px; font-weight: 600; cursor: pointer;
|
||||||
|
box-shadow: 0 4px 14px rgba(37, 99, 235, .3);
|
||||||
|
transition: transform .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 20px rgba(37, 99, 235, .35);
|
||||||
|
}
|
||||||
|
.form-foot { font-size: 12px; color: var(--text-muted); margin-top: 20px; text-align: center; line-height: 1.7; }
|
||||||
|
|
||||||
|
/* 标签 */
|
||||||
|
.badge {
|
||||||
|
display: inline-block; padding: 3px 10px; border-radius: 999px;
|
||||||
|
font-size: 11px; font-weight: 700; letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
.badge-success { background: var(--success-bg); color: var(--success); }
|
||||||
|
.badge-info { background: var(--primary-light); color: var(--primary-dark); }
|
||||||
|
.badge-warn { background: var(--warn-bg); color: var(--warn); }
|
||||||
|
.badge-danger { background: var(--danger-bg); color: var(--danger); }
|
||||||
|
|
||||||
|
/* 候选列表 */
|
||||||
|
.candidate-list { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.candidate-card {
|
||||||
|
display: block; position: relative;
|
||||||
|
padding: 18px 20px 18px 72px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||||
|
background: #fafbfc; text-decoration: none; color: inherit;
|
||||||
|
transition: border-color .2s, box-shadow .2s, transform .2s, background .2s;
|
||||||
|
}
|
||||||
|
.candidate-card:hover {
|
||||||
|
border-color: var(--primary); background: #fff;
|
||||||
|
box-shadow: var(--shadow-hover); transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.candidate-card.match {
|
||||||
|
border-color: #6ee7b7; background: linear-gradient(135deg, #f0fdf4 0%, #fff 100%);
|
||||||
|
}
|
||||||
|
.candidate-card.match:hover { border-color: var(--success); }
|
||||||
|
.candidate-avatar {
|
||||||
|
position: absolute; left: 18px; top: 50%; transform: translateY(-50%);
|
||||||
|
width: 44px; height: 44px; border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: #fff; font-size: 18px; font-weight: 700;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.candidate-card.match .candidate-avatar {
|
||||||
|
background: linear-gradient(135deg, var(--success) 0%, #34d399 100%);
|
||||||
|
}
|
||||||
|
.candidate-name { font-size: 16px; font-weight: 700; color: var(--text); margin-bottom: 4px; padding-right: 100px; }
|
||||||
|
.candidate-orcid { font-size: 13px; color: var(--primary); font-family: ui-monospace, monospace; margin-bottom: 4px; }
|
||||||
|
.candidate-affil { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
|
||||||
|
.candidate-meta { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||||
|
.candidate-action {
|
||||||
|
position: absolute; right: 18px; top: 50%; transform: translateY(-50%);
|
||||||
|
font-size: 13px; font-weight: 600; color: var(--primary);
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.candidate-card:hover .candidate-action { color: var(--primary-dark); }
|
||||||
|
|
||||||
|
/* 信息块 */
|
||||||
|
.info-panel {
|
||||||
|
background: #f8fafc; border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm); padding: 16px 18px; margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.info-panel dt { font-size: 12px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; margin-top: 10px; }
|
||||||
|
.info-panel dt:first-child { margin-top: 0; }
|
||||||
|
.info-panel dd { margin: 4px 0 0; font-size: 15px; color: var(--text); }
|
||||||
|
|
||||||
|
/* 报告页 */
|
||||||
|
.report-hero {
|
||||||
|
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 55%, #0d9488 100%);
|
||||||
|
color: #fff; margin: -32px -36px 28px; padding: 28px 36px 24px;
|
||||||
|
}
|
||||||
|
.report-hero h1 { font-size: 22px; margin: 0 0 12px; font-weight: 700; letter-spacing: -.02em; }
|
||||||
|
.report-meta { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.report-chip {
|
||||||
|
font-size: 12px; padding: 5px 12px; border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,.15); backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255,255,255,.2);
|
||||||
|
}
|
||||||
|
.risk-banner {
|
||||||
|
padding: 16px 20px; border-radius: var(--radius-sm); margin-bottom: 24px;
|
||||||
|
font-size: 15px; font-weight: 600; display: flex; align-items: flex-start; gap: 12px;
|
||||||
|
}
|
||||||
|
.risk-banner::before { font-size: 22px; line-height: 1; }
|
||||||
|
.risk-low { background: var(--success-bg); color: #065f46; border: 1px solid #a7f3d0; }
|
||||||
|
.risk-low::before { content: "✓"; }
|
||||||
|
.risk-mid { background: var(--warn-bg); color: #92400e; border: 1px solid #fde68a; }
|
||||||
|
.risk-mid::before { content: "⚠"; }
|
||||||
|
.risk-high { background: var(--danger-bg); color: #991b1b; border: 1px solid #fecaca; }
|
||||||
|
.risk-high::before { content: "✕"; }
|
||||||
|
.risk-default { background: #f1f5f9; color: var(--text); border: 1px solid var(--border); }
|
||||||
|
.risk-default::before { content: "ℹ"; }
|
||||||
|
|
||||||
|
.sec-title {
|
||||||
|
font-size: 16px; font-weight: 700; color: var(--text);
|
||||||
|
margin: 28px 0 14px; padding-left: 14px;
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.sec-num {
|
||||||
|
display: inline-flex; width: 24px; height: 24px; border-radius: 6px;
|
||||||
|
background: var(--primary-light); color: var(--primary);
|
||||||
|
font-size: 12px; font-weight: 800; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.data-table {
|
||||||
|
width: 100%; border-collapse: separate; border-spacing: 0;
|
||||||
|
font-size: 14px; margin-bottom: 8px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden;
|
||||||
|
}
|
||||||
|
.data-table th, .data-table td {
|
||||||
|
padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.data-table tr:last-child th, .data-table tr:last-child td { border-bottom: none; }
|
||||||
|
.data-table th {
|
||||||
|
background: #f8fafc; width: 30%; font-weight: 600;
|
||||||
|
color: var(--text-muted); font-size: 13px;
|
||||||
|
}
|
||||||
|
.data-table td { color: var(--text); }
|
||||||
|
.data-table tr:hover td { background: #fafbfc; }
|
||||||
|
|
||||||
|
.tag { display: inline-block; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 700; }
|
||||||
|
.tag-ok { background: var(--success-bg); color: var(--success); }
|
||||||
|
.tag-warn { background: var(--warn-bg); color: var(--warn); }
|
||||||
|
.tag-bad { background: var(--danger-bg); color: var(--danger); }
|
||||||
|
|
||||||
|
.paper-item { font-size: 14px; padding: 10px 0; border-bottom: 1px dashed var(--border); line-height: 1.65; }
|
||||||
|
.paper-item:last-child { border-bottom: none; }
|
||||||
|
.paper-link { color: var(--primary); font-weight: 500; }
|
||||||
|
.paper-link:hover { text-decoration: underline; }
|
||||||
|
.paper-meta { color: var(--text-muted); font-size: 12px; margin-left: 6px; }
|
||||||
|
|
||||||
|
.dup-card {
|
||||||
|
margin: 12px 0; padding: 14px 16px;
|
||||||
|
background: #fffbeb; border: 1px solid #fde68a;
|
||||||
|
border-left: 4px solid var(--warn); border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.src-tag {
|
||||||
|
display: inline-block; padding: 2px 8px; border-radius: 4px;
|
||||||
|
font-size: 11px; font-weight: 700; margin-right: 4px;
|
||||||
|
}
|
||||||
|
.src-orcid { background: var(--accent-light); color: #0f766e; }
|
||||||
|
.src-pubmed { background: var(--primary-light); color: var(--primary-dark); }
|
||||||
|
.match-doi { background: var(--success-bg); color: var(--success); }
|
||||||
|
.match-name { background: var(--warn-bg); color: var(--warn); }
|
||||||
|
.match-loose { background: var(--danger-bg); color: var(--danger); }
|
||||||
|
|
||||||
|
.btn-scopus {
|
||||||
|
display: inline-flex; align-items: center; gap: 8px; margin-top: 10px;
|
||||||
|
padding: 12px 22px; background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
|
||||||
|
color: #fff !important; border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px; font-weight: 700; text-decoration: none;
|
||||||
|
box-shadow: 0 4px 14px rgba(234, 88, 12, .25);
|
||||||
|
transition: transform .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.btn-scopus:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(234, 88, 12, .3); color: #fff !important; }
|
||||||
|
a.ext { color: var(--primary); font-weight: 500; }
|
||||||
|
a.ext:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.report-notes {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
.report-notes-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.report-notes ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
.report-notes li {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-foot {
|
||||||
|
margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border);
|
||||||
|
font-size: 12px; color: var(--text-muted); text-align: center; line-height: 1.7;
|
||||||
|
}
|
||||||
|
.stat-row { display: flex; flex-wrap: wrap; gap: 12px; margin: 16px 0 8px; }
|
||||||
|
.stat-box {
|
||||||
|
flex: 1; min-width: 120px; padding: 14px 16px;
|
||||||
|
background: #f8fafc; border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-box strong { display: block; font-size: 24px; color: var(--primary); font-weight: 800; line-height: 1.2; }
|
||||||
|
.stat-box span { font-size: 12px; color: var(--text-muted); }
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
body { padding: 16px 12px 32px; }
|
||||||
|
.card { padding: 24px 20px; }
|
||||||
|
.report-hero { margin: -24px -20px 20px; padding: 22px 20px; }
|
||||||
|
.candidate-card { padding-left: 18px; padding-top: 52px; }
|
||||||
|
.candidate-avatar { top: 16px; left: 18px; transform: none; }
|
||||||
|
.candidate-name { padding-right: 0; }
|
||||||
|
.candidate-action { top: auto; bottom: 16px; right: 18px; transform: none; }
|
||||||
|
.data-table th, .data-table td { padding: 10px 12px; font-size: 13px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
52
application/api/view/author/index.html
Normal file
52
application/api/view/author/index.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>作者背调查询</title>
|
||||||
|
{include file="author/_styles" /}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-form">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">📋</div>
|
||||||
|
<h1 class="card-title">医学期刊 · 作者背景调查</h1>
|
||||||
|
<p class="card-subtitle">青年编委 / 特约审稿人 / 作者资质初审</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
填写 <strong>ORCID</strong> 可直接生成报告;未填 ORCID 时<strong>姓氏必填</strong>、机构选填,系统按姓名检索后供您选择。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notempty name="error_msg"}
|
||||||
|
<div class="alert alert-error">{$error_msg|htmlspecialchars}</div>
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
<form method="get" action="{$form_action}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">ORCID <span class="opt">— 填写后直接出报告</span></label>
|
||||||
|
<input class="form-input" type="text" name="orcid" placeholder="0000-0002-6388-7847" value="{$orcid|default=''|htmlspecialchars}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">姓 Last Name <span class="opt">— 未填 ORCID 时必填</span></label>
|
||||||
|
<input class="form-input" type="text" name="lastName" placeholder="PENG" value="{$last_name|default=''|htmlspecialchars}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">名 First Name <span class="opt">— 选填</span></label>
|
||||||
|
<input class="form-input" type="text" name="firstName" placeholder="Sijing" value="{$first_name|default=''|htmlspecialchars}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">机构 Institution <span class="opt">— 选填,用于候选列表排序</span></label>
|
||||||
|
<input class="form-input" type="text" name="institution" placeholder="University of Ibadan" value="{$institution|default=''|htmlspecialchars}">
|
||||||
|
</div>
|
||||||
|
<button class="btn-primary" type="submit">生成背调报告</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="form-foot">
|
||||||
|
作者可在 <a href="https://orcid.org/" target="_blank" rel="noopener">orcid.org</a> 注册或查询自己的 ORCID iD
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
55
application/api/view/author/orcid_required.html
Normal file
55
application/api/view/author/orcid_required.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>缺少 ORCID</title>
|
||||||
|
{include file="author/_styles" /}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-narrow">
|
||||||
|
<div class="topbar" style="justify-content:center;">
|
||||||
|
<a href="{$form_action}">← 返回查询页</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon" style="background:linear-gradient(135deg,#f59e0b 0%,#ef4444 100%);">⚠</div>
|
||||||
|
<h1 class="card-title" style="color:#dc2626;">无法自动匹配 ORCID</h1>
|
||||||
|
<p class="card-subtitle">请手动补充 ORCID 后重新生成报告</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warn">
|
||||||
|
系统已按 <strong>姓名</strong> 在 OpenAlex、ORCID 官网、Scopus 检索,未找到可用结果。
|
||||||
|
{notempty name="hint"}<br><br>{$hint|htmlspecialchars}{/notempty}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notempty name="submitted_name"}
|
||||||
|
<dl class="info-panel">
|
||||||
|
<dt>已提交姓名</dt>
|
||||||
|
<dd>{$submitted_name|htmlspecialchars}</dd>
|
||||||
|
{notempty name="submitted_institution"}
|
||||||
|
<dt>已提交机构</dt>
|
||||||
|
<dd>{$submitted_institution|htmlspecialchars}</dd>
|
||||||
|
{/notempty}
|
||||||
|
</dl>
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
<form method="get" action="{$form_action}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">请填写 ORCID <span style="color:#dc2626">*</span></label>
|
||||||
|
<input class="form-input" type="text" name="orcid" placeholder="0000-0002-6388-7847" required autofocus>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="lastName" value="{$last_name|htmlspecialchars}">
|
||||||
|
<input type="hidden" name="firstName" value="{$first_name|htmlspecialchars}">
|
||||||
|
<input type="hidden" name="institution" value="{$institution|htmlspecialchars}">
|
||||||
|
<button class="btn-primary" type="submit">补充 ORCID 并重新生成</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="form-foot">
|
||||||
|
<a href="https://orcid.org/orcid-search/search" target="_blank" rel="noopener">在 ORCID 官网查找 ↗</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
249
application/api/view/author/report.html
Normal file
249
application/api/view/author/report.html
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>作者背调报告 · {$report.basic.display_name|htmlspecialchars}</title>
|
||||||
|
{include file="author/_styles" /}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="topbar">
|
||||||
|
<a href="{$form_action}">← 返回查询页</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="report-hero">
|
||||||
|
<h1>作者背景调查报告</h1>
|
||||||
|
<div class="report-meta">
|
||||||
|
<span class="report-chip">{$report.basic.display_name|htmlspecialchars}</span>
|
||||||
|
{notempty name="report.query.orcid"}
|
||||||
|
<span class="report-chip">ORCID {$report.query.orcid|htmlspecialchars}</span>
|
||||||
|
{/notempty}
|
||||||
|
<span class="report-chip">{$report.query.way|htmlspecialchars}</span>
|
||||||
|
<span class="report-chip">{$report.report_at|htmlspecialchars}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="risk-banner {$risk_class}">
|
||||||
|
<div>{$report.conclusion.risk_level|htmlspecialchars}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec-title"><span class="sec-num">1</span>学术指标</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<div class="stat-box"><strong>{$report.metrics.works_count}</strong><span>论文总数</span></div>
|
||||||
|
<div class="stat-box"><strong>{$report.metrics.cited_by_count}</strong><span>总被引</span></div>
|
||||||
|
<div class="stat-box"><strong>{$report.metrics.h_index}</strong><span>H 指数</span></div>
|
||||||
|
<div class="stat-box"><strong>{$report.metrics.i10_index}</strong><span>i10 指数</span></div>
|
||||||
|
<div class="stat-box"><strong>{$report.metrics.pubmed_total}</strong><span>PubMed</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="data-table">
|
||||||
|
{notempty name="report.metrics.pubmed_query"}
|
||||||
|
<tr><th>PubMed 检索式</th><td>
|
||||||
|
{$report.metrics.pubmed_query|htmlspecialchars}
|
||||||
|
{gt name="report.metrics.pubmed_total" value="0"}
|
||||||
|
— <a class="ext" href="{$report.metrics.pubmed_url|htmlspecialchars}" target="_blank" rel="noopener">在 PubMed 打开 ↗</a>
|
||||||
|
{/gt}
|
||||||
|
</td></tr>
|
||||||
|
{/notempty}
|
||||||
|
{notempty name="report.metrics.topics"}
|
||||||
|
<tr><th>主要研究方向</th><td>{$topics_text|htmlspecialchars}</td></tr>
|
||||||
|
{/notempty}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="sec-title"><span class="sec-num">2</span>诚信 / 不良记录</div>
|
||||||
|
<div class="alert alert-info" style="font-size:13px;">
|
||||||
|
<strong>比对策略:</strong>
|
||||||
|
① 有 DOI 的 ORCID 作品 → 撤稿库 DOI 精确比对 |
|
||||||
|
② 无 DOI → 姓名+题目回退匹配(同名有风险)<br>
|
||||||
|
已比对 DOI <strong>{$report.retraction_watch.checked_doi_count|default=0}</strong> 篇 ·
|
||||||
|
无 DOI <strong>{$report.retraction_watch.no_doi_count|default=0}</strong> 篇
|
||||||
|
{gt name="rw_match_total" value="0"}
|
||||||
|
· 命中:
|
||||||
|
<span class="src-tag match-doi">DOI {$report.retraction_watch.doi_match_count|default=0}</span>
|
||||||
|
<span class="src-tag match-name">姓名+题目 {$report.retraction_watch.name_match_count|default=0}</span>
|
||||||
|
<span class="src-tag match-loose">姓名+机构 {$report.retraction_watch.name_loose_match_count|default=0}</span>
|
||||||
|
{/gt}
|
||||||
|
</div>
|
||||||
|
{empty name="report.retraction_watch.ok"}
|
||||||
|
<p><span class="tag tag-warn">⚠ {$report.retraction_watch.msg|default='撤稿数据库暂不可用'|htmlspecialchars}</span></p>
|
||||||
|
{else/}
|
||||||
|
{empty name="report.retraction_watch.items"}
|
||||||
|
<p><span class="tag tag-ok">✅ 未匹配到撤稿 / 关注声明记录</span></p>
|
||||||
|
{else/}
|
||||||
|
<p style="margin-bottom:14px;">
|
||||||
|
{gt name="report.retraction_watch.misconduct_count" value="0"}
|
||||||
|
<span class="tag tag-bad">发现不端相关记录</span>
|
||||||
|
{else/}
|
||||||
|
<span class="tag tag-warn">发现撤稿 / 关注声明</span>
|
||||||
|
{/gt}
|
||||||
|
撤稿 {$report.retraction_watch.retraction_count|default=0} 条 · 不端 {$report.retraction_watch.misconduct_count|default=0} 条
|
||||||
|
</p>
|
||||||
|
<ul style="list-style:none;padding:0;margin:0;">
|
||||||
|
{volist name="report.retraction_watch.items" id="it"}
|
||||||
|
<li class="paper-item" style="padding:14px 0;">
|
||||||
|
{if condition="$it['match_type'] eq 'doi'"}
|
||||||
|
<span class="src-tag match-doi">{$it.match_label|htmlspecialchars}</span>
|
||||||
|
{elseif condition="$it['match_type'] eq 'name'"/}
|
||||||
|
<span class="src-tag match-name">{$it.match_label|htmlspecialchars}</span>
|
||||||
|
{else/}
|
||||||
|
<span class="src-tag match-loose">{$it.match_label|htmlspecialchars}</span>
|
||||||
|
{/if}
|
||||||
|
<strong>{$it.nature|htmlspecialchars}</strong> —
|
||||||
|
<a class="paper-link" href="{$it.link_url|htmlspecialchars}" target="_blank" rel="noopener">{$it.display_title|htmlspecialchars} ↗</a>
|
||||||
|
{notempty name="it.misconduct"}<em style="color:#dc2626;"> [不端]</em>{/notempty}
|
||||||
|
<br><small style="color:var(--text-muted);">
|
||||||
|
{notempty name="it.doi"}DOI <a class="ext" href="https://doi.org/{$it.doi|htmlspecialchars}" target="_blank">{$it.doi|htmlspecialchars}</a> · {/notempty}
|
||||||
|
{$it.reason_short|htmlspecialchars} · {$it.date|htmlspecialchars}
|
||||||
|
</small>
|
||||||
|
</li>
|
||||||
|
{/volist}
|
||||||
|
</ul>
|
||||||
|
{/empty}
|
||||||
|
{/empty}
|
||||||
|
|
||||||
|
<div class="sec-title"><span class="sec-num">3</span>基本信息</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<tr><th>姓名</th><td><strong>{$report.basic.display_name|htmlspecialchars}</strong></td></tr>
|
||||||
|
<tr><th>ORCID</th><td><a class="ext" href="{$report.basic.orcid_url|htmlspecialchars}" target="_blank" rel="noopener">{$report.basic.orcid|htmlspecialchars} ↗</a></td></tr>
|
||||||
|
{notempty name="report.basic.orcid_affiliations"}
|
||||||
|
<tr><th>ORCID 单位</th><td>{$orcid_affiliations_text|htmlspecialchars}</td></tr>
|
||||||
|
{/notempty}
|
||||||
|
{notempty name="report.basic.openalex_institutions"}
|
||||||
|
<tr><th>OpenAlex 机构</th><td>{$openalex_institutions_text|htmlspecialchars}</td></tr>
|
||||||
|
{/notempty}
|
||||||
|
{notempty name="report.basic.openalex_url"}
|
||||||
|
<tr><th>OpenAlex 档案</th><td><a class="ext" href="{$report.basic.openalex_url|htmlspecialchars}" target="_blank" rel="noopener">查看学者页面 ↗</a></td></tr>
|
||||||
|
{/notempty}
|
||||||
|
{notempty name="report.basic.scopus_url"}
|
||||||
|
<tr><th>Scopus 作者页</th><td>
|
||||||
|
<a class="ext" href="{$report.basic.scopus_url|htmlspecialchars}" target="_blank" rel="noopener">
|
||||||
|
打开 Scopus 档案(ID: {$report.basic.scopus_id|htmlspecialchars})↗
|
||||||
|
</a>
|
||||||
|
</td></tr>
|
||||||
|
{/notempty}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="sec-title"><span class="sec-num">4</span>Scopus 检索</div>
|
||||||
|
{notempty name="report.scopus.search_url"}
|
||||||
|
<p style="font-size:14px;color:var(--text-muted);margin:0 0 10px;">
|
||||||
|
检索方式:{notempty name="report.query.orcid"}ORCID {$report.query.orcid|htmlspecialchars}{else/}{$report.query.first_name|htmlspecialchars} {$report.query.last_name|htmlspecialchars} · {$report.query.institution|htmlspecialchars}{/notempty}
|
||||||
|
</p>
|
||||||
|
<a class="btn-scopus" href="{$report.scopus.search_url|htmlspecialchars}" target="_blank" rel="noopener">打开 Scopus 作者检索 ↗</a>
|
||||||
|
<p style="font-size:12px;color:var(--text-muted);margin-top:10px;">需机构 Scopus 订阅账号登录后查看完整数据</p>
|
||||||
|
{else/}
|
||||||
|
<div class="alert alert-warn">请填写 ORCID,或填写姓名与机构后生成 Scopus 链接。</div>
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
{notempty name="report.scopus.api.ok"}
|
||||||
|
<p style="margin-top:18px;font-size:14px;"><strong>Scopus API:</strong> {$report.scopus.api.msg|htmlspecialchars}</p>
|
||||||
|
{notempty name="report.scopus.api.entries"}
|
||||||
|
<table class="data-table" style="margin-top:10px;">
|
||||||
|
<tr>
|
||||||
|
<th>姓名</th><th>机构</th><th>论文</th><th>被引</th><th>H指数</th><th>操作</th>
|
||||||
|
</tr>
|
||||||
|
{volist name="report.scopus.api.entries" id="se"}
|
||||||
|
<tr>
|
||||||
|
<td>{$se.name|htmlspecialchars}</td>
|
||||||
|
<td>{$se.affiliation|htmlspecialchars}</td>
|
||||||
|
<td>{$se.document_count}</td>
|
||||||
|
<td>{$se.cited_by_count}</td>
|
||||||
|
<td>{$se.h_index}</td>
|
||||||
|
<td>
|
||||||
|
{notempty name="se.url"}
|
||||||
|
<a class="ext" href="{$se.url|htmlspecialchars}" target="_blank" rel="noopener">进入 ↗</a>
|
||||||
|
{else/}—{/notempty}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/volist}
|
||||||
|
</table>
|
||||||
|
{/notempty}
|
||||||
|
{else/}
|
||||||
|
<p style="font-size:13px;color:var(--text-muted);margin-top:12px;">{$report.scopus.api.msg|default='未配置 Scopus API Key'|htmlspecialchars}</p>
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
<div class="sec-title"><span class="sec-num">5</span>本地辅助检测</div>
|
||||||
|
<p>
|
||||||
|
题目重复:
|
||||||
|
{empty name="report.duplicates"}
|
||||||
|
<span class="tag tag-ok">未发现</span>
|
||||||
|
{else/}
|
||||||
|
<span class="tag tag-warn">{$dup_group_count} 组重复(共 {$dup_paper_count} 篇)</span>
|
||||||
|
{/empty}
|
||||||
|
</p>
|
||||||
|
{notempty name="report.duplicates"}
|
||||||
|
{volist name="report.duplicates" id="dg" key="gi"}
|
||||||
|
<div class="dup-card">
|
||||||
|
<strong>第 {$gi} 组:</strong>{$dg.title|htmlspecialchars}
|
||||||
|
<small style="color:var(--text-muted);">({$dg.paper_count} 篇相同)</small>
|
||||||
|
<ul style="margin:10px 0 0;padding-left:18px;">
|
||||||
|
{volist name="dg.papers" id="dp" key="pi"}
|
||||||
|
<li class="paper-item" style="border:none;padding:6px 0;">
|
||||||
|
<span class="src-tag src-{$dp.source_class}">{$dp.source|default='未知'|htmlspecialchars}</span>
|
||||||
|
[{$dp.year|htmlspecialchars}]
|
||||||
|
{notempty name="dp.open_url"}
|
||||||
|
<a class="paper-link" href="{$dp.open_url|htmlspecialchars}" target="_blank" rel="noopener">{$dp.title|htmlspecialchars} ↗</a>
|
||||||
|
{else/}{$dp.title|htmlspecialchars}{/notempty}
|
||||||
|
{notempty name="dp.journal"} <em style="color:var(--text-muted);">— {$dp.journal|htmlspecialchars}</em>{/notempty}
|
||||||
|
</li>
|
||||||
|
{/volist}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/volist}
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
{gt name="report.metrics.pubmed_total" value="0"}
|
||||||
|
<div class="sec-title"><span class="sec-num">6</span>PubMed 文献({$report.metrics.pubmed_total} 篇)</div>
|
||||||
|
{empty name="report.pubmed_papers"}
|
||||||
|
<p style="color:var(--text-muted);font-size:14px;">未获取到文献详情,请点击上方 PubMed 链接查看。</p>
|
||||||
|
{else/}
|
||||||
|
{volist name="report.pubmed_papers" id="p" key="i" length="10"}
|
||||||
|
<div class="paper-item">{$i}. [{$p.year|htmlspecialchars}]
|
||||||
|
{notempty name="p.open_url"}
|
||||||
|
<a class="paper-link" href="{$p.open_url|htmlspecialchars}" target="_blank" rel="noopener">{$p.title|htmlspecialchars} ↗</a>
|
||||||
|
{else/}{$p.title|htmlspecialchars}{/notempty}
|
||||||
|
{notempty name="p.journal"} <em style="color:var(--text-muted);">— {$p.journal|htmlspecialchars}</em>{/notempty}
|
||||||
|
<span class="paper-meta">
|
||||||
|
{notempty name="p.pmid"}<a class="ext" href="https://pubmed.ncbi.nlm.nih.gov/{$p.pmid|htmlspecialchars}/" target="_blank">PubMed</a>{/notempty}
|
||||||
|
{notempty name="p.doi"} · <a class="ext" href="https://doi.org/{$p.doi|htmlspecialchars}" target="_blank">DOI</a>{/notempty}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/volist}
|
||||||
|
{/empty}
|
||||||
|
{/gt}
|
||||||
|
|
||||||
|
{notempty name="report.orcid_papers.papers"}
|
||||||
|
<div class="sec-title"><span class="sec-num">{$orcid_section_num}</span>ORCID 作品({$report.orcid_papers.total} 条)</div>
|
||||||
|
{volist name="report.orcid_papers.papers" id="p" key="i" length="10"}
|
||||||
|
<div class="paper-item">{$i}. [{$p.year|htmlspecialchars}]
|
||||||
|
{notempty name="p.open_url"}
|
||||||
|
<a class="paper-link" href="{$p.open_url|htmlspecialchars}" target="_blank" rel="noopener">{$p.title|htmlspecialchars} ↗</a>
|
||||||
|
{else/}{$p.title|htmlspecialchars}{/notempty}
|
||||||
|
{notempty name="p.journal"} <em style="color:var(--text-muted);">— {$p.journal|htmlspecialchars}</em>{/notempty}
|
||||||
|
<span class="paper-meta">
|
||||||
|
{notempty name="p.doi"}<a class="ext" href="https://doi.org/{$p.doi|htmlspecialchars}" target="_blank">DOI</a>{/notempty}
|
||||||
|
{notempty name="p.pmid"} · <a class="ext" href="https://pubmed.ncbi.nlm.nih.gov/{$p.pmid|htmlspecialchars}/" target="_blank">PubMed</a>{/notempty}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/volist}
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
{notempty name="report.conclusion.notes"}
|
||||||
|
<div class="report-notes">
|
||||||
|
<p class="report-notes-title">说明</p>
|
||||||
|
<ul>
|
||||||
|
{volist name="report.conclusion.notes" id="note"}
|
||||||
|
<li>{$note|htmlspecialchars}</li>
|
||||||
|
{/volist}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/notempty}
|
||||||
|
|
||||||
|
<p class="report-foot">
|
||||||
|
数据来源:OpenAlex · ORCID · PubMed · Scopus · Retraction Watch<br>
|
||||||
|
适用于青年编委 / 特约审稿人 / 作者资质初审
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
application/api/view/author/select_orcid.html
Normal file
48
application/api/view/author/select_orcid.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>选择作者 ORCID</title>
|
||||||
|
{include file="author/_styles" /}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="topbar">
|
||||||
|
<a href="{$form_action}">← 返回查询页</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" style="text-align:left;border-bottom:none;padding-bottom:0;margin-bottom:20px;">
|
||||||
|
<h1 class="card-title" style="text-align:left;">匹配到 {$candidate_count} 位作者</h1>
|
||||||
|
<p class="card-subtitle" style="text-align:left;margin-top:8px;">
|
||||||
|
检索姓名:<strong>{$submitted_name|htmlspecialchars}</strong>
|
||||||
|
{notempty name="submitted_institution"} · 参考机构:<strong>{$submitted_institution|htmlspecialchars}</strong>{/notempty}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
已按<strong>姓名</strong>检索 ORCID。若填写了机构,<span class="badge badge-success">机构一致</span> 的候选项排在最前,请点击确认后生成报告。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="candidate-list">
|
||||||
|
{volist name="candidates" id="item"}
|
||||||
|
<a class="candidate-card {$item.matched_class}" href="{$item.report_url|htmlspecialchars}">
|
||||||
|
<div class="candidate-avatar">{$item.avatar_letter|htmlspecialchars}</div>
|
||||||
|
<span class="candidate-action">生成报告 →</span>
|
||||||
|
<div class="candidate-name">{$item.display_name|default='(姓名未知)'|htmlspecialchars}</div>
|
||||||
|
<div class="candidate-orcid">{$item.orcid|htmlspecialchars}</div>
|
||||||
|
{notempty name="item.affiliations_text"}
|
||||||
|
<div class="candidate-affil">{$item.affiliations_text|htmlspecialchars}</div>
|
||||||
|
{/notempty}
|
||||||
|
<div class="candidate-meta">
|
||||||
|
{eq name="item.institution_matched" value="1"}<span class="badge badge-success">机构一致</span>{/eq}
|
||||||
|
{notempty name="item.sources_text"}<span class="badge badge-info">{$item.sources_text|htmlspecialchars}</span>{/notempty}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/volist}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1378
application/common/service/AuthorBackgroundService.php
Normal file
1378
application/common/service/AuthorBackgroundService.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,4 +18,23 @@ return [
|
|||||||
':name' => ['index/hello', ['method' => 'post']],
|
':name' => ['index/hello', ['method' => 'post']],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Author 背调 / Scopus(兼容 /api/author/ 与 /api/Author/)
|
||||||
|
'api/author/index' => 'api/Author/index',
|
||||||
|
'api/author/background_report' => 'api/Author/background_report',
|
||||||
|
'api/author/due_diligence' => 'api/Author/due_diligence',
|
||||||
|
'api/author/background_check' => 'api/Author/background_check',
|
||||||
|
'api/author/get_hindex' => 'api/Author/get_hindex',
|
||||||
|
'api/author/get_scopus_id' => 'api/Author/get_scopus_id',
|
||||||
|
'api/author/check_scopus_cookie' => 'api/Author/check_scopus_cookie',
|
||||||
|
'api/author/check_elsevier_api' => 'api/Author/check_elsevier_api',
|
||||||
|
'api/Author/index' => 'api/Author/index',
|
||||||
|
'api/Author/background_report' => 'api/Author/background_report',
|
||||||
|
'api/Author/backgroundReport' => 'api/Author/background_report',
|
||||||
|
'api/Author/due_diligence' => 'api/Author/due_diligence',
|
||||||
|
'api/Author/dueDiligence' => 'api/Author/due_diligence',
|
||||||
|
'api/Author/background_check' => 'api/Author/background_check',
|
||||||
|
'api/Author/get_hindex' => 'api/Author/get_hindex',
|
||||||
|
'api/Author/get_scopus_id' => 'api/Author/get_scopus_id',
|
||||||
|
'api/Author/check_elsevier_api' => 'api/Author/check_elsevier_api',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user