相关性检测
This commit is contained in:
@@ -19,8 +19,8 @@ const service = axios.create({
|
||||
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
||||
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
||||
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
||||
baseURL: '/api', //本地
|
||||
// baseURL: '/', //正式
|
||||
// baseURL: '/api', //本地
|
||||
baseURL: '/', //正式
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ const en = {
|
||||
plagiarismNotChecked: 'Not checked',
|
||||
plagiarismChecking: 'Checking…',
|
||||
plagiarismRecheck: 'Re-check',
|
||||
plagiarismRecheckConfirm: 'Start a new plagiarism check for this manuscript?',
|
||||
plagiarismRecheckCancel: 'Cancel',
|
||||
plagiarismDuplicateCheck: 'Re-check',
|
||||
plagiarismCheckFailed: 'Failed to start plagiarism check.',
|
||||
plagiarismStatusFailed: 'Failed to load plagiarism status.',
|
||||
@@ -62,6 +64,9 @@ const en = {
|
||||
plagiarismSimilarity: 'Similarity',
|
||||
plagiarismFile: 'File',
|
||||
plagiarismPreviewPdf: 'Preview report',
|
||||
plagiarismDownloadReport: 'Download report',
|
||||
plagiarismOnlinePreview: 'Online preview',
|
||||
plagiarismOnlinePreviewFailed: 'Failed to load online preview URL.',
|
||||
plagiarismReportLink: 'Report',
|
||||
plagiarismNoPdfLink: 'No link',
|
||||
plagiarismPreviewClose: 'Close',
|
||||
@@ -1569,8 +1574,48 @@ const en = {
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
refRelevance: {
|
||||
startDetect: 'Start relevance check',
|
||||
detecting: 'Detecting…',
|
||||
startDetect: 'Batch Audit Reference Context',
|
||||
startDetectTip:
|
||||
'Launch AI semantic consistency verification for all references against in-text citations.',
|
||||
checkComplete: 'Relevance check completed',
|
||||
detectingAi: 'Proofreading',
|
||||
auditProgressTitle: 'Batch reference context audit in progress',
|
||||
queuePositionLabel: 'Queue position:',
|
||||
queuePositionNum: 'No. {n}',
|
||||
queueAheadPrefix: 'Ahead:',
|
||||
queueAheadSuffix: ' article(s) waiting for reference relevance check',
|
||||
progressCount: '{done}/{total}',
|
||||
progressCountShort: '{done} done',
|
||||
startFailed: 'Failed to start proofreading',
|
||||
statusFailed: 'Failed to fetch proofreading status',
|
||||
progressFailed: 'Failed to fetch check progress',
|
||||
detailsFailed: 'Failed to load AI proofreading details',
|
||||
detailsEmpty: 'No AI proofreading details available',
|
||||
noReferId: 'Missing reference ID',
|
||||
noArticleId: 'Missing article ID; cannot run check',
|
||||
detecting: 'Checking',
|
||||
pendingDetect: 'Pending',
|
||||
detectFailed: 'Check failed',
|
||||
notFound: 'Not found',
|
||||
copyInsight: 'Copy analysis',
|
||||
copySuccess: 'Copied',
|
||||
copyFailed: 'Copy failed',
|
||||
copyEmpty: 'Nothing to copy',
|
||||
redetect: 'Re-check',
|
||||
redetectFailed: 'Re-check failed',
|
||||
noReason: '(No details)',
|
||||
aiAnalysis: 'AI analysis',
|
||||
expandReason: 'Expand',
|
||||
collapseReason: 'Collapse',
|
||||
expandChips: 'Show all AI Relevance Assessment Results',
|
||||
collapseChips: 'Hide AI Relevance Assessment Results',
|
||||
expandChipsAll: 'Expand all ({n})',
|
||||
collapseChipsAll: 'Collapse all',
|
||||
summaryPartial: 'Checking {pending}, {done} done',
|
||||
summaryPartialDone: ', {done} done',
|
||||
summaryPartialPending: 'Checking {pending}',
|
||||
summaryChecking: 'Checking',
|
||||
pendingCites: '{n} more checking…',
|
||||
filterAll: 'All ({count})',
|
||||
filterModify: 'Needs revision ({count})',
|
||||
columnTitle: 'AI citation relevance review',
|
||||
@@ -1578,6 +1623,7 @@ const en = {
|
||||
uncitedDesc: 'This reference appears in the list but no matching in-text citation was found.',
|
||||
uncitedTip: 'Remove it from the reference list or add an in-text citation in the manuscript.',
|
||||
citationN: 'Cite {n}',
|
||||
citeInParagraphN: 'Occurrence {n} in paragraph',
|
||||
relevancePct: 'Relevance {score}%',
|
||||
relevancePctShort: '{score}%',
|
||||
viewAiAnalysis: 'View AI analysis →',
|
||||
@@ -1591,7 +1637,7 @@ const en = {
|
||||
locationMatcher: 'Location context matcher',
|
||||
manuscriptSection: 'Manuscript section',
|
||||
highlightedMatch: 'Highlighted match',
|
||||
excerptEmpty: 'No excerpt available yet.',
|
||||
excerptEmpty: 'Original cited content not found',
|
||||
annotationComment: 'Comment',
|
||||
annotationDelete: 'Suggest removal',
|
||||
annotationRevision: 'Revision',
|
||||
|
||||
@@ -48,6 +48,8 @@ const zh = {
|
||||
plagiarismNotChecked: '未检测',
|
||||
plagiarismChecking: '正在检测…',
|
||||
plagiarismRecheck: '重新查重',
|
||||
plagiarismRecheckConfirm: '确认要重新发起查重吗?',
|
||||
plagiarismRecheckCancel: '取消',
|
||||
plagiarismDuplicateCheck: '重复检查',
|
||||
plagiarismCheckFailed: '查重任务启动失败。',
|
||||
plagiarismStatusFailed: '获取查重状态失败。',
|
||||
@@ -60,6 +62,9 @@ const zh = {
|
||||
plagiarismSimilarity: '相似度',
|
||||
plagiarismFile: '文件',
|
||||
plagiarismPreviewPdf: '预览报告',
|
||||
plagiarismDownloadReport: '下载报告',
|
||||
plagiarismOnlinePreview: '在线预览',
|
||||
plagiarismOnlinePreviewFailed: '获取在线预览地址失败。',
|
||||
plagiarismReportLink: '报告',
|
||||
plagiarismNoPdfLink: '无链接',
|
||||
plagiarismPreviewClose: '关闭',
|
||||
@@ -1550,8 +1555,47 @@ const zh = {
|
||||
cancel: '取消'
|
||||
},
|
||||
refRelevance: {
|
||||
startDetect: '开始检测相关性',
|
||||
startDetect: '全量引文相关性核查',
|
||||
startDetectTip: '一键启动对本篇稿件全部参考文献与正文语境的 AI 一致性核查',
|
||||
checkComplete: '相关性检测已完成',
|
||||
detectingAi: '校对中',
|
||||
auditProgressTitle: '全量引文相关性核查中',
|
||||
queuePositionLabel: '当前排队位置:',
|
||||
queuePositionNum: '第 {n} 篇',
|
||||
queueAheadPrefix: '前方有',
|
||||
queueAheadSuffix: ' 篇文章参考文献相关性待检测',
|
||||
progressCount: '{done}/{total}',
|
||||
progressCountShort: '已完成 {done}',
|
||||
startFailed: '启动校对失败',
|
||||
statusFailed: '获取校对状态失败',
|
||||
progressFailed: '获取检测进度失败',
|
||||
detailsFailed: '获取 AI 校对详情失败',
|
||||
detailsEmpty: '暂无 AI 校对详情',
|
||||
noReferId: '缺少参考文献 ID',
|
||||
noArticleId: '缺少稿件 ID,无法检测',
|
||||
detecting: '检测中…',
|
||||
pendingDetect: '待检测',
|
||||
detectFailed: '检测失败',
|
||||
notFound: '未找到',
|
||||
copyInsight: '复制分析',
|
||||
copySuccess: '已复制',
|
||||
copyFailed: '复制失败',
|
||||
copyEmpty: '暂无可复制内容',
|
||||
redetect: '重新检测',
|
||||
redetectFailed: '重新检测失败',
|
||||
noReason: '(暂无说明)',
|
||||
aiAnalysis: 'AI 分析',
|
||||
expandReason: '展开',
|
||||
collapseReason: '收起',
|
||||
expandChips: '显示全部 AI 相关性评定结果',
|
||||
collapseChips: '隐藏 AI 相关性评定结果',
|
||||
expandChipsAll: '展开全部({n})',
|
||||
collapseChipsAll: '收起全部',
|
||||
summaryPartial: '正在检测 {pending} 处,已完成 {done} 处',
|
||||
summaryPartialDone: ',已完成 {done} 处',
|
||||
summaryPartialPending: '正在检测 {pending} 处',
|
||||
summaryChecking: '正在检测',
|
||||
pendingCites: '另有 {n} 处检测中…',
|
||||
filterAll: '全部({count})',
|
||||
filterModify: '需修改({count})',
|
||||
columnTitle: 'AI 智能评定相关性建议',
|
||||
@@ -1559,6 +1603,7 @@ const zh = {
|
||||
uncitedDesc: '该文献已列入参考文献列表,但正文中未找到对应的引用上标。',
|
||||
uncitedTip: '建议:从参考文献列表中删除,或在正文中补充引用。',
|
||||
citationN: '第{n}处',
|
||||
citeInParagraphN: '本段第{n}处',
|
||||
relevancePct: '相关性 {score}%',
|
||||
relevancePctShort: '{score}%',
|
||||
viewAiAnalysis: '查看 AI 分析 →',
|
||||
@@ -1572,7 +1617,7 @@ const zh = {
|
||||
locationMatcher: '引用位置匹配',
|
||||
manuscriptSection: '稿件段落',
|
||||
highlightedMatch: '高亮匹配',
|
||||
excerptEmpty: '暂无段落原文,待接口返回',
|
||||
excerptEmpty: '原引用内容未找到',
|
||||
annotationComment: '批注',
|
||||
annotationDelete: '建议删除',
|
||||
annotationRevision: '修订',
|
||||
|
||||
@@ -1343,7 +1343,7 @@ export default {
|
||||
p_article_id: this.p_article_id
|
||||
})
|
||||
.then((res) => {
|
||||
this.chanFerForm = res.data.refers;
|
||||
this.chanFerForm = (res.data.refers || []).slice();
|
||||
this.chanFerFormRepeatList = Object.values(res.data.repeat);
|
||||
for (var i = 0; i < this.chanFerForm.length; i++) {
|
||||
this.chanFerForm[i].edit_mark = 1;
|
||||
|
||||
@@ -1439,7 +1439,7 @@ export default {
|
||||
p_article_id: this.p_article_id
|
||||
})
|
||||
.then((res) => {
|
||||
this.chanFerForm = res.data.refers;
|
||||
this.chanFerForm = (res.data.refers || []).slice();
|
||||
this.chanFerFormRepeatList = Object.values(res.data.repeat);
|
||||
for (var i = 0; i < this.chanFerForm.length; i++) {
|
||||
this.chanFerForm[i].edit_mark = 1;
|
||||
|
||||
@@ -976,27 +976,29 @@
|
||||
element-loading-background="transparent"
|
||||
>
|
||||
<div class="plagiarism-check-header">
|
||||
<span class="plagiarism-check-title">{{ $t('articleListEditor.plagiarismListTitle') }}</span>
|
||||
<div class="plagiarism-check-actions">
|
||||
<!-- <el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
icon="el-icon-document-checked"
|
||||
:loading="plagiarismSubmitLoading"
|
||||
@click="submitPlagiarismCheck"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismDuplicateCheck') }}
|
||||
</el-button> -->
|
||||
<div class="plagiarism-check-header-left">
|
||||
<span class="plagiarism-check-title">{{ $t('articleListEditor.plagiarismListTitle') }}</span>
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
class="plagiarism-refresh-btn"
|
||||
:loading="plagiarismListLoading"
|
||||
@click="fetchPlagiarismList(true)"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismRefresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
icon="el-icon-document-checked"
|
||||
class="plagiarism-recheck-btn"
|
||||
:loading="plagiarismSubmitLoading"
|
||||
@click="confirmPlagiarismRecheck"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="plagiarism-check-list-wrap">
|
||||
<div
|
||||
@@ -1019,17 +1021,20 @@
|
||||
</span>
|
||||
<span class="plagiarism-sim-report">
|
||||
<a
|
||||
v-if="hasPlagiarismPdf(row)"
|
||||
v-if="canPlagiarismOnlinePreview(row)"
|
||||
href="javascript:;"
|
||||
class="plagiarism-report-preview"
|
||||
:title="$t('articleListEditor.plagiarismPreviewOpenTab')"
|
||||
@click.prevent="openPlagiarismReportPage(row)"
|
||||
class="plagiarism-report-online"
|
||||
@click.prevent="openPlagiarismOnlinePreview(row)"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismPreviewPdf') }}
|
||||
<i class="el-icon-link"></i>
|
||||
<i
|
||||
v-if="isPlagiarismOnlinePreviewLoading(row)"
|
||||
class="el-icon-loading"
|
||||
></i>
|
||||
<i v-else class="el-icon-view"></i>
|
||||
{{ $t('articleListEditor.plagiarismOnlinePreview') }}
|
||||
</a>
|
||||
<span
|
||||
v-else-if="!isPlagiarismStateLoading(row)"
|
||||
v-if="!canPlagiarismOnlinePreview(row) && !isPlagiarismStateLoading(row)"
|
||||
class="plagiarism-check-no-pdf"
|
||||
>{{ $t('articleListEditor.plagiarismNoPdfLink') }}</span
|
||||
>
|
||||
@@ -1870,7 +1875,8 @@ export default {
|
||||
plagiarismPdfPreviewVisible: false,
|
||||
plagiarismPdfPreviewUrl: '',
|
||||
plagiarismPdfPreviewTitle: '',
|
||||
plagiarismPdfPreviewLoading: false
|
||||
plagiarismPdfPreviewLoading: false,
|
||||
plagiarismOnlinePreviewCheckId: null
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
@@ -3051,6 +3057,22 @@ export default {
|
||||
this.plagiarismPollTimer = null;
|
||||
}
|
||||
},
|
||||
async confirmPlagiarismRecheck() {
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t('articleListEditor.plagiarismRecheckConfirm'),
|
||||
this.$t('articleListEditor.plagiarismRecheck'),
|
||||
{
|
||||
confirmButtonText: this.$t('articleListEditor.plagiarismRecheck'),
|
||||
cancelButtonText: this.$t('articleListEditor.plagiarismRecheckCancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
await this.submitPlagiarismCheck();
|
||||
} catch (e) {
|
||||
/* 用户取消 */
|
||||
}
|
||||
},
|
||||
async submitPlagiarismCheck() {
|
||||
const articleId = String((this.editform && this.editform.articleId) || this.$route.query.id || '').trim();
|
||||
if (!articleId) {
|
||||
@@ -3149,12 +3171,31 @@ export default {
|
||||
if (n < 30) return 'sim-low';
|
||||
return 'sim-high';
|
||||
},
|
||||
formatPlagiarismDateTime(tsSeconds) {
|
||||
const date = new Date(tsSeconds * 1000);
|
||||
if (isNaN(date.getTime())) return '—';
|
||||
const pad = (v) => String(v).padStart(2, '0');
|
||||
return (
|
||||
date.getFullYear() +
|
||||
'-' +
|
||||
pad(date.getMonth() + 1) +
|
||||
'-' +
|
||||
pad(date.getDate()) +
|
||||
' ' +
|
||||
pad(date.getHours()) +
|
||||
':' +
|
||||
pad(date.getMinutes()) +
|
||||
':' +
|
||||
pad(date.getSeconds())
|
||||
);
|
||||
},
|
||||
formatPlagiarismDate(row) {
|
||||
if (!row || typeof row !== 'object') return '—';
|
||||
const raw =
|
||||
row.finish_time ||
|
||||
row.finished_at ||
|
||||
row.update_time ||
|
||||
row.utime ||
|
||||
row.ctime ||
|
||||
row.create_time ||
|
||||
row.created_at ||
|
||||
@@ -3163,27 +3204,26 @@ export default {
|
||||
const s = String(raw).trim();
|
||||
if (/^\d+$/.test(s)) {
|
||||
const num = Number(s);
|
||||
const ts = num > 1e12 ? num : num * 1000;
|
||||
try {
|
||||
return this.formatDate(Math.floor(ts / 1000));
|
||||
} catch (e) {
|
||||
return s;
|
||||
}
|
||||
const tsSec = num > 1e12 ? Math.floor(num / 1000) : num;
|
||||
return this.formatPlagiarismDateTime(tsSec);
|
||||
}
|
||||
const d = new Date(s.replace(/-/g, '/'));
|
||||
if (!isNaN(d.getTime())) {
|
||||
const pad = (v) => String(v).padStart(2, '0');
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||||
return this.formatPlagiarismDateTime(Math.floor(d.getTime() / 1000));
|
||||
}
|
||||
return s.length > 10 ? s.slice(0, 10) : s;
|
||||
return s.length > 19 ? s.slice(0, 19) : s;
|
||||
},
|
||||
resolvePlagiarismPdfUrl(row) {
|
||||
if (!row || typeof row !== 'object') return '';
|
||||
const raw = String(row.viewer_url || row.local_pdf_url || row.localPdfUrl || '').trim();
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) return raw;
|
||||
return this.normalizePlagiarismViewUrl(raw);
|
||||
},
|
||||
normalizePlagiarismViewUrl(raw) {
|
||||
const url = String(raw || '').trim();
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
|
||||
let path = raw.replace(/^\/+/, '');
|
||||
let path = url.replace(/^\/+/, '');
|
||||
if (!/^public\//i.test(path)) {
|
||||
const media = String(this.mediaUrl || '/public/').replace(/\/+$/, '');
|
||||
if (/^https?:\/\//i.test(media)) {
|
||||
@@ -3200,14 +3240,66 @@ export default {
|
||||
}
|
||||
return path;
|
||||
},
|
||||
getPlagiarismCheckId(row) {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
const id = row.check_id != null ? row.check_id : row.id;
|
||||
return id != null && id !== '' ? id : null;
|
||||
},
|
||||
canPlagiarismOnlinePreview(row) {
|
||||
if (!row || this.isPlagiarismStateLoading(row)) return false;
|
||||
if (!this.getPlagiarismCheckId(row)) return false;
|
||||
const s = Number(row.state);
|
||||
if (s === 1 || s === 4 || s === 5) return false;
|
||||
return true;
|
||||
},
|
||||
isPlagiarismOnlinePreviewLoading(row) {
|
||||
const id = this.getPlagiarismCheckId(row);
|
||||
return id != null && String(this.plagiarismOnlinePreviewCheckId) === String(id);
|
||||
},
|
||||
async fetchPlagiarismReportUrl(checkId) {
|
||||
const res = await this.$api.post('api/Plagiarism/getReportUrl', { check_id: checkId });
|
||||
if (!res || res.code !== 0) {
|
||||
throw new Error((res && res.msg) || this.$t('articleListEditor.plagiarismOnlinePreviewFailed'));
|
||||
}
|
||||
const data = res.data;
|
||||
let url = '';
|
||||
if (typeof data === 'string') {
|
||||
url = data.trim();
|
||||
} else if (data && typeof data === 'object') {
|
||||
url = String(
|
||||
data.view_only_url ||
|
||||
data.url ||
|
||||
data.report_url ||
|
||||
data.viewer_url ||
|
||||
data.online_url ||
|
||||
''
|
||||
).trim();
|
||||
}
|
||||
if (!url) {
|
||||
throw new Error(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||
}
|
||||
return this.normalizePlagiarismViewUrl(url);
|
||||
},
|
||||
async openPlagiarismOnlinePreview(row) {
|
||||
const checkId = this.getPlagiarismCheckId(row);
|
||||
if (!checkId) return;
|
||||
this.plagiarismOnlinePreviewCheckId = checkId;
|
||||
try {
|
||||
const url = await this.fetchPlagiarismReportUrl(checkId);
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
} catch (err) {
|
||||
this.$message.error(
|
||||
(err && err.message) || this.$t('articleListEditor.plagiarismOnlinePreviewFailed')
|
||||
);
|
||||
} finally {
|
||||
this.plagiarismOnlinePreviewCheckId = null;
|
||||
}
|
||||
},
|
||||
hasPlagiarismPdf(row) {
|
||||
return !!this.resolvePlagiarismPdfUrl(row);
|
||||
},
|
||||
openPlagiarismReportPage(row) {
|
||||
|
||||
|
||||
const url = this.resolvePlagiarismPdfUrl(row);
|
||||
|
||||
if (!url) return;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
},
|
||||
@@ -3371,13 +3463,42 @@ export default {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.plagiarism-check-actions {
|
||||
display: flex;
|
||||
.plagiarism-check-header-left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.plagiarism-check-actions .el-button {
|
||||
padding: 0 6px;
|
||||
.plagiarism-refresh-btn.el-button--text {
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.plagiarism-refresh-btn.el-button--text:hover,
|
||||
.plagiarism-refresh-btn.el-button--text:focus {
|
||||
color: #409eff;
|
||||
}
|
||||
.plagiarism-recheck-btn.el-button--mini {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #2ec4b6 0%, #0d9b8f 45%, #0a7f76 100%);
|
||||
box-shadow: 0 2px 6px rgba(13, 155, 143, 0.35);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.plagiarism-recheck-btn.el-button--primary:hover,
|
||||
.plagiarism-recheck-btn.el-button--primary:focus {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3dd4c6 0%, #14b0a3 45%, #0e948a 100%);
|
||||
box-shadow: 0 3px 10px rgba(13, 155, 143, 0.45);
|
||||
}
|
||||
.plagiarism-recheck-btn.el-button--primary.is-loading {
|
||||
background: linear-gradient(135deg, #2ec4b6 0%, #0d9b8f 45%, #0a7f76 100%);
|
||||
}
|
||||
.plagiarism-check-title {
|
||||
font-size: 13px;
|
||||
@@ -3437,7 +3558,9 @@ export default {
|
||||
}
|
||||
.plagiarism-sim-date {
|
||||
flex-shrink: 0;
|
||||
min-width: 132px;
|
||||
color: #909399;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.plagiarism-sim-state {
|
||||
flex: 1;
|
||||
@@ -3473,20 +3596,22 @@ export default {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
gap: 16px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.plagiarism-report-preview {
|
||||
color: #409eff;
|
||||
.plagiarism-report-online {
|
||||
color: #006699;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.plagiarism-report-preview:hover {
|
||||
.plagiarism-report-online:hover {
|
||||
text-decoration: underline;
|
||||
color: #66b1ff;
|
||||
color: #0088bb;
|
||||
}
|
||||
.plagiarism-report-preview .el-icon-link {
|
||||
.plagiarism-report-online .el-icon-view {
|
||||
font-size: 14px;
|
||||
}
|
||||
.plagiarism-check-no-pdf {
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
<el-input v-model="AuthorMes.email"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('articleDetailEmail.content')">
|
||||
<div class="email-letter-lang">
|
||||
<span class="email-letter-lang-label">{{ $t('articleDetailEmail.letterLang') }}</span>
|
||||
<el-radio-group v-model="emailLetterLang" size="small" @change="applyEmailLetterBlocks">
|
||||
<el-radio-button label="zh">{{ $t('articleDetailEmail.langZh') }}</el-radio-button>
|
||||
<el-radio-button label="en">{{ $t('articleDetailEmail.langEn') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<p v-html="EmailData.topmail"></p>
|
||||
<p v-html="EmailData.articleInfor"></p>
|
||||
<el-input type="textarea" rows="9" v-model="EmailData.substance" @input="btn_ft=false">
|
||||
@@ -69,13 +62,11 @@
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
const savedLang = localStorage.getItem('langs');
|
||||
return {
|
||||
loading: false,
|
||||
baseUrl: this.Common.baseUrl,
|
||||
articleId: this.$route.query.id,
|
||||
AuthorMes: {},
|
||||
emailLetterLang: savedLang === 'zh' ? 'zh' : 'en',
|
||||
authorDisplayName: '',
|
||||
journalMeta: {
|
||||
title: '',
|
||||
@@ -99,14 +90,6 @@
|
||||
created: function() {
|
||||
this.getData();
|
||||
},
|
||||
watch: {
|
||||
'$i18n.locale'(val) {
|
||||
if (val === 'zh' || val === 'en') {
|
||||
this.emailLetterLang = val;
|
||||
this.applyEmailLetterBlocks();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
upload_enclosure: function() {
|
||||
return this.baseUrl + 'api/Email/up_enclosure_file';
|
||||
@@ -114,8 +97,8 @@
|
||||
},
|
||||
methods: {
|
||||
letterT(key, params) {
|
||||
const loc = this.emailLetterLang === 'zh' ? 'zh' : 'en';
|
||||
return params ? this.$t('articleDetailEmail.' + key, loc, params) : this.$t('articleDetailEmail.' + key, loc);
|
||||
const path = 'articleDetailEmail.' + key;
|
||||
return params ? this.$t(path, 'en', params) : this.$t(path, 'en');
|
||||
},
|
||||
applyEmailLetterBlocks() {
|
||||
const name = this.authorDisplayName || '';
|
||||
@@ -305,20 +288,6 @@ if(this.$route.query.user_id){
|
||||
margin: 30px 0 0 0;
|
||||
}
|
||||
|
||||
.email-letter-lang {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.email-letter-lang-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Email_Data .el-textarea__inner {
|
||||
line-height: 26px;
|
||||
margin: 10px 0 -20px 0;
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="140">
|
||||
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="240">
|
||||
<template slot-scope="scope">
|
||||
{{ $t('reviewerAdd.total') }}: {{ scope.row.rs_num }}
|
||||
<br />
|
||||
@@ -93,6 +93,12 @@
|
||||
<span style="margin-right: 4px; color: red">{{ scope.row.error_times }}</span>
|
||||
<span>({{ scope.row.error_rate }}%)</span>
|
||||
</div>
|
||||
|
||||
<span style="font-size: 12px;color: #aaa;"><span style="color: #888;
|
||||
margin: 0 5px 0 0;
|
||||
">
|
||||
{{ $t('reviewerAdd.latestInvitationLabel') }}
|
||||
</span>{{formatInviteTime(scope.row) }}</span>
|
||||
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -158,17 +164,7 @@ export default {
|
||||
methods: {
|
||||
formatInviteTime(row) {
|
||||
const raw =
|
||||
row.invited_time != null && row.invited_time !== ''
|
||||
? row.invited_time
|
||||
: row.last_invite_time != null && row.last_invite_time !== ''
|
||||
? row.last_invite_time
|
||||
: row.invite_time != null && row.invite_time !== ''
|
||||
? row.invite_time
|
||||
: row.invitation_time != null && row.invitation_time !== ''
|
||||
? row.invitation_time
|
||||
: row.invit_time != null && row.invit_time !== ''
|
||||
? row.invit_time
|
||||
: row.inviteTime;
|
||||
row.last_invite_time? row.last_invite_time : '1778306077';
|
||||
if (raw == null || raw === '') {
|
||||
return '-';
|
||||
}
|
||||
@@ -191,7 +187,8 @@ export default {
|
||||
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
|
||||
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
|
||||
return Y + M + D + ' ' + h + ':' + m;
|
||||
const s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
|
||||
return Y + M + D + ' ' + h + ':' + m + ':' + s;
|
||||
},
|
||||
// 获取编辑列表数据
|
||||
getDate() {
|
||||
|
||||
@@ -130,10 +130,18 @@
|
||||
></commonMajorTableList>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="160">
|
||||
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="240">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.rs_num }}
|
||||
|
||||
<div>
|
||||
{{ scope.row.rs_num }}
|
||||
<font style="margin-left: 5px">({{ $t('reviewerAdd.reviewing') }}:{{ scope.row.now }})</font>
|
||||
</div>
|
||||
<span style="font-size: 12px;color: #aaa;"><span style="color: #888;
|
||||
margin: 0 5px 0 0;
|
||||
">
|
||||
{{ $t('reviewerAdd.latestInvitationLabel') }}
|
||||
</span>{{formatInviteTime(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -270,17 +278,7 @@ export default {
|
||||
},
|
||||
formatInviteTime(row) {
|
||||
const raw =
|
||||
row.invited_time != null && row.invited_time !== ''
|
||||
? row.invited_time
|
||||
: row.last_invite_time != null && row.last_invite_time !== ''
|
||||
? row.last_invite_time
|
||||
: row.invite_time != null && row.invite_time !== ''
|
||||
? row.invite_time
|
||||
: row.invitation_time != null && row.invitation_time !== ''
|
||||
? row.invitation_time
|
||||
: row.invit_time != null && row.invit_time !== ''
|
||||
? row.invit_time
|
||||
: row.inviteTime;
|
||||
row.last_invite_time? row.last_invite_time : '1778306077';
|
||||
if (raw == null || raw === '') {
|
||||
return '-';
|
||||
}
|
||||
@@ -303,7 +301,8 @@ export default {
|
||||
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
|
||||
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
|
||||
return Y + M + D + ' ' + h + ':' + m;
|
||||
const s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
|
||||
return Y + M + D + ' ' + h + ':' + m + ':' + s;
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push({
|
||||
|
||||
@@ -418,6 +418,106 @@ export default {
|
||||
this.$message.error('Upload failed. Removing temporary placeholder.');
|
||||
}
|
||||
},
|
||||
escapeLatexForWmathAttr(latex) {
|
||||
return String(latex || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<');
|
||||
},
|
||||
createWmathHtml(latex, wrap) {
|
||||
const uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
|
||||
const safe = this.escapeLatexForWmathAttr((latex || '').trim());
|
||||
if (!safe) return '';
|
||||
const mode = wrap === 'inline' ? 'inline' : 'block';
|
||||
return (
|
||||
'<wmath contenteditable="false" data-id="' +
|
||||
uid +
|
||||
'" data-latex="' +
|
||||
safe +
|
||||
'" data-wrap="' +
|
||||
mode +
|
||||
'">' +
|
||||
safe +
|
||||
'</wmath>'
|
||||
);
|
||||
},
|
||||
normalizeClipboardLatex(text) {
|
||||
let t = String(text || '')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.trim();
|
||||
if (!t) return '';
|
||||
if (/^\$\$[\s\S]+\$\$$/.test(t)) {
|
||||
t = t.replace(/^\$\$([\s\S]+)\$\$$/, '$1').trim();
|
||||
} else if (/^\$[\s\S]+\$$/.test(t)) {
|
||||
t = t.replace(/^\$([\s\S]+)\$$/, '$1').trim();
|
||||
}
|
||||
return t.replace(/\r\n/g, '\n');
|
||||
},
|
||||
/** 无 $ 包裹的纯 LaTeX,如 Precision_i=\frac{TP_i}{TP_i + FP_i} */
|
||||
isPlainLatexText(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || t.length > 8000) return false;
|
||||
if (/<[a-z][\s\S]*>/i.test(t)) return false;
|
||||
return /\\[a-zA-Z@]+|[_\^]\{|=\s*\\frac|\\frac\{|\\sqrt|\\sum|\\int|\\alpha|\\beta|\\cdot|\\times|\\leq|\\geq|\\neq|\\pm|\\infty|\\left|\\right/.test(
|
||||
t
|
||||
);
|
||||
},
|
||||
resolveWmathWrapMode(latex) {
|
||||
const t = String(latex || '').trim();
|
||||
if (!t) return 'inline';
|
||||
if (/^\\begin\{/.test(t) || /\n\s*\n/.test(t)) return 'block';
|
||||
if (/\\frac|\\dfrac|\\tfrac|\\sum|\\int|\\prod|\\lim|\\sqrt\{|\\displaystyle/.test(t)) {
|
||||
return 'block';
|
||||
}
|
||||
return 'inline';
|
||||
},
|
||||
buildWmathHtmlFromLatexText(text) {
|
||||
const normalized = this.normalizeClipboardLatex(text);
|
||||
if (!normalized) return '';
|
||||
|
||||
if (/\$[\s\S]*\$/.test(normalized)) {
|
||||
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
|
||||
return normalized.replace(mathRegex, (match, blockFormula, inlineFormula) => {
|
||||
const formula = (blockFormula || inlineFormula || '').trim();
|
||||
if (!formula) return '';
|
||||
const mode = blockFormula ? 'block' : 'inline';
|
||||
return this.createWmathHtml(formula, mode);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isPlainLatexText(normalized)) return '';
|
||||
|
||||
const lines = normalized.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
||||
if (lines.length > 1 && lines.every((l) => this.isPlainLatexText(l))) {
|
||||
return lines.map((l) => this.createWmathHtml(l, this.resolveWmathWrapMode(l))).join('');
|
||||
}
|
||||
|
||||
return this.createWmathHtml(normalized, this.resolveWmathWrapMode(normalized));
|
||||
},
|
||||
async pasteClipboardAsMath(ed) {
|
||||
const editor = ed || (window.tinymce && window.tinymce.get(this.tinymceId));
|
||||
if (!editor) return;
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const html = this.buildWmathHtmlFromLatexText(text);
|
||||
if (!html) return;
|
||||
editor.insertContent(html);
|
||||
const editorId = editor.id || this.tinymceId;
|
||||
setTimeout(() => {
|
||||
if (typeof window.renderMathJax === 'function') {
|
||||
window.renderMathJax(editorId);
|
||||
}
|
||||
}, 10);
|
||||
} catch (err) {
|
||||
console.warn('pasteClipboardAsMath failed', err);
|
||||
const isZh = this.$i18n && this.$i18n.locale === 'zh';
|
||||
this.$message.warning(
|
||||
isZh
|
||||
? '无法读取剪贴板。请允许浏览器权限,或复制 LaTeX 后使用 Ctrl+Shift+V(Mac:Cmd+Shift+V)粘贴为公式。'
|
||||
: 'Cannot read clipboard. Allow browser permission, then use Ctrl+Shift+V (Mac: Cmd+Shift+V) to paste as formula.'
|
||||
);
|
||||
}
|
||||
},
|
||||
// 辅助工具:Base64 转 Blob
|
||||
dataURLtoBlob(dataurl) {
|
||||
const arr = dataurl.split(','),
|
||||
@@ -625,6 +725,12 @@ export default {
|
||||
setup(ed) {
|
||||
let currentPasteImages = [];
|
||||
_this.$commonJS.initEditorButton(_this, ed);
|
||||
ed.on('keydown', function (e) {
|
||||
if (!(e.ctrlKey || e.metaKey) || !e.shiftKey) return;
|
||||
if (e.key !== 'v' && e.key !== 'V') return;
|
||||
e.preventDefault();
|
||||
_this.pasteClipboardAsMath(ed);
|
||||
});
|
||||
var currentWmathElement = null;
|
||||
ed.on('click', function (e) {
|
||||
const wmathElement = e.target.closest('wmath');
|
||||
@@ -847,16 +953,19 @@ export default {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
|
||||
content = content.replace(mathRegex, function (match, blockFormula, inlineFormula) {
|
||||
const formula = blockFormula || inlineFormula;
|
||||
const mode = blockFormula ? 'block' : 'inline';
|
||||
console.log(`Matched ${mode} formula:`, formula);
|
||||
|
||||
return `<wmath data-wrap="${mode}" data-latex="${formula.trim()}">${formula.trim()}</wmath>`;
|
||||
});
|
||||
|
||||
|
||||
const plainText = (tempDiv.textContent || tempDiv.innerText || '').trim();
|
||||
const builtPlain = _this.buildWmathHtmlFromLatexText(plainText);
|
||||
if (builtPlain && _this.isPlainLatexText(plainText)) {
|
||||
content = builtPlain;
|
||||
} else {
|
||||
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
|
||||
content = content.replace(mathRegex, function (match, blockFormula, inlineFormula) {
|
||||
const formula = (blockFormula || inlineFormula || '').trim();
|
||||
if (!formula) return '';
|
||||
const mode = blockFormula ? 'block' : 'inline';
|
||||
return _this.createWmathHtml(formula, mode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (args.event) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,8 @@ module.exports = {
|
||||
devServer: {
|
||||
|
||||
// public: 'http://192.168.110.159:8080/', // 你自己本地的ip地址:端口号
|
||||
port: '8080',
|
||||
port: 8080,
|
||||
|
||||
open: true,
|
||||
overlay: {
|
||||
warnings: false,
|
||||
|
||||
Reference in New Issue
Block a user