添加期刊引用分析
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"mammoth": "^1.5.1",
|
"mammoth": "^1.5.1",
|
||||||
"mathlive": "^0.104.0",
|
"mathlive": "^0.104.0",
|
||||||
|
"mathml-to-latex": "^1.5.0",
|
||||||
"mavon-editor": "^2.6.17",
|
"mavon-editor": "^2.6.17",
|
||||||
"multi-items-input": "^0.2.0",
|
"multi-items-input": "^0.2.0",
|
||||||
"pizzip": "^3.1.7",
|
"pizzip": "^3.1.7",
|
||||||
@@ -35543,6 +35544,22 @@
|
|||||||
"url": "https://paypal.me/arnogourdol"
|
"url": "https://paypal.me/arnogourdol"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mathml-to-latex": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mathml-to-latex/-/mathml-to-latex-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-gQ0uK3zqB8HwlfaXJkEL5rgaZNbKUiBMmBP/B/W+b+t6KcseLSuYb1b0BjLgS9ZiQa24ePkqTX8/6FaQuDL7wQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.9.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mathml-to-latex/node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.9.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
|
||||||
|
"integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mavon-editor": {
|
"node_modules/mavon-editor": {
|
||||||
"version": "2.10.4",
|
"version": "2.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/mavon-editor/-/mavon-editor-2.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/mavon-editor/-/mavon-editor-2.10.4.tgz",
|
||||||
|
|||||||
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',
|
databases:'Database inclusion',
|
||||||
|
|
||||||
},
|
},
|
||||||
|
journalCiteMate: {
|
||||||
|
button: 'Citation analysis',
|
||||||
|
yearRange: 'Time range',
|
||||||
|
yearOptionOne: 'Last {n} year',
|
||||||
|
yearOptionOther: 'Last {n} years',
|
||||||
|
yearPlaceholder: 'Enter years',
|
||||||
|
yearInvalid: 'Please enter a whole number greater than 0.',
|
||||||
|
yearMax: 'Time range cannot exceed 100 years.',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
topicTitle: 'Topics',
|
||||||
|
countryTitle: 'country',
|
||||||
|
name: 'Name',
|
||||||
|
citeCount: 'Citations',
|
||||||
|
totalCites: 'Total citations',
|
||||||
|
loadFail: 'Failed to load citation analysis',
|
||||||
|
},
|
||||||
|
|
||||||
partyRole: {
|
partyRole: {
|
||||||
identity: 'Identity',
|
identity: 'Identity',
|
||||||
@@ -939,6 +955,8 @@ const en = {
|
|||||||
exitSourceMode: 'Exit source mode',
|
exitSourceMode: 'Exit source mode',
|
||||||
sourcePlaceholder: 'Paste or edit full HTML here (DOCTYPE, html, head, body supported)',
|
sourcePlaceholder: 'Paste or edit full HTML here (DOCTYPE, html, head, body supported)',
|
||||||
editorPlaceholder: 'Please enter...',
|
editorPlaceholder: 'Please enter...',
|
||||||
|
clearEditor: 'Clear',
|
||||||
|
clearEditorConfirm: 'Clear all email content?',
|
||||||
validateTo: 'Please add at least one addressee',
|
validateTo: 'Please add at least one addressee',
|
||||||
validateSubject: 'Please enter mail subject',
|
validateSubject: 'Please enter mail subject',
|
||||||
sendSuccess: 'Send success',
|
sendSuccess: 'Send success',
|
||||||
@@ -1128,6 +1146,54 @@ const en = {
|
|||||||
AnnotationList: 'Annotation List',
|
AnnotationList: 'Annotation List',
|
||||||
Annotations: 'Comments',
|
Annotations: 'Comments',
|
||||||
exportWord: 'Export Word',
|
exportWord: 'Export Word',
|
||||||
|
exportManuscriptEmpty: 'No content to export.',
|
||||||
|
exportManuscriptSuccess: 'Word downloaded.',
|
||||||
|
exportManuscriptFail: 'Failed to export Word.',
|
||||||
|
citeRelevanceDetect: 'Download relevance HTML',
|
||||||
|
citeRelevanceTitle: 'Citation relevance check',
|
||||||
|
citeRelevanceTitleProgress: 'Citation relevance ({current}/{total})',
|
||||||
|
citeRelevanceEmpty: 'No citations to review.',
|
||||||
|
citeRelevanceNoContent: 'No manuscript content.',
|
||||||
|
citeRelevanceNoCitations: 'No citation markers found (e.g. [1], [1-7]).',
|
||||||
|
citeRelevanceLoadFail: 'Failed to load references.',
|
||||||
|
citeRelevanceParagraph: 'Paragraph {n}',
|
||||||
|
citeRelevanceRefLabel: 'Reference [{n}]',
|
||||||
|
citeRelevanceRefMissing: 'Reference not found',
|
||||||
|
citeRelevanceParagraphTitle: 'Paragraph',
|
||||||
|
citeRelevanceReferenceTitle: 'Reference [{n}]',
|
||||||
|
citeRelevanceWenaiTitle: 'Full prompt for WenAI',
|
||||||
|
citeRelevanceCopy: 'Copy',
|
||||||
|
citeRelevanceCopyWenai: 'Copy full prompt',
|
||||||
|
citeRelevanceCopySuccess: 'Copied to clipboard',
|
||||||
|
citeRelevanceCopyFail: 'Copy failed',
|
||||||
|
citeRelevanceCopyEmpty: 'Nothing to copy',
|
||||||
|
citeRelevancePrev: 'Previous',
|
||||||
|
citeRelevanceNext: 'Next',
|
||||||
|
citeRelevanceClose: 'Close',
|
||||||
|
citeRelevanceCopyAllWenai: 'Copy all prompts',
|
||||||
|
citeRelevanceBatchTip: '{n} references per batch within each paragraph',
|
||||||
|
citeRelevanceRefBatch: 'Batch refs {nums}',
|
||||||
|
citeRelevanceBatchProgress: 'Paragraph batch {current}/{total}',
|
||||||
|
citeRelevanceGroupTotal: '{n} paragraphs to review',
|
||||||
|
citeRelevanceGroupNavLabel: 'Para {paragraph} · {refs}',
|
||||||
|
citeRelevanceJumpTo: 'Jump to',
|
||||||
|
citeRelevanceCopyGroup: 'Copy paragraph',
|
||||||
|
citeRelevanceDownloadHtml: 'Download HTML',
|
||||||
|
citeRelevanceDownloadHtmlSuccess: 'HTML downloaded',
|
||||||
|
refHtmlDownload: 'Download references HTML',
|
||||||
|
refHtmlDownloadSuccess: 'References HTML downloaded',
|
||||||
|
refHtmlToWord: 'References HTML to Word',
|
||||||
|
refHtmlToWordSuccess: 'Word exported with edited references',
|
||||||
|
refHtmlToWordFail: 'Failed to convert references HTML to Word',
|
||||||
|
refHtmlLoadFail: 'Failed to load references',
|
||||||
|
refHtmlEmpty: 'No references found',
|
||||||
|
refHtmlParseEmpty: 'No reference entries found in HTML. Please use the HTML downloaded from this system.',
|
||||||
|
refHtmlPageTitle: 'References editor',
|
||||||
|
refHtmlReferencesTitle: 'References',
|
||||||
|
refHtmlIntro: 'Edit each reference directly, or paste AI output (supports <i> italics and other HTML). When done, click "Download edited HTML", then use "References HTML to Word" on the typesetting page.',
|
||||||
|
refHtmlSaveTip: 'Tip: save the HTML after editing, then import it on the typesetting page to export Word.',
|
||||||
|
refHtmlDownloadEdited: 'Download edited HTML',
|
||||||
|
refHtmlDownloadFileName: 'references-edited.html',
|
||||||
exportImg: 'Export PNG',
|
exportImg: 'Export PNG',
|
||||||
PaperRotation: 'Paper Rotation',
|
PaperRotation: 'Paper Rotation',
|
||||||
removeAnnotations: 'Are you sure you want to delete this Annotation?',
|
removeAnnotations: 'Are you sure you want to delete this Annotation?',
|
||||||
|
|||||||
@@ -158,6 +158,22 @@ const zh = {
|
|||||||
databases:'收录数据库',
|
databases:'收录数据库',
|
||||||
|
|
||||||
},
|
},
|
||||||
|
journalCiteMate: {
|
||||||
|
button: '期刊引用分析',
|
||||||
|
yearRange: '统计范围',
|
||||||
|
yearOptionOne: '近 {n} 年',
|
||||||
|
yearOptionOther: '近 {n} 年',
|
||||||
|
yearPlaceholder: '输入年数',
|
||||||
|
yearInvalid: '请输入大于 0 的整数',
|
||||||
|
yearMax: '统计范围最多为 100 年',
|
||||||
|
refresh: '刷新',
|
||||||
|
topicTitle: '话题引用',
|
||||||
|
countryTitle: 'country',
|
||||||
|
name: '名称',
|
||||||
|
citeCount: '引用数量',
|
||||||
|
totalCites: '引用总数',
|
||||||
|
loadFail: '加载期刊引用分析失败',
|
||||||
|
},
|
||||||
paperArticleCount: {
|
paperArticleCount: {
|
||||||
Periodroll: '期卷',
|
Periodroll: '期卷',
|
||||||
article: '文章',
|
article: '文章',
|
||||||
@@ -924,6 +940,8 @@ const zh = {
|
|||||||
exitSourceMode: '退出源码编辑',
|
exitSourceMode: '退出源码编辑',
|
||||||
sourcePlaceholder: '在此粘贴或编辑完整 HTML,支持 <!DOCTYPE>、<html>、<head>、<body> 等',
|
sourcePlaceholder: '在此粘贴或编辑完整 HTML,支持 <!DOCTYPE>、<html>、<head>、<body> 等',
|
||||||
editorPlaceholder: '请输入邮件内容...',
|
editorPlaceholder: '请输入邮件内容...',
|
||||||
|
clearEditor: '清空',
|
||||||
|
clearEditorConfirm: '确定清空邮件正文吗?',
|
||||||
validateTo: '请至少添加一个收件人',
|
validateTo: '请至少添加一个收件人',
|
||||||
validateSubject: '请输入邮件主题',
|
validateSubject: '请输入邮件主题',
|
||||||
sendSuccess: '发送成功',
|
sendSuccess: '发送成功',
|
||||||
@@ -1114,6 +1132,54 @@ const zh = {
|
|||||||
AnnotationList: '批注列表',
|
AnnotationList: '批注列表',
|
||||||
Annotations: '批注',
|
Annotations: '批注',
|
||||||
exportWord: '导出 Word',
|
exportWord: '导出 Word',
|
||||||
|
exportManuscriptEmpty: '没有可导出的内容。',
|
||||||
|
exportManuscriptSuccess: 'Word 已下载。',
|
||||||
|
exportManuscriptFail: 'Word 导出失败。',
|
||||||
|
citeRelevanceDetect: '下载相关性 HTML',
|
||||||
|
citeRelevanceTitle: '引文相关性核查',
|
||||||
|
citeRelevanceTitleProgress: '引文相关性核查 ({current}/{total})',
|
||||||
|
citeRelevanceEmpty: '当前没有可核查的引文。',
|
||||||
|
citeRelevanceNoContent: '没有正文内容。',
|
||||||
|
citeRelevanceNoCitations: '正文中未找到引用标记(如 [1]、[1-7])。',
|
||||||
|
citeRelevanceLoadFail: '加载参考文献失败。',
|
||||||
|
citeRelevanceParagraph: '正文段落 {n}',
|
||||||
|
citeRelevanceRefLabel: '参考文献 [{n}]',
|
||||||
|
citeRelevanceRefMissing: '未找到对应文献',
|
||||||
|
citeRelevanceParagraphTitle: '正文段落',
|
||||||
|
citeRelevanceReferenceTitle: '参考文献 [{n}]',
|
||||||
|
citeRelevanceWenaiTitle: '复制到 WenAI 的完整提示',
|
||||||
|
citeRelevanceCopy: '复制',
|
||||||
|
citeRelevanceCopyWenai: '复制完整提示',
|
||||||
|
citeRelevanceCopySuccess: '已复制到剪贴板',
|
||||||
|
citeRelevanceCopyFail: '复制失败',
|
||||||
|
citeRelevanceCopyEmpty: '没有可复制的内容',
|
||||||
|
citeRelevancePrev: '上一条',
|
||||||
|
citeRelevanceNext: '下一条',
|
||||||
|
citeRelevanceClose: '关闭',
|
||||||
|
citeRelevanceCopyAllWenai: '复制全部提示',
|
||||||
|
citeRelevanceBatchTip: '每段正文按 {n} 条参考文献一批提问',
|
||||||
|
citeRelevanceRefBatch: '本批文献 {nums}',
|
||||||
|
citeRelevanceBatchProgress: '本段第 {current}/{total} 批',
|
||||||
|
citeRelevanceGroupTotal: '共 {n} 段待核查',
|
||||||
|
citeRelevanceGroupNavLabel: '段落 {paragraph} · {refs}',
|
||||||
|
citeRelevanceJumpTo: '快速跳转',
|
||||||
|
citeRelevanceCopyGroup: '复制本段',
|
||||||
|
citeRelevanceDownloadHtml: '下载 HTML',
|
||||||
|
citeRelevanceDownloadHtmlSuccess: 'HTML 已下载',
|
||||||
|
refHtmlDownload: '下载参考文献 HTML',
|
||||||
|
refHtmlDownloadSuccess: '参考文献 HTML 已下载',
|
||||||
|
refHtmlToWord: '参考文献 HTML 转 Word',
|
||||||
|
refHtmlToWordSuccess: 'Word 已导出(含编辑后的参考文献)',
|
||||||
|
refHtmlToWordFail: '参考文献 HTML 转 Word 失败',
|
||||||
|
refHtmlLoadFail: '加载参考文献失败',
|
||||||
|
refHtmlEmpty: '暂无参考文献',
|
||||||
|
refHtmlParseEmpty: 'HTML 中未找到参考文献条目,请使用本系统下载的参考文献 HTML',
|
||||||
|
refHtmlPageTitle: '参考文献编辑',
|
||||||
|
refHtmlReferencesTitle: 'References',
|
||||||
|
refHtmlIntro: '可直接编辑每条文献,或粘贴 AI 返回的内容(支持 <i> 斜体等 HTML 标签)。编辑完成后点击「下载编辑后的 HTML」,再在排版页使用「参考文献 HTML 转 Word」。',
|
||||||
|
refHtmlSaveTip: '提示:编辑后请点击上方按钮保存 HTML;也可在排版页导入该文件导出 Word。',
|
||||||
|
refHtmlDownloadEdited: '下载编辑后的 HTML',
|
||||||
|
refHtmlDownloadFileName: 'references-edited.html',
|
||||||
exportImg: '导出 图片',
|
exportImg: '导出 图片',
|
||||||
PaperRotation: '纸张方向',
|
PaperRotation: '纸张方向',
|
||||||
removeAnnotations: '确定要删除这条批注吗?',
|
removeAnnotations: '确定要删除这条批注吗?',
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
<div style="width: 100%; width: calc(100% - 260px); float: right; height: calc(100% - 0px); background-color: #e4e9ed">
|
<div style="width: 100%; width: calc(100% - 260px); float: right; height: calc(100% - 0px); background-color: #e4e9ed">
|
||||||
<common-word
|
<common-word
|
||||||
:articleId="articleId"
|
:articleId="articleId"
|
||||||
|
:pArticleId="pArticleId"
|
||||||
v-if="htmlContent"
|
v-if="htmlContent"
|
||||||
ref="commonWord"
|
ref="commonWord"
|
||||||
:value="htmlContent"
|
:value="htmlContent"
|
||||||
@@ -464,6 +465,7 @@ export default {
|
|||||||
ManuscirptContent: [],
|
ManuscirptContent: [],
|
||||||
mathFormulasList: [],
|
mathFormulasList: [],
|
||||||
articleId: this.$route.query.id,
|
articleId: this.$route.query.id,
|
||||||
|
pArticleId: this.$route.query.p_article_id || '',
|
||||||
isShowComment: false,
|
isShowComment: false,
|
||||||
urlList: {
|
urlList: {
|
||||||
executeProofreading: 'api/Proofread/change',
|
executeProofreading: 'api/Proofread/change',
|
||||||
|
|||||||
@@ -143,6 +143,21 @@
|
|||||||
<i class="el-icon-connection" style="color: #4665b0"></i> {{ $t('menu.ClassificationmanagementInfo') }}
|
<i class="el-icon-connection" style="color: #4665b0"></i> {{ $t('menu.ClassificationmanagementInfo') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
@click="openJournalCiteMate(scope.row)"
|
||||||
|
style="
|
||||||
|
background-color: #fdf6ec;
|
||||||
|
color: #e6a23c;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 2px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="el-icon-data-analysis" style="color: #e6a23c"></i> {{ $t('journalCiteMate.button') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@click="
|
@click="
|
||||||
openJournalAgreement({
|
openJournalAgreement({
|
||||||
@@ -382,6 +397,91 @@
|
|||||||
>
|
>
|
||||||
<common-agreement ref="commonAgreementRef" :journal="currentJournal" :urlList="urlList"></common-agreement>
|
<common-agreement ref="commonAgreementRef" :journal="currentJournal" :urlList="urlList"></common-agreement>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
<el-drawer
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
custom-class="journal-cite-mate-drawer"
|
||||||
|
:title="$t('GroupClassification.Journal') + ' : ' + currentJournal.title"
|
||||||
|
:visible.sync="drawerCiteMate"
|
||||||
|
direction="rtl"
|
||||||
|
:before-close="handleCiteMateClose"
|
||||||
|
:wrapperClosable="false"
|
||||||
|
size="70%"
|
||||||
|
@opened="updateCiteMateTableHeight"
|
||||||
|
>
|
||||||
|
<div class="journal-cite-mate-panel" v-loading="citeMateLoading">
|
||||||
|
<div class="journal-cite-mate-toolbar">
|
||||||
|
<span class="journal-cite-mate-label">{{ $t('journalCiteMate.yearRange') }}</span>
|
||||||
|
<el-select
|
||||||
|
v-model="citeMateYear"
|
||||||
|
size="small"
|
||||||
|
class="journal-cite-mate-year-select"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
:placeholder="$t('journalCiteMate.yearPlaceholder')"
|
||||||
|
@change="handleCiteMateYearChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in citeMateYearOptionList"
|
||||||
|
:key="item"
|
||||||
|
:label="formatCiteMateYearLabel(item)"
|
||||||
|
:value="String(item)"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-button size="small" type="primary" plain icon="el-icon-refresh" @click="loadJournalCiteMate">
|
||||||
|
{{ $t('journalCiteMate.refresh') }}
|
||||||
|
</el-button>
|
||||||
|
<span v-if="citeMateCitesNum != null" class="journal-cite-mate-total">
|
||||||
|
{{ $t('journalCiteMate.totalCites') }}:<strong>{{ citeMateCitesNum }}</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="16" class="journal-cite-mate-body">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div ref="citeMateBlock" class="journal-cite-mate-block">
|
||||||
|
<div class="journal-cite-mate-block-title">
|
||||||
|
<span class="journal-cite-mate-block-name">{{ $t('journalCiteMate.topicTitle') }}</span>
|
||||||
|
<span class="journal-cite-mate-count">{{ citeMateTopicList.length }}</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="citeMateTopicList" border size="mini" :height="citeMateTableHeight" empty-text="—">
|
||||||
|
<el-table-column type="index" label="#" width="50" align="center"></el-table-column>
|
||||||
|
<el-table-column :label="$t('journalCiteMate.name')" min-width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getCiteMateRowName(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('journalCiteMate.citeCount')" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getCiteMateRowCount(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="journal-cite-mate-block">
|
||||||
|
<div class="journal-cite-mate-block-title">
|
||||||
|
<span class="journal-cite-mate-block-name">{{ $t('journalCiteMate.countryTitle') }}</span>
|
||||||
|
<span class="journal-cite-mate-count">{{ citeMateCountryList.length }}</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="citeMateCountryList" border size="mini" :height="citeMateTableHeight" empty-text="—">
|
||||||
|
<el-table-column type="index" label="#" width="50" align="center"></el-table-column>
|
||||||
|
<el-table-column :label="$t('journalCiteMate.name')" min-width="160">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getCiteMateRowName(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column :label="$t('journalCiteMate.citeCount')" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getCiteMateRowCount(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
<el-dialog :title="$t('JournalCitationAnalysis.feeDialogTitle')" :visible.sync="feeDialogVisible" width="800px">
|
<el-dialog :title="$t('JournalCitationAnalysis.feeDialogTitle')" :visible.sync="feeDialogVisible" width="800px">
|
||||||
<el-alert
|
<el-alert
|
||||||
:title="$t('JournalCitationAnalysis.feeWarningTitle')"
|
:title="$t('JournalCitationAnalysis.feeWarningTitle')"
|
||||||
@@ -442,6 +542,23 @@ import commonclass from './class.vue';
|
|||||||
import commonAgreement from './agreement.vue';
|
import commonAgreement from './agreement.vue';
|
||||||
import commonJournalInstallment from '../JournalInstallment/index.vue';
|
import commonJournalInstallment from '../JournalInstallment/index.vue';
|
||||||
import bus from '@/components/common/bus';
|
import bus from '@/components/common/bus';
|
||||||
|
|
||||||
|
function normalizeCiteMateLists(data) {
|
||||||
|
if (!data) {
|
||||||
|
return { topics: [], countries: [], citesNum: null };
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return { topics: data, countries: [], citesNum: null };
|
||||||
|
}
|
||||||
|
const topics = data.topic || data.topics || data.topic_list || data.topicList || [];
|
||||||
|
const countries = data.country || data.countries || data.country_list || data.countryList || data.nation || data.nation_list || [];
|
||||||
|
const citesNum = data.cites_num != null ? data.cites_num : data.cite_num != null ? data.cite_num : data.total != null ? data.total : null;
|
||||||
|
return {
|
||||||
|
topics: Array.isArray(topics) ? topics : [],
|
||||||
|
countries: Array.isArray(countries) ? countries : [],
|
||||||
|
citesNum: citesNum
|
||||||
|
};
|
||||||
|
}
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
props: ['urlList', 'source'],
|
props: ['urlList', 'source'],
|
||||||
@@ -500,6 +617,14 @@ export default {
|
|||||||
drawer: false,
|
drawer: false,
|
||||||
drawerJournalInstallment: false,
|
drawerJournalInstallment: false,
|
||||||
drawerAgreement: false,
|
drawerAgreement: false,
|
||||||
|
drawerCiteMate: false,
|
||||||
|
citeMateLoading: false,
|
||||||
|
citeMateYear: '2',
|
||||||
|
citeMateYearOptions: [1, 2, 3, 4, 5, 6],
|
||||||
|
citeMateTopicList: [],
|
||||||
|
citeMateCountryList: [],
|
||||||
|
citeMateCitesNum: null,
|
||||||
|
citeMateTableHeight: 420,
|
||||||
bd: true,
|
bd: true,
|
||||||
ruleForm: {
|
ruleForm: {
|
||||||
major_title: '',
|
major_title: '',
|
||||||
@@ -607,6 +732,28 @@ export default {
|
|||||||
this.ruleForm = {};
|
this.ruleForm = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
drawerCiteMate(val) {
|
||||||
|
if (val) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateCiteMateTableHeight();
|
||||||
|
window.addEventListener('resize', this.updateCiteMateTableHeight);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('resize', this.updateCiteMateTableHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
citeMateYearOptionList() {
|
||||||
|
const presets = this.citeMateYearOptions.slice();
|
||||||
|
const current = parseInt(this.citeMateYear, 10);
|
||||||
|
if (Number.isFinite(current) && current > 0 && presets.indexOf(current) === -1) {
|
||||||
|
presets.push(current);
|
||||||
|
}
|
||||||
|
return presets.sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -617,6 +764,9 @@ export default {
|
|||||||
async activated() {
|
async activated() {
|
||||||
await this.getDate();
|
await this.getDate();
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.updateCiteMateTableHeight);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 失去焦点事件
|
// 失去焦点事件
|
||||||
onEditorBlur(quill) {
|
onEditorBlur(quill) {
|
||||||
@@ -960,6 +1110,118 @@ export default {
|
|||||||
that.$refs.commonAgreementRef.init();
|
that.$refs.commonAgreementRef.init();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
openJournalCiteMate(data) {
|
||||||
|
this.currentJournal = data || {};
|
||||||
|
this.citeMateYear = '2';
|
||||||
|
this.drawerCiteMate = true;
|
||||||
|
this.loadJournalCiteMate();
|
||||||
|
},
|
||||||
|
handleCiteMateClose(done) {
|
||||||
|
this.drawerCiteMate = false;
|
||||||
|
if (typeof done === 'function') {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateCiteMateTableHeight() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const block = this.$refs.citeMateBlock;
|
||||||
|
if (!block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = Array.isArray(block) ? block[0] : block;
|
||||||
|
const title = el.querySelector('.journal-cite-mate-block-title');
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const paddingTop = parseFloat(style.paddingTop) || 0;
|
||||||
|
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
||||||
|
let titleHeight = 0;
|
||||||
|
if (title) {
|
||||||
|
const titleStyle = window.getComputedStyle(title);
|
||||||
|
titleHeight = title.offsetHeight + (parseFloat(titleStyle.marginBottom) || 0);
|
||||||
|
}
|
||||||
|
const nextHeight = el.clientHeight - paddingTop - paddingBottom - titleHeight;
|
||||||
|
if (nextHeight > 0) {
|
||||||
|
this.citeMateTableHeight = nextHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatCiteMateYearLabel(years) {
|
||||||
|
const num = parseInt(years, 10);
|
||||||
|
if (!Number.isFinite(num) || num < 1) {
|
||||||
|
return String(years);
|
||||||
|
}
|
||||||
|
if (num === 1) {
|
||||||
|
return this.$t('journalCiteMate.yearOptionOne', { n: num });
|
||||||
|
}
|
||||||
|
return this.$t('journalCiteMate.yearOptionOther', { n: num });
|
||||||
|
},
|
||||||
|
normalizeCiteMateYear(value) {
|
||||||
|
const parsed = parseInt(String(value || '').trim(), 10);
|
||||||
|
if (!Number.isFinite(parsed) || parsed < 1) {
|
||||||
|
this.$message.warning(this.$t('journalCiteMate.yearInvalid'));
|
||||||
|
return '2';
|
||||||
|
}
|
||||||
|
if (parsed > 100) {
|
||||||
|
this.$message.warning(this.$t('journalCiteMate.yearMax'));
|
||||||
|
return '100';
|
||||||
|
}
|
||||||
|
return String(parsed);
|
||||||
|
},
|
||||||
|
handleCiteMateYearChange(value) {
|
||||||
|
this.citeMateYear = this.normalizeCiteMateYear(value);
|
||||||
|
this.loadJournalCiteMate();
|
||||||
|
},
|
||||||
|
getCiteMateRowName(row) {
|
||||||
|
if (!row) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
return row.topic_name || row.country || row.country_name || row.nation_name || row.name || row.title || '—';
|
||||||
|
},
|
||||||
|
getCiteMateRowCount(row) {
|
||||||
|
const value = row.num != null ? row.num : row.count != null ? row.count : row.cite_num;
|
||||||
|
return value != null ? value : '—';
|
||||||
|
},
|
||||||
|
loadJournalCiteMate() {
|
||||||
|
const journalId = this.currentJournal && this.currentJournal.journal_id;
|
||||||
|
if (!journalId) {
|
||||||
|
this.citeMateTopicList = [];
|
||||||
|
this.citeMateCountryList = [];
|
||||||
|
this.citeMateCitesNum = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.citeMateLoading = true;
|
||||||
|
this.$api
|
||||||
|
.post('api/journal/citeMate', {
|
||||||
|
journal_id: String(journalId),
|
||||||
|
year: String(this.citeMateYear || '2')
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code == 0) {
|
||||||
|
const lists = normalizeCiteMateLists(res.data);
|
||||||
|
this.citeMateTopicList = lists.topics;
|
||||||
|
this.citeMateCountryList = lists.countries;
|
||||||
|
this.citeMateCitesNum = lists.citesNum;
|
||||||
|
} else {
|
||||||
|
this.citeMateTopicList = [];
|
||||||
|
this.citeMateCountryList = [];
|
||||||
|
this.citeMateCitesNum = null;
|
||||||
|
this.$message.error(res.msg || this.$t('journalCiteMate.loadFail'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.citeMateTopicList = [];
|
||||||
|
this.citeMateCountryList = [];
|
||||||
|
this.citeMateCitesNum = null;
|
||||||
|
this.$message.error(this.$t('journalCiteMate.loadFail'));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.citeMateLoading = false;
|
||||||
|
if (this.drawerCiteMate) {
|
||||||
|
this.updateCiteMateTableHeight();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
renameKeys(obj, oldKey, newKey) {
|
renameKeys(obj, oldKey, newKey) {
|
||||||
if (obj[oldKey]) {
|
if (obj[oldKey]) {
|
||||||
obj[newKey] = obj[oldKey];
|
obj[newKey] = obj[oldKey];
|
||||||
@@ -1457,6 +1719,100 @@ export default {
|
|||||||
padding: 22px 0 0 0;
|
padding: 22px 0 0 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-drawer .el-drawer__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-body.el-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-body > .el-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-year-select {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-block-title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-block-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-total {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-cite-mate-total strong {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
.WeChatCode .avatar-uploader .el-upload {
|
.WeChatCode .avatar-uploader .el-upload {
|
||||||
width: 120px !important;
|
width: 120px !important;
|
||||||
height: 120px !important;
|
height: 120px !important;
|
||||||
|
|||||||
@@ -1421,7 +1421,8 @@ export default {
|
|||||||
this.$router.resolve({
|
this.$router.resolve({
|
||||||
path: '/GenerateCharts',
|
path: '/GenerateCharts',
|
||||||
query: {
|
query: {
|
||||||
id: this.$route.query.id
|
id: this.$route.query.id,
|
||||||
|
p_article_id: this.p_article_id
|
||||||
}
|
}
|
||||||
}).href,
|
}).href,
|
||||||
'_blank'
|
'_blank'
|
||||||
|
|||||||
@@ -54,7 +54,21 @@
|
|||||||
<!-- 基本信息 -->
|
<!-- 基本信息 -->
|
||||||
<div :ref="tabsList[0].refName" class="scroll-item">
|
<div :ref="tabsList[0].refName" class="scroll-item">
|
||||||
<div class="bor_style_onli" style="margin: 20px 0">
|
<div class="bor_style_onli" style="margin: 20px 0">
|
||||||
<h4>{{ tabsList[0].name }}</h4>
|
<h4>
|
||||||
|
{{ tabsList[0].name }}
|
||||||
|
<el-button
|
||||||
|
v-if="zyModeEnabled"
|
||||||
|
style="float: right"
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
:loading="importingSubmission"
|
||||||
|
:disabled="!detailMes.article_id"
|
||||||
|
@click="importFromSubmission"
|
||||||
|
>
|
||||||
|
Import from Submission
|
||||||
|
</el-button>
|
||||||
|
</h4>
|
||||||
<el-form ref="Mes_Form" :model="detailMes" :rules="rules" label-width="220px">
|
<el-form ref="Mes_Form" :model="detailMes" :rules="rules" label-width="220px">
|
||||||
<el-form-item label="Title :" prop="title">
|
<el-form-item label="Title :" prop="title">
|
||||||
<el-input v-model="detailMes.title"></el-input>
|
<el-input v-model="detailMes.title"></el-input>
|
||||||
@@ -235,7 +249,14 @@
|
|||||||
Add Author</el-button
|
Add Author</el-button
|
||||||
>
|
>
|
||||||
</h4>
|
</h4>
|
||||||
<el-table :data="authorData" border class="table" ref="multipleTable" header-cell-class-name="table-header">
|
<el-table
|
||||||
|
:key="'author-' + authorTableKey"
|
||||||
|
:data="authorData"
|
||||||
|
border
|
||||||
|
class="table"
|
||||||
|
ref="authorTable"
|
||||||
|
header-cell-class-name="table-header"
|
||||||
|
>
|
||||||
<el-table-column prop="author_name" label="Author"></el-table-column>
|
<el-table-column prop="author_name" label="Author"></el-table-column>
|
||||||
<el-table-column label="First author" align="center" width="100px">
|
<el-table-column label="First author" align="center" width="100px">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
@@ -454,7 +475,14 @@
|
|||||||
>
|
>
|
||||||
</h4>
|
</h4>
|
||||||
<!-- 机构列表 -->
|
<!-- 机构列表 -->
|
||||||
<el-table :data="schoolData" border class="table" ref="multipleTable" header-cell-class-name="table-header">
|
<el-table
|
||||||
|
:key="'school-' + schoolTableKey"
|
||||||
|
:data="schoolData"
|
||||||
|
border
|
||||||
|
class="table"
|
||||||
|
ref="schoolTable"
|
||||||
|
header-cell-class-name="table-header"
|
||||||
|
>
|
||||||
<el-table-column type="index" label="No." width="55" align="center"></el-table-column>
|
<el-table-column type="index" label="No." width="55" align="center"></el-table-column>
|
||||||
<el-table-column prop="organ_name" label="Affiliation"></el-table-column>
|
<el-table-column prop="organ_name" label="Affiliation"></el-table-column>
|
||||||
<el-table-column label="" width="190" align="center">
|
<el-table-column label="" width="190" align="center">
|
||||||
@@ -952,6 +980,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入前作者预览 -->
|
||||||
|
<el-dialog
|
||||||
|
title="Review Authors Before Import"
|
||||||
|
:visible.sync="importAuthorPreviewVisible"
|
||||||
|
width="960px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<p style="color: #666; line-height: 22px; margin: 0 0 12px 0">
|
||||||
|
Check recognized authors, adjust order manually, or paste a name sequence to auto-sort. Required empty fields will be saved as a space.
|
||||||
|
</p>
|
||||||
|
<el-input
|
||||||
|
v-model="importAuthorOrderText"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
placeholder="eg: Sijing PENG1,¶, Ting ZHENG1,¶, Jin CONG1, Xiang WANG1, Chuanying ZHANG1, Jiejie GE1,2*"
|
||||||
|
></el-input>
|
||||||
|
<div style="margin: 10px 0 16px 0">
|
||||||
|
<el-button size="mini" type="primary" plain @click="applyImportAuthorOrder">Apply Order</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="importPreviewAuthors" border size="small" max-height="360">
|
||||||
|
<el-table-column type="index" label="No." width="50" align="center"></el-table-column>
|
||||||
|
<el-table-column label="First name" min-width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.firstname || scope.row.first_name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Last name" min-width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.lastname || scope.row.last_name }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="First author" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag round :type="Number(scope.row.is_super) === 1 ? 'success' : 'danger'" size="mini">
|
||||||
|
{{ Number(scope.row.is_super) === 1 ? '√' : '×' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Corresponding" width="110" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag round :type="Number(scope.row.is_report) === 1 ? 'success' : 'danger'" size="mini">
|
||||||
|
{{ Number(scope.row.is_report) === 1 ? '√' : '×' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="email" label="Email" min-width="150" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ Number(scope.row.is_report) === 1 ? scope.row.email || ' ' : '' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Affiliation" min-width="220" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getImportAuthorAffiliation(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Order" width="100" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" :disabled="scope.$index === 0" @click="moveImportAuthor(scope.$index, 'up')">Up</el-button>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
:disabled="scope.$index === importPreviewAuthors.length - 1"
|
||||||
|
@click="moveImportAuthor(scope.$index, 'down')"
|
||||||
|
>Down</el-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="importAuthorPreviewVisible = false">Cancel</el-button>
|
||||||
|
<el-button type="primary" :loading="importingSubmission" @click="confirmImportFromSubmission">Confirm Import</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 上线弹出框 --->
|
<!-- 上线弹出框 --->
|
||||||
<el-dialog title="Tips" :visible.sync="onlineVisible" width="680px" :close-on-click-modal="false">
|
<el-dialog title="Tips" :visible.sync="onlineVisible" width="680px" :close-on-click-modal="false">
|
||||||
<p style="line-height: 24px">Are you sure you want to push this manuscript to the official website? You can preview first.</p>
|
<p style="line-height: 24px">Are you sure you want to push this manuscript to the official website? You can preview first.</p>
|
||||||
@@ -1067,9 +1168,31 @@ import comArtHtmlCreatNewProduce from './comArtHtmlCreatNewProduce.vue';
|
|||||||
import commonTopic from '../page/components/article/topic.vue';
|
import commonTopic from '../page/components/article/topic.vue';
|
||||||
import commonRelated from '../page/components/article/Related.vue';
|
import commonRelated from '../page/components/article/Related.vue';
|
||||||
import Tinymce from '@/components/page/components/Tinymce';
|
import Tinymce from '@/components/page/components/Tinymce';
|
||||||
|
import {
|
||||||
|
mapEssentialFields,
|
||||||
|
buildOrganIdMap,
|
||||||
|
buildProductionAuthorPayload,
|
||||||
|
buildAuthorReferenceAbbr,
|
||||||
|
collectSubmissionOrgans,
|
||||||
|
resolveSubmissionAuthors,
|
||||||
|
normalizeSubmissionAuthors,
|
||||||
|
normalizeRichTextContent,
|
||||||
|
sortAuthorsByOrderString,
|
||||||
|
moveAuthorInList,
|
||||||
|
getAuthorDisplayName,
|
||||||
|
getAuthorAffiliationPreview
|
||||||
|
} from '@/utils/productionSubmissionImport';
|
||||||
|
import { isZyModeEnabled } from '@/utils/zyMode';
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
importingSubmission: false,
|
||||||
|
importAuthorPreviewVisible: false,
|
||||||
|
importAuthorOrderText: '',
|
||||||
|
importPreviewAuthors: [],
|
||||||
|
importPreviewContext: null,
|
||||||
|
authorTableKey: 0,
|
||||||
|
schoolTableKey: 0,
|
||||||
isGenerating: false, // 按钮加载状态
|
isGenerating: false, // 按钮加载状态
|
||||||
percentage: 0, // 进度条
|
percentage: 0, // 进度条
|
||||||
finalReview: null,
|
finalReview: null,
|
||||||
@@ -1609,6 +1732,19 @@ export default {
|
|||||||
updateChange(type, content) {
|
updateChange(type, content) {
|
||||||
this[type] = content;
|
this[type] = content;
|
||||||
},
|
},
|
||||||
|
applyRichTextToEditor(refName, field, content) {
|
||||||
|
const normalized = normalizeRichTextContent(content);
|
||||||
|
this[field] = normalized;
|
||||||
|
if (field === 'abstract') {
|
||||||
|
this.detailMes.abstract = normalized;
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const editor = this.$refs[refName];
|
||||||
|
if (editor && typeof editor.setContent === 'function') {
|
||||||
|
editor.setContent(normalized);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getTinymceContent(type) {
|
getTinymceContent(type) {
|
||||||
this.$refs.tinymceChild1.getContent(type);
|
this.$refs.tinymceChild1.getContent(type);
|
||||||
@@ -1905,34 +2041,278 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getAuthorJG() {
|
getAuthorJG() {
|
||||||
// 获取作者列表
|
return this.refreshAuthorOrganLists().catch((err) => {
|
||||||
this.$api
|
|
||||||
.post('api/Production/getAuthorlist', this.idform)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.code == 0) {
|
|
||||||
this.authorData = res.data.authors;
|
|
||||||
} else {
|
|
||||||
this.$message.error(res.msg);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.$message.error(err);
|
this.$message.error(err);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
refreshAuthorOrganLists() {
|
||||||
|
this.idform.p_article_id = this.p_article_id;
|
||||||
|
return Promise.all([
|
||||||
|
this.$api.post('api/Production/getAuthorlist', this.idform),
|
||||||
|
this.$api.post('api/Production/getOrganList', this.idform)
|
||||||
|
]).then(([authorRes, organRes]) => {
|
||||||
|
if (authorRes.code == 0) {
|
||||||
|
this.$set(this, 'authorData', [...(authorRes.data.authors || [])]);
|
||||||
|
} else {
|
||||||
|
throw authorRes.msg || 'Failed to load authors';
|
||||||
|
}
|
||||||
|
if (organRes.code == 0) {
|
||||||
|
this.$set(this, 'schoolData', [...(organRes.data.organs || [])]);
|
||||||
|
this.$set(this, 'mechanism', [...(organRes.data.organs || [])]);
|
||||||
|
} else {
|
||||||
|
throw organRes.msg || 'Failed to load affiliations';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchAuthorList() {
|
||||||
|
this.idform.p_article_id = this.p_article_id;
|
||||||
|
return this.$api.post('api/Production/getAuthorlist', this.idform).then((authorRes) => {
|
||||||
|
if (authorRes.code == 0) {
|
||||||
|
this.$set(this, 'authorData', [...(authorRes.data.authors || [])]);
|
||||||
|
return authorRes;
|
||||||
|
}
|
||||||
|
throw authorRes.msg || 'Failed to load authors';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchOrganList() {
|
||||||
|
this.idform.p_article_id = this.p_article_id;
|
||||||
|
return this.$api.post('api/Production/getOrganList', this.idform).then((organRes) => {
|
||||||
|
if (organRes.code == 0) {
|
||||||
|
this.$set(this, 'schoolData', [...(organRes.data.organs || [])]);
|
||||||
|
this.$set(this, 'mechanism', [...(organRes.data.organs || [])]);
|
||||||
|
return organRes;
|
||||||
|
}
|
||||||
|
throw organRes.msg || 'Failed to load affiliations';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async refreshAuthorSection(options = {}) {
|
||||||
|
const { authorsOnly = false, organsOnly = false } = options;
|
||||||
|
if (organsOnly) {
|
||||||
|
await this.fetchOrganList();
|
||||||
|
this.schoolTableKey += 1;
|
||||||
|
} else if (authorsOnly) {
|
||||||
|
await this.fetchAuthorList();
|
||||||
|
this.authorTableKey += 1;
|
||||||
|
} else {
|
||||||
|
await this.refreshAuthorOrganLists();
|
||||||
|
this.authorTableKey += 1;
|
||||||
|
this.schoolTableKey += 1;
|
||||||
|
}
|
||||||
|
await this.$nextTick();
|
||||||
|
this.$forceUpdate();
|
||||||
|
await this.$nextTick();
|
||||||
|
if (this.$refs.authorTable && this.$refs.authorTable.doLayout) {
|
||||||
|
this.$refs.authorTable.doLayout();
|
||||||
|
}
|
||||||
|
if (this.$refs.schoolTable && this.$refs.schoolTable.doLayout) {
|
||||||
|
this.$refs.schoolTable.doLayout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getImportAuthorName(author) {
|
||||||
|
return getAuthorDisplayName(author);
|
||||||
|
},
|
||||||
|
getImportAuthorAffiliation(author) {
|
||||||
|
return getAuthorAffiliationPreview(author);
|
||||||
|
},
|
||||||
|
applyImportAuthorOrder() {
|
||||||
|
if (!this.importAuthorOrderText.trim()) {
|
||||||
|
this.$message.warning('Please paste an author order string first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.importPreviewAuthors = sortAuthorsByOrderString(this.importPreviewAuthors, this.importAuthorOrderText);
|
||||||
|
this.$message.success('Author order updated.');
|
||||||
|
},
|
||||||
|
moveImportAuthor(index, direction) {
|
||||||
|
this.importPreviewAuthors = moveAuthorInList(this.importPreviewAuthors, index, direction);
|
||||||
|
},
|
||||||
|
async fetchSubmissionImportContext(articleId) {
|
||||||
|
const detailRes = await this.$api.post('api/Article/getArticleDetail', {
|
||||||
|
articleId,
|
||||||
|
human: 'editor'
|
||||||
|
});
|
||||||
|
const article = (detailRes && detailRes.article) || {};
|
||||||
|
const userId = article.user_id;
|
||||||
|
|
||||||
// 获取机构列表
|
const requestList = [this.$api.post('api/Article/getAuthors', { article_id: articleId }).catch(() => null)];
|
||||||
this.$api
|
if (userId) {
|
||||||
.post('api/Production/getOrganList', this.idform)
|
requestList.push(
|
||||||
.then((res) => {
|
this.$api.post('api/Organ/lists', { article_id: articleId, user_id: userId }).catch(() => null)
|
||||||
if (res.code == 0) {
|
);
|
||||||
this.schoolData = res.data.organs;
|
|
||||||
this.mechanism = res.data.organs;
|
|
||||||
} else {
|
|
||||||
this.$message.error(res.msg);
|
|
||||||
}
|
}
|
||||||
})
|
const [authorsRes, organsRes] = await Promise.all(requestList);
|
||||||
.catch((err) => {
|
|
||||||
this.$message.error(err);
|
const authors = normalizeSubmissionAuthors(resolveSubmissionAuthors(authorsRes, detailRes));
|
||||||
|
const submissionOrgans = collectSubmissionOrgans(
|
||||||
|
organsRes && Number(organsRes.status) === 1 && Array.isArray(organsRes.data) ? organsRes.data : [],
|
||||||
|
authors
|
||||||
|
);
|
||||||
|
const mapped = mapEssentialFields(article, authors);
|
||||||
|
|
||||||
|
return {
|
||||||
|
article,
|
||||||
|
authors,
|
||||||
|
submissionOrgans,
|
||||||
|
mapped
|
||||||
|
};
|
||||||
|
},
|
||||||
|
applyImportedEssentialFields(mapped) {
|
||||||
|
this.detailMes.title = mapped.title;
|
||||||
|
this.detailMes.type = mapped.type;
|
||||||
|
this.detailMes.keywords = mapped.keywords;
|
||||||
|
this.detailMes.acknowledgment = mapped.acknowledgment;
|
||||||
|
this.detailMes.author_contribution = mapped.author_contribution;
|
||||||
|
this.detailMes.abbreviation = mapped.abbreviation;
|
||||||
|
this.applyRichTextToEditor('tinymceChild1', 'abstract', mapped.abstract);
|
||||||
|
},
|
||||||
|
async importFromSubmission() {
|
||||||
|
const articleId = this.detailMes && this.detailMes.article_id;
|
||||||
|
if (!articleId) {
|
||||||
|
this.$message.error('No linked submission article found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = this.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: 'Loading submission data...',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
});
|
});
|
||||||
|
this.importingSubmission = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = await this.fetchSubmissionImportContext(articleId);
|
||||||
|
this.importPreviewContext = context;
|
||||||
|
this.importPreviewAuthors = [...context.authors];
|
||||||
|
this.importAuthorOrderText = '';
|
||||||
|
this.importAuthorPreviewVisible = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error(typeof err === 'string' ? err : 'Failed to load submission data.');
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
this.importingSubmission = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async confirmImportFromSubmission() {
|
||||||
|
if (!this.importPreviewContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = this.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: 'Importing from submission...',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
});
|
||||||
|
this.importingSubmission = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { submissionOrgans, mapped } = this.importPreviewContext;
|
||||||
|
const authors = [...this.importPreviewAuthors];
|
||||||
|
|
||||||
|
this.applyImportedEssentialFields(mapped);
|
||||||
|
|
||||||
|
let organIdMap = {};
|
||||||
|
let authorImported = false;
|
||||||
|
let affiliationImported = false;
|
||||||
|
let addedAuthorCount = 0;
|
||||||
|
const importedAuthors = [];
|
||||||
|
|
||||||
|
if (!this.schoolData.length && submissionOrgans.length) {
|
||||||
|
for (const organ of submissionOrgans) {
|
||||||
|
const organName = String(organ.organ_name || '').trim();
|
||||||
|
if (!organName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const addRes = await this.$api.post('api/Production/addAuthorOrgan', {
|
||||||
|
p_article_id: this.p_article_id,
|
||||||
|
organ_name: organName
|
||||||
|
});
|
||||||
|
if (addRes.code != 0) {
|
||||||
|
throw addRes.msg || 'Failed to add affiliation';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
affiliationImported = true;
|
||||||
|
await this.refreshAuthorSection({ organsOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
organIdMap = buildOrganIdMap(submissionOrgans, this.schoolData);
|
||||||
|
|
||||||
|
if (!this.authorData.length && authors.length) {
|
||||||
|
if (!this.schoolData.length) {
|
||||||
|
throw 'Please import affiliations before authors.';
|
||||||
|
}
|
||||||
|
for (const author of authors) {
|
||||||
|
const payload = buildProductionAuthorPayload(
|
||||||
|
author,
|
||||||
|
this.p_article_id,
|
||||||
|
organIdMap,
|
||||||
|
this.schoolData
|
||||||
|
);
|
||||||
|
if (!payload.first_name || !payload.last_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!payload.organs.length) {
|
||||||
|
throw `Author ${payload.first_name} ${payload.last_name} has no matched affiliation.`;
|
||||||
|
}
|
||||||
|
const addRes = await this.$api.post('api/Production/addAuthor', payload);
|
||||||
|
if (addRes.code != 0) {
|
||||||
|
throw addRes.msg || `Failed to add author ${payload.first_name} ${payload.last_name}`;
|
||||||
|
}
|
||||||
|
addedAuthorCount += 1;
|
||||||
|
importedAuthors.push({
|
||||||
|
...author,
|
||||||
|
firstname: payload.first_name,
|
||||||
|
first_name: payload.first_name,
|
||||||
|
lastname: payload.last_name,
|
||||||
|
last_name: payload.last_name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (addedAuthorCount > 0) {
|
||||||
|
authorImported = true;
|
||||||
|
this.detailMes.abbr = buildAuthorReferenceAbbr(importedAuthors);
|
||||||
|
await this.refreshAuthorSection({ authorsOnly: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (affiliationImported && !authorImported) {
|
||||||
|
await this.refreshAuthorSection({ organsOnly: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = ['Essential Information filled from submission.'];
|
||||||
|
if (affiliationImported) {
|
||||||
|
messages.push('Affiliations imported.');
|
||||||
|
} else if (submissionOrgans.length && this.schoolData.length) {
|
||||||
|
messages.push('Affiliations already exist, skipped.');
|
||||||
|
}
|
||||||
|
if (authorImported) {
|
||||||
|
messages.push(`${addedAuthorCount} author(s) imported.`);
|
||||||
|
} else if (authors.length && this.authorData.length) {
|
||||||
|
messages.push('Authors already exist, skipped.');
|
||||||
|
} else if (authors.length && !addedAuthorCount) {
|
||||||
|
messages.push('No authors were imported. Please check author data.');
|
||||||
|
}
|
||||||
|
messages.push('Please review the fields and click Save Essential Information.');
|
||||||
|
this.$message.success(messages.join(' '));
|
||||||
|
|
||||||
|
this.importAuthorPreviewVisible = false;
|
||||||
|
this.importPreviewContext = null;
|
||||||
|
|
||||||
|
if (authorImported || affiliationImported) {
|
||||||
|
await this.$nextTick();
|
||||||
|
this.jumpTab(1, this.tabsList[1]);
|
||||||
|
} else {
|
||||||
|
await this.$nextTick();
|
||||||
|
this.$forceUpdate();
|
||||||
|
this.jumpTab(0, this.tabsList[0]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error(typeof err === 'string' ? err : 'Import failed. Please try again.');
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
this.importingSubmission = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 1----时间初始化
|
// 1----时间初始化
|
||||||
@@ -2125,7 +2505,7 @@ export default {
|
|||||||
this.addAuthor = false;
|
this.addAuthor = false;
|
||||||
this.$refs.add_Author.resetFields();
|
this.$refs.add_Author.resetFields();
|
||||||
this.addFomauthor.organs = [];
|
this.addFomauthor.organs = [];
|
||||||
this.getAuthorJG();
|
this.refreshAuthorSection({ authorsOnly: true });
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg);
|
this.$message.error(res.msg);
|
||||||
}
|
}
|
||||||
@@ -2213,7 +2593,7 @@ export default {
|
|||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
this.addSchool = false;
|
this.addSchool = false;
|
||||||
this.$refs.add_School.resetFields();
|
this.$refs.add_School.resetFields();
|
||||||
this.getAuthorJG();
|
this.refreshAuthorSection({ organsOnly: true });
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg);
|
this.$message.error(res.msg);
|
||||||
}
|
}
|
||||||
@@ -3210,6 +3590,11 @@ export default {
|
|||||||
// this.jumpTab(0,this.tabsList[0])
|
// this.jumpTab(0,this.tabsList[0])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
zyModeEnabled() {
|
||||||
|
return isZyModeEnabled(this.$route);
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
quillEditor,
|
quillEditor,
|
||||||
Tinymce,
|
Tinymce,
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ export default {
|
|||||||
goEdit() {
|
goEdit() {
|
||||||
window.open(this.$router.resolve({ path: '/GenerateCharts',
|
window.open(this.$router.resolve({ path: '/GenerateCharts',
|
||||||
query: {
|
query: {
|
||||||
id: this.detailMes.article_id
|
id: this.detailMes.article_id,
|
||||||
|
p_article_id: this.Art_Id
|
||||||
} }).href, '_blank');
|
} }).href, '_blank');
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
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: '' },
|
value: { type: String, default: '' },
|
||||||
id: { type: String, default: () => 'tiny-' + +new Date() },
|
id: { type: String, default: () => 'tiny-' + +new Date() },
|
||||||
showSelectTemplateButton: { type: Boolean, default: false },
|
showSelectTemplateButton: { type: Boolean, default: false },
|
||||||
|
showEmptyButton: { type: Boolean, default: true },
|
||||||
// 仅在需要的页面开启“安全初始化内容”
|
// 仅在需要的页面开启“安全初始化内容”
|
||||||
useSafeInitContent: { type: Boolean, default: false }
|
useSafeInitContent: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
@@ -121,6 +122,8 @@ autoInlineStyles(htmlContent) {
|
|||||||
initTiny() {
|
initTiny() {
|
||||||
const vueInstance = this;
|
const vueInstance = this;
|
||||||
const hasTemplateBtn = this.showSelectTemplateButton;
|
const hasTemplateBtn = this.showSelectTemplateButton;
|
||||||
|
const hasEmptyBtn = this.showEmptyButton;
|
||||||
|
const toolbarSuffix = hasEmptyBtn ? ' | clearButton | fullscreen' : ' | fullscreen';
|
||||||
window.tinymce.init({
|
window.tinymce.init({
|
||||||
selector: `#${this.tinymceId}`,
|
selector: `#${this.tinymceId}`,
|
||||||
language: 'zh_CN', // 记得下载语言包,没有就删掉这行
|
language: 'zh_CN', // 记得下载语言包,没有就删掉这行
|
||||||
@@ -137,8 +140,11 @@ autoInlineStyles(htmlContent) {
|
|||||||
|
|
||||||
// 按照图片布局精准排序:
|
// 按照图片布局精准排序:
|
||||||
toolbar: hasTemplateBtn
|
toolbar: hasTemplateBtn
|
||||||
? 'undo redo | selectTemplate | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link | fullscreen'
|
? 'undo redo | selectTemplate | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link' +
|
||||||
: 'undo redo | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link | fullscreen',
|
toolbarSuffix
|
||||||
|
: 'undo redo | code preview | fontselect fontsizeselect | formatselect | bold italic underline strikethrough | forecolor backcolor | bullist numlist | lineheight outdent indent | alignleft aligncenter alignright alignjustify | table link' +
|
||||||
|
toolbarSuffix,
|
||||||
|
menubar: 'file edit view insert format tools table',
|
||||||
|
|
||||||
// 补充:让字体和字号选择器更有序
|
// 补充:让字体和字号选择器更有序
|
||||||
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px',
|
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px',
|
||||||
@@ -158,6 +164,16 @@ autoInlineStyles(htmlContent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasEmptyBtn) {
|
||||||
|
editor.ui.registry.addButton('clearButton', {
|
||||||
|
text: 'Empty',
|
||||||
|
onAction() {
|
||||||
|
editor.setContent('');
|
||||||
|
vueInstance.$emit('input', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 统一给自定义按钮加样式(无按钮时不会产生影响)
|
// 统一给自定义按钮加样式(无按钮时不会产生影响)
|
||||||
CommonJS.inTinymceButtonClass();
|
CommonJS.inTinymceButtonClass();
|
||||||
},
|
},
|
||||||
@@ -177,7 +193,6 @@ autoInlineStyles(htmlContent) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 假设 rawHtml 是你那一大串完整的 HTML
|
// 假设 rawHtml 是你那一大串完整的 HTML
|
||||||
prepareContentForEditor(rawHtml) {
|
prepareContentForEditor(rawHtml) {
|
||||||
// 1. 分离出 Head 及其之前的部分 (包含 doctype, html, head)
|
// 1. 分离出 Head 及其之前的部分 (包含 doctype, html, head)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<span v-html="renderText(processedItem.title)"></span>
|
<span v-html="renderText(processedItem.title)"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table border="1" class="custom-table-core">
|
<table class="custom-table-core">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row, rIdx) in processedItem.table.tableHeader" :key="'h-' + rIdx" class="table-header-row">
|
<tr v-for="(row, rIdx) in processedItem.table.tableHeader" :key="'h-' + rIdx" class="table-header-row">
|
||||||
<td v-for="(cell, cIdx) in row" :key="'hc-'+cIdx" :rowspan="cell.rowspan" :colspan="cell.colspan">
|
<td v-for="(cell, cIdx) in row" :key="'hc-'+cIdx" :rowspan="cell.rowspan" :colspan="cell.colspan">
|
||||||
@@ -201,7 +201,7 @@ export default {
|
|||||||
min-width: 794px; /* A4 宽度参考 */
|
min-width: 794px; /* A4 宽度参考 */
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 10px 20px;
|
padding: 2.54cm 1.91cm;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
|
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -243,8 +243,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-table-core {
|
.custom-table-core {
|
||||||
width: 100%;
|
width: 98.3%;
|
||||||
|
margin: 0 auto;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
table-layout: auto;
|
||||||
|
font-family: 'Charis SIL', serif;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-paper-view ::v-deep .custom-table-core tr td {
|
||||||
|
padding: 0 0.19cm;
|
||||||
|
line-height: 10pt;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font {
|
.font {
|
||||||
@@ -258,22 +270,56 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: rgb(210, 90, 90);
|
color: rgb(210, 90, 90);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
|
font-family: 'Charis SIL', serif;
|
||||||
|
line-height: 13px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageTitle {
|
||||||
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableNote, .imageNote {
|
.tableNote, .imageNote {
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
font-family: 'Charis SIL', serif;
|
||||||
|
line-height: 10pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageNote {
|
.imageNote {
|
||||||
text-align: center;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oddColor td {
|
.oddColor td {
|
||||||
background: rgb(250, 231, 232) !important;
|
background: rgb(250, 231, 232) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-paper-view ::v-deep .custom-table-core tr.table-header-row td span + span::before {
|
||||||
|
content: ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-paper-view ::v-deep .custom-table-core tr.table-header-row td {
|
||||||
|
font-size: 7.5pt;
|
||||||
|
font-family: 'Charis SIL', serif;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-top: 0.5pt solid #000;
|
||||||
|
border-bottom: 0.5pt solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-paper-view ::v-deep .custom-table-core tr.table-content-row td {
|
||||||
|
font-size: 7.5pt;
|
||||||
|
font-family: 'Charis SIL', serif;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-paper-view ::v-deep .custom-table-core tr.table-content-row:last-child td {
|
||||||
|
border-bottom: 0.5pt solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
.table-fade-enter-active, .table-fade-leave-active { transition: opacity 0.3s ease; }
|
.table-fade-enter-active, .table-fade-leave-active { transition: opacity 0.3s ease; }
|
||||||
.table-fade-enter, .table-fade-leave-to { opacity: 0; }
|
.table-fade-enter, .table-fade-leave-to { opacity: 0; }
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,85 @@
|
|||||||
<i class="el-icon-document base-margin" :style="{ '--m-r': '2px' }"> </i>
|
<i class="el-icon-document base-margin" :style="{ '--m-r': '2px' }"> </i>
|
||||||
{{ $t('commonTable.BatchAddcontent') }}
|
{{ $t('commonTable.BatchAddcontent') }}
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="zyModeEnabled"
|
||||||
|
@click="handleExportManuscriptWord"
|
||||||
|
class="base-font-size base-bg-imp base-padding-all"
|
||||||
|
:style="{ '--f-s': '12px', '--f-c': '#333' }"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="el-icon-download base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-if="!exportingManuscriptWord"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
class="el-icon-loading base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-else
|
||||||
|
></i>
|
||||||
|
{{ $t('commonTable.exportWord') }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="zyModeEnabled"
|
||||||
|
@click="handleDownloadReferencesHtml"
|
||||||
|
class="base-font-size base-bg-imp base-padding-all"
|
||||||
|
:style="{ '--f-s': '12px', '--f-c': '#333' }"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="el-icon-document base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-if="!referencesHtmlLoading"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
class="el-icon-loading base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-else
|
||||||
|
></i>
|
||||||
|
{{ $t('commonTable.refHtmlDownload') }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="zyModeEnabled"
|
||||||
|
@click="triggerReferencesHtmlToWord"
|
||||||
|
class="base-font-size base-bg-imp base-padding-all"
|
||||||
|
:style="{ '--f-s': '12px', '--f-c': '#333' }"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="el-icon-upload2 base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-if="!exportingReferencesHtmlWord"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
class="el-icon-loading base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-else
|
||||||
|
></i>
|
||||||
|
{{ $t('commonTable.refHtmlToWord') }}
|
||||||
|
</li>
|
||||||
|
<input
|
||||||
|
ref="referencesHtmlFileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".html,text/html"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleReferencesHtmlToWord"
|
||||||
|
/>
|
||||||
|
<li
|
||||||
|
v-if="zyModeEnabled"
|
||||||
|
@click="handleOpenCitationRelevance"
|
||||||
|
class="base-font-size base-bg-imp base-padding-all"
|
||||||
|
:style="{ '--f-s': '12px', '--f-c': '#333' }"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="el-icon-connection base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-if="!citationRelevanceLoading"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
class="el-icon-loading base-margin"
|
||||||
|
:style="{ '--m-r': '2px' }"
|
||||||
|
v-else
|
||||||
|
></i>
|
||||||
|
{{ $t('commonTable.citeRelevanceDetect') }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
@@ -1064,6 +1143,18 @@ import { TableUtils } from '@/common/js/TableUtils';
|
|||||||
import { debounce, throttle } from '@/common/js/debounce';
|
import { debounce, throttle } from '@/common/js/debounce';
|
||||||
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
|
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
|
||||||
import LatexDataPanel from './LatexDataPanel.vue';
|
import LatexDataPanel from './LatexDataPanel.vue';
|
||||||
|
import { downloadManuscriptWord, fetchManuscriptReferenceList } from '@/utils/exportManuscriptWord';
|
||||||
|
import {
|
||||||
|
buildReferencesHtmlLabels,
|
||||||
|
downloadReferencesEditableHtml,
|
||||||
|
parseReferencesEditableHtml
|
||||||
|
} from '@/utils/manuscriptReferenceHtml';
|
||||||
|
import {
|
||||||
|
buildCitationReviewQueue,
|
||||||
|
buildCitationReviewHtmlLabels,
|
||||||
|
downloadCitationReviewHtml
|
||||||
|
} from '@/utils/manuscriptCitationRelevance';
|
||||||
|
import { isZyModeEnabled } from '@/utils/zyMode';
|
||||||
export default {
|
export default {
|
||||||
name: 'tinymce',
|
name: 'tinymce',
|
||||||
components: {
|
components: {
|
||||||
@@ -1076,6 +1167,9 @@ export default {
|
|||||||
articleId: {
|
articleId: {
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
pArticleId: {
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
isPreview: {
|
isPreview: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default() {
|
default() {
|
||||||
@@ -1185,6 +1279,11 @@ export default {
|
|||||||
imagePath: require('@/assets/img/carriageReturn.png'), // 或者你可以设置其他路径
|
imagePath: require('@/assets/img/carriageReturn.png'), // 或者你可以设置其他路径
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
wordList: [],
|
wordList: [],
|
||||||
|
exportingManuscriptWord: false,
|
||||||
|
referencesHtmlLoading: false,
|
||||||
|
exportingReferencesHtmlWord: false,
|
||||||
|
citationRelevanceLoading: false,
|
||||||
|
manuscriptReferences: [],
|
||||||
proofreadingList: [],
|
proofreadingList: [],
|
||||||
commentList: [],
|
commentList: [],
|
||||||
visibleDivs: [], // 存储当前可见的 div id
|
visibleDivs: [], // 存储当前可见的 div id
|
||||||
@@ -1309,6 +1408,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
zyModeEnabled() {
|
||||||
|
return isZyModeEnabled(this.$route);
|
||||||
|
},
|
||||||
sortedProofreadingList() {
|
sortedProofreadingList() {
|
||||||
const order = [2, 1, 3];
|
const order = [2, 1, 3];
|
||||||
const rank = { 2: 0, 1: 1, 3: 2 };
|
const rank = { 2: 0, 1: 1, 3: 2 };
|
||||||
@@ -1444,6 +1546,170 @@ export default {
|
|||||||
this.editors = {};
|
this.editors = {};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async handleExportManuscriptWord() {
|
||||||
|
if (this.exportingManuscriptWord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.wordList || !this.wordList.length) {
|
||||||
|
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty') || 'No content to export.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.exportingManuscriptWord = true;
|
||||||
|
try {
|
||||||
|
await downloadManuscriptWord(this.wordList, this.mediaUrl, 'manuscript', {
|
||||||
|
fetchReferences: true,
|
||||||
|
apiClient: this.$api,
|
||||||
|
articleId: this.articleId,
|
||||||
|
pArticleId: this.pArticleId
|
||||||
|
});
|
||||||
|
this.$message.success(this.$t('commonTable.exportManuscriptSuccess') || 'Word downloaded.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && err.message === 'NO_CONTENT') {
|
||||||
|
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty') || 'No content to export.');
|
||||||
|
} else {
|
||||||
|
this.$message.error(this.$t('commonTable.exportManuscriptFail') || 'Failed to export Word.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.exportingManuscriptWord = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleDownloadReferencesHtml() {
|
||||||
|
if (this.referencesHtmlLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.$api) {
|
||||||
|
this.$message.error(this.$t('commonTable.refHtmlLoadFail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.referencesHtmlLoading = true;
|
||||||
|
try {
|
||||||
|
let references = this.manuscriptReferences;
|
||||||
|
if (!references || !references.length) {
|
||||||
|
references = await fetchManuscriptReferenceList(this.$api, this.articleId, this.pArticleId);
|
||||||
|
this.manuscriptReferences = references || [];
|
||||||
|
}
|
||||||
|
if (!references || !references.length) {
|
||||||
|
this.$message.warning(this.$t('commonTable.refHtmlEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = buildReferencesHtmlLabels(this.$t.bind(this));
|
||||||
|
const fileName = 'references-editor' + (this.articleId ? '-' + this.articleId : '') + '.html';
|
||||||
|
downloadReferencesEditableHtml(references, labels, fileName);
|
||||||
|
this.$message.success(this.$t('commonTable.refHtmlDownloadSuccess'));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error(this.$t('commonTable.refHtmlLoadFail'));
|
||||||
|
} finally {
|
||||||
|
this.referencesHtmlLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerReferencesHtmlToWord() {
|
||||||
|
if (this.exportingReferencesHtmlWord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const input = this.$refs.referencesHtmlFileInput;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleReferencesHtmlToWord(event) {
|
||||||
|
const input = event && event.target;
|
||||||
|
const file = input && input.files && input.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.wordList || !this.wordList.length) {
|
||||||
|
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty'));
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exportingReferencesHtmlWord = true;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async () => {
|
||||||
|
try {
|
||||||
|
const referenceHtmlItems = parseReferencesEditableHtml(String(reader.result || ''));
|
||||||
|
if (!referenceHtmlItems.length) {
|
||||||
|
this.$message.warning(this.$t('commonTable.refHtmlParseEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadManuscriptWord(this.wordList, this.mediaUrl, 'manuscript', {
|
||||||
|
fetchReferences: false,
|
||||||
|
referenceHtmlItems: referenceHtmlItems,
|
||||||
|
apiClient: this.$api,
|
||||||
|
articleId: this.articleId,
|
||||||
|
pArticleId: this.pArticleId
|
||||||
|
});
|
||||||
|
this.$message.success(this.$t('commonTable.refHtmlToWordSuccess'));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && err.message === 'NO_CONTENT') {
|
||||||
|
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty'));
|
||||||
|
} else {
|
||||||
|
this.$message.error(this.$t('commonTable.refHtmlToWordFail'));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.exportingReferencesHtmlWord = false;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
this.exportingReferencesHtmlWord = false;
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
this.$message.error(this.$t('commonTable.refHtmlToWordFail'));
|
||||||
|
};
|
||||||
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
},
|
||||||
|
async handleOpenCitationRelevance() {
|
||||||
|
if (this.citationRelevanceLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.wordList || !this.wordList.length) {
|
||||||
|
this.$message.warning(this.$t('commonTable.citeRelevanceNoContent'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.$api) {
|
||||||
|
this.$message.error(this.$t('commonTable.citeRelevanceLoadFail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.citationRelevanceLoading = true;
|
||||||
|
try {
|
||||||
|
let references = this.manuscriptReferences;
|
||||||
|
if (!references || !references.length) {
|
||||||
|
references = await fetchManuscriptReferenceList(this.$api, this.articleId, this.pArticleId);
|
||||||
|
this.manuscriptReferences = references || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = buildCitationReviewQueue(this.wordList, this.manuscriptReferences);
|
||||||
|
if (!items.length) {
|
||||||
|
this.$message.warning(this.$t('commonTable.citeRelevanceNoCitations'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = buildCitationReviewHtmlLabels(this.$t.bind(this));
|
||||||
|
const fileName = 'citation-relevance' + (this.articleId ? '-' + this.articleId : '') + '.html';
|
||||||
|
downloadCitationReviewHtml(items, labels, fileName);
|
||||||
|
this.$message.success(this.$t('commonTable.citeRelevanceDownloadHtmlSuccess'));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error(this.$t('commonTable.citeRelevanceLoadFail'));
|
||||||
|
} finally {
|
||||||
|
this.citationRelevanceLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
getInvolvedPMain(range) {
|
getInvolvedPMain(range) {
|
||||||
// 1. 找到起始节点所属的 .pMain
|
// 1. 找到起始节点所属的 .pMain
|
||||||
let startNode = range.startContainer;
|
let startNode = range.startContainer;
|
||||||
|
|||||||
@@ -65,6 +65,24 @@
|
|||||||
style="width: 200px; float: right; padding: 10px; height: calc(100% - 28px); box-sizing: border-box; overflow-y: auto"
|
style="width: 200px; float: right; padding: 10px; height: calc(100% - 28px); box-sizing: border-box; overflow-y: auto"
|
||||||
class="arrlist"
|
class="arrlist"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="zyModeEnabled && currentMenu == 1 && exportableImages.length > 0"
|
||||||
|
class="tables-panel-download-icon"
|
||||||
|
@click.stop="downloadAllFiguresWord"
|
||||||
|
>
|
||||||
|
<el-tooltip effect="dark" content="Download All Figures Word" placement="left">
|
||||||
|
<i :class="exportingAllFiguresWord ? 'el-icon-loading' : 'el-icon-download'"></i>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="zyModeEnabled && currentMenu == 2 && tables.length > 0"
|
||||||
|
class="tables-panel-download-icon"
|
||||||
|
@click.stop="downloadAllTablesWord"
|
||||||
|
>
|
||||||
|
<el-tooltip effect="dark" content="Download All Tables Word" placement="left">
|
||||||
|
<i :class="exportingAllWord ? 'el-icon-loading' : 'el-icon-download'"></i>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
<ul style="width: 100%; height: auto">
|
<ul style="width: 100%; height: auto">
|
||||||
<li v-show="currentMenu == 1">
|
<li v-show="currentMenu == 1">
|
||||||
<div style="" class="go-content-charts-item-box" :class="isCollapse ? 'double' : 'single'">
|
<div style="" class="go-content-charts-item-box" :class="isCollapse ? 'double' : 'single'">
|
||||||
@@ -137,7 +155,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<p>
|
<p>
|
||||||
<span style="" @click="edit(img, 'img')">
|
<span v-if="zyModeEnabled && isFigureExportable(img)" @click.stop="downloadSingleFigureWord(img)">
|
||||||
|
<el-tooltip effect="dark" content="Download Word" placement="top">
|
||||||
|
<i
|
||||||
|
:class="exportingFigureId === (img.article_image_id || img.ami_id) ? 'el-icon-loading' : 'el-icon-download'"
|
||||||
|
style="color: #909399; font-size: 12px"
|
||||||
|
></i>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 6px" @click="edit(img, 'img')">
|
||||||
<i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i>
|
<i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i>
|
||||||
</span>
|
</span>
|
||||||
<span style="margin-left: 6px" @click="handledelete(img, 'img')">
|
<span style="margin-left: 6px" @click="handledelete(img, 'img')">
|
||||||
@@ -329,7 +355,15 @@
|
|||||||
</el-tooltip> </span
|
</el-tooltip> </span
|
||||||
></span>
|
></span>
|
||||||
<p>
|
<p>
|
||||||
<span style="" @click="edit(table, 'table')">
|
<span v-if="zyModeEnabled && !isMathFormulaTable(table)" @click.stop="downloadSingleTableWord(table)">
|
||||||
|
<el-tooltip effect="dark" content="Download Word" placement="top">
|
||||||
|
<i
|
||||||
|
:class="exportingTableId === table.article_table_id ? 'el-icon-loading' : 'el-icon-download'"
|
||||||
|
style="color: #909399; font-size: 12px"
|
||||||
|
></i>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
<span style="margin-left: 6px" @click="edit(table, 'table')">
|
||||||
<i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i>
|
<i class="el-icon-edit" style="color: #409eff; font-size: 12px"></i>
|
||||||
</span>
|
</span>
|
||||||
<span style="margin-left: 6px" @click="handledelete(table, 'table')">
|
<span style="margin-left: 6px" @click="handledelete(table, 'table')">
|
||||||
@@ -475,6 +509,13 @@
|
|||||||
import DynamicTable from './DynamicTable.vue';
|
import DynamicTable from './DynamicTable.vue';
|
||||||
import { mediaUrl } from '@/common/js/commonJS.js';
|
import { mediaUrl } from '@/common/js/commonJS.js';
|
||||||
import { isMathFormulaTableRecord, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
|
import { isMathFormulaTableRecord, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
|
||||||
|
import { downloadAllTablesWord as exportAllTablesWordFile, downloadTableWord, prepareTableExportItem } from '@/utils/exportTableWord';
|
||||||
|
import {
|
||||||
|
downloadAllFiguresWord as exportAllFiguresWordFile,
|
||||||
|
downloadFigureWord,
|
||||||
|
isExportableFigureItem as canExportFigureItem
|
||||||
|
} from '@/utils/exportFigureWord';
|
||||||
|
import { isZyModeEnabled } from '@/utils/zyMode';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
||||||
@@ -505,12 +546,24 @@ export default {
|
|||||||
mathFormulasList: [],
|
mathFormulasList: [],
|
||||||
tablesHtml: [],
|
tablesHtml: [],
|
||||||
imagesHtml: [],
|
imagesHtml: [],
|
||||||
activeNames: ['images', 'tables']
|
activeNames: ['images', 'tables'],
|
||||||
|
exportingAllWord: false,
|
||||||
|
exportingTableId: null,
|
||||||
|
exportingAllFiguresWord: false,
|
||||||
|
exportingFigureId: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
DynamicTable
|
DynamicTable
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
zyModeEnabled() {
|
||||||
|
return isZyModeEnabled(this.$route);
|
||||||
|
},
|
||||||
|
exportableImages() {
|
||||||
|
return (this.images || []).filter((img) => canExportFigureItem(img));
|
||||||
|
}
|
||||||
|
},
|
||||||
directives: {
|
directives: {
|
||||||
// 注册一个局部的自定义指令 v-focus
|
// 注册一个局部的自定义指令 v-focus
|
||||||
focus: {
|
focus: {
|
||||||
@@ -526,6 +579,9 @@ export default {
|
|||||||
changeIsCollapse(e) {
|
changeIsCollapse(e) {
|
||||||
localStorage.setItem('isCollapse', e);
|
localStorage.setItem('isCollapse', e);
|
||||||
},
|
},
|
||||||
|
isFigureExportable(item) {
|
||||||
|
return canExportFigureItem(item);
|
||||||
|
},
|
||||||
isShowEditComment() {
|
isShowEditComment() {
|
||||||
if (localStorage.getItem('U_role')) {
|
if (localStorage.getItem('U_role')) {
|
||||||
var identity = localStorage.getItem('U_role');
|
var identity = localStorage.getItem('U_role');
|
||||||
@@ -930,7 +986,85 @@ if(newData.ami_id) {
|
|||||||
},
|
},
|
||||||
openPreview(index, type,data) {
|
openPreview(index, type,data) {
|
||||||
this.$refs.myTableModal.open(type,data);
|
this.$refs.myTableModal.open(type,data);
|
||||||
|
},
|
||||||
|
async downloadAllTablesWord() {
|
||||||
|
if (!this.tables.length) {
|
||||||
|
this.$message.warning('No tables to export.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.exportingAllWord = true;
|
||||||
|
try {
|
||||||
|
await exportAllTablesWordFile(this.tables, 'tables');
|
||||||
|
this.$message.success('Word downloaded.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && err.message === 'NO_TABLES') {
|
||||||
|
this.$message.warning('No exportable tables found.');
|
||||||
|
} else {
|
||||||
|
this.$message.error('Failed to export Word.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.exportingAllWord = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async downloadSingleTableWord(table) {
|
||||||
|
if (!table || this.isMathFormulaTable(table)) {
|
||||||
|
this.$message.warning('This table cannot be exported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tableId = table.article_table_id || table.amt_id;
|
||||||
|
this.exportingTableId = tableId;
|
||||||
|
try {
|
||||||
|
const prepared = prepareTableExportItem(table);
|
||||||
|
if (!prepared) {
|
||||||
|
this.$message.warning('Failed to prepare table.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await downloadTableWord(prepared, table.title);
|
||||||
|
this.$message.success('Word downloaded.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error('Failed to export Word.');
|
||||||
|
} finally {
|
||||||
|
this.exportingTableId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async downloadAllFiguresWord() {
|
||||||
|
if (!this.exportableImages.length) {
|
||||||
|
this.$message.warning('No figures to export.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.exportingAllFiguresWord = true;
|
||||||
|
try {
|
||||||
|
await exportAllFiguresWordFile(this.images, this.mediaUrl, 'figures');
|
||||||
|
this.$message.success('Word downloaded.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err && err.message === 'NO_FIGURES') {
|
||||||
|
this.$message.warning('No exportable figures found.');
|
||||||
|
} else {
|
||||||
|
this.$message.error('Failed to export Word.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.exportingAllFiguresWord = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async downloadSingleFigureWord(img) {
|
||||||
|
if (!img || !canExportFigureItem(img)) {
|
||||||
|
this.$message.warning('This figure cannot be exported.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const figureId = img.article_image_id || img.ami_id;
|
||||||
|
this.exportingFigureId = figureId;
|
||||||
|
try {
|
||||||
|
await downloadFigureWord(img, this.mediaUrl, img.title);
|
||||||
|
this.$message.success('Word downloaded.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.$message.error('Failed to export Word.');
|
||||||
|
} finally {
|
||||||
|
this.exportingFigureId = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async getWordimgList() {
|
async getWordimgList() {
|
||||||
var that = this;
|
var that = this;
|
||||||
@@ -1049,6 +1183,25 @@ if(newData.ami_id) {
|
|||||||
.el-menu-vertical-demo {
|
.el-menu-vertical-demo {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arrlist {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-panel-download-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables-panel-download-icon:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
.el-menu-vertical-demo li {
|
.el-menu-vertical-demo li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -297,19 +297,21 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ scope.row.author }} <span v-html="formatTitle(scope.row.title)"></span>. <em>{{ scope.row.joura }}</em
|
{{ 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
|
>. <span :class="getJournalDateno(scope.row.dateno, 'title')">{{ scope.row.dateno }}</span
|
||||||
>.<br />
|
>.<reference-search-links :row="scope.row"></reference-search-links><br />
|
||||||
</p>
|
</p>
|
||||||
<a class="doiLink" :href="scope.row.doilink" target="_blank">{{ scope.row.doilink }}</a>
|
<a class="doiLink" :href="scope.row.doilink" target="_blank">{{ scope.row.doilink }}</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- book 形式 -->
|
<!-- book 形式 -->
|
||||||
<div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item">
|
<div style="text-align: left" v-if="scope.row.refer_type == 'book'" class="reference-item">
|
||||||
<p>{{ scope.row.author }} <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>
|
<a class="doiLink" :href="scope.row.isbn" target="_blank">{{ scope.row.isbn }}</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- other 形式 -->
|
<!-- other 形式 -->
|
||||||
<p class="wrongLine reference-item" style="text-align: left" v-if="scope.row.refer_type == 'other'">
|
<p class="wrongLine reference-item" style="text-align: left" v-if="scope.row.refer_type == 'other'">
|
||||||
<span v-html="formatTitle(scope.row.refer_frag)"></span>
|
<span v-html="formatTitle(scope.row.refer_frag)"></span>
|
||||||
|
<reference-search-links :row="scope.row"></reference-search-links>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -895,6 +897,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import VueUeditorWrap from 'vue-ueditor-wrap'; // ES6 Module
|
import VueUeditorWrap from 'vue-ueditor-wrap'; // ES6 Module
|
||||||
|
import ReferenceSearchLinks from '@/components/page/components/ReferenceSearchLinks.vue';
|
||||||
|
|
||||||
/** 单条引用 records[].status:0 待检测 2 已完成 3 检测失败 */
|
/** 单条引用 records[].status:0 待检测 2 已完成 3 检测失败 */
|
||||||
const REF_RELEVANCE_RECORD_STATUS_PENDING = 0;
|
const REF_RELEVANCE_RECORD_STATUS_PENDING = 0;
|
||||||
@@ -1450,8 +1453,7 @@ export default {
|
|||||||
return title.replace(reg, (match) => {
|
return title.replace(reg, (match) => {
|
||||||
return `<span style="color: red; font-weight: bold;">${match}</span>`;
|
return `<span style="color: red; font-weight: bold;">${match}</span>`;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
,
|
|
||||||
getJournalDateno(dateno, type) {
|
getJournalDateno(dateno, type) {
|
||||||
if (dateno && typeof dateno === 'string') {
|
if (dateno && typeof dateno === 'string') {
|
||||||
const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === '');
|
const hasInvalidColon = !dateno.includes(':') || (dateno.includes(':') && dateno.split(':').pop().trim() === '');
|
||||||
@@ -5244,7 +5246,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
VueUeditorWrap
|
VueUeditorWrap,
|
||||||
|
ReferenceSearchLinks
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
role: {
|
role: {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
:title="articleContextDialogTitle"
|
:title="articleContextDialogTitle"
|
||||||
:visible.sync="articleContextDialogVisible"
|
:visible.sync="articleContextDialogVisible"
|
||||||
width="1100px"
|
width="1140px"
|
||||||
top="8vh"
|
top="8vh"
|
||||||
append-to-body
|
append-to-body
|
||||||
custom-class="article-context-dialog"
|
custom-class="article-context-dialog"
|
||||||
@@ -54,23 +54,16 @@
|
|||||||
<el-button type="primary" size="small" :loading="articleContextLoading" @click="loadArticleContext()">
|
<el-button type="primary" size="small" :loading="articleContextLoading" @click="loadArticleContext()">
|
||||||
{{ $t('mailboxSend.articleContextSearch') }}
|
{{ $t('mailboxSend.articleContextSearch') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
|
||||||
v-if="articleContext.loaded && hasArticleTemplatePlaceholders"
|
|
||||||
size="small"
|
|
||||||
type="warning"
|
|
||||||
plain
|
|
||||||
icon="el-icon-magic-stick"
|
|
||||||
@click="applyArticleTemplateVariables"
|
|
||||||
>
|
|
||||||
{{ $t('mailboxSend.articleContextReplaceVars') }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="articleContext.loaded" class="article-context-loaded-hint">
|
<div v-if="articleContext.loaded" class="article-context-meta-card">
|
||||||
<div class="article-loaded-line">
|
<div class="article-context-meta-card__row">
|
||||||
<span class="article-loaded-sn">SN {{ articleContext.acceptSn || '—' }}</span>
|
<span class="article-meta-label">SN</span>
|
||||||
|
<span class="article-loaded-sn">{{ articleContext.acceptSn || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="articleContext.title" class="article-context-meta-card__title">
|
||||||
|
{{ articleContext.title }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="articleContext.title" class="article-loaded-title">{{ articleContext.title }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!articleContextLoading" class="article-contact-empty article-contact-empty--hint">
|
<div v-else-if="!articleContextLoading" class="article-contact-empty article-contact-empty--hint">
|
||||||
{{ $t('mailboxSend.articleContextSearchHint') }}
|
{{ $t('mailboxSend.articleContextSearchHint') }}
|
||||||
@@ -123,7 +116,7 @@
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th class="review_table_index">No.</th>
|
<th class="review_table_index">No.</th>
|
||||||
<th>{{ $t('mailboxSend.tableName') }}</th>
|
<th class="review_table_name">{{ $t('mailboxSend.tableName') }}</th>
|
||||||
<th class="review_table_role">{{ $t('mailboxSend.tableAuthorRole') }}</th>
|
<th class="review_table_role">{{ $t('mailboxSend.tableAuthorRole') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -131,7 +124,12 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="(author, idx) in articleContext.authors"
|
v-for="(author, idx) in articleContext.authors"
|
||||||
:key="author.key"
|
:key="author.key"
|
||||||
:class="{ 'review_table_row--disabled': !hasContactEmail(author) }"
|
class="review_table_row--clickable"
|
||||||
|
:class="{
|
||||||
|
'review_table_row--disabled': !hasContactEmail(author),
|
||||||
|
'review_table_row--selected': isContactSelected('authors', author.key)
|
||||||
|
}"
|
||||||
|
@click="handleContactRowClick('authors', author, $event)"
|
||||||
>
|
>
|
||||||
<td class="review_table_check">
|
<td class="review_table_check">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
@@ -141,17 +139,36 @@
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="review_table_index">{{ idx + 1 }}</td>
|
<td class="review_table_index">{{ idx + 1 }}</td>
|
||||||
<td>
|
<td class="review_table_name">
|
||||||
<div class="author-name-email-cell">
|
<div class="author-name-email-cell">
|
||||||
<div class="author-name-email-cell__name">{{ author.name || '—' }}</div>
|
<div class="author-name-email-cell__name">{{ author.name || '—' }}</div>
|
||||||
<div v-if="author.email" class="author-name-email-cell__email">
|
<div v-if="author.email" class="author-name-email-cell__email">
|
||||||
<span class="is-clickable" @click="addContactToTo(author)">{{ author.email }}</span>
|
<span class="is-clickable" @click.stop="addContactToTo(author)">{{ author.email }}</span>
|
||||||
<i class="el-icon-document-copy article-copy-icon" :title="$t('mailboxSend.copyText')" @click.stop="copyContactText(author.email)"></i>
|
<i class="el-icon-document-copy article-copy-icon" :title="$t('mailboxSend.copyText')" @click.stop="copyContactText(author.email)"></i>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="author-name-email-cell__email reviewer-no-email">{{ $t('mailboxSend.articleContextNoEmail') }}</div>
|
<div v-else class="author-name-email-cell__email reviewer-no-email">{{ $t('mailboxSend.articleContextNoEmail') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="review_table_role">{{ formatAuthorRoles(author.roles) }}</td>
|
<td class="review_table_role">
|
||||||
|
<div v-if="formatAuthorRoles(author.roles) !== '—'" class="author-role-tags">
|
||||||
|
<el-tag
|
||||||
|
v-if="(author.roles || []).indexOf('firstAuthor') >= 0"
|
||||||
|
size="mini"
|
||||||
|
type="info"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ $t('mailboxSend.roleFirstAuthor') }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="(author.roles || []).indexOf('correspondingAuthor') >= 0"
|
||||||
|
size="mini"
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{{ $t('mailboxSend.roleCorrespondingAuthor') }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else>—</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -184,7 +201,12 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="(row, idx) in articleContext.reviewRows"
|
v-for="(row, idx) in articleContext.reviewRows"
|
||||||
:key="row.key"
|
:key="row.key"
|
||||||
:class="{ 'review_table_row--disabled': !hasContactEmail(row) }"
|
class="review_table_row--clickable"
|
||||||
|
:class="{
|
||||||
|
'review_table_row--disabled': !hasContactEmail(row),
|
||||||
|
'review_table_row--selected': isContactSelected('review', row.key)
|
||||||
|
}"
|
||||||
|
@click="handleContactRowClick('review', row, $event)"
|
||||||
>
|
>
|
||||||
<td class="review_table_check">
|
<td class="review_table_check">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
@@ -243,7 +265,12 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="(row, idx) in articleContext.finalReviewers"
|
v-for="(row, idx) in articleContext.finalReviewers"
|
||||||
:key="row.key"
|
:key="row.key"
|
||||||
:class="{ 'review_table_row--disabled': !hasContactEmail(row) }"
|
class="review_table_row--clickable"
|
||||||
|
:class="{
|
||||||
|
'review_table_row--disabled': !hasContactEmail(row),
|
||||||
|
'review_table_row--selected': isContactSelected('final', row.key)
|
||||||
|
}"
|
||||||
|
@click="handleContactRowClick('final', row, $event)"
|
||||||
>
|
>
|
||||||
<td class="review_table_check">
|
<td class="review_table_check">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
@@ -269,6 +296,15 @@
|
|||||||
<span class="article-context-selected-tab is-active">
|
<span class="article-context-selected-tab is-active">
|
||||||
{{ $t('mailboxSend.to') }} ({{ articleSelectedTotalCount }})
|
{{ $t('mailboxSend.to') }} ({{ articleSelectedTotalCount }})
|
||||||
</span>
|
</span>
|
||||||
|
<el-button
|
||||||
|
v-if="articleSelectedTotalCount"
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
class="article-context-selected-clear"
|
||||||
|
@click="clearArticleContacts(articleContactActiveTab)"
|
||||||
|
>
|
||||||
|
{{ $t('mailboxSend.articleContextClearAll') }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="article-context-selected-list">
|
<div class="article-context-selected-list">
|
||||||
<div v-if="!articleSelectedContactList.length" class="article-context-selected-empty">
|
<div v-if="!articleSelectedContactList.length" class="article-context-selected-empty">
|
||||||
@@ -1180,6 +1216,17 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
isContactSelected(module, key) {
|
isContactSelected(module, key) {
|
||||||
return (this.articleContactSelected[module] || []).indexOf(key) >= 0;
|
return (this.articleContactSelected[module] || []).indexOf(key) >= 0;
|
||||||
},
|
},
|
||||||
|
handleContactRowClick(module, contact, event) {
|
||||||
|
if (!contact || !this.hasContactEmail(contact)) return;
|
||||||
|
if (
|
||||||
|
event.target.closest(
|
||||||
|
'.el-checkbox, .el-checkbox__input, .article-copy-icon, .is-clickable, a, button, input, label'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.toggleContactSelected(module, contact.key, !this.isContactSelected(module, contact.key));
|
||||||
|
},
|
||||||
toggleContactSelected(module, key, checked) {
|
toggleContactSelected(module, key, checked) {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
const contact = this.getModuleContactList(module).find(function (item) {
|
const contact = this.getModuleContactList(module).find(function (item) {
|
||||||
@@ -2005,27 +2052,48 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
.article-context-search-bar {
|
.article-context-search-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-context-dialog-body {
|
.article-context-dialog-body {
|
||||||
max-height: 60vh;
|
max-height: 62vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 10px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-context-loaded-hint {
|
.article-context-meta-card {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 4px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-context-meta-card__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #909399;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-context-meta-card__title {
|
||||||
|
margin-top: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
flex-direction: column;
|
word-break: break-word;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-template-vars-bar {
|
.article-template-vars-bar {
|
||||||
@@ -2045,14 +2113,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
.article-loaded-sn {
|
.article-loaded-sn {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
font-size: 13px;
|
||||||
|
|
||||||
.article-loaded-title {
|
|
||||||
width: 100%;
|
|
||||||
color: #606266;
|
|
||||||
line-height: 1.5;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-contact-empty--hint {
|
.article-contact-empty--hint {
|
||||||
@@ -2064,7 +2125,8 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
|
|
||||||
.article-context-layout {
|
.article-context-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 320px;
|
min-height: 360px;
|
||||||
|
max-height: 48vh;
|
||||||
border: 1px solid #ebeef5;
|
border: 1px solid #ebeef5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -2109,13 +2171,13 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
.article-context-main {
|
.article-context-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 12px;
|
padding: 0;
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
border-right: 1px solid #ebeef5;
|
border-right: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-context-selected {
|
.article-context-selected {
|
||||||
width: 280px;
|
width: 300px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -2123,10 +2185,19 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
}
|
}
|
||||||
|
|
||||||
.article-context-selected-head {
|
.article-context-selected-head {
|
||||||
padding: 12px 14px 0;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px 0;
|
||||||
border-bottom: 1px solid #ebeef5;
|
border-bottom: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-context-selected-clear {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.article-context-selected-tab {
|
.article-context-selected-tab {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@@ -2144,7 +2215,6 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
.article-context-selected-list {
|
.article-context-selected-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-height: 360px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
@@ -2354,22 +2424,30 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overflow-x-auto {
|
.overflow-x-auto {
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review_table {
|
.review_table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review_table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.review_table th,
|
.review_table th,
|
||||||
.review_table td {
|
.review_table td {
|
||||||
padding: 8px 12px;
|
padding: 10px 12px;
|
||||||
border-bottom: 1px solid #ebeef5;
|
border-bottom: 1px solid #ebeef5;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
@@ -2394,6 +2472,22 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
color: #c0c4cc;
|
color: #c0c4cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review_table_row--clickable:not(.review_table_row--disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review_table_row--selected td {
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review_table_row--clickable:not(.review_table_row--disabled):hover td {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review_table_row--selected.review_table_row--clickable:not(.review_table_row--disabled):hover td {
|
||||||
|
background: #e6f1fc;
|
||||||
|
}
|
||||||
|
|
||||||
.review_table_row--disabled .reviewer-score {
|
.review_table_row--disabled .reviewer-score {
|
||||||
color: #d4d7de;
|
color: #d4d7de;
|
||||||
}
|
}
|
||||||
@@ -2416,12 +2510,34 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review_table_name {
|
||||||
|
width: auto;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.review_table_role {
|
.review_table_role {
|
||||||
min-width: 220px;
|
width: 148px;
|
||||||
white-space: nowrap;
|
min-width: 148px;
|
||||||
|
max-width: 148px;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.author-role-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-role-tags .el-tag {
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.reviewer-name-cell {
|
.reviewer-name-cell {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|||||||
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