添加期刊引用分析

This commit is contained in:
2026-06-17 09:35:15 +08:00
parent ec4a59eedb
commit ea5695f913
36 changed files with 6129 additions and 103 deletions

17
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"katex": "^0.16.21", "katex": "^0.16.21",
"mammoth": "^1.5.1", "mammoth": "^1.5.1",
"mathlive": "^0.104.0", "mathlive": "^0.104.0",
"mathml-to-latex": "^1.5.0",
"mavon-editor": "^2.6.17", "mavon-editor": "^2.6.17",
"multi-items-input": "^0.2.0", "multi-items-input": "^0.2.0",
"pizzip": "^3.1.7", "pizzip": "^3.1.7",
@@ -35543,6 +35544,22 @@
"url": "https://paypal.me/arnogourdol" "url": "https://paypal.me/arnogourdol"
} }
}, },
"node_modules/mathml-to-latex": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/mathml-to-latex/-/mathml-to-latex-1.8.0.tgz",
"integrity": "sha512-gQ0uK3zqB8HwlfaXJkEL5rgaZNbKUiBMmBP/B/W+b+t6KcseLSuYb1b0BjLgS9ZiQa24ePkqTX8/6FaQuDL7wQ==",
"dependencies": {
"@xmldom/xmldom": "^0.9.10"
}
},
"node_modules/mathml-to-latex/node_modules/@xmldom/xmldom": {
"version": "0.9.10",
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
"integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
"engines": {
"node": ">=14.6"
}
},
"node_modules/mavon-editor": { "node_modules/mavon-editor": {
"version": "2.10.4", "version": "2.10.4",
"resolved": "https://registry.npmjs.org/mavon-editor/-/mavon-editor-2.10.4.tgz", "resolved": "https://registry.npmjs.org/mavon-editor/-/mavon-editor-2.10.4.tgz",

View File

@@ -0,0 +1,154 @@
import {
Document,
Paragraph,
TextRun,
CommentRangeStart,
CommentRangeEnd,
CommentReference,
LevelFormat,
LevelSuffix,
AlignmentType,
Packer
} from 'docx';
import JSZip from 'jszip';
import fs from 'fs';
function htmlToPlainText(html) {
return String(html || '')
.replace(/ /g, ' ')
.replace(/<br\s*\/?>/gi, ' ')
.replace(/<[^>]*>/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function parseSingleAuthorName(nameStr) {
const token = String(nameStr || '').trim();
if (!token || /^et al\.?$/i.test(token)) return null;
const parts = token.split(/\s+/).filter(Boolean);
if (!parts.length) return null;
if (parts.length === 1) return { first: '', last: parts[0] };
return { first: parts.slice(0, -1).join(' '), last: parts[parts.length - 1] };
}
function firstNameToAmaInitials(first) {
return String(first || '')
.split(/[\s-]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase())
.join('');
}
function formatOneAmaAuthor(nameStr) {
const parsed = parseSingleAuthorName(nameStr);
if (!parsed || !parsed.last) return '';
const initials = firstNameToAmaInitials(parsed.first);
return initials ? parsed.last + ' ' + initials : parsed.last;
}
function splitAuthorList(authorText) {
return String(authorText || '')
.split(/\s*,\s*/)
.map((part) => part.trim())
.filter(Boolean);
}
function isAmaAbbreviatedAuthorList(authorText) {
const authors = splitAuthorList(htmlToPlainText(authorText));
if (!authors.length) return true;
return authors.every((token) => {
if (/^et al\.?$/i.test(token)) return true;
return /^[\p{L}][\p{L}\-'']*\s+[A-Z](?:-?[A-Z])*\.?$/u.test(token);
});
}
function needsAmaAuthorAbbreviation(authorText) {
const plain = htmlToPlainText(authorText).trim();
if (!plain || isAmaAbbreviatedAuthorList(plain)) return false;
return splitAuthorList(plain).some((token) => {
const parts = token.split(/\s+/).filter(Boolean);
if (parts.length < 2) return false;
return parts[0].length > 2;
});
}
function buildAmaAuthorAbbreviation(authorText) {
const authors = splitAuthorList(htmlToPlainText(authorText));
return authors
.map((token) => (/^et al\.?$/i.test(token) ? 'et al' : formatOneAmaAuthor(token)))
.filter(Boolean)
.join(', ');
}
function buildBookAmaAuthorComment(ref) {
if (!ref || ref.refer_type !== 'book') return '';
const authorSource = ref.author ? htmlToPlainText(ref.author) : '';
if (!authorSource || !needsAmaAuthorAbbreviation(authorSource)) return '';
const amaAuthors = buildAmaAuthorAbbreviation(authorSource);
return amaAuthors ? 'AMA格式作者缩写' + amaAuthors : '';
}
const ref = {
refer_type: 'book',
author: 'Jacqueline Fawcett, Susan DeSanto-Madeya',
title: 'Contemporary Nursing Knowledge',
dateno: '(3rd Edition). Philadelphia, PA: F. A. Davis Company; 2013.',
isbn: '978-0-8036-2765-9'
};
const commentText = buildBookAmaAuthorComment(ref);
console.log('comment text:', commentText);
const commentId = 0;
const doc = new Document({
numbering: {
config: [{
reference: 'ref-num',
levels: [{
level: 0,
format: LevelFormat.DECIMAL,
text: '%1.',
alignment: AlignmentType.START,
suffix: LevelSuffix.TAB,
style: { paragraph: { indent: { left: 720, hanging: 360 } } }
}]
}]
},
comments: {
children: [{
id: commentId,
author: 'Editor',
initials: 'Ed',
date: new Date(),
children: [new Paragraph({ children: [new TextRun(commentText)] })]
}]
},
sections: [{
children: [new Paragraph({
numbering: { reference: 'ref-num', level: 0 },
children: [
new CommentRangeStart(commentId),
new TextRun('Jacqueline Fawcett, Susan DeSanto-Madeya. Contemporary Nursing Knowledge.'),
new CommentRangeEnd(commentId),
new TextRun({ children: [new CommentReference(commentId)] })
]
})]
}]
});
const buf = await Packer.toBuffer(doc);
fs.writeFileSync('test-ref-comment-standalone.docx', buf);
const zip = await JSZip.loadAsync(buf);
const docXml = await zip.file('word/document.xml').async('string');
const commentsXml = zip.file('word/comments.xml');
const relsXml = zip.file('word/_rels/document.xml.rels');
const contentTypes = zip.file('[Content_Types].xml');
console.log('has comments.xml:', !!commentsXml);
console.log('has commentReference in document:', docXml.includes('commentReference'));
console.log('has comments rel in document.xml.rels:', relsXml ? (await relsXml.async('string')).includes('comments') : false);
console.log('has comments in Content_Types:', contentTypes ? (await contentTypes.async('string')).includes('comments') : false);
if (commentsXml) {
console.log('comments preview:', (await commentsXml.async('string')).slice(0, 800));
}

View File

@@ -0,0 +1,40 @@
import {
Document,
Paragraph,
TextRun,
CommentRangeStart,
CommentRangeEnd,
CommentReference,
LevelFormat,
LevelSuffix,
AlignmentType,
Packer
} from 'docx';
import JSZip from 'jszip';
import fs from 'fs';
import { buildBookAmaAuthorComment } from '../src/utils/referenceAuthorAma.js';
import { buildManuscriptWordDocument } from '../src/utils/exportManuscriptWord.js';
const ref = {
refer_type: 'book',
author: 'Jacqueline Fawcett, Susan DeSanto-Madeya',
title: 'Contemporary Nursing Knowledge',
dateno: '(3rd Edition). Philadelphia, PA: F. A. Davis Company; 2013.',
isbn: '978-0-8036-2765-9'
};
console.log('comment text:', buildBookAmaAuthorComment(ref));
const doc = buildManuscriptWordDocument([], '', [ref], null, null);
const buf = await Packer.toBuffer(doc);
fs.writeFileSync('test-ref-comment.docx', buf);
const zip = await JSZip.loadAsync(buf);
const docXml = await zip.file('word/document.xml').async('string');
const commentsXml = zip.file('word/comments.xml');
console.log('has comments.xml:', !!commentsXml);
console.log('has commentReference:', docXml.includes('commentReference'));
console.log('has commentRangeStart:', docXml.includes('commentRangeStart'));
if (commentsXml) {
console.log('comments preview:', (await commentsXml.async('string')).slice(0, 500));
}

View File

@@ -0,0 +1,49 @@
import {
Document,
Paragraph,
TextRun,
DeletedTextRun,
InsertedTextRun,
LevelFormat,
LevelSuffix,
AlignmentType,
Packer
} from 'docx';
import JSZip from 'jszip';
import fs from 'fs';
const doc = new Document({
features: { trackRevisions: true },
sections: [{
children: [new Paragraph({
children: [
new DeletedTextRun({
id: 1,
author: 'Editor',
date: new Date().toISOString(),
text: 'David Harvey ',
font: 'Charis SIL',
size: 15
}),
new InsertedTextRun({
id: 2,
author: 'Editor',
date: new Date().toISOString(),
text: 'Harvey D ',
font: 'Charis SIL',
size: 15
}),
new TextRun('Contemporary Nursing Knowledge.')
]
})]
}]
});
const buf = await Packer.toBuffer(doc);
fs.writeFileSync('test-ref-revision.docx', buf);
const zip = await JSZip.loadAsync(buf);
const docXml = await zip.file('word/document.xml').async('string');
const settingsXml = await zip.file('word/settings.xml').async('string');
console.log('has w:ins:', docXml.includes('<w:ins'));
console.log('has w:del:', docXml.includes('<w:del'));
console.log('trackRevisions:', settingsXml.includes('trackRevisions'));

View File

@@ -165,6 +165,22 @@ const en = {
databases:'Database inclusion', databases:'Database inclusion',
}, },
journalCiteMate: {
button: 'Citation analysis',
yearRange: 'Time range',
yearOptionOne: 'Last {n} year',
yearOptionOther: 'Last {n} years',
yearPlaceholder: 'Enter years',
yearInvalid: 'Please enter a whole number greater than 0.',
yearMax: 'Time range cannot exceed 100 years.',
refresh: 'Refresh',
topicTitle: 'Topics',
countryTitle: 'country',
name: 'Name',
citeCount: 'Citations',
totalCites: 'Total citations',
loadFail: 'Failed to load citation analysis',
},
partyRole: { partyRole: {
identity: 'Identity', identity: 'Identity',
@@ -939,6 +955,8 @@ const en = {
exitSourceMode: 'Exit source mode', exitSourceMode: 'Exit source mode',
sourcePlaceholder: 'Paste or edit full HTML here (DOCTYPE, html, head, body supported)', sourcePlaceholder: 'Paste or edit full HTML here (DOCTYPE, html, head, body supported)',
editorPlaceholder: 'Please enter...', editorPlaceholder: 'Please enter...',
clearEditor: 'Clear',
clearEditorConfirm: 'Clear all email content?',
validateTo: 'Please add at least one addressee', validateTo: 'Please add at least one addressee',
validateSubject: 'Please enter mail subject', validateSubject: 'Please enter mail subject',
sendSuccess: 'Send success', sendSuccess: 'Send success',
@@ -1128,6 +1146,54 @@ const en = {
AnnotationList: 'Annotation List', AnnotationList: 'Annotation List',
Annotations: 'Comments', Annotations: 'Comments',
exportWord: 'Export Word', exportWord: 'Export Word',
exportManuscriptEmpty: 'No content to export.',
exportManuscriptSuccess: 'Word downloaded.',
exportManuscriptFail: 'Failed to export Word.',
citeRelevanceDetect: 'Download relevance HTML',
citeRelevanceTitle: 'Citation relevance check',
citeRelevanceTitleProgress: 'Citation relevance ({current}/{total})',
citeRelevanceEmpty: 'No citations to review.',
citeRelevanceNoContent: 'No manuscript content.',
citeRelevanceNoCitations: 'No citation markers found (e.g. [1], [1-7]).',
citeRelevanceLoadFail: 'Failed to load references.',
citeRelevanceParagraph: 'Paragraph {n}',
citeRelevanceRefLabel: 'Reference [{n}]',
citeRelevanceRefMissing: 'Reference not found',
citeRelevanceParagraphTitle: 'Paragraph',
citeRelevanceReferenceTitle: 'Reference [{n}]',
citeRelevanceWenaiTitle: 'Full prompt for WenAI',
citeRelevanceCopy: 'Copy',
citeRelevanceCopyWenai: 'Copy full prompt',
citeRelevanceCopySuccess: 'Copied to clipboard',
citeRelevanceCopyFail: 'Copy failed',
citeRelevanceCopyEmpty: 'Nothing to copy',
citeRelevancePrev: 'Previous',
citeRelevanceNext: 'Next',
citeRelevanceClose: 'Close',
citeRelevanceCopyAllWenai: 'Copy all prompts',
citeRelevanceBatchTip: '{n} references per batch within each paragraph',
citeRelevanceRefBatch: 'Batch refs {nums}',
citeRelevanceBatchProgress: 'Paragraph batch {current}/{total}',
citeRelevanceGroupTotal: '{n} paragraphs to review',
citeRelevanceGroupNavLabel: 'Para {paragraph} · {refs}',
citeRelevanceJumpTo: 'Jump to',
citeRelevanceCopyGroup: 'Copy paragraph',
citeRelevanceDownloadHtml: 'Download HTML',
citeRelevanceDownloadHtmlSuccess: 'HTML downloaded',
refHtmlDownload: 'Download references HTML',
refHtmlDownloadSuccess: 'References HTML downloaded',
refHtmlToWord: 'References HTML to Word',
refHtmlToWordSuccess: 'Word exported with edited references',
refHtmlToWordFail: 'Failed to convert references HTML to Word',
refHtmlLoadFail: 'Failed to load references',
refHtmlEmpty: 'No references found',
refHtmlParseEmpty: 'No reference entries found in HTML. Please use the HTML downloaded from this system.',
refHtmlPageTitle: 'References editor',
refHtmlReferencesTitle: 'References',
refHtmlIntro: 'Edit each reference directly, or paste AI output (supports <i> italics and other HTML). When done, click "Download edited HTML", then use "References HTML to Word" on the typesetting page.',
refHtmlSaveTip: 'Tip: save the HTML after editing, then import it on the typesetting page to export Word.',
refHtmlDownloadEdited: 'Download edited HTML',
refHtmlDownloadFileName: 'references-edited.html',
exportImg: 'Export PNG', exportImg: 'Export PNG',
PaperRotation: 'Paper Rotation', PaperRotation: 'Paper Rotation',
removeAnnotations: 'Are you sure you want to delete this Annotation?', removeAnnotations: 'Are you sure you want to delete this Annotation?',

View File

@@ -158,6 +158,22 @@ const zh = {
databases:'收录数据库', databases:'收录数据库',
}, },
journalCiteMate: {
button: '期刊引用分析',
yearRange: '统计范围',
yearOptionOne: '近 {n} 年',
yearOptionOther: '近 {n} 年',
yearPlaceholder: '输入年数',
yearInvalid: '请输入大于 0 的整数',
yearMax: '统计范围最多为 100 年',
refresh: '刷新',
topicTitle: '话题引用',
countryTitle: 'country',
name: '名称',
citeCount: '引用数量',
totalCites: '引用总数',
loadFail: '加载期刊引用分析失败',
},
paperArticleCount: { paperArticleCount: {
Periodroll: '期卷', Periodroll: '期卷',
article: '文章', article: '文章',
@@ -924,6 +940,8 @@ const zh = {
exitSourceMode: '退出源码编辑', exitSourceMode: '退出源码编辑',
sourcePlaceholder: '在此粘贴或编辑完整 HTML支持 <!DOCTYPE>、<html>、<head>、<body> 等', sourcePlaceholder: '在此粘贴或编辑完整 HTML支持 <!DOCTYPE>、<html>、<head>、<body> 等',
editorPlaceholder: '请输入邮件内容...', editorPlaceholder: '请输入邮件内容...',
clearEditor: '清空',
clearEditorConfirm: '确定清空邮件正文吗?',
validateTo: '请至少添加一个收件人', validateTo: '请至少添加一个收件人',
validateSubject: '请输入邮件主题', validateSubject: '请输入邮件主题',
sendSuccess: '发送成功', sendSuccess: '发送成功',
@@ -1114,6 +1132,54 @@ const zh = {
AnnotationList: '批注列表', AnnotationList: '批注列表',
Annotations: '批注', Annotations: '批注',
exportWord: '导出 Word', exportWord: '导出 Word',
exportManuscriptEmpty: '没有可导出的内容。',
exportManuscriptSuccess: 'Word 已下载。',
exportManuscriptFail: 'Word 导出失败。',
citeRelevanceDetect: '下载相关性 HTML',
citeRelevanceTitle: '引文相关性核查',
citeRelevanceTitleProgress: '引文相关性核查 ({current}/{total})',
citeRelevanceEmpty: '当前没有可核查的引文。',
citeRelevanceNoContent: '没有正文内容。',
citeRelevanceNoCitations: '正文中未找到引用标记(如 [1]、[1-7])。',
citeRelevanceLoadFail: '加载参考文献失败。',
citeRelevanceParagraph: '正文段落 {n}',
citeRelevanceRefLabel: '参考文献 [{n}]',
citeRelevanceRefMissing: '未找到对应文献',
citeRelevanceParagraphTitle: '正文段落',
citeRelevanceReferenceTitle: '参考文献 [{n}]',
citeRelevanceWenaiTitle: '复制到 WenAI 的完整提示',
citeRelevanceCopy: '复制',
citeRelevanceCopyWenai: '复制完整提示',
citeRelevanceCopySuccess: '已复制到剪贴板',
citeRelevanceCopyFail: '复制失败',
citeRelevanceCopyEmpty: '没有可复制的内容',
citeRelevancePrev: '上一条',
citeRelevanceNext: '下一条',
citeRelevanceClose: '关闭',
citeRelevanceCopyAllWenai: '复制全部提示',
citeRelevanceBatchTip: '每段正文按 {n} 条参考文献一批提问',
citeRelevanceRefBatch: '本批文献 {nums}',
citeRelevanceBatchProgress: '本段第 {current}/{total} 批',
citeRelevanceGroupTotal: '共 {n} 段待核查',
citeRelevanceGroupNavLabel: '段落 {paragraph} · {refs}',
citeRelevanceJumpTo: '快速跳转',
citeRelevanceCopyGroup: '复制本段',
citeRelevanceDownloadHtml: '下载 HTML',
citeRelevanceDownloadHtmlSuccess: 'HTML 已下载',
refHtmlDownload: '下载参考文献 HTML',
refHtmlDownloadSuccess: '参考文献 HTML 已下载',
refHtmlToWord: '参考文献 HTML 转 Word',
refHtmlToWordSuccess: 'Word 已导出(含编辑后的参考文献)',
refHtmlToWordFail: '参考文献 HTML 转 Word 失败',
refHtmlLoadFail: '加载参考文献失败',
refHtmlEmpty: '暂无参考文献',
refHtmlParseEmpty: 'HTML 中未找到参考文献条目,请使用本系统下载的参考文献 HTML',
refHtmlPageTitle: '参考文献编辑',
refHtmlReferencesTitle: 'References',
refHtmlIntro: '可直接编辑每条文献,或粘贴 AI 返回的内容(支持 <i> 斜体等 HTML 标签)。编辑完成后点击「下载编辑后的 HTML」再在排版页使用「参考文献 HTML 转 Word」。',
refHtmlSaveTip: '提示:编辑后请点击上方按钮保存 HTML也可在排版页导入该文件导出 Word。',
refHtmlDownloadEdited: '下载编辑后的 HTML',
refHtmlDownloadFileName: 'references-edited.html',
exportImg: '导出 图片', exportImg: '导出 图片',
PaperRotation: '纸张方向', PaperRotation: '纸张方向',
removeAnnotations: '确定要删除这条批注吗?', removeAnnotations: '确定要删除这条批注吗?',

View File

@@ -92,6 +92,7 @@
<div style="width: 100%; width: calc(100% - 260px); float: right; height: calc(100% - 0px); background-color: #e4e9ed"> <div style="width: 100%; width: calc(100% - 260px); float: right; height: calc(100% - 0px); background-color: #e4e9ed">
<common-word <common-word
:articleId="articleId" :articleId="articleId"
:pArticleId="pArticleId"
v-if="htmlContent" v-if="htmlContent"
ref="commonWord" ref="commonWord"
:value="htmlContent" :value="htmlContent"
@@ -464,6 +465,7 @@ export default {
ManuscirptContent: [], ManuscirptContent: [],
mathFormulasList: [], mathFormulasList: [],
articleId: this.$route.query.id, articleId: this.$route.query.id,
pArticleId: this.$route.query.p_article_id || '',
isShowComment: false, isShowComment: false,
urlList: { urlList: {
executeProofreading: 'api/Proofread/change', executeProofreading: 'api/Proofread/change',

View File

@@ -143,6 +143,21 @@
<i class="el-icon-connection" style="color: #4665b0"></i> {{ $t('menu.ClassificationmanagementInfo') }} <i class="el-icon-connection" style="color: #4665b0"></i> {{ $t('menu.ClassificationmanagementInfo') }}
</p> </p>
<p
@click="openJournalCiteMate(scope.row)"
style="
background-color: #fdf6ec;
color: #e6a23c;
cursor: pointer;
padding: 2px 2px;
font-weight: bold;
border-radius: 4px;
margin-bottom: 5px;
"
>
<i class="el-icon-data-analysis" style="color: #e6a23c"></i> {{ $t('journalCiteMate.button') }}
</p>
<p <p
@click=" @click="
openJournalAgreement({ openJournalAgreement({
@@ -382,6 +397,91 @@
> >
<common-agreement ref="commonAgreementRef" :journal="currentJournal" :urlList="urlList"></common-agreement> <common-agreement ref="commonAgreementRef" :journal="currentJournal" :urlList="urlList"></common-agreement>
</el-drawer> </el-drawer>
<el-drawer
append-to-body
destroy-on-close
custom-class="journal-cite-mate-drawer"
:title="$t('GroupClassification.Journal') + ' : ' + currentJournal.title"
:visible.sync="drawerCiteMate"
direction="rtl"
:before-close="handleCiteMateClose"
:wrapperClosable="false"
size="70%"
@opened="updateCiteMateTableHeight"
>
<div class="journal-cite-mate-panel" v-loading="citeMateLoading">
<div class="journal-cite-mate-toolbar">
<span class="journal-cite-mate-label">{{ $t('journalCiteMate.yearRange') }}</span>
<el-select
v-model="citeMateYear"
size="small"
class="journal-cite-mate-year-select"
filterable
allow-create
default-first-option
:placeholder="$t('journalCiteMate.yearPlaceholder')"
@change="handleCiteMateYearChange"
>
<el-option
v-for="item in citeMateYearOptionList"
:key="item"
:label="formatCiteMateYearLabel(item)"
:value="String(item)"
></el-option>
</el-select>
<el-button size="small" type="primary" plain icon="el-icon-refresh" @click="loadJournalCiteMate">
{{ $t('journalCiteMate.refresh') }}
</el-button>
<span v-if="citeMateCitesNum != null" class="journal-cite-mate-total">
{{ $t('journalCiteMate.totalCites') }}<strong>{{ citeMateCitesNum }}</strong>
</span>
</div>
<el-row :gutter="16" class="journal-cite-mate-body">
<el-col :span="12">
<div ref="citeMateBlock" class="journal-cite-mate-block">
<div class="journal-cite-mate-block-title">
<span class="journal-cite-mate-block-name">{{ $t('journalCiteMate.topicTitle') }}</span>
<span class="journal-cite-mate-count">{{ citeMateTopicList.length }}</span>
</div>
<el-table :data="citeMateTopicList" border size="mini" :height="citeMateTableHeight" empty-text="">
<el-table-column type="index" label="#" width="50" align="center"></el-table-column>
<el-table-column :label="$t('journalCiteMate.name')" min-width="160">
<template slot-scope="scope">
{{ getCiteMateRowName(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="$t('journalCiteMate.citeCount')" width="100" align="center">
<template slot-scope="scope">
{{ getCiteMateRowCount(scope.row) }}
</template>
</el-table-column>
</el-table>
</div>
</el-col>
<el-col :span="12">
<div class="journal-cite-mate-block">
<div class="journal-cite-mate-block-title">
<span class="journal-cite-mate-block-name">{{ $t('journalCiteMate.countryTitle') }}</span>
<span class="journal-cite-mate-count">{{ citeMateCountryList.length }}</span>
</div>
<el-table :data="citeMateCountryList" border size="mini" :height="citeMateTableHeight" empty-text="">
<el-table-column type="index" label="#" width="50" align="center"></el-table-column>
<el-table-column :label="$t('journalCiteMate.name')" min-width="160">
<template slot-scope="scope">
{{ getCiteMateRowName(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="$t('journalCiteMate.citeCount')" width="100" align="center">
<template slot-scope="scope">
{{ getCiteMateRowCount(scope.row) }}
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</div>
</el-drawer>
<el-dialog :title="$t('JournalCitationAnalysis.feeDialogTitle')" :visible.sync="feeDialogVisible" width="800px"> <el-dialog :title="$t('JournalCitationAnalysis.feeDialogTitle')" :visible.sync="feeDialogVisible" width="800px">
<el-alert <el-alert
:title="$t('JournalCitationAnalysis.feeWarningTitle')" :title="$t('JournalCitationAnalysis.feeWarningTitle')"
@@ -442,6 +542,23 @@ import commonclass from './class.vue';
import commonAgreement from './agreement.vue'; import commonAgreement from './agreement.vue';
import commonJournalInstallment from '../JournalInstallment/index.vue'; import commonJournalInstallment from '../JournalInstallment/index.vue';
import bus from '@/components/common/bus'; import bus from '@/components/common/bus';
function normalizeCiteMateLists(data) {
if (!data) {
return { topics: [], countries: [], citesNum: null };
}
if (Array.isArray(data)) {
return { topics: data, countries: [], citesNum: null };
}
const topics = data.topic || data.topics || data.topic_list || data.topicList || [];
const countries = data.country || data.countries || data.country_list || data.countryList || data.nation || data.nation_list || [];
const citesNum = data.cites_num != null ? data.cites_num : data.cite_num != null ? data.cite_num : data.total != null ? data.total : null;
return {
topics: Array.isArray(topics) ? topics : [],
countries: Array.isArray(countries) ? countries : [],
citesNum: citesNum
};
}
export default { export default {
name: 'app', name: 'app',
props: ['urlList', 'source'], props: ['urlList', 'source'],
@@ -500,6 +617,14 @@ export default {
drawer: false, drawer: false,
drawerJournalInstallment: false, drawerJournalInstallment: false,
drawerAgreement: false, drawerAgreement: false,
drawerCiteMate: false,
citeMateLoading: false,
citeMateYear: '2',
citeMateYearOptions: [1, 2, 3, 4, 5, 6],
citeMateTopicList: [],
citeMateCountryList: [],
citeMateCitesNum: null,
citeMateTableHeight: 420,
bd: true, bd: true,
ruleForm: { ruleForm: {
major_title: '', major_title: '',
@@ -607,6 +732,28 @@ export default {
this.ruleForm = {}; this.ruleForm = {};
} }
} }
},
drawerCiteMate(val) {
if (val) {
this.$nextTick(() => {
this.updateCiteMateTableHeight();
window.addEventListener('resize', this.updateCiteMateTableHeight);
});
} else {
window.removeEventListener('resize', this.updateCiteMateTableHeight);
}
}
},
computed: {
citeMateYearOptionList() {
const presets = this.citeMateYearOptions.slice();
const current = parseInt(this.citeMateYear, 10);
if (Number.isFinite(current) && current > 0 && presets.indexOf(current) === -1) {
presets.push(current);
}
return presets.sort(function (a, b) {
return a - b;
});
} }
}, },
async created() { async created() {
@@ -617,6 +764,9 @@ export default {
async activated() { async activated() {
await this.getDate(); await this.getDate();
}, },
beforeDestroy() {
window.removeEventListener('resize', this.updateCiteMateTableHeight);
},
methods: { methods: {
// 失去焦点事件 // 失去焦点事件
onEditorBlur(quill) { onEditorBlur(quill) {
@@ -960,6 +1110,118 @@ export default {
that.$refs.commonAgreementRef.init(); that.$refs.commonAgreementRef.init();
}); });
}, },
openJournalCiteMate(data) {
this.currentJournal = data || {};
this.citeMateYear = '2';
this.drawerCiteMate = true;
this.loadJournalCiteMate();
},
handleCiteMateClose(done) {
this.drawerCiteMate = false;
if (typeof done === 'function') {
done();
}
},
updateCiteMateTableHeight() {
this.$nextTick(() => {
const block = this.$refs.citeMateBlock;
if (!block) {
return;
}
const el = Array.isArray(block) ? block[0] : block;
const title = el.querySelector('.journal-cite-mate-block-title');
const style = window.getComputedStyle(el);
const paddingTop = parseFloat(style.paddingTop) || 0;
const paddingBottom = parseFloat(style.paddingBottom) || 0;
let titleHeight = 0;
if (title) {
const titleStyle = window.getComputedStyle(title);
titleHeight = title.offsetHeight + (parseFloat(titleStyle.marginBottom) || 0);
}
const nextHeight = el.clientHeight - paddingTop - paddingBottom - titleHeight;
if (nextHeight > 0) {
this.citeMateTableHeight = nextHeight;
}
});
},
formatCiteMateYearLabel(years) {
const num = parseInt(years, 10);
if (!Number.isFinite(num) || num < 1) {
return String(years);
}
if (num === 1) {
return this.$t('journalCiteMate.yearOptionOne', { n: num });
}
return this.$t('journalCiteMate.yearOptionOther', { n: num });
},
normalizeCiteMateYear(value) {
const parsed = parseInt(String(value || '').trim(), 10);
if (!Number.isFinite(parsed) || parsed < 1) {
this.$message.warning(this.$t('journalCiteMate.yearInvalid'));
return '2';
}
if (parsed > 100) {
this.$message.warning(this.$t('journalCiteMate.yearMax'));
return '100';
}
return String(parsed);
},
handleCiteMateYearChange(value) {
this.citeMateYear = this.normalizeCiteMateYear(value);
this.loadJournalCiteMate();
},
getCiteMateRowName(row) {
if (!row) {
return '—';
}
return row.topic_name || row.country || row.country_name || row.nation_name || row.name || row.title || '—';
},
getCiteMateRowCount(row) {
const value = row.num != null ? row.num : row.count != null ? row.count : row.cite_num;
return value != null ? value : '—';
},
loadJournalCiteMate() {
const journalId = this.currentJournal && this.currentJournal.journal_id;
if (!journalId) {
this.citeMateTopicList = [];
this.citeMateCountryList = [];
this.citeMateCitesNum = null;
return;
}
this.citeMateLoading = true;
this.$api
.post('api/journal/citeMate', {
journal_id: String(journalId),
year: String(this.citeMateYear || '2')
})
.then((res) => {
if (res.code == 0) {
const lists = normalizeCiteMateLists(res.data);
this.citeMateTopicList = lists.topics;
this.citeMateCountryList = lists.countries;
this.citeMateCitesNum = lists.citesNum;
} else {
this.citeMateTopicList = [];
this.citeMateCountryList = [];
this.citeMateCitesNum = null;
this.$message.error(res.msg || this.$t('journalCiteMate.loadFail'));
}
})
.catch((err) => {
console.error(err);
this.citeMateTopicList = [];
this.citeMateCountryList = [];
this.citeMateCitesNum = null;
this.$message.error(this.$t('journalCiteMate.loadFail'));
})
.finally(() => {
this.citeMateLoading = false;
if (this.drawerCiteMate) {
this.updateCiteMateTableHeight();
}
});
},
renameKeys(obj, oldKey, newKey) { renameKeys(obj, oldKey, newKey) {
if (obj[oldKey]) { if (obj[oldKey]) {
obj[newKey] = obj[oldKey]; obj[newKey] = obj[oldKey];
@@ -1457,6 +1719,100 @@ export default {
padding: 22px 0 0 0; padding: 22px 0 0 0;
text-align: center; text-align: center;
} }
.journal-cite-mate-panel {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 0 20px 20px;
box-sizing: border-box;
}
.journal-cite-mate-drawer .el-drawer__body {
display: flex;
flex-direction: column;
overflow: hidden;
padding-bottom: 0;
}
.journal-cite-mate-body {
flex: 1;
min-height: 0;
}
.journal-cite-mate-body.el-row {
display: flex;
flex-wrap: nowrap;
height: 100%;
}
.journal-cite-mate-body > .el-col {
display: flex;
flex-direction: column;
height: 100%;
}
.journal-cite-mate-block {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
box-sizing: border-box;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
}
.journal-cite-mate-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-shrink: 0;
}
.journal-cite-mate-year-select {
width: 150px;
}
.journal-cite-mate-label {
font-size: 13px;
color: #606266;
}
.journal-cite-mate-block-title {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.journal-cite-mate-block-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.journal-cite-mate-count {
font-weight: 600;
color: #409eff;
font-size: 16px;
line-height: 1;
}
.journal-cite-mate-total {
margin-left: auto;
font-size: 14px;
color: #303133;
}
.journal-cite-mate-total strong {
color: #409eff;
font-size: 16px;
}
.WeChatCode .avatar-uploader .el-upload { .WeChatCode .avatar-uploader .el-upload {
width: 120px !important; width: 120px !important;
height: 120px !important; height: 120px !important;

View File

@@ -1421,7 +1421,8 @@ export default {
this.$router.resolve({ this.$router.resolve({
path: '/GenerateCharts', path: '/GenerateCharts',
query: { query: {
id: this.$route.query.id id: this.$route.query.id,
p_article_id: this.p_article_id
} }
}).href, }).href,
'_blank' '_blank'

View File

@@ -54,7 +54,21 @@
<!-- 基本信息 --> <!-- 基本信息 -->
<div :ref="tabsList[0].refName" class="scroll-item"> <div :ref="tabsList[0].refName" class="scroll-item">
<div class="bor_style_onli" style="margin: 20px 0"> <div class="bor_style_onli" style="margin: 20px 0">
<h4>{{ tabsList[0].name }}</h4> <h4>
{{ tabsList[0].name }}
<el-button
v-if="zyModeEnabled"
style="float: right"
type="warning"
plain
icon="el-icon-download"
:loading="importingSubmission"
:disabled="!detailMes.article_id"
@click="importFromSubmission"
>
Import from Submission
</el-button>
</h4>
<el-form ref="Mes_Form" :model="detailMes" :rules="rules" label-width="220px"> <el-form ref="Mes_Form" :model="detailMes" :rules="rules" label-width="220px">
<el-form-item label="Title :" prop="title"> <el-form-item label="Title :" prop="title">
<el-input v-model="detailMes.title"></el-input> <el-input v-model="detailMes.title"></el-input>
@@ -235,7 +249,14 @@
Add Author</el-button Add Author</el-button
> >
</h4> </h4>
<el-table :data="authorData" border class="table" ref="multipleTable" header-cell-class-name="table-header"> <el-table
:key="'author-' + authorTableKey"
:data="authorData"
border
class="table"
ref="authorTable"
header-cell-class-name="table-header"
>
<el-table-column prop="author_name" label="Author"></el-table-column> <el-table-column prop="author_name" label="Author"></el-table-column>
<el-table-column label="First author" align="center" width="100px"> <el-table-column label="First author" align="center" width="100px">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -454,7 +475,14 @@
> >
</h4> </h4>
<!-- 机构列表 --> <!-- 机构列表 -->
<el-table :data="schoolData" border class="table" ref="multipleTable" header-cell-class-name="table-header"> <el-table
:key="'school-' + schoolTableKey"
:data="schoolData"
border
class="table"
ref="schoolTable"
header-cell-class-name="table-header"
>
<el-table-column type="index" label="No." width="55" align="center"></el-table-column> <el-table-column type="index" label="No." width="55" align="center"></el-table-column>
<el-table-column prop="organ_name" label="Affiliation"></el-table-column> <el-table-column prop="organ_name" label="Affiliation"></el-table-column>
<el-table-column label="" width="190" align="center"> <el-table-column label="" width="190" align="center">
@@ -952,6 +980,79 @@
</div> </div>
</div> </div>
<!-- 导入前作者预览 -->
<el-dialog
title="Review Authors Before Import"
:visible.sync="importAuthorPreviewVisible"
width="960px"
:close-on-click-modal="false"
>
<p style="color: #666; line-height: 22px; margin: 0 0 12px 0">
Check recognized authors, adjust order manually, or paste a name sequence to auto-sort. Required empty fields will be saved as a space.
</p>
<el-input
v-model="importAuthorOrderText"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
placeholder="eg: Sijing PENG1,¶, Ting ZHENG1,¶, Jin CONG1, Xiang WANG1, Chuanying ZHANG1, Jiejie GE1,2*"
></el-input>
<div style="margin: 10px 0 16px 0">
<el-button size="mini" type="primary" plain @click="applyImportAuthorOrder">Apply Order</el-button>
</div>
<el-table :data="importPreviewAuthors" border size="small" max-height="360">
<el-table-column type="index" label="No." width="50" align="center"></el-table-column>
<el-table-column label="First name" min-width="120">
<template slot-scope="scope">
{{ scope.row.firstname || scope.row.first_name }}
</template>
</el-table-column>
<el-table-column label="Last name" min-width="100">
<template slot-scope="scope">
{{ scope.row.lastname || scope.row.last_name }}
</template>
</el-table-column>
<el-table-column label="First author" width="100" align="center">
<template slot-scope="scope">
<el-tag round :type="Number(scope.row.is_super) === 1 ? 'success' : 'danger'" size="mini">
{{ Number(scope.row.is_super) === 1 ? '√' : '×' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="Corresponding" width="110" align="center">
<template slot-scope="scope">
<el-tag round :type="Number(scope.row.is_report) === 1 ? 'success' : 'danger'" size="mini">
{{ Number(scope.row.is_report) === 1 ? '√' : '×' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="email" label="Email" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
{{ Number(scope.row.is_report) === 1 ? scope.row.email || ' ' : '' }}
</template>
</el-table-column>
<el-table-column label="Affiliation" min-width="220" show-overflow-tooltip>
<template slot-scope="scope">
{{ getImportAuthorAffiliation(scope.row) }}
</template>
</el-table-column>
<el-table-column label="Order" width="100" align="center">
<template slot-scope="scope">
<el-button type="text" :disabled="scope.$index === 0" @click="moveImportAuthor(scope.$index, 'up')">Up</el-button>
<el-button
type="text"
:disabled="scope.$index === importPreviewAuthors.length - 1"
@click="moveImportAuthor(scope.$index, 'down')"
>Down</el-button
>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="importAuthorPreviewVisible = false">Cancel</el-button>
<el-button type="primary" :loading="importingSubmission" @click="confirmImportFromSubmission">Confirm Import</el-button>
</span>
</el-dialog>
<!-- 上线弹出框 ---> <!-- 上线弹出框 --->
<el-dialog title="Tips" :visible.sync="onlineVisible" width="680px" :close-on-click-modal="false"> <el-dialog title="Tips" :visible.sync="onlineVisible" width="680px" :close-on-click-modal="false">
<p style="line-height: 24px">Are you sure you want to push this manuscript to the official website? You can preview first.</p> <p style="line-height: 24px">Are you sure you want to push this manuscript to the official website? You can preview first.</p>
@@ -1067,9 +1168,31 @@ import comArtHtmlCreatNewProduce from './comArtHtmlCreatNewProduce.vue';
import commonTopic from '../page/components/article/topic.vue'; import commonTopic from '../page/components/article/topic.vue';
import commonRelated from '../page/components/article/Related.vue'; import commonRelated from '../page/components/article/Related.vue';
import Tinymce from '@/components/page/components/Tinymce'; import Tinymce from '@/components/page/components/Tinymce';
import {
mapEssentialFields,
buildOrganIdMap,
buildProductionAuthorPayload,
buildAuthorReferenceAbbr,
collectSubmissionOrgans,
resolveSubmissionAuthors,
normalizeSubmissionAuthors,
normalizeRichTextContent,
sortAuthorsByOrderString,
moveAuthorInList,
getAuthorDisplayName,
getAuthorAffiliationPreview
} from '@/utils/productionSubmissionImport';
import { isZyModeEnabled } from '@/utils/zyMode';
export default { export default {
data() { data() {
return { return {
importingSubmission: false,
importAuthorPreviewVisible: false,
importAuthorOrderText: '',
importPreviewAuthors: [],
importPreviewContext: null,
authorTableKey: 0,
schoolTableKey: 0,
isGenerating: false, // 按钮加载状态 isGenerating: false, // 按钮加载状态
percentage: 0, // 进度条 percentage: 0, // 进度条
finalReview: null, finalReview: null,
@@ -1609,6 +1732,19 @@ export default {
updateChange(type, content) { updateChange(type, content) {
this[type] = content; this[type] = content;
}, },
applyRichTextToEditor(refName, field, content) {
const normalized = normalizeRichTextContent(content);
this[field] = normalized;
if (field === 'abstract') {
this.detailMes.abstract = normalized;
}
this.$nextTick(() => {
const editor = this.$refs[refName];
if (editor && typeof editor.setContent === 'function') {
editor.setContent(normalized);
}
});
},
getTinymceContent(type) { getTinymceContent(type) {
this.$refs.tinymceChild1.getContent(type); this.$refs.tinymceChild1.getContent(type);
@@ -1905,34 +2041,278 @@ export default {
}, },
getAuthorJG() { getAuthorJG() {
// 获取作者列表 return this.refreshAuthorOrganLists().catch((err) => {
this.$api
.post('api/Production/getAuthorlist', this.idform)
.then((res) => {
if (res.code == 0) {
this.authorData = res.data.authors;
} else {
this.$message.error(res.msg);
}
})
.catch((err) => {
this.$message.error(err); this.$message.error(err);
}); });
},
refreshAuthorOrganLists() {
this.idform.p_article_id = this.p_article_id;
return Promise.all([
this.$api.post('api/Production/getAuthorlist', this.idform),
this.$api.post('api/Production/getOrganList', this.idform)
]).then(([authorRes, organRes]) => {
if (authorRes.code == 0) {
this.$set(this, 'authorData', [...(authorRes.data.authors || [])]);
} else {
throw authorRes.msg || 'Failed to load authors';
}
if (organRes.code == 0) {
this.$set(this, 'schoolData', [...(organRes.data.organs || [])]);
this.$set(this, 'mechanism', [...(organRes.data.organs || [])]);
} else {
throw organRes.msg || 'Failed to load affiliations';
}
});
},
fetchAuthorList() {
this.idform.p_article_id = this.p_article_id;
return this.$api.post('api/Production/getAuthorlist', this.idform).then((authorRes) => {
if (authorRes.code == 0) {
this.$set(this, 'authorData', [...(authorRes.data.authors || [])]);
return authorRes;
}
throw authorRes.msg || 'Failed to load authors';
});
},
fetchOrganList() {
this.idform.p_article_id = this.p_article_id;
return this.$api.post('api/Production/getOrganList', this.idform).then((organRes) => {
if (organRes.code == 0) {
this.$set(this, 'schoolData', [...(organRes.data.organs || [])]);
this.$set(this, 'mechanism', [...(organRes.data.organs || [])]);
return organRes;
}
throw organRes.msg || 'Failed to load affiliations';
});
},
async refreshAuthorSection(options = {}) {
const { authorsOnly = false, organsOnly = false } = options;
if (organsOnly) {
await this.fetchOrganList();
this.schoolTableKey += 1;
} else if (authorsOnly) {
await this.fetchAuthorList();
this.authorTableKey += 1;
} else {
await this.refreshAuthorOrganLists();
this.authorTableKey += 1;
this.schoolTableKey += 1;
}
await this.$nextTick();
this.$forceUpdate();
await this.$nextTick();
if (this.$refs.authorTable && this.$refs.authorTable.doLayout) {
this.$refs.authorTable.doLayout();
}
if (this.$refs.schoolTable && this.$refs.schoolTable.doLayout) {
this.$refs.schoolTable.doLayout();
}
},
getImportAuthorName(author) {
return getAuthorDisplayName(author);
},
getImportAuthorAffiliation(author) {
return getAuthorAffiliationPreview(author);
},
applyImportAuthorOrder() {
if (!this.importAuthorOrderText.trim()) {
this.$message.warning('Please paste an author order string first.');
return;
}
this.importPreviewAuthors = sortAuthorsByOrderString(this.importPreviewAuthors, this.importAuthorOrderText);
this.$message.success('Author order updated.');
},
moveImportAuthor(index, direction) {
this.importPreviewAuthors = moveAuthorInList(this.importPreviewAuthors, index, direction);
},
async fetchSubmissionImportContext(articleId) {
const detailRes = await this.$api.post('api/Article/getArticleDetail', {
articleId,
human: 'editor'
});
const article = (detailRes && detailRes.article) || {};
const userId = article.user_id;
// 获取机构列表 const requestList = [this.$api.post('api/Article/getAuthors', { article_id: articleId }).catch(() => null)];
this.$api if (userId) {
.post('api/Production/getOrganList', this.idform) requestList.push(
.then((res) => { this.$api.post('api/Organ/lists', { article_id: articleId, user_id: userId }).catch(() => null)
if (res.code == 0) { );
this.schoolData = res.data.organs;
this.mechanism = res.data.organs;
} else {
this.$message.error(res.msg);
} }
}) const [authorsRes, organsRes] = await Promise.all(requestList);
.catch((err) => {
this.$message.error(err); const authors = normalizeSubmissionAuthors(resolveSubmissionAuthors(authorsRes, detailRes));
const submissionOrgans = collectSubmissionOrgans(
organsRes && Number(organsRes.status) === 1 && Array.isArray(organsRes.data) ? organsRes.data : [],
authors
);
const mapped = mapEssentialFields(article, authors);
return {
article,
authors,
submissionOrgans,
mapped
};
},
applyImportedEssentialFields(mapped) {
this.detailMes.title = mapped.title;
this.detailMes.type = mapped.type;
this.detailMes.keywords = mapped.keywords;
this.detailMes.acknowledgment = mapped.acknowledgment;
this.detailMes.author_contribution = mapped.author_contribution;
this.detailMes.abbreviation = mapped.abbreviation;
this.applyRichTextToEditor('tinymceChild1', 'abstract', mapped.abstract);
},
async importFromSubmission() {
const articleId = this.detailMes && this.detailMes.article_id;
if (!articleId) {
this.$message.error('No linked submission article found.');
return;
}
const loading = this.$loading({
lock: true,
text: 'Loading submission data...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
}); });
this.importingSubmission = true;
try {
const context = await this.fetchSubmissionImportContext(articleId);
this.importPreviewContext = context;
this.importPreviewAuthors = [...context.authors];
this.importAuthorOrderText = '';
this.importAuthorPreviewVisible = true;
} catch (err) {
console.error(err);
this.$message.error(typeof err === 'string' ? err : 'Failed to load submission data.');
} finally {
loading.close();
this.importingSubmission = false;
}
},
async confirmImportFromSubmission() {
if (!this.importPreviewContext) {
return;
}
const loading = this.$loading({
lock: true,
text: 'Importing from submission...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
this.importingSubmission = true;
try {
const { submissionOrgans, mapped } = this.importPreviewContext;
const authors = [...this.importPreviewAuthors];
this.applyImportedEssentialFields(mapped);
let organIdMap = {};
let authorImported = false;
let affiliationImported = false;
let addedAuthorCount = 0;
const importedAuthors = [];
if (!this.schoolData.length && submissionOrgans.length) {
for (const organ of submissionOrgans) {
const organName = String(organ.organ_name || '').trim();
if (!organName) {
continue;
}
const addRes = await this.$api.post('api/Production/addAuthorOrgan', {
p_article_id: this.p_article_id,
organ_name: organName
});
if (addRes.code != 0) {
throw addRes.msg || 'Failed to add affiliation';
}
}
affiliationImported = true;
await this.refreshAuthorSection({ organsOnly: true });
}
organIdMap = buildOrganIdMap(submissionOrgans, this.schoolData);
if (!this.authorData.length && authors.length) {
if (!this.schoolData.length) {
throw 'Please import affiliations before authors.';
}
for (const author of authors) {
const payload = buildProductionAuthorPayload(
author,
this.p_article_id,
organIdMap,
this.schoolData
);
if (!payload.first_name || !payload.last_name) {
continue;
}
if (!payload.organs.length) {
throw `Author ${payload.first_name} ${payload.last_name} has no matched affiliation.`;
}
const addRes = await this.$api.post('api/Production/addAuthor', payload);
if (addRes.code != 0) {
throw addRes.msg || `Failed to add author ${payload.first_name} ${payload.last_name}`;
}
addedAuthorCount += 1;
importedAuthors.push({
...author,
firstname: payload.first_name,
first_name: payload.first_name,
lastname: payload.last_name,
last_name: payload.last_name
});
}
if (addedAuthorCount > 0) {
authorImported = true;
this.detailMes.abbr = buildAuthorReferenceAbbr(importedAuthors);
await this.refreshAuthorSection({ authorsOnly: true });
}
}
if (affiliationImported && !authorImported) {
await this.refreshAuthorSection({ organsOnly: true });
}
const messages = ['Essential Information filled from submission.'];
if (affiliationImported) {
messages.push('Affiliations imported.');
} else if (submissionOrgans.length && this.schoolData.length) {
messages.push('Affiliations already exist, skipped.');
}
if (authorImported) {
messages.push(`${addedAuthorCount} author(s) imported.`);
} else if (authors.length && this.authorData.length) {
messages.push('Authors already exist, skipped.');
} else if (authors.length && !addedAuthorCount) {
messages.push('No authors were imported. Please check author data.');
}
messages.push('Please review the fields and click Save Essential Information.');
this.$message.success(messages.join(' '));
this.importAuthorPreviewVisible = false;
this.importPreviewContext = null;
if (authorImported || affiliationImported) {
await this.$nextTick();
this.jumpTab(1, this.tabsList[1]);
} else {
await this.$nextTick();
this.$forceUpdate();
this.jumpTab(0, this.tabsList[0]);
}
} catch (err) {
console.error(err);
this.$message.error(typeof err === 'string' ? err : 'Import failed. Please try again.');
} finally {
loading.close();
this.importingSubmission = false;
}
}, },
// 1----时间初始化 // 1----时间初始化
@@ -2125,7 +2505,7 @@ export default {
this.addAuthor = false; this.addAuthor = false;
this.$refs.add_Author.resetFields(); this.$refs.add_Author.resetFields();
this.addFomauthor.organs = []; this.addFomauthor.organs = [];
this.getAuthorJG(); this.refreshAuthorSection({ authorsOnly: true });
} else { } else {
this.$message.error(res.msg); this.$message.error(res.msg);
} }
@@ -2213,7 +2593,7 @@ export default {
if (res.code == 0) { if (res.code == 0) {
this.addSchool = false; this.addSchool = false;
this.$refs.add_School.resetFields(); this.$refs.add_School.resetFields();
this.getAuthorJG(); this.refreshAuthorSection({ organsOnly: true });
} else { } else {
this.$message.error(res.msg); this.$message.error(res.msg);
} }
@@ -3210,6 +3590,11 @@ export default {
// this.jumpTab(0,this.tabsList[0]) // this.jumpTab(0,this.tabsList[0])
} }
}, },
computed: {
zyModeEnabled() {
return isZyModeEnabled(this.$route);
}
},
components: { components: {
quillEditor, quillEditor,
Tinymce, Tinymce,

View File

@@ -204,7 +204,8 @@ export default {
goEdit() { goEdit() {
window.open(this.$router.resolve({ path: '/GenerateCharts', window.open(this.$router.resolve({ path: '/GenerateCharts',
query: { query: {
id: this.detailMes.article_id id: this.detailMes.article_id,
p_article_id: this.Art_Id
} }).href, '_blank'); } }).href, '_blank');
}, },

View File

@@ -0,0 +1,87 @@
<template>
<span v-if="zyModeEnabled" class="ref-search-links">
<template v-if="links.isBook">
<a
v-if="links.googleLink"
class="ref-search-link"
:href="links.googleLink"
target="_blank"
rel="noopener noreferrer"
@click.stop
>
<i class="el-icon-search"></i>
</a>
</template>
<template v-else>
<a
v-if="links.pubmedLink"
class="ref-search-link"
:href="links.pubmedLink"
target="_blank"
rel="noopener noreferrer"
@click.stop
>
<i class="el-icon-search"></i>
</a>
<a
v-else-if="links.scholarLink"
class="ref-search-link"
:href="links.scholarLink"
target="_blank"
rel="noopener noreferrer"
@click.stop
>
<i class="el-icon-search"></i>
</a>
</template>
</span>
</template>
<script>
import { getReferenceLinksFromRow } from '@/utils/referenceLinks';
import { isZyModeEnabled } from '@/utils/zyMode';
export default {
name: 'ReferenceSearchLinks',
props: {
row: {
type: Object,
required: true
}
},
computed: {
zyModeEnabled() {
return isZyModeEnabled(this.$route);
},
links() {
return getReferenceLinksFromRow(this.row);
}
}
};
</script>
<style scoped>
.ref-search-links {
display: inline-flex;
align-items: center;
gap: 2px;
margin-left: 4px;
vertical-align: middle;
}
.ref-search-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
color: #909399;
font-size: 11px;
line-height: 1;
text-decoration: none;
}
.ref-search-link:hover {
color: #409eff;
}
</style>

View File

@@ -13,6 +13,7 @@ export default {
value: { type: String, default: '' }, value: { type: String, default: '' },
id: { type: String, default: () => 'tiny-' + +new Date() }, id: { type: String, default: () => 'tiny-' + +new Date() },
showSelectTemplateButton: { type: Boolean, default: false }, showSelectTemplateButton: { type: Boolean, default: false },
showEmptyButton: { type: Boolean, default: true },
// 仅在需要的页面开启“安全初始化内容” // 仅在需要的页面开启“安全初始化内容”
useSafeInitContent: { type: Boolean, default: false } useSafeInitContent: { type: Boolean, default: false }
}, },
@@ -121,6 +122,8 @@ autoInlineStyles(htmlContent) {
initTiny() { initTiny() {
const vueInstance = this; const vueInstance = this;
const hasTemplateBtn = this.showSelectTemplateButton; const hasTemplateBtn = this.showSelectTemplateButton;
const hasEmptyBtn = this.showEmptyButton;
const toolbarSuffix = hasEmptyBtn ? ' | clearButton | fullscreen' : ' | fullscreen';
window.tinymce.init({ window.tinymce.init({
selector: `#${this.tinymceId}`, selector: `#${this.tinymceId}`,
language: 'zh_CN', // 记得下载语言包,没有就删掉这行 language: 'zh_CN', // 记得下载语言包,没有就删掉这行
@@ -137,8 +140,11 @@ autoInlineStyles(htmlContent) {
// 按照图片布局精准排序: // 按照图片布局精准排序:
toolbar: hasTemplateBtn toolbar: hasTemplateBtn
? 'undo redo | selectTemplate | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link | fullscreen' ? 'undo redo | selectTemplate | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link' +
: 'undo redo | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link | fullscreen', toolbarSuffix
: 'undo redo | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link' +
toolbarSuffix,
menubar: 'file edit view insert format tools table',
// 补充:让字体和字号选择器更有序 // 补充:让字体和字号选择器更有序
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px', fontsize_formats: '12px 14px 16px 18px 24px 36px 48px',
@@ -158,6 +164,16 @@ autoInlineStyles(htmlContent) {
}); });
} }
if (hasEmptyBtn) {
editor.ui.registry.addButton('clearButton', {
text: 'Empty',
onAction() {
editor.setContent('');
vueInstance.$emit('input', '');
}
});
}
// 统一给自定义按钮加样式(无按钮时不会产生影响) // 统一给自定义按钮加样式(无按钮时不会产生影响)
CommonJS.inTinymceButtonClass(); CommonJS.inTinymceButtonClass();
}, },
@@ -177,7 +193,6 @@ autoInlineStyles(htmlContent) {
} }
}); });
}, },
// 假设 rawHtml 是你那一大串完整的 HTML // 假设 rawHtml 是你那一大串完整的 HTML
prepareContentForEditor(rawHtml) { prepareContentForEditor(rawHtml) {
// 1. 分离出 Head 及其之前的部分 (包含 doctype, html, head) // 1. 分离出 Head 及其之前的部分 (包含 doctype, html, head)

View File

@@ -29,7 +29,7 @@
<span v-html="renderText(processedItem.title)"></span> <span v-html="renderText(processedItem.title)"></span>
</div> </div>
<table border="1" class="custom-table-core"> <table class="custom-table-core">
<tbody> <tbody>
<tr v-for="(row, rIdx) in processedItem.table.tableHeader" :key="'h-' + rIdx" class="table-header-row"> <tr v-for="(row, rIdx) in processedItem.table.tableHeader" :key="'h-' + rIdx" class="table-header-row">
<td v-for="(cell, cIdx) in row" :key="'hc-'+cIdx" :rowspan="cell.rowspan" :colspan="cell.colspan"> <td v-for="(cell, cIdx) in row" :key="'hc-'+cIdx" :rowspan="cell.rowspan" :colspan="cell.colspan">
@@ -201,7 +201,7 @@ export default {
min-width: 794px; /* A4 宽度参考 */ min-width: 794px; /* A4 宽度参考 */
max-width: 90vw; max-width: 90vw;
background-color: #fff; background-color: #fff;
padding: 10px 20px; padding: 2.54cm 1.91cm;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 12px 40px rgba(0,0,0,0.5); box-shadow: 0 12px 40px rgba(0,0,0,0.5);
border-radius: 4px; border-radius: 4px;
@@ -243,8 +243,20 @@ export default {
} }
.custom-table-core { .custom-table-core {
width: 100%; width: 98.3%;
margin: 0 auto;
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0;
table-layout: auto;
font-family: 'Charis SIL', serif;
font-size: 7.5pt;
}
.table-paper-view ::v-deep .custom-table-core tr td {
padding: 0 0.19cm;
line-height: 10pt;
vertical-align: middle;
text-align: left;
} }
.font { .font {
@@ -258,22 +270,56 @@ export default {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
color: rgb(210, 90, 90); color: rgb(210, 90, 90);
font-size: 14px; font-size: 13px;
font-family: 'Charis SIL', serif;
line-height: 13px;
margin: 0;
padding: 0;
}
.imageTitle {
text-align: justify;
} }
.tableNote, .imageNote { .tableNote, .imageNote {
color: #333; color: #333;
margin-top: 15px; margin-top: 15px;
font-size: 7.5pt;
font-family: 'Charis SIL', serif;
line-height: 10pt;
} }
.imageNote { .imageNote {
text-align: center; text-align: justify;
} }
.oddColor td { .oddColor td {
background: rgb(250, 231, 232) !important; background: rgb(250, 231, 232) !important;
} }
.table-paper-view ::v-deep .custom-table-core tr.table-header-row td span + span::before {
content: ' ';
}
.table-paper-view ::v-deep .custom-table-core tr.table-header-row td {
font-size: 7.5pt;
font-family: 'Charis SIL', serif;
border-left: none;
border-right: none;
border-top: 0.5pt solid #000;
border-bottom: 0.5pt solid #000;
}
.table-paper-view ::v-deep .custom-table-core tr.table-content-row td {
font-size: 7.5pt;
font-family: 'Charis SIL', serif;
border: none;
}
.table-paper-view ::v-deep .custom-table-core tr.table-content-row:last-child td {
border-bottom: 0.5pt solid #000;
}
.table-fade-enter-active, .table-fade-leave-active { transition: opacity 0.3s ease; } .table-fade-enter-active, .table-fade-leave-active { transition: opacity 0.3s ease; }
.table-fade-enter, .table-fade-leave-to { opacity: 0; } .table-fade-enter, .table-fade-leave-to { opacity: 0; }

View File

@@ -74,6 +74,85 @@
<i class="el-icon-document base-margin" :style="{ '--m-r': '2px' }"> </i> <i class="el-icon-document base-margin" :style="{ '--m-r': '2px' }"> </i>
{{ $t('commonTable.BatchAddcontent') }} {{ $t('commonTable.BatchAddcontent') }}
</li> </li>
<li
v-if="zyModeEnabled"
@click="handleExportManuscriptWord"
class="base-font-size base-bg-imp base-padding-all"
:style="{ '--f-s': '12px', '--f-c': '#333' }"
>
<i
class="el-icon-download base-margin"
:style="{ '--m-r': '2px' }"
v-if="!exportingManuscriptWord"
></i>
<i
class="el-icon-loading base-margin"
:style="{ '--m-r': '2px' }"
v-else
></i>
{{ $t('commonTable.exportWord') }}
</li>
<li
v-if="zyModeEnabled"
@click="handleDownloadReferencesHtml"
class="base-font-size base-bg-imp base-padding-all"
:style="{ '--f-s': '12px', '--f-c': '#333' }"
>
<i
class="el-icon-document base-margin"
:style="{ '--m-r': '2px' }"
v-if="!referencesHtmlLoading"
></i>
<i
class="el-icon-loading base-margin"
:style="{ '--m-r': '2px' }"
v-else
></i>
{{ $t('commonTable.refHtmlDownload') }}
</li>
<li
v-if="zyModeEnabled"
@click="triggerReferencesHtmlToWord"
class="base-font-size base-bg-imp base-padding-all"
:style="{ '--f-s': '12px', '--f-c': '#333' }"
>
<i
class="el-icon-upload2 base-margin"
:style="{ '--m-r': '2px' }"
v-if="!exportingReferencesHtmlWord"
></i>
<i
class="el-icon-loading base-margin"
:style="{ '--m-r': '2px' }"
v-else
></i>
{{ $t('commonTable.refHtmlToWord') }}
</li>
<input
ref="referencesHtmlFileInput"
type="file"
accept=".html,text/html"
style="display: none"
@change="handleReferencesHtmlToWord"
/>
<li
v-if="zyModeEnabled"
@click="handleOpenCitationRelevance"
class="base-font-size base-bg-imp base-padding-all"
:style="{ '--f-s': '12px', '--f-c': '#333' }"
>
<i
class="el-icon-connection base-margin"
:style="{ '--m-r': '2px' }"
v-if="!citationRelevanceLoading"
></i>
<i
class="el-icon-loading base-margin"
:style="{ '--m-r': '2px' }"
v-else
></i>
{{ $t('commonTable.citeRelevanceDetect') }}
</li>
</ul> </ul>
</div> </div>
<li <li
@@ -1064,6 +1143,18 @@ import { TableUtils } from '@/common/js/TableUtils';
import { debounce, throttle } from '@/common/js/debounce'; import { debounce, throttle } from '@/common/js/debounce';
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles'; import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
import LatexDataPanel from './LatexDataPanel.vue'; import LatexDataPanel from './LatexDataPanel.vue';
import { downloadManuscriptWord, fetchManuscriptReferenceList } from '@/utils/exportManuscriptWord';
import {
buildReferencesHtmlLabels,
downloadReferencesEditableHtml,
parseReferencesEditableHtml
} from '@/utils/manuscriptReferenceHtml';
import {
buildCitationReviewQueue,
buildCitationReviewHtmlLabels,
downloadCitationReviewHtml
} from '@/utils/manuscriptCitationRelevance';
import { isZyModeEnabled } from '@/utils/zyMode';
export default { export default {
name: 'tinymce', name: 'tinymce',
components: { components: {
@@ -1076,6 +1167,9 @@ export default {
articleId: { articleId: {
default: '' default: ''
}, },
pArticleId: {
default: ''
},
isPreview: { isPreview: {
type: Boolean, type: Boolean,
default() { default() {
@@ -1185,6 +1279,11 @@ export default {
imagePath: require('@/assets/img/carriageReturn.png'), // 或者你可以设置其他路径 imagePath: require('@/assets/img/carriageReturn.png'), // 或者你可以设置其他路径
scrollPosition: 0, scrollPosition: 0,
wordList: [], wordList: [],
exportingManuscriptWord: false,
referencesHtmlLoading: false,
exportingReferencesHtmlWord: false,
citationRelevanceLoading: false,
manuscriptReferences: [],
proofreadingList: [], proofreadingList: [],
commentList: [], commentList: [],
visibleDivs: [], // 存储当前可见的 div id visibleDivs: [], // 存储当前可见的 div id
@@ -1309,6 +1408,9 @@ export default {
} }
}, },
computed: { computed: {
zyModeEnabled() {
return isZyModeEnabled(this.$route);
},
sortedProofreadingList() { sortedProofreadingList() {
const order = [2, 1, 3]; const order = [2, 1, 3];
const rank = { 2: 0, 1: 1, 3: 2 }; const rank = { 2: 0, 1: 1, 3: 2 };
@@ -1444,6 +1546,170 @@ export default {
this.editors = {}; this.editors = {};
}, },
methods: { methods: {
async handleExportManuscriptWord() {
if (this.exportingManuscriptWord) {
return;
}
if (!this.wordList || !this.wordList.length) {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty') || 'No content to export.');
return;
}
this.exportingManuscriptWord = true;
try {
await downloadManuscriptWord(this.wordList, this.mediaUrl, 'manuscript', {
fetchReferences: true,
apiClient: this.$api,
articleId: this.articleId,
pArticleId: this.pArticleId
});
this.$message.success(this.$t('commonTable.exportManuscriptSuccess') || 'Word downloaded.');
} catch (err) {
console.error(err);
if (err && err.message === 'NO_CONTENT') {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty') || 'No content to export.');
} else {
this.$message.error(this.$t('commonTable.exportManuscriptFail') || 'Failed to export Word.');
}
} finally {
this.exportingManuscriptWord = false;
}
},
async handleDownloadReferencesHtml() {
if (this.referencesHtmlLoading) {
return;
}
if (!this.$api) {
this.$message.error(this.$t('commonTable.refHtmlLoadFail'));
return;
}
this.referencesHtmlLoading = true;
try {
let references = this.manuscriptReferences;
if (!references || !references.length) {
references = await fetchManuscriptReferenceList(this.$api, this.articleId, this.pArticleId);
this.manuscriptReferences = references || [];
}
if (!references || !references.length) {
this.$message.warning(this.$t('commonTable.refHtmlEmpty'));
return;
}
const labels = buildReferencesHtmlLabels(this.$t.bind(this));
const fileName = 'references-editor' + (this.articleId ? '-' + this.articleId : '') + '.html';
downloadReferencesEditableHtml(references, labels, fileName);
this.$message.success(this.$t('commonTable.refHtmlDownloadSuccess'));
} catch (err) {
console.error(err);
this.$message.error(this.$t('commonTable.refHtmlLoadFail'));
} finally {
this.referencesHtmlLoading = false;
}
},
triggerReferencesHtmlToWord() {
if (this.exportingReferencesHtmlWord) {
return;
}
const input = this.$refs.referencesHtmlFileInput;
if (input) {
input.value = '';
input.click();
}
},
handleReferencesHtmlToWord(event) {
const input = event && event.target;
const file = input && input.files && input.files[0];
if (!file) {
return;
}
if (!this.wordList || !this.wordList.length) {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty'));
if (input) {
input.value = '';
}
return;
}
this.exportingReferencesHtmlWord = true;
const reader = new FileReader();
reader.onload = async () => {
try {
const referenceHtmlItems = parseReferencesEditableHtml(String(reader.result || ''));
if (!referenceHtmlItems.length) {
this.$message.warning(this.$t('commonTable.refHtmlParseEmpty'));
return;
}
await downloadManuscriptWord(this.wordList, this.mediaUrl, 'manuscript', {
fetchReferences: false,
referenceHtmlItems: referenceHtmlItems,
apiClient: this.$api,
articleId: this.articleId,
pArticleId: this.pArticleId
});
this.$message.success(this.$t('commonTable.refHtmlToWordSuccess'));
} catch (err) {
console.error(err);
if (err && err.message === 'NO_CONTENT') {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty'));
} else {
this.$message.error(this.$t('commonTable.refHtmlToWordFail'));
}
} finally {
this.exportingReferencesHtmlWord = false;
if (input) {
input.value = '';
}
}
};
reader.onerror = () => {
this.exportingReferencesHtmlWord = false;
if (input) {
input.value = '';
}
this.$message.error(this.$t('commonTable.refHtmlToWordFail'));
};
reader.readAsText(file, 'UTF-8');
},
async handleOpenCitationRelevance() {
if (this.citationRelevanceLoading) {
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;
}
this.citationRelevanceLoading = true;
try {
let references = this.manuscriptReferences;
if (!references || !references.length) {
references = await fetchManuscriptReferenceList(this.$api, this.articleId, this.pArticleId);
this.manuscriptReferences = references || [];
}
const items = buildCitationReviewQueue(this.wordList, this.manuscriptReferences);
if (!items.length) {
this.$message.warning(this.$t('commonTable.citeRelevanceNoCitations'));
return;
}
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'));
} catch (err) {
console.error(err);
this.$message.error(this.$t('commonTable.citeRelevanceLoadFail'));
} finally {
this.citationRelevanceLoading = false;
}
},
getInvolvedPMain(range) { getInvolvedPMain(range) {
// 1. 找到起始节点所属的 .pMain // 1. 找到起始节点所属的 .pMain
let startNode = range.startContainer; let startNode = range.startContainer;

View File

@@ -65,6 +65,24 @@
style="width: 200px; float: right; padding: 10px; height: calc(100% - 28px); box-sizing: border-box; overflow-y: auto" style="width: 200px; float: right; padding: 10px; height: calc(100% - 28px); box-sizing: border-box; overflow-y: auto"
class="arrlist" class="arrlist"
> >
<span
v-if="zyModeEnabled && currentMenu == 1 && exportableImages.length > 0"
class="tables-panel-download-icon"
@click.stop="downloadAllFiguresWord"
>
<el-tooltip effect="dark" content="Download All Figures Word" placement="left">
<i :class="exportingAllFiguresWord ? 'el-icon-loading' : 'el-icon-download'"></i>
</el-tooltip>
</span>
<span
v-if="zyModeEnabled && currentMenu == 2 && tables.length > 0"
class="tables-panel-download-icon"
@click.stop="downloadAllTablesWord"
>
<el-tooltip effect="dark" content="Download All Tables Word" placement="left">
<i :class="exportingAllWord ? 'el-icon-loading' : 'el-icon-download'"></i>
</el-tooltip>
</span>
<ul style="width: 100%; height: auto"> <ul style="width: 100%; height: auto">
<li v-show="currentMenu == 1"> <li v-show="currentMenu == 1">
<div style="" class="go-content-charts-item-box" :class="isCollapse ? 'double' : 'single'"> <div style="" class="go-content-charts-item-box" :class="isCollapse ? 'double' : 'single'">
@@ -137,7 +155,15 @@
</a> </a>
</span> </span>
<p> <p>
<span style="" @click="edit(img, 'img')"> <span v-if="zyModeEnabled && isFigureExportable(img)" @click.stop="downloadSingleFigureWord(img)">
<el-tooltip effect="dark" content="Download Word" placement="top">
<i
:class="exportingFigureId === (img.article_image_id || img.ami_id) ? 'el-icon-loading' : 'el-icon-download'"
style="color: #909399; font-size: 12px"
></i>
</el-tooltip>
</span>
<span style="margin-left: 6px" @click="edit(img, 'img')">
<i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i> <i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i>
</span> </span>
<span style="margin-left: 6px" @click="handledelete(img, 'img')"> <span style="margin-left: 6px" @click="handledelete(img, 'img')">
@@ -329,7 +355,15 @@
</el-tooltip> </span </el-tooltip> </span
></span> ></span>
<p> <p>
<span style="" @click="edit(table, 'table')"> <span v-if="zyModeEnabled && !isMathFormulaTable(table)" @click.stop="downloadSingleTableWord(table)">
<el-tooltip effect="dark" content="Download Word" placement="top">
<i
:class="exportingTableId === table.article_table_id ? 'el-icon-loading' : 'el-icon-download'"
style="color: #909399; font-size: 12px"
></i>
</el-tooltip>
</span>
<span style="margin-left: 6px" @click="edit(table, 'table')">
<i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i> <i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i>
</span> </span>
<span style="margin-left: 6px" @click="handledelete(table, 'table')"> <span style="margin-left: 6px" @click="handledelete(table, 'table')">
@@ -475,6 +509,13 @@
import DynamicTable from './DynamicTable.vue'; import DynamicTable from './DynamicTable.vue';
import { mediaUrl } from '@/common/js/commonJS.js'; import { mediaUrl } from '@/common/js/commonJS.js';
import { isMathFormulaTableRecord, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule'; import { isMathFormulaTableRecord, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
import { downloadAllTablesWord as exportAllTablesWordFile, downloadTableWord, prepareTableExportItem } from '@/utils/exportTableWord';
import {
downloadAllFiguresWord as exportAllFiguresWordFile,
downloadFigureWord,
isExportableFigureItem as canExportFigureItem
} from '@/utils/exportFigureWord';
import { isZyModeEnabled } from '@/utils/zyMode';
export default { export default {
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'], props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
@@ -505,12 +546,24 @@ export default {
mathFormulasList: [], mathFormulasList: [],
tablesHtml: [], tablesHtml: [],
imagesHtml: [], imagesHtml: [],
activeNames: ['images', 'tables'] activeNames: ['images', 'tables'],
exportingAllWord: false,
exportingTableId: null,
exportingAllFiguresWord: false,
exportingFigureId: null
}; };
}, },
components: { components: {
DynamicTable DynamicTable
}, },
computed: {
zyModeEnabled() {
return isZyModeEnabled(this.$route);
},
exportableImages() {
return (this.images || []).filter((img) => canExportFigureItem(img));
}
},
directives: { directives: {
// 注册一个局部的自定义指令 v-focus // 注册一个局部的自定义指令 v-focus
focus: { focus: {
@@ -526,6 +579,9 @@ export default {
changeIsCollapse(e) { changeIsCollapse(e) {
localStorage.setItem('isCollapse', e); localStorage.setItem('isCollapse', e);
}, },
isFigureExportable(item) {
return canExportFigureItem(item);
},
isShowEditComment() { isShowEditComment() {
if (localStorage.getItem('U_role')) { if (localStorage.getItem('U_role')) {
var identity = localStorage.getItem('U_role'); var identity = localStorage.getItem('U_role');
@@ -930,7 +986,85 @@ if(newData.ami_id) {
}, },
openPreview(index, type,data) { openPreview(index, type,data) {
this.$refs.myTableModal.open(type,data); this.$refs.myTableModal.open(type,data);
},
async downloadAllTablesWord() {
if (!this.tables.length) {
this.$message.warning('No tables to export.');
return;
}
this.exportingAllWord = true;
try {
await exportAllTablesWordFile(this.tables, 'tables');
this.$message.success('Word downloaded.');
} catch (err) {
console.error(err);
if (err && err.message === 'NO_TABLES') {
this.$message.warning('No exportable tables found.');
} else {
this.$message.error('Failed to export Word.');
}
} finally {
this.exportingAllWord = false;
}
},
async downloadSingleTableWord(table) {
if (!table || this.isMathFormulaTable(table)) {
this.$message.warning('This table cannot be exported.');
return;
}
const tableId = table.article_table_id || table.amt_id;
this.exportingTableId = tableId;
try {
const prepared = prepareTableExportItem(table);
if (!prepared) {
this.$message.warning('Failed to prepare table.');
return;
}
await downloadTableWord(prepared, table.title);
this.$message.success('Word downloaded.');
} catch (err) {
console.error(err);
this.$message.error('Failed to export Word.');
} finally {
this.exportingTableId = null;
}
},
async downloadAllFiguresWord() {
if (!this.exportableImages.length) {
this.$message.warning('No figures to export.');
return;
}
this.exportingAllFiguresWord = true;
try {
await exportAllFiguresWordFile(this.images, this.mediaUrl, 'figures');
this.$message.success('Word downloaded.');
} catch (err) {
console.error(err);
if (err && err.message === 'NO_FIGURES') {
this.$message.warning('No exportable figures found.');
} else {
this.$message.error('Failed to export Word.');
}
} finally {
this.exportingAllFiguresWord = false;
}
},
async downloadSingleFigureWord(img) {
if (!img || !canExportFigureItem(img)) {
this.$message.warning('This figure cannot be exported.');
return;
}
const figureId = img.article_image_id || img.ami_id;
this.exportingFigureId = figureId;
try {
await downloadFigureWord(img, this.mediaUrl, img.title);
this.$message.success('Word downloaded.');
} catch (err) {
console.error(err);
this.$message.error('Failed to export Word.');
} finally {
this.exportingFigureId = null;
}
}, },
async getWordimgList() { async getWordimgList() {
var that = this; var that = this;
@@ -1049,6 +1183,25 @@ if(newData.ami_id) {
.el-menu-vertical-demo { .el-menu-vertical-demo {
width: 60px; width: 60px;
} }
.arrlist {
position: relative;
}
.tables-panel-download-icon {
position: absolute;
top: 6px;
right: 8px;
z-index: 2;
color: #909399;
font-size: 16px;
line-height: 1;
cursor: pointer;
}
.tables-panel-download-icon:hover {
color: #409eff;
}
.el-menu-vertical-demo li { .el-menu-vertical-demo li {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -297,19 +297,21 @@
<p> <p>
{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>. &nbsp;<em>{{ scope.row.joura }}</em {{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>. &nbsp;<em>{{ scope.row.joura }}</em
>.&nbsp;<span :class="getJournalDateno(scope.row.dateno, 'title')">{{ scope.row.dateno }}</span >.&nbsp;<span :class="getJournalDateno(scope.row.dateno, 'title')">{{ scope.row.dateno }}</span
>.<br /> >.<reference-search-links :row="scope.row"></reference-search-links><br />
</p> </p>
<a class="doiLink" :href="scope.row.doilink" target="_blank">{{ scope.row.doilink }}</a> <a class="doiLink" :href="scope.row.doilink" target="_blank">{{ scope.row.doilink }}</a>
</div> </div>
<!-- book 形式 --> <!-- book 形式 -->
<div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item"> <div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item">
<p>{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>.&nbsp;{{ scope.row.dateno }}.&nbsp;<br /></p> <p>
{{ scope.row.author }}&nbsp;<span v-html="formatTitle(scope.row.title)"></span>.&nbsp;{{ scope.row.dateno }}.&nbsp;<reference-search-links :row="scope.row"></reference-search-links><br />
</p>
<a class="doiLink" :href="scope.row.isbn" target="_blank">{{ scope.row.isbn }}</a> <a class="doiLink" :href="scope.row.isbn" target="_blank">{{ scope.row.isbn }}</a>
</div> </div>
<!-- other 形式 --> <!-- other 形式 -->
<p class="wrongLine reference-item" style="text-align: left" v-if="scope.row.refer_type == 'other'"> <p class="wrongLine reference-item" style="text-align: left" v-if="scope.row.refer_type == 'other'">
<span v-html="formatTitle(scope.row.refer_frag)"></span> <span v-html="formatTitle(scope.row.refer_frag)"></span>
<reference-search-links :row="scope.row"></reference-search-links>
</p> </p>
</template> </template>
</el-table-column> </el-table-column>
@@ -895,6 +897,7 @@
<script> <script>
import VueUeditorWrap from 'vue-ueditor-wrap'; // ES6 Module import VueUeditorWrap from 'vue-ueditor-wrap'; // ES6 Module
import ReferenceSearchLinks from '@/components/page/components/ReferenceSearchLinks.vue';
/** 单条引用 records[].status0 待检测 2 已完成 3 检测失败 */ /** 单条引用 records[].status0 待检测 2 已完成 3 检测失败 */
const REF_RELEVANCE_RECORD_STATUS_PENDING = 0; const REF_RELEVANCE_RECORD_STATUS_PENDING = 0;
@@ -1450,8 +1453,7 @@ export default {
return title.replace(reg, (match) => { return title.replace(reg, (match) => {
return `<span style="color: red; font-weight: bold;">${match}</span>`; return `<span style="color: red; font-weight: bold;">${match}</span>`;
}); });
} },
,
getJournalDateno(dateno, type) { getJournalDateno(dateno, type) {
if (dateno && typeof dateno === 'string') { if (dateno && typeof dateno === 'string') {
const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === ''); const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === '');
@@ -5244,7 +5246,8 @@ export default {
} }
}, },
components: { components: {
VueUeditorWrap VueUeditorWrap,
ReferenceSearchLinks
}, },
watch: { watch: {
role: { role: {

View File

@@ -34,7 +34,7 @@
<el-dialog <el-dialog
:title="articleContextDialogTitle" :title="articleContextDialogTitle"
:visible.sync="articleContextDialogVisible" :visible.sync="articleContextDialogVisible"
width="1100px" width="1140px"
top="8vh" top="8vh"
append-to-body append-to-body
custom-class="article-context-dialog" custom-class="article-context-dialog"
@@ -54,23 +54,16 @@
<el-button type="primary" size="small" :loading="articleContextLoading" @click="loadArticleContext()"> <el-button type="primary" size="small" :loading="articleContextLoading" @click="loadArticleContext()">
{{ $t('mailboxSend.articleContextSearch') }} {{ $t('mailboxSend.articleContextSearch') }}
</el-button> </el-button>
<el-button
v-if="articleContext.loaded && hasArticleTemplatePlaceholders"
size="small"
type="warning"
plain
icon="el-icon-magic-stick"
@click="applyArticleTemplateVariables"
>
{{ $t('mailboxSend.articleContextReplaceVars') }}
</el-button>
</div> </div>
<div v-if="articleContext.loaded" class="article-context-loaded-hint"> <div v-if="articleContext.loaded" class="article-context-meta-card">
<div class="article-loaded-line"> <div class="article-context-meta-card__row">
<span class="article-loaded-sn">SN {{ articleContext.acceptSn || '—' }}</span> <span class="article-meta-label">SN</span>
<span class="article-loaded-sn">{{ articleContext.acceptSn || '—' }}</span>
</div>
<div v-if="articleContext.title" class="article-context-meta-card__title">
{{ articleContext.title }}
</div> </div>
<div v-if="articleContext.title" class="article-loaded-title">{{ articleContext.title }}</div>
</div> </div>
<div v-else-if="!articleContextLoading" class="article-contact-empty article-contact-empty--hint"> <div v-else-if="!articleContextLoading" class="article-contact-empty article-contact-empty--hint">
{{ $t('mailboxSend.articleContextSearchHint') }} {{ $t('mailboxSend.articleContextSearchHint') }}
@@ -123,7 +116,7 @@
/> />
</th> </th>
<th class="review_table_index">No.</th> <th class="review_table_index">No.</th>
<th>{{ $t('mailboxSend.tableName') }}</th> <th class="review_table_name">{{ $t('mailboxSend.tableName') }}</th>
<th class="review_table_role">{{ $t('mailboxSend.tableAuthorRole') }}</th> <th class="review_table_role">{{ $t('mailboxSend.tableAuthorRole') }}</th>
</tr> </tr>
</thead> </thead>
@@ -131,7 +124,12 @@
<tr <tr
v-for="(author, idx) in articleContext.authors" v-for="(author, idx) in articleContext.authors"
:key="author.key" :key="author.key"
:class="{ 'review_table_row--disabled': !hasContactEmail(author) }" class="review_table_row--clickable"
:class="{
'review_table_row--disabled': !hasContactEmail(author),
'review_table_row--selected': isContactSelected('authors', author.key)
}"
@click="handleContactRowClick('authors', author, $event)"
> >
<td class="review_table_check"> <td class="review_table_check">
<el-checkbox <el-checkbox
@@ -141,17 +139,36 @@
/> />
</td> </td>
<td class="review_table_index">{{ idx + 1 }}</td> <td class="review_table_index">{{ idx + 1 }}</td>
<td> <td class="review_table_name">
<div class="author-name-email-cell"> <div class="author-name-email-cell">
<div class="author-name-email-cell__name">{{ author.name || '—' }}</div> <div class="author-name-email-cell__name">{{ author.name || '—' }}</div>
<div v-if="author.email" class="author-name-email-cell__email"> <div v-if="author.email" class="author-name-email-cell__email">
<span class="is-clickable" @click="addContactToTo(author)">{{ author.email }}</span> <span class="is-clickable" @click.stop="addContactToTo(author)">{{ author.email }}</span>
<i class="el-icon-document-copy article-copy-icon" :title="$t('mailboxSend.copyText')" @click.stop="copyContactText(author.email)"></i> <i class="el-icon-document-copy article-copy-icon" :title="$t('mailboxSend.copyText')" @click.stop="copyContactText(author.email)"></i>
</div> </div>
<div v-else class="author-name-email-cell__email reviewer-no-email">{{ $t('mailboxSend.articleContextNoEmail') }}</div> <div v-else class="author-name-email-cell__email reviewer-no-email">{{ $t('mailboxSend.articleContextNoEmail') }}</div>
</div> </div>
</td> </td>
<td class="review_table_role">{{ formatAuthorRoles(author.roles) }}</td> <td class="review_table_role">
<div v-if="formatAuthorRoles(author.roles) !== '—'" class="author-role-tags">
<el-tag
v-if="(author.roles || []).indexOf('firstAuthor') >= 0"
size="mini"
type="info"
effect="plain"
>
{{ $t('mailboxSend.roleFirstAuthor') }}
</el-tag>
<el-tag
v-if="(author.roles || []).indexOf('correspondingAuthor') >= 0"
size="mini"
effect="plain"
>
{{ $t('mailboxSend.roleCorrespondingAuthor') }}
</el-tag>
</div>
<span v-else></span>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -184,7 +201,12 @@
<tr <tr
v-for="(row, idx) in articleContext.reviewRows" v-for="(row, idx) in articleContext.reviewRows"
:key="row.key" :key="row.key"
:class="{ 'review_table_row--disabled': !hasContactEmail(row) }" class="review_table_row--clickable"
:class="{
'review_table_row--disabled': !hasContactEmail(row),
'review_table_row--selected': isContactSelected('review', row.key)
}"
@click="handleContactRowClick('review', row, $event)"
> >
<td class="review_table_check"> <td class="review_table_check">
<el-checkbox <el-checkbox
@@ -243,7 +265,12 @@
<tr <tr
v-for="(row, idx) in articleContext.finalReviewers" v-for="(row, idx) in articleContext.finalReviewers"
:key="row.key" :key="row.key"
:class="{ 'review_table_row--disabled': !hasContactEmail(row) }" class="review_table_row--clickable"
:class="{
'review_table_row--disabled': !hasContactEmail(row),
'review_table_row--selected': isContactSelected('final', row.key)
}"
@click="handleContactRowClick('final', row, $event)"
> >
<td class="review_table_check"> <td class="review_table_check">
<el-checkbox <el-checkbox
@@ -269,6 +296,15 @@
<span class="article-context-selected-tab is-active"> <span class="article-context-selected-tab is-active">
{{ $t('mailboxSend.to') }} ({{ articleSelectedTotalCount }}) {{ $t('mailboxSend.to') }} ({{ articleSelectedTotalCount }})
</span> </span>
<el-button
v-if="articleSelectedTotalCount"
type="text"
size="mini"
class="article-context-selected-clear"
@click="clearArticleContacts(articleContactActiveTab)"
>
{{ $t('mailboxSend.articleContextClearAll') }}
</el-button>
</div> </div>
<div class="article-context-selected-list"> <div class="article-context-selected-list">
<div v-if="!articleSelectedContactList.length" class="article-context-selected-empty"> <div v-if="!articleSelectedContactList.length" class="article-context-selected-empty">
@@ -1180,6 +1216,17 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
isContactSelected(module, key) { isContactSelected(module, key) {
return (this.articleContactSelected[module] || []).indexOf(key) >= 0; return (this.articleContactSelected[module] || []).indexOf(key) >= 0;
}, },
handleContactRowClick(module, contact, event) {
if (!contact || !this.hasContactEmail(contact)) return;
if (
event.target.closest(
'.el-checkbox, .el-checkbox__input, .article-copy-icon, .is-clickable, a, button, input, label'
)
) {
return;
}
this.toggleContactSelected(module, contact.key, !this.isContactSelected(module, contact.key));
},
toggleContactSelected(module, key, checked) { toggleContactSelected(module, key, checked) {
if (checked) { if (checked) {
const contact = this.getModuleContactList(module).find(function (item) { const contact = this.getModuleContactList(module).find(function (item) {
@@ -2005,27 +2052,48 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.article-context-search-bar { .article-context-search-bar {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-bottom: 20px; margin-bottom: 12px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
} }
.article-context-dialog-body { .article-context-dialog-body {
max-height: 60vh; max-height: 62vh;
overflow-y: auto; overflow-y: auto;
padding-right: 10px; padding-right: 4px;
} }
.article-context-loaded-hint { .article-context-meta-card {
display: flex;
align-items: flex-start;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 12px; margin-bottom: 12px;
padding: 10px 12px;
background: #f5f7fa;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.article-context-meta-card__row {
display: flex;
align-items: center;
gap: 8px;
}
.article-meta-label {
display: inline-block;
padding: 0 6px;
font-size: 11px;
line-height: 20px;
color: #909399;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 3px;
}
.article-context-meta-card__title {
margin-top: 6px;
font-size: 13px; font-size: 13px;
line-height: 1.5;
color: #606266; color: #606266;
flex-direction: column; word-break: break-word;
width: 100%;
} }
.article-template-vars-bar { .article-template-vars-bar {
@@ -2045,14 +2113,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.article-loaded-sn { .article-loaded-sn {
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
} font-size: 13px;
.article-loaded-title {
width: 100%;
color: #606266;
line-height: 1.5;
word-break: break-word;
white-space: normal;
} }
.article-contact-empty--hint { .article-contact-empty--hint {
@@ -2064,7 +2125,8 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.article-context-layout { .article-context-layout {
display: flex; display: flex;
min-height: 320px; min-height: 360px;
max-height: 48vh;
border: 1px solid #ebeef5; border: 1px solid #ebeef5;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
@@ -2109,13 +2171,13 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.article-context-main { .article-context-main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 12px; padding: 0;
overflow-x: auto; overflow: auto;
border-right: 1px solid #ebeef5; border-right: 1px solid #ebeef5;
} }
.article-context-selected { .article-context-selected {
width: 280px; width: 300px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -2123,10 +2185,19 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
} }
.article-context-selected-head { .article-context-selected-head {
padding: 12px 14px 0; display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 14px 0;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
.article-context-selected-clear {
padding: 0;
margin-bottom: 6px;
}
.article-context-selected-tab { .article-context-selected-tab {
display: inline-block; display: inline-block;
padding-bottom: 10px; padding-bottom: 10px;
@@ -2144,7 +2215,6 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.article-context-selected-list { .article-context-selected-list {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
max-height: 360px;
overflow-y: auto; overflow-y: auto;
padding: 8px 0; padding: 8px 0;
} }
@@ -2354,22 +2424,30 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
} }
.overflow-x-auto { .overflow-x-auto {
overflow-x: auto; overflow: auto;
height: 100%;
} }
.review_table { .review_table {
width: 100%; width: 100%;
table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 20px; margin-bottom: 0;
background-color: #fff; background-color: #fff;
} }
.review_table thead th {
position: sticky;
top: 0;
z-index: 1;
}
.review_table th, .review_table th,
.review_table td { .review_table td {
padding: 8px 12px; padding: 10px 12px;
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
text-align: left; text-align: left;
font-size: 14px; font-size: 13px;
word-break: break-word; word-break: break-word;
white-space: normal; white-space: normal;
vertical-align: top; vertical-align: top;
@@ -2394,6 +2472,22 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
color: #c0c4cc; color: #c0c4cc;
} }
.review_table_row--clickable:not(.review_table_row--disabled) {
cursor: pointer;
}
.review_table_row--selected td {
background: #ecf5ff;
}
.review_table_row--clickable:not(.review_table_row--disabled):hover td {
background: #f5f7fa;
}
.review_table_row--selected.review_table_row--clickable:not(.review_table_row--disabled):hover td {
background: #e6f1fc;
}
.review_table_row--disabled .reviewer-score { .review_table_row--disabled .reviewer-score {
color: #d4d7de; color: #d4d7de;
} }
@@ -2416,12 +2510,34 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
white-space: nowrap; white-space: nowrap;
} }
.review_table_name {
width: auto;
min-width: 180px;
}
.review_table_role { .review_table_role {
min-width: 220px; width: 148px;
white-space: nowrap; min-width: 148px;
max-width: 148px;
word-break: normal; word-break: normal;
} }
.author-role-tags {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.author-role-tags .el-tag {
max-width: 100%;
white-space: normal;
height: auto;
line-height: 1.4;
padding-top: 2px;
padding-bottom: 2px;
}
.reviewer-name-cell { .reviewer-name-cell {
min-width: 220px; min-width: 220px;
} }

View File

@@ -0,0 +1,166 @@
import { AlignmentType, ExternalHyperlink, LineRuleType, Packer, Paragraph, TextRun } from 'docx';
import { saveAs } from 'file-saver';
import {
appendTableBlockEmptyLines,
createTableParagraph,
createWordDocument,
FONT_NAME,
htmlToPlainText,
htmlToTextRuns,
splitHtmlSegments,
TABLE_FONT_SIZE,
TITLE_COLOR
} from '@/utils/exportTableWord';
const LINK_COLOR = '0563C1';
function sanitizeFileName(name, fallback) {
const base = String(name || fallback || 'figure')
.replace(/<[^>]+>/g, '')
.replace(/[\\/:*?"<>|]/g, '')
.trim()
.slice(0, 80);
return base || fallback || 'figure';
}
function getFigureImagePath(item) {
if (!item) {
return '';
}
return item.url || item.image || '';
}
function getFullImageUrl(mediaUrl, imagePath) {
const path = String(imagePath || '').trim();
if (!path) {
return '';
}
if (/^https?:\/\//i.test(path)) {
return path;
}
return String(mediaUrl || '') + path;
}
export function isExportableFigureItem(item) {
return !!getFigureImagePath(item);
}
export function prepareFigureExportItem(item, mediaUrl) {
const imagePath = getFigureImagePath(item);
const imageUrl = getFullImageUrl(mediaUrl, imagePath);
if (!imageUrl) {
return null;
}
return {
title: (item && item.title) || '',
note: (item && item.note) || '',
imageUrl: imageUrl
};
}
function buildFigureWordChildren(item, mediaUrl) {
const prepared = prepareFigureExportItem(item, mediaUrl);
if (!prepared) {
return [];
}
const children = [];
children.push(
new Paragraph({
alignment: AlignmentType.CENTER,
spacing: {
before: 0,
after: 0,
line: 200,
lineRule: LineRuleType.EXACT
},
children: [
new ExternalHyperlink({
link: prepared.imageUrl,
children: [
new TextRun({
text: prepared.imageUrl,
font: FONT_NAME,
size: TABLE_FONT_SIZE,
color: LINK_COLOR,
underline: {}
})
]
})
]
})
);
if (prepared.title) {
splitHtmlSegments(prepared.title).forEach((segment) => {
children.push(
createTableParagraph(
[
new TextRun({
text: htmlToPlainText(segment),
bold: true,
color: TITLE_COLOR,
font: FONT_NAME,
size: TABLE_FONT_SIZE
})
],
AlignmentType.JUSTIFIED
)
);
});
}
if (prepared.note) {
splitHtmlSegments(prepared.note).forEach((segment) => {
children.push(createTableParagraph(htmlToTextRuns(segment, { bold: false }), AlignmentType.JUSTIFIED));
});
}
appendTableBlockEmptyLines(children);
return children;
}
export function buildFigureWordDocument(item, mediaUrl) {
return createWordDocument(buildFigureWordChildren(item, mediaUrl));
}
export function buildAllFiguresWordDocument(items, mediaUrl) {
const children = [];
(items || []).forEach((item) => {
const blockChildren = buildFigureWordChildren(item, mediaUrl);
blockChildren.forEach((child) => {
children.push(child);
});
});
return createWordDocument(children);
}
export async function downloadFigureWord(item, mediaUrl, fileName) {
const prepared = prepareFigureExportItem(item, mediaUrl);
if (!prepared) {
const error = new Error('NO_FIGURE');
throw error;
}
const doc = buildFigureWordDocument(item, mediaUrl);
const blob = await Packer.toBlob(doc);
saveAs(blob, `${sanitizeFileName(fileName || item.title, 'figure')}.docx`);
}
export async function downloadAllFiguresWord(items, mediaUrl, fileName) {
const exportItems = [];
(items || []).forEach((item) => {
if (prepareFigureExportItem(item, mediaUrl)) {
exportItems.push(item);
}
});
if (!exportItems.length) {
const error = new Error('NO_FIGURES');
throw error;
}
const doc = buildAllFiguresWordDocument(exportItems, mediaUrl);
const blob = await Packer.toBlob(doc);
saveAs(blob, `${sanitizeFileName(fileName, 'figures')}.docx`);
}

View File

@@ -0,0 +1,377 @@
import { AlignmentType, ImageRun, Paragraph, TextRun } from 'docx';
import { htmlToPlainText, TABLE_FONT_SIZE, WORD_PARAGRAPH_SPACING } from '@/utils/exportTableWord';
import orcidIconUrl from '@/assets/img/orcid.png';
/** 作者区字体 Calibri 六号7.5pt */
const AUTHOR_FONT_NAME = 'Calibri';
/** 标题略大 */
const AUTHOR_TITLE_FONT_SIZE = 24;
/** 上标 rgb(0, 112, 192) */
const AUTHOR_SUPER_COLOR = '0070C0';
const ORCID_ICON_SIZE = 13;
let orcidImageCache = null;
export async function loadOrcidImageData() {
if (orcidImageCache) {
return orcidImageCache;
}
if (typeof fetch === 'undefined') {
return null;
}
const response = await fetch(orcidIconUrl);
const buffer = await response.arrayBuffer();
orcidImageCache = new Uint8Array(buffer);
return orcidImageCache;
}
export async function fetchManuscriptHeaderData(apiClient, articleId, pArticleId, resolvePArticleId) {
if (!apiClient) {
return null;
}
const resolvedId = await resolvePArticleId(apiClient, articleId, pArticleId);
if (!resolvedId) {
return null;
}
const idPayload = { p_article_id: resolvedId };
const results = await Promise.all([
apiClient.post('api/Production/getProductionDetail', idPayload).catch(function () {
return null;
}),
apiClient.post('api/Production/getAuthorlist', idPayload).catch(function () {
return null;
}),
apiClient.post('api/Production/getProductionPreview', idPayload).catch(function () {
return null;
})
]);
const detailRes = results[0];
const authorListRes = results[1];
const previewRes = results[2];
const production =
detailRes && detailRes.code == 0 && detailRes.data && detailRes.data.production
? detailRes.data.production
: null;
const authors =
authorListRes && authorListRes.code == 0 && authorListRes.data && authorListRes.data.authors
? authorListRes.data.authors
: [];
const previewAuthor =
previewRes && previewRes.code == 0 && previewRes.data && previewRes.data.author ? previewRes.data.author : null;
if (!production && !authors.length && !previewAuthor) {
return null;
}
return {
pArticleId: resolvedId,
production: production,
authors: authors,
previewAuthor: previewAuthor
};
}
function createAuthorTextRun(text, options) {
const opts = options || {};
const runOptions = {
text: text || '',
font: AUTHOR_FONT_NAME,
size: TABLE_FONT_SIZE
};
if (opts.bold) {
runOptions.bold = true;
}
if (opts.italics) {
runOptions.italics = true;
}
if (opts.superScript) {
runOptions.superScript = true;
}
if (opts.color) {
runOptions.color = opts.color;
}
if (opts.size) {
runOptions.size = opts.size;
}
return new TextRun(runOptions);
}
function createOrcidImageRun(orcidImageData) {
if (!orcidImageData) {
return createAuthorTextRun('');
}
return new ImageRun({
type: 'png',
data: orcidImageData,
transformation: {
width: ORCID_ICON_SIZE,
height: ORCID_ICON_SIZE
}
});
}
/** 与排版预览一致:<q> 转上标,作者之间加空格 */
function preparePreviewAuthorHtml(html) {
let text = String(html || '');
text = text.replace(/<q>/gi, '<sup>');
text = text.replace(/<\/q>/gi, '</sup>');
text = text.replace(/<\/sup>,/gi, '</sup>, ');
text = text.replace(/<span[^>]*margin-left:\s*8px[^>]*>\s*<\/span>/gi, ' ');
text = text.replace(/,\s*$/, '');
return text.trim();
}
function decodeAuthorHtmlEntities(text) {
return String(text || '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"');
}
function htmlToAuthorLineRuns(html, orcidImageData, isAuthorNameLine) {
const runs = [];
const raw = String(html || '');
if (!raw) {
return runs;
}
if (typeof document === 'undefined') {
runs.push(
createAuthorTextRun(htmlToPlainText(raw), {
bold: !!isAuthorNameLine
})
);
return runs;
}
const root = document.createElement('div');
root.innerHTML = raw;
function appendRun(text, style) {
if (!text) {
return;
}
runs.push(
createAuthorTextRun(text, {
bold: style.bold,
italics: style.italic,
superScript: style.super,
color: style.color
})
);
}
function walk(node, style) {
if (node.nodeType === Node.TEXT_NODE) {
appendRun(decodeAuthorHtmlEntities(node.textContent).replace(/\u00a0/g, ' '), style);
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
const tag = node.tagName.toLowerCase();
if (tag === 'br') {
return;
}
if (tag === 'img') {
runs.push(createOrcidImageRun(orcidImageData));
return;
}
if (tag === 'span' && !node.textContent.trim()) {
return;
}
const nextStyle = {
bold: style.bold,
italic: style.italic,
super: style.super,
color: style.color
};
if (tag === 'b' || tag === 'strong') {
nextStyle.bold = true;
}
if (tag === 'i' || tag === 'em') {
nextStyle.italic = true;
}
if (tag === 'sup') {
nextStyle.super = true;
nextStyle.bold = false;
nextStyle.color = AUTHOR_SUPER_COLOR;
}
Array.from(node.childNodes).forEach(function (child) {
walk(child, nextStyle);
});
}
walk(root, {
bold: !!isAuthorNameLine,
italic: false,
super: false,
color: undefined
});
if (!runs.length) {
runs.push(
createAuthorTextRun(htmlToPlainText(raw), {
bold: !!isAuthorNameLine
})
);
}
return runs;
}
function buildAuthorHtmlParagraphs(authorHtml, orcidImageData) {
const prepared = preparePreviewAuthorHtml(authorHtml);
const segments = prepared.split(/<br\s*\/?>/gi).filter(function (segment) {
return htmlToPlainText(segment).trim();
});
if (!segments.length) {
return [];
}
return segments.map(function (segment, index) {
const isAuthorNameLine = index === 0;
const runs = htmlToAuthorLineRuns(segment, orcidImageData, isAuthorNameLine);
return new Paragraph({
alignment: AlignmentType.JUSTIFIED,
spacing: WORD_PARAGRAPH_SPACING,
children: runs.length ? runs : [createAuthorTextRun('')]
});
});
}
function buildAuthorListFallbackParagraph(authors, orcidImageData) {
const runs = [];
const list = (authors || []).filter(Boolean);
list.forEach(function (author, index) {
if (index > 0) {
runs.push(createAuthorTextRun(', '));
}
const name =
author.author_name ||
String((author.first_name || '') + ' ' + (author.last_name || '')).trim();
runs.push(createAuthorTextRun(name, { bold: true }));
const marks = [];
if (author.is_first == 1 || author.is_first == '1') {
marks.push('#');
}
if (author.is_report == 1 || author.is_report == '1') {
marks.push('*');
}
if (marks.length) {
runs.push(
createAuthorTextRun(marks.join(''), {
superScript: true,
color: AUTHOR_SUPER_COLOR
})
);
}
if (author.orcid && String(author.orcid).trim()) {
runs.push(createOrcidImageRun(orcidImageData));
}
});
return new Paragraph({
alignment: AlignmentType.JUSTIFIED,
spacing: WORD_PARAGRAPH_SPACING,
children: runs.length ? runs : [createAuthorTextRun('')]
});
}
function createAffiliationParagraph(index, addressText) {
const text = String(addressText || '').trim();
if (!text) {
return null;
}
return new Paragraph({
alignment: AlignmentType.JUSTIFIED,
spacing: WORD_PARAGRAPH_SPACING,
children: [
createAuthorTextRun(String(index + 1), {
superScript: true,
color: AUTHOR_SUPER_COLOR
}),
createAuthorTextRun(' ' + text)
]
});
}
function createAuthorTitleParagraph(title) {
const text = String(title || '').trim();
if (!text) {
return null;
}
return new Paragraph({
alignment: AlignmentType.JUSTIFIED,
spacing: WORD_PARAGRAPH_SPACING,
children: [
createAuthorTextRun(text, {
bold: true,
size: AUTHOR_TITLE_FONT_SIZE
})
]
});
}
export function buildAuthorHeaderSectionChildren(headerData, orcidImageData) {
if (!headerData) {
return [];
}
const children = [];
const production = headerData.production || {};
const previewAuthor = headerData.previewAuthor || {};
const titleParagraph = createAuthorTitleParagraph(htmlToPlainText(production.title || ''));
if (titleParagraph) {
children.push(titleParagraph);
}
if (previewAuthor && previewAuthor.author) {
buildAuthorHtmlParagraphs(previewAuthor.author, orcidImageData).forEach(function (paragraph) {
children.push(paragraph);
});
} else if (headerData.authors && headerData.authors.length) {
children.push(buildAuthorListFallbackParagraph(headerData.authors, orcidImageData));
}
const addressList = (previewAuthor && previewAuthor.addressList) || [];
addressList.forEach(function (address, index) {
const paragraph = createAffiliationParagraph(index, htmlToPlainText(address));
if (paragraph) {
children.push(paragraph);
}
});
if (children.length) {
children.push(
new Paragraph({
spacing: WORD_PARAGRAPH_SPACING,
children: [createAuthorTextRun('')]
})
);
}
return children;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,640 @@
import {
AlignmentType,
BorderStyle,
Document,
LineRuleType,
Packer,
Paragraph,
ShadingType,
Table,
TableCell,
TableLayoutType,
TableRow,
TextRun,
VerticalAlign,
WidthType
} from 'docx';
import { saveAs } from 'file-saver';
import { TableUtils } from '@/common/js/TableUtils';
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
/** 斑马纹 rgb(250, 231, 232) */
const ODD_ROW_FILL = 'FAE7E8';
/** 标题 rgb(210, 90, 90) */
const TITLE_COLOR = 'D25A5A';
/** 引用 blue rgb(0, 130, 170) */
const BLUE_COLOR = '0082AA';
const FONT_NAME = 'Charis SIL';
/** 六号 = 7.5pt */
const TABLE_FONT_SIZE = 15;
const TITLE_FONT_SIZE = TABLE_FONT_SIZE;
function cmToTwips(cm) {
return Math.round((cm * 1440) / 2.54);
}
/** 页面边距:上/下 2.54cm,左/右 1.91cm */
const PAGE_MARGINS = {
top: cmToTwips(2.54),
bottom: cmToTwips(2.54),
left: cmToTwips(1.91),
right: cmToTwips(1.91)
};
/** 表格属性:宽 98.3%,整体居中,无环绕 */
const TABLE_WIDTH_PERCENT = 98.3;
/** 表格选项(图二):单元格边距 上/下 0、左/右 0.19cm,自动适应内容 */
const TABLE_CELL_MARGIN_TWIPS = cmToTwips(0.19);
const TABLE_CELL_MARGINS = {
top: 0,
bottom: 0,
left: TABLE_CELL_MARGIN_TWIPS,
right: TABLE_CELL_MARGIN_TWIPS
};
/** 行属性:允许跨页断行,不指定行高 */
const TABLE_ROW_OPTIONS = {
cantSplit: false
};
/** 全篇段落:固定行距 10 磅1pt = 20 twips → 10pt = 200 */
const WORD_PARAGRAPH_SPACING = {
before: 0,
after: 0,
line: 200,
lineRule: LineRuleType.EXACT,
beforeAutoSpacing: false,
afterAutoSpacing: false
};
const WORD_DOCUMENT_STYLES = {
default: {
document: {
paragraph: {
spacing: WORD_PARAGRAPH_SPACING
},
run: {
font: FONT_NAME,
size: TABLE_FONT_SIZE
}
}
},
paragraphStyles: [
{
id: 'Normal',
name: 'Normal',
quickFormat: true,
spacing: WORD_PARAGRAPH_SPACING,
run: {
font: FONT_NAME,
size: TABLE_FONT_SIZE
}
},
{
id: 'Heading1',
name: 'Heading 1',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
spacing: WORD_PARAGRAPH_SPACING,
run: {
font: FONT_NAME,
size: TITLE_FONT_SIZE,
bold: true,
color: TITLE_COLOR
}
}
]
};
/** 每个表格块末尾空行数 */
const TABLE_BLOCK_EMPTY_LINE_COUNT = 4;
function createWordParagraph(children, options) {
const opts = options || {};
let alignment = AlignmentType.JUSTIFIED;
if (opts.alignment !== undefined && opts.alignment !== null) {
alignment = opts.alignment;
}
const paragraphOptions = {
spacing: WORD_PARAGRAPH_SPACING,
indent: {
left: 0,
right: 0
},
children
};
if (opts.title) {
paragraphOptions.alignment = AlignmentType.CENTER;
paragraphOptions.style = 'Heading1';
paragraphOptions.outlineLevel = 0;
} else {
paragraphOptions.alignment = alignment;
paragraphOptions.style = 'Normal';
}
return new Paragraph(paragraphOptions);
}
function appendTableBlockEmptyLines(children) {
let i = 0;
for (i = 0; i < TABLE_BLOCK_EMPTY_LINE_COUNT; i += 1) {
children.push(createTableParagraph([new TextRun('')], AlignmentType.JUSTIFIED));
}
}
/** 表格标题段落:居中,大纲 1 级,固定行距 10pt段前段后 0无缩进 */
function createTitleParagraph(children) {
return createWordParagraph(children, { title: true });
}
function createTableParagraph(children, alignment) {
return createWordParagraph(children, { alignment });
}
function splitHtmlSegments(html) {
const raw = String(html || '');
if (!raw) {
return [''];
}
const parts = raw.split(/<br\s*\/?>/gi);
if (!parts.length) {
return [''];
}
return parts;
}
function decodeHtmlEntities(text) {
return String(text || '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"');
}
function htmlToPlainText(html) {
const raw = String(html || '');
if (typeof document !== 'undefined') {
const node = document.createElement('div');
node.innerHTML = raw;
return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ').trim();
}
return decodeHtmlEntities(
raw
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]*>/g, '')
).trim();
}
function isBlueElement(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
return false;
}
const tag = node.tagName.toLowerCase();
if (tag === 'blue') {
return true;
}
if (tag === 'span' && node.classList && node.classList.contains('blue')) {
return true;
}
return !!(node.classList && node.classList.contains('color-highlight'));
}
function htmlToTextRuns(html, options) {
const { bold = false } = options || {};
const runs = [];
const raw = String(html || '');
if (!raw) {
return [
new TextRun({
text: '',
font: FONT_NAME,
size: TABLE_FONT_SIZE,
bold
})
];
}
if (typeof document === 'undefined') {
return [
new TextRun({
text: htmlToPlainText(raw),
font: FONT_NAME,
size: TABLE_FONT_SIZE,
bold
})
];
}
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
})
);
}
function walk(node, style) {
if (node.nodeType === Node.TEXT_NODE) {
appendRun(decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' '), style);
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
const tag = node.tagName.toLowerCase();
if (tag === 'br') {
appendRun('\n', style);
return;
}
const nextStyle = {
bold: style.bold,
italic: style.italic,
sup: style.sup,
sub: style.sub,
blue: style.blue
};
if (tag === 'b' || tag === 'strong') {
nextStyle.bold = true;
}
if (tag === 'i' || tag === 'em') {
nextStyle.italic = true;
}
if (tag === 'sup') {
nextStyle.sup = true;
}
if (tag === 'sub') {
nextStyle.sub = true;
}
if (isBlueElement(node)) {
nextStyle.blue = true;
}
Array.from(node.childNodes).forEach((child) => walk(child, nextStyle));
}
walk(root, {
bold: false,
italic: false,
sup: false,
sub: false,
blue: false
});
if (!runs.length) {
return [
new TextRun({
text: htmlToPlainText(raw),
font: FONT_NAME,
size: TABLE_FONT_SIZE,
bold
})
];
}
return runs;
}
/** 表头:相邻片段之间补空格,再规范为“一词一空” */
function formatHeaderWordSpacing(html) {
const raw = String(html || '');
if (!raw) {
return '';
}
if (typeof document !== 'undefined') {
const root = document.createElement('div');
root.innerHTML = raw;
const parts = [];
function collect(node) {
if (node.nodeType === Node.TEXT_NODE) {
const text = decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' ').trim();
if (text) {
parts.push(text);
}
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
if (node.tagName.toLowerCase() === 'br') {
parts.push('\n');
return;
}
Array.from(node.childNodes).forEach(collect);
}
collect(root);
return parts
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
return htmlToPlainText(raw)
.split(/\s+/)
.filter(Boolean)
.join(' ');
}
function htmlToHeaderTextRuns(html) {
return [
new TextRun({
text: formatHeaderWordSpacing(html),
font: FONT_NAME,
size: TABLE_FONT_SIZE,
bold: true
})
];
}
/** 表格边框:黑色 0.5 磅OOXML sz 单位为 1/8 pt → 0.5pt = 4 */
const TABLE_BORDER = {
style: BorderStyle.SINGLE,
size: 4,
color: '000000'
};
const TABLE_BORDER_NONE = {
style: BorderStyle.NONE,
size: 0,
color: 'FFFFFF'
};
function createBorder(show) {
return show ? TABLE_BORDER : TABLE_BORDER_NONE;
}
function createTableCell(cell, options) {
const { header = false, odd = false, lastRow = false } = options;
if (!cell || cell.rowspan === 0 || cell.colspan === 0) {
return null;
}
const cellOptions = {
verticalAlign: VerticalAlign.CENTER,
borders: {
top: createBorder(header),
bottom: createBorder(header || lastRow),
left: createBorder(false),
right: createBorder(false)
},
children: splitHtmlSegments(cell.text).map((segment) =>
createTableParagraph(
header ? htmlToHeaderTextRuns(segment) : htmlToTextRuns(segment, { bold: false }),
AlignmentType.LEFT
)
)
};
if (cell.colspan > 1) {
cellOptions.columnSpan = cell.colspan;
}
if (cell.rowspan > 1) {
cellOptions.rowSpan = cell.rowspan;
}
if (odd) {
cellOptions.shading = {
fill: ODD_ROW_FILL,
type: ShadingType.CLEAR,
color: 'auto'
};
}
return new TableCell(cellOptions);
}
function createTableRow(cells) {
return new TableRow({
...TABLE_ROW_OPTIONS,
children: cells
});
}
function buildTableRows(table) {
const headers = (table && table.tableHeader) || [];
const contents = (table && table.tableContent) || [];
const oddRowIds = (table && table.oddRowIds) || [];
const rows = [];
headers.forEach((row) => {
const cells = (row || []).map((cell) => createTableCell(cell, { header: true })).filter(Boolean);
if (cells.length) {
rows.push(createTableRow(cells));
}
});
contents.forEach((row, rowIndex) => {
const odd = oddRowIds.length ? oddRowIds.includes(row.rowId) : rowIndex % 2 === 0;
const cells = (row || [])
.map((cell) =>
createTableCell(cell, {
odd,
lastRow: rowIndex === contents.length - 1
})
)
.filter(Boolean);
if (cells.length) {
rows.push(createTableRow(cells));
}
});
return rows;
}
function createWordDocument(children) {
return new Document({
styles: WORD_DOCUMENT_STYLES,
sections: [
{
properties: {
page: {
margin: PAGE_MARGINS
},
grid: {
linePitch: 200
}
},
children
}
]
});
}
export function prepareTableExportItem(item) {
if (!item || isMathFormulaTableRecord(item)) {
return null;
}
if (item.table && item.table.tableHeader) {
return {
title: item.title || '',
note: item.note || '',
table: item.table
};
}
try {
const tableList = typeof item.table === 'string' ? JSON.parse(item.table) : item.table;
const splitResult = TableUtils.splitTable(tableList);
const rowResult = TableUtils.addRowIdToData(splitResult.content);
return {
title: item.title || '',
note: item.note || '',
table: {
tableHeader: splitResult.header,
tableContent: rowResult.rowData,
oddRowIds: rowResult.rowIds
}
};
} catch (e) {
return null;
}
}
function buildTableWordChildren(processedItem, options) {
const opts = options || {};
const appendTrailingEmptyLines = opts.appendTrailingEmptyLines !== false;
const title = (processedItem && processedItem.title) || '';
const note = (processedItem && processedItem.note) || '';
const table = (processedItem && processedItem.table) || {};
const children = [];
if (title) {
splitHtmlSegments(title).forEach((segment) => {
children.push(
createTitleParagraph([
new TextRun({
text: htmlToPlainText(segment),
bold: true,
color: TITLE_COLOR,
font: FONT_NAME,
size: TITLE_FONT_SIZE
})
])
);
});
}
const tableRows = buildTableRows(table);
if (tableRows.length) {
children.push(
new Table({
width: {
size: TABLE_WIDTH_PERCENT,
type: WidthType.PERCENTAGE
},
alignment: AlignmentType.CENTER,
layout: TableLayoutType.AUTOFIT,
margins: TABLE_CELL_MARGINS,
borders: {
top: TABLE_BORDER_NONE,
bottom: TABLE_BORDER_NONE,
left: TABLE_BORDER_NONE,
right: TABLE_BORDER_NONE,
insideHorizontal: TABLE_BORDER_NONE,
insideVertical: TABLE_BORDER_NONE
},
rows: tableRows
})
);
}
if (note) {
splitHtmlSegments(note).forEach((segment) => {
children.push(
createTableParagraph(htmlToTextRuns(segment, { bold: false }), AlignmentType.JUSTIFIED)
);
});
}
if (appendTrailingEmptyLines) {
appendTableBlockEmptyLines(children);
}
return children;
}
export function buildTableWordDocument(processedItem) {
return createWordDocument(buildTableWordChildren(processedItem));
}
export function buildAllTablesWordDocument(items) {
const children = [];
(items || []).forEach((item) => {
const prepared = prepareTableExportItem(item);
if (!prepared) {
return;
}
const blockChildren = buildTableWordChildren(prepared);
blockChildren.forEach((child) => {
children.push(child);
});
});
return createWordDocument(children);
}
function sanitizeFileName(name) {
const base = String(name || 'table')
.replace(/<[^>]+>/g, '')
.replace(/[\\/:*?"<>|]/g, '')
.trim()
.slice(0, 80);
return base || 'table';
}
export async function downloadTableWord(processedItem, fileName) {
const doc = buildTableWordDocument(processedItem);
const blob = await Packer.toBlob(doc);
saveAs(blob, `${sanitizeFileName(fileName || processedItem.title)}.docx`);
}
export async function downloadAllTablesWord(items, fileName) {
const exportItems = [];
(items || []).forEach((item) => {
const prepared = prepareTableExportItem(item);
if (prepared) {
exportItems.push(prepared);
}
});
if (!exportItems.length) {
const error = new Error('NO_TABLES');
throw error;
}
const doc = buildAllTablesWordDocument(exportItems);
const blob = await Packer.toBlob(doc);
saveAs(blob, `${sanitizeFileName(fileName || 'tables')}.docx`);
}
export {
appendTableBlockEmptyLines,
buildTableWordChildren,
createTableParagraph,
createWordDocument,
FONT_NAME,
htmlToPlainText,
htmlToTextRuns,
PAGE_MARGINS,
splitHtmlSegments,
TABLE_FONT_SIZE,
TITLE_COLOR,
WORD_PARAGRAPH_SPACING
};

View File

@@ -0,0 +1,102 @@
import { Column, DocumentGridType, SectionType } from 'docx';
import { PAGE_MARGINS, WORD_PARAGRAPH_SPACING } from '@/utils/exportTableWord';
/** A4 纵向页宽/页高twips */
export const PAGE_WIDTH_TWIPS = 11906;
export const PAGE_HEIGHT_TWIPS = 16838;
export const DUAL_COLUMN_COUNT = 2;
/**
* Word 分栏对话框(两栏预设):
* 栏1 22.18 字符 | 间距 2.02 字符 | 栏2 22.18 字符 | 无分隔线 | 栏宽相等=否
*/
export const MANUSCRIPT_DUAL_COLUMN = {
count: 2,
col1WidthChars: 22.18,
spaceChars: 2.02,
col2WidthChars: 22.18,
separate: false,
equalWidth: false
};
function getTextAreaWidthTwips() {
return PAGE_WIDTH_TWIPS - PAGE_MARGINS.left - PAGE_MARGINS.right;
}
function getDualColumnTotalChars() {
return (
MANUSCRIPT_DUAL_COLUMN.col1WidthChars +
MANUSCRIPT_DUAL_COLUMN.spaceChars +
MANUSCRIPT_DUAL_COLUMN.col2WidthChars
);
}
/** 版心宽度 / 46.38 字符 → 每字符网格宽度twips */
export function getGridCharPitchTwips() {
return getTextAreaWidthTwips() / getDualColumnTotalChars();
}
/**
* 文档网格:仅固定行距 10 磅。
* 栏宽由 createDualColumnProperties 按 22.18/2.02/22.18 字符比例换算 twips
* 不再启用 charSpace 字符网格,否则与两端对齐叠加会出现过大词间距。
*/
export function getDocumentGridProperties() {
return {
type: DocumentGridType.LINES,
linePitch: WORD_PARAGRAPH_SPACING.line
};
}
/** 按字符比例换算栏宽/栏间距twips保证三栏之和等于版心宽度 */
export function createDualColumnProperties() {
const textWidth = getTextAreaWidthTwips();
const totalChars = getDualColumnTotalChars();
const col1Width = Math.round((textWidth * MANUSCRIPT_DUAL_COLUMN.col1WidthChars) / totalChars);
const colSpace = Math.round((textWidth * MANUSCRIPT_DUAL_COLUMN.spaceChars) / totalChars);
const col2Width = textWidth - col1Width - colSpace;
return {
count: MANUSCRIPT_DUAL_COLUMN.count,
equalWidth: MANUSCRIPT_DUAL_COLUMN.equalWidth,
separate: MANUSCRIPT_DUAL_COLUMN.separate,
children: [new Column({ width: col1Width, space: colSpace }), new Column({ width: col2Width })]
};
}
export function getDualColumnWidthPt() {
const textWidth = getTextAreaWidthTwips();
const totalChars =
MANUSCRIPT_DUAL_COLUMN.col1WidthChars +
MANUSCRIPT_DUAL_COLUMN.spaceChars +
MANUSCRIPT_DUAL_COLUMN.col2WidthChars;
const col1WidthTwips = Math.round((textWidth * MANUSCRIPT_DUAL_COLUMN.col1WidthChars) / totalChars);
return Math.round(col1WidthTwips / 20);
}
export function createWordSectionProperties(columnCount, isFirstSection) {
const properties = {
page: {
size: {
width: PAGE_WIDTH_TWIPS,
height: PAGE_HEIGHT_TWIPS
},
margin: PAGE_MARGINS
},
grid: getDocumentGridProperties()
};
if (columnCount === DUAL_COLUMN_COUNT) {
properties.column = createDualColumnProperties();
} else {
properties.column = {
count: 1
};
}
if (!isFirstSection) {
properties.type = SectionType.CONTINUOUS;
}
return properties;
}

View File

@@ -0,0 +1,406 @@
/** 正文引用标记,如 [1]、[1-7]、<blue>[4,6,8]</blue> */
const CITATION_BRACKET_RE = /(?:<blue>\s*)?\[([^\]]+)\](?:\s*<\/blue>)?/gi;
function decodeHtmlEntities(text) {
return String(text || '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"');
}
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
export function htmlToPlainText(html) {
const raw = String(html || '');
if (typeof document !== 'undefined') {
const node = document.createElement('div');
node.innerHTML = raw;
return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ').trim();
}
return decodeHtmlEntities(raw.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, '')).trim();
}
export function expandCitationBracket(inner) {
const result = [];
const text = String(inner || '')
.replace(/<[^>]+>/g, '')
.trim();
if (!text) {
return result;
}
text.split(/\s*,\s*/).forEach(function (part) {
const token = part.trim();
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);
let i = 0;
for (i = lo; i <= hi; i += 1) {
result.push(i);
}
return;
}
if (/^\d+$/.test(token)) {
result.push(Number(token));
}
});
return result;
}
/** 从段落 HTML 提取引用编号(按出现顺序,段内去重) */
export function parseCitationNumbersFromHtml(html) {
const nums = [];
const seen = {};
const re = new RegExp(CITATION_BRACKET_RE.source, 'gi');
let match = null;
while ((match = re.exec(String(html || ''))) !== null) {
expandCitationBracket(match[1]).forEach(function (num) {
if (!seen[num]) {
seen[num] = true;
nums.push(num);
}
});
}
return nums;
}
export function stripHtmlTags(html) {
return htmlToPlainText(html);
}
export function formatReferencePlainText(ref) {
if (!ref) {
return '';
}
if (ref.refer_frag) {
const frag = stripHtmlTags(ref.refer_frag);
if (frag) {
return frag;
}
}
const parts = [];
if (ref.author) {
parts.push(stripHtmlTags(ref.author));
}
if (ref.title) {
parts.push(stripHtmlTags(ref.title));
}
if (ref.joura) {
parts.push(stripHtmlTags(ref.joura));
}
if (ref.dateno) {
parts.push(stripHtmlTags(ref.dateno));
}
if (ref.doilink) {
parts.push(String(ref.doilink).trim());
}
if (ref.isbn) {
parts.push('ISBN: ' + String(ref.isbn).trim());
}
return parts.filter(Boolean).join('. ').replace(/\.\s*\./g, '.').trim();
}
function isTextParagraphItem(item) {
if (!item) {
return false;
}
if (item.type == 1 || item.type == 2) {
return false;
}
const html = String(item.content || item.text || '').trim();
return !!html;
}
function buildCitationEntry(citeNum, refList) {
const ref = refList[citeNum - 1] || null;
return {
citeNum: citeNum,
reference: ref,
referenceText: formatReferencePlainText(ref),
referenceMissing: !ref
};
}
export function formatCitationNumsLabel(citeNums) {
return (citeNums || []).map(function (n) {
return '[' + n + ']';
}).join(' ');
}
export function getCitationReviewGroupMeta(item, index, labels) {
const l = labels || {};
const groupNo = index + 1;
const paragraphNo = item.paragraphIndex + 1;
const citeNumsLabel = formatCitationNumsLabel(item.citeNums);
const citeNumsPlain = (item.citeNums || []).join('、');
let title = '正文段落 ' + paragraphNo + ' · ' + citeNumsLabel;
if (l.groupLabel) {
title = l.groupLabel
.replace('{group}', groupNo)
.replace('{paragraph}', paragraphNo)
.replace('{refs}', citeNumsLabel);
}
return {
id: 'cite-group-' + groupNo,
groupNo: groupNo,
paragraphNo: paragraphNo,
citeNumsLabel: citeNumsLabel,
citeNumsPlain: citeNumsPlain,
title: title
};
}
/**
* 构建核查队列:每个含引文的正文段落一组,该段全部参考文献一并询问;
* 同一条文献出现在不同段落时,会在各段落中分别询问。
*/
export function buildCitationReviewQueue(wordList, references) {
const items = [];
const refList = references || [];
(wordList || []).forEach(function (item, paragraphIndex) {
if (!isTextParagraphItem(item)) {
return;
}
const html = item.content || item.text || '';
const citeNums = parseCitationNumbersFromHtml(html);
if (!citeNums.length) {
return;
}
const paragraphText = htmlToPlainText(html);
const citations = citeNums.map(function (citeNum) {
return buildCitationEntry(citeNum, refList);
});
items.push({
paragraphIndex: paragraphIndex,
amId: item.am_id,
paragraphHtml: html,
paragraphText: paragraphText,
citeNums: citeNums.slice(),
citations: citations,
hasMissingReference: citations.some(function (c) {
return c.referenceMissing;
})
});
});
return items;
}
export function buildWenaiRelevancePrompt(item) {
if (!item) {
return '';
}
const paragraphText = item.paragraphText || '';
const citations = item.citations || [];
const citeLabels = citations.map(function (c) {
return '[' + c.citeNum + ']';
});
let refBlock = '';
citations.forEach(function (c) {
const text = c.referenceText || '(未找到该条参考文献)';
refBlock += '[' + c.citeNum + ']\n' + text + '\n\n';
});
const labelList = citeLabels.join('、') || '';
return (
'请审读以下正文段落及其引用文献,结合该段落的论述内容,判断各文献与正文的相关程度。\n\n' +
'【正文段落】\n' +
paragraphText +
'\n\n' +
'【参考文献】\n' +
refBlock.trim() +
'\n\n' +
'【输出要求】\n' +
'请以期刊编辑审稿批注的语气,为作者撰写一段文件批注(不要用邮件格式,不要称呼、落款、主题行或分条邮件体)。\n' +
'请按文献序号 ' +
labelList +
' 依次说明,保留原文序号且顺序连续,不要重新编号或跳号。\n' +
'对每条文献分别给出关联判断:直接相关 / 强相关 / 弱相关 / 不相关,并用一句话简要说明理由。\n' +
'将全部内容合并写成一段连贯的批注文字,语气客观、专业、克制,可直接作为稿件文件批注使用。'
);
}
/** 合并全部批次提示词 */
export function buildAllWenaiRelevancePrompts(reviewItems, labels) {
return (reviewItems || [])
.map(function (item, index) {
const meta = getCitationReviewGroupMeta(item, index, labels);
const header = '========== ' + meta.title + ' ==========';
return header + '\n\n' + buildWenaiRelevancePrompt(item);
})
.filter(Boolean)
.join('\n\n\n');
}
/** 生成可独立打开的 HTML 文档(含锚点导航与复制) */
export function buildCitationReviewStandaloneHtml(reviewItems, labels) {
const l = labels || {};
const items = reviewItems || [];
const total = items.length;
const pageTitle = l.pageTitle || '引文相关性核查';
const groupTotalText = (l.groupTotal || '共 {n} 组').replace('{n}', total);
const jumpTitle = l.jumpTitle || '快速跳转';
const copyGroupText = l.copyGroup || '复制本组';
const copyAllText = l.copyAll || '复制全部';
const copySuccessText = l.copySuccess || '已复制';
const copyFailText = l.copyFail || '复制失败';
let navHtml = '';
let bodyHtml = '';
let i = 0;
for (i = 0; i < items.length; i += 1) {
const item = items[i];
const meta = getCitationReviewGroupMeta(item, i, labels);
const prompt = buildWenaiRelevancePrompt(item);
const promptId = 'cite-prompt-' + meta.groupNo;
navHtml +=
'<a class="nav-link" href="#' +
meta.id +
'">' +
escapeHtml(meta.title) +
'</a>';
bodyHtml +=
'<section class="group-card" id="' +
meta.id +
'">' +
'<div class="group-head">' +
'<h3>' +
escapeHtml(meta.title) +
'</h3>' +
'<button type="button" class="copy-btn" data-target="' +
promptId +
'">' +
escapeHtml(copyGroupText) +
'</button>' +
'</div>' +
'<pre class="group-prompt" id="' +
promptId +
'">' +
escapeHtml(prompt) +
'</pre>' +
'</section>';
}
const allPrompt = escapeHtml(buildAllWenaiRelevancePrompts(items, labels));
return (
'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">' +
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
'<title>' +
escapeHtml(pageTitle) +
'</title>' +
'<style>' +
'*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
'.page{max-width:960px;margin:0 auto;padding:20px}' +
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.header h1{margin:0 0 8px;font-size:20px}' +
'.summary{color:#606266;font-size:14px;margin-bottom:12px}' +
'.actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}' +
'.btn{border:1px solid #409eff;background:#409eff;color:#fff;border-radius:4px;padding:6px 14px;font-size:13px;cursor:pointer}' +
'.btn.plain{background:#ecf5ff;color:#409eff}' +
'.nav{background:#fff;border-radius:8px;padding:12px 16px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.nav-title{font-weight:600;margin-bottom:8px;font-size:14px}' +
'.nav-links{display:flex;flex-wrap:wrap;gap:8px}' +
'.nav-link{display:inline-block;padding:4px 10px;background:#f4f4f5;border-radius:4px;color:#409eff;text-decoration:none;font-size:12px}' +
'.nav-link:hover{background:#ecf5ff}' +
'.group-card{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08);scroll-margin-top:16px}' +
'.group-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}' +
'.group-head h3{margin:0;font-size:15px;color:#303133}' +
'.copy-btn{border:1px solid #409eff;background:#ecf5ff;color:#409eff;border-radius:4px;padding:4px 12px;font-size:12px;cursor:pointer;white-space:nowrap}' +
'.copy-btn:hover{background:#409eff;color:#fff}' +
'.group-prompt{margin:0;padding:12px;background:#fafafa;border:1px solid #ebeef5;border-radius:4px;white-space:pre-wrap;word-break:break-word;font-size:13px;font-family:Consolas,"Courier New",monospace}' +
'#all-prompt{display:none}' +
'</style></head><body><div class="page">' +
'<div class="header"><h1>' +
escapeHtml(pageTitle) +
'</h1>' +
'<div class="summary">' +
escapeHtml(groupTotalText) +
'</div>' +
'<div class="actions">' +
'<button type="button" class="btn plain" id="copy-all-btn">' +
escapeHtml(copyAllText) +
'</button></div></div>' +
'<div class="nav"><div class="nav-title">' +
escapeHtml(jumpTitle) +
'</div><div class="nav-links">' +
navHtml +
'</div></div>' +
bodyHtml +
'<pre id="all-prompt">' +
allPrompt +
'</pre></div>' +
'<script>(function(){' +
'var okMsg=' +
JSON.stringify(copySuccessText) +
';var failMsg=' +
JSON.stringify(copyFailText) +
';function copyFromEl(el,btn){var text=el?(el.innerText||el.textContent||""):"";if(!text){alert(failMsg);return;}' +
'function done(){if(btn){var old=btn.innerText;btn.innerText=okMsg;setTimeout(function(){btn.innerText=old;},1200);}else{alert(okMsg);}}' +
'if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(done).catch(function(){fallback(text,done);});return;}' +
'fallback(text,done);}' +
'function fallback(text,done){var ta=document.createElement("textarea");ta.value=text;ta.style.position="fixed";ta.style.opacity="0";document.body.appendChild(ta);ta.select();try{document.execCommand("copy");done();}catch(e){alert(failMsg);}document.body.removeChild(ta);}' +
'document.querySelectorAll(".copy-btn").forEach(function(btn){btn.addEventListener("click",function(){var id=btn.getAttribute("data-target");copyFromEl(document.getElementById(id),btn);});});' +
'var allBtn=document.getElementById("copy-all-btn");if(allBtn){allBtn.addEventListener("click",function(){copyFromEl(document.getElementById("all-prompt"),allBtn);});}' +
'})();<' +
'/script></body></html>'
);
}
export function downloadCitationReviewHtml(reviewItems, labels, fileName) {
const html = buildCitationReviewStandaloneHtml(reviewItems, labels);
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName || 'citation-relevance.html';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export function buildCitationReviewHtmlLabels(translate) {
const t = translate || function (key) {
return key;
};
return {
pageTitle: t('commonTable.citeRelevanceTitle'),
groupTotal: t('commonTable.citeRelevanceGroupTotal', { n: '{n}' }),
jumpTitle: t('commonTable.citeRelevanceJumpTo'),
copyGroup: t('commonTable.citeRelevanceCopyGroup'),
copyAll: t('commonTable.citeRelevanceCopyAllWenai'),
copySuccess: t('commonTable.citeRelevanceCopySuccess'),
copyFail: t('commonTable.citeRelevanceCopyFail'),
groupLabel: t('commonTable.citeRelevanceGroupNavLabel')
};
}

View File

@@ -0,0 +1,236 @@
import { htmlToPlainText } from '@/utils/exportTableWord';
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function hasItalicHtmlTag(html) {
return /<i\b|<em\b/i.test(String(html || ''));
}
function stripAvailableAtSuffix(html) {
return String(html || '')
.replace(/\s*Available at:\s*[\s\S]*$/i, '')
.trim();
}
function normalizeReferenceLink(url) {
const raw = String(url || '').trim();
if (!raw) {
return '';
}
if (/^https?:\/\//i.test(raw)) {
return raw;
}
if (/^10\./.test(raw)) {
return 'https://doi.org/' + raw;
}
return raw;
}
function getReferenceIsbnValue(ref) {
if (!ref) {
return '';
}
return String(ref.isbn || ref.doilink || '').trim();
}
function appendHtmlField(parts, html, suffix, options) {
const opts = options || {};
const raw = String(html || '').trim();
if (!raw) {
return;
}
if (opts.italics && !hasItalicHtmlTag(raw)) {
parts.push('<i>' + raw + '</i>');
} else {
parts.push(raw);
}
if (suffix) {
parts.push(escapeHtml(suffix));
}
}
/** 将接口参考文献转为可编辑 HTML支持后续粘贴带 <i> 的内容) */
export function referenceRecordToEditableHtml(ref) {
if (!ref) {
return '';
}
const referType = String(ref.refer_type || 'journal').toLowerCase();
if (!ref.author && ref.refer_frag) {
let html = stripAvailableAtSuffix(ref.refer_frag);
const link = normalizeReferenceLink(ref.doilink || getReferenceIsbnValue(ref));
if (link) {
html += '<br>Available at:<br>' + escapeHtml(ref.doilink || ref.isbn || link);
}
return html;
}
const parts = [];
if (referType === 'book') {
if (ref.author) {
appendHtmlField(parts, ref.author, ' ');
}
if (ref.title) {
appendHtmlField(parts, ref.title, '. ', { italics: true });
}
if (ref.dateno) {
appendHtmlField(parts, ref.dateno, '. ');
}
const isbnValue = getReferenceIsbnValue(ref);
if (isbnValue) {
parts.push('Available at:<br>ISBN: ' + escapeHtml(isbnValue));
}
return parts.join('');
}
if (referType === 'other') {
let html = stripAvailableAtSuffix(ref.refer_frag);
const link = normalizeReferenceLink(ref.doilink);
if (link) {
html += '<br>Available at:<br>' + escapeHtml(ref.doilink || link);
}
return html;
}
if (ref.author) {
appendHtmlField(parts, ref.author, ' ');
}
if (ref.title) {
appendHtmlField(parts, ref.title, '. ');
}
if (ref.joura) {
appendHtmlField(parts, ref.joura, '. ', { italics: !hasItalicHtmlTag(ref.joura) });
}
if (ref.dateno) {
appendHtmlField(parts, ref.dateno, '. ');
}
const doiLink = normalizeReferenceLink(ref.doilink);
if (doiLink) {
parts.push('Available at:<br>' + escapeHtml(ref.doilink || doiLink));
}
return parts.join('');
}
export function buildReferencesEditableHtml(references, labels) {
const list = (references || []).filter(Boolean);
const L = labels || {};
const pageTitle = L.pageTitle || 'References Editor';
const intro = L.intro || '';
const saveTip = L.saveTip || '';
const downloadBtn = L.downloadEdited || 'Download edited HTML';
const refTitle = L.referencesTitle || 'References';
let listHtml = '';
list.forEach(function (ref, index) {
const referType = String(ref.refer_type || 'journal').toLowerCase();
const content = referenceRecordToEditableHtml(ref);
listHtml +=
'<li class="ref-item" contenteditable="true" spellcheck="false"' +
' data-ref-index="' + index + '"' +
' data-ref-type="' + escapeHtml(referType) + '">' +
content +
'</li>';
});
return (
'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">' +
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
'<title>' + escapeHtml(pageTitle) + '</title>' +
'<style>' +
'*{box-sizing:border-box}body{margin:0;font-family:Charis SIL,Georgia,"Times New Roman",serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
'.page{max-width:920px;margin:0 auto;padding:20px}' +
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
'.header h1{margin:0 0 8px;font-size:20px;text-align:center;font-style:italic;font-weight:bold;color:#008080}' +
'.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.plain{background:#ecf5ff;color:#409eff}' +
'.ref-panel{background:#fff;border-radius:8px;padding:20px 24px;box-shadow:0 1px 4px rgba(0,0,0,.08);font-size:12px}' +
'.ref-list{margin:0;padding-left:1.4em;list-style:decimal}' +
'.ref-item{margin-bottom:10px;padding:4px 0;outline:none;min-height:1.4em}' +
'.ref-item:focus{background:#fafcff;box-shadow:inset 0 0 0 1px #c6e2ff;border-radius:2px}' +
'.ref-item i,.ref-item em{font-style:italic}' +
'</style></head><body><div class="page">' +
'<div class="header"><h1>' + escapeHtml(refTitle) + '</h1>' +
'<div class="intro">' + escapeHtml(intro) + '</div>' +
'<div class="actions">' +
'<button type="button" class="btn" id="download-edited-btn">' + escapeHtml(downloadBtn) + '</button>' +
'</div>' +
'<div class="intro" style="font-size:12px;color:#909399">' + escapeHtml(saveTip) + '</div>' +
'</div>' +
'<div class="ref-panel"><ol class="ref-list" id="ref-list">' +
listHtml +
'</ol></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></body></html>'
);
}
export function parseReferencesEditableHtml(htmlText) {
const raw = String(htmlText || '');
if (!raw.trim()) {
return [];
}
const doc = new DOMParser().parseFromString(raw, 'text/html');
const nodes = doc.querySelectorAll('#ref-list .ref-item, #ref-list li[data-ref-index], ol.ref-list li');
const items = [];
nodes.forEach(function (node) {
const html = (node.innerHTML || '').trim();
if (!html) {
return;
}
items.push({
refer_type: String(node.getAttribute('data-ref-type') || 'journal').toLowerCase(),
html: html
});
});
return items;
}
export function downloadReferencesEditableHtml(references, labels, fileName) {
const html = buildReferencesEditableHtml(references, labels);
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName || 'references-editor.html';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
export function buildReferencesHtmlLabels(translate) {
const t = translate || function (key) {
return key;
};
return {
pageTitle: t('commonTable.refHtmlPageTitle'),
referencesTitle: t('commonTable.refHtmlReferencesTitle'),
intro: t('commonTable.refHtmlIntro'),
saveTip: t('commonTable.refHtmlSaveTip'),
downloadEdited: t('commonTable.refHtmlDownloadEdited'),
downloadFileName: t('commonTable.refHtmlDownloadFileName')
};
}

View File

@@ -0,0 +1,409 @@
/**
* 从投稿稿同步到排版 production 的工具方法
*/
export function resolveSubmissionAuthors(authorsRes, detailRes) {
const fromGetAuthors =
authorsRes &&
authorsRes.data &&
Array.isArray(authorsRes.data.authors) &&
authorsRes.data.authors.length
? authorsRes.data.authors
: null;
if (fromGetAuthors) {
return fromGetAuthors;
}
const fromDetail =
detailRes && Array.isArray(detailRes.authors) && detailRes.authors.length ? detailRes.authors : null;
if (fromDetail) {
return fromDetail;
}
return [];
}
export function fillImportPlaceholder(value, fallback = '111') {
const text = String(value || '').trim();
return text || fallback;
}
/** 必填项无值时的占位(单个空格,满足校验) */
export const REQUIRED_FIELD_PLACEHOLDER = ' ';
export function fillRequiredValue(value) {
const text = String(value || '').trim();
return text || REQUIRED_FIELD_PLACEHOLDER;
}
function capitalizeNamePart(part) {
const text = String(part || '').trim();
if (!text) {
return '';
}
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
}
/** 中文名(无空格/连字符):拆分为音节后再用 - 连接,如 Lixiao -> Li-Xiao */
function splitChineseGivenNameParts(raw) {
const lower = String(raw || '').trim().toLowerCase();
if (!lower) {
return [];
}
if (lower.length <= 4) {
return [lower];
}
if (lower.length === 5) {
return [lower];
}
if (lower.length % 2 === 0) {
const half = lower.length / 2;
const first = lower.slice(0, half);
const second = lower.slice(half);
if (first === second) {
return [first, second];
}
}
if (lower.length === 6) {
const twoFour = [lower.slice(0, 2), lower.slice(2)];
const secondPart = twoFour[1];
if (secondPart.length === 4 && /^[aeiou]/i.test(secondPart)) {
return [lower.slice(0, 3), lower.slice(3)];
}
return twoFour;
}
if (lower.length === 7 || lower.length === 8) {
return [lower.slice(0, 4), lower.slice(4)];
}
const mid = Math.ceil(lower.length / 2);
return [lower.slice(0, mid), lower.slice(mid)];
}
/**
* 名Author first name名之间用 - 连接,每段首字母大写
* 例Yan Lixiao 中名 Lixiao -> Li-Xiaoqian wen -> Qian-Wen
*/
export function formatAuthorFirstName(firstname) {
const raw = String(firstname || '').trim();
if (!raw) {
return '';
}
const parts = raw.split(/[\s-]+/).filter(Boolean);
if (parts.length > 1) {
return parts.map((part) => capitalizeNamePart(part)).join('-');
}
const syllables = splitChineseGivenNameParts(raw);
if (syllables.length > 1) {
return syllables.map((part) => capitalizeNamePart(part)).join('-');
}
return capitalizeNamePart(raw);
}
/** 姓Author last name首字母大写如 Yan */
export function formatAuthorLastName(lastname) {
const raw = String(lastname || '').trim();
if (!raw) {
return '';
}
return capitalizeNamePart(raw);
}
/** 按排版规则规范化投稿作者 */
export function formatSubmissionAuthor(author) {
const rawFirst = author.firstname || author.first_name || '';
const rawLast = author.lastname || author.last_name || '';
const first = formatAuthorFirstName(rawFirst);
const last = formatAuthorLastName(rawLast);
return {
...author,
firstname: first,
first_name: first,
lastname: last,
last_name: last
};
}
export function normalizeSubmissionAuthors(authors) {
return (Array.isArray(authors) ? authors : []).map((author) => formatSubmissionAuthor(author));
}
export function getAuthorDisplayName(author) {
const first = String(author.firstname || author.first_name || '').trim();
const last = String(author.lastname || author.last_name || '').trim();
return `${first} ${last}`.trim();
}
export function getAuthorAffiliationPreview(author) {
const names = resolveOrganNames(author);
return names.length ? names.join('; ') : '-';
}
export function normalizeAuthorOrderToken(token) {
return String(token || '')
.replace(/¶/g, '')
.replace(/\*/g, '')
.replace(/\d+/g, '')
.replace(/-/g, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
export function parseAuthorOrderString(orderStr) {
return String(orderStr || '')
.split(',')
.map((item) => normalizeAuthorOrderToken(item))
.filter(Boolean);
}
function authorMatchKey(author) {
return normalizeAuthorOrderToken(getAuthorDisplayName(author));
}
export function sortAuthorsByOrderString(authors, orderStr) {
const list = Array.isArray(authors) ? [...authors] : [];
const tokens = parseAuthorOrderString(orderStr);
if (!tokens.length) {
return list;
}
const used = new Set();
const sorted = [];
tokens.forEach((token) => {
const idx = list.findIndex((author, index) => {
if (used.has(index)) {
return false;
}
const key = authorMatchKey(author);
return key === token || key.replace(/\s/g, '') === token.replace(/\s/g, '');
});
if (idx >= 0) {
used.add(idx);
sorted.push(list[idx]);
}
});
list.forEach((author, index) => {
if (!used.has(index)) {
sorted.push(author);
}
});
return sorted;
}
export function moveAuthorInList(list, index, direction) {
const next = [...(list || [])];
const targetIndex = direction === 'up' ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= next.length) {
return next;
}
const temp = next[index];
next[index] = next[targetIndex];
next[targetIndex] = temp;
return next;
}
function resolveAuthorFirstLastName(author) {
const first = author.firstname || author.first_name || '';
const last = author.lastname || author.last_name || '';
return {
first: formatAuthorFirstName(first),
last: formatAuthorLastName(last)
};
}
/**
* 参考文献作者缩写Author's short name in reference format
* 1. 单个作者:姓全称 + 空格 + 名各段首字母大写Yang meilan -> Yang MLYang he -> Yang H
* 2. 多个作者:用 ", " 分隔,最后一位后不加标点
* 3. 人数 ≤6全部列出人数 >6前 3 位 + ", et al"
*/
export function buildAuthorReferenceAbbr(authors) {
if (!Array.isArray(authors) || !authors.length) {
return '';
}
const formatOne = (author) => {
const { first, last } = resolveAuthorFirstLastName(author);
if (!last) {
return '';
}
const initials = first
.split(/[\s-]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase())
.join('');
return initials ? `${last} ${initials}` : last;
};
const formatted = authors.map(formatOne).filter(Boolean);
if (authors.length > 6) {
return formatted.slice(0, 3).join(', ').concat(', et al');
}
return formatted.join(', ');
}
export function normalizeRichTextContent(content) {
const text = String(content || '').trim();
if (!text) {
return '';
}
if (/<[a-z][\s\S]*>/i.test(text)) {
return text;
}
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<p>${escaped.replace(/\r\n|\r|\n/g, '<br>')}</p>`;
}
export function mapEssentialFields(article, authors) {
const safeArticle = article || {};
const abstractRaw =
safeArticle.abstrart ||
safeArticle.abstract ||
safeArticle.summary ||
safeArticle.introduction ||
'';
return {
title: safeArticle.title || '',
type: safeArticle.type || '',
keywords: safeArticle.keywords || '',
abbr: buildAuthorReferenceAbbr(authors),
acknowledgment: fillImportPlaceholder(safeArticle.acknowledgment || safeArticle.acknowledgement),
author_contribution: fillImportPlaceholder(safeArticle.author_contribution),
abbreviation: fillImportPlaceholder(safeArticle.abbreviation),
abstract: normalizeRichTextContent(abstractRaw)
};
}
function normalizeOrganIds(organId) {
if (Array.isArray(organId)) {
return organId.map(String).filter(Boolean);
}
if (organId == null || organId === '') {
return [];
}
return String(organId)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}
function resolveOrganNames(author) {
if (Array.isArray(author.organ_name) && author.organ_name.length) {
return author.organ_name.map((item) => String(item || '').trim()).filter(Boolean);
}
if (author.company) {
return String(author.company)
.split('/')
.map((item) => item.trim())
.filter(Boolean);
}
return [];
}
function findProductionOrgan(productionOrgans, organName) {
const target = String(organName || '').trim().toLowerCase();
if (!target) {
return null;
}
return (productionOrgans || []).find((item) => String(item.organ_name || '').trim().toLowerCase() === target);
}
export function buildOrganIdMap(submissionOrgans, productionOrgans) {
const map = {};
const prodList = Array.isArray(productionOrgans) ? productionOrgans : [];
const subList = Array.isArray(submissionOrgans) ? submissionOrgans : [];
subList.forEach((subOrgan, index) => {
const subName = String(subOrgan.organ_name || '').trim();
if (!subName) {
return;
}
let matched = prodList.find((prodOrgan) => String(prodOrgan.organ_name || '').trim() === subName);
if (!matched) {
matched = findProductionOrgan(prodList, subName);
}
if (!matched && prodList[index]) {
matched = prodList[index];
}
if (matched && matched.p_article_organ_id != null) {
map[String(subOrgan.organ_id)] = String(matched.p_article_organ_id);
}
});
return map;
}
/** 从作者信息中提取不重复的单位地址 */
export function collectSubmissionOrgans(submissionOrgans, authors) {
const result = [];
const seen = new Set();
(Array.isArray(submissionOrgans) ? submissionOrgans : []).forEach((organ, index) => {
const organName = String(organ.organ_name || '').trim();
if (!organName || seen.has(organName)) {
return;
}
seen.add(organName);
result.push({
organ_id: organ.organ_id != null ? organ.organ_id : `organ-${index + 1}`,
organ_name: organName
});
});
(Array.isArray(authors) ? authors : []).forEach((author) => {
resolveOrganNames(author).forEach((organName, index) => {
if (!organName || seen.has(organName)) {
return;
}
seen.add(organName);
result.push({
organ_id: `derived-${result.length + index + 1}`,
organ_name: organName
});
});
});
return result;
}
/** 通讯作者详情缺失时的占位 */
function resolveCorrespondingAuthorDetails(author) {
return fillRequiredValue(author.mailing_address || author.address);
}
export function buildProductionAuthorPayload(author, pArticleId, organIdMap, productionOrgans) {
const isReport = Number(author.is_report) === 1;
const organIds = normalizeOrganIds(author.organ_id);
let organs = organIds.map((id) => organIdMap[id]).filter(Boolean);
if (!organs.length) {
const organNames = resolveOrganNames(author);
organs = organNames
.map((name) => {
const matched = findProductionOrgan(productionOrgans, name);
return matched ? String(matched.p_article_organ_id) : '';
})
.filter(Boolean);
}
return {
p_article_id: pArticleId,
first_name: formatAuthorFirstName(author.firstname || author.first_name || ''),
last_name: formatAuthorLastName(author.lastname || author.last_name || ''),
is_first: Number(author.is_super) === 1 ? '1' : '0',
is_report: isReport ? '1' : '0',
email: isReport ? fillRequiredValue(author.email) : '',
organs,
author_country: fillRequiredValue(author.country),
orcid: author.orcid || '',
mailing_address: isReport ? resolveCorrespondingAuthorDetails(author) : ''
};
}

View File

@@ -0,0 +1,251 @@
import { htmlToPlainText } from '@/utils/exportTableWord';
function parseSingleAuthorName(nameStr) {
const token = String(nameStr || '').trim();
if (!token || /^et al\.?$/i.test(token)) {
return null;
}
if (token.includes(',')) {
const commaParts = token.split(/\s*,\s*/).map(function (part) {
return part.trim();
}).filter(Boolean);
if (commaParts.length === 2) {
const maybeLast = commaParts[0];
const maybeFirst = commaParts[1];
if (maybeLast && maybeFirst && !isInitialsToken(maybeFirst)) {
return { first: maybeFirst, last: maybeLast };
}
}
}
const parts = token.split(/\s+/).filter(Boolean);
if (!parts.length) {
return null;
}
if (parts.length === 1) {
return { first: '', last: parts[0] };
}
if (parts.length === 2 && isInitialsToken(parts[1])) {
return { first: '', last: parts[0], initials: normalizeInitialsToken(parts[1]) };
}
return {
first: parts.slice(0, -1).join(' '),
last: parts[parts.length - 1]
};
}
function isInitialsToken(token) {
return /^[A-Z](?:-?[A-Z])*(?:\.[A-Z](?:-?[A-Z])*)*\.?$/u.test(String(token || '').trim());
}
function normalizeInitialsToken(token) {
return String(token || '')
.replace(/\./g, '')
.trim();
}
function firstNameToAmaInitials(first) {
return String(first || '')
.split(/[\s-]+/)
.filter(Boolean)
.map(function (part) {
return part.charAt(0).toUpperCase();
})
.join('');
}
function formatOneAmaAuthor(nameStr) {
const parsed = parseSingleAuthorName(nameStr);
if (!parsed || !parsed.last) {
return '';
}
if (parsed.initials) {
return parsed.last + ' ' + parsed.initials;
}
const initials = firstNameToAmaInitials(parsed.first);
return initials ? parsed.last + ' ' + initials : parsed.last;
}
function looksLikeAbbreviatedAuthorToken(token) {
return /^[\p{L}][\p{L}\-']*\s+[A-Z](?:-?[A-Z])*\.?$/u.test(String(token || '').trim());
}
function looksLikeFirstNameToken(token) {
const words = String(token || '').trim().split(/\s+/).filter(Boolean);
if (!words.length) {
return false;
}
return words.every(function (word) {
return word.length > 1 && /^[\p{L}][\p{L}\-']*$/u.test(word) && !isInitialsToken(word);
});
}
function splitAuthorList(authorText) {
const parts = htmlToPlainText(authorText)
.split(/\s*,\s*/)
.map(function (part) {
return part.trim();
})
.filter(Boolean);
const merged = [];
let index = 0;
while (index < parts.length) {
const current = parts[index];
const next = parts[index + 1];
if (
next &&
!looksLikeAbbreviatedAuthorToken(current) &&
!looksLikeAbbreviatedAuthorToken(next) &&
current.split(/\s+/).filter(Boolean).length === 1 &&
looksLikeFirstNameToken(next)
) {
merged.push(current + ', ' + next);
index += 2;
continue;
}
merged.push(current);
index += 1;
}
return merged;
}
/** 是否已是 AMA 式缩写(如 Smith MJ, Melnyk BM首字母后无句点 */
export function isAmaAbbreviatedAuthorList(authorText) {
const authors = splitAuthorList(authorText);
if (!authors.length) {
return true;
}
return authors.every(function (token) {
if (/^et al\.?$/i.test(token)) {
return true;
}
return looksLikeAbbreviatedAuthorToken(token) && !/\b[A-Z]\./.test(token);
});
}
function hasDottedAuthorInitials(authorText) {
const plain = htmlToPlainText(authorText);
return /\b[\p{L}][\p{L}\-']*\s+[A-Z]\.(?:\s*[A-Z]\.)+/u.test(plain)
|| /\b[\p{L}][\p{L}\-']*\s+[A-Z]\.(?=\s*,|\s*$)/u.test(plain);
}
/** 参考文献作者是否需要 AMA 缩写建议 */
export function needsAmaAuthorAbbreviation(authorText) {
const plain = htmlToPlainText(authorText).trim();
if (!plain) {
return false;
}
if (hasDottedAuthorInitials(plain)) {
return true;
}
if (isAmaAbbreviatedAuthorList(plain)) {
return false;
}
return splitAuthorList(plain).some(function (token) {
const parts = token.split(/\s+/).filter(Boolean);
if (parts.length < 2) {
return false;
}
if (parts.length === 2 && isInitialsToken(parts[1])) {
return false;
}
return parts[0].length > 2 || (parts.length >= 2 && parts[parts.length - 1].length > 2);
});
}
/** 将作者列表转为 AMA 式缩写(首字母后无句点,如 Melnyk BM, Fineout-Overholt E */
export function buildAmaAuthorAbbreviation(authorText) {
const authors = splitAuthorList(authorText);
if (!authors.length) {
return '';
}
const formatted = authors
.map(function (token) {
if (/^et al\.?$/i.test(token)) {
return 'et al';
}
return formatOneAmaAuthor(token);
})
.filter(Boolean);
return formatted.join(', ');
}
function isBookReference(ref) {
return String(ref && ref.refer_type || '').toLowerCase() === 'book';
}
function isJournalReference(ref) {
return String(ref && ref.refer_type || '').toLowerCase() === 'journal';
}
export function extractAuthorSource(ref) {
if (!ref) {
return '';
}
const authorPlain = htmlToPlainText(ref.author).trim();
if (authorPlain) {
return authorPlain;
}
if (isBookReference(ref) && ref.refer_frag) {
const plain = htmlToPlainText(ref.refer_frag).replace(/\s*Available at:[\s\S]*$/i, '').trim();
const match = plain.match(/^(.+?)\.\s+/);
if (match) {
return match[1].trim();
}
}
return '';
}
export function extractBookAuthorSource(ref) {
if (!isBookReference(ref)) {
return '';
}
return extractAuthorSource(ref);
}
export function buildAmaAuthorComment(ref) {
const revision = buildAmaAuthorRevision(ref);
if (!revision) {
return '';
}
return 'AMA格式作者缩写' + revision.suggested;
}
export function buildAmaAuthorRevision(ref) {
const authorSource = extractAuthorSource(ref);
if (!authorSource || !needsAmaAuthorAbbreviation(authorSource)) {
return null;
}
const suggested = buildAmaAuthorAbbreviation(authorSource);
if (!suggested || authorSource === suggested) {
return null;
}
return {
original: authorSource,
suggested: suggested,
suffix: '. '
};
}
export function buildBookAmaAuthorComment(ref) {
return buildAmaAuthorComment(ref);
}

View File

@@ -0,0 +1,282 @@
import { htmlToPlainText } from '@/utils/exportTableWord';
const STOP_WORDS = new Set([
'a', 'an', 'the', 'of', 'and', 'for', 'in', 'on', 'at', 'to', 'from', 'with', 'by',
'de', 'du', 'des', 'la', 'le', 'les', 'et', 'und', 'der', 'die', 'das'
]);
const KNOWN_WORD_ABBREVIATIONS = {
journal: 'J',
international: 'Int',
american: 'Am',
european: 'Eur',
british: 'Br',
medicine: 'Med',
medical: 'Med',
nursing: 'Nurs',
inquiry: 'Inq',
science: 'Sci',
sciences: 'Sci',
research: 'Res',
review: 'Rev',
reports: 'Rep',
health: 'Health',
healthcare: 'Health',
clinical: 'Clin',
surgery: 'Surg',
pediatric: 'Pediatr',
paediatric: 'Paediatr',
oncology: 'Oncol',
psychiatry: 'Psychiatry',
psychology: 'Psychol',
education: 'Educ',
technology: 'Technol',
engineering: 'Eng',
management: 'Manage',
studies: 'Stud',
practice: 'Pract',
traditional: 'Tradit',
tradit: 'Tradit',
clinical: 'Clin',
complementary: 'Complement',
alternative: 'Altern',
integrative: 'Integr',
pharmacology: 'Pharmacol',
biochemistry: 'Biochem',
physiology: 'Physiol',
pathology: 'Pathol',
radiology: 'Radiol',
cardiology: 'Cardiol',
neurology: 'Neurol',
dermatology: 'Dermatol',
obstetrics: 'Obstet',
gynecology: 'Gynecol',
gynaecology: 'Gynaecol',
anesthesiology: 'Anesthesiol',
anaesthesiology: 'Anaesthesiol',
epidemiology: 'Epidemiol',
immunology: 'Immunol',
microbiology: 'Microbiol',
nutrition: 'Nutr',
public: 'Public',
environmental: 'Environ',
occupational: 'Occup',
behavioral: 'Behav',
behavioural: 'Behav',
biological: 'Biol',
molecular: 'Mol',
cellular: 'Cell',
applied: 'Appl',
advanced: 'Adv',
general: 'Gen',
internal: 'Intern',
family: 'Fam',
community: 'Community',
global: 'Glob',
world: 'World',
asian: 'Asian',
chinese: 'Chin',
japanese: 'Jpn',
korean: 'Korean',
indian: 'Indian',
african: 'Afr',
australian: 'Aust',
canadian: 'Can',
quarterly: 'Q',
monthly: 'Mon',
weekly: 'Wkly',
annual: 'Annu',
proceedings: 'Proc',
transactions: 'Trans',
bulletin: 'Bull',
letters: 'Lett',
communications: 'Commun',
perspectives: 'Perspect',
insights: 'Insights',
trends: 'Trends',
systems: 'Syst',
informatics: 'Inform',
bioinformatics: 'Bioinform',
therapeutics: 'Ther',
diagnostics: 'Diagn',
imaging: 'Imaging',
rehabilitation: 'Rehabil',
emergency: 'Emerg',
critical: 'Crit',
care: 'Care',
patient: 'Patient',
patients: 'Patient',
hospital: 'Hosp',
hospitals: 'Hosp',
university: 'Univ',
academy: 'Acad',
society: 'Soc',
association: 'Assoc',
federation: 'Fed',
institute: 'Inst',
college: 'Coll',
school: 'Sch',
department: 'Dep',
division: 'Div',
section: 'Sect',
series: 'Ser',
supplement: 'Suppl',
edition: 'Ed',
online: 'Online',
open: 'Open',
access: 'Access'
};
function splitJournalWords(journalText) {
return String(journalText || '')
.replace(/\./g, ' ')
.split(/[\s/&]+/)
.map(function (part) {
return part.trim();
})
.filter(Boolean);
}
function abbreviateJournalWord(word) {
const cleaned = String(word || '').replace(/[^\p{L}\-]/gu, '');
if (!cleaned) {
return '';
}
const lower = cleaned.toLowerCase();
if (KNOWN_WORD_ABBREVIATIONS[lower]) {
return KNOWN_WORD_ABBREVIATIONS[lower];
}
if (cleaned.length <= 4) {
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}
const vowels = 'aeiouy';
let result = cleaned.charAt(0).toUpperCase();
const rest = cleaned.slice(1).toLowerCase();
let index = 0;
while (result.length < 4 && index < rest.length) {
const ch = rest.charAt(index);
index += 1;
if (!vowels.includes(ch)) {
result += ch;
} else if (result.length === 1) {
result += ch;
}
}
if (result.length < 3) {
const fallback = cleaned.slice(0, Math.min(4, cleaned.length));
result = fallback.charAt(0).toUpperCase() + fallback.slice(1).toLowerCase();
}
return result;
}
function normalizeJournalText(text) {
return String(text || '')
.replace(/\./g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/** 是否已是期刊缩写形式(如 Nurs Inq、Tradit Med Res */
export function isAbbreviatedJournalName(journalText) {
const plain = normalizeJournalText(htmlToPlainText(journalText));
if (!plain) {
return true;
}
const words = splitJournalWords(plain);
if (!words.length) {
return true;
}
return !words.some(function (word) {
if (STOP_WORDS.has(word.toLowerCase())) {
return false;
}
return word.length >= 7;
});
}
/** 将期刊全称转为无句点缩写(如 Nursing Inquiry -> Nurs Inq */
export function buildJournalAbbreviation(journalText) {
const plain = normalizeJournalText(htmlToPlainText(journalText));
if (!plain) {
return '';
}
const abbreviated = splitJournalWords(plain)
.map(function (word) {
if (STOP_WORDS.has(word.toLowerCase())) {
return '';
}
return abbreviateJournalWord(word);
})
.filter(Boolean);
return abbreviated.join(' ');
}
/** journal 参考文献期刊名是否需要缩写建议 */
export function needsJournalAbbreviation(journalText) {
const plain = normalizeJournalText(htmlToPlainText(journalText));
if (!plain || isAbbreviatedJournalName(plain)) {
return false;
}
const abbreviated = buildJournalAbbreviation(plain);
if (!abbreviated) {
return false;
}
return plain.toLowerCase() !== abbreviated.toLowerCase();
}
function isJournalReference(ref) {
return String(ref && ref.refer_type || '').toLowerCase() === 'journal';
}
export function extractJournalSource(ref) {
if (!isJournalReference(ref)) {
return '';
}
const jouraPlain = htmlToPlainText(ref.joura).trim();
if (jouraPlain) {
return jouraPlain;
}
return '';
}
export function buildJournalAbbrevComment(ref) {
const revision = buildJournalAbbrevRevision(ref);
if (!revision) {
return '';
}
return '期刊名称缩写:' + revision.suggested;
}
export function buildJournalAbbrevRevision(ref) {
const journalSource = extractJournalSource(ref);
if (!journalSource || !needsJournalAbbreviation(journalSource)) {
return null;
}
const suggested = buildJournalAbbreviation(journalSource);
if (!suggested || journalSource.toLowerCase() === suggested.toLowerCase()) {
return null;
}
return {
original: journalSource,
suggested: suggested,
suffix: '. ',
italics: true
};
}

116
src/utils/referenceLinks.js Normal file
View File

@@ -0,0 +1,116 @@
function stripHtml(text) {
return String(text || '')
.replace(/<[^>]+>/g, '')
.trim();
}
/** 去掉末尾 [doi] 等杂质,返回纯 DOI */
function cleanDoiString(raw) {
let text = String(raw || '').trim();
if (!text) {
return null;
}
text = text.replace(/\[doi\]$/i, '').trim();
text = text.replace(/[.,;)\]]+$/g, '').trim();
return text || null;
}
/** 从文献行数据中解析 DOI */
export function resolveReferenceDoi(row) {
const candidates = [row && row.doilink, row && row.refer_doi, row && row.doi].filter(Boolean);
for (let i = 0; i < candidates.length; i += 1) {
const text = String(candidates[i]).trim();
if (!text) {
continue;
}
const match = text.match(/10\.\d{4,9}\/[^\s\[]+/i);
if (match) {
return cleanDoiString(match[0]);
}
if (/^10\.\d/i.test(text)) {
return cleanDoiString(text);
}
}
return null;
}
/** 从 book 类型文献中解析 ISBN */
export function resolveReferenceIsbn(row) {
const raw = String((row && row.isbn) || '').trim();
if (!raw) {
return null;
}
const isbn13 = raw.match(/97[89][-\s.]?(?:\d[-\s.]?){9}[\dXx]/i);
if (isbn13) {
return isbn13[0].replace(/\s+/g, '-');
}
const isbn10 = raw.match(/(?<!\d)(?:\d[-\s.]?){9}[\dXx](?!\d)/i);
if (isbn10) {
return isbn10[0].replace(/\s+/g, '-');
}
const compact = raw.replace(/[^\dXx]/gi, '');
if (compact.length === 10 || compact.length === 13) {
return raw.replace(/\s+/g, ' ').trim();
}
return null;
}
export function resolveReferenceTitle(row) {
if (!row) {
return '';
}
return stripHtml(row.title || row.refer_frag || '');
}
/**
* 生成 journal/other 文献跳转链接
* @param {string|null|undefined} doi - 文献 DOI
* @param {string} title - 文献标题(无 DOI 时用于搜索)
*/
export function getReferenceLinks(doi, title) {
const normalizedDoi = cleanDoiString(doi);
const normalizedTitle = stripHtml(title);
const encodedTitle = encodeURIComponent(normalizedTitle);
const pubmedLink = normalizedDoi
? `https://pubmed.ncbi.nlm.nih.gov/?term=${encodeURIComponent(normalizedDoi)}`
: null;
const scholarLink = normalizedDoi
? `https://scholar.google.com/scholar?q=${encodeURIComponent(normalizedDoi)}`
: `https://scholar.google.com/scholar?q=${encodedTitle}`;
const doiLink = normalizedDoi ? `https://doi.org/${encodeURIComponent(normalizedDoi)}` : null;
return {
pubmedLink,
scholarLink,
doiLink,
googleLink: null,
isBook: false
};
}
/** book 类型:用 ISBN 在 Google 检索 */
export function getBookReferenceLinks(row) {
const isbn = resolveReferenceIsbn(row);
const title = resolveReferenceTitle(row);
const searchQuery = isbn || title;
const encodedQuery = encodeURIComponent(searchQuery);
return {
pubmedLink: null,
scholarLink: null,
doiLink: null,
googleLink: searchQuery ? `https://www.google.com/search?q=${encodedQuery}` : null,
isBook: true,
searchQuery
};
}
export function getReferenceLinksFromRow(row) {
if (row && row.refer_type === 'book') {
return getBookReferenceLinks(row);
}
return getReferenceLinks(resolveReferenceDoi(row), resolveReferenceTitle(row));
}

7
src/utils/zyMode.js Normal file
View File

@@ -0,0 +1,7 @@
/** 地址栏携带 zy=1 时启用内部调试/扩展功能 */
export function isZyModeEnabled(route) {
if (!route || !route.query) {
return false;
}
return String(route.query.zy) === '1';
}

BIN
test-comment.docx Normal file

Binary file not shown.

BIN
test-h1-roundrect.docx Normal file

Binary file not shown.

BIN
test-h1-shape.docx Normal file

Binary file not shown.

BIN
test-h1.docx Normal file

Binary file not shown.

Binary file not shown.

BIN
test-ref-revision.docx Normal file

Binary file not shown.