审稿人证书生成
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -2178,13 +2178,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
|
||||
"integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
||||
@@ -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: '/', //正式
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MathfieldElement } from 'mathlive';
|
||||
import 'mathlive/dist/mathlive-static.css';
|
||||
import 'mathlive/dist/mathlive-fonts.css';
|
||||
import { importWordDocumentWithMath as parseWordDocumentWithMath, parseHtmlToLatex as convertHtmlToLatex, postProcessImportedWordHtml, parseImportedHtmlToContentRows, mergeAdjacentBlueTags, normalizeSpacesAroundBlueTags } from '@/utils/wordMathImport';
|
||||
import { wrapReferenceHighlightsInBlueHtml } from '@/utils/manuscriptMediaReferences';
|
||||
import api from '../../api/index.js';
|
||||
import Common from '@/components/common/common'
|
||||
import Tiff from 'tiff.js';
|
||||
@@ -852,32 +853,8 @@ export default {
|
||||
// 首字母大写
|
||||
str = capitalizeFirstLetter(str);
|
||||
|
||||
// 添加蓝色标签
|
||||
// 1. 修改正则,只匹配不在 <blue> 标签内的 [数字]
|
||||
// 这里的思路是:匹配 [内容],但通过逻辑过滤掉已经被包裹的情况
|
||||
const regex = /\[(\d+(?:–\d+)?(?:, ?\d+(?:–\d+)?)*)\]/g;
|
||||
|
||||
// 注意:不要在最上面执行 str.replace(/<blue>/g, ''),否则之前的标记就全白做了
|
||||
|
||||
str = str.replace(regex, function (match, content, offset, fullString) {
|
||||
// 【关键判断】:检查匹配位置的前后,是否已经存在 <blue> 和 </blue>
|
||||
const prefix = fullString.substring(offset - 6, offset); // <blue> 是 6 位
|
||||
const suffix = fullString.substring(offset + match.length, offset + match.length + 7); // </blue> 是 7 位
|
||||
|
||||
if (prefix === '<blue>' && suffix === '</blue>') {
|
||||
return match; // 如果已经有标签了,原样返回,不重复标记
|
||||
}
|
||||
|
||||
// 判断逻辑:纯数字、逗号空格、连字符
|
||||
if (/^\d+$/.test(content) || /, ?/.test(content) || /–/.test(content)) {
|
||||
return `<blue>${match}</blue>`;
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 添加蓝色标签:参考文献标号 + Figure/Table 引用
|
||||
str = wrapReferenceHighlightsInBlueHtml(str);
|
||||
|
||||
return str;
|
||||
|
||||
@@ -1581,6 +1558,7 @@ str = str.replace(regex, function (match, content, offset, fullString) {
|
||||
text = text.replace(/<[^\/>]+>\s*<\/[^>]+>/gi, (match) => (match.trim() === '' ? '' : match));
|
||||
text = mergeAdjacentBlueTags(text);
|
||||
text = normalizeSpacesAroundBlueTags(text);
|
||||
text = wrapReferenceHighlightsInBlueHtml(text);
|
||||
parsedData.push(text.trim() === '' ? '' : text);
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,17 @@
|
||||
<template v-else>
|
||||
<el-menu-item :index="item.index" :key="item.index">
|
||||
<i :class="item.icon"></i>
|
||||
<span slot="title"> {{ item.title }}</span>
|
||||
<template slot="title">
|
||||
<el-badge
|
||||
v-if="item.index === 'journalFeeApproval'"
|
||||
is-dot
|
||||
:hidden="applyBadgeFeeApproval <= 0"
|
||||
class="sidebar-menu-fee-approval-badge"
|
||||
>
|
||||
<span>{{ item.title }}</span>
|
||||
</el-badge>
|
||||
<span v-else>{{ item.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
@@ -237,6 +247,7 @@ export default {
|
||||
menuList: [],
|
||||
/** 青年编委申请红点:Young Scientist 父级 + Apply 子项,数据来自 getYboardApplys */
|
||||
applyBadgeYouth: 0,
|
||||
applyBadgeFeeApproval: 0,
|
||||
items: [],
|
||||
// 作者
|
||||
author_items: [
|
||||
@@ -333,7 +344,6 @@ export default {
|
||||
title: this.$t('menu.userManSys6')
|
||||
},
|
||||
{
|
||||
//论文出版监督
|
||||
icon: 'el-icon-lx-copy',
|
||||
index: 'JournalCitationAnalysis',
|
||||
title: this.$t('menu.JournalCitationAnalysis')
|
||||
@@ -773,74 +783,26 @@ export default {
|
||||
//超级管理员
|
||||
this.items = this.admin_items;
|
||||
}
|
||||
if (this.isFeeApprovalAccount()) {
|
||||
this.items.push({
|
||||
icon: 'el-icon-money',
|
||||
index: 'journalFeeApproval',
|
||||
title: this.$t('menu.journalFeeApproval')
|
||||
});
|
||||
this.fetchFeeApprovalBadge();
|
||||
}
|
||||
if (this.isJiajianAccount()) {
|
||||
if (!this.user_cap.includes('superadmin')) {
|
||||
this.injectChiefInspectorMenu();
|
||||
}
|
||||
}
|
||||
if (this.user_cap.includes('superadmin')) {
|
||||
var superadminData = [];
|
||||
|
||||
this.items.splice(
|
||||
3,
|
||||
0,
|
||||
|
||||
{
|
||||
icon: 'el-icon-s-platform',
|
||||
index: '13',
|
||||
title: this.$t('sidebar.chiefInspector'),
|
||||
subs: [
|
||||
{
|
||||
index: 'Classificationmanagement',
|
||||
title: this.$t('sidebar.journalManagement'),
|
||||
subs: [
|
||||
{
|
||||
index: 'JournalManagementAll',
|
||||
title: this.$t('sidebar.journalList')
|
||||
},
|
||||
|
||||
{
|
||||
index: 'GroupClassification',
|
||||
title: this.$t('sidebar.GroupClassification')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
index: 'Academicresourcesupervise',
|
||||
title: this.$t('menu.Academicresourcesupervise'),
|
||||
subs: [
|
||||
{
|
||||
index: 'editorialBoard',
|
||||
title: this.$t('sidebar.editorialBoard1')
|
||||
},
|
||||
{
|
||||
index: 'superYoungScientistManagement',
|
||||
title: this.$t('sidebar.editorialBoard2')
|
||||
},
|
||||
{
|
||||
index: 'superJournalsManagement',
|
||||
title: this.$t('sidebar.editorialBoard3')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
index: 'publicationsupervise',
|
||||
title: this.$t('menu.publicationsupervise'),
|
||||
subs: [
|
||||
{
|
||||
index: 'allPaperSubmitanalysis',
|
||||
// index: 'superYoungScientistManagement',
|
||||
title: this.$t('menu.papersubmit')
|
||||
},
|
||||
|
||||
{
|
||||
index: '18',
|
||||
title: this.$t('menu.ArticlePublication')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
index: 'allJournalCitationAnalysis',
|
||||
title: this.$t('menu.JournalCitationAnalysis')
|
||||
}
|
||||
]
|
||||
}
|
||||
this.getChiefInspectorMenuItem()
|
||||
);
|
||||
|
||||
this.items = [
|
||||
@@ -868,13 +830,88 @@ export default {
|
||||
bus.$on('apply-badge-refresh', () => {
|
||||
this.fetchApplyBadgeSummary();
|
||||
});
|
||||
bus.$on('fee-approval-badge-refresh', () => {
|
||||
this.fetchFeeApprovalBadge();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('apply-badge-refresh');
|
||||
bus.$off('fee-approval-badge-refresh');
|
||||
},
|
||||
methods: {
|
||||
// 获取数据
|
||||
getDate() {},
|
||||
isFeeApprovalAccount() {
|
||||
return String(localStorage.getItem('U_id') || '') === '24950';
|
||||
},
|
||||
isJiajianAccount() {
|
||||
const name = String(localStorage.getItem('U_name') || '').toLowerCase();
|
||||
const uid = String(localStorage.getItem('U_id') || '');
|
||||
return name === 'jiajian' || uid === '29578';
|
||||
},
|
||||
getChiefInspectorMenuItem() {
|
||||
return {
|
||||
icon: 'el-icon-s-platform',
|
||||
index: '13',
|
||||
title: this.$t('sidebar.chiefInspector'),
|
||||
subs: [
|
||||
{
|
||||
index: 'Classificationmanagement',
|
||||
title: this.$t('sidebar.journalManagement'),
|
||||
subs: [
|
||||
{
|
||||
index: 'JournalManagementAll',
|
||||
title: this.$t('sidebar.journalList')
|
||||
},
|
||||
{
|
||||
index: 'GroupClassification',
|
||||
title: this.$t('sidebar.GroupClassification')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
index: 'Academicresourcesupervise',
|
||||
title: this.$t('menu.Academicresourcesupervise'),
|
||||
subs: [
|
||||
{
|
||||
index: 'editorialBoard',
|
||||
title: this.$t('sidebar.editorialBoard1')
|
||||
},
|
||||
{
|
||||
index: 'superYoungScientistManagement',
|
||||
title: this.$t('sidebar.editorialBoard2')
|
||||
},
|
||||
{
|
||||
index: 'superJournalsManagement',
|
||||
title: this.$t('sidebar.editorialBoard3')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
index: 'publicationsupervise',
|
||||
title: this.$t('menu.publicationsupervise'),
|
||||
subs: [
|
||||
{
|
||||
index: 'allPaperSubmitanalysis',
|
||||
title: this.$t('menu.papersubmit')
|
||||
},
|
||||
{
|
||||
index: '18',
|
||||
title: this.$t('menu.ArticlePublication')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
index: 'allJournalCitationAnalysis',
|
||||
title: this.$t('menu.JournalCitationAnalysis')
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
injectChiefInspectorMenu() {
|
||||
if (this.items.some((item) => item.index === '13')) return;
|
||||
this.items.splice(3, 0, this.getChiefInspectorMenuItem());
|
||||
},
|
||||
fetchApplyBadgeSummary() {
|
||||
if (String(this.userrole) !== '1') return;
|
||||
const editorId = localStorage.getItem('U_id');
|
||||
@@ -891,6 +928,127 @@ export default {
|
||||
.catch(() => {
|
||||
this.applyBadgeYouth = 0;
|
||||
});
|
||||
},
|
||||
fetchFeeApprovalBadge() {
|
||||
if (!this.isFeeApprovalAccount()) return;
|
||||
this.$api
|
||||
.post('api/Order/getApplyCount', {})
|
||||
.then((res) => {
|
||||
if (res && res.code === 0) {
|
||||
const pendingCount = this.extractFeeApplyPendingCount(res.data);
|
||||
this.applyBadgeFeeApproval = pendingCount > 0 ? 1 : 0;
|
||||
if (pendingCount > 0) {
|
||||
this.showFeeApprovalNotifyOnce(pendingCount);
|
||||
}
|
||||
} else {
|
||||
this.applyBadgeFeeApproval = 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.applyBadgeFeeApproval = 0;
|
||||
});
|
||||
},
|
||||
showFeeApprovalNotifyOnce(pendingCount) {
|
||||
const uid = String(localStorage.getItem('U_id') || '');
|
||||
const storageKey = 'feeApprovalNotifyShown_' + uid;
|
||||
if (!uid || sessionStorage.getItem(storageKey)) return;
|
||||
if (this.$route.path === '/journalFeeApproval') {
|
||||
sessionStorage.setItem(storageKey, '1');
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(storageKey, '1');
|
||||
const h = this.$createElement;
|
||||
this.$notify({
|
||||
title: this.$t('journalFeeApproval.notifyTitle'),
|
||||
message: h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
width: '260px',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.$router.push('/journalFeeApproval');
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
h(
|
||||
'p',
|
||||
{
|
||||
style: {
|
||||
color: '#006699',
|
||||
margin: '0 0 6px',
|
||||
lineHeight: '1.5'
|
||||
}
|
||||
},
|
||||
this.$t('journalFeeApproval.notifyMessage', { count: pendingCount })
|
||||
),
|
||||
h(
|
||||
'p',
|
||||
{
|
||||
style: {
|
||||
color: '#909399',
|
||||
margin: 0,
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5'
|
||||
}
|
||||
},
|
||||
this.$t('journalFeeApproval.notifyHint')
|
||||
)
|
||||
]
|
||||
),
|
||||
type: 'warning',
|
||||
position: 'bottom-right',
|
||||
duration: 3000,
|
||||
onClick: () => {
|
||||
this.$router.push('/journalFeeApproval');
|
||||
}
|
||||
});
|
||||
},
|
||||
extractFeeApplyList(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (!data || typeof data !== 'object') return [];
|
||||
return data.list || data.applies || data.applys || data.items || data.data || [];
|
||||
},
|
||||
extractFeeApplyPendingCount(data) {
|
||||
if (!data) return 0;
|
||||
if (Array.isArray(data)) {
|
||||
const pendingItem = data.find(function (item) {
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const state = item.state !== undefined ? item.state : item.status;
|
||||
return Number(state) === 0;
|
||||
});
|
||||
if (pendingItem) {
|
||||
return Number(pendingItem.count != null ? pendingItem.count : pendingItem.num != null ? pendingItem.num : pendingItem.total) || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
return (
|
||||
Number(
|
||||
data.pending != null
|
||||
? data.pending
|
||||
: data.wait != null
|
||||
? data.wait
|
||||
: data.pending_count != null
|
||||
? data.pending_count
|
||||
: data.state0 != null
|
||||
? data.state0
|
||||
: data['0'] != null
|
||||
? data['0']
|
||||
: 0
|
||||
) || 0
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
isFeeApplyPending(item) {
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const status = item.status !== undefined ? item.status : item.state;
|
||||
if (status === undefined || status === null || status === '') return true;
|
||||
return status === 0 || status === '0' || status === 'pending' || status === 'wait' || status === 'waiting';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -999,4 +1157,16 @@ export default {
|
||||
color: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-menu-fee-approval-badge {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-menu-fee-approval-badge ::v-deep .el-badge__content.is-dot {
|
||||
top: 50%;
|
||||
right: -8px;
|
||||
border: 0;
|
||||
background-color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
//记得切换
|
||||
|
||||
//正式
|
||||
// const mediaUrl = '/public/';
|
||||
// const baseUrl = '/';
|
||||
const mediaUrl = '/public/';
|
||||
const baseUrl = '/';
|
||||
|
||||
//正式环境
|
||||
|
||||
const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||
const baseUrl = '/api'
|
||||
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||
// const baseUrl = '/api'
|
||||
|
||||
//测试环境
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ const en = {
|
||||
PaperSubmitanalysis: 'Paper Submit Analysis',
|
||||
ArticlePublicationanalysis: 'Article Publication Analysis',
|
||||
journalArticleCount: 'Article Count',
|
||||
journalFeeApproval: 'Journal Fee Approval',
|
||||
Promotionsystem: 'Promotion System',
|
||||
Userdatabase: 'User Database',
|
||||
analysis: 'Article Analysis',
|
||||
@@ -1211,6 +1212,112 @@ const en = {
|
||||
citeRelevanceCopyGroup: 'Copy paragraph',
|
||||
citeRelevanceDownloadHtml: 'Download HTML',
|
||||
citeRelevanceDownloadHtmlSuccess: 'HTML downloaded',
|
||||
citeRelevanceWithUploadedRefs: 'Group export will use {n} uploaded reference(s)',
|
||||
citeRelevanceDownloadHtmlSuccessWithUploaded: 'HTML downloaded (based on {n} uploaded reference(s))',
|
||||
refAnnotationReportTip: 'Download annotated HTML relevance report (run AI check first)',
|
||||
refAnnotationReportShort: 'Report',
|
||||
exportRelevanceWordTip: 'Export Word with citation relevance comments (requires AI audit; yellow/red items become comments)',
|
||||
exportRelevanceWordShort: 'Rel. Word',
|
||||
exportRelevanceWordSuccess: 'Word with relevance comments downloaded',
|
||||
exportRelevanceWordFail: 'Failed to export Word with relevance comments',
|
||||
mediaRefCheckWarn: 'Media reference check: body cites missing {list}',
|
||||
mediaRefCheckMore: ' ({n} total)',
|
||||
wordRefsPreviewShort: 'Word refs',
|
||||
wordRefsPreviewTitle: 'Original Word · References',
|
||||
wordRefsPreviewTotal: 'Total: {count} references',
|
||||
wordRefsPreviewClose: 'Close',
|
||||
wordRefsPreviewCopy: 'Copy plain text',
|
||||
wordRefsPreviewCopySuccess: 'References copied',
|
||||
wordRefsPreviewCopyFail: 'Copy failed',
|
||||
wordRefsPreviewNoArticle: 'Missing article ID — cannot load Word manuscript',
|
||||
wordRefsPreviewNoManuscript: 'Manuscript Word file not found (gridData / getFilesForArticle)',
|
||||
wordRefsPreviewEmpty: 'No References section found in the original Word manuscript',
|
||||
wordRefsPreviewFail: 'Failed to load Word references',
|
||||
wordRefBatchUploadShort: 'Upload proofread Word',
|
||||
wordRefBatchUploadTip: 'Upload proofread Word references, map fields by index, and batch save',
|
||||
wordRefBatchDialogTitle: 'Word references · field mapping',
|
||||
wordRefBatchApply: 'Apply',
|
||||
wordRefBatchStatus: 'Status',
|
||||
wordRefBatchType: 'Type',
|
||||
wordRefBatchAuthor: 'Author',
|
||||
wordRefBatchTitle: 'Title',
|
||||
wordRefBatchJournal: 'Journal',
|
||||
wordRefBatchDateno: 'Publication',
|
||||
wordRefBatchDoi: 'DOI/URL',
|
||||
wordRefBatchExisting: 'Current in system',
|
||||
wordRefBatchMatched: 'Matched',
|
||||
wordRefBatchMissing: 'Missing in Word',
|
||||
wordRefBatchExtra: 'No system row',
|
||||
wordRefBatchSelectAll: 'Select all',
|
||||
wordRefBatchSelectNone: 'Select none',
|
||||
wordRefBatchReparse: 'Re-parse fields (server)',
|
||||
wordRefBatchReparseSuccess: 'Fields re-parsed',
|
||||
wordRefBatchReparseFail: 'Field parsing failed',
|
||||
wordRefBatchSave: 'Save selected',
|
||||
wordRefBatchSaveSuccess: 'Saved {ok}, failed {fail}',
|
||||
wordRefBatchSaveFail: 'Batch save failed',
|
||||
wordRefBatchNothingSelected: 'Select at least one reference to save',
|
||||
wordRefBatchParseFail: 'Failed to parse Word references',
|
||||
wordRefBatchCountMismatch: 'Count mismatch: system {existing}, Word {uploaded}. Please verify before saving.',
|
||||
wordRefBatchClickToEdit: 'Click text to edit',
|
||||
wordRefBatchFieldEmpty: 'Click to fill',
|
||||
wordRefBatchShowExisting: 'Compare',
|
||||
wordRefBatchHideExisting: 'Hide',
|
||||
wordRefBatchParseFailed: 'Incomplete parsing — please fill in each field',
|
||||
wordRefBatchWordOriginal: 'Word original',
|
||||
refAnnotationReportSuccess: 'Annotation report HTML downloaded',
|
||||
refAnnotationReportFail: 'Failed to download annotation report',
|
||||
refAnnotationReportNoArticle: 'Missing article ID — cannot load AI check data',
|
||||
refAnnotationReportNoData: 'No AI relevance results yet — run the check on Reference Conversion first',
|
||||
refAnnotationReportTitleSuffix: 'Review report',
|
||||
refAnnotationReportManuscriptPreview: 'Manuscript preview: {id}',
|
||||
refAnnotationReportRefMatchPrefix: 'Reference source: ',
|
||||
refAnnotationReportRefLibraryPrefix: 'Reference library: ',
|
||||
refAnnotationReportRefTotal: '{n} entries',
|
||||
refAnnotationReportAnalysisTitle: 'Reference analysis',
|
||||
refAnnotationReportStatTotal: 'Reviewed',
|
||||
refAnnotationReportStatDirect: 'Direct match',
|
||||
refAnnotationReportStatStrong: 'Strong match',
|
||||
refAnnotationReportStatWeak: 'Weak match',
|
||||
refAnnotationReportStatNone: 'Not relevant',
|
||||
refAnnotationReportLocateTip: 'Locate and highlight the paragraph on the left',
|
||||
refAnnotationReportLocatePara: 'Paragraph {n} on the left (click to locate)',
|
||||
refAnnotationReportParaRefsPrefix: 'Citations in paragraph: ',
|
||||
refAnnotationReportVerdictLabel: 'Verdict: ',
|
||||
refAnnotationReportReasonLabel: 'Reason: ',
|
||||
refAnnotationReportAuthorCommentLabel: 'Editor comment: ',
|
||||
refAnnotationReportNoReason: 'No details',
|
||||
refAnnotationReportConclusionLabel: 'Conclusion: ',
|
||||
refAnnotationReportConclusionKeep: 'Keep',
|
||||
refAnnotationReportConclusionRevise: 'Revise suggested',
|
||||
refAnnotationReportNoConclusion: 'No conclusion',
|
||||
refAnnotationReportScoreLabel: 'Relevance: {score}%',
|
||||
refAnnotationReportCiteLocLabel: 'Citation {n}',
|
||||
refAnnotationReportCiteParaSuffix: ' · Para {para}',
|
||||
refAnnotationReportParaSectionTitle: 'By paragraph',
|
||||
refAnnotationReportRefConclusionSectionTitle: 'References · all review conclusions',
|
||||
refAnnotationReportRefCiteTotal: '{n} in-text citation(s)',
|
||||
refAnnotationReportSummaryTitle: 'AI summary: ',
|
||||
refAnnotationReportSummaryPrefix: 'The following references are weakly related; please verify: ',
|
||||
refAnnotationReportSummaryRefPrefix: 'Ref. ',
|
||||
refAnnotationReportWarnBadge: 'Weak / irrelevant citations found',
|
||||
refAnnotationReportIssueNavTitle: 'Paragraphs needing attention',
|
||||
refAnnotationReportIssueNavHint: 'Click a paragraph number to jump to review notes',
|
||||
refAnnotationReportIssueCountLabel: '{n} revise suggested',
|
||||
refAnnotationReportJumpToReview: 'View notes →',
|
||||
refAnnotationReportJumpToReviewTip: 'Jump to review notes for this paragraph',
|
||||
refAnnotationReportLocateSource: '↩ Locate source',
|
||||
refAnnotationReportParaBadge: 'Paragraph {n}',
|
||||
refAnnotationReportParaOkLabel: 'Citations OK',
|
||||
refAnnotationReportLegendIssue: 'Red border: paragraph has citations to revise',
|
||||
refAnnotationReportLegendOk: 'Normal paragraph: citations checked and OK',
|
||||
refAnnotationReportLegendCite: 'Click citation no. or “View notes” to jump to the right panel',
|
||||
refAnnotationReportNoData: 'No AI relevance results yet. Run the check on Reference Conversion first.',
|
||||
refAnnotationReportDefaultTitle: 'manuscript',
|
||||
refAnnotationReportLevelDirect: 'Direct match',
|
||||
refAnnotationReportLevelStrong: 'Strong match',
|
||||
refAnnotationReportLevelWeak: 'Weak match',
|
||||
refAnnotationReportLevelNone: 'Not relevant',
|
||||
refHtmlCopy: 'Copy references',
|
||||
refHtmlCopyShort: 'Copy',
|
||||
refHtmlCopySuccess: 'References copied (numbering, italic journal, blue DOI, Available at line break)',
|
||||
@@ -1251,6 +1358,10 @@ const en = {
|
||||
refHtmlSaveTip: 'Tip: save the HTML after editing the full block, or copy the content and use "Paste references" on the typesetting page.',
|
||||
refHtmlDownloadEdited: 'Download edited HTML',
|
||||
refHtmlDownloadFileName: 'references-edited.html',
|
||||
refHtmlCopyBracket: 'Copy [n] format',
|
||||
refHtmlCopyBracketSuccess: 'Copied to clipboard ([n] numbering, line break after Available at:)',
|
||||
refHtmlCopyBracketEmpty: 'Nothing to copy in the editor',
|
||||
refHtmlCopyBracketFail: 'Copy failed — please select and copy manually',
|
||||
exportImg: 'Export PNG',
|
||||
PaperRotation: 'Paper Rotation',
|
||||
removeAnnotations: 'Are you sure you want to delete this Annotation?',
|
||||
@@ -1345,6 +1456,7 @@ const en = {
|
||||
youhui: 'Final price',
|
||||
discountprice: 'Final price after discount',
|
||||
youhuiremark: 'Discount description',
|
||||
applyPrice: 'Apply',
|
||||
submitOrder: 'Make a payment',
|
||||
state0: 'Pending payment',
|
||||
state1: 'Payment successfully',
|
||||
@@ -1358,6 +1470,71 @@ const en = {
|
||||
Paymentstatus: 'Payment status',
|
||||
time: 'Payment time',
|
||||
},
|
||||
journalFeeApproval: {
|
||||
title: 'Journal Fee Approval',
|
||||
pendingList: 'Pending Price Change Requests',
|
||||
pageDesc: 'Review APC change requests submitted by editors. Search by article title or SN, verify the original fee, requested fee, and remark, then approve or reject.',
|
||||
searchLabel: 'Search',
|
||||
colArticleInfo: 'Article Information',
|
||||
colPriceChange: 'Price Change Details',
|
||||
colApproval: 'Approval',
|
||||
historyTitle: 'Application History',
|
||||
no: 'No.',
|
||||
articleId: 'Article ID',
|
||||
journal: 'Journal',
|
||||
articleTitle: 'Article Title',
|
||||
sn: 'SN',
|
||||
originalFee: 'Original APC (USD)',
|
||||
applyFee: 'Requested APC (USD)',
|
||||
remark: 'Change Remark',
|
||||
status: 'Status',
|
||||
operator: 'Applicant',
|
||||
applyTime: 'Applied At',
|
||||
action: 'Approval',
|
||||
accept: 'Approve',
|
||||
reject: 'Reject',
|
||||
cancel: 'Cancel',
|
||||
tip: 'Tip',
|
||||
acceptConfirm: 'Approve this price change request?',
|
||||
rejectTitle: 'Reject Application',
|
||||
rejectRemark: 'Reject Remark',
|
||||
rejectRemarkPlaceholder: 'Please enter the reason for rejection',
|
||||
rejectRemarkRequired: 'Reject remark is required',
|
||||
noPending: 'No pending applications',
|
||||
noAccepted: 'No approved applications',
|
||||
noRejected: 'No rejected applications',
|
||||
tabPending: 'Pending',
|
||||
tabAccepted: 'Approved',
|
||||
tabRejected: 'Rejected',
|
||||
noHistory: 'No application history',
|
||||
viewHistory: 'Application History',
|
||||
refresh: 'Refresh',
|
||||
search: 'Search',
|
||||
searchPlaceholder: 'Enter article title or SN',
|
||||
noSearchResult: 'No matching pending applications',
|
||||
currentArticle: 'Current article',
|
||||
selectArticleHint: 'Click a row in the pending list, or enter an article ID to view history',
|
||||
articleIdPlaceholder: 'Article ID',
|
||||
searchHistory: 'Search',
|
||||
articleIdRequired: 'Please enter an article ID',
|
||||
missingArticleId: 'Missing article_id on this record',
|
||||
historyLoadFailed: 'Failed to load application history',
|
||||
feeChange: 'Fee Change',
|
||||
loadFailed: 'Failed to load data',
|
||||
actionFailed: 'Operation failed',
|
||||
acceptSuccess: 'Application approved',
|
||||
rejectSuccess: 'Application rejected',
|
||||
noPermission: 'You do not have permission to access this page',
|
||||
statusApply: 'Submitted',
|
||||
statusAccept: 'Approved',
|
||||
statusReject: 'Rejected',
|
||||
statusPending: 'Pending',
|
||||
statusOther: 'Other',
|
||||
viewArticleDetail: 'Click to view article details',
|
||||
notifyTitle: 'Journal Fee Approval',
|
||||
notifyMessage: 'You have {count} pending price change request(s)',
|
||||
notifyHint: 'Click to open the approval page',
|
||||
},
|
||||
PreAccept: {
|
||||
successInfo: 'Congratulations! Your manuscript has entered into <b>Pre-accept</b> status. Now please check and complete the necessary information of your manuscript for final publication.',
|
||||
step1: 'Article Processing Charge',
|
||||
@@ -1941,6 +2118,16 @@ const en = {
|
||||
briefNeedDiff: 'Clarify',
|
||||
briefDelete: 'Drop',
|
||||
briefYearMismatch: 'Year?'
|
||||
},
|
||||
reviewHistory: {
|
||||
certificateTitle: 'Certificate',
|
||||
exportCertificateImage: 'Download Image',
|
||||
generateCertificatePdf: 'Generate PDF',
|
||||
exportCertificateImageSuccess: 'Certificate image exported',
|
||||
exportCertificateImageFail: 'Failed to export certificate image',
|
||||
generateCertificatePdfSuccess: 'Certificate PDF generated',
|
||||
generateCertificatePdfFail: 'Failed to generate certificate PDF',
|
||||
noCertificate: 'No certificate'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ const zh = {
|
||||
ArticlePublicationanalysis: '文章发表分析',
|
||||
|
||||
journalArticleCount: '文章计数',
|
||||
journalFeeApproval: '期刊收费审批',
|
||||
Promotionsystem: '推广管理',
|
||||
Userdatabase: '用户数据库',
|
||||
analysis: '文章分析',
|
||||
@@ -1197,6 +1198,112 @@ const zh = {
|
||||
citeRelevanceCopyGroup: '复制本段',
|
||||
citeRelevanceDownloadHtml: '下载 HTML',
|
||||
citeRelevanceDownloadHtmlSuccess: 'HTML 已下载',
|
||||
citeRelevanceWithUploadedRefs: '将使用已上传的 {n} 条参考文献进行分组导出',
|
||||
citeRelevanceDownloadHtmlSuccessWithUploaded: 'HTML 已下载(基于已上传的 {n} 条参考文献)',
|
||||
refAnnotationReportTip: '下载带批注的 HTML 相关性分析报告(需先完成 AI 检测)',
|
||||
refAnnotationReportShort: '批注报告',
|
||||
exportRelevanceWordTip: '导出带引文相关性批注的 Word(需先完成 AI 检测;黄/红项原因写入批注)',
|
||||
exportRelevanceWordShort: '相关性Word',
|
||||
exportRelevanceWordSuccess: '带相关性批注的 Word 已下载',
|
||||
exportRelevanceWordFail: '带相关性批注的 Word 导出失败',
|
||||
mediaRefCheckWarn: '图表引用校对:正文引用了未排版的 {list}',
|
||||
mediaRefCheckMore: ' 等共 {n} 处',
|
||||
wordRefsPreviewShort: 'Word 参考文献',
|
||||
wordRefsPreviewTitle: '原 Word 稿件 · 参考文献',
|
||||
wordRefsPreviewTotal: '共 {count} 条参考文献',
|
||||
wordRefsPreviewClose: '关闭',
|
||||
wordRefsPreviewCopy: '复制纯文本',
|
||||
wordRefsPreviewCopySuccess: '参考文献内容已复制',
|
||||
wordRefsPreviewCopyFail: '复制失败',
|
||||
wordRefsPreviewNoArticle: '缺少稿件 ID,无法读取 Word 正文',
|
||||
wordRefsPreviewNoManuscript: '未找到稿件 Word 文件(gridData / getFilesForArticle)',
|
||||
wordRefsPreviewEmpty: '未在原 Word 稿件中识别到 References / 参考文献 段落',
|
||||
wordRefsPreviewFail: '读取 Word 参考文献失败',
|
||||
wordRefBatchUploadShort: '上传校对 Word',
|
||||
wordRefBatchUploadTip: '上传校对改好的 Word 参考文献,按序号拆解字段并批量保存到系统',
|
||||
wordRefBatchDialogTitle: 'Word 参考文献 · 字段对照',
|
||||
wordRefBatchApply: '应用',
|
||||
wordRefBatchStatus: '状态',
|
||||
wordRefBatchType: '类型',
|
||||
wordRefBatchAuthor: '作者',
|
||||
wordRefBatchTitle: '标题',
|
||||
wordRefBatchJournal: '期刊',
|
||||
wordRefBatchDateno: '出版信息',
|
||||
wordRefBatchDoi: 'DOI/链接',
|
||||
wordRefBatchExisting: '系统现有',
|
||||
wordRefBatchMatched: '已对应',
|
||||
wordRefBatchMissing: 'Word 缺失',
|
||||
wordRefBatchExtra: '系统无此项',
|
||||
wordRefBatchSelectAll: '全选',
|
||||
wordRefBatchSelectNone: '全不选',
|
||||
wordRefBatchReparse: '服务端精拆字段',
|
||||
wordRefBatchReparseSuccess: '字段已重新拆解',
|
||||
wordRefBatchReparseFail: '字段拆解失败',
|
||||
wordRefBatchSave: '保存选中项',
|
||||
wordRefBatchSaveSuccess: '已保存 {ok} 条,失败 {fail} 条',
|
||||
wordRefBatchSaveFail: '批量保存失败',
|
||||
wordRefBatchNothingSelected: '请至少勾选一条可保存的参考文献',
|
||||
wordRefBatchParseFail: 'Word 参考文献解析失败',
|
||||
wordRefBatchCountMismatch: '条数不一致:系统 {existing} 条,Word {uploaded} 条,请核对后再保存',
|
||||
wordRefBatchClickToEdit: '点击文字即可修改',
|
||||
wordRefBatchFieldEmpty: '点击填写',
|
||||
wordRefBatchShowExisting: '对比系统',
|
||||
wordRefBatchHideExisting: '收起',
|
||||
wordRefBatchParseFailed: '拆解不完整,请逐项填写',
|
||||
wordRefBatchWordOriginal: 'Word 原文',
|
||||
refAnnotationReportSuccess: '批注分析报告 HTML 已下载',
|
||||
refAnnotationReportFail: '批注分析报告下载失败',
|
||||
refAnnotationReportNoArticle: '缺少稿件 ID,无法获取 AI 检测数据',
|
||||
refAnnotationReportNoData: '暂无 AI 相关性检测结果,请先在参考文献转换页完成检测',
|
||||
refAnnotationReportTitleSuffix: '审稿报告',
|
||||
refAnnotationReportManuscriptPreview: 'word 原稿预览:{id}',
|
||||
refAnnotationReportRefMatchPrefix: '文献匹配依据:',
|
||||
refAnnotationReportRefLibraryPrefix: '匹配文献库:',
|
||||
refAnnotationReportRefTotal: '共 {n} 条',
|
||||
refAnnotationReportAnalysisTitle: '文献深度分析',
|
||||
refAnnotationReportStatTotal: '已审文献数',
|
||||
refAnnotationReportStatDirect: '直接相关',
|
||||
refAnnotationReportStatStrong: '强相关',
|
||||
refAnnotationReportStatWeak: '弱相关',
|
||||
refAnnotationReportStatNone: '不相关',
|
||||
refAnnotationReportLocateTip: '点击定位并在左侧高亮原文段落',
|
||||
refAnnotationReportLocatePara: '对应左侧段落 {n} (点击定位)',
|
||||
refAnnotationReportParaRefsPrefix: '本段引用:',
|
||||
refAnnotationReportVerdictLabel: '判定:',
|
||||
refAnnotationReportReasonLabel: '理由:',
|
||||
refAnnotationReportAuthorCommentLabel: '编辑批注:',
|
||||
refAnnotationReportNoReason: '暂无说明',
|
||||
refAnnotationReportConclusionLabel: '校对结论:',
|
||||
refAnnotationReportConclusionKeep: '可保留',
|
||||
refAnnotationReportConclusionRevise: '建议修改',
|
||||
refAnnotationReportNoConclusion: '暂无结论',
|
||||
refAnnotationReportScoreLabel: '相关性:{score}%',
|
||||
refAnnotationReportCiteLocLabel: '引用处 {n}',
|
||||
refAnnotationReportCiteParaSuffix: ' · 段落 {para}',
|
||||
refAnnotationReportParaSectionTitle: '按正文段落',
|
||||
refAnnotationReportRefConclusionSectionTitle: '参考文献 · 全部校对结论',
|
||||
refAnnotationReportRefCiteTotal: '共 {n} 处引用',
|
||||
refAnnotationReportSummaryTitle: 'AI批注总结:',
|
||||
refAnnotationReportSummaryPrefix: '以下文献相关性较弱,建议核实:',
|
||||
refAnnotationReportSummaryRefPrefix: '文献',
|
||||
refAnnotationReportWarnBadge: '发现弱/不相关文献',
|
||||
refAnnotationReportIssueNavTitle: '需关注段落',
|
||||
refAnnotationReportIssueNavHint: '点击段落号快速跳转到右侧批注意见',
|
||||
refAnnotationReportIssueCountLabel: '{n} 处建议修改',
|
||||
refAnnotationReportJumpToReview: '查看批注 →',
|
||||
refAnnotationReportJumpToReviewTip: '跳转到右侧该段批注意见',
|
||||
refAnnotationReportLocateSource: '↩ 定位原文',
|
||||
refAnnotationReportParaBadge: '段落 {n}',
|
||||
refAnnotationReportParaOkLabel: '引用正常',
|
||||
refAnnotationReportLegendIssue: '红色边框:该段有建议修改的引用',
|
||||
refAnnotationReportLegendOk: '正常段落:引用已检测且可保留',
|
||||
refAnnotationReportLegendCite: '点击引用编号或「查看批注」跳转右侧意见',
|
||||
refAnnotationReportNoData: '暂无 AI 相关性检测结果,请先在参考文献转换页完成检测。',
|
||||
refAnnotationReportDefaultTitle: 'manuscript',
|
||||
refAnnotationReportLevelDirect: '直接相关',
|
||||
refAnnotationReportLevelStrong: '强相关',
|
||||
refAnnotationReportLevelWeak: '弱相关',
|
||||
refAnnotationReportLevelNone: '不相关',
|
||||
refHtmlCopy: '复制参考文献',
|
||||
refHtmlCopyShort: '复制',
|
||||
refHtmlCopySuccess: '参考文献已复制(含编号、期刊斜体、蓝色 DOI、Available at 换行)',
|
||||
@@ -1237,6 +1344,10 @@ const zh = {
|
||||
refHtmlSaveTip: '提示:整段编辑后点击上方按钮保存 HTML;也可复制内容后在排版页「粘贴参考文献」导出 Word。',
|
||||
refHtmlDownloadEdited: '下载编辑后的 HTML',
|
||||
refHtmlDownloadFileName: 'references-edited.html',
|
||||
refHtmlCopyBracket: '复制 [编号] 格式',
|
||||
refHtmlCopyBracketSuccess: '已复制到剪贴板([n] 编号,Available at: 后换行)',
|
||||
refHtmlCopyBracketEmpty: '编辑框中没有可复制的内容',
|
||||
refHtmlCopyBracketFail: '复制失败,请手动选择复制',
|
||||
exportImg: '导出 图片',
|
||||
PaperRotation: '纸张方向',
|
||||
removeAnnotations: '确定要删除这条批注吗?',
|
||||
@@ -1329,6 +1440,7 @@ const zh = {
|
||||
youhui: '折扣价',
|
||||
discountprice: '折扣价',
|
||||
youhuiremark: '折扣说明',
|
||||
applyPrice: '申请',
|
||||
submitOrder: '付款',
|
||||
state0: '待付款',
|
||||
state1: '已缴费',
|
||||
@@ -1342,6 +1454,71 @@ const zh = {
|
||||
Paymentstatus: '缴费状态',
|
||||
time: '缴费时间',
|
||||
},
|
||||
journalFeeApproval: {
|
||||
title: '期刊收费审批',
|
||||
pendingList: '待审批改价申请',
|
||||
pageDesc: '以下为编辑提交的稿件处理费改价申请。您可按文章标题或 SN 号搜索,核对原金额、申请金额及改价说明后,选择同意或驳回。',
|
||||
searchLabel: '搜索',
|
||||
colArticleInfo: '文章信息',
|
||||
colPriceChange: '改价详情',
|
||||
colApproval: '审批操作',
|
||||
historyTitle: '申请历史',
|
||||
no: '序号',
|
||||
articleId: '文章 ID',
|
||||
journal: '期刊名称',
|
||||
articleTitle: '文章标题',
|
||||
sn: 'SN 号',
|
||||
originalFee: '原处理费 (USD)',
|
||||
applyFee: '申请改价 (USD)',
|
||||
remark: '改价说明',
|
||||
status: '审批状态',
|
||||
operator: '申请人',
|
||||
applyTime: '申请时间',
|
||||
action: '审批操作',
|
||||
accept: '同意',
|
||||
reject: '驳回',
|
||||
cancel: '取消',
|
||||
tip: '提示',
|
||||
acceptConfirm: '确认同意该改价申请?',
|
||||
rejectTitle: '驳回申请',
|
||||
rejectRemark: '驳回备注',
|
||||
rejectRemarkPlaceholder: '请填写驳回原因',
|
||||
rejectRemarkRequired: '请填写驳回备注',
|
||||
noPending: '暂无待审批申请',
|
||||
noAccepted: '暂无已通过申请',
|
||||
noRejected: '暂无已驳回申请',
|
||||
tabPending: '待审批',
|
||||
tabAccepted: '已通过',
|
||||
tabRejected: '已驳回',
|
||||
noHistory: '暂无申请历史',
|
||||
viewHistory: '申请历史',
|
||||
refresh: '刷新',
|
||||
search: '搜索',
|
||||
searchPlaceholder: '输入文章标题或 SN 号',
|
||||
noSearchResult: '未找到匹配的待审批申请',
|
||||
currentArticle: '当前文章',
|
||||
selectArticleHint: '点击待审批列表中的文章,或输入文章 ID 查询申请历史',
|
||||
articleIdPlaceholder: '文章 ID',
|
||||
searchHistory: '查询历史',
|
||||
articleIdRequired: '请输入文章 ID',
|
||||
missingArticleId: '该记录缺少 article_id,无法查询历史',
|
||||
historyLoadFailed: '加载申请历史失败',
|
||||
feeChange: '金额变更',
|
||||
loadFailed: '加载失败,请稍后重试',
|
||||
actionFailed: '操作失败,请稍后重试',
|
||||
acceptSuccess: '已同意该申请',
|
||||
rejectSuccess: '已驳回该申请',
|
||||
noPermission: '无权限访问该页面',
|
||||
statusApply: '提交申请',
|
||||
statusAccept: '审批通过',
|
||||
statusReject: '审批驳回',
|
||||
statusPending: '待审批',
|
||||
statusOther: '其他记录',
|
||||
viewArticleDetail: '点击查看文章详情',
|
||||
notifyTitle: '期刊收费审批',
|
||||
notifyMessage: '您有 {count} 条待审批改价申请',
|
||||
notifyHint: '点击前往审批页面处理',
|
||||
},
|
||||
PreAccept: {
|
||||
successInfo: 'Congratulations! Your manuscript has entered into <b>Pre-accept</b> status. Now please check and complete the necessary information of your manuscript for final publication.',
|
||||
step1: '文章处理费用',
|
||||
@@ -1919,6 +2096,16 @@ const zh = {
|
||||
briefNeedDiff: '需说明差异',
|
||||
briefDelete: '删除',
|
||||
briefYearMismatch: '年份不符'
|
||||
},
|
||||
reviewHistory: {
|
||||
certificateTitle: '证书',
|
||||
exportCertificateImage: '下载图片',
|
||||
generateCertificatePdf: '生成 PDF',
|
||||
exportCertificateImageSuccess: '证书图片已导出',
|
||||
exportCertificateImageFail: '证书图片导出失败',
|
||||
generateCertificatePdfSuccess: '证书 PDF 已生成',
|
||||
generateCertificatePdfFail: '证书 PDF 生成失败',
|
||||
noCertificate: '暂无证书'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -115,6 +115,64 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div
|
||||
v-if="priceApplyHistory.length > 0"
|
||||
class="price-apply-history"
|
||||
v-loading="priceApplyHistoryLoading"
|
||||
>
|
||||
<h5 class="price-apply-history__title">{{ $t('journalFeeApproval.historyTitle') }}</h5>
|
||||
<div class="price-apply-history__list">
|
||||
<div
|
||||
v-for="(item, index) in priceApplyHistory"
|
||||
:key="index"
|
||||
class="price-apply-history__item"
|
||||
>
|
||||
<div class="price-apply-history__track">
|
||||
<i
|
||||
class="price-apply-history__dot"
|
||||
:class="'is-' + applyTimelineType(item.statusText)"
|
||||
></i>
|
||||
<i
|
||||
v-if="index < priceApplyHistory.length - 1"
|
||||
class="price-apply-history__tail"
|
||||
></i>
|
||||
</div>
|
||||
<div class="price-apply-history__row">
|
||||
<el-tag size="mini" :type="applyTagType(item.statusText)" class="price-apply-history__tag">
|
||||
{{ item.statusText }}
|
||||
</el-tag>
|
||||
<span v-if="item.applyTime" class="price-apply-history__time">
|
||||
{{ formatApplyDateTime(item.applyTime) }}
|
||||
</span>
|
||||
<span class="price-apply-history__text">
|
||||
{{ $t('pendingPayment.total') }}: {{ formatApplyFee(item.originalFee) }} USD
|
||||
</span>
|
||||
<span class="price-apply-history__sep">|</span>
|
||||
<span class="price-apply-history__text">
|
||||
{{ $t('pendingPayment.discountprice') }}: {{ formatApplyFee(item.applyFee) }} USD
|
||||
</span>
|
||||
<template v-if="item.remark">
|
||||
<span class="price-apply-history__sep">|</span>
|
||||
<span class="price-apply-history__text">
|
||||
{{ $t('pendingPayment.youhuiremark') }}: {{ item.remark }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="item.rejectRemark">
|
||||
<span class="price-apply-history__sep">|</span>
|
||||
<span class="price-apply-history__text">
|
||||
{{ $t('journalFeeApproval.rejectRemark') }}: {{ item.rejectRemark }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="item.operator && item.operator !== '-'">
|
||||
<span class="price-apply-history__sep">|</span>
|
||||
<span class="price-apply-history__text">
|
||||
{{ $t('journalFeeApproval.operator') }}: {{ item.operator }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -256,7 +314,7 @@
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="finalFeeVisible = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="saveFinalFee">Save</el-button>
|
||||
<el-button type="primary" @click="saveFinalFee">{{ $t('pendingPayment.applyPrice') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -281,6 +339,8 @@ export default {
|
||||
tableData: [],
|
||||
talkMsgs: [],
|
||||
finalFeeVisible: false,
|
||||
priceApplyHistoryLoading: false,
|
||||
priceApplyHistory: [],
|
||||
FeeVisible: false,
|
||||
communVisible: false,
|
||||
msgform: {
|
||||
@@ -581,6 +641,7 @@ export default {
|
||||
created() {
|
||||
this.opMedical = this.$commonJS.opMedicalList();
|
||||
this.getPreacceptPayment();
|
||||
this.fetchArticlePriceApplyHistory();
|
||||
this.getHight();
|
||||
window.addEventListener('resize', this.getHight);
|
||||
// this.getData();
|
||||
@@ -591,6 +652,7 @@ export default {
|
||||
activated() {
|
||||
this.opMedical = this.$commonJS.opMedicalList();
|
||||
this.getPreacceptPayment();
|
||||
this.fetchArticlePriceApplyHistory();
|
||||
this.getHight();
|
||||
window.addEventListener('resize', this.getHight);
|
||||
// this.getData();
|
||||
@@ -614,11 +676,10 @@ export default {
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
this.$api
|
||||
.post('api/Order/changePrice', {
|
||||
.post('api/Order/applyPrice', {
|
||||
article_id: this.$route.query.id,
|
||||
editor_id: localStorage.getItem('U_id'),
|
||||
fee: Number(this.finalFeeData.fee).toFixed(2),
|
||||
fee_remark: this.finalFeeData.fee_remark
|
||||
remark: this.finalFeeData.fee_remark
|
||||
})
|
||||
.then((res) => {
|
||||
load.close();
|
||||
@@ -626,6 +687,7 @@ export default {
|
||||
if (res.code == 0) {
|
||||
this.finalFeeVisible = false;
|
||||
this.getPreacceptPayment();
|
||||
this.fetchArticlePriceApplyHistory();
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
@@ -646,6 +708,106 @@ export default {
|
||||
fee_remark: this.article_pay_info.fee_remark
|
||||
};
|
||||
},
|
||||
fetchArticlePriceApplyHistory() {
|
||||
const articleId = this.$route.query.id;
|
||||
if (!articleId) return;
|
||||
this.priceApplyHistoryLoading = true;
|
||||
this.$api
|
||||
.post('api/Order/getArticlePriceApplyList', {
|
||||
article_id: articleId
|
||||
})
|
||||
.then((res) => {
|
||||
this.priceApplyHistoryLoading = false;
|
||||
if (res.code == 0) {
|
||||
this.priceApplyHistory = this.normalizePriceApplyHistory(res.data);
|
||||
} else {
|
||||
this.priceApplyHistory = [];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.priceApplyHistoryLoading = false;
|
||||
this.priceApplyHistory = [];
|
||||
});
|
||||
},
|
||||
extractPriceApplyList(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (!data || typeof data !== 'object') return [];
|
||||
return data.list || data.applies || data.applys || data.items || data.logs || data.data || [];
|
||||
},
|
||||
pickApplyValue() {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
const value = arguments[i];
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
normalizePriceApplyHistory(data) {
|
||||
const rawList = this.extractPriceApplyList(data);
|
||||
return rawList
|
||||
.map((row) => {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
const status = row.status !== undefined ? row.status : row.state;
|
||||
return {
|
||||
originalFee: this.pickApplyValue(row.old_fee, row.original_fee, row.origin_fee, row.original_price, row.total, ''),
|
||||
applyFee: this.pickApplyValue(row.fee, row.apply_fee, row.new_fee, ''),
|
||||
remark: this.pickApplyValue(row.remark, row.fee_remark, row.apply_remark, ''),
|
||||
rejectRemark: this.pickApplyValue(row.reject_remark, row.reject_reason, row.audit_remark, ''),
|
||||
operator: row.operator || row.account || row.editor_name || row.apply_user || row.username || row.realname || '-',
|
||||
applyTime: row.ctime || row.apply_time || row.create_time || row.add_time || row.update_time || row.time || 0,
|
||||
statusText: this.getApplyStatusText(status, row)
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => Number(b.applyTime) - Number(a.applyTime));
|
||||
},
|
||||
getApplyStatusText(status, row) {
|
||||
const raw = status !== undefined && status !== null && status !== '' ? status : row.action || row.type || '';
|
||||
const text = String(raw).toLowerCase();
|
||||
if (text.includes('reject') || text === '2' || text === '拒绝' || text === '驳回') {
|
||||
return this.$t('journalFeeApproval.statusReject');
|
||||
}
|
||||
if (text.includes('accept') || text.includes('agree') || text === '1' || text === '同意' || text === '通过') {
|
||||
return this.$t('journalFeeApproval.statusAccept');
|
||||
}
|
||||
if (text.includes('pending') || text.includes('wait') || text === '0' || text === '待审' || text === '待审批') {
|
||||
return this.$t('journalFeeApproval.statusPending');
|
||||
}
|
||||
if (text.includes('apply') || text === '申请' || text === '提交') {
|
||||
return this.$t('journalFeeApproval.statusApply');
|
||||
}
|
||||
return raw ? String(raw) : this.$t('journalFeeApproval.statusOther');
|
||||
},
|
||||
applyTimelineType(statusText) {
|
||||
if (statusText === this.$t('journalFeeApproval.statusAccept')) return 'success';
|
||||
if (statusText === this.$t('journalFeeApproval.statusReject')) return 'danger';
|
||||
if (statusText === this.$t('journalFeeApproval.statusPending')) return 'warning';
|
||||
return 'primary';
|
||||
},
|
||||
applyTagType(statusText) {
|
||||
if (statusText === this.$t('journalFeeApproval.statusAccept')) return 'success';
|
||||
if (statusText === this.$t('journalFeeApproval.statusReject')) return 'danger';
|
||||
if (statusText === this.$t('journalFeeApproval.statusPending')) return 'warning';
|
||||
return '';
|
||||
},
|
||||
formatApplyFee(value) {
|
||||
if (value === '' || value === null || value === undefined) return '-';
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) return value;
|
||||
return num.toFixed(2);
|
||||
},
|
||||
formatApplyDateTime(timestamp) {
|
||||
if (!timestamp) return '-';
|
||||
if (typeof timestamp === 'string' && timestamp.indexOf('-') > 0) {
|
||||
return timestamp;
|
||||
}
|
||||
const ts = String(timestamp).length === 10 ? Number(timestamp) * 1000 : Number(timestamp);
|
||||
const date = new Date(ts);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n);
|
||||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
},
|
||||
|
||||
formatAmount(amount) {
|
||||
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
@@ -2112,6 +2274,99 @@ export default {
|
||||
.payment_info .price {
|
||||
font-weight: 700;
|
||||
}
|
||||
.price-apply-history {
|
||||
margin-top: 18px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.price-apply-history__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
.price-apply-history__list {
|
||||
padding: 0;
|
||||
}
|
||||
.price-apply-history__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.price-apply-history__item + .price-apply-history__item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.price-apply-history__track {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
align-self: stretch;
|
||||
}
|
||||
.price-apply-history__dot {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin: 4px auto 0;
|
||||
background: #409eff;
|
||||
}
|
||||
.price-apply-history__dot.is-success {
|
||||
background: #67c23a;
|
||||
}
|
||||
.price-apply-history__dot.is-danger {
|
||||
background: #f56c6c;
|
||||
}
|
||||
.price-apply-history__dot.is-warning {
|
||||
background: #e6a23c;
|
||||
}
|
||||
.price-apply-history__dot.is-primary {
|
||||
background: #409eff;
|
||||
}
|
||||
.price-apply-history__dot.is-info {
|
||||
background: #909399;
|
||||
}
|
||||
.price-apply-history__tail {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 16px;
|
||||
bottom: -10px;
|
||||
width: 2px;
|
||||
margin-left: -1px;
|
||||
background: #e4e7ed;
|
||||
}
|
||||
.price-apply-history__row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.price-apply-history__tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.price-apply-history__time {
|
||||
flex-shrink: 0;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.price-apply-history__text {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
.price-apply-history__sep {
|
||||
color: #dcdfe6;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.price-apply-history__empty {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.payment_info .remark {
|
||||
/* color: #888; */
|
||||
}
|
||||
|
||||
@@ -770,6 +770,12 @@ export default {
|
||||
this.$refs.editPublicRefRdit.$forceUpdate();
|
||||
console.log('editPublicRefRdit');
|
||||
},
|
||||
grabReferencesFromWord() {
|
||||
if (this.$refs.editPublicRefRdit && this.$refs.editPublicRefRdit.handleGrabReferencesFromWord) {
|
||||
return this.$refs.editPublicRefRdit.handleGrabReferencesFromWord();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
// 跳转邮件
|
||||
linkEmail() {
|
||||
this.$router.push({
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="ms-title">User Register</div>
|
||||
<el-form class="ms-content" :rules="registerRules" ref="registerForm" :model="registerForm" label-width="140px">
|
||||
<!-- 用户名 -->
|
||||
<el-form-item prop="username" class="form-item" label="English Name :">
|
||||
<el-form-item prop="username" class="form-item" label="Account :">
|
||||
|
||||
<el-input size="small" v-model="registerForm.username" auto-complete="off" placeholder="">
|
||||
<i slot="prefix" class="el-icon-user"></i>
|
||||
@@ -18,7 +18,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<!-- 姓名 -->
|
||||
<el-form-item prop="name" class="form-item" label="Real name :">
|
||||
<el-form-item prop="name" class="form-item" label="English name :">
|
||||
<el-input size="small" v-model="registerForm.name" auto-complete="off" placeholder="">
|
||||
<i slot="prefix" class="el-icon-edit-outline"></i>
|
||||
</el-input>
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<div class="tab_post">
|
||||
<el-button
|
||||
v-if="zyModeEnabled"
|
||||
size="mini"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-document"
|
||||
:loading="wordRefsLoading"
|
||||
:disabled="!gridData || wordRefsLoading"
|
||||
@click="handleShowWordReferences"
|
||||
class="tab-post-word-refs-btn"
|
||||
>
|
||||
{{ $t('commonTable.wordRefsPreviewShort') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-tickets"
|
||||
@click="showdetaileditor(detailMes)"
|
||||
style="padding: 6px; margin-left: 0px; position: absolute; bottom: 20px; left: 0px"
|
||||
class="tab-post-detail-ms-btn"
|
||||
>
|
||||
Detailed for MS</el-button
|
||||
>
|
||||
@@ -1153,6 +1166,25 @@
|
||||
<el-button type="primary" @click="preArtVisible = false">OK</el-button>
|
||||
</span> -->
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="wordRefsDialogTitle"
|
||||
:visible.sync="wordRefsDialogVisible"
|
||||
width="860px"
|
||||
top="6vh"
|
||||
custom-class="word-refs-preview-dialog"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="wordRefsCount > 0" class="word-refs-preview-meta">
|
||||
{{ $t('commonTable.wordRefsPreviewTotal', { count: wordRefsCount }) }}
|
||||
</div>
|
||||
<div v-loading="wordRefsLoading" class="word-refs-preview-body" v-html="wordRefsHtml"></div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="wordRefsDialogVisible = false">{{ $t('commonTable.wordRefsPreviewClose') }}</el-button>
|
||||
<el-button type="primary" plain @click="copyWordRefsPlainText">{{ $t('commonTable.wordRefsPreviewCopy') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1183,9 +1215,15 @@ import {
|
||||
getAuthorAffiliationPreview
|
||||
} from '@/utils/productionSubmissionImport';
|
||||
import { isZyModeEnabled, isZySkipCheckEnabled } from '@/utils/zyMode';
|
||||
import { fetchWordReferencesSection } from '@/utils/manuscriptWordReferences';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
wordRefsDialogVisible: false,
|
||||
wordRefsLoading: false,
|
||||
wordRefsHtml: '',
|
||||
wordRefsPlainText: '',
|
||||
wordRefsCount: 0,
|
||||
importingSubmission: false,
|
||||
importAuthorPreviewVisible: false,
|
||||
importAuthorOrderText: '',
|
||||
@@ -1759,6 +1797,66 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
async handleShowWordReferences() {
|
||||
if (this.wordRefsLoading) return;
|
||||
const manuscriptPath = String(this.gridData || '').trim();
|
||||
if (!manuscriptPath) {
|
||||
this.$message.warning(this.$t('commonTable.wordRefsPreviewNoManuscript'));
|
||||
return;
|
||||
}
|
||||
this.wordRefsLoading = true;
|
||||
try {
|
||||
const extracted = await fetchWordReferencesSection(this.$api, {
|
||||
manuscriptPath: manuscriptPath,
|
||||
gridData: manuscriptPath,
|
||||
mediaUrl: this.mediaUrl,
|
||||
baseUrl: this.baseUrl,
|
||||
useGridDataOnly: true
|
||||
});
|
||||
if (!extracted.found) {
|
||||
this.$message.warning(this.$t('commonTable.wordRefsPreviewEmpty'));
|
||||
return;
|
||||
}
|
||||
this.wordRefsHtml = extracted.html;
|
||||
this.wordRefsPlainText = extracted.plainText || '';
|
||||
this.wordRefsCount = extracted.count || 0;
|
||||
this.wordRefsDialogVisible = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (err && err.message === 'NO_MANUSCRIPT_FILE') {
|
||||
this.$message.warning(this.$t('commonTable.wordRefsPreviewNoManuscript'));
|
||||
} else {
|
||||
this.$message.error(this.$t('commonTable.wordRefsPreviewFail'));
|
||||
}
|
||||
} finally {
|
||||
this.wordRefsLoading = false;
|
||||
}
|
||||
},
|
||||
async copyWordRefsPlainText() {
|
||||
const text = String(this.wordRefsPlainText || '').trim();
|
||||
if (!text) {
|
||||
this.$message.warning(this.$t('commonTable.wordRefsPreviewEmpty'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
this.$message.success(this.$t('commonTable.wordRefsPreviewCopySuccess'));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.$message.error(this.$t('commonTable.wordRefsPreviewCopyFail'));
|
||||
}
|
||||
},
|
||||
allLoad() {
|
||||
let excelList = [
|
||||
'https://submission.tmrjournals.com/public/manuscirpt/20220831/c7d75d49bf25cf56906d56d07e00a31e.docx',
|
||||
@@ -1914,7 +2012,12 @@ export default {
|
||||
type: 'm'
|
||||
})
|
||||
.then((res) => {
|
||||
this.gridData = res.data.files[res.data.files.length - 1].file_url;
|
||||
const files = res.data && Array.isArray(res.data.files) ? res.data.files : [];
|
||||
if (files.length > 0) {
|
||||
this.gridData = files[files.length - 1].file_url || '';
|
||||
} else {
|
||||
this.gridData = '';
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
@@ -3599,6 +3702,13 @@ export default {
|
||||
computed: {
|
||||
zyModeEnabled() {
|
||||
return isZyModeEnabled(this.$route);
|
||||
},
|
||||
wordRefsDialogTitle() {
|
||||
const base = this.$t('commonTable.wordRefsPreviewTitle');
|
||||
if (this.wordRefsCount > 0) {
|
||||
return base + ' (' + this.wordRefsCount + ')';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -3699,6 +3809,73 @@ export default {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.tab-post-word-refs-btn,
|
||||
.tab-post-detail-ms-btn {
|
||||
padding: 6px;
|
||||
margin-left: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.tab-post-word-refs-btn {
|
||||
bottom: 58px;
|
||||
}
|
||||
|
||||
.tab-post-detail-ms-btn {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
::v-deep .word-refs-preview-dialog .el-dialog__body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.word-refs-preview-meta {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #f4f8ff;
|
||||
border: 1px solid #d9e8ff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.word-refs-preview-body {
|
||||
max-height: 68vh;
|
||||
overflow: auto;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
line-height: 1.65;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.word-refs-preview-body .word-ref-block {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.word-refs-preview-body .word-ref-h1 {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 8px 0 12px;
|
||||
}
|
||||
|
||||
.word-refs-preview-body .word-ref-h2,
|
||||
.word-refs-preview-body .word-ref-h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.word-refs-preview-body .word-ref-p p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.word-refs-preview-body blue {
|
||||
color: #006699;
|
||||
}
|
||||
|
||||
.tab_post > div {
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -114,6 +114,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="zyModeEnabled" v-loading="zyStatsLoading" class="zy-monthly-stats-bar">
|
||||
<div class="zy-monthly-stats-head">
|
||||
<span class="zy-monthly-stats-title">{{ $t('autoPromotionLogs.monthlyStatsTitle') }}</span>
|
||||
<el-date-picker
|
||||
v-model="zyStatsMonth"
|
||||
type="month"
|
||||
value-format="yyyy-MM"
|
||||
size="small"
|
||||
:placeholder="$t('autoPromotionLogs.monthlyStatsMonthPlaceholder')"
|
||||
@change="fetchZyMonthlyStats"
|
||||
/>
|
||||
</div>
|
||||
<div class="num-grid zy-monthly-stats-grid">
|
||||
<div class="num-item">
|
||||
<div class="label">{{ $t('autoPromotionLogs.monthlyStatsTaskCount') }}</div>
|
||||
<div class="value neutral">{{ zyMonthlyStats.taskCount }}</div>
|
||||
</div>
|
||||
<div class="num-item">
|
||||
<div class="label">{{ $t('autoPromotionLogs.totalCount') }}</div>
|
||||
<div class="value primary">{{ zyMonthlyStats.total }}</div>
|
||||
</div>
|
||||
<div class="num-item">
|
||||
<div class="label">{{ $t('autoPromotionLogs.sentCount') }}</div>
|
||||
<div class="value success">{{ zyMonthlyStats.sent }}</div>
|
||||
</div>
|
||||
<div class="num-item">
|
||||
<div class="label">{{ $t('autoPromotionLogs.failCount') }}</div>
|
||||
<div class="value danger">{{ zyMonthlyStats.fail }}</div>
|
||||
</div>
|
||||
<div class="num-item">
|
||||
<div class="label">{{ $t('autoPromotionLogs.bounceCount') }}</div>
|
||||
<div :class="['value', zyMonthlyStats.bounce > 0 ? 'warning' : 'neutral']">{{ zyMonthlyStats.bounce }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -313,6 +349,7 @@ import CkeditorMail from '@/components/page/components/email/CkeditorMail.vue';
|
||||
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue';
|
||||
import PromotionFactoryTaskDialog from '@/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue';
|
||||
import PromotionDetailDrawer from '@/components/page/components/autoPromotion/PromotionDetailDrawer.vue';
|
||||
import { isZyModeEnabled } from '@/utils/zyMode';
|
||||
// 这里假设你已经定义了 API 地址
|
||||
const API = {
|
||||
getAllJournal: 'api/email_client/getPromotionJournalList',
|
||||
@@ -389,10 +426,25 @@ export default {
|
||||
run_at: '',
|
||||
subject: '',
|
||||
content: ''
|
||||
},
|
||||
zyStatsMonth: (function () {
|
||||
const now = new Date();
|
||||
return now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
|
||||
})(),
|
||||
zyStatsLoading: false,
|
||||
zyMonthlyStats: {
|
||||
taskCount: 0,
|
||||
total: 0,
|
||||
sent: 0,
|
||||
fail: 0,
|
||||
bounce: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
zyModeEnabled() {
|
||||
return isZyModeEnabled(this.$route);
|
||||
},
|
||||
/** 当前选中工厂任务是否运行中(与下拉 options 中 running 一致) */
|
||||
headerFactoryTaskRunning() {
|
||||
const id = String(this.headerPromotionFactoryId || '').trim();
|
||||
@@ -617,6 +669,7 @@ export default {
|
||||
if (this.selectedJournalId) {
|
||||
await this.fetchFactoryTasksForHeader();
|
||||
await this.fetchList();
|
||||
await this.fetchZyMonthlyStats();
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
@@ -804,6 +857,7 @@ export default {
|
||||
this.query.pageIndex = 1;
|
||||
this.replacePromotionFactoryIdInUrl(next);
|
||||
this.fetchList();
|
||||
this.fetchZyMonthlyStats();
|
||||
},
|
||||
async handleCreateEmailClientTask() {
|
||||
const pid = String(this.headerPromotionFactoryId || this.routePromotionFactoryId || '').trim();
|
||||
@@ -1100,35 +1154,7 @@ export default {
|
||||
const data = (res && res.data) || {};
|
||||
const rawList = Array.isArray(data.list) ? data.list : [];
|
||||
|
||||
this.list = rawList.map((item, idx) => {
|
||||
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
|
||||
const state = String(item.state != null ? item.state : '');
|
||||
const promotionFactoryId =
|
||||
item.promotion_factory_id != null ? item.promotion_factory_id : item.id != null ? item.id : item.task_id;
|
||||
return {
|
||||
id: item.id || item.task_id || `task_${idx + 1}`,
|
||||
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
||||
task_id: String(item.task_id != null ? item.task_id : item.id || ''),
|
||||
task_name: item.task_name || item.name || '',
|
||||
scene: item.scene || '',
|
||||
name: item.name || item.expert_name || '',
|
||||
email: item.email || item.to_email || '',
|
||||
template_id: String(item.template_id || item.default_template_id || ''),
|
||||
style_id: String(item.style_id || item.default_style_id || ''),
|
||||
style_name: item.style_name || item.default_style_name || '',
|
||||
state: state,
|
||||
paused: state === '2',
|
||||
run_at: runAt,
|
||||
total_count: Number(item.total_count || 0),
|
||||
sent_count: Number(item.sent_count || 0),
|
||||
fail_count: Number(item.fail_count || 0),
|
||||
bounce_count: Number(item.bounce_count || 0),
|
||||
min_interval: Number(item.min_interval || 0),
|
||||
max_interval: Number(item.max_interval || 0),
|
||||
send_start_hour: item.send_start_hour != null ? item.send_start_hour : '-',
|
||||
send_end_hour: item.send_end_hour != null ? item.send_end_hour : '-'
|
||||
};
|
||||
});
|
||||
this.list = rawList.map((item, idx) => this.mapTaskListItem(item, idx));
|
||||
|
||||
this.total = Number(data.total || data.count || this.list.length || 0);
|
||||
// 兼容后端返回 page/per_page
|
||||
@@ -1240,6 +1266,113 @@ export default {
|
||||
handleSearch() {
|
||||
this.query.pageIndex = 1;
|
||||
this.fetchList();
|
||||
this.fetchZyMonthlyStats();
|
||||
},
|
||||
extractTaskRunYearMonth(runAt) {
|
||||
const raw = String(runAt || '').trim();
|
||||
if (!raw) return '';
|
||||
const match = raw.match(/^(\d{4})-(\d{2})/);
|
||||
if (match) {
|
||||
return match[1] + '-' + match[2];
|
||||
}
|
||||
const date = new Date(raw);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0');
|
||||
},
|
||||
mapTaskListItem(item, idx) {
|
||||
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
|
||||
const state = String(item.state != null ? item.state : '');
|
||||
const promotionFactoryId =
|
||||
item.promotion_factory_id != null ? item.promotion_factory_id : item.id != null ? item.id : item.task_id;
|
||||
return {
|
||||
id: item.id || item.task_id || `task_${idx + 1}`,
|
||||
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
||||
task_id: String(item.task_id != null ? item.task_id : item.id || ''),
|
||||
task_name: item.task_name || item.name || '',
|
||||
scene: item.scene || '',
|
||||
name: item.name || item.expert_name || '',
|
||||
email: item.email || item.to_email || '',
|
||||
template_id: String(item.template_id || item.default_template_id || ''),
|
||||
style_id: String(item.style_id || item.default_style_id || ''),
|
||||
style_name: item.style_name || item.default_style_name || '',
|
||||
state: state,
|
||||
paused: state === '2',
|
||||
run_at: runAt,
|
||||
total_count: Number(item.total_count || 0),
|
||||
sent_count: Number(item.sent_count || 0),
|
||||
fail_count: Number(item.fail_count || 0),
|
||||
bounce_count: Number(item.bounce_count || 0),
|
||||
min_interval: Number(item.min_interval || 0),
|
||||
max_interval: Number(item.max_interval || 0),
|
||||
send_start_hour: item.send_start_hour != null ? item.send_start_hour : '-',
|
||||
send_end_hour: item.send_end_hour != null ? item.send_end_hour : '-'
|
||||
};
|
||||
},
|
||||
async fetchAllTaskRowsForStats() {
|
||||
const rows = [];
|
||||
let page = 1;
|
||||
const perPage = 200;
|
||||
let total = 0;
|
||||
do {
|
||||
const params = {
|
||||
journal_id: String(this.selectedJournalId || ''),
|
||||
factory_id: String(this.routePromotionFactoryId || ''),
|
||||
page: page,
|
||||
per_page: perPage
|
||||
};
|
||||
const res = await this.$api.post(API.list, params);
|
||||
const data = (res && res.data) || {};
|
||||
const rawList = Array.isArray(data.list) ? data.list : [];
|
||||
rows.push.apply(rows, rawList);
|
||||
total = Number(data.total || data.count || rows.length || 0);
|
||||
if (!rawList.length || rows.length >= total) {
|
||||
break;
|
||||
}
|
||||
page += 1;
|
||||
} while (page <= 100);
|
||||
return rows;
|
||||
},
|
||||
async fetchZyMonthlyStats() {
|
||||
if (!this.zyModeEnabled || !this.selectedJournalId) {
|
||||
return;
|
||||
}
|
||||
const month = String(this.zyStatsMonth || '').trim();
|
||||
if (!month) {
|
||||
return;
|
||||
}
|
||||
this.zyStatsLoading = true;
|
||||
try {
|
||||
const rawRows = await this.fetchAllTaskRowsForStats();
|
||||
const stats = {
|
||||
taskCount: 0,
|
||||
total: 0,
|
||||
sent: 0,
|
||||
fail: 0,
|
||||
bounce: 0
|
||||
};
|
||||
rawRows.forEach(function (item, idx) {
|
||||
const mapped = this.mapTaskListItem(item, idx);
|
||||
if (this.extractTaskRunYearMonth(mapped.run_at) !== month) {
|
||||
return;
|
||||
}
|
||||
stats.taskCount += 1;
|
||||
stats.total += mapped.total_count;
|
||||
stats.sent += mapped.sent_count;
|
||||
stats.fail += mapped.fail_count;
|
||||
stats.bounce += mapped.bounce_count;
|
||||
}, this);
|
||||
this.zyMonthlyStats = stats;
|
||||
} catch (e) {
|
||||
this.zyMonthlyStats = {
|
||||
taskCount: 0,
|
||||
total: 0,
|
||||
sent: 0,
|
||||
fail: 0,
|
||||
bounce: 0
|
||||
};
|
||||
} finally {
|
||||
this.zyStatsLoading = false;
|
||||
}
|
||||
},
|
||||
handleStateChange() {
|
||||
this.query.pageIndex = 1;
|
||||
@@ -1723,6 +1856,27 @@ export default {
|
||||
.delete-btn:hover {
|
||||
color: #be123c !important;
|
||||
}
|
||||
.zy-monthly-stats-bar {
|
||||
margin: 12px 0 14px;
|
||||
padding: 12px 14px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.zy-monthly-stats-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.zy-monthly-stats-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
.zy-monthly-stats-grid {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.delivery-dashboard {
|
||||
padding: 0px 0;
|
||||
}
|
||||
|
||||
@@ -140,8 +140,12 @@
|
||||
v-if="zyModeEnabled"
|
||||
@click="handleOpenCitationRelevance"
|
||||
class="zy-toolbar-btn"
|
||||
:class="{ 'zy-toolbar-btn--ready': uploadedReferencesCount }"
|
||||
>
|
||||
<el-tooltip :content="$t('commonTable.citeRelevanceDetect')" placement="bottom">
|
||||
<el-tooltip
|
||||
:content="uploadedReferencesCount ? $t('commonTable.citeRelevanceWithUploadedRefs', { n: uploadedReferencesCount }) : $t('commonTable.citeRelevanceDetect')"
|
||||
placement="bottom"
|
||||
>
|
||||
<span class="zy-toolbar-btn-inner">
|
||||
<i
|
||||
class="el-icon-connection"
|
||||
@@ -152,6 +156,25 @@
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li
|
||||
v-if="zyModeEnabled"
|
||||
@click="handleExportRelevanceWord"
|
||||
class="zy-toolbar-btn"
|
||||
>
|
||||
<el-tooltip
|
||||
:content="$t('commonTable.exportRelevanceWordTip')"
|
||||
placement="bottom"
|
||||
>
|
||||
<span class="zy-toolbar-btn-inner">
|
||||
<i
|
||||
class="el-icon-chat-line-square"
|
||||
v-if="!exportingRelevanceWord"
|
||||
></i>
|
||||
<i class="el-icon-loading" v-else></i>
|
||||
<span class="zy-toolbar-label">{{ $t('commonTable.exportRelevanceWordShort') }}</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<li
|
||||
@@ -1179,7 +1202,11 @@ import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { debounce, throttle } from '@/common/js/debounce';
|
||||
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
|
||||
import LatexDataPanel from './LatexDataPanel.vue';
|
||||
import { downloadManuscriptWord, fetchManuscriptReferenceList, summarizeManuscriptExportItems } from '@/utils/exportManuscriptWord';
|
||||
import { downloadManuscriptWord, downloadManuscriptWordWithRelevanceComments, fetchManuscriptReferenceList, summarizeManuscriptExportItems } from '@/utils/exportManuscriptWord';
|
||||
import {
|
||||
validateMediaReferencesInWordList,
|
||||
wrapReferenceHighlightsInBlueHtml
|
||||
} from '@/utils/manuscriptMediaReferences';
|
||||
import {
|
||||
buildReferencesCopyText,
|
||||
buildReferencesHtmlLabels,
|
||||
@@ -1187,7 +1214,8 @@ import {
|
||||
countParsedReferences,
|
||||
downloadReferencesEditableHtml,
|
||||
parseReferencesBulkContent,
|
||||
parseReferencesEditableHtml
|
||||
parseReferencesEditableHtml,
|
||||
referencesFromUploadedHtmlItems
|
||||
} from '@/utils/manuscriptReferenceHtml';
|
||||
import {
|
||||
buildCitationReviewQueue,
|
||||
@@ -1320,6 +1348,7 @@ export default {
|
||||
scrollPosition: 0,
|
||||
wordList: [],
|
||||
exportingManuscriptWord: false,
|
||||
exportingRelevanceWord: false,
|
||||
referencesCopyLoading: false,
|
||||
referencesHtmlLoading: false,
|
||||
referencesUploadLoading: false,
|
||||
@@ -1612,6 +1641,12 @@ export default {
|
||||
this.manuscriptReferences = references || [];
|
||||
return this.manuscriptReferences;
|
||||
},
|
||||
async resolveReferencesForCitationReview() {
|
||||
if (this.uploadedReferenceHtmlItems && this.uploadedReferenceHtmlItems.length) {
|
||||
return referencesFromUploadedHtmlItems(this.uploadedReferenceHtmlItems);
|
||||
}
|
||||
return this.loadManuscriptReferences(false);
|
||||
},
|
||||
async handleCopyReferences() {
|
||||
if (this.referencesCopyLoading) {
|
||||
return;
|
||||
@@ -1810,6 +1845,50 @@ export default {
|
||||
this.exportingManuscriptWord = false;
|
||||
}
|
||||
},
|
||||
async handleExportRelevanceWord() {
|
||||
if (this.exportingRelevanceWord) {
|
||||
return;
|
||||
}
|
||||
if (!this.wordList || !this.wordList.length) {
|
||||
this.$message.warning(this.$t('commonTable.citeRelevanceNoContent'));
|
||||
return;
|
||||
}
|
||||
if (!this.$api) {
|
||||
this.$message.error(this.$t('commonTable.citeRelevanceLoadFail'));
|
||||
return;
|
||||
}
|
||||
if (!this.pArticleId) {
|
||||
this.$message.warning(this.$t('commonTable.refAnnotationReportNoArticle'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportingRelevanceWord = true;
|
||||
try {
|
||||
const hasUploaded =
|
||||
this.uploadedReferenceHtmlItems && this.uploadedReferenceHtmlItems.length;
|
||||
await downloadManuscriptWordWithRelevanceComments(this.wordList, this.mediaUrl, 'manuscript-relevance', {
|
||||
fetchReferences: !hasUploaded,
|
||||
referenceHtmlItems: hasUploaded ? this.uploadedReferenceHtmlItems : null,
|
||||
apiClient: this.$api,
|
||||
articleId: this.articleId,
|
||||
pArticleId: this.pArticleId,
|
||||
referenceRelevanceLabels: buildReferenceAnnotationReportLabels(this.$t.bind(this)),
|
||||
translate: this.$t.bind(this)
|
||||
});
|
||||
this.$message.success(this.$t('commonTable.exportRelevanceWordSuccess'));
|
||||
} catch (err) {
|
||||
console.error('[Word export] 相关性批注导出失败', err);
|
||||
if (err && err.message === 'NO_CONTENT') {
|
||||
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty') || 'No content to export.');
|
||||
} else if (err && err.message === 'NO_RELEVANCE_DATA') {
|
||||
this.$message.warning(this.$t('commonTable.refAnnotationReportNoData'));
|
||||
} else {
|
||||
this.$message.error(this.$t('commonTable.exportRelevanceWordFail'));
|
||||
}
|
||||
} finally {
|
||||
this.exportingRelevanceWord = false;
|
||||
}
|
||||
},
|
||||
async handleOpenCitationRelevance() {
|
||||
if (this.citationRelevanceLoading) {
|
||||
return;
|
||||
@@ -1825,9 +1904,10 @@ export default {
|
||||
|
||||
this.citationRelevanceLoading = true;
|
||||
try {
|
||||
const references = await this.loadManuscriptReferences(false);
|
||||
const references = await this.resolveReferencesForCitationReview();
|
||||
const usingUploaded = !!(this.uploadedReferenceHtmlItems && this.uploadedReferenceHtmlItems.length);
|
||||
|
||||
const items = buildCitationReviewQueue(this.wordList, this.manuscriptReferences);
|
||||
const items = buildCitationReviewQueue(this.wordList, references);
|
||||
if (!items.length) {
|
||||
this.$message.warning(this.$t('commonTable.citeRelevanceNoCitations'));
|
||||
return;
|
||||
@@ -1836,7 +1916,15 @@ export default {
|
||||
const labels = buildCitationReviewHtmlLabels(this.$t.bind(this));
|
||||
const fileName = 'citation-relevance' + (this.articleId ? '-' + this.articleId : '') + '.html';
|
||||
downloadCitationReviewHtml(items, labels, fileName);
|
||||
this.$message.success(this.$t('commonTable.citeRelevanceDownloadHtmlSuccess'));
|
||||
if (usingUploaded) {
|
||||
this.$message.success(
|
||||
this.$t('commonTable.citeRelevanceDownloadHtmlSuccessWithUploaded', {
|
||||
n: this.uploadedReferencesCount
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.$message.success(this.$t('commonTable.citeRelevanceDownloadHtmlSuccess'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.$message.error(this.$t('commonTable.citeRelevanceLoadFail'));
|
||||
@@ -4233,6 +4321,7 @@ export default {
|
||||
getContent1(type, content) {
|
||||
|
||||
content = this.$commonJS.transformHtmlString(content);
|
||||
content = wrapReferenceHighlightsInBlueHtml(content);
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = content; // 将 HTML 字符串加载到 div 中
|
||||
@@ -4253,6 +4342,22 @@ export default {
|
||||
|
||||
// 获取最终修改后的 HTML
|
||||
content = div.innerHTML;
|
||||
|
||||
const mediaRefIssues = validateMediaReferencesInWordList(this.wordList);
|
||||
if (mediaRefIssues.length) {
|
||||
const preview = mediaRefIssues
|
||||
.slice(0, 3)
|
||||
.map(function (issue) {
|
||||
const label = issue.kind === 'figure' ? 'Figure' : 'Table';
|
||||
return label + ' ' + issue.number;
|
||||
})
|
||||
.join('、');
|
||||
const suffix =
|
||||
mediaRefIssues.length > 3
|
||||
? this.$t('commonTable.mediaRefCheckMore', { n: mediaRefIssues.length })
|
||||
: '';
|
||||
this.$message.warning(this.$t('commonTable.mediaRefCheckWarn', { list: preview + suffix }));
|
||||
}
|
||||
|
||||
this.$api
|
||||
.post('api/Proofread/modify', {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
897
src/components/page/journalFeeApproval.vue
Normal file
897
src/components/page/journalFeeApproval.vue
Normal file
@@ -0,0 +1,897 @@
|
||||
<template>
|
||||
<div class="journal-fee-approval">
|
||||
<div class="crumbs">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item>
|
||||
<i class="el-icon-money"></i> {{ $t('journalFeeApproval.title') }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="handle-box">
|
||||
<el-tabs
|
||||
:value="String(activeState)"
|
||||
type="card"
|
||||
class="status-tabs"
|
||||
@tab-click="handleTabClick"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.state"
|
||||
:name="String(tab.state)"
|
||||
>
|
||||
<span slot="label">{{ tab.label }} ({{ tab.count }})</span>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-button type="primary" icon="el-icon-refresh" :loading="loading" class="refresh-btn" @click="refreshAll">
|
||||
{{ $t('journalFeeApproval.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="displayList"
|
||||
border
|
||||
stripe
|
||||
class="table"
|
||||
header-cell-class-name="table-header"
|
||||
:empty-text="emptyTipText"
|
||||
>
|
||||
<el-table-column type="index" :label="$t('journalFeeApproval.no')" width="55" align="center" />
|
||||
<el-table-column prop="acceptSn" :label="$t('journalFeeApproval.sn')" width="150" show-overflow-tooltip />
|
||||
<el-table-column :label="$t('journalFeeApproval.colArticleInfo')" min-width="380">
|
||||
<template slot-scope="scope">
|
||||
<p class="tab_tie_col">
|
||||
<span>{{ $t('journalFeeApproval.journal') }}:</span>
|
||||
<b>{{ scope.row.journalName }}</b>
|
||||
</p>
|
||||
<p class="tab_tie_col article-title">
|
||||
<span>{{ $t('journalFeeApproval.articleTitle') }}:</span>
|
||||
<span
|
||||
v-if="scope.row.articleId"
|
||||
class="article-title-link"
|
||||
:title="$t('journalFeeApproval.viewArticleDetail')"
|
||||
@click="goArticleDetail(scope.row)"
|
||||
>{{ scope.row.articleTitle }}</span>
|
||||
<span v-else>{{ scope.row.articleTitle }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('journalFeeApproval.colPriceChange')" min-width="360">
|
||||
<template slot-scope="scope">
|
||||
<p class="tab_tie_col">
|
||||
<span>{{ $t('journalFeeApproval.originalFee') }}:</span>
|
||||
<b>{{ formatFee(scope.row.originalFee) }} USD</b>
|
||||
</p>
|
||||
<p class="tab_tie_col">
|
||||
<span>{{ $t('journalFeeApproval.applyFee') }}:</span>
|
||||
<b class="apply-fee">{{ formatFee(scope.row.applyFee) }} USD</b>
|
||||
</p>
|
||||
<p class="tab_tie_col">
|
||||
<span>{{ $t('journalFeeApproval.remark') }}:</span>{{ scope.row.remark || '-' }}
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('journalFeeApproval.applyTime')" width="165" align="center">
|
||||
<template slot-scope="scope">{{ formatDateTime(scope.row.applyTime) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="activeState !== 0" :label="$t('journalFeeApproval.status')" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="statusTagType(scope.row.status)">{{ getStatusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('journalFeeApproval.colApproval')" width="180" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<div class="approval-box">
|
||||
<template v-if="activeState === 0">
|
||||
<el-button type="success" size="mini" plain icon="el-icon-check" @click="handleAccept(scope.row)">
|
||||
{{ $t('journalFeeApproval.accept') }}
|
||||
</el-button>
|
||||
<el-button type="danger" size="mini" plain icon="el-icon-close" @click="openRejectDialog(scope.row)">
|
||||
{{ $t('journalFeeApproval.reject') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button type="primary" size="mini" plain icon="el-icon-time" @click="openHistoryDialog(scope.row)">
|
||||
{{ $t('journalFeeApproval.viewHistory') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="total > 0" class="pagination-box">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:current-page.sync="pageIndex"
|
||||
:page-size.sync="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="fetchList"
|
||||
@size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('journalFeeApproval.rejectTitle')"
|
||||
:visible.sync="rejectVisible"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetRejectForm"
|
||||
>
|
||||
<el-form ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="140px">
|
||||
<el-form-item :label="$t('journalFeeApproval.sn')">
|
||||
<span>{{ rejectForm.acceptSn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('journalFeeApproval.articleTitle')">
|
||||
<span>{{ rejectForm.articleTitle }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('journalFeeApproval.rejectRemark')" prop="remark">
|
||||
<el-input v-model="rejectForm.remark" type="textarea" :rows="4" :placeholder="$t('journalFeeApproval.rejectRemarkPlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="rejectVisible = false">{{ $t('journalFeeApproval.cancel') }}</el-button>
|
||||
<el-button type="danger" :loading="rejectSubmitting" @click="submitReject">{{ $t('journalFeeApproval.reject') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('journalFeeApproval.historyTitle')"
|
||||
:visible.sync="historyVisible"
|
||||
width="640px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetHistoryDialog"
|
||||
>
|
||||
<div v-loading="historyLoading" class="history-dialog">
|
||||
<p class="history-dialog__meta">
|
||||
<span>{{ $t('journalFeeApproval.sn') }}: <b>{{ historyArticle.acceptSn }}</b></span>
|
||||
</p>
|
||||
<div class="history-dialog__article">
|
||||
<span class="history-dialog__article-label">{{ $t('journalFeeApproval.articleTitle') }}:</span>
|
||||
<p class="history-dialog__title">{{ historyArticle.articleTitle || '-' }}</p>
|
||||
</div>
|
||||
<el-timeline v-if="historyList.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="(item, index) in historyList"
|
||||
:key="index"
|
||||
placement="top"
|
||||
:type="statusTagType(item.status)"
|
||||
>
|
||||
<div class="history-dialog__item">
|
||||
<p class="history-dialog__meta-line">
|
||||
<span class="history-dialog__time">{{ formatDateTime(item.applyTime) }}</span>
|
||||
<el-tag size="mini" :type="statusTagType(item.status)">{{ getStatusLabel(item.status) }}</el-tag>
|
||||
</p>
|
||||
<p class="tab_tie_col">
|
||||
<span>{{ $t('journalFeeApproval.feeChange') }}:</span>
|
||||
{{ formatFee(item.originalFee) }} → {{ formatFee(item.applyFee) }} USD
|
||||
</p>
|
||||
<p class="tab_tie_col" v-if="item.remark">
|
||||
<span>{{ $t('journalFeeApproval.remark') }}:</span>{{ item.remark }}
|
||||
</p>
|
||||
<p class="tab_tie_col" v-if="item.rejectRemark">
|
||||
<span>{{ $t('journalFeeApproval.rejectRemark') }}:</span>{{ item.rejectRemark }}
|
||||
</p>
|
||||
<p class="tab_tie_col" v-if="item.operator && item.operator !== '-'">
|
||||
<span>{{ $t('journalFeeApproval.operator') }}:</span>{{ item.operator }}
|
||||
</p>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<p v-else-if="!historyLoading" class="history-dialog__empty">{{ $t('journalFeeApproval.noHistory') }}</p>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="historyVisible = false">{{ $t('journalFeeApproval.cancel') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus';
|
||||
|
||||
const PENDING_STATUS = new Set([0, '0', 'pending', 'wait', 'waiting']);
|
||||
|
||||
function pickValue() {
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
const value = arguments[i];
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeHistoryItem(row) {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
const status = row.status !== undefined ? row.status : row.state;
|
||||
const userInfo = row.user_info || row.editor_info || row.apply_user_info || {};
|
||||
return {
|
||||
originalFee: pickValue(row.old_fee, row.original_fee, row.origin_fee, ''),
|
||||
applyFee: pickValue(row.fee, row.apply_fee, ''),
|
||||
remark: pickValue(row.remark, row.fee_remark, ''),
|
||||
rejectRemark: pickValue(row.reject_remark, row.reject_reason, row.audit_remark, ''),
|
||||
operator: pickValue(
|
||||
row.operator,
|
||||
row.editor_name,
|
||||
row.apply_user,
|
||||
row.username,
|
||||
row.realname,
|
||||
row.account,
|
||||
userInfo.realname,
|
||||
userInfo.account,
|
||||
userInfo.username,
|
||||
userInfo.name,
|
||||
'-'
|
||||
),
|
||||
applyTime: pickValue(row.ctime, row.apply_time, row.create_time, row.add_time, row.update_time, row.time, 0),
|
||||
status: status
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApplyRow(row) {
|
||||
if (!row || typeof row !== 'object') return null;
|
||||
const articleInfo = row.article_info || {};
|
||||
const journalInfo = row.journal_info || {};
|
||||
const userInfo = row.user_info || row.editor_info || row.apply_user_info || {};
|
||||
const status = row.status !== undefined ? row.status : row.state;
|
||||
const rawHistory = Array.isArray(articleInfo.history) ? articleInfo.history : Array.isArray(row.history) ? row.history : [];
|
||||
|
||||
return {
|
||||
id: pickValue(row.id, row.apply_id, ''),
|
||||
articleId: pickValue(row.article_id, articleInfo.article_id, ''),
|
||||
journalId: pickValue(journalInfo.journal_id, articleInfo.journal_id, row.journal_id, ''),
|
||||
journalName: pickValue(journalInfo.title, journalInfo.journal_name, journalInfo.name, row.journal_name, row.journal_title, '-'),
|
||||
articleTitle: pickValue(articleInfo.article_title, articleInfo.title, articleInfo.name, row.article_title, row.title, '-'),
|
||||
acceptSn: pickValue(articleInfo.accept_sn, articleInfo.sn, row.accept_sn, row.sn, '-'),
|
||||
originalFee: pickValue(row.old_fee, row.original_fee, row.origin_fee, articleInfo.old_fee, ''),
|
||||
applyFee: pickValue(row.fee, row.apply_fee, articleInfo.fee, ''),
|
||||
remark: pickValue(row.remark, row.fee_remark, articleInfo.fee_remark, articleInfo.remark, ''),
|
||||
operator: pickValue(
|
||||
row.operator,
|
||||
row.editor_name,
|
||||
row.apply_user,
|
||||
row.username,
|
||||
row.realname,
|
||||
row.account,
|
||||
userInfo.realname,
|
||||
userInfo.account,
|
||||
userInfo.username,
|
||||
userInfo.name,
|
||||
'-'
|
||||
),
|
||||
applyTime: pickValue(row.ctime, row.apply_time, row.create_time, row.add_time, row.update_time, row.time, 0),
|
||||
status: status,
|
||||
history: rawHistory.map(normalizeHistoryItem).filter(Boolean).sort((a, b) => Number(b.applyTime) - Number(a.applyTime))
|
||||
};
|
||||
}
|
||||
|
||||
function isPendingStatus(status) {
|
||||
if (status === undefined || status === null || status === '') return true;
|
||||
return PENDING_STATUS.has(status);
|
||||
}
|
||||
|
||||
function extractApplyCounts(data) {
|
||||
const counts = { 0: 0, 1: 0, 2: 0 };
|
||||
if (!data) return counts;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(function (item) {
|
||||
if (!item || typeof item !== 'object') return;
|
||||
const state = item.state !== undefined ? item.state : item.status;
|
||||
const count = Number(item.count != null ? item.count : item.num != null ? item.num : item.total);
|
||||
if ([0, 1, 2, '0', '1', '2'].indexOf(state) >= 0 && !isNaN(count)) {
|
||||
counts[Number(state)] = count;
|
||||
}
|
||||
});
|
||||
return counts;
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
counts[0] =
|
||||
Number(
|
||||
data.pending != null
|
||||
? data.pending
|
||||
: data.wait != null
|
||||
? data.wait
|
||||
: data.pending_count != null
|
||||
? data.pending_count
|
||||
: data.state0 != null
|
||||
? data.state0
|
||||
: data['0'] != null
|
||||
? data['0']
|
||||
: 0
|
||||
) || 0;
|
||||
counts[1] =
|
||||
Number(
|
||||
data.accept != null
|
||||
? data.accept
|
||||
: data.pass != null
|
||||
? data.pass
|
||||
: data.accept_count != null
|
||||
? data.accept_count
|
||||
: data.state1 != null
|
||||
? data.state1
|
||||
: data['1'] != null
|
||||
? data['1']
|
||||
: 0
|
||||
) || 0;
|
||||
counts[2] =
|
||||
Number(
|
||||
data.reject != null
|
||||
? data.reject
|
||||
: data.refuse != null
|
||||
? data.refuse
|
||||
: data.reject_count != null
|
||||
? data.reject_count
|
||||
: data.state2 != null
|
||||
? data.state2
|
||||
: data['2'] != null
|
||||
? data['2']
|
||||
: 0
|
||||
) || 0;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function extractListTotal(data, fallbackLength) {
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return Number(fallbackLength) || 0;
|
||||
}
|
||||
const total = Number(data.total != null ? data.total : data.count != null ? data.count : data.totalCount);
|
||||
if (!isNaN(total) && total > 0) return total;
|
||||
return Number(fallbackLength) || 0;
|
||||
}
|
||||
|
||||
function sortByApplyTimeDesc(list) {
|
||||
return (list || []).slice().sort(function (a, b) {
|
||||
return Number(b.applyTime || 0) - Number(a.applyTime || 0);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JournalFeeApproval',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
activeState: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
displayList: [],
|
||||
statusCounts: {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0
|
||||
},
|
||||
rejectVisible: false,
|
||||
rejectSubmitting: false,
|
||||
rejectForm: {
|
||||
id: '',
|
||||
acceptSn: '',
|
||||
articleTitle: '',
|
||||
remark: ''
|
||||
},
|
||||
historyVisible: false,
|
||||
historyLoading: false,
|
||||
historyList: [],
|
||||
historyArticle: {
|
||||
articleId: '',
|
||||
acceptSn: '',
|
||||
articleTitle: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
rejectRules() {
|
||||
return {
|
||||
remark: [
|
||||
{
|
||||
required: true,
|
||||
message: this.$t('journalFeeApproval.rejectRemarkRequired'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
statusTabs() {
|
||||
return [
|
||||
{
|
||||
state: 0,
|
||||
label: this.$t('journalFeeApproval.tabPending'),
|
||||
count: this.statusCounts[0] || 0
|
||||
},
|
||||
{
|
||||
state: 1,
|
||||
label: this.$t('journalFeeApproval.tabAccepted'),
|
||||
count: this.statusCounts[1] || 0
|
||||
},
|
||||
{
|
||||
state: 2,
|
||||
label: this.$t('journalFeeApproval.tabRejected'),
|
||||
count: this.statusCounts[2] || 0
|
||||
}
|
||||
];
|
||||
},
|
||||
emptyTipText() {
|
||||
if (this.activeState === 1) {
|
||||
return this.$t('journalFeeApproval.noAccepted');
|
||||
}
|
||||
if (this.activeState === 2) {
|
||||
return this.$t('journalFeeApproval.noRejected');
|
||||
}
|
||||
return this.$t('journalFeeApproval.noPending');
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.isFeeApprovalAccount()) {
|
||||
this.$message.warning(this.$t('journalFeeApproval.noPermission'));
|
||||
this.$router.replace('/dashboard');
|
||||
return;
|
||||
}
|
||||
this.refreshAll();
|
||||
},
|
||||
methods: {
|
||||
isFeeApprovalAccount() {
|
||||
return String(localStorage.getItem('U_id') || '') === '24950';
|
||||
},
|
||||
goArticleDetail(row) {
|
||||
if (!row || !row.articleId) {
|
||||
this.$message.warning(this.$t('journalFeeApproval.missingArticleId'));
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/articleDetailEditor',
|
||||
query: {
|
||||
id: row.articleId
|
||||
}
|
||||
});
|
||||
},
|
||||
fetchCounts() {
|
||||
return this.$api
|
||||
.post('api/Order/getApplyCount', {})
|
||||
.then((res) => {
|
||||
if (res && res.code === 0) {
|
||||
this.statusCounts = extractApplyCounts(res.data);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
// 保留已有数量
|
||||
});
|
||||
},
|
||||
refreshAll() {
|
||||
this.fetchCounts().finally(() => {
|
||||
this.fetchList();
|
||||
});
|
||||
},
|
||||
switchState(state) {
|
||||
if (this.activeState === state) return;
|
||||
this.activeState = state;
|
||||
this.pageIndex = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
handleTabClick(tab) {
|
||||
this.switchState(Number(tab.name));
|
||||
},
|
||||
handlePageSizeChange() {
|
||||
this.pageIndex = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
fetchList() {
|
||||
this.loading = true;
|
||||
this.$api
|
||||
.post('api/Order/getApplyList', {
|
||||
state: this.activeState,
|
||||
pageIndex: this.pageIndex,
|
||||
pageSize: this.pageSize
|
||||
})
|
||||
.then((res) => {
|
||||
this.loading = false;
|
||||
if (res.code === 0) {
|
||||
const rawList = this.extractList(res.data);
|
||||
const normalized = rawList.map(normalizeApplyRow).filter(Boolean);
|
||||
this.displayList = sortByApplyTimeDesc(normalized);
|
||||
this.total = extractListTotal(res.data, this.displayList.length);
|
||||
this.emitFeeApprovalBadgeRefresh();
|
||||
} else {
|
||||
this.displayList = [];
|
||||
this.total = 0;
|
||||
this.emitFeeApprovalBadgeRefresh();
|
||||
this.$message.error(res.msg || this.$t('journalFeeApproval.loadFailed'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
this.displayList = [];
|
||||
this.total = 0;
|
||||
this.emitFeeApprovalBadgeRefresh();
|
||||
this.$message.error(this.$t('journalFeeApproval.loadFailed'));
|
||||
});
|
||||
},
|
||||
emitFeeApprovalBadgeRefresh() {
|
||||
bus.$emit('fee-approval-badge-refresh');
|
||||
},
|
||||
extractList(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (!data || typeof data !== 'object') return [];
|
||||
return data.list || data.applies || data.applys || data.items || data.data || data.history || [];
|
||||
},
|
||||
normalizeArticleHistoryList(data) {
|
||||
return this.extractList(data)
|
||||
.map(normalizeHistoryItem)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => Number(b.applyTime) - Number(a.applyTime));
|
||||
},
|
||||
extractHistoryArticleMeta(data) {
|
||||
if (!data || typeof data !== 'object') return {};
|
||||
const articleInfo = data.article_info || data.article || {};
|
||||
return {
|
||||
acceptSn: pickValue(articleInfo.accept_sn, articleInfo.sn, data.accept_sn, data.sn, ''),
|
||||
articleTitle: pickValue(
|
||||
articleInfo.article_title,
|
||||
articleInfo.title,
|
||||
articleInfo.name,
|
||||
data.article_title,
|
||||
data.title,
|
||||
''
|
||||
)
|
||||
};
|
||||
},
|
||||
applyHistoryArticleMeta(data, fallback) {
|
||||
const meta = this.extractHistoryArticleMeta(data);
|
||||
if (meta.acceptSn) {
|
||||
this.historyArticle.acceptSn = meta.acceptSn;
|
||||
} else if (fallback && fallback.acceptSn) {
|
||||
this.historyArticle.acceptSn = fallback.acceptSn;
|
||||
}
|
||||
if (meta.articleTitle) {
|
||||
this.historyArticle.articleTitle = meta.articleTitle;
|
||||
} else if (fallback && fallback.articleTitle) {
|
||||
this.historyArticle.articleTitle = fallback.articleTitle;
|
||||
}
|
||||
},
|
||||
openHistoryDialog(row) {
|
||||
if (!row || !row.articleId) {
|
||||
this.$message.warning(this.$t('journalFeeApproval.missingArticleId'));
|
||||
return;
|
||||
}
|
||||
this.historyArticle = {
|
||||
articleId: row.articleId,
|
||||
acceptSn: row.acceptSn,
|
||||
articleTitle: row.articleTitle
|
||||
};
|
||||
this.historyVisible = true;
|
||||
this.historyLoading = true;
|
||||
this.historyList = [];
|
||||
this.$api
|
||||
.post('api/Order/getArticlePriceApplyList', {
|
||||
article_id: row.articleId
|
||||
})
|
||||
.then((res) => {
|
||||
this.historyLoading = false;
|
||||
if (res.code === 0) {
|
||||
this.applyHistoryArticleMeta(res.data, row);
|
||||
this.historyList = this.normalizeArticleHistoryList(res.data);
|
||||
if (this.historyList.length === 0 && row.history && row.history.length > 0) {
|
||||
this.historyList = row.history;
|
||||
}
|
||||
} else {
|
||||
this.applyHistoryArticleMeta(null, row);
|
||||
this.historyList = row.history && row.history.length > 0 ? row.history : [];
|
||||
if (this.historyList.length === 0) {
|
||||
this.$message.error(res.msg || this.$t('journalFeeApproval.historyLoadFailed'));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.historyLoading = false;
|
||||
this.applyHistoryArticleMeta(null, row);
|
||||
this.historyList = row.history && row.history.length > 0 ? row.history : [];
|
||||
if (this.historyList.length === 0) {
|
||||
this.$message.error(this.$t('journalFeeApproval.historyLoadFailed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
resetHistoryDialog() {
|
||||
this.historyLoading = false;
|
||||
this.historyList = [];
|
||||
this.historyArticle = {
|
||||
articleId: '',
|
||||
acceptSn: '',
|
||||
articleTitle: ''
|
||||
};
|
||||
},
|
||||
formatFee(value) {
|
||||
if (value === '' || value === null || value === undefined) return '-';
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) return value;
|
||||
return num.toFixed(2);
|
||||
},
|
||||
formatDateTime(timestamp) {
|
||||
if (!timestamp) return '-';
|
||||
if (typeof timestamp === 'string' && timestamp.indexOf('-') > 0) {
|
||||
return timestamp;
|
||||
}
|
||||
const ts = String(timestamp).length === 10 ? Number(timestamp) * 1000 : Number(timestamp);
|
||||
const date = new Date(ts);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n);
|
||||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
},
|
||||
getStatusLabel(status) {
|
||||
const text = String(status).toLowerCase();
|
||||
if (text === '2' || text.includes('reject') || text === '拒绝' || text === '驳回') {
|
||||
return this.$t('journalFeeApproval.statusReject');
|
||||
}
|
||||
if (text === '1' || text.includes('accept') || text.includes('agree') || text === '同意' || text === '通过') {
|
||||
return this.$t('journalFeeApproval.statusAccept');
|
||||
}
|
||||
if (text === '0' || text.includes('pending') || text.includes('wait') || text === '待审' || text === '待审批') {
|
||||
return this.$t('journalFeeApproval.statusPending');
|
||||
}
|
||||
return this.$t('journalFeeApproval.statusOther');
|
||||
},
|
||||
statusTagType(status) {
|
||||
const text = String(status).toLowerCase();
|
||||
if (text === '2' || text.includes('reject')) return 'danger';
|
||||
if (text === '1' || text.includes('accept')) return 'success';
|
||||
if (text === '0' || text.includes('pending')) return 'warning';
|
||||
return 'info';
|
||||
},
|
||||
handleAccept(row) {
|
||||
this.$confirm(this.$t('journalFeeApproval.acceptConfirm'), this.$t('journalFeeApproval.tip'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$t('journalFeeApproval.accept'),
|
||||
cancelButtonText: this.$t('journalFeeApproval.cancel')
|
||||
})
|
||||
.then(() => {
|
||||
const load = this.$loading({
|
||||
lock: true,
|
||||
text: 'Loading...',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
this.$api
|
||||
.post('api/Order/acceptPriceApply', { id: row.id })
|
||||
.then((res) => {
|
||||
load.close();
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.$t('journalFeeApproval.acceptSuccess'));
|
||||
this.refreshAll();
|
||||
} else {
|
||||
this.$message.error(res.msg || this.$t('journalFeeApproval.actionFailed'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
load.close();
|
||||
this.$message.error(this.$t('journalFeeApproval.actionFailed'));
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
openRejectDialog(row) {
|
||||
this.rejectForm = {
|
||||
id: row.id,
|
||||
acceptSn: row.acceptSn,
|
||||
articleTitle: row.articleTitle,
|
||||
remark: row.remark || ''
|
||||
};
|
||||
this.rejectVisible = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.rejectFormRef) this.$refs.rejectFormRef.clearValidate();
|
||||
});
|
||||
},
|
||||
resetRejectForm() {
|
||||
this.rejectForm = {
|
||||
id: '',
|
||||
acceptSn: '',
|
||||
articleTitle: '',
|
||||
remark: ''
|
||||
};
|
||||
this.rejectSubmitting = false;
|
||||
},
|
||||
submitReject() {
|
||||
this.$refs.rejectFormRef.validate((valid) => {
|
||||
if (!valid) return;
|
||||
this.rejectSubmitting = true;
|
||||
this.$api
|
||||
.post('api/Order/rejectPriceApply', {
|
||||
id: this.rejectForm.id,
|
||||
remark: this.rejectForm.remark
|
||||
})
|
||||
.then((res) => {
|
||||
this.rejectSubmitting = false;
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.$t('journalFeeApproval.rejectSuccess'));
|
||||
this.rejectVisible = false;
|
||||
this.refreshAll();
|
||||
} else {
|
||||
this.$message.error(res.msg || this.$t('journalFeeApproval.actionFailed'));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.rejectSubmitting = false;
|
||||
this.$message.error(this.$t('journalFeeApproval.actionFailed'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.handle-box {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.status-tabs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep .el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep .el-tabs__item {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep #tab-0 {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep #tab-0.is-active {
|
||||
color: #e6a23c;
|
||||
background: #fdf6ec;
|
||||
border-color: #f3d19e;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep #tab-1 {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep #tab-1.is-active {
|
||||
color: #67c23a;
|
||||
background: #f0f9eb;
|
||||
border-color: #c2e7b0;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep #tab-2 {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.status-tabs ::v-deep #tab-2.is-active {
|
||||
color: #f56c6c;
|
||||
background: #fef0f0;
|
||||
border-color: #fbc4c4;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pagination-box {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab_tie_col {
|
||||
margin-bottom: 6px;
|
||||
color: #333;
|
||||
word-wrap: break-word;
|
||||
word-break: normal;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tab_tie_col > span {
|
||||
color: #888;
|
||||
margin-right: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.article-title-link {
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.article-title-link:hover {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.apply-fee {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.approval-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.history-dialog__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin: 0 0 8px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.history-dialog__article {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.history-dialog__article-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.history-dialog__title {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.history-dialog__item {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.history-dialog__meta-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.history-dialog__time {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.history-dialog__empty {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.history-dialog {
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -232,12 +232,38 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 证书弹出框 -->
|
||||
<el-dialog :visible.sync="cerVisible" width="600px" :close-on-click-modal="false">
|
||||
<el-image class="table-td-thumb rev_digol" :src="this.IMG_Url"></el-image>
|
||||
<el-dialog :title="$t('reviewHistory.certificateTitle')" :visible.sync="cerVisible" width="640px" :close-on-click-modal="false">
|
||||
<div class="certificate-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-picture-outline"
|
||||
:loading="exportingCertificateImage"
|
||||
:disabled="!IMG_Url || exportingCertificatePdf"
|
||||
@click="downloadCertificateImage"
|
||||
>
|
||||
{{ $t('reviewHistory.exportCertificateImage') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-document"
|
||||
:loading="exportingCertificatePdf"
|
||||
:disabled="!IMG_Url || exportingCertificateImage"
|
||||
@click="downloadCertificatePdf"
|
||||
>
|
||||
{{ $t('reviewHistory.generateCertificatePdf') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-image
|
||||
ref="certificateImage"
|
||||
class="table-td-thumb rev_digol certificate-preview"
|
||||
:src="IMG_Url"
|
||||
fit="contain"
|
||||
></el-image>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :visible.sync="feilVisible" width="600px" :close-on-click-modal="false">
|
||||
<h2 style="text-align: center">No certificate</h2>
|
||||
<h2 style="text-align: center">{{ $t('reviewHistory.noCertificate') }}</h2>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="feilVisible = false">OK</el-button>
|
||||
</span>
|
||||
@@ -249,6 +275,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { downloadCertificateImage, downloadCertificatePdf } from '@/utils/certificateExport';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -264,8 +292,12 @@ export default {
|
||||
},
|
||||
link_Tota3: 0,
|
||||
IMG_Url: '',
|
||||
certificateRow: null,
|
||||
certificateFileName: 'certificate',
|
||||
cerVisible: false,
|
||||
feilVisible: false,
|
||||
exportingCertificateImage: false,
|
||||
exportingCertificatePdf: false,
|
||||
dynamicTags: [
|
||||
{
|
||||
label: 'Submission System 2.0',
|
||||
@@ -383,6 +415,8 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.IMG_Url = this.Common.mediaUrl + res.data.icon;
|
||||
this.certificateRow = row;
|
||||
this.certificateFileName = this.buildCertificateFileName(row);
|
||||
this.cerVisible = true;
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
@@ -394,6 +428,60 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
buildCertificateFileName(row) {
|
||||
const title = row && row.article_title ? String(row.article_title).trim() : '';
|
||||
if (title) {
|
||||
return 'certificate-' + title;
|
||||
}
|
||||
if (row && row.art_rev_id) {
|
||||
return 'certificate-' + row.art_rev_id;
|
||||
}
|
||||
return 'certificate';
|
||||
},
|
||||
|
||||
buildCertificatePdfFileName(row) {
|
||||
const acceptSn = row && row.accept_sn ? String(row.accept_sn).trim() : '';
|
||||
if (acceptSn) {
|
||||
return 'Certificate-' + acceptSn;
|
||||
}
|
||||
if (row && row.art_rev_id) {
|
||||
return 'Certificate-' + row.art_rev_id;
|
||||
}
|
||||
return 'Certificate';
|
||||
},
|
||||
|
||||
async downloadCertificateImage() {
|
||||
if (!this.IMG_Url || this.exportingCertificateImage) {
|
||||
return;
|
||||
}
|
||||
this.exportingCertificateImage = true;
|
||||
try {
|
||||
await downloadCertificateImage(this.IMG_Url, this.certificateFileName);
|
||||
this.$message.success(this.$t('reviewHistory.exportCertificateImageSuccess'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.$message.error(this.$t('reviewHistory.exportCertificateImageFail'));
|
||||
} finally {
|
||||
this.exportingCertificateImage = false;
|
||||
}
|
||||
},
|
||||
|
||||
async downloadCertificatePdf() {
|
||||
if (!this.IMG_Url || this.exportingCertificatePdf) {
|
||||
return;
|
||||
}
|
||||
this.exportingCertificatePdf = true;
|
||||
try {
|
||||
await downloadCertificatePdf(this.IMG_Url, this.buildCertificatePdfFileName(this.certificateRow));
|
||||
this.$message.success(this.$t('reviewHistory.generateCertificatePdfSuccess'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.$message.error(this.$t('reviewHistory.generateCertificatePdfFail'));
|
||||
} finally {
|
||||
this.exportingCertificatePdf = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭标签
|
||||
handleClose(tag) {
|
||||
// this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
|
||||
@@ -473,4 +561,14 @@ td {
|
||||
/* text-align: center; */
|
||||
display: flex;
|
||||
}
|
||||
.certificate-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.certificate-preview {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -361,6 +361,13 @@ export default new Router({
|
||||
title: 'Order List'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/journalFeeApproval',
|
||||
component: () => import('../components/page/journalFeeApproval.vue'),
|
||||
meta: {
|
||||
title: 'Journal Fee Approval'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/articleReviewer',
|
||||
component: () => import('../components/page/articleReviewer.vue'),
|
||||
|
||||
250
src/utils/certificateExport.js
Normal file
250
src/utils/certificateExport.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import { saveAs } from 'file-saver';
|
||||
import { resolveManuscriptFetchUrl } from './manuscriptWordReferences';
|
||||
|
||||
function sanitizeFileName(name) {
|
||||
const base = String(name || 'certificate')
|
||||
.replace(/[\\/:*?"<>|]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.trim()
|
||||
.slice(0, 80);
|
||||
return base || 'certificate';
|
||||
}
|
||||
|
||||
async function fetchImageBlob(fetchUrl) {
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load certificate image');
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
function loadImageElement(imageUrl) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = function () {
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = function () {
|
||||
reject(new Error('Failed to load certificate image'));
|
||||
};
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCertificateImage(imageUrl) {
|
||||
const fetchUrl = resolveManuscriptFetchUrl(imageUrl) || imageUrl;
|
||||
|
||||
try {
|
||||
const blob = await fetchImageBlob(fetchUrl);
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
const img = await loadImageElement(objectUrl);
|
||||
return {
|
||||
img: img,
|
||||
revoke: function () {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
throw error;
|
||||
}
|
||||
} catch (fetchError) {
|
||||
const img = await loadImageElement(fetchUrl);
|
||||
return {
|
||||
img: img,
|
||||
revoke: function () {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function imageToPngDataUrl(img) {
|
||||
const width = img.naturalWidth || img.width;
|
||||
const height = img.naturalHeight || img.height;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Canvas is not supported');
|
||||
}
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
return {
|
||||
dataUrl: canvas.toDataURL('image/png'),
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
|
||||
function imageToJpegBytes(img, quality) {
|
||||
const width = img.naturalWidth || img.width;
|
||||
const height = img.naturalHeight || img.height;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Canvas is not supported');
|
||||
}
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', quality == null ? 0.92 : quality);
|
||||
return {
|
||||
bytes: dataUrlToUint8Array(dataUrl),
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
|
||||
function triggerBlobDownload(blob, fileName) {
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
function dataUrlToUint8Array(dataUrl) {
|
||||
const parts = String(dataUrl).split(',');
|
||||
const binary = atob(parts[1]);
|
||||
const len = binary.length;
|
||||
const buffer = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
buffer[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function dataUrlToBlob(dataUrl) {
|
||||
const parts = String(dataUrl).split(',');
|
||||
const mimeMatch = parts[0].match(/:(.*?);/);
|
||||
const mime = mimeMatch ? mimeMatch[1] : 'image/png';
|
||||
return new Blob([dataUrlToUint8Array(dataUrl)], { type: mime });
|
||||
}
|
||||
|
||||
function concatUint8Arrays(chunks) {
|
||||
const total = chunks.reduce(function (sum, chunk) {
|
||||
return sum + chunk.length;
|
||||
}, 0);
|
||||
const out = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
chunks.forEach(function (chunk) {
|
||||
out.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function stringToUint8Array(text) {
|
||||
const len = text.length;
|
||||
const out = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
out[i] = text.charCodeAt(i) & 0xff;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 单页 JPEG 图片 PDF,不依赖 jspdf,避免 webpack 编译 node_modules 新语法 */
|
||||
function buildSingleImagePdf(jpegBytes, widthPx, heightPx) {
|
||||
const widthPt = Math.round(widthPx * 0.75 * 100) / 100;
|
||||
const heightPt = Math.round(heightPx * 0.75 * 100) / 100;
|
||||
const contentStream = 'q\n' + widthPt + ' 0 0 ' + heightPt + ' 0 0 cm\n/Im1 Do\nQ\n';
|
||||
|
||||
const chunks = [];
|
||||
const objOffsets = {};
|
||||
|
||||
function totalLen() {
|
||||
let length = 0;
|
||||
chunks.forEach(function (chunk) {
|
||||
length += chunk.length;
|
||||
});
|
||||
return length;
|
||||
}
|
||||
|
||||
function pushText(text) {
|
||||
chunks.push(stringToUint8Array(text));
|
||||
}
|
||||
|
||||
function startObject(num) {
|
||||
objOffsets[num] = totalLen();
|
||||
}
|
||||
|
||||
pushText('%PDF-1.4\n');
|
||||
|
||||
startObject(1);
|
||||
pushText('1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n');
|
||||
|
||||
startObject(2);
|
||||
pushText('2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n');
|
||||
|
||||
startObject(3);
|
||||
pushText(
|
||||
'3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' +
|
||||
widthPt +
|
||||
' ' +
|
||||
heightPt +
|
||||
'] /Resources << /XObject << /Im1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n'
|
||||
);
|
||||
|
||||
startObject(4);
|
||||
pushText(
|
||||
'4 0 obj\n<< /Type /XObject /Subtype /Image /Width ' +
|
||||
widthPx +
|
||||
' /Height ' +
|
||||
heightPx +
|
||||
' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ' +
|
||||
jpegBytes.length +
|
||||
' >>\nstream\n'
|
||||
);
|
||||
chunks.push(jpegBytes);
|
||||
pushText('\nendstream\nendobj\n');
|
||||
|
||||
startObject(5);
|
||||
pushText(
|
||||
'5 0 obj\n<< /Length ' +
|
||||
contentStream.length +
|
||||
' >>\nstream\n' +
|
||||
contentStream +
|
||||
'endstream\nendobj\n'
|
||||
);
|
||||
|
||||
const xrefPos = totalLen();
|
||||
let xref = 'xref\n0 6\n0000000000 65535 f \n';
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
xref += String(objOffsets[i]).padStart(10, '0') + ' 00000 n \n';
|
||||
}
|
||||
xref += 'trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n' + xrefPos + '\n%%EOF';
|
||||
pushText(xref);
|
||||
|
||||
return concatUint8Arrays(chunks);
|
||||
}
|
||||
|
||||
/** 导出审稿证书为 PNG 图片 */
|
||||
export async function downloadCertificateImage(imageUrl, fileName) {
|
||||
if (!imageUrl) {
|
||||
throw new Error('Certificate image is missing');
|
||||
}
|
||||
|
||||
const loaded = await loadCertificateImage(imageUrl);
|
||||
try {
|
||||
const png = imageToPngDataUrl(loaded.img);
|
||||
const blob = dataUrlToBlob(png.dataUrl);
|
||||
triggerBlobDownload(blob, sanitizeFileName(fileName) + '.png');
|
||||
} finally {
|
||||
loaded.revoke();
|
||||
}
|
||||
}
|
||||
|
||||
/** 将审稿证书导出为 PDF(单页,尺寸与图片一致) */
|
||||
export async function downloadCertificatePdf(imageUrl, fileName) {
|
||||
if (!imageUrl) {
|
||||
throw new Error('Certificate image is missing');
|
||||
}
|
||||
|
||||
const loaded = await loadCertificateImage(imageUrl);
|
||||
try {
|
||||
const jpeg = imageToJpegBytes(loaded.img, 0.92);
|
||||
const pdfBytes = buildSingleImagePdf(jpeg.bytes, jpeg.width, jpeg.height);
|
||||
triggerBlobDownload(new Blob([pdfBytes], { type: 'application/pdf' }), sanitizeFileName(fileName) + '.pdf');
|
||||
} finally {
|
||||
loaded.revoke();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
AlignmentType,
|
||||
BorderStyle,
|
||||
CommentRangeEnd,
|
||||
CommentRangeStart,
|
||||
CommentReference,
|
||||
ExternalHyperlink,
|
||||
ImageRun,
|
||||
LineRuleType,
|
||||
@@ -25,7 +28,11 @@ import {
|
||||
getDocumentGridProperties
|
||||
} from '@/utils/exportWordLayout';
|
||||
import orcidIconUrl from '@/assets/img/orcid.png';
|
||||
import { createTmrLogoImageRun } from '@/utils/exportManuscriptWordHeader';
|
||||
import {
|
||||
createTmrLogoImageRun,
|
||||
resolveManuscriptPageHeaderCitationParts,
|
||||
resolveManuscriptPageHeaderTypeText
|
||||
} from '@/utils/exportManuscriptWordHeader';
|
||||
|
||||
/** 标题:阿里巴巴普惠体 H / Calibri,四号 14pt */
|
||||
const TITLE_FONT_EAST_ASIA = '阿里巴巴普惠体 H';
|
||||
@@ -80,6 +87,8 @@ const HOME_PAGE_LEFT_FONT_SIZE = 13;
|
||||
const CITATION_CONTENT_COLOR = '0070C0';
|
||||
/** 缺少 online_date / 其它元数据时占位文案 */
|
||||
const MISSING_DATA_PLACEHOLDER = 'No data';
|
||||
const ACCEPTED_DATE_PLACEHOLDER = 'XXX';
|
||||
const ONLINE_DATE_PLACEHOLDER = 'input online_date';
|
||||
/** 缺少数据时显示正红色 rgb(255, 0, 0) */
|
||||
const MISSING_DATA_COLOR = 'FF0000';
|
||||
/** @deprecated 使用 MISSING_DATA_COLOR */
|
||||
@@ -118,7 +127,11 @@ const HOME_PAGE_LEFT_PARAGRAPH_OOXML =
|
||||
'<w:widowControl w:val="0"/>';
|
||||
/** Abstract 结构化标签:粗体蓝色 rgb(0, 112, 192) */
|
||||
const ABSTRACT_LABEL_COLOR = '0070C0';
|
||||
const ABSTRACT_SECTION_LABEL_PATTERN = /\b(Background:|Methods:|Results:|Conclusion:)\s*/gi;
|
||||
const ABSTRACT_SECTION_LABEL_PATTERN = /\b(Background:|Objective:|Methods:|Results:|Conclusion:)\s*/gi;
|
||||
const ABSTRACT_OBJECTIVE_BACKGROUND_COMMENT_TEXT =
|
||||
'根据本刊摘要格式要求,请将"Objective"修改为"Background",并同时将冒号后的内容调整为研究相关的背景介绍,而非直接陈述研究目的。';
|
||||
const ABSTRACT_NEXT_SECTION_AFTER_OBJECTIVE_PATTERN =
|
||||
/\b(Background:|Methods:|Results:|Conclusion:)\s*/i;
|
||||
/** Abstract 段落(图一):段前 0.1 行、段后 0、固定行距 10 磅、两端对齐、对齐网格 */
|
||||
const ABSTRACT_PARAGRAPH_SPACING = {
|
||||
before: 0,
|
||||
@@ -204,8 +217,19 @@ const TITLE_PARAGRAPH_SPACING_OOXML =
|
||||
' w:beforeAutospacing="0" w:afterAutospacing="0"/>';
|
||||
|
||||
const COMPETING_INTERESTS_TEXT = 'The authors declare no conflicts of interest.';
|
||||
const PEER_REVIEW_TEXT =
|
||||
'We thank the peer reviewers for their constructive comments and suggestions, which helped improve the quality of this manuscript.';
|
||||
|
||||
function resolveJournalDisplayName(headerData) {
|
||||
const journal = (headerData && headerData.journal) || {};
|
||||
return String(journal.title || journal.jname || journal.name || journal.jabbr || '').trim();
|
||||
}
|
||||
|
||||
function buildPeerReviewText(headerData) {
|
||||
const journalName = resolveJournalDisplayName(headerData);
|
||||
if (journalName) {
|
||||
return journalName + ' reviewers for their contribution to the peer review of this paper.';
|
||||
}
|
||||
return 'Reviewers for their contribution to the peer review of this paper.';
|
||||
}
|
||||
|
||||
/** 元数据双栏表格:无边框 */
|
||||
const METADATA_TABLE_BORDER_NONE = {
|
||||
@@ -1769,29 +1793,65 @@ function getProductionOnlineDate(production, headerData) {
|
||||
return String(fromHeader || fromProduction || fromTypeset || '').trim();
|
||||
}
|
||||
|
||||
/** Available online 专用:使用 online_date,缺失时显示 No data. */
|
||||
/** Available online 专用:使用 online_date,缺失时显示 input online_date(红色) */
|
||||
function formatOnlineDateDisplay(production, headerData) {
|
||||
const raw = getProductionOnlineDate(production, headerData);
|
||||
if (!raw || /^no time$/i.test(raw)) {
|
||||
return { text: MISSING_DATA_PLACEHOLDER, missing: true };
|
||||
return { text: ONLINE_DATE_PLACEHOLDER, missing: true };
|
||||
}
|
||||
|
||||
const formatted = formatManuscriptDisplayDate(raw);
|
||||
if (formatted.missing) {
|
||||
return { text: MISSING_DATA_PLACEHOLDER, missing: true };
|
||||
return { text: ONLINE_DATE_PLACEHOLDER, missing: true };
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/** Accepted 专用:缺失时显示 No data. */
|
||||
/** Accepted 专用:缺失时显示 XXX(红色) */
|
||||
function formatAcceptedDateDisplay(review) {
|
||||
const formatted = formatManuscriptDisplayDate(review && review.accepted_time);
|
||||
if (formatted.missing) {
|
||||
return { text: MISSING_DATA_PLACEHOLDER, missing: true };
|
||||
return { text: ACCEPTED_DATE_PLACEHOLDER, missing: true };
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function resolveCopyrightYear(headerData) {
|
||||
const production = (headerData && headerData.production) || {};
|
||||
const stageInfo = (headerData && headerData.stageInfo) || {};
|
||||
const fromStageInfo = stageInfo.stage_year != null ? String(stageInfo.stage_year).trim() : '';
|
||||
if (fromStageInfo) {
|
||||
return fromStageInfo;
|
||||
}
|
||||
const fromProduction = production.stage_year != null ? String(production.stage_year).trim() : '';
|
||||
if (fromProduction) {
|
||||
return fromProduction;
|
||||
}
|
||||
const citationParts = resolveManuscriptPageHeaderCitationParts(headerData || {});
|
||||
const stageMatch = String(citationParts.stage || '').trim().match(/^(\d{4})/);
|
||||
if (stageMatch) {
|
||||
return stageMatch[1];
|
||||
}
|
||||
const review = (headerData && headerData.finalReview) || {};
|
||||
const accepted = formatManuscriptDisplayDate(review.accepted_time);
|
||||
if (!accepted.missing) {
|
||||
const acceptedYearMatch = String(accepted.text || '').match(/(\d{4})/);
|
||||
if (acceptedYearMatch) {
|
||||
return acceptedYearMatch[1];
|
||||
}
|
||||
}
|
||||
return String(new Date().getFullYear());
|
||||
}
|
||||
|
||||
function appendCopyrightNoticeParagraph(paragraphs, headerData) {
|
||||
const year = resolveCopyrightYear(headerData);
|
||||
const text =
|
||||
'© ' +
|
||||
year +
|
||||
' Author(s). Published by TMR Publishing Group Limited. This is an open access article under the CC-BY license. (https://creativecommons.org/licenses/by/4.0/)';
|
||||
paragraphs.push(createHomePageLeftParagraph([createStyledTextRun(text, getHomePageLeftContentFontStyle())]));
|
||||
}
|
||||
|
||||
function buildCitationRuns(headerData) {
|
||||
const production = (headerData && headerData.production) || {};
|
||||
const journal = (headerData && headerData.journal) || {};
|
||||
@@ -1803,6 +1863,12 @@ function buildCitationRuns(headerData) {
|
||||
const abbr = String(production.abbr || '').trim();
|
||||
const title = resolveManuscriptDisplayTitle(headerData);
|
||||
const journalJabbr = String((journal && journal.jabbr) || '').trim();
|
||||
const citationParts = resolveManuscriptPageHeaderCitationParts(headerData);
|
||||
let stage = String(citationParts.stage || '').trim();
|
||||
if (stage.endsWith('.')) {
|
||||
stage = stage.slice(0, -1);
|
||||
}
|
||||
const doiSuffix = String(citationParts.doiSuffix || '').trim();
|
||||
|
||||
if (!abbr && !title) {
|
||||
return [];
|
||||
@@ -1818,6 +1884,12 @@ function buildCitationRuns(headerData) {
|
||||
if (journalJabbr) {
|
||||
runs.push(createStyledTextRun(journalJabbr + '.', journalStyle));
|
||||
}
|
||||
if (stage) {
|
||||
runs.push(createStyledTextRun(' ' + stage + '.', citationStyle));
|
||||
}
|
||||
if (doiSuffix) {
|
||||
runs.push(createStyledTextRun(' doi: 10.53388/' + doiSuffix + '.', citationStyle));
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
@@ -1834,16 +1906,20 @@ function createMetadataBodyParagraph(text) {
|
||||
return createHomePageLeftParagraph([createStyledTextRun(content, getHomePageLeftContentFontStyle())]);
|
||||
}
|
||||
|
||||
function appendMetadataInlinePlain(paragraphs, label, text) {
|
||||
function appendMetadataInlinePlain(paragraphs, label, text, options) {
|
||||
const opts = options || {};
|
||||
const trailingPeriod = opts.trailingPeriod === true;
|
||||
const content = normalizeMetadataFieldValue(text);
|
||||
paragraphs.push(
|
||||
createHomePageLeftParagraph([
|
||||
createStyledTextRun(label + ' ', getHomePageLeftPlainFontStyle()),
|
||||
content
|
||||
? createStyledTextRun(content, getHomePageLeftContentFontStyle())
|
||||
: createMissingDataRun()
|
||||
])
|
||||
);
|
||||
const runs = [
|
||||
createStyledTextRun(label + ' ', getHomePageLeftPlainFontStyle()),
|
||||
content
|
||||
? createStyledTextRun(content, getHomePageLeftContentFontStyle())
|
||||
: createMissingDataRun()
|
||||
];
|
||||
if (trailingPeriod) {
|
||||
runs.push(createStyledTextRun('.', getHomePageLeftContentFontStyle()));
|
||||
}
|
||||
paragraphs.push(createHomePageLeftParagraph(runs));
|
||||
}
|
||||
|
||||
function appendReviewDatesParagraph(paragraphs, finalReview, production, headerData) {
|
||||
@@ -1870,6 +1946,7 @@ function appendReviewDatesParagraph(paragraphs, finalReview, production, headerD
|
||||
}
|
||||
});
|
||||
|
||||
runs.push(createStyledTextRun('.', contentStyle));
|
||||
paragraphs.push(createHomePageLeftParagraph(runs));
|
||||
}
|
||||
|
||||
@@ -1893,8 +1970,118 @@ function createAbstractHeadingParagraph() {
|
||||
]);
|
||||
}
|
||||
|
||||
function buildAbstractContentRuns(text) {
|
||||
const content = String(text || '').trim();
|
||||
function hasAbstractSectionLabel(text, label) {
|
||||
return new RegExp('\\b' + label + '\\s*:\\s*', 'i').test(String(text || ''));
|
||||
}
|
||||
|
||||
function isArticleManuscriptType(headerData) {
|
||||
return resolveManuscriptPageHeaderTypeText(headerData) === 'ARTICLE';
|
||||
}
|
||||
|
||||
function shouldAddAbstractObjectiveBackgroundComment(headerData, abstractText) {
|
||||
const content = String(abstractText || '').trim();
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
if (!isArticleManuscriptType(headerData)) {
|
||||
return false;
|
||||
}
|
||||
if (hasAbstractSectionLabel(content, 'Background')) {
|
||||
return false;
|
||||
}
|
||||
if (!hasAbstractSectionLabel(content, 'Objective')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function extractAbstractObjectiveSectionRange(text) {
|
||||
const content = String(text || '');
|
||||
const objectiveMatch = content.match(/\bObjective\s*:\s*/i);
|
||||
if (!objectiveMatch || objectiveMatch.index == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = objectiveMatch.index;
|
||||
const afterObjective = start + objectiveMatch[0].length;
|
||||
const rest = content.slice(afterObjective);
|
||||
const nextLabelMatch = rest.match(ABSTRACT_NEXT_SECTION_AFTER_OBJECTIVE_PATTERN);
|
||||
const end =
|
||||
nextLabelMatch && nextLabelMatch.index != null
|
||||
? afterObjective + nextLabelMatch.index
|
||||
: content.length;
|
||||
|
||||
let trimmedEnd = end;
|
||||
while (trimmedEnd > start && /\s/.test(content.charAt(trimmedEnd - 1))) {
|
||||
trimmedEnd -= 1;
|
||||
}
|
||||
|
||||
return { start, end: trimmedEnd };
|
||||
}
|
||||
|
||||
export function buildAbstractObjectiveWordCommentDefinitions(headerData) {
|
||||
const production = (headerData && headerData.production) || {};
|
||||
const abstractText = htmlToPlainText(production.abstract || '');
|
||||
if (!shouldAddAbstractObjectiveBackgroundComment(headerData, abstractText)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const range = extractAbstractObjectiveSectionRange(abstractText);
|
||||
if (!range) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
author: 'Editor',
|
||||
initials: 'Ed',
|
||||
date: new Date(),
|
||||
text: ABSTRACT_OBJECTIVE_BACKGROUND_COMMENT_TEXT,
|
||||
range
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export function buildManuscriptWordDocumentComments(definitions) {
|
||||
if (!definitions || !definitions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
children: definitions.map(function (def) {
|
||||
const textBlocks = String(def.text || '')
|
||||
.split(/\n\s*\n/)
|
||||
.map(function (block) {
|
||||
return block.trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
const paragraphs =
|
||||
textBlocks.length > 0
|
||||
? textBlocks.map(function (block) {
|
||||
return new Paragraph({
|
||||
children: [new TextRun(block)]
|
||||
});
|
||||
})
|
||||
: [
|
||||
new Paragraph({
|
||||
children: [new TextRun('')]
|
||||
})
|
||||
];
|
||||
|
||||
return {
|
||||
id: def.id,
|
||||
author: def.author || 'Editor',
|
||||
initials: def.initials || 'Ed',
|
||||
date: def.date || new Date(),
|
||||
children: paragraphs
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function buildAbstractStyledContentRuns(text) {
|
||||
const content = String(text || '');
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
@@ -1925,11 +2112,53 @@ function buildAbstractContentRuns(text) {
|
||||
runs.push(createStyledTextRun(content.slice(lastIndex), baseStyle));
|
||||
}
|
||||
|
||||
return runs.length ? runs : [createStyledTextRun(content, baseStyle)];
|
||||
return runs;
|
||||
}
|
||||
|
||||
function createAbstractContentParagraph(text) {
|
||||
const runs = buildAbstractContentRuns(text);
|
||||
function buildAbstractContentRuns(text, options) {
|
||||
const opts = options || {};
|
||||
const content = String(text || '').trim();
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const commentRange = opts.commentRange;
|
||||
if (
|
||||
commentRange &&
|
||||
Number.isFinite(commentRange.start) &&
|
||||
Number.isFinite(commentRange.end) &&
|
||||
Number.isFinite(commentRange.commentId)
|
||||
) {
|
||||
const safeStart = Math.max(0, Math.min(commentRange.start, content.length));
|
||||
const safeEnd = Math.max(safeStart, Math.min(commentRange.end, content.length));
|
||||
const runs = [];
|
||||
|
||||
if (safeStart > 0) {
|
||||
runs.push.apply(runs, buildAbstractStyledContentRuns(content.slice(0, safeStart)));
|
||||
}
|
||||
if (safeEnd > safeStart) {
|
||||
runs.push(new CommentRangeStart(commentRange.commentId));
|
||||
runs.push.apply(runs, buildAbstractStyledContentRuns(content.slice(safeStart, safeEnd)));
|
||||
runs.push(new CommentRangeEnd(commentRange.commentId));
|
||||
runs.push(
|
||||
new TextRun({
|
||||
children: [new CommentReference(commentRange.commentId)]
|
||||
})
|
||||
);
|
||||
}
|
||||
if (safeEnd < content.length) {
|
||||
runs.push.apply(runs, buildAbstractStyledContentRuns(content.slice(safeEnd)));
|
||||
}
|
||||
|
||||
return runs.length ? runs : [createStyledTextRun(content, getMetadataTableFontStyle())];
|
||||
}
|
||||
|
||||
const styledRuns = buildAbstractStyledContentRuns(content);
|
||||
return styledRuns.length ? styledRuns : [createStyledTextRun(content, getMetadataTableFontStyle())];
|
||||
}
|
||||
|
||||
function createAbstractContentParagraph(text, options) {
|
||||
const runs = buildAbstractContentRuns(text, options);
|
||||
if (!runs.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -1974,18 +2203,28 @@ function buildLeftMetadataParagraphs(headerData) {
|
||||
paragraphs.push(createHomePageLeftEmptyLineParagraph());
|
||||
}
|
||||
|
||||
appendMetadataBlock(paragraphs, 'Peer review information', PEER_REVIEW_TEXT);
|
||||
appendMetadataBlock(paragraphs, 'Peer review information', buildPeerReviewText(headerData));
|
||||
|
||||
const boardName =
|
||||
finalReview && finalReview.reviewer_id && finalReview.realname ? finalReview.realname : '';
|
||||
appendMetadataInlinePlain(paragraphs, 'Editorial Advisory Board:', boardName);
|
||||
appendMetadataInlinePlain(paragraphs, 'Production editor:', htmlToPlainText(production.executive_editor || ''));
|
||||
appendMetadataInlinePlain(paragraphs, 'Editorial Advisory Board:', boardName, { trailingPeriod: true });
|
||||
appendMetadataInlinePlain(paragraphs, 'Production editor:', htmlToPlainText(production.executive_editor || ''), {
|
||||
trailingPeriod: true
|
||||
});
|
||||
appendReviewDatesParagraph(paragraphs, finalReview, production, headerData);
|
||||
appendCopyrightNoticeParagraph(paragraphs, headerData);
|
||||
|
||||
return paragraphs.length ? paragraphs : [createMissingMetadataBodyParagraph()];
|
||||
}
|
||||
|
||||
function buildRightMetadataParagraphs(headerData) {
|
||||
function formatHomePageKeywords(text) {
|
||||
return htmlToPlainText(text)
|
||||
.trim()
|
||||
.replace(/\s*,\s*/g, '; ');
|
||||
}
|
||||
|
||||
function buildRightMetadataParagraphs(headerData, options) {
|
||||
const opts = options || {};
|
||||
const production = (headerData && headerData.production) || {};
|
||||
const paragraphs = [];
|
||||
|
||||
@@ -1993,7 +2232,9 @@ function buildRightMetadataParagraphs(headerData) {
|
||||
|
||||
const abstractText = htmlToPlainText(production.abstract || '');
|
||||
if (abstractText) {
|
||||
const abstractParagraph = createAbstractContentParagraph(abstractText);
|
||||
const abstractParagraph = createAbstractContentParagraph(abstractText, {
|
||||
commentRange: opts.abstractObjectiveComment || null
|
||||
});
|
||||
if (abstractParagraph) {
|
||||
paragraphs.push(abstractParagraph);
|
||||
}
|
||||
@@ -2001,7 +2242,7 @@ function buildRightMetadataParagraphs(headerData) {
|
||||
paragraphs.push(createMissingAbstractBodyParagraph());
|
||||
}
|
||||
|
||||
const keywords = String(production.keywords || '').trim();
|
||||
const keywords = formatHomePageKeywords(production.keywords || '');
|
||||
if (keywords) {
|
||||
if (abstractText) {
|
||||
paragraphs.push(createAbstractEmptyLineParagraph());
|
||||
@@ -2040,10 +2281,10 @@ function createMetadataTableCell(paragraphs, widthTwips) {
|
||||
return new TableCell(cellOptions);
|
||||
}
|
||||
|
||||
function buildMetadataTable(headerData) {
|
||||
function buildMetadataTable(headerData, options) {
|
||||
/** 首页表格:左栏 Author contributions 6.44cm,右栏 Abstract 11.36cm */
|
||||
const leftParagraphs = buildLeftMetadataParagraphs(headerData);
|
||||
const rightParagraphs = buildRightMetadataParagraphs(headerData);
|
||||
const rightParagraphs = buildRightMetadataParagraphs(headerData, options);
|
||||
|
||||
return new Table({
|
||||
width: {
|
||||
@@ -2509,8 +2750,8 @@ export async function patchMetadataTableProperties(blob) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildHomePageTableSectionChildren(headerData) {
|
||||
return [buildMetadataTable(headerData)];
|
||||
function buildHomePageTableSectionChildren(headerData, options) {
|
||||
return [buildMetadataTable(headerData, options)];
|
||||
}
|
||||
|
||||
function buildMetadataSectionChildren(headerData) {
|
||||
@@ -2577,7 +2818,7 @@ function buildTitleBlockChildren(headerData, orcidImageData, tmrLogoImageData) {
|
||||
return children;
|
||||
}
|
||||
|
||||
export function buildAuthorHeaderSectionGroups(headerData, orcidImageData, tmrLogoImageData) {
|
||||
export function buildAuthorHeaderSectionGroups(headerData, orcidImageData, tmrLogoImageData, options) {
|
||||
if (!headerData) {
|
||||
return [];
|
||||
}
|
||||
@@ -2587,7 +2828,7 @@ export function buildAuthorHeaderSectionGroups(headerData, orcidImageData, tmrLo
|
||||
return [];
|
||||
}
|
||||
|
||||
const homePageTableChildren = buildHomePageTableSectionChildren(headerData);
|
||||
const homePageTableChildren = buildHomePageTableSectionChildren(headerData, options);
|
||||
if (!homePageTableChildren.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
AlignmentType,
|
||||
BorderStyle,
|
||||
CommentRangeEnd,
|
||||
CommentRangeStart,
|
||||
CommentReference,
|
||||
Document,
|
||||
LineRuleType,
|
||||
Packer,
|
||||
@@ -18,6 +21,8 @@ import { saveAs } from 'file-saver';
|
||||
import JSZip from 'jszip';
|
||||
import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
|
||||
import { expandCitationBracket } from '@/utils/manuscriptCitationRelevance';
|
||||
import { tokenizeReferenceHighlightSegments } from '@/utils/manuscriptMediaReferences';
|
||||
|
||||
/** 斑马纹 rgb(250, 231, 232) */
|
||||
const ODD_ROW_FILL = 'FAE7E8';
|
||||
@@ -430,33 +435,117 @@ function normalizeCitationBracketCommaSpacing(text) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 匹配正文参考文献标号:\[[0-9, -\-\]{1,}\] */
|
||||
const CITATION_BRACKET_PATTERN = /\[[0-9, \-]+\]/g;
|
||||
|
||||
function appendRunsForCitationText(text, style, appendRun) {
|
||||
function createCitationCommentMatcher(citationComments) {
|
||||
const byRef = {};
|
||||
(citationComments || []).forEach(function (comment) {
|
||||
if (!comment || comment.refNo == null) {
|
||||
return;
|
||||
}
|
||||
const key = String(comment.refNo);
|
||||
if (!byRef[key]) {
|
||||
byRef[key] = [];
|
||||
}
|
||||
byRef[key].push(comment);
|
||||
});
|
||||
Object.keys(byRef).forEach(function (key) {
|
||||
byRef[key].sort(function (a, b) {
|
||||
return Number(a.occurrence || 0) - Number(b.occurrence || 0);
|
||||
});
|
||||
});
|
||||
|
||||
const nextIndex = {};
|
||||
|
||||
return {
|
||||
matchBracket: function (inner) {
|
||||
const nums = expandCitationBracket(inner);
|
||||
for (let i = 0; i < nums.length; i += 1) {
|
||||
const refNo = nums[i];
|
||||
const key = String(refNo);
|
||||
const idx = nextIndex[key] || 0;
|
||||
nextIndex[key] = idx + 1;
|
||||
const list = byRef[key];
|
||||
if (list && list[idx]) {
|
||||
return list[idx].commentId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function appendRunsForReferenceHighlightText(text, style, appendRun, commentMatcher) {
|
||||
const normalized = normalizeCitationBracketCommaSpacing(text);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
CITATION_BRACKET_PATTERN.lastIndex = 0;
|
||||
while ((match = CITATION_BRACKET_PATTERN.exec(normalized)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
appendRun(normalized.slice(lastIndex, match.index), style);
|
||||
const segments = tokenizeReferenceHighlightSegments(normalized);
|
||||
segments.forEach(function (segment) {
|
||||
if (!segment || !segment.text) {
|
||||
return;
|
||||
}
|
||||
appendRun(match[0], Object.assign({}, style, { blue: true }));
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (segment.blue && /^\[/.test(segment.text)) {
|
||||
const bracketText = segment.text;
|
||||
const inner = bracketText.slice(1, -1);
|
||||
const commentId = commentMatcher ? commentMatcher.matchBracket(inner) : null;
|
||||
if (commentId != null && typeof appendRun.onCommentBracket === 'function') {
|
||||
appendRun.onCommentBracket(bracketText, Object.assign({}, style, { blue: true }), commentId);
|
||||
} else {
|
||||
appendRun(bracketText, Object.assign({}, style, { blue: true }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (segment.blue) {
|
||||
appendRun(segment.text, Object.assign({}, style, { blue: true }));
|
||||
return;
|
||||
}
|
||||
appendRun(segment.text, style);
|
||||
});
|
||||
}
|
||||
|
||||
if (lastIndex < normalized.length) {
|
||||
appendRun(normalized.slice(lastIndex), style);
|
||||
function createStyledExportTextRun(text, style, boldDefault) {
|
||||
return new TextRun({
|
||||
text,
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold: boldDefault || (style && style.bold),
|
||||
italics: style && style.italic,
|
||||
superScript: style && style.sup,
|
||||
subScript: style && style.sub,
|
||||
color: style && style.blue ? BLUE_COLOR : undefined,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
});
|
||||
}
|
||||
|
||||
function createCitationAppendRun(runs, boldDefault, commentMatcher) {
|
||||
const appendRun = function (text, style) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
runs.push(createStyledExportTextRun(text, style, boldDefault));
|
||||
};
|
||||
if (commentMatcher) {
|
||||
appendRun.onCommentBracket = function (bracketText, style, commentId) {
|
||||
runs.push(new CommentRangeStart(commentId));
|
||||
runs.push(createStyledExportTextRun(bracketText, style, boldDefault));
|
||||
runs.push(new CommentRangeEnd(commentId));
|
||||
runs.push(
|
||||
new TextRun({
|
||||
children: [new CommentReference(commentId)]
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
return appendRun;
|
||||
}
|
||||
|
||||
function htmlToTextRuns(html, options) {
|
||||
const { bold = false } = options || {};
|
||||
const opts = options || {};
|
||||
const bold = opts.bold === true;
|
||||
const citationComments = opts.citationComments || null;
|
||||
const commentMatcher =
|
||||
citationComments && citationComments.length ? createCitationCommentMatcher(citationComments) : null;
|
||||
const runs = [];
|
||||
const raw = String(html || '');
|
||||
|
||||
@@ -474,24 +563,13 @@ function htmlToTextRuns(html, options) {
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
const plainRuns = [];
|
||||
appendRunsForCitationText(htmlToPlainText(raw), { bold, italics: false, sup: false, sub: false, blue: false }, function (
|
||||
text,
|
||||
style
|
||||
) {
|
||||
plainRuns.push(
|
||||
new TextRun({
|
||||
text,
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold: bold || style.bold,
|
||||
italics: style.italic,
|
||||
superScript: style.sup,
|
||||
subScript: style.sub,
|
||||
color: style.blue ? BLUE_COLOR : undefined,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
);
|
||||
});
|
||||
const appendRun = createCitationAppendRun(plainRuns, bold, commentMatcher);
|
||||
appendRunsForReferenceHighlightText(
|
||||
htmlToPlainText(raw),
|
||||
{ bold, italics: false, sup: false, sub: false, blue: false },
|
||||
appendRun,
|
||||
commentMatcher
|
||||
);
|
||||
if (plainRuns.length) {
|
||||
return plainRuns;
|
||||
}
|
||||
@@ -508,32 +586,15 @@ function htmlToTextRuns(html, options) {
|
||||
|
||||
const root = document.createElement('div');
|
||||
root.innerHTML = raw;
|
||||
|
||||
function appendRun(text, style) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
runs.push(
|
||||
new TextRun({
|
||||
text,
|
||||
font: FONT_NAME,
|
||||
size: TABLE_FONT_SIZE,
|
||||
bold: bold || style.bold,
|
||||
italics: style.italic,
|
||||
superScript: style.sup,
|
||||
subScript: style.sub,
|
||||
color: style.blue ? BLUE_COLOR : undefined,
|
||||
kern: FONT_KERN_MIN_1PT
|
||||
})
|
||||
);
|
||||
}
|
||||
const appendRun = createCitationAppendRun(runs, bold, commentMatcher);
|
||||
|
||||
function walk(node, style) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
appendRunsForCitationText(
|
||||
appendRunsForReferenceHighlightText(
|
||||
decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' '),
|
||||
style,
|
||||
appendRun
|
||||
appendRun,
|
||||
commentMatcher
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -596,6 +657,10 @@ function htmlToTextRuns(html, options) {
|
||||
return runs;
|
||||
}
|
||||
|
||||
function htmlToTextRunsWithCitationComments(html, options, citationComments) {
|
||||
return htmlToTextRuns(html, Object.assign({}, options || {}, { citationComments: citationComments || [] }));
|
||||
}
|
||||
|
||||
/** 表头:相邻片段之间补空格,再规范为“一词一空” */
|
||||
function formatHeaderWordSpacing(html) {
|
||||
const raw = String(html || '');
|
||||
@@ -857,7 +922,7 @@ function buildTableWordChildren(processedItem, options) {
|
||||
return children;
|
||||
}
|
||||
|
||||
/** 稿件导出:标题 + 表格 + 表注均为单栏(分节符在标题前 / 表前,双栏恢复在表注后 / 表后) */
|
||||
/** 稿件导出:标题 + 表格 + 表注均为单栏(与图片块相同,由 exportManuscriptWord 分节) */
|
||||
export function buildTableWordManuscriptParts(processedItem, options) {
|
||||
const blockChildren = buildTableWordChildren(processedItem, options);
|
||||
const tableIndex = blockChildren.findIndex(function (child) {
|
||||
@@ -947,6 +1012,7 @@ export {
|
||||
FONT_NAME,
|
||||
htmlToPlainText,
|
||||
htmlToTextRuns,
|
||||
htmlToTextRunsWithCitationComments,
|
||||
PAGE_MARGINS,
|
||||
patchBodyTableCaptionCenter,
|
||||
splitHtmlSegments,
|
||||
|
||||
289
src/utils/manuscriptMediaReferences.js
Normal file
289
src/utils/manuscriptMediaReferences.js
Normal file
@@ -0,0 +1,289 @@
|
||||
/** 正文图表引用:Fig. 1 / Figure 1 / Figures 10 and 11 / Table 1 / Tables 2–4 等 */
|
||||
|
||||
const MEDIA_NUMBER_LIST = '(\\d+(?:\\s*(?:,|and|&|to|–|—|-)\\s*\\d+)*)';
|
||||
|
||||
export const FIGURE_REFERENCE_PATTERN = new RegExp(
|
||||
'\\bFig(?:s?\\.)?(?:ure)?s?\\s+' + MEDIA_NUMBER_LIST,
|
||||
'gi'
|
||||
);
|
||||
|
||||
export const TABLE_REFERENCE_PATTERN = new RegExp('\\bTables?\\s+' + MEDIA_NUMBER_LIST, 'gi');
|
||||
|
||||
const CITATION_BRACKET_PATTERN = /\[(\d+(?:–\d+)?(?:, ?\d+(?:–\d+)?)*)\]/g;
|
||||
|
||||
function isOffsetInsideBlueTag(fullString, offset) {
|
||||
const before = String(fullString || '').slice(0, offset);
|
||||
const open = before.lastIndexOf('<blue>');
|
||||
if (open < 0) {
|
||||
return false;
|
||||
}
|
||||
const close = before.lastIndexOf('</blue>');
|
||||
return open > close;
|
||||
}
|
||||
|
||||
function wrapRegexMatchesInBlue(html, pattern) {
|
||||
const source = String(html || '');
|
||||
if (!source) {
|
||||
return source;
|
||||
}
|
||||
const re = new RegExp(pattern.source, pattern.flags);
|
||||
return source.replace(re, function (match, _numbers, offset, fullString) {
|
||||
if (isOffsetInsideBlueTag(fullString, offset)) {
|
||||
return match;
|
||||
}
|
||||
const prefix = fullString.substring(offset - 6, offset);
|
||||
const suffix = fullString.substring(offset + match.length, offset + match.length + 7);
|
||||
if (prefix === '<blue>' && suffix === '</blue>') {
|
||||
return match;
|
||||
}
|
||||
return '<blue>' + match + '</blue>';
|
||||
});
|
||||
}
|
||||
|
||||
export function expandMediaNumberList(text) {
|
||||
const result = [];
|
||||
String(text || '')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.split(/\s*(?:,|and|&)\s*/i)
|
||||
.forEach(function (part) {
|
||||
const token = part.trim();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const range = token.match(/^(\d+)\s*[–—-]\s*(\d+)$/);
|
||||
if (range) {
|
||||
const a = Number(range[1]);
|
||||
const b = Number(range[2]);
|
||||
const lo = Math.min(a, b);
|
||||
const hi = Math.max(a, b);
|
||||
for (let i = lo; i <= hi; i += 1) {
|
||||
result.push(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (/^\d+$/.test(token)) {
|
||||
result.push(Number(token));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function extractFigureNumbersFromText(text) {
|
||||
const nums = [];
|
||||
const re = new RegExp(FIGURE_REFERENCE_PATTERN.source, 'gi');
|
||||
let match = null;
|
||||
while ((match = re.exec(String(text || ''))) !== null) {
|
||||
expandMediaNumberList(match[1]).forEach(function (n) {
|
||||
nums.push(n);
|
||||
});
|
||||
}
|
||||
return nums;
|
||||
}
|
||||
|
||||
export function extractTableNumbersFromText(text) {
|
||||
const nums = [];
|
||||
const re = new RegExp(TABLE_REFERENCE_PATTERN.source, 'gi');
|
||||
let match = null;
|
||||
while ((match = re.exec(String(text || ''))) !== null) {
|
||||
expandMediaNumberList(match[1]).forEach(function (n) {
|
||||
nums.push(n);
|
||||
});
|
||||
}
|
||||
return nums;
|
||||
}
|
||||
|
||||
export function parseFigureNumberFromTitle(title) {
|
||||
const m = String(title || '')
|
||||
.trim()
|
||||
.match(/^Fig(?:\.|ure)?s?\.?\s*(\d+)/i);
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
|
||||
export function parseTableNumberFromTitle(title) {
|
||||
const m = String(title || '')
|
||||
.trim()
|
||||
.match(/^Table\s+(\d+)/i);
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
|
||||
export function collectFigureNumbersFromWordList(wordList) {
|
||||
const set = {};
|
||||
(wordList || []).forEach(function (item) {
|
||||
if (!item || item.type != 1) {
|
||||
return;
|
||||
}
|
||||
const title = item.image && item.image.title ? item.image.title : '';
|
||||
const n = parseFigureNumberFromTitle(title);
|
||||
if (n != null && !isNaN(n)) {
|
||||
set[n] = true;
|
||||
}
|
||||
});
|
||||
return Object.keys(set)
|
||||
.map(Number)
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
export function collectTableNumbersFromWordList(wordList) {
|
||||
const set = {};
|
||||
(wordList || []).forEach(function (item) {
|
||||
if (!item || item.type != 2) {
|
||||
return;
|
||||
}
|
||||
const title = item.table && item.table.title ? item.table.title : '';
|
||||
const n = parseTableNumberFromTitle(title);
|
||||
if (n != null && !isNaN(n)) {
|
||||
set[n] = true;
|
||||
}
|
||||
});
|
||||
return Object.keys(set)
|
||||
.map(Number)
|
||||
.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
function stripHtmlToPlain(html) {
|
||||
return String(html || '')
|
||||
.replace(/<br\s*\/?>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将正文中的参考文献标号、Figure/Figure、Table 引用包裹为 <blue>(已包裹则跳过)
|
||||
*/
|
||||
export function wrapReferenceHighlightsInBlueHtml(html) {
|
||||
let next = String(html || '');
|
||||
if (!next) {
|
||||
return next;
|
||||
}
|
||||
|
||||
next = next.replace(CITATION_BRACKET_PATTERN, function (match, content, offset, fullString) {
|
||||
if (isOffsetInsideBlueTag(fullString, offset)) {
|
||||
return match;
|
||||
}
|
||||
const prefix = fullString.substring(offset - 6, offset);
|
||||
const suffix = fullString.substring(offset + match.length, offset + match.length + 7);
|
||||
if (prefix === '<blue>' && suffix === '</blue>') {
|
||||
return match;
|
||||
}
|
||||
if (/^\d+$/.test(content) || /, ?/.test(content) || /–/.test(content)) {
|
||||
return '<blue>' + match + '</blue>';
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
next = wrapRegexMatchesInBlue(next, FIGURE_REFERENCE_PATTERN);
|
||||
next = wrapRegexMatchesInBlue(next, TABLE_REFERENCE_PATTERN);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验正文中的 Figure/Table 引用是否存在于当前稿件图表列表
|
||||
* @returns {Array<{ kind: 'figure'|'table', number: number, amId: *, snippet: string }>}
|
||||
*/
|
||||
export function validateMediaReferencesInWordList(wordList) {
|
||||
const figureNums = collectFigureNumbersFromWordList(wordList);
|
||||
const tableNums = collectTableNumbersFromWordList(wordList);
|
||||
const figureSet = {};
|
||||
const tableSet = {};
|
||||
figureNums.forEach(function (n) {
|
||||
figureSet[n] = true;
|
||||
});
|
||||
tableNums.forEach(function (n) {
|
||||
tableSet[n] = true;
|
||||
});
|
||||
|
||||
const issues = [];
|
||||
(wordList || []).forEach(function (item) {
|
||||
if (!item || item.type == 1 || item.type == 2) {
|
||||
return;
|
||||
}
|
||||
const html = String(item.content || item.text || '');
|
||||
if (!html.trim()) {
|
||||
return;
|
||||
}
|
||||
const plain = stripHtmlToPlain(html);
|
||||
const amId = item.am_id != null ? item.am_id : item.p_main_id;
|
||||
|
||||
extractFigureNumbersFromText(plain).forEach(function (num) {
|
||||
if (!figureSet[num]) {
|
||||
issues.push({
|
||||
kind: 'figure',
|
||||
number: num,
|
||||
amId: amId,
|
||||
snippet: plain.slice(0, 120)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
extractTableNumbersFromText(plain).forEach(function (num) {
|
||||
if (!tableSet[num]) {
|
||||
issues.push({
|
||||
kind: 'table',
|
||||
number: num,
|
||||
amId: amId,
|
||||
snippet: plain.slice(0, 120)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const seen = {};
|
||||
return issues.filter(function (issue) {
|
||||
const key = issue.kind + '|' + issue.number + '|' + issue.amId;
|
||||
if (seen[key]) {
|
||||
return false;
|
||||
}
|
||||
seen[key] = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** 按 token 切分文本,供 Word 导出时标蓝 */
|
||||
export function tokenizeReferenceHighlightSegments(text) {
|
||||
const normalized = String(text || '').replace(/\u00a0/g, ' ');
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const combined = new RegExp(
|
||||
'(\\[[0-9, \\-]+\\])|(' +
|
||||
FIGURE_REFERENCE_PATTERN.source +
|
||||
')|(' +
|
||||
TABLE_REFERENCE_PATTERN.source +
|
||||
')',
|
||||
'gi'
|
||||
);
|
||||
|
||||
const segments = [];
|
||||
let lastIndex = 0;
|
||||
let match = null;
|
||||
|
||||
while ((match = combined.exec(normalized)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
segments.push({
|
||||
text: normalized.slice(lastIndex, match.index),
|
||||
blue: false
|
||||
});
|
||||
}
|
||||
segments.push({
|
||||
text: match[0],
|
||||
blue: true
|
||||
});
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (lastIndex < normalized.length) {
|
||||
segments.push({
|
||||
text: normalized.slice(lastIndex),
|
||||
blue: false
|
||||
});
|
||||
}
|
||||
|
||||
return segments.length ? segments : [{ text: normalized, blue: false }];
|
||||
}
|
||||
1009
src/utils/manuscriptReferenceAnnotationReport.js
Normal file
1009
src/utils/manuscriptReferenceAnnotationReport.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -131,6 +131,7 @@ export function buildReferencesEditableHtml(references, labels) {
|
||||
const intro = L.intro || '';
|
||||
const saveTip = L.saveTip || '';
|
||||
const downloadBtn = L.downloadEdited || 'Download edited HTML';
|
||||
const copyBracketBtn = L.copyBracket || 'Copy [n] format';
|
||||
const refTitle = L.referencesTitle || 'References';
|
||||
const bulkHint = L.bulkHint || '';
|
||||
const bulkContent = buildReferencesBulkHtml(list);
|
||||
@@ -147,6 +148,7 @@ export function buildReferencesEditableHtml(references, labels) {
|
||||
'.intro{color:#606266;font-size:14px;margin-bottom:12px;white-space:pre-wrap}' +
|
||||
'.actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}' +
|
||||
'.btn{border:1px solid #409eff;background:#409eff;color:#fff;border-radius:4px;padding:8px 16px;font-size:13px;cursor:pointer}' +
|
||||
'.btn--secondary{border-color:#67c23a;background:#67c23a}' +
|
||||
'.ref-panel{background:#fff;border-radius:8px;padding:20px 24px;box-shadow:0 1px 4px rgba(0,0,0,.08);font-size:12px}' +
|
||||
'.ref-bulk{min-height:480px;padding:12px 14px;outline:none;border:1px solid #dcdfe6;border-radius:4px;line-height:1.6;white-space:pre-wrap;word-break:break-word}' +
|
||||
'.ref-bulk:focus{border-color:#409eff;box-shadow:0 0 0 2px rgba(64,158,255,.15)}' +
|
||||
@@ -157,6 +159,7 @@ export function buildReferencesEditableHtml(references, labels) {
|
||||
'<div class="intro">' + escapeHtml(intro) + '</div>' +
|
||||
'<div class="actions">' +
|
||||
'<button type="button" class="btn" id="download-edited-btn">' + escapeHtml(downloadBtn) + '</button>' +
|
||||
'<button type="button" class="btn btn--secondary" id="copy-bracket-btn">' + escapeHtml(copyBracketBtn) + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="intro" style="font-size:12px;color:#909399">' + escapeHtml(saveTip) + '</div>' +
|
||||
'</div>' +
|
||||
@@ -165,16 +168,9 @@ export function buildReferencesEditableHtml(references, labels) {
|
||||
'<div id="ref-bulk" class="ref-bulk" contenteditable="true" spellcheck="false">' +
|
||||
bulkContent +
|
||||
'</div></div></div>' +
|
||||
'<script>(function(){' +
|
||||
'function serializePage(){var clone=document.documentElement.cloneNode(true);' +
|
||||
'var btn=clone.querySelector("#download-edited-btn");if(btn){btn.remove();}' +
|
||||
'return "<!DOCTYPE html>"+clone.outerHTML;}' +
|
||||
'document.getElementById("download-edited-btn").addEventListener("click",function(){' +
|
||||
'var html=serializePage();var blob=new Blob([html],{type:"text/html;charset=utf-8"});' +
|
||||
'var url=URL.createObjectURL(blob);var a=document.createElement("a");' +
|
||||
'a.href=url;a.download=' + JSON.stringify(L.downloadFileName || 'references-edited.html') + ';' +
|
||||
'document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);});' +
|
||||
'})();<' +
|
||||
'<script>' +
|
||||
buildReferencesEditorPageScript(L) +
|
||||
'<' +
|
||||
'/script></body></html>'
|
||||
);
|
||||
}
|
||||
@@ -232,6 +228,15 @@ function splitReferenceSegmentsByBlockElements(raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const numberedRefItems = root.querySelectorAll('p.word-ref-item[data-list-num], p[data-list-num]');
|
||||
if (numberedRefItems.length > 1) {
|
||||
return Array.from(numberedRefItems)
|
||||
.map(function (node) {
|
||||
return unwrapReferenceHtmlSegment(node.innerHTML || node.textContent || '');
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const directBlocks = Array.from(root.children).filter(function (el) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
return tag === 'p' || tag === 'div';
|
||||
@@ -316,7 +321,10 @@ function inferReferenceTypeFromHtml(html) {
|
||||
if (/ISBN\s*:/i.test(raw)) {
|
||||
return 'book';
|
||||
}
|
||||
return 'journal';
|
||||
if (/doi\.org\/10\.\d{4,9}\//i.test(raw)) {
|
||||
return 'journal';
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function plainSegmentToReferenceHtml(segment) {
|
||||
@@ -341,6 +349,104 @@ function segmentToReferenceItem(segment) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Available at: 前合并为单行,冒号后换行;序号改为 [n] */
|
||||
function formatReferenceSegmentForBracketCopy(segment, index) {
|
||||
let seg = stripLeadingReferenceNumber(String(segment || '').trim());
|
||||
if (!seg) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const availableMatch = seg.match(/^([\s\S]*?)(Available at:\s*)([\s\S]*)$/i);
|
||||
if (availableMatch) {
|
||||
const before = availableMatch[1]
|
||||
.replace(/<br\s*\/?>/gi, ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const linkPart = availableMatch[3].replace(/^<br\s*\/?>/i, '').trim();
|
||||
seg = before + ' Available at:<br>' + linkPart;
|
||||
} else {
|
||||
seg = seg.replace(/<br\s*\/?>/gi, ' ').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
return '[' + (index + 1) + '] ' + seg;
|
||||
}
|
||||
|
||||
/** 将编辑框整段文献转为 [n] 编号 + Available at 换行格式 */
|
||||
export function formatReferenceBulkForBracketCopy(bulkHtml) {
|
||||
const segments = splitReferenceSegments(normalizeReferenceBulkHtml(bulkHtml));
|
||||
const items = segments
|
||||
.map(function (segment, index) {
|
||||
return formatReferenceSegmentForBracketCopy(segment, index);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (!items.length) {
|
||||
return { plain: '', html: '', items: [] };
|
||||
}
|
||||
|
||||
const htmlBlock = items.join('<br><br>');
|
||||
const plain = htmlToPlainText(htmlBlock.replace(/Available at:<br\s*\/?>/gi, 'Available at:\n')).trim();
|
||||
const htmlDoc =
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="font-family:' +
|
||||
REF_COPY_FONT +
|
||||
';font-size:12pt">' +
|
||||
items
|
||||
.map(function (item) {
|
||||
return '<p style="margin:0 0 8pt;line-height:1.5">' + item + '</p>';
|
||||
})
|
||||
.join('') +
|
||||
'</body></html>';
|
||||
|
||||
return { plain: plain, html: htmlDoc, items: items };
|
||||
}
|
||||
|
||||
function buildReferencesEditorPageScript(labels) {
|
||||
const L = labels || {};
|
||||
const copySuccess = JSON.stringify(L.copyBracketSuccess || 'Copied to clipboard');
|
||||
const copyEmpty = JSON.stringify(L.copyBracketEmpty || 'Nothing to copy');
|
||||
const copyFail = JSON.stringify(L.copyBracketFail || 'Copy failed');
|
||||
|
||||
return (
|
||||
'(function(){' +
|
||||
'function stripLeadingReferenceNumber(text){return String(text||"").replace(/^\\s*(?:<[^>]+>\\s*)*\\[\\d+\\]\\s*/i,"").replace(/^\\s*(?:<[^>]+>\\s*)*\\d+\\.\\s*/i,"").trim();}' +
|
||||
'function normalizeReferenceBulkHtml(html){return String(html||"").replace(/\\r\\n/g,"\\n").replace(/<div>\\s*<br\\s*\\/?>\\s*<\\/div>/gi,"<br><br>").replace(/<\\/div>\\s*<div[^>]*>/gi,"<br><br>").replace(/^<div[^>]*>/i,"").replace(/<\\/div>$/i,"");}' +
|
||||
'function splitReferenceSegments(body){var raw=normalizeReferenceBulkHtml(body);raw=String(raw||"").trim();if(!raw){return[];}if(/<br\\s*\\/?>\\s*<br\\s*\\/?>/i.test(raw)){return raw.split(/<br\\s*\\/?>\\s*<br\\s*\\/?>/i).map(function(p){return p.trim();}).filter(Boolean);}var normalized=raw.replace(/\\r\\n/g,"\\n").replace(/<br\\s*\\/?>/gi,"\\n");if(/\\n(?=\\d+\\.\\s)/.test(normalized)){return normalized.split(/\\n(?=\\d+\\.\\s)/).map(function(p){return p.trim();}).filter(Boolean);}if(/\\n(?=\\[\\d+\\])/.test(normalized)){return normalized.split(/\\n(?=\\[\\d+\\])/).map(function(p){return p.trim();}).filter(Boolean);}var parts=normalized.split(/\\n\\s*\\n+/);if(parts.length>1){return parts.map(function(p){return p.trim();}).filter(Boolean);}return[raw];}' +
|
||||
'function formatReferenceSegmentForBracketCopy(segment,index){var seg=stripLeadingReferenceNumber(String(segment||"").trim());if(!seg){return"";}var availableMatch=seg.match(/^([\\s\\S]*?)(Available at:\\s*)([\\s\\S]*)$/i);if(availableMatch){var before=availableMatch[1].replace(/<br\\s*\\/?>/gi," ").replace(/\\n/g," ").replace(/\\s+/g," ").trim();var linkPart=availableMatch[3].replace(/^<br\\s*\\/?>/i,"").trim();seg=before+" Available at:<br>"+linkPart;}else{seg=seg.replace(/<br\\s*\\/?>/gi," ").replace(/\\n/g," ").replace(/\\s+/g," ").trim();}return"["+(index+1)+"] "+seg;}' +
|
||||
'function htmlToPlainText(html){var node=document.createElement("div");node.innerHTML=String(html||"");return(node.textContent||node.innerText||"").replace(/\\u00a0/g," ").trim();}' +
|
||||
'function formatReferenceBulkForBracketCopy(bulkHtml){var segments=splitReferenceSegments(bulkHtml);var items=segments.map(function(seg,i){return formatReferenceSegmentForBracketCopy(seg,i);}).filter(Boolean);if(!items.length){return{plain:"",html:""};}var htmlBlock=items.join("<br><br>");var plain=htmlToPlainText(htmlBlock.replace(/Available at:<br\\s*\\/?>/gi,"Available at:\\n")).trim();var htmlDoc="<!DOCTYPE html><html><head><meta charset=\\"utf-8\\"></head><body style=\\"font-family:Charis SIL,Georgia,serif;font-size:12pt\\">"+items.map(function(item){return"<p style=\\"margin:0 0 8pt;line-height:1.5\\">"+item+"</p>";}).join("")+"</body></html>";return{plain:plain,html:htmlDoc};}' +
|
||||
'function copyRichTextToClipboard(plain,html){plain=String(plain||"").trim();html=String(html||"").trim();if(!plain){return Promise.reject(new Error("EMPTY"));}if(navigator.clipboard&&window.ClipboardItem&&html){var htmlBlob=new Blob([html],{type:"text/html"});var plainBlob=new Blob([plain],{type:"text/plain"});return navigator.clipboard.write([new ClipboardItem({"text/html":htmlBlob,"text/plain":plainBlob})]).catch(function(){return navigator.clipboard.writeText(plain);});}if(navigator.clipboard&&navigator.clipboard.writeText){return navigator.clipboard.writeText(plain);}var ta=document.createElement("textarea");ta.value=plain;document.body.appendChild(ta);ta.select();document.execCommand("copy");document.body.removeChild(ta);return Promise.resolve();}' +
|
||||
'function serializePage(){var clone=document.documentElement.cloneNode(true);var copyBtn=clone.querySelector("#copy-bracket-btn");if(copyBtn){copyBtn.remove();}var dlBtn=clone.querySelector("#download-edited-btn");if(dlBtn){dlBtn.remove();}return"<!DOCTYPE html>"+clone.outerHTML;}' +
|
||||
'document.getElementById("download-edited-btn").addEventListener("click",function(){var html=serializePage();var blob=new Blob([html],{type:"text/html;charset=utf-8"});var url=URL.createObjectURL(blob);var a=document.createElement("a");a.href=url;a.download=' +
|
||||
JSON.stringify(L.downloadFileName || 'references-edited.html') +
|
||||
';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);});' +
|
||||
'document.getElementById("copy-bracket-btn").addEventListener("click",function(){var bulk=document.getElementById("ref-bulk");var result=formatReferenceBulkForBracketCopy(bulk?bulk.innerHTML:"");if(!result.plain){alert(' +
|
||||
copyEmpty +
|
||||
');return;}copyRichTextToClipboard(result.plain,result.html).then(function(){alert(' +
|
||||
copySuccess +
|
||||
');}).catch(function(){alert(' +
|
||||
copyFail +
|
||||
');});});' +
|
||||
'})();'
|
||||
);
|
||||
}
|
||||
|
||||
/** 将上传/粘贴的参考文献 HTML 条目转为引文相关性核查用的 ref 列表 */
|
||||
export function referencesFromUploadedHtmlItems(items) {
|
||||
return (items || [])
|
||||
.map(function (item) {
|
||||
const html = item && (item.html || item.refer_frag);
|
||||
if (!html) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
refer_frag: html,
|
||||
refer_type: item.refer_type
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** 将整段参考文献内容拆分为 Word 导出条目 */
|
||||
export function parseReferencesBulkContent(content) {
|
||||
const raw = String(content || '').trim();
|
||||
@@ -433,6 +539,10 @@ export function buildReferencesHtmlLabels(translate) {
|
||||
saveTip: t('commonTable.refHtmlSaveTip'),
|
||||
downloadEdited: t('commonTable.refHtmlDownloadEdited'),
|
||||
downloadFileName: t('commonTable.refHtmlDownloadFileName'),
|
||||
copyBracket: t('commonTable.refHtmlCopyBracket'),
|
||||
copyBracketSuccess: t('commonTable.refHtmlCopyBracketSuccess'),
|
||||
copyBracketEmpty: t('commonTable.refHtmlCopyBracketEmpty'),
|
||||
copyBracketFail: t('commonTable.refHtmlCopyBracketFail'),
|
||||
bulkHint: t('commonTable.refHtmlBulkHint')
|
||||
};
|
||||
}
|
||||
|
||||
177
src/utils/manuscriptReferenceWordComments.js
Normal file
177
src/utils/manuscriptReferenceWordComments.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import { expandCitationBracket } from '@/utils/manuscriptCitationRelevance';
|
||||
import {
|
||||
dedupeRowsByParaCite,
|
||||
flattenProgressRecords
|
||||
} from '@/utils/manuscriptReferenceAnnotationReport';
|
||||
|
||||
const CITATION_BRACKET_RE = /(?:<blue>\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi;
|
||||
|
||||
function bracketContainsCiteNum(inner, citeNum) {
|
||||
const n = Number(citeNum);
|
||||
if (isNaN(n)) {
|
||||
return false;
|
||||
}
|
||||
return expandCitationBracket(inner).indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
/** 在段落 HTML 中定位第 N 次出现的引用标号括号(0-based occurrence) */
|
||||
export function findCiteMatchInHtml(html, citeNum, targetOccurrence) {
|
||||
const raw = String(html || '');
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const re = new RegExp(CITATION_BRACKET_RE.source, 'gi');
|
||||
let occurrence = 0;
|
||||
let match = null;
|
||||
|
||||
while ((match = re.exec(raw)) !== null) {
|
||||
if (bracketContainsCiteNum(match[1], citeNum)) {
|
||||
if (occurrence === targetOccurrence) {
|
||||
return { index: match.index, len: match[0].length };
|
||||
}
|
||||
occurrence += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Word 批注正文:无中英文标签,各段之间空一行 */
|
||||
export function buildRelevanceCommentText(row) {
|
||||
const parts = [];
|
||||
const reason = String(row.reason || row.combinedReason || '').trim();
|
||||
if (reason) {
|
||||
parts.push(reason);
|
||||
}
|
||||
const authorComment = String(row.authorComment || '').trim();
|
||||
if (authorComment) {
|
||||
parts.push(authorComment);
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
function resolveWordItemAmKey(item) {
|
||||
if (!item) {
|
||||
return '';
|
||||
}
|
||||
if (item.am_id != null && item.am_id !== '') {
|
||||
return String(item.am_id);
|
||||
}
|
||||
if (item.p_main_id != null && item.p_main_id !== '') {
|
||||
return String(item.p_main_id);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function findWordItemByAmKey(wordList, amKey) {
|
||||
if (!amKey) {
|
||||
return null;
|
||||
}
|
||||
return (wordList || []).find(function (item) {
|
||||
return resolveWordItemAmKey(item) === amKey;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 referenceCheckProgressAI 进度构建 Word 批注定义,以及按 am_id 分组的正文引用批注锚点。
|
||||
* 仅包含需修改项(弱相关/不相关,对应分析报告 HTML 中的黄/红标记)。
|
||||
*/
|
||||
export function buildReferenceRelevanceWordCommentAnnotations(wordList, progressList, labels, existingDefinitions) {
|
||||
const flatRecords = flattenProgressRecords(progressList);
|
||||
const issueRows = dedupeRowsByParaCite(flatRecords).filter(function (row) {
|
||||
return row && row.needModify;
|
||||
});
|
||||
|
||||
const definitions = [];
|
||||
const commentsByAmId = {};
|
||||
let nextId = 1;
|
||||
(existingDefinitions || []).forEach(function (def) {
|
||||
if (def && Number.isFinite(def.id)) {
|
||||
nextId = Math.max(nextId, def.id + 1);
|
||||
}
|
||||
});
|
||||
|
||||
issueRows.forEach(function (row) {
|
||||
const amKey = row.amId != null ? String(row.amId) : '';
|
||||
if (!amKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = findWordItemByAmKey(wordList, amKey);
|
||||
if (!item || item.type == 1 || item.type == 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = String(item.content || item.text || '');
|
||||
if (!html.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const citeOccurrence = Math.max(0, (row.citeIndex != null ? Number(row.citeIndex) : 1) - 1);
|
||||
const hit = findCiteMatchInHtml(html, row.refNo, citeOccurrence);
|
||||
if (!hit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentText = buildRelevanceCommentText(row);
|
||||
if (!commentText) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentId = nextId;
|
||||
nextId += 1;
|
||||
|
||||
definitions.push({
|
||||
id: commentId,
|
||||
author: 'Editor',
|
||||
initials: 'Ed',
|
||||
date: new Date(),
|
||||
text: commentText
|
||||
});
|
||||
|
||||
if (!commentsByAmId[amKey]) {
|
||||
commentsByAmId[amKey] = [];
|
||||
}
|
||||
commentsByAmId[amKey].push({
|
||||
refNo: row.refNo,
|
||||
occurrence: citeOccurrence,
|
||||
commentId: commentId
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
definitions: definitions,
|
||||
commentsByAmId: commentsByAmId
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeWordCommentDefinitions(baseDefinitions, extraDefinitions) {
|
||||
const base = Array.isArray(baseDefinitions) ? baseDefinitions.slice() : [];
|
||||
const extra = Array.isArray(extraDefinitions) ? extraDefinitions : [];
|
||||
if (!extra.length) {
|
||||
return base;
|
||||
}
|
||||
|
||||
let maxId = -1;
|
||||
base.forEach(function (def) {
|
||||
if (def && Number.isFinite(def.id)) {
|
||||
maxId = Math.max(maxId, def.id);
|
||||
}
|
||||
});
|
||||
extra.forEach(function (def) {
|
||||
if (!def) {
|
||||
return;
|
||||
}
|
||||
const copy = Object.assign({}, def);
|
||||
if (!Number.isFinite(copy.id) || base.some(function (existing) {
|
||||
return existing && existing.id === copy.id;
|
||||
})) {
|
||||
maxId += 1;
|
||||
copy.id = maxId;
|
||||
} else {
|
||||
maxId = Math.max(maxId, copy.id);
|
||||
}
|
||||
base.push(copy);
|
||||
});
|
||||
return base;
|
||||
}
|
||||
431
src/utils/manuscriptWordReferences.js
Normal file
431
src/utils/manuscriptWordReferences.js
Normal file
@@ -0,0 +1,431 @@
|
||||
import { htmlToPlainText } from '@/utils/manuscriptCitationRelevance';
|
||||
import mammoth from 'mammoth';
|
||||
import {
|
||||
importWordDocumentWithMath,
|
||||
isReferencesHeadingText,
|
||||
isBibliographyStartParagraph,
|
||||
annotateOrderedListItemsInHtml
|
||||
} from '@/utils/wordMathImport';
|
||||
|
||||
function normalizePlain(text) {
|
||||
return String(text || '')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function collectParagraphLikeNodes(root) {
|
||||
const nodes = [];
|
||||
const walk = function (parent) {
|
||||
Array.from(parent.childNodes || []).forEach(function (node) {
|
||||
if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
const tag = node.tagName.toLowerCase();
|
||||
if (tag === 'p' || /^h[1-6]$/.test(tag)) {
|
||||
nodes.push(node);
|
||||
return;
|
||||
}
|
||||
if (tag === 'li') {
|
||||
nodes.push(node);
|
||||
return;
|
||||
}
|
||||
if (tag === 'table') {
|
||||
nodes.push(node);
|
||||
return;
|
||||
}
|
||||
if (tag === 'div') {
|
||||
if (node.classList && node.classList.contains('wordTableHtml')) {
|
||||
nodes.push(node);
|
||||
return;
|
||||
}
|
||||
walk(node);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(root);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function findReferencesStartInNodes(nodes) {
|
||||
const list = nodes || [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const plain = normalizePlain(list[i].textContent);
|
||||
if (isReferencesHeadingText(plain)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// 纯参考文献 Word(如 reference.docx):全文为编号条目,无 References 标题
|
||||
if (isReferenceOnlyWordNodes(list)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const startScan = Math.max(0, Math.floor(list.length * 0.25));
|
||||
for (let j = startScan; j < list.length; j++) {
|
||||
const plain = normalizePlain(list[j].textContent);
|
||||
if (isBibliographyStartParagraph(plain)) {
|
||||
return j;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function nodeLooksLikeReferenceEntry(node, plain) {
|
||||
if (!plain) {
|
||||
return false;
|
||||
}
|
||||
const listNum = node.getAttribute && node.getAttribute('data-list-num');
|
||||
const isRefItem = node.classList && node.classList.contains('word-ref-item');
|
||||
return !!(listNum || isRefItem || isBibliographyStartParagraph(plain));
|
||||
}
|
||||
|
||||
/** 判断是否整篇 Word 仅含参考文献(校对后单独导出的 reference.docx) */
|
||||
function isReferenceOnlyWordNodes(nodes) {
|
||||
const list = (nodes || []).filter(function (node) {
|
||||
return normalizePlain(node.textContent).length > 0;
|
||||
});
|
||||
if (list.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstPlain = normalizePlain(list[0].textContent);
|
||||
if (!nodeLooksLikeReferenceEntry(list[0], firstPlain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let refCount = 0;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const plain = normalizePlain(list[i].textContent);
|
||||
if (nodeLooksLikeReferenceEntry(list[i], plain)) {
|
||||
refCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return refCount >= Math.max(2, Math.ceil(list.length * 0.6));
|
||||
}
|
||||
|
||||
function nodeToReferenceBulkSegment(node) {
|
||||
if (!node) return '';
|
||||
const tag = String(node.tagName || '').toLowerCase();
|
||||
const listNum = node.getAttribute && node.getAttribute('data-list-num');
|
||||
const listAttr = listNum ? ' data-list-num="' + listNum + '"' : '';
|
||||
if (tag === 'li') {
|
||||
return '<p' + listAttr + '>' + (node.innerHTML || '') + '</p>';
|
||||
}
|
||||
if (tag === 'p' || /^h[1-6]$/.test(tag)) {
|
||||
if (listNum) {
|
||||
return '<p' + listAttr + '>' + (node.innerHTML || '') + '</p>';
|
||||
}
|
||||
return node.outerHTML || '<p>' + (node.innerHTML || '') + '</p>';
|
||||
}
|
||||
return '<p>' + (node.innerHTML || node.textContent || '') + '</p>';
|
||||
}
|
||||
|
||||
function stripLeadingReferenceNumber(text) {
|
||||
return String(text || '')
|
||||
.replace(/^\s*(?:<[^>]+>\s*)*\[\d+\]\s*/i, '')
|
||||
.replace(/^\s*(?:<[^>]+>\s*)*\d+\.\s*/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseLeadingReferenceNumber(segment) {
|
||||
const raw = String(segment || '').trim();
|
||||
if (!raw) return '';
|
||||
|
||||
let match = raw.match(/^\s*(?:<[^>]+>\s*)*\[(\d+)\]\s*/i);
|
||||
if (match) return match[1];
|
||||
|
||||
match = raw.match(/^\s*(?:<[^>]+>\s*)*(\d+)\.\s*/i);
|
||||
if (match) return match[1];
|
||||
|
||||
const plain = normalizePlain(htmlToPlainText(raw));
|
||||
match = plain.match(/^\[(\d+)\]/);
|
||||
if (match) return match[1];
|
||||
match = plain.match(/^(\d+)\.\s*/);
|
||||
if (match) return match[1];
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function normalizeReferenceSegmentAvailableAt(seg) {
|
||||
const availableMatch = seg.match(/^([\s\S]*?)(Available at:\s*)([\s\S]*)$/i);
|
||||
if (availableMatch) {
|
||||
const before = availableMatch[1]
|
||||
.replace(/<br\s*\/?>/gi, ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const linkPart = availableMatch[3].replace(/^<br\s*\/?>/i, '').trim();
|
||||
return before + ' Available at:<br>' + linkPart;
|
||||
}
|
||||
return seg.replace(/<br\s*\/?>/gi, ' ').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function formatReferenceSegmentWithOriginalNumber(segment, listNumHint) {
|
||||
const rawSegment = typeof segment === 'string' ? segment : String((segment && segment.html) || '');
|
||||
const num =
|
||||
listNumHint ||
|
||||
(typeof segment === 'object' && segment && segment.listNum) ||
|
||||
parseLeadingReferenceNumber(rawSegment);
|
||||
let seg = stripLeadingReferenceNumber(rawSegment.trim());
|
||||
if (!seg) return '';
|
||||
seg = normalizeReferenceSegmentAvailableAt(seg);
|
||||
if (num) {
|
||||
return '[' + num + '] ' + seg;
|
||||
}
|
||||
return seg;
|
||||
}
|
||||
|
||||
function splitReferenceBodySegments(bulkHtml) {
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = bulkHtml;
|
||||
const blocks = [];
|
||||
container.querySelectorAll('p, li').forEach(function (node) {
|
||||
const html = (node.innerHTML || '').trim();
|
||||
if (!html) return;
|
||||
blocks.push({
|
||||
html: html,
|
||||
listNum: node.getAttribute('data-list-num') || ''
|
||||
});
|
||||
});
|
||||
if (blocks.length) return blocks;
|
||||
|
||||
const normalized = bulkHtml.replace(/<br\s*\/?>/gi, '\n');
|
||||
if (/\n(?=\d+\.\s)/.test(normalized)) {
|
||||
return normalized
|
||||
.split(/\n(?=\d+\.\s)/)
|
||||
.map(function (part) {
|
||||
return { html: part.trim(), listNum: parseLeadingReferenceNumber(part.trim()) };
|
||||
})
|
||||
.filter(function (item) {
|
||||
return item.html;
|
||||
});
|
||||
}
|
||||
if (/\n(?=\[\d+\])/.test(normalized)) {
|
||||
return normalized
|
||||
.split(/\n(?=\[\d+\])/)
|
||||
.map(function (part) {
|
||||
return { html: part.trim(), listNum: parseLeadingReferenceNumber(part.trim()) };
|
||||
})
|
||||
.filter(function (item) {
|
||||
return item.html;
|
||||
});
|
||||
}
|
||||
return bulkHtml.trim() ? [{ html: bulkHtml.trim(), listNum: '' }] : [];
|
||||
}
|
||||
|
||||
function formatExtractedReferences(nodes, startIdx) {
|
||||
const sectionNodes = nodes.slice(startIdx);
|
||||
if (!sectionNodes.length) {
|
||||
return { html: '', plainText: '', count: 0 };
|
||||
}
|
||||
|
||||
let bodyStart = 0;
|
||||
const firstPlain = normalizePlain(sectionNodes[0].textContent);
|
||||
if (isReferencesHeadingText(firstPlain)) {
|
||||
bodyStart = 1;
|
||||
}
|
||||
|
||||
const headingHtml = bodyStart > 0 ? nodeToReferenceBulkSegment(sectionNodes[0]) : '';
|
||||
const bulkHtml = sectionNodes
|
||||
.slice(bodyStart)
|
||||
.map(nodeToReferenceBulkSegment)
|
||||
.join('');
|
||||
|
||||
if (!bulkHtml.trim()) {
|
||||
const headingPlain = headingHtml ? htmlToPlainText(headingHtml).trim() : '';
|
||||
return {
|
||||
html: headingHtml,
|
||||
plainText: headingPlain,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
|
||||
const items = splitReferenceBodySegments(bulkHtml)
|
||||
.map(function (block) {
|
||||
return formatReferenceSegmentWithOriginalNumber(block.html, block.listNum);
|
||||
})
|
||||
.filter(Boolean);
|
||||
const bodyHtml = items
|
||||
.map(function (item) {
|
||||
return '<p class="word-ref-p">' + item + '</p>';
|
||||
})
|
||||
.join('');
|
||||
|
||||
const headingPlain = headingHtml ? htmlToPlainText(headingHtml).trim() : '';
|
||||
const plainParts = [];
|
||||
if (headingPlain) plainParts.push(headingPlain);
|
||||
if (items.length) {
|
||||
plainParts.push(
|
||||
items
|
||||
.map(function (item) {
|
||||
return htmlToPlainText(item).replace(/\s+/g, ' ').trim();
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
html: headingHtml + bodyHtml,
|
||||
plainText: plainParts.join('\n\n'),
|
||||
count: items.length
|
||||
};
|
||||
}
|
||||
|
||||
/** 从 Word 导入 HTML 中提取 References 段落(含标题) */
|
||||
export function extractReferencesSectionFromWordHtml(html) {
|
||||
if (!html || typeof document === 'undefined') {
|
||||
return { found: false, html: '', plainText: '', count: 0, startIndex: -1, manuscriptUrl: '' };
|
||||
}
|
||||
|
||||
html = annotateOrderedListItemsInHtml(html);
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
const nodes = collectParagraphLikeNodes(container);
|
||||
const startIdx = findReferencesStartInNodes(nodes);
|
||||
if (startIdx < 0) {
|
||||
return { found: false, html: '', plainText: '', count: 0, startIndex: -1, manuscriptUrl: '' };
|
||||
}
|
||||
|
||||
const formatted = formatExtractedReferences(nodes, startIdx);
|
||||
|
||||
return {
|
||||
found: true,
|
||||
html: formatted.html,
|
||||
plainText: formatted.plainText,
|
||||
count: formatted.count || 0,
|
||||
startIndex: startIdx,
|
||||
manuscriptUrl: ''
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveManuscriptFileUrl(path, mediaUrl, baseUrl) {
|
||||
const raw = String(path || '').trim();
|
||||
if (!raw) return '';
|
||||
|
||||
if (/^https?:\/\//i.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const media = String(mediaUrl || '');
|
||||
if (media) {
|
||||
return media + raw.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
const normalized = raw.replace(/^\/+/, '');
|
||||
if (normalized.indexOf('public/') === 0) {
|
||||
const root = String(baseUrl || '').replace(/\/$/, '');
|
||||
return root ? root + '/' + normalized : '/' + normalized;
|
||||
}
|
||||
|
||||
const root = String(baseUrl || '').replace(/\/$/, '');
|
||||
return root ? root + '/public/' + normalized : '/public/' + normalized;
|
||||
}
|
||||
|
||||
/** fetch 下载用同源路径,避免 mediaUrl 跨域 */
|
||||
export function resolveManuscriptFetchUrl(path, mediaUrl) {
|
||||
const raw = String(path || '').trim();
|
||||
if (!raw) return '';
|
||||
|
||||
let relative = raw;
|
||||
if (/^https?:\/\//i.test(raw)) {
|
||||
const media = String(mediaUrl || '');
|
||||
if (media && raw.indexOf(media) === 0) {
|
||||
relative = raw.slice(media.length);
|
||||
} else {
|
||||
const publicIdx = raw.indexOf('/public/');
|
||||
if (publicIdx >= 0) {
|
||||
relative = raw.slice(publicIdx + '/public/'.length);
|
||||
} else {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relative = relative.replace(/^\/+/, '');
|
||||
if (relative.indexOf('public/') === 0) {
|
||||
relative = relative.slice('public/'.length);
|
||||
}
|
||||
return '/public/' + relative;
|
||||
}
|
||||
|
||||
async function downloadManuscriptDocx(manuscriptPath, mediaUrl, baseUrl) {
|
||||
const fileUrl = resolveManuscriptFetchUrl(manuscriptPath, mediaUrl);
|
||||
const displayUrl = resolveManuscriptFileUrl(manuscriptPath, mediaUrl, baseUrl);
|
||||
if (!fileUrl) {
|
||||
throw new Error('NO_MANUSCRIPT_URL');
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(fileUrl);
|
||||
} catch (error) {
|
||||
const err = new Error('FETCH_DOCX_CORS');
|
||||
err.cause = error;
|
||||
err.fileUrl = fileUrl;
|
||||
throw err;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const err = new Error('FETCH_DOCX_FAILED');
|
||||
err.status = response.status;
|
||||
err.fileUrl = fileUrl;
|
||||
throw err;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const fileName = manuscriptPath.split('/').pop() || 'manuscript.docx';
|
||||
return {
|
||||
file: new File([blob], fileName, {
|
||||
type: blob.type || 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
}),
|
||||
fileUrl: displayUrl || fileUrl
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchArticleManuscriptPath(apiClient, articleId) {
|
||||
const res = await apiClient.post('api/Article/getArticleDetail', {
|
||||
articleId: articleId,
|
||||
human: 'editor'
|
||||
});
|
||||
const article = (res && res.article) || {};
|
||||
return String(article.manuscript_url || '').trim();
|
||||
}
|
||||
|
||||
async function importWordHtmlWithMammoth(file) {
|
||||
if (!file || !file.arrayBuffer) return '';
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const result = await mammoth.convertToHtml({ arrayBuffer });
|
||||
return result.value || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Word 稿件识别并提取 References。
|
||||
* 优先使用 options.manuscriptPath / gridData(如 getFilesForArticle 的 file_url),
|
||||
* 否则回退 getArticleDetail.manuscript_url。
|
||||
*/
|
||||
export async function fetchWordReferencesSection(apiClient, options) {
|
||||
const opts = options || {};
|
||||
let manuscriptPath = String(opts.manuscriptPath || opts.gridData || opts.fileUrl || '').trim();
|
||||
const useGridDataOnly = !!opts.useGridDataOnly;
|
||||
|
||||
if (!manuscriptPath) {
|
||||
if (useGridDataOnly || !apiClient || opts.articleId == null || opts.articleId === '') {
|
||||
const err = new Error('NO_MANUSCRIPT_FILE');
|
||||
throw err;
|
||||
}
|
||||
manuscriptPath = await fetchArticleManuscriptPath(apiClient, opts.articleId);
|
||||
}
|
||||
|
||||
if (!manuscriptPath) {
|
||||
const err = new Error('NO_MANUSCRIPT_FILE');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const downloaded = await downloadManuscriptDocx(manuscriptPath, opts.mediaUrl, opts.baseUrl);
|
||||
let wordHtml = await importWordDocumentWithMath(downloaded.file, { keepReferences: true });
|
||||
let extracted = extractReferencesSectionFromWordHtml(wordHtml);
|
||||
if (!extracted.found) {
|
||||
const mammothHtml = await importWordHtmlWithMammoth(downloaded.file);
|
||||
extracted = extractReferencesSectionFromWordHtml(mammothHtml);
|
||||
}
|
||||
extracted.manuscriptUrl = downloaded.fileUrl;
|
||||
return extracted;
|
||||
}
|
||||
542
src/utils/parseReferenceFields.js
Normal file
542
src/utils/parseReferenceFields.js
Normal file
@@ -0,0 +1,542 @@
|
||||
import { htmlToPlainText } from '@/utils/manuscriptCitationRelevance';
|
||||
import { parseBookReferenceContent } from '@/utils/parseBookReference';
|
||||
|
||||
/** 导入时去掉 (Chinese) 并规整空格 */
|
||||
export function normalizeImportedReferenceText(text) {
|
||||
return String(text || '')
|
||||
.replace(/\(\s*Chinese\s*\)/gi, '')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.replace(/\s+\./g, '.')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sanitizeImportedReferenceHtml(html) {
|
||||
return String(html || '')
|
||||
.replace(/\(\s*Chinese\s*\)/gi, '')
|
||||
.replace(/\(\s*Chinese\s*\)\s*\./gi, '.')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.replace(/\s+\./g, '.')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function normalizeImportedReferenceFields(fields) {
|
||||
if (!fields) return fields;
|
||||
const keys = ['author', 'title', 'joura', 'dateno', 'doilink', 'doi', 'isbn', 'content'];
|
||||
const out = Object.assign({}, fields);
|
||||
keys.forEach(function (key) {
|
||||
if (out[key] != null && out[key] !== '') {
|
||||
out[key] = normalizeImportedReferenceText(out[key]);
|
||||
}
|
||||
});
|
||||
return swapMisassignedYearAndSource(out);
|
||||
}
|
||||
|
||||
/** 年份误写入 joura、来源误写入 dateno 时纠正(web/news 引用常见) */
|
||||
function swapMisassignedYearAndSource(fields) {
|
||||
if (!fields) return fields;
|
||||
const joura = String(fields.joura || '').trim();
|
||||
const dateno = String(fields.dateno || '').trim();
|
||||
if (
|
||||
looksLikePlainYear(joura) &&
|
||||
dateno &&
|
||||
!looksLikePlainYear(dateno) &&
|
||||
!looksLikeDateno(dateno)
|
||||
) {
|
||||
return Object.assign({}, fields, {
|
||||
joura: dateno,
|
||||
dateno: normalizePlainYearDateno(joura)
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function splitAtFirstPeriod(text) {
|
||||
const raw = String(text || '').trim();
|
||||
if (!raw) return { before: '', after: '' };
|
||||
const match = raw.match(/\s*\.\s+/);
|
||||
if (!match || match.index == null) {
|
||||
return { before: raw, after: '' };
|
||||
}
|
||||
return {
|
||||
before: raw.slice(0, match.index).trim(),
|
||||
after: raw.slice(match.index + match[0].length).trim()
|
||||
};
|
||||
}
|
||||
|
||||
function withAuthorPeriod(name) {
|
||||
const t = String(name || '').trim();
|
||||
if (!t) return '';
|
||||
return t.endsWith('.') ? t : t + '.';
|
||||
}
|
||||
|
||||
function trimField(text) {
|
||||
return normalizeImportedReferenceText(
|
||||
String(text || '')
|
||||
.replace(/\s+\.\s*$/, '')
|
||||
.replace(/\.\s*$/, '')
|
||||
);
|
||||
}
|
||||
|
||||
function stripLeadingReferenceNumber(text) {
|
||||
return String(text || '')
|
||||
.replace(/^\s*(?:<[^>]+>\s*)*\[\d+\]\s*/i, '')
|
||||
.replace(/^\s*(?:<[^>]+>\s*)*\d+\.\s*/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function fixMissingSpaceAfterPeriod(plain) {
|
||||
return String(plain || '').replace(/([A-Za-z)\]])\.([A-Za-z])/g, '$1. $2');
|
||||
}
|
||||
|
||||
function looksLikeDateno(text) {
|
||||
const value = String(text || '').trim();
|
||||
return /^\d{4}[;:]\d+(?:\([^)]*\))?(?::[\w\-–—eE.]+(?:[\-–—][\w\-–—eE.]+)?)?\.?$/.test(value);
|
||||
}
|
||||
|
||||
function looksLikePlainYear(text) {
|
||||
return /^\d{4}\.?$/.test(String(text || '').trim());
|
||||
}
|
||||
|
||||
function normalizePlainYearDateno(text) {
|
||||
return String(text || '').trim().replace(/\.$/, '');
|
||||
}
|
||||
|
||||
/** 提取末尾纯年份(如 web/news 引用:Source. 2025.) */
|
||||
function extractPlainYearFromTail(plain) {
|
||||
const text = String(plain || '').trim();
|
||||
const match = text.match(/(?:^|\.\s+)(\d{4})\.\s*$/);
|
||||
if (!match) {
|
||||
return { body: text, dateno: '' };
|
||||
}
|
||||
const year = match[1];
|
||||
let body = text.slice(0, match.index).trim().replace(/\.\s*$/, '');
|
||||
return {
|
||||
body: body,
|
||||
dateno: normalizePlainYearDateno(year)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDatenoField(dateno) {
|
||||
return normalizeImportedReferenceText(String(dateno || '').trim()).replace(/^(\d{4}):/, '$1;');
|
||||
}
|
||||
|
||||
function extractDoilink(html) {
|
||||
const raw = String(html || '');
|
||||
let body = raw.trim();
|
||||
let doilink = '';
|
||||
|
||||
const availMatch = raw.match(/Available\s+at:\s*([\s\S]*)$/i);
|
||||
if (availMatch) {
|
||||
const tail = availMatch[1];
|
||||
const anchorMatch = tail.match(/<a\b[^>]*\bhref=["']([^"']+)["'][^>]*>/i);
|
||||
doilink = anchorMatch
|
||||
? anchorMatch[1].trim()
|
||||
: htmlToPlainText(tail).replace(/\s+/g, ' ').trim();
|
||||
body = raw.slice(0, availMatch.index).trim();
|
||||
} else {
|
||||
const anchorMatch = raw.match(/<a\b[^>]*\bhref=["']([^"']+)["'][^>]*>/i);
|
||||
if (anchorMatch) {
|
||||
doilink = anchorMatch[1].trim();
|
||||
body = raw.replace(anchorMatch[0], '').trim();
|
||||
} else {
|
||||
const urlMatch = raw.match(/(https?:\/\/(?:dx\.)?doi\.org\/[^\s<]+|10\.\d{4,9}\/[^\s<]+)\s*$/i);
|
||||
if (urlMatch) {
|
||||
doilink = htmlToPlainText(urlMatch[1]).replace(/\s+/g, ' ').trim();
|
||||
body = raw.slice(0, urlMatch.index).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doilink && !/^https?:\/\//i.test(doilink)) {
|
||||
doilink = 'https://doi.org/' + doilink.replace(/^https?:\/\/doi\.org\//i, '');
|
||||
}
|
||||
|
||||
return { body: body, doilink: doilink };
|
||||
}
|
||||
|
||||
function mergeAdjacentItalicHtml(html) {
|
||||
let result = String(html || '');
|
||||
let prev = '';
|
||||
while (prev !== result) {
|
||||
prev = result;
|
||||
result = result.replace(/<\/(?:i|em)>\s*<(?:i|em)(?:\s[^>]*)?>/gi, '');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractAllItalicTexts(html) {
|
||||
const merged = mergeAdjacentItalicHtml(html);
|
||||
const texts = [];
|
||||
const re = /<(?:i|em)\b[^>]*>([\s\S]*?)<\/(?:i|em)>/gi;
|
||||
let match;
|
||||
while ((match = re.exec(merged)) !== null) {
|
||||
const text = htmlToPlainText(match[1]).replace(/\s+/g, ' ').trim();
|
||||
if (text) {
|
||||
texts.push({ text: text, index: match.index, length: match[0].length });
|
||||
}
|
||||
}
|
||||
return { merged: merged, texts: texts };
|
||||
}
|
||||
|
||||
function cleanJournalName(name) {
|
||||
return trimField(String(name || ''))
|
||||
.replace(/\s*\.?\s*-+\.?\s*$/g, '')
|
||||
.replace(/\s*\.\s*$/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractJournalFromPlainBeforeDateno(plain) {
|
||||
const text = String(plain || '').trim();
|
||||
const patterns = [
|
||||
/\.\s*([^.\d][^.]*?)\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?:)/,
|
||||
/\.\s*([^.\d][^.]+?)\s+(\d{4}[;:]\d+(?:\([^)]*\))?:)/,
|
||||
/\s([A-Za-z][A-Za-z0-9 .&'-]{1,80})\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?:)/,
|
||||
/\.\s*([^.\d][^.]*?)\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?)\.?\s*$/,
|
||||
/\s([A-Za-z][A-Za-z0-9 .&'-]{1,80})\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?)\.?\s*$/
|
||||
];
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
const match = text.match(patterns[i]);
|
||||
if (match && match[1] && !looksLikeDateno(match[1])) {
|
||||
return cleanJournalName(match[1]);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractJouraFromHead(head) {
|
||||
const parts = String(head || '')
|
||||
.split(/\.\s+/)
|
||||
.map(function (part) {
|
||||
return part.trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
return { joura: '', rest: String(head || '').trim(), plainYear: '' };
|
||||
}
|
||||
|
||||
while (parts.length && looksLikeDateno(parts[parts.length - 1])) {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
let plainYear = '';
|
||||
if (parts.length && looksLikePlainYear(parts[parts.length - 1])) {
|
||||
plainYear = normalizePlainYearDateno(parts.pop());
|
||||
}
|
||||
|
||||
if (parts.length < 2) {
|
||||
return { joura: '', rest: parts.join('. '), plainYear: plainYear };
|
||||
}
|
||||
|
||||
let joura = cleanJournalName(parts.pop());
|
||||
if (!joura || looksLikeDateno(joura) || looksLikePlainYear(joura)) {
|
||||
if (joura && looksLikePlainYear(joura) && !plainYear) {
|
||||
plainYear = normalizePlainYearDateno(joura);
|
||||
}
|
||||
return { joura: '', rest: parts.join('. '), plainYear: plainYear };
|
||||
}
|
||||
return { joura: joura, rest: parts.join('. '), plainYear: plainYear };
|
||||
}
|
||||
|
||||
function extractItalicJournal(html) {
|
||||
const raw = String(html || '');
|
||||
const italicInfo = extractAllItalicTexts(raw);
|
||||
if (!italicInfo.texts.length) {
|
||||
return { body: raw.trim(), joura: '' };
|
||||
}
|
||||
|
||||
const plainForPos = htmlToPlainText(italicInfo.merged).replace(/\s+/g, ' ').trim();
|
||||
const datenoMatch = plainForPos.match(/\d{4}[;:]\d+(?:\([^)]*\))?:/);
|
||||
const datenoPos = datenoMatch ? datenoMatch.index : plainForPos.length;
|
||||
|
||||
let joura = '';
|
||||
for (let i = italicInfo.texts.length - 1; i >= 0; i--) {
|
||||
const item = italicInfo.texts[i];
|
||||
const pos = htmlToPlainText(italicInfo.merged.slice(0, item.index)).replace(/\s+/g, ' ').length;
|
||||
if (pos <= datenoPos && item.text.length <= 120) {
|
||||
joura = item.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!joura) {
|
||||
joura = italicInfo.texts[italicInfo.texts.length - 1].text;
|
||||
}
|
||||
joura = cleanJournalName(joura);
|
||||
|
||||
const body = italicInfo.merged
|
||||
.replace(/<(?:i|em)\b[^>]*>([\s\S]*?)<\/(?:i|em)>/gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return { body: body, joura: joura };
|
||||
}
|
||||
|
||||
function extractDateno(plain) {
|
||||
const text = String(plain || '').trim();
|
||||
const patterns = [
|
||||
/(\d{4}[;:]\d+(?:\([^)]*\))?:[\w\-–—eE.]+(?:[\-–—][\w\-–—eE.]+)?)\.?\s*$/,
|
||||
/(\d{4}[;:]\d+(?:\([^)]*\))?:[\w\-–—eE]+)\.?\s*$/,
|
||||
/(\d{4}[;:]\d+(?:\([^)]*\))?)\.?\s*$/
|
||||
];
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
const match = text.match(patterns[i]);
|
||||
if (match) {
|
||||
return {
|
||||
body: text.slice(0, match.index).trim().replace(/\.\s*$/, ''),
|
||||
dateno: normalizeDatenoField(match[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
return { body: text, dateno: '' };
|
||||
}
|
||||
|
||||
function parseJournalReferenceContent(content) {
|
||||
let html = stripLeadingReferenceNumber(String(content || '').trim());
|
||||
if (!html) return null;
|
||||
|
||||
const linkPart = extractDoilink(html);
|
||||
html = linkPart.body;
|
||||
const italicPart = extractItalicJournal(html);
|
||||
html = italicPart.body;
|
||||
|
||||
let plain = fixMissingSpaceAfterPeriod(normalizeImportedReferenceText(htmlToPlainText(html)));
|
||||
plain = plain.replace(/\.\s*$/, '');
|
||||
|
||||
const datenoPart = extractDateno(plain);
|
||||
let dateno = datenoPart.dateno;
|
||||
let head = datenoPart.body;
|
||||
|
||||
if (!dateno) {
|
||||
const plainYearPart = extractPlainYearFromTail(head || plain);
|
||||
if (plainYearPart.dateno) {
|
||||
dateno = plainYearPart.dateno;
|
||||
head = plainYearPart.body;
|
||||
}
|
||||
}
|
||||
|
||||
let joura = cleanJournalName(italicPart.joura || '');
|
||||
if (looksLikeDateno(joura) || looksLikePlainYear(joura)) {
|
||||
if (looksLikePlainYear(joura) && !dateno) {
|
||||
dateno = normalizePlainYearDateno(joura);
|
||||
}
|
||||
joura = '';
|
||||
}
|
||||
|
||||
if (!joura) {
|
||||
joura = extractJournalFromPlainBeforeDateno(head || plain);
|
||||
}
|
||||
|
||||
const jouraFromHead = extractJouraFromHead(head);
|
||||
if (!joura && jouraFromHead.joura) {
|
||||
joura = jouraFromHead.joura;
|
||||
head = jouraFromHead.rest;
|
||||
} else if (joura && head) {
|
||||
const jouraEsc = joura.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
head = head
|
||||
.replace(new RegExp('\\.\s*' + jouraEsc + '\.?\s*$', 'i'), '')
|
||||
.replace(/\s*\.?\s*-+\.?\s*$/g, '')
|
||||
.trim();
|
||||
}
|
||||
if (!dateno && jouraFromHead.plainYear) {
|
||||
dateno = jouraFromHead.plainYear;
|
||||
}
|
||||
|
||||
if (looksLikeDateno(joura) && !dateno) {
|
||||
dateno = normalizeDatenoField(joura);
|
||||
const recovered = extractJouraFromHead(head);
|
||||
joura = extractJournalFromPlainBeforeDateno(head) || recovered.joura;
|
||||
head = recovered.rest;
|
||||
if (!dateno && recovered.plainYear) {
|
||||
dateno = recovered.plainYear;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dateno) {
|
||||
const trailingYear = extractPlainYearFromTail(head);
|
||||
if (trailingYear.dateno) {
|
||||
dateno = trailingYear.dateno;
|
||||
head = trailingYear.body;
|
||||
}
|
||||
}
|
||||
|
||||
joura = cleanJournalName(joura);
|
||||
|
||||
const authorSplit = splitAtFirstPeriod(head);
|
||||
const author = withAuthorPeriod(authorSplit.before);
|
||||
const title = trimField(authorSplit.after);
|
||||
|
||||
if (!author && !title && !joura && !dateno && !linkPart.doilink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
refer_type: 'journal',
|
||||
author,
|
||||
title,
|
||||
joura,
|
||||
dateno: dateno,
|
||||
doilink: linkPart.doilink,
|
||||
doi: linkPart.doilink ? linkPart.doilink.replace(/^https?:\/\/doi\.org\//i, '').toLowerCase() : '',
|
||||
isbn: '',
|
||||
content: ''
|
||||
};
|
||||
}
|
||||
|
||||
function hasReferenceDoiLink(fields, rawHtml) {
|
||||
const link = String((fields && (fields.doilink || fields.doi)) || '').trim();
|
||||
if (link) {
|
||||
if (/^https?:\/\/(?:dx\.)?doi\.org\//i.test(link)) {
|
||||
return true;
|
||||
}
|
||||
if (/^doi:/i.test(link)) {
|
||||
return true;
|
||||
}
|
||||
if (/^10\.\d{4,9}\//i.test(link)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return /doi\.org\/10\.\d{4,9}\//i.test(String(rawHtml || ''));
|
||||
}
|
||||
|
||||
function hasReferenceIsbn(fields, rawHtml) {
|
||||
const isbn = String((fields && fields.isbn) || '').trim();
|
||||
if (isbn) {
|
||||
return true;
|
||||
}
|
||||
return /ISBN\s*:/i.test(String(rawHtml || ''));
|
||||
}
|
||||
|
||||
function buildReferenceContentFromPartialFields(fields) {
|
||||
const parts = [];
|
||||
if (fields && fields.author) {
|
||||
parts.push(String(fields.author).trim());
|
||||
}
|
||||
if (fields && fields.title) {
|
||||
parts.push(String(fields.title).trim());
|
||||
}
|
||||
if (fields && fields.joura) {
|
||||
parts.push(String(fields.joura).trim());
|
||||
}
|
||||
if (fields && fields.dateno) {
|
||||
parts.push(String(fields.dateno).trim());
|
||||
}
|
||||
|
||||
let content = parts
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+\./g, '.')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
const link = String((fields && fields.doilink) || '').trim();
|
||||
if (link && !/Available at:/i.test(content)) {
|
||||
content = content + '. Available at: ' + link;
|
||||
}
|
||||
|
||||
return normalizeImportedReferenceText(content);
|
||||
}
|
||||
|
||||
/** 无 DOI 且无 ISBN 的条目归为 other;有 DOI 为 journal,有 ISBN 为 book */
|
||||
export function finalizeReferenceFields(fields, html) {
|
||||
const base = Object.assign({}, fields || {});
|
||||
const rawHtml = String(html || '').trim();
|
||||
const plainFromHtml = normalizeImportedReferenceText(htmlToPlainText(rawHtml));
|
||||
|
||||
if (hasReferenceIsbn(base, rawHtml)) {
|
||||
return normalizeImportedReferenceFields(
|
||||
Object.assign({}, base, {
|
||||
refer_type: 'book'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (hasReferenceDoiLink(base, rawHtml)) {
|
||||
return normalizeImportedReferenceFields(
|
||||
Object.assign({}, base, {
|
||||
refer_type: 'journal'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const content =
|
||||
base.content ||
|
||||
plainFromHtml ||
|
||||
buildReferenceContentFromPartialFields(base);
|
||||
|
||||
return normalizeImportedReferenceFields({
|
||||
refer_type: 'other',
|
||||
author: '',
|
||||
title: '',
|
||||
joura: '',
|
||||
dateno: '',
|
||||
doilink: base.doilink || '',
|
||||
doi: base.doi || '',
|
||||
isbn: '',
|
||||
content: content
|
||||
});
|
||||
}
|
||||
|
||||
/** 将单条参考文献 HTML/文本拆解为结构化字段 */
|
||||
export function parseReferenceFieldsFromHtml(html, referTypeHint) {
|
||||
const raw = sanitizeImportedReferenceHtml(stripLeadingReferenceNumber(String(html || '').trim()));
|
||||
if (!raw) return null;
|
||||
|
||||
const hinted = String(referTypeHint || '').toLowerCase();
|
||||
if (hinted === 'book' || /ISBN\s*:/i.test(raw)) {
|
||||
const plain = normalizeImportedReferenceText(htmlToPlainText(raw));
|
||||
const parsed = parseBookReferenceContent(plain);
|
||||
if (parsed && parsed.isbn) {
|
||||
return finalizeReferenceFields(
|
||||
{
|
||||
refer_type: 'book',
|
||||
author: parsed.author || '',
|
||||
title: parsed.title || '',
|
||||
joura: '',
|
||||
dateno: parsed.dateno || '',
|
||||
doilink: '',
|
||||
doi: '',
|
||||
isbn: parsed.isbn || '',
|
||||
content: ''
|
||||
},
|
||||
raw
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const journalParsed = parseJournalReferenceContent(raw);
|
||||
if (journalParsed) {
|
||||
return finalizeReferenceFields(journalParsed, raw);
|
||||
}
|
||||
|
||||
return finalizeReferenceFields(
|
||||
{
|
||||
refer_type: hinted || 'other',
|
||||
author: '',
|
||||
title: '',
|
||||
joura: '',
|
||||
dateno: '',
|
||||
doilink: '',
|
||||
doi: '',
|
||||
isbn: '',
|
||||
content: normalizeImportedReferenceText(htmlToPlainText(raw))
|
||||
},
|
||||
raw
|
||||
);
|
||||
}
|
||||
|
||||
export function applyDealContentToFields(fields, dealData) {
|
||||
const base = fields || {};
|
||||
const data = dealData || {};
|
||||
if (!data || typeof data !== 'object') return normalizeImportedReferenceFields(base);
|
||||
return normalizeImportedReferenceFields({
|
||||
refer_type: base.refer_type || 'journal',
|
||||
author: data.author || base.author || '',
|
||||
title: data.title || base.title || '',
|
||||
joura: data.joura || base.joura || '',
|
||||
dateno: data.dateno || base.dateno || '',
|
||||
doilink: data.doilink || data.doi || base.doilink || '',
|
||||
doi: data.doi || data.doilink || base.doi || '',
|
||||
isbn: data.isbn || base.isbn || '',
|
||||
content: base.content || ''
|
||||
});
|
||||
}
|
||||
@@ -107,6 +107,238 @@ function allByLocal(parent, name) {
|
||||
return Array.from(parent.children || []).filter((child) => localName(child) === target);
|
||||
}
|
||||
|
||||
function getWordAttrVal(el) {
|
||||
if (!el) return '';
|
||||
return el.getAttribute('w:val') || el.getAttribute('val') || '';
|
||||
}
|
||||
|
||||
function getParagraphNumPr(p) {
|
||||
const pPr = firstByLocal(p, 'pPr');
|
||||
if (pPr) {
|
||||
const numPr = firstByLocal(pPr, 'numPr');
|
||||
if (numPr) return numPr;
|
||||
}
|
||||
return firstByLocal(p, 'numPr');
|
||||
}
|
||||
|
||||
function toRomanNumber(num) {
|
||||
const n = parseInt(num, 10);
|
||||
if (!Number.isFinite(n) || n <= 0) return String(num);
|
||||
const pairs = [
|
||||
[1000, 'M'],
|
||||
[900, 'CM'],
|
||||
[500, 'D'],
|
||||
[400, 'CD'],
|
||||
[100, 'C'],
|
||||
[90, 'XC'],
|
||||
[50, 'L'],
|
||||
[40, 'XL'],
|
||||
[10, 'X'],
|
||||
[9, 'IX'],
|
||||
[5, 'V'],
|
||||
[4, 'IV'],
|
||||
[1, 'I']
|
||||
];
|
||||
let rest = n;
|
||||
let out = '';
|
||||
pairs.forEach(([val, sym]) => {
|
||||
while (rest >= val) {
|
||||
out += sym;
|
||||
rest -= val;
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function formatNumberByNumFmt(n, numFmt) {
|
||||
const num = parseInt(n, 10);
|
||||
if (!Number.isFinite(num)) return String(n);
|
||||
switch (String(numFmt || 'decimal').toLowerCase()) {
|
||||
case 'decimal':
|
||||
case 'decimalzero':
|
||||
return String(num);
|
||||
case 'lowerletter':
|
||||
return String.fromCharCode(97 + ((num - 1) % 26));
|
||||
case 'upperletter':
|
||||
return String.fromCharCode(65 + ((num - 1) % 26));
|
||||
case 'lowerroman':
|
||||
return toRomanNumber(num).toLowerCase();
|
||||
case 'upperroman':
|
||||
return toRomanNumber(num);
|
||||
case 'bullet':
|
||||
return '•';
|
||||
default:
|
||||
return String(num);
|
||||
}
|
||||
}
|
||||
|
||||
function parseWordNumberingXml(numberingXml) {
|
||||
const abstractNums = {};
|
||||
const nums = {};
|
||||
if (!numberingXml) return { abstractNums, nums };
|
||||
|
||||
const doc = new DOMParser().parseFromString(numberingXml, 'application/xml');
|
||||
const root = doc.documentElement;
|
||||
if (!root) return { abstractNums, nums };
|
||||
|
||||
allByLocal(root, 'abstractNum').forEach((el) => {
|
||||
const id = el.getAttribute('w:abstractNumId') || el.getAttribute('abstractNumId') || '';
|
||||
if (!id) return;
|
||||
const levels = {};
|
||||
allByLocal(el, 'lvl').forEach((lvl) => {
|
||||
const ilvl = lvl.getAttribute('w:ilvl') || lvl.getAttribute('ilvl') || '0';
|
||||
levels[ilvl] = {
|
||||
start: parseInt(getWordAttrVal(firstByLocal(lvl, 'start')) || '1', 10) || 1,
|
||||
numFmt: getWordAttrVal(firstByLocal(lvl, 'numFmt')) || 'decimal',
|
||||
lvlText: getWordAttrVal(firstByLocal(lvl, 'lvlText')) || `%${parseInt(ilvl, 10) + 1}.`
|
||||
};
|
||||
});
|
||||
abstractNums[id] = { levels };
|
||||
});
|
||||
|
||||
allByLocal(root, 'num').forEach((el) => {
|
||||
const numId = el.getAttribute('w:numId') || el.getAttribute('numId') || '';
|
||||
if (!numId) return;
|
||||
const abstractNumId = getWordAttrVal(firstByLocal(el, 'abstractNumId'));
|
||||
const overrides = {};
|
||||
allByLocal(el, 'lvlOverride').forEach((ov) => {
|
||||
const ilvl = ov.getAttribute('w:ilvl') || ov.getAttribute('ilvl') || '0';
|
||||
overrides[ilvl] = overrides[ilvl] || {};
|
||||
const startOverride = firstByLocal(ov, 'startOverride');
|
||||
if (startOverride) {
|
||||
overrides[ilvl].start = parseInt(getWordAttrVal(startOverride) || '1', 10) || 1;
|
||||
}
|
||||
const lvlEl = firstByLocal(ov, 'lvl');
|
||||
if (lvlEl) {
|
||||
const ovIlvl = lvlEl.getAttribute('w:ilvl') || lvlEl.getAttribute('ilvl') || ilvl;
|
||||
overrides[ovIlvl] = overrides[ovIlvl] || {};
|
||||
overrides[ovIlvl].lvl = {
|
||||
start: parseInt(getWordAttrVal(firstByLocal(lvlEl, 'start')) || '1', 10) || 1,
|
||||
numFmt: getWordAttrVal(firstByLocal(lvlEl, 'numFmt')) || 'decimal',
|
||||
lvlText: getWordAttrVal(firstByLocal(lvlEl, 'lvlText')) || `%${parseInt(ovIlvl, 10) + 1}.`
|
||||
};
|
||||
}
|
||||
});
|
||||
nums[numId] = { abstractNumId, overrides };
|
||||
});
|
||||
|
||||
return { abstractNums, nums };
|
||||
}
|
||||
|
||||
async function loadWordNumberingDefinitions(zip) {
|
||||
const numberingFile = zip.file('word/numbering.xml');
|
||||
if (!numberingFile) {
|
||||
return { abstractNums: {}, nums: {} };
|
||||
}
|
||||
const numberingXml = await numberingFile.async('string');
|
||||
return parseWordNumberingXml(numberingXml);
|
||||
}
|
||||
|
||||
function createWordListNumberTracker(definitions) {
|
||||
const defs = definitions || { abstractNums: {}, nums: {} };
|
||||
const states = {};
|
||||
let lastListNumber = '';
|
||||
|
||||
function getLevelDef(numId, ilvl) {
|
||||
const numDef = defs.nums[numId];
|
||||
if (!numDef) return null;
|
||||
const absDef = defs.abstractNums[numDef.abstractNumId];
|
||||
if (!absDef) return null;
|
||||
const base = absDef.levels[ilvl] || absDef.levels[String(parseInt(ilvl, 10))];
|
||||
const ov = numDef.overrides && (numDef.overrides[ilvl] || numDef.overrides[String(ilvl)]);
|
||||
if (ov && ov.lvl) {
|
||||
return {
|
||||
start: ov.start != null ? ov.start : ov.lvl.start,
|
||||
numFmt: ov.lvl.numFmt,
|
||||
lvlText: ov.lvl.lvlText
|
||||
};
|
||||
}
|
||||
if (!base) return null;
|
||||
return {
|
||||
start: ov && ov.start != null ? ov.start : base.start,
|
||||
numFmt: base.numFmt,
|
||||
lvlText: base.lvlText
|
||||
};
|
||||
}
|
||||
|
||||
function ensureCounter(numId, ilvl) {
|
||||
if (!states[numId]) states[numId] = { levels: {} };
|
||||
const st = states[numId].levels;
|
||||
if (st[ilvl] == null) {
|
||||
const def = getLevelDef(numId, ilvl);
|
||||
st[ilvl] = (def && def.start != null ? def.start : 1) - 1;
|
||||
}
|
||||
return st[ilvl];
|
||||
}
|
||||
|
||||
function advanceCounter(numId, ilvl) {
|
||||
ensureCounter(numId, ilvl);
|
||||
states[numId].levels[ilvl] += 1;
|
||||
Object.keys(states[numId].levels).forEach((key) => {
|
||||
if (parseInt(key, 10) > parseInt(ilvl, 10)) {
|
||||
delete states[numId].levels[key];
|
||||
}
|
||||
});
|
||||
return states[numId].levels[ilvl];
|
||||
}
|
||||
|
||||
function formatLabel(numId, ilvl, currentVal) {
|
||||
const levelDef = getLevelDef(numId, ilvl);
|
||||
if (!levelDef) return String(currentVal);
|
||||
const lvlText = levelDef.lvlText || `%${parseInt(ilvl, 10) + 1}.`;
|
||||
return lvlText.replace(/%(\d+)/g, (_, levelStr) => {
|
||||
const idx = parseInt(levelStr, 10) - 1;
|
||||
const idxStr = String(idx);
|
||||
const counterVal = idxStr === ilvl ? currentVal : ensureCounter(numId, idxStr) + 1;
|
||||
const def = getLevelDef(numId, idxStr);
|
||||
return formatNumberByNumFmt(counterVal, def && def.numFmt);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
resolveParagraphListLabel(p) {
|
||||
const numPr = getParagraphNumPr(p);
|
||||
if (!numPr) {
|
||||
lastListNumber = '';
|
||||
return '';
|
||||
}
|
||||
const numId = getWordAttrVal(firstByLocal(numPr, 'numId'));
|
||||
if (!numId) {
|
||||
lastListNumber = '';
|
||||
return '';
|
||||
}
|
||||
const ilvl = getWordAttrVal(firstByLocal(numPr, 'ilvl')) || '0';
|
||||
const levelDef = getLevelDef(numId, ilvl);
|
||||
if (levelDef && String(levelDef.numFmt).toLowerCase() === 'bullet') {
|
||||
lastListNumber = '';
|
||||
return levelDef.lvlText || '•';
|
||||
}
|
||||
const currentVal = advanceCounter(numId, ilvl);
|
||||
lastListNumber = String(currentVal);
|
||||
return formatLabel(numId, ilvl, currentVal);
|
||||
},
|
||||
getLastListNumber() {
|
||||
return lastListNumber;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** mammoth 等输出的 <ol><li> 补全 data-list-num,便于保留原序号 */
|
||||
export function annotateOrderedListItemsInHtml(html) {
|
||||
if (!html || typeof document === 'undefined') return html || '';
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
div.querySelectorAll('ol').forEach((ol) => {
|
||||
let n = parseInt(ol.getAttribute('start') || '1', 10) || 1;
|
||||
Array.from(ol.children || []).forEach((child) => {
|
||||
if (!child || child.tagName.toLowerCase() !== 'li') return;
|
||||
child.setAttribute('data-list-num', String(n));
|
||||
n += 1;
|
||||
});
|
||||
});
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** Word OMML m:dPr 上的分隔符(begChr / endChr) */
|
||||
function getDelimCharFromD(node, which) {
|
||||
const dPr = firstByLocal(node, 'dPr');
|
||||
@@ -657,7 +889,12 @@ export function isReferencesHeadingText(text) {
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return /^references\.?\s*$/i.test(t);
|
||||
if (/^references\.?\s*$/i.test(t)) return true;
|
||||
if (/^reference\.?\s*$/i.test(t)) return true;
|
||||
if (/^bibliography\.?\s*$/i.test(t)) return true;
|
||||
if (/^参考文献\.?\s*$/i.test(t)) return true;
|
||||
if (/^(\d+[\.\)]\s*)?(references|reference|bibliography|参考文献)\.?\s*$/i.test(t)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 参考文献条目(含无编号、Available at / ISBN 格式) */
|
||||
@@ -1308,8 +1545,20 @@ function paragraphToHtmlFromRunsOnly(p, imageCache) {
|
||||
return html.trim();
|
||||
}
|
||||
|
||||
function mergeAdjacentItalicTagsInHtml(html) {
|
||||
let result = String(html || '');
|
||||
let prev = '';
|
||||
while (prev !== result) {
|
||||
prev = result;
|
||||
result = result.replace(/<\/i>\s*<i>/gi, '');
|
||||
result = result.replace(/<\/em>\s*<em>/gi, '');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeParagraphHtml(html) {
|
||||
if (!html) return '';
|
||||
html = mergeAdjacentItalicTagsInHtml(html);
|
||||
const plain = stripHtmlToPlain(html);
|
||||
const cleaned = cleanImportedPlainText(plain);
|
||||
if (!cleaned) return '';
|
||||
@@ -1634,7 +1883,50 @@ function tableToHtml(table, imageCache) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function documentXmlToHtml(documentXml, imageCache) {
|
||||
function paragraphInnerHasListPrefix(inner, prefix) {
|
||||
const plainInner = String(inner || '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.trim();
|
||||
const plainPrefix = String(prefix || '')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.trim();
|
||||
if (!plainPrefix || !plainInner) return false;
|
||||
const escaped = plainPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp('^\\s*' + escaped + '(?:\\s|$)').test(plainInner);
|
||||
}
|
||||
|
||||
function appendImportedParagraphHtml(target, p, imageCache, listTracker) {
|
||||
let inner = paragraphToHtml(p, imageCache);
|
||||
let listNum = '';
|
||||
let listPrefix = '';
|
||||
if (listTracker) {
|
||||
listPrefix = listTracker.resolveParagraphListLabel(p);
|
||||
if (listPrefix) {
|
||||
listNum = listTracker.getLastListNumber() || '';
|
||||
if (!paragraphInnerHasListPrefix(inner, listPrefix)) {
|
||||
inner = escapeHtmlPlainTextForPaste(listPrefix) + (/\s$/.test(listPrefix) ? '' : ' ') + inner;
|
||||
}
|
||||
}
|
||||
}
|
||||
const isEmpty = !inner;
|
||||
if (isEmpty) {
|
||||
if (target.lastWasEmpty) return;
|
||||
target.lastWasEmpty = true;
|
||||
target.html += '<p><br></p>';
|
||||
} else {
|
||||
target.lastWasEmpty = false;
|
||||
if (listNum) {
|
||||
target.html += `<p class="word-ref-item" data-list-num="${escapeHtmlPlainTextForPaste(listNum)}">${inner}</p>`;
|
||||
} else {
|
||||
target.html += `<p>${inner}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function documentXmlToHtml(documentXml, imageCache, options) {
|
||||
const includeReferencesSection = !!(options && options.includeReferencesSection);
|
||||
const listTracker = options && options.listTracker;
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(documentXml, 'application/xml');
|
||||
const body =
|
||||
@@ -1642,34 +1934,28 @@ function documentXmlToHtml(documentXml, imageCache) {
|
||||
Array.from(xmlDoc.getElementsByTagName('*')).find((el) => localName(el) === 'body');
|
||||
if (!body) return '';
|
||||
|
||||
let html = '';
|
||||
let reachedReferences = false;
|
||||
let lastWasEmpty = false;
|
||||
const target = { html: '', lastWasEmpty: false };
|
||||
let omitRest = false;
|
||||
Array.from(body.children || []).forEach((child) => {
|
||||
if (reachedReferences) return;
|
||||
if (omitRest) return;
|
||||
const name = localName(child);
|
||||
if (name === 'p') {
|
||||
if (isSectionBreakOnlyParagraph(child)) return;
|
||||
if (isReferencesHeadingParagraph(child)) {
|
||||
reachedReferences = true;
|
||||
if (includeReferencesSection) {
|
||||
appendImportedParagraphHtml(target, child, imageCache, listTracker);
|
||||
return;
|
||||
}
|
||||
omitRest = true;
|
||||
return;
|
||||
}
|
||||
const inner = paragraphToHtml(child, imageCache);
|
||||
const isEmpty = !inner;
|
||||
if (isEmpty) {
|
||||
if (lastWasEmpty) return;
|
||||
lastWasEmpty = true;
|
||||
html += '<p><br></p>';
|
||||
} else {
|
||||
lastWasEmpty = false;
|
||||
html += `<p>${inner}</p>`;
|
||||
}
|
||||
appendImportedParagraphHtml(target, child, imageCache, listTracker);
|
||||
} else if (name === 'tbl') {
|
||||
lastWasEmpty = false;
|
||||
html += wrapImportedTableHtml(tableToHtml(child, imageCache));
|
||||
target.lastWasEmpty = false;
|
||||
target.html += wrapImportedTableHtml(tableToHtml(child, imageCache));
|
||||
}
|
||||
});
|
||||
return html;
|
||||
return target.html;
|
||||
}
|
||||
|
||||
export function collapseEmptyParagraphsInHtml(html) {
|
||||
@@ -1892,7 +2178,12 @@ export async function importWordDocumentWithMath(file, options = {}) {
|
||||
const documentXml = await documentFile.async('string');
|
||||
const imageRelMap = await buildImageRelMap(zip);
|
||||
const imageCache = await buildImageDataUriCache(zip, documentXml, imageRelMap);
|
||||
let html = documentXmlToHtml(documentXml, imageCache);
|
||||
const numberingDefs = await loadWordNumberingDefinitions(zip);
|
||||
const listTracker = createWordListNumberTracker(numberingDefs);
|
||||
let html = documentXmlToHtml(documentXml, imageCache, {
|
||||
includeReferencesSection: !!options.keepReferences,
|
||||
listTracker
|
||||
});
|
||||
if (!html || !html.replace(/<[^>]+>/g, '').trim()) {
|
||||
const result = await mammoth.convertToHtml(
|
||||
{ arrayBuffer },
|
||||
@@ -1911,6 +2202,13 @@ export async function importWordDocumentWithMath(file, options = {}) {
|
||||
if (options.textOnly) {
|
||||
return prepareWordHtmlForEditor(html);
|
||||
}
|
||||
if (options.keepReferences) {
|
||||
html = annotateOrderedListItemsInHtml(html);
|
||||
html = collapseEmptyParagraphsInHtml(
|
||||
mergeAdjacentBlueTags(normalizeSpacesAroundBlueTags(convertStyledBlueToTags(html)))
|
||||
);
|
||||
return html;
|
||||
}
|
||||
html = collapseEmptyParagraphsInHtml(postProcessImportedWordHtml(html));
|
||||
return parseHtmlToLatex(html);
|
||||
}
|
||||
|
||||
485
src/utils/wordReferenceBatchImport.js
Normal file
485
src/utils/wordReferenceBatchImport.js
Normal file
@@ -0,0 +1,485 @@
|
||||
import { htmlToPlainText } from '@/utils/manuscriptCitationRelevance';
|
||||
import { parseReferencesBulkContent } from '@/utils/manuscriptReferenceHtml';
|
||||
import { extractReferencesSectionFromWordHtml, fetchWordReferencesSection } from '@/utils/manuscriptWordReferences';
|
||||
import { importWordDocumentWithMath } from '@/utils/wordMathImport';
|
||||
import { applyDealContentToFields, parseReferenceFieldsFromHtml, normalizeImportedReferenceFields, normalizeImportedReferenceText, finalizeReferenceFields } from '@/utils/parseReferenceFields';
|
||||
|
||||
function mapUploadedItemsToRows(items) {
|
||||
return (items || []).map(function (item, index) {
|
||||
const referType = String(item.refer_type || 'journal').toLowerCase();
|
||||
const fields = parseReferenceFieldsFromHtml(item.html, referType);
|
||||
return {
|
||||
index: index + 1,
|
||||
html: item.html || '',
|
||||
refer_type: fields ? fields.refer_type : referType,
|
||||
fields: fields
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 解析本地 Word 文件中的 References 条目 */
|
||||
export async function parseLocalWordReferencesFile(file) {
|
||||
if (!file) {
|
||||
return { found: false, rows: [], count: 0 };
|
||||
}
|
||||
const html = await importWordDocumentWithMath(file, { keepReferences: true });
|
||||
const extracted = extractReferencesSectionFromWordHtml(html);
|
||||
if (!extracted.found) {
|
||||
return { found: false, rows: [], count: 0 };
|
||||
}
|
||||
const items = parseReferencesBulkContent(extracted.html);
|
||||
const rows = mapUploadedItemsToRows(items);
|
||||
return {
|
||||
found: rows.length > 0,
|
||||
rows: rows,
|
||||
count: rows.length
|
||||
};
|
||||
}
|
||||
|
||||
/** 从 gridData 稿件路径下载并解析 References */
|
||||
export async function parseManuscriptWordReferences(apiClient, options) {
|
||||
const extracted = await fetchWordReferencesSection(apiClient, options || {});
|
||||
if (!extracted.found) {
|
||||
return { found: false, rows: [], count: 0 };
|
||||
}
|
||||
const items = parseReferencesBulkContent(extracted.html);
|
||||
const rows = mapUploadedItemsToRows(items);
|
||||
return {
|
||||
found: rows.length > 0,
|
||||
rows: rows,
|
||||
count: rows.length
|
||||
};
|
||||
}
|
||||
|
||||
function previewExistingReference(row) {
|
||||
if (!row) return '';
|
||||
if (row.refer_frag) {
|
||||
return htmlToPlainText(row.refer_frag).replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
const parts = [];
|
||||
if (row.author) parts.push(row.author);
|
||||
if (row.title) parts.push(row.title);
|
||||
if (row.joura) parts.push(row.joura);
|
||||
if (row.dateno) parts.push(row.dateno);
|
||||
if (row.doilink) parts.push('Available at: ' + row.doilink);
|
||||
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalizeWordRefBatchRow(row) {
|
||||
if (!row) return row;
|
||||
let normalized = normalizeImportedReferenceFields(row);
|
||||
if (normalized.doilink) {
|
||||
normalized.doilink = normalizeDoilinkFull(normalized.doilink);
|
||||
normalized.doi = normalizeDoiValue(normalized.doilink);
|
||||
}
|
||||
if (normalized.dateno) {
|
||||
normalized.dateno = normalized.dateno.replace(/^(\d{4}):/, '$1;');
|
||||
}
|
||||
normalized = finalizeReferenceFields(normalized, row.wordHtml || normalized.content || '');
|
||||
return Object.assign({}, row, normalized);
|
||||
}
|
||||
|
||||
/** 按序号将 Word 条目与现有参考文献一一对应 */
|
||||
export function buildWordRefBatchRows(existingRows, uploadedRows) {
|
||||
const existing = existingRows || [];
|
||||
const uploaded = uploadedRows || [];
|
||||
const maxLen = Math.max(existing.length, uploaded.length);
|
||||
const rows = [];
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const existingRow = existing[i] || null;
|
||||
const uploadedRow = uploaded[i] || null;
|
||||
const fields = (uploadedRow && uploadedRow.fields) || null;
|
||||
const prevExistingRow = i > 0 ? existing[i - 1] : null;
|
||||
rows.push(
|
||||
normalizeWordRefBatchRow({
|
||||
index: i + 1,
|
||||
apply: !!(uploadedRow && fields),
|
||||
p_refer_id: existingRow ? existingRow.p_refer_id : null,
|
||||
pre_p_refer_id: existingRow
|
||||
? existingRow.pre_p_refer_id
|
||||
: prevExistingRow
|
||||
? prevExistingRow.p_refer_id
|
||||
: null,
|
||||
existingType: existingRow ? existingRow.refer_type : '',
|
||||
existingPreview: previewExistingReference(existingRow),
|
||||
refer_type: fields ? fields.refer_type : uploadedRow ? uploadedRow.refer_type : 'journal',
|
||||
author: fields ? fields.author : '',
|
||||
title: fields ? fields.title : '',
|
||||
joura: fields ? fields.joura : '',
|
||||
dateno: fields ? fields.dateno : '',
|
||||
doilink: fields ? fields.doilink : '',
|
||||
doi: fields ? fields.doi || fields.doilink : '',
|
||||
isbn: fields ? fields.isbn : '',
|
||||
content: fields ? fields.content : '',
|
||||
wordHtml: uploadedRow ? uploadedRow.html : '',
|
||||
unmatched: !existingRow || !uploadedRow,
|
||||
missingExisting: !existingRow,
|
||||
missingUploaded: !uploadedRow
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 调用 dealContent 精修字段拆解 */
|
||||
export async function enrichWordRefBatchRowsWithDealContent(apiClient, rows) {
|
||||
const list = rows || [];
|
||||
const updated = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const row = list[i];
|
||||
if (!row || !row.wordHtml) {
|
||||
updated.push(normalizeWordRefBatchRow(row));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const content = normalizeImportedReferenceText(htmlToPlainText(row.wordHtml));
|
||||
const res = await apiClient.post('api/References/dealContent', { content: content });
|
||||
const data = res && res.data ? res.data : null;
|
||||
if (data) {
|
||||
const merged = applyDealContentToFields(
|
||||
{
|
||||
refer_type: row.refer_type,
|
||||
author: row.author,
|
||||
title: row.title,
|
||||
joura: row.joura,
|
||||
dateno: row.dateno,
|
||||
doilink: row.doilink,
|
||||
doi: row.doi || row.doilink,
|
||||
isbn: row.isbn,
|
||||
content: row.content
|
||||
},
|
||||
data
|
||||
);
|
||||
const nextRow = normalizeWordRefBatchRow(
|
||||
Object.assign({}, row, merged, {
|
||||
refer_type: merged.refer_type || row.refer_type,
|
||||
doi: merged.doi || merged.doilink || row.doi || row.doilink
|
||||
})
|
||||
);
|
||||
updated.push(nextRow);
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
// 保留本地拆解结果
|
||||
}
|
||||
updated.push(normalizeWordRefBatchRow(row));
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
function normalizeDoiValue(doilink) {
|
||||
let raw = String(doilink || '').trim();
|
||||
if (!raw) return '';
|
||||
raw = raw.replace(/^https?:\/\/doi\.org\//i, '');
|
||||
raw = raw.replace(/^doi:/i, '');
|
||||
return raw.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeDoilinkFull(doilink) {
|
||||
const raw = String(doilink || '').trim();
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) return raw;
|
||||
const doi = normalizeDoiValue(raw);
|
||||
return doi ? 'https://doi.org/' + doi : raw;
|
||||
}
|
||||
|
||||
function appendReferPart(content, part) {
|
||||
const piece = String(part || '').trim();
|
||||
if (!piece) return content;
|
||||
let out = String(content || '').trim();
|
||||
if (!out) return piece;
|
||||
if (out.endsWith('.')) {
|
||||
return out + piece;
|
||||
}
|
||||
return out + '.' + piece;
|
||||
}
|
||||
|
||||
/** journal 的 content 字段,与 editRefer 接口一致 */
|
||||
export function buildJournalReferContent(fields) {
|
||||
const author = String((fields && fields.author) || '').trim();
|
||||
const title = String((fields && fields.title) || '').trim();
|
||||
const joura = String((fields && fields.joura) || '').trim();
|
||||
const dateno = String((fields && fields.dateno) || '').trim();
|
||||
const doilink = normalizeDoilinkFull(fields && fields.doilink);
|
||||
|
||||
let content = author;
|
||||
if (title) {
|
||||
if (!content) {
|
||||
content = title;
|
||||
} else if (content.endsWith('.')) {
|
||||
content = content + ' ' + title;
|
||||
} else {
|
||||
content = content + '. ' + title;
|
||||
}
|
||||
}
|
||||
if (joura) {
|
||||
content = content + ' ' + joura;
|
||||
if (!/\.\s*$/.test(joura)) {
|
||||
content = content + '.';
|
||||
}
|
||||
}
|
||||
if (dateno) {
|
||||
const datenoForContent = dateno.replace(/^(\d{4});/, '$1:');
|
||||
content = appendReferPart(content, datenoForContent);
|
||||
}
|
||||
if (doilink) {
|
||||
content = content + '.Available at: ' + doilink;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/** book 的 content 字段 */
|
||||
export function buildBookReferContent(fields) {
|
||||
const author = String((fields && fields.author) || '').trim();
|
||||
const title = String((fields && fields.title) || '').trim();
|
||||
const dateno = String((fields && fields.dateno) || '').trim();
|
||||
const isbn = String((fields && fields.isbn) || '').trim();
|
||||
|
||||
let content = author;
|
||||
if (title) {
|
||||
content = appendReferPart(content, title);
|
||||
}
|
||||
if (dateno) {
|
||||
content = appendReferPart(content, dateno);
|
||||
}
|
||||
if (isbn) {
|
||||
content = content + '. Available at: ISBN: ' + isbn;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function buildEditReferPayload(row, pArticleId) {
|
||||
const normalized = normalizeWordRefBatchRow(row || {});
|
||||
const referType = String(normalized.refer_type || 'journal').toLowerCase();
|
||||
const base = {
|
||||
p_article_id: pArticleId != null && pArticleId !== '' ? pArticleId : '',
|
||||
p_refer_id: normalized.p_refer_id,
|
||||
pre_p_refer_id: normalized.pre_p_refer_id != null && normalized.pre_p_refer_id !== '' ? normalized.pre_p_refer_id : ''
|
||||
};
|
||||
|
||||
if (referType === 'book') {
|
||||
return Object.assign(base, {
|
||||
doi: '',
|
||||
refer_type: 'book',
|
||||
author: normalized.author || '',
|
||||
title: normalized.title || '',
|
||||
dateno: normalized.dateno || '',
|
||||
isbn: normalized.isbn || '',
|
||||
joura: '',
|
||||
doilink: '',
|
||||
content: buildBookReferContent(normalized)
|
||||
});
|
||||
}
|
||||
|
||||
if (referType === 'other') {
|
||||
return Object.assign(base, {
|
||||
doi: normalizeDoiValue(normalized.doilink || normalized.doi),
|
||||
refer_type: 'other',
|
||||
author: '',
|
||||
title: '',
|
||||
dateno: '',
|
||||
joura: '',
|
||||
doilink: normalizeDoilinkFull(normalized.doilink) || '',
|
||||
isbn: '',
|
||||
content: normalized.content || ''
|
||||
});
|
||||
}
|
||||
|
||||
const doilink = normalizeDoilinkFull(normalized.doilink);
|
||||
const doi = normalizeDoiValue(normalized.doilink || normalized.doi);
|
||||
return Object.assign(base, {
|
||||
doi: doi,
|
||||
refer_type: 'journal',
|
||||
author: normalized.author || '',
|
||||
title: normalized.title || '',
|
||||
joura: normalized.joura || '',
|
||||
dateno: normalized.dateno || '',
|
||||
doilink: doilink,
|
||||
isbn: '',
|
||||
content: buildJournalReferContent(normalized)
|
||||
});
|
||||
}
|
||||
|
||||
export function extractAddedReferId(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return '';
|
||||
}
|
||||
if (data.p_refer_id != null && data.p_refer_id !== '') {
|
||||
return String(data.p_refer_id);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 新增参考文献 payload,p_refer_id 留空,pre_p_refer_id 指向前一条 */
|
||||
export function buildAddReferPayload(row, pArticleId, preReferId) {
|
||||
const payload = buildEditReferPayload(row, pArticleId);
|
||||
delete payload.p_refer_id;
|
||||
payload.pre_p_refer_id = preReferId != null && preReferId !== '' ? String(preReferId) : '';
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function canWordRefBatchRowSave(row) {
|
||||
if (!row || row.missingUploaded) {
|
||||
return false;
|
||||
}
|
||||
if (row.p_refer_id) {
|
||||
return true;
|
||||
}
|
||||
return !!row.missingExisting;
|
||||
}
|
||||
|
||||
const WORD_REF_BATCH_FIELD_DEFS = {
|
||||
journal: ['author', 'title', 'joura', 'dateno', 'doilink'],
|
||||
book: ['author', 'title', 'dateno', 'isbn'],
|
||||
other: ['content']
|
||||
};
|
||||
|
||||
const WORD_REF_BATCH_REQUIRED_FIELDS = {
|
||||
journal: ['title', 'joura', 'dateno'],
|
||||
book: ['author', 'title', 'dateno'],
|
||||
other: ['content']
|
||||
};
|
||||
|
||||
export function getWordRefBatchFieldKeys(referType) {
|
||||
const type = String(referType || 'journal').toLowerCase();
|
||||
return (WORD_REF_BATCH_FIELD_DEFS[type] || WORD_REF_BATCH_FIELD_DEFS.journal).slice();
|
||||
}
|
||||
|
||||
export function isWordRefBatchRowParseIncomplete(row) {
|
||||
if (!row || row.missingUploaded) {
|
||||
return false;
|
||||
}
|
||||
const type = String(row.refer_type || 'journal').toLowerCase();
|
||||
const required = WORD_REF_BATCH_REQUIRED_FIELDS[type] || WORD_REF_BATCH_REQUIRED_FIELDS.journal;
|
||||
return required.some(function (key) {
|
||||
return !String(row[key] || '').trim();
|
||||
});
|
||||
}
|
||||
|
||||
export function stripWordRefBatchHtmlPreview(html) {
|
||||
return htmlToPlainText(String(html || ''))
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** 切换为 other 时:用 Word 原文(或已拆字段拼接)填入 content */
|
||||
export function buildWordRefBatchContentFromRow(row) {
|
||||
if (!row) {
|
||||
return '';
|
||||
}
|
||||
const fromWord = stripWordRefBatchHtmlPreview(row.wordHtml);
|
||||
if (fromWord) {
|
||||
return normalizeImportedReferenceText(fromWord);
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (row.author) {
|
||||
parts.push(String(row.author).trim());
|
||||
}
|
||||
if (row.title) {
|
||||
parts.push(String(row.title).trim());
|
||||
}
|
||||
if (row.joura) {
|
||||
parts.push(String(row.joura).trim());
|
||||
}
|
||||
if (row.dateno) {
|
||||
parts.push(String(row.dateno).trim());
|
||||
}
|
||||
|
||||
let content = parts
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.replace(/\s+\./g, '.')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
const doilink = String(row.doilink || '').trim();
|
||||
if (doilink && !/Available at:/i.test(content)) {
|
||||
content = content + '. Available at: ' + doilink;
|
||||
}
|
||||
|
||||
return normalizeImportedReferenceText(content || row.content || '');
|
||||
}
|
||||
|
||||
export function applyWordRefBatchOtherType(row) {
|
||||
if (!row) {
|
||||
return row;
|
||||
}
|
||||
row.refer_type = 'other';
|
||||
row.content = buildWordRefBatchContentFromRow(row);
|
||||
const plain = String(row.content || '');
|
||||
const urlMatch = plain.match(/Available at:\s*(https?:\/\/\S+)/i);
|
||||
if (urlMatch) {
|
||||
row.doilink = urlMatch[1].replace(/[.,;)\]]+$/, '');
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按序号批量保存:已有条目走 editRefer,Word 多出的条目走 addReferByParticleid,
|
||||
* 每条成功后用返回/已有的 p_refer_id 作为下一条的 pre_p_refer_id。
|
||||
*/
|
||||
export async function saveWordRefBatchRows(apiClient, options) {
|
||||
const opts = options || {};
|
||||
const rows = (opts.rows || [])
|
||||
.filter(function (row) {
|
||||
return row && row.apply && canWordRefBatchRowSave(row);
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return (a.index || 0) - (b.index || 0);
|
||||
});
|
||||
|
||||
const pArticleId = opts.pArticleId;
|
||||
const useUserAdd = opts.role === 'user';
|
||||
const addEndpoint = useUserAdd ? 'api/Preaccept/addRefer' : 'api/Preaccept/addReferByParticleid';
|
||||
|
||||
let lastReferId = null;
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
try {
|
||||
if (row.p_refer_id) {
|
||||
const payload = buildEditReferPayload(row, pArticleId);
|
||||
if (lastReferId != null && lastReferId !== '') {
|
||||
payload.pre_p_refer_id = lastReferId;
|
||||
}
|
||||
const res = await apiClient.post('api/Preaccept/editRefer', payload);
|
||||
if (res && res.code == 0) {
|
||||
ok += 1;
|
||||
lastReferId = String(row.p_refer_id);
|
||||
} else {
|
||||
fail += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (row.missingExisting) {
|
||||
const payload = buildAddReferPayload(row, pArticleId, lastReferId);
|
||||
if (useUserAdd && opts.articleId != null && opts.articleId !== '') {
|
||||
payload.article_id = opts.articleId;
|
||||
}
|
||||
const res = await apiClient.post(addEndpoint, payload);
|
||||
if (res && res.code == 0) {
|
||||
ok += 1;
|
||||
const newId = extractAddedReferId(res.data);
|
||||
if (newId) {
|
||||
lastReferId = newId;
|
||||
}
|
||||
} else {
|
||||
fail += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
fail += 1;
|
||||
} catch (err) {
|
||||
fail += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: ok, fail: fail };
|
||||
}
|
||||
Reference in New Issue
Block a user