添加期刊引用分析
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
"katex": "^0.16.21",
|
||||
"mammoth": "^1.5.1",
|
||||
"mathlive": "^0.104.0",
|
||||
"mathml-to-latex": "^1.5.0",
|
||||
"mavon-editor": "^2.6.17",
|
||||
"multi-items-input": "^0.2.0",
|
||||
"pizzip": "^3.1.7",
|
||||
@@ -35543,6 +35544,22 @@
|
||||
"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": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/mavon-editor/-/mavon-editor-2.10.4.tgz",
|
||||
|
||||
154
scripts/test-ref-comment-standalone.mjs
Normal file
154
scripts/test-ref-comment-standalone.mjs
Normal 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));
|
||||
}
|
||||
40
scripts/test-ref-comment.mjs
Normal file
40
scripts/test-ref-comment.mjs
Normal 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));
|
||||
}
|
||||
49
scripts/test-ref-revision.mjs
Normal file
49
scripts/test-ref-revision.mjs
Normal 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'));
|
||||
@@ -165,6 +165,22 @@ const en = {
|
||||
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: {
|
||||
identity: 'Identity',
|
||||
@@ -939,6 +955,8 @@ const en = {
|
||||
exitSourceMode: 'Exit source mode',
|
||||
sourcePlaceholder: 'Paste or edit full HTML here (DOCTYPE, html, head, body supported)',
|
||||
editorPlaceholder: 'Please enter...',
|
||||
clearEditor: 'Clear',
|
||||
clearEditorConfirm: 'Clear all email content?',
|
||||
validateTo: 'Please add at least one addressee',
|
||||
validateSubject: 'Please enter mail subject',
|
||||
sendSuccess: 'Send success',
|
||||
@@ -1128,6 +1146,54 @@ const en = {
|
||||
AnnotationList: 'Annotation List',
|
||||
Annotations: 'Comments',
|
||||
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',
|
||||
PaperRotation: 'Paper Rotation',
|
||||
removeAnnotations: 'Are you sure you want to delete this Annotation?',
|
||||
|
||||
@@ -158,6 +158,22 @@ const zh = {
|
||||
databases:'收录数据库',
|
||||
|
||||
},
|
||||
journalCiteMate: {
|
||||
button: '期刊引用分析',
|
||||
yearRange: '统计范围',
|
||||
yearOptionOne: '近 {n} 年',
|
||||
yearOptionOther: '近 {n} 年',
|
||||
yearPlaceholder: '输入年数',
|
||||
yearInvalid: '请输入大于 0 的整数',
|
||||
yearMax: '统计范围最多为 100 年',
|
||||
refresh: '刷新',
|
||||
topicTitle: '话题引用',
|
||||
countryTitle: 'country',
|
||||
name: '名称',
|
||||
citeCount: '引用数量',
|
||||
totalCites: '引用总数',
|
||||
loadFail: '加载期刊引用分析失败',
|
||||
},
|
||||
paperArticleCount: {
|
||||
Periodroll: '期卷',
|
||||
article: '文章',
|
||||
@@ -924,6 +940,8 @@ const zh = {
|
||||
exitSourceMode: '退出源码编辑',
|
||||
sourcePlaceholder: '在此粘贴或编辑完整 HTML,支持 <!DOCTYPE>、<html>、<head>、<body> 等',
|
||||
editorPlaceholder: '请输入邮件内容...',
|
||||
clearEditor: '清空',
|
||||
clearEditorConfirm: '确定清空邮件正文吗?',
|
||||
validateTo: '请至少添加一个收件人',
|
||||
validateSubject: '请输入邮件主题',
|
||||
sendSuccess: '发送成功',
|
||||
@@ -1114,6 +1132,54 @@ const zh = {
|
||||
AnnotationList: '批注列表',
|
||||
Annotations: '批注',
|
||||
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: '导出 图片',
|
||||
PaperRotation: '纸张方向',
|
||||
removeAnnotations: '确定要删除这条批注吗?',
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
<div style="width: 100%; width: calc(100% - 260px); float: right; height: calc(100% - 0px); background-color: #e4e9ed">
|
||||
<common-word
|
||||
:articleId="articleId"
|
||||
:pArticleId="pArticleId"
|
||||
v-if="htmlContent"
|
||||
ref="commonWord"
|
||||
:value="htmlContent"
|
||||
@@ -464,6 +465,7 @@ export default {
|
||||
ManuscirptContent: [],
|
||||
mathFormulasList: [],
|
||||
articleId: this.$route.query.id,
|
||||
pArticleId: this.$route.query.p_article_id || '',
|
||||
isShowComment: false,
|
||||
urlList: {
|
||||
executeProofreading: 'api/Proofread/change',
|
||||
|
||||
@@ -143,6 +143,21 @@
|
||||
<i class="el-icon-connection" style="color: #4665b0"></i> {{ $t('menu.ClassificationmanagementInfo') }}
|
||||
</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
|
||||
@click="
|
||||
openJournalAgreement({
|
||||
@@ -382,6 +397,91 @@
|
||||
>
|
||||
<common-agreement ref="commonAgreementRef" :journal="currentJournal" :urlList="urlList"></common-agreement>
|
||||
</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-alert
|
||||
:title="$t('JournalCitationAnalysis.feeWarningTitle')"
|
||||
@@ -442,6 +542,23 @@ import commonclass from './class.vue';
|
||||
import commonAgreement from './agreement.vue';
|
||||
import commonJournalInstallment from '../JournalInstallment/index.vue';
|
||||
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 {
|
||||
name: 'app',
|
||||
props: ['urlList', 'source'],
|
||||
@@ -500,6 +617,14 @@ export default {
|
||||
drawer: false,
|
||||
drawerJournalInstallment: false,
|
||||
drawerAgreement: false,
|
||||
drawerCiteMate: false,
|
||||
citeMateLoading: false,
|
||||
citeMateYear: '2',
|
||||
citeMateYearOptions: [1, 2, 3, 4, 5, 6],
|
||||
citeMateTopicList: [],
|
||||
citeMateCountryList: [],
|
||||
citeMateCitesNum: null,
|
||||
citeMateTableHeight: 420,
|
||||
bd: true,
|
||||
ruleForm: {
|
||||
major_title: '',
|
||||
@@ -607,6 +732,28 @@ export default {
|
||||
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() {
|
||||
@@ -617,6 +764,9 @@ export default {
|
||||
async activated() {
|
||||
await this.getDate();
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateCiteMateTableHeight);
|
||||
},
|
||||
methods: {
|
||||
// 失去焦点事件
|
||||
onEditorBlur(quill) {
|
||||
@@ -960,6 +1110,118 @@ export default {
|
||||
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) {
|
||||
if (obj[oldKey]) {
|
||||
obj[newKey] = obj[oldKey];
|
||||
@@ -1457,6 +1719,100 @@ export default {
|
||||
padding: 22px 0 0 0;
|
||||
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 {
|
||||
width: 120px !important;
|
||||
height: 120px !important;
|
||||
|
||||
@@ -1421,7 +1421,8 @@ export default {
|
||||
this.$router.resolve({
|
||||
path: '/GenerateCharts',
|
||||
query: {
|
||||
id: this.$route.query.id
|
||||
id: this.$route.query.id,
|
||||
p_article_id: this.p_article_id
|
||||
}
|
||||
}).href,
|
||||
'_blank'
|
||||
|
||||
@@ -54,7 +54,21 @@
|
||||
<!-- 基本信息 -->
|
||||
<div :ref="tabsList[0].refName" class="scroll-item">
|
||||
<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-item label="Title :" prop="title">
|
||||
<el-input v-model="detailMes.title"></el-input>
|
||||
@@ -235,7 +249,14 @@
|
||||
Add Author</el-button
|
||||
>
|
||||
</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 label="First author" align="center" width="100px">
|
||||
<template slot-scope="scope">
|
||||
@@ -454,7 +475,14 @@
|
||||
>
|
||||
</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 prop="organ_name" label="Affiliation"></el-table-column>
|
||||
<el-table-column label="" width="190" align="center">
|
||||
@@ -952,6 +980,79 @@
|
||||
</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">
|
||||
<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 commonRelated from '../page/components/article/Related.vue';
|
||||
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 {
|
||||
data() {
|
||||
return {
|
||||
importingSubmission: false,
|
||||
importAuthorPreviewVisible: false,
|
||||
importAuthorOrderText: '',
|
||||
importPreviewAuthors: [],
|
||||
importPreviewContext: null,
|
||||
authorTableKey: 0,
|
||||
schoolTableKey: 0,
|
||||
isGenerating: false, // 按钮加载状态
|
||||
percentage: 0, // 进度条
|
||||
finalReview: null,
|
||||
@@ -1609,6 +1732,19 @@ export default {
|
||||
updateChange(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) {
|
||||
this.$refs.tinymceChild1.getContent(type);
|
||||
@@ -1905,34 +2041,278 @@ export default {
|
||||
},
|
||||
|
||||
getAuthorJG() {
|
||||
// 获取作者列表
|
||||
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) => {
|
||||
return this.refreshAuthorOrganLists().catch((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;
|
||||
|
||||
// 获取机构列表
|
||||
this.$api
|
||||
.post('api/Production/getOrganList', this.idform)
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.schoolData = res.data.organs;
|
||||
this.mechanism = res.data.organs;
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
const requestList = [this.$api.post('api/Article/getAuthors', { article_id: articleId }).catch(() => null)];
|
||||
if (userId) {
|
||||
requestList.push(
|
||||
this.$api.post('api/Organ/lists', { article_id: articleId, user_id: userId }).catch(() => null)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$message.error(err);
|
||||
const [authorsRes, organsRes] = await Promise.all(requestList);
|
||||
|
||||
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----时间初始化
|
||||
@@ -2125,7 +2505,7 @@ export default {
|
||||
this.addAuthor = false;
|
||||
this.$refs.add_Author.resetFields();
|
||||
this.addFomauthor.organs = [];
|
||||
this.getAuthorJG();
|
||||
this.refreshAuthorSection({ authorsOnly: true });
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
@@ -2213,7 +2593,7 @@ export default {
|
||||
if (res.code == 0) {
|
||||
this.addSchool = false;
|
||||
this.$refs.add_School.resetFields();
|
||||
this.getAuthorJG();
|
||||
this.refreshAuthorSection({ organsOnly: true });
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
@@ -3210,6 +3590,11 @@ export default {
|
||||
// this.jumpTab(0,this.tabsList[0])
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
zyModeEnabled() {
|
||||
return isZyModeEnabled(this.$route);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
quillEditor,
|
||||
Tinymce,
|
||||
|
||||
@@ -204,7 +204,8 @@ export default {
|
||||
goEdit() {
|
||||
window.open(this.$router.resolve({ path: '/GenerateCharts',
|
||||
query: {
|
||||
id: this.detailMes.article_id
|
||||
id: this.detailMes.article_id,
|
||||
p_article_id: this.Art_Id
|
||||
} }).href, '_blank');
|
||||
|
||||
},
|
||||
|
||||
87
src/components/page/components/ReferenceSearchLinks.vue
Normal file
87
src/components/page/components/ReferenceSearchLinks.vue
Normal 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>
|
||||
@@ -13,6 +13,7 @@ export default {
|
||||
value: { type: String, default: '' },
|
||||
id: { type: String, default: () => 'tiny-' + +new Date() },
|
||||
showSelectTemplateButton: { type: Boolean, default: false },
|
||||
showEmptyButton: { type: Boolean, default: true },
|
||||
// 仅在需要的页面开启“安全初始化内容”
|
||||
useSafeInitContent: { type: Boolean, default: false }
|
||||
},
|
||||
@@ -121,6 +122,8 @@ autoInlineStyles(htmlContent) {
|
||||
initTiny() {
|
||||
const vueInstance = this;
|
||||
const hasTemplateBtn = this.showSelectTemplateButton;
|
||||
const hasEmptyBtn = this.showEmptyButton;
|
||||
const toolbarSuffix = hasEmptyBtn ? ' | clearButton | fullscreen' : ' | fullscreen';
|
||||
window.tinymce.init({
|
||||
selector: `#${this.tinymceId}`,
|
||||
language: 'zh_CN', // 记得下载语言包,没有就删掉这行
|
||||
@@ -137,8 +140,11 @@ autoInlineStyles(htmlContent) {
|
||||
|
||||
// 按照图片布局精准排序:
|
||||
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 | 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' +
|
||||
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',
|
||||
@@ -158,6 +164,16 @@ autoInlineStyles(htmlContent) {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasEmptyBtn) {
|
||||
editor.ui.registry.addButton('clearButton', {
|
||||
text: 'Empty',
|
||||
onAction() {
|
||||
editor.setContent('');
|
||||
vueInstance.$emit('input', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 统一给自定义按钮加样式(无按钮时不会产生影响)
|
||||
CommonJS.inTinymceButtonClass();
|
||||
},
|
||||
@@ -177,7 +193,6 @@ autoInlineStyles(htmlContent) {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 假设 rawHtml 是你那一大串完整的 HTML
|
||||
prepareContentForEditor(rawHtml) {
|
||||
// 1. 分离出 Head 及其之前的部分 (包含 doctype, html, head)
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<span v-html="renderText(processedItem.title)"></span>
|
||||
</div>
|
||||
|
||||
<table border="1" class="custom-table-core">
|
||||
<table class="custom-table-core">
|
||||
<tbody>
|
||||
<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">
|
||||
@@ -201,7 +201,7 @@ export default {
|
||||
min-width: 794px; /* A4 宽度参考 */
|
||||
max-width: 90vw;
|
||||
background-color: #fff;
|
||||
padding: 10px 20px;
|
||||
padding: 2.54cm 1.91cm;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
|
||||
border-radius: 4px;
|
||||
@@ -243,8 +243,20 @@ export default {
|
||||
}
|
||||
|
||||
.custom-table-core {
|
||||
width: 100%;
|
||||
width: 98.3%;
|
||||
margin: 0 auto;
|
||||
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 {
|
||||
@@ -258,22 +270,56 @@ export default {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
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 {
|
||||
color: #333;
|
||||
margin-top: 15px;
|
||||
font-size: 7.5pt;
|
||||
font-family: 'Charis SIL', serif;
|
||||
line-height: 10pt;
|
||||
}
|
||||
|
||||
.imageNote {
|
||||
text-align: center;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.oddColor td {
|
||||
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, .table-fade-leave-to { opacity: 0; }
|
||||
|
||||
|
||||
@@ -74,6 +74,85 @@
|
||||
<i class="el-icon-document base-margin" :style="{ '--m-r': '2px' }"> </i>
|
||||
{{ $t('commonTable.BatchAddcontent') }}
|
||||
</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>
|
||||
</div>
|
||||
<li
|
||||
@@ -1064,6 +1143,18 @@ import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { debounce, throttle } from '@/common/js/debounce';
|
||||
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
|
||||
import LatexDataPanel from './LatexDataPanel.vue';
|
||||
import { downloadManuscriptWord, fetchManuscriptReferenceList } from '@/utils/exportManuscriptWord';
|
||||
import {
|
||||
buildReferencesHtmlLabels,
|
||||
downloadReferencesEditableHtml,
|
||||
parseReferencesEditableHtml
|
||||
} from '@/utils/manuscriptReferenceHtml';
|
||||
import {
|
||||
buildCitationReviewQueue,
|
||||
buildCitationReviewHtmlLabels,
|
||||
downloadCitationReviewHtml
|
||||
} from '@/utils/manuscriptCitationRelevance';
|
||||
import { isZyModeEnabled } from '@/utils/zyMode';
|
||||
export default {
|
||||
name: 'tinymce',
|
||||
components: {
|
||||
@@ -1076,6 +1167,9 @@ export default {
|
||||
articleId: {
|
||||
default: ''
|
||||
},
|
||||
pArticleId: {
|
||||
default: ''
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
@@ -1185,6 +1279,11 @@ export default {
|
||||
imagePath: require('@/assets/img/carriageReturn.png'), // 或者你可以设置其他路径
|
||||
scrollPosition: 0,
|
||||
wordList: [],
|
||||
exportingManuscriptWord: false,
|
||||
referencesHtmlLoading: false,
|
||||
exportingReferencesHtmlWord: false,
|
||||
citationRelevanceLoading: false,
|
||||
manuscriptReferences: [],
|
||||
proofreadingList: [],
|
||||
commentList: [],
|
||||
visibleDivs: [], // 存储当前可见的 div id
|
||||
@@ -1309,6 +1408,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
zyModeEnabled() {
|
||||
return isZyModeEnabled(this.$route);
|
||||
},
|
||||
sortedProofreadingList() {
|
||||
const order = [2, 1, 3];
|
||||
const rank = { 2: 0, 1: 1, 3: 2 };
|
||||
@@ -1444,6 +1546,170 @@ export default {
|
||||
this.editors = {};
|
||||
},
|
||||
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) {
|
||||
// 1. 找到起始节点所属的 .pMain
|
||||
let startNode = range.startContainer;
|
||||
|
||||
@@ -65,6 +65,24 @@
|
||||
style="width: 200px; float: right; padding: 10px; height: calc(100% - 28px); box-sizing: border-box; overflow-y: auto"
|
||||
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">
|
||||
<li v-show="currentMenu == 1">
|
||||
<div style="" class="go-content-charts-item-box" :class="isCollapse ? 'double' : 'single'">
|
||||
@@ -137,7 +155,15 @@
|
||||
</a>
|
||||
</span>
|
||||
<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>
|
||||
</span>
|
||||
<span style="margin-left: 6px" @click="handledelete(img, 'img')">
|
||||
@@ -329,7 +355,15 @@
|
||||
</el-tooltip> </span
|
||||
></span>
|
||||
<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>
|
||||
</span>
|
||||
<span style="margin-left: 6px" @click="handledelete(table, 'table')">
|
||||
@@ -475,6 +509,13 @@
|
||||
import DynamicTable from './DynamicTable.vue';
|
||||
import { mediaUrl } from '@/common/js/commonJS.js';
|
||||
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 {
|
||||
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
||||
@@ -505,12 +546,24 @@ export default {
|
||||
mathFormulasList: [],
|
||||
tablesHtml: [],
|
||||
imagesHtml: [],
|
||||
activeNames: ['images', 'tables']
|
||||
activeNames: ['images', 'tables'],
|
||||
exportingAllWord: false,
|
||||
exportingTableId: null,
|
||||
exportingAllFiguresWord: false,
|
||||
exportingFigureId: null
|
||||
};
|
||||
},
|
||||
components: {
|
||||
DynamicTable
|
||||
},
|
||||
computed: {
|
||||
zyModeEnabled() {
|
||||
return isZyModeEnabled(this.$route);
|
||||
},
|
||||
exportableImages() {
|
||||
return (this.images || []).filter((img) => canExportFigureItem(img));
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
// 注册一个局部的自定义指令 v-focus
|
||||
focus: {
|
||||
@@ -526,6 +579,9 @@ export default {
|
||||
changeIsCollapse(e) {
|
||||
localStorage.setItem('isCollapse', e);
|
||||
},
|
||||
isFigureExportable(item) {
|
||||
return canExportFigureItem(item);
|
||||
},
|
||||
isShowEditComment() {
|
||||
if (localStorage.getItem('U_role')) {
|
||||
var identity = localStorage.getItem('U_role');
|
||||
@@ -930,7 +986,85 @@ if(newData.ami_id) {
|
||||
},
|
||||
openPreview(index, 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() {
|
||||
var that = this;
|
||||
@@ -1049,6 +1183,25 @@ if(newData.ami_id) {
|
||||
.el-menu-vertical-demo {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -297,19 +297,21 @@
|
||||
<p>
|
||||
{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. <em>{{ scope.row.joura }}</em
|
||||
>. <span :class="getJournalDateno(scope.row.dateno, 'title')">{{ scope.row.dateno }}</span
|
||||
>.<br />
|
||||
>.<reference-search-links :row="scope.row"></reference-search-links><br />
|
||||
</p>
|
||||
<a class="doiLink" :href="scope.row.doilink" target="_blank">{{ scope.row.doilink }}</a>
|
||||
</div>
|
||||
<!-- book 形式 -->
|
||||
<div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item">
|
||||
<p>{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. {{ scope.row.dateno }}. <br /></p>
|
||||
<p>
|
||||
{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. {{ scope.row.dateno }}. <reference-search-links :row="scope.row"></reference-search-links><br />
|
||||
</p>
|
||||
<a class="doiLink" :href="scope.row.isbn" target="_blank">{{ scope.row.isbn }}</a>
|
||||
</div>
|
||||
<!-- 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>
|
||||
|
||||
<reference-search-links :row="scope.row"></reference-search-links>
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -895,6 +897,7 @@
|
||||
|
||||
<script>
|
||||
import VueUeditorWrap from 'vue-ueditor-wrap'; // ES6 Module
|
||||
import ReferenceSearchLinks from '@/components/page/components/ReferenceSearchLinks.vue';
|
||||
|
||||
/** 单条引用 records[].status:0 待检测 2 已完成 3 检测失败 */
|
||||
const REF_RELEVANCE_RECORD_STATUS_PENDING = 0;
|
||||
@@ -1450,8 +1453,7 @@ export default {
|
||||
return title.replace(reg, (match) => {
|
||||
return `<span style="color: red; font-weight: bold;">${match}</span>`;
|
||||
});
|
||||
}
|
||||
,
|
||||
},
|
||||
getJournalDateno(dateno, type) {
|
||||
if (dateno && typeof dateno === 'string') {
|
||||
const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === '');
|
||||
@@ -5244,7 +5246,8 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
VueUeditorWrap
|
||||
VueUeditorWrap,
|
||||
ReferenceSearchLinks
|
||||
},
|
||||
watch: {
|
||||
role: {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<el-dialog
|
||||
:title="articleContextDialogTitle"
|
||||
:visible.sync="articleContextDialogVisible"
|
||||
width="1100px"
|
||||
width="1140px"
|
||||
top="8vh"
|
||||
append-to-body
|
||||
custom-class="article-context-dialog"
|
||||
@@ -54,23 +54,16 @@
|
||||
<el-button type="primary" size="small" :loading="articleContextLoading" @click="loadArticleContext()">
|
||||
{{ $t('mailboxSend.articleContextSearch') }}
|
||||
</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 v-if="articleContext.loaded" class="article-context-loaded-hint">
|
||||
<div class="article-loaded-line">
|
||||
<span class="article-loaded-sn">SN {{ articleContext.acceptSn || '—' }}</span>
|
||||
<div v-if="articleContext.loaded" class="article-context-meta-card">
|
||||
<div class="article-context-meta-card__row">
|
||||
<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 v-if="articleContext.title" class="article-loaded-title">{{ articleContext.title }}</div>
|
||||
</div>
|
||||
<div v-else-if="!articleContextLoading" class="article-contact-empty article-contact-empty--hint">
|
||||
{{ $t('mailboxSend.articleContextSearchHint') }}
|
||||
@@ -123,7 +116,7 @@
|
||||
/>
|
||||
</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -131,7 +124,12 @@
|
||||
<tr
|
||||
v-for="(author, idx) in articleContext.authors"
|
||||
: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">
|
||||
<el-checkbox
|
||||
@@ -141,17 +139,36 @@
|
||||
/>
|
||||
</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__name">{{ author.name || '—' }}</div>
|
||||
<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>
|
||||
</div>
|
||||
<div v-else class="author-name-email-cell__email reviewer-no-email">{{ $t('mailboxSend.articleContextNoEmail') }}</div>
|
||||
</div>
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -184,7 +201,12 @@
|
||||
<tr
|
||||
v-for="(row, idx) in articleContext.reviewRows"
|
||||
: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">
|
||||
<el-checkbox
|
||||
@@ -243,7 +265,12 @@
|
||||
<tr
|
||||
v-for="(row, idx) in articleContext.finalReviewers"
|
||||
: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">
|
||||
<el-checkbox
|
||||
@@ -269,6 +296,15 @@
|
||||
<span class="article-context-selected-tab is-active">
|
||||
{{ $t('mailboxSend.to') }} ({{ articleSelectedTotalCount }})
|
||||
</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 class="article-context-selected-list">
|
||||
<div v-if="!articleSelectedContactList.length" class="article-context-selected-empty">
|
||||
@@ -1180,6 +1216,17 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
isContactSelected(module, key) {
|
||||
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) {
|
||||
if (checked) {
|
||||
const contact = this.getModuleContactList(module).find(function (item) {
|
||||
@@ -2005,27 +2052,48 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
.article-context-search-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.article-context-dialog-body {
|
||||
max-height: 60vh;
|
||||
max-height: 62vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.article-context-loaded-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
.article-context-meta-card {
|
||||
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;
|
||||
line-height: 1.5;
|
||||
color: #606266;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.article-template-vars-bar {
|
||||
@@ -2045,14 +2113,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
.article-loaded-sn {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.article-loaded-title {
|
||||
width: 100%;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.article-contact-empty--hint {
|
||||
@@ -2064,7 +2125,8 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
|
||||
.article-context-layout {
|
||||
display: flex;
|
||||
min-height: 320px;
|
||||
min-height: 360px;
|
||||
max-height: 48vh;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -2109,13 +2171,13 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
.article-context-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.article-context-selected {
|
||||
width: 280px;
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2123,10 +2185,19 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.article-context-selected-clear {
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.article-context-selected-tab {
|
||||
display: inline-block;
|
||||
padding-bottom: 10px;
|
||||
@@ -2144,7 +2215,6 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
.article-context-selected-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
@@ -2354,22 +2424,30 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
}
|
||||
|
||||
.overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.review_table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.review_table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.review_table th,
|
||||
.review_table td {
|
||||
padding: 8px 12px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
@@ -2394,6 +2472,22 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
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 {
|
||||
color: #d4d7de;
|
||||
}
|
||||
@@ -2416,12 +2510,34 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review_table_name {
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.review_table_role {
|
||||
min-width: 220px;
|
||||
white-space: nowrap;
|
||||
width: 148px;
|
||||
min-width: 148px;
|
||||
max-width: 148px;
|
||||
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 {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
166
src/utils/exportFigureWord.js
Normal file
166
src/utils/exportFigureWord.js
Normal 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`);
|
||||
}
|
||||
377
src/utils/exportManuscriptAuthor.js
Normal file
377
src/utils/exportManuscriptAuthor.js
Normal 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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/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;
|
||||
}
|
||||
1211
src/utils/exportManuscriptWord.js
Normal file
1211
src/utils/exportManuscriptWord.js
Normal file
File diff suppressed because it is too large
Load Diff
640
src/utils/exportTableWord.js
Normal file
640
src/utils/exportTableWord.js
Normal 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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/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
|
||||
};
|
||||
102
src/utils/exportWordLayout.js
Normal file
102
src/utils/exportWordLayout.js
Normal 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;
|
||||
}
|
||||
406
src/utils/manuscriptCitationRelevance.js
Normal file
406
src/utils/manuscriptCitationRelevance.js
Normal 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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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')
|
||||
};
|
||||
}
|
||||
236
src/utils/manuscriptReferenceHtml.js
Normal file
236
src/utils/manuscriptReferenceHtml.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import { htmlToPlainText } from '@/utils/exportTableWord';
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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')
|
||||
};
|
||||
}
|
||||
409
src/utils/productionSubmissionImport.js
Normal file
409
src/utils/productionSubmissionImport.js
Normal 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-Xiao;qian 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 ML;Yang 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
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) : ''
|
||||
};
|
||||
}
|
||||
251
src/utils/referenceAuthorAma.js
Normal file
251
src/utils/referenceAuthorAma.js
Normal 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);
|
||||
}
|
||||
282
src/utils/referenceJournalAbbrev.js
Normal file
282
src/utils/referenceJournalAbbrev.js
Normal 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
116
src/utils/referenceLinks.js
Normal 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
7
src/utils/zyMode.js
Normal 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
BIN
test-comment.docx
Normal file
Binary file not shown.
BIN
test-h1-roundrect.docx
Normal file
BIN
test-h1-roundrect.docx
Normal file
Binary file not shown.
BIN
test-h1-shape.docx
Normal file
BIN
test-h1-shape.docx
Normal file
Binary file not shown.
BIN
test-h1.docx
Normal file
BIN
test-h1.docx
Normal file
Binary file not shown.
BIN
test-ref-comment-standalone.docx
Normal file
BIN
test-ref-comment-standalone.docx
Normal file
Binary file not shown.
BIN
test-ref-revision.docx
Normal file
BIN
test-ref-revision.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user