终审编委信息

This commit is contained in:
2026-07-06 09:11:20 +08:00
parent 5260ca8ea5
commit 262e634f85
24 changed files with 5529 additions and 519 deletions

BIN
src/assets/img/tmr-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -976,6 +976,13 @@ const en = {
sendProgressRunning: 'Sending… {done}/{total} completed',
sendProgressAllSuccess: 'All sent successfully ({count})',
sendProgressDone: 'Done: {ok} sent, {fail} failed',
sendPreviewTitle: 'Send preview',
sendPreviewSub: 'Review the email each recipient will receive',
sendPreviewCount: '{count} email(s)',
sendPreviewRecipient: 'To',
sendPreviewSubject: 'Subject',
sendPreviewConfirm: 'Confirm send',
sendPreviewRealHint: 'Each recipient will receive a separate email matching the preview above',
backToInbox: 'Back to email list',
articleContextId: 'Article ID:',
articleContextIdPlaceholder: 'Enter article ID',
@@ -1018,8 +1025,26 @@ const en = {
articleContextRepeatReviewLine: '{n}nd review: {label}',
articleContextFinalReviewLine: 'Final decision: {decision}',
articleContextReplaceVars: 'Replace template vars',
articleContextReplaceVarsSuccess: 'Replaced {{article_title}} / {{article_sn}}',
articleContextReplaceVarsEmpty: 'Subject or body has no {{article_title}} / {{article_sn}} placeholders',
articleContextReplaceVarsSuccess: 'Replaced template variables (title, SN, name, article_id, final_r_id, etc.)',
articleContextReplaceVarsEmpty: 'Subject or body has no replaceable placeholders (e.g. {{article_title}}, [[name]], {{final_r_id}})',
recipientVarsSendHint: 'Keep placeholders in the body; each recipient gets a separate email with values from the table',
recipientVarsPanelTitle: 'Recipient variables',
recipientVarsPanelSub: 'One email per recipient; placeholders are replaced using the values below when sending',
recipientVarsSharedTitle: 'Shared (same for all recipients on this article)',
recipientVarsPerRecipientTitle: 'Per recipient',
recipientVarsColEmail: 'Recipient',
recipientVarsMissingWarn: '{count} recipient(s) missing name or final_r_id — load article and add from Final tab',
templateVar_name: 'Name [[name]]',
templateVar_article_id: 'Article ID',
templateVar_final_r_id: 'final_r_id',
templateVar_article_title: 'Article title',
templateVar_title: 'Title',
templateVar_manuscript_title: 'Manuscript title',
templateVar_accept_sn: 'SN',
templateVar_article_sn: 'SN',
templateVar_sn: 'SN',
templateVar_journal_name: 'Journal name',
templateVar_journal_url: 'Journal URL',
roleFirstAuthor: 'First author',
roleCorrespondingAuthor: 'Corresponding author',
roleFirstAuthorShort: '1st auth.',

View File

@@ -961,6 +961,13 @@ const zh = {
sendProgressRunning: '正在发送,已完成 {done}/{total}',
sendProgressAllSuccess: '全部发送成功,共 {count} 封',
sendProgressDone: '发送完成:成功 {ok} 封,失败 {fail} 封',
sendPreviewTitle: '发送预览',
sendPreviewSub: '请确认每位收件人将收到的邮件内容',
sendPreviewCount: '共 {count} 封',
sendPreviewRecipient: '收件人',
sendPreviewSubject: '主题',
sendPreviewConfirm: '确认发送',
sendPreviewRealHint: '确认后将按上方预览内容,为每位收件人逐封发送',
backToInbox: '返回收件箱',
articleContextId: '文章 ID',
articleContextIdPlaceholder: '请输入文章 ID',
@@ -1003,8 +1010,26 @@ const zh = {
articleContextRepeatReviewLine: '第 {n} 次复审:{label}',
articleContextFinalReviewLine: '终审:{decision}',
articleContextReplaceVars: '替换模板变量',
articleContextReplaceVarsSuccess: '已替换 {{article_title}} / {{article_sn}}',
articleContextReplaceVarsEmpty: '主题或正文不含 {{article_title}}、{{article_sn}} 等占位符',
articleContextReplaceVarsSuccess: '已替换模板变量标题、SN、name、article_id、final_r_id 等)',
articleContextReplaceVarsEmpty: '主题或正文不含可替换占位符(如 {{article_title}}、[[name]]、{{final_r_id}}',
recipientVarsSendHint: '正文保留占位符,发送时按左侧表格为每人单独替换并发送',
recipientVarsPanelTitle: '收件人变量',
recipientVarsPanelSub: '每人一封邮件,发送时按下列值替换模板中的占位符',
recipientVarsSharedTitle: '公共变量(本稿所有人相同)',
recipientVarsPerRecipientTitle: '每人不同',
recipientVarsColEmail: '收件人',
recipientVarsMissingWarn: '有 {count} 位收件人缺少 name 或 final_r_id请从终审列表添加或先加载稿件',
templateVar_name: '姓名 [[name]]',
templateVar_article_id: '文章 ID',
templateVar_final_r_id: 'final_r_id',
templateVar_article_title: '稿件标题',
templateVar_title: '标题',
templateVar_manuscript_title: '稿件标题',
templateVar_accept_sn: 'SN',
templateVar_article_sn: 'SN',
templateVar_sn: 'SN',
templateVar_journal_name: '期刊名',
templateVar_journal_url: '期刊网址',
roleFirstAuthor: '第一作者',
roleCorrespondingAuthor: '通讯作者',
roleFirstAuthorShort: '一作',

View File

@@ -79,8 +79,10 @@
v-if="Main_List.length > 0"
:content="Main_List"
:articleId="articleId"
:strippingHeadingNumbers="strippingHeadingNumbers"
ref="catalogue"
@goToListComment="goToListComment"
@stripHeadingNumbers="stripHeadingNumbersFromOutline"
style="width: 100%; height: 100%; padding: 0 0px; box-sizing: border-box; background-color: #fff"
>
</catalogue>
@@ -425,6 +427,22 @@
</el-form>
<span slot="footer" class="dialog-footer">
<el-button
v-if="zyModeEnabled"
type="warning"
plain
@click="handleConvertAddContentSupToBlue"
>
上标转引用
</el-button>
<el-button
v-if="zyModeEnabled"
type="warning"
plain
@click="handleMoveAddContentCitationBeforePeriod"
>
引用挪到句号前
</el-button>
<el-button @click="addContentVisible = false"> Cancel </el-button>
<el-button type="primary" @click="handleSaveAddContent">
<!-- <i class="el-icon-finished" style="margin-right: 5px"></i> -->
@@ -447,6 +465,7 @@ import Tinymce from '@/components/page/components/Tinymce';
import bottomTinymce from '@/components/page/components/Tinymce';
import catalogue from '@/components/page/components/table/catalogue.vue';
import { isZyModeEnabled } from '@/utils/zyMode';
import { collectHeadingNumberStripUpdates } from '@/utils/stripHeadingNumber';
export default {
data() {
return {
@@ -466,6 +485,7 @@ export default {
currentId: null,
ManuscirptContent: [],
mathFormulasList: [],
strippingHeadingNumbers: false,
articleId: this.$route.query.id,
pArticleId: this.$route.query.p_article_id || '',
isShowComment: false,
@@ -701,6 +721,34 @@ export default {
handleSaveAddContent() {
this.$refs.addContent.getTinymceContent('addcontent');
},
/** 临时Add Content 内数字上标 → blue 引用标签 */
handleConvertAddContentSupToBlue() {
const ref = this.$refs.addContent;
if (!ref || typeof ref.convertNumericSuperscriptsToBlue !== 'function') {
this.$message.warning('编辑器未就绪');
return;
}
const count = ref.convertNumericSuperscriptsToBlue();
if (count > 0) {
this.$message.success('已转换 ' + count + ' 处数字上标为 blue 引用');
} else {
this.$message.info('未找到可转换的数字上标(需为上标且内容仅为数字、逗号或连字符)');
}
},
/** 临时Add Content 内 blue 引用挪到句号前 */
handleMoveAddContentCitationBeforePeriod() {
const ref = this.$refs.addContent;
if (!ref || typeof ref.moveBlueCitationsBeforePeriod !== 'function') {
this.$message.warning('编辑器未就绪');
return;
}
const count = ref.moveBlueCitationsBeforePeriod();
if (count > 0) {
this.$message.success('已调整 ' + count + ' 处参考文献位置');
} else {
this.$message.info('未找到需要调整的 blue 参考文献(句号已在引用之后则不会重复处理)');
}
},
handleSaveEditProofreadingContent() {
this.$refs.addContent.getTinymceContent('addcontent');
},
@@ -991,6 +1039,65 @@ export default {
this.$refs.commonWord.goToComment(mainId);
});
},
async stripHeadingNumbersFromOutline(updates) {
const list = updates && updates.length ? updates : collectHeadingNumberStripUpdates(this.Main_List);
if (!list.length) {
this.$message.info('No heading numbers found to remove.');
return;
}
try {
await this.$confirm(
'Remove numbering from ' + list.length + ' heading(s)? (e.g. "1 Introduction" → "Introduction")',
'Remove Heading Numbers',
{
confirmButtonText: 'Remove',
cancelButtonText: 'Cancel',
type: 'warning'
}
);
} catch (e) {
return;
}
this.strippingHeadingNumbers = true;
const loading = this.$loading({
lock: true,
text: 'Removing heading numbers...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
let successCount = 0;
try {
for (let i = 0; i < list.length; i++) {
const item = list[i];
let str = String(item.content || '')
.replace(/^<p>\s*(.*?)\s*<\/p>$/, '$1')
.trim();
str = str.replace(/<br\s*\/?>/gi, '');
str = await this.$commonJS.decodeHtml(str);
const res = await this.$api.post(this.urlList.editContent, {
am_id: item.am_id,
content: str
});
if (res && res.code == 0) {
successCount++;
await this.refreshCurrentContent('content', item.am_id, res.data);
} else {
throw new Error((res && res.msg) || 'Save failed');
}
}
this.$message.success('Removed numbering from ' + successCount + ' heading(s).');
} catch (err) {
console.error(err);
this.$message.error((err && err.message) || 'Failed to remove heading numbers.');
} finally {
loading.close();
this.strippingHeadingNumbers = false;
}
},
getTables(tables, html) {
this.tables = data.map((e, i) => ({ ...e, p_main_table_id: 2141 + i, p_article_id: 2141 }));
this.tablesHtml = html;

View File

@@ -74,29 +74,46 @@
</el-select>
</el-form-item>
<el-form-item label="Manuscript :" prop="manuscirpt" label-width="120px">
<el-upload
:on-progress="handleProgress"
ref="uploadFileManuscirpt"
class="upload-demo up_newstyle"
:action="upload_manuscirpt"
accept=".docx"
name="manuscirpt"
:before-upload="beforeupload_manuscirpt"
:on-error="uperr_coverLetter"
:on-success="upSuccess_manuscirpt"
:limit="1"
:on-exceed="alertlimit"
:on-remove="removefilemanuscirpt"
:file-list="fileL_manuscirpt"
:on-preview="dowloadFile"
>
<div class="el-upload__text" @click="removefilemanuscirpt">
<em>Upload</em>
<div class="manuscript-upload-wrap">
<el-upload
:on-progress="handleProgress"
ref="uploadFileManuscirpt"
class="upload-demo up_newstyle"
:action="upload_manuscirpt"
accept=".docx"
name="manuscirpt"
:before-upload="beforeupload_manuscirpt"
:on-error="uperr_coverLetter"
:on-success="upSuccess_manuscirpt"
:limit="1"
:on-exceed="alertlimit"
:on-remove="removefilemanuscirpt"
:file-list="fileL_manuscirpt"
:on-preview="dowloadFile"
>
<div class="el-upload__text" @click="removefilemanuscirpt">
<em>Upload</em>
</div>
<div class="el-upload__tip" slot="tip">
Only Word and compressed files can be uploaded (file format: .docx).
</div>
</el-upload>
<div class="manuscript-template-notice">
<i class="el-icon-info manuscript-template-notice__icon"></i>
<p>
Kindly prepare and submit your manuscript in DOCX format using our
<a
href="javascript:void(0)"
class="template-download-link"
@click.prevent="dowloadFileTemplate"
>
<i class="el-icon-download"></i> official template
</a>.
Compliance with the template requirements allows us to process your submission more
efficiently and refine the articles final typesetting to a higher standard.
</p>
</div>
<div class="el-upload__tip" slot="tip">
Only Word and compressed files can be uploaded (file format: .docx).
</div>
</el-upload>
</div>
</el-form-item>
<el-form-item label="Manuscript Title :" prop="title" label-width="160px">
<el-input v-model="form.title" type="textarea" rows="2"></el-input>
@@ -4365,6 +4382,55 @@ export default {
font-size: 13px;
}
.manuscript-upload-wrap {
position: relative;
min-height: 32px;
}
.manuscript-template-notice {
display: flex;
align-items: flex-start;
gap: 8px;
margin-top: 12px;
padding: 10px 12px;
background: #f5f9fb;
border: 1px solid #e3eef3;
border-left: 3px solid #006699;
border-radius: 4px;
}
.manuscript-template-notice__icon {
flex-shrink: 0;
margin-top: 2px;
color: #006699;
font-size: 16px;
}
.manuscript-template-notice p {
margin: 0;
font-size: 13px;
line-height: 1.65;
color: #555;
}
.template-download-link {
color: #006699;
font-weight: 600;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.template-download-link:hover {
text-decoration: underline;
}
.template-download-link .el-icon-download {
font-weight: bold;
margin-right: 2px;
}
.el-collapse-item__content {
padding-bottom: 8px !important;
}

View File

@@ -154,23 +154,40 @@
>
<el-table-column type="index" label="No." align="center" width="50"></el-table-column>
<el-table-column prop="User Message" label="Realname" width="340">
<el-table-column prop="User Message" label="Realname" width="320">
<template slot-scope="scope">
<p style="font-size: 14px; font-weight: bold; margin-top: 2px">{{ scope.row.realname }}</p>
<p style="margin: 2px 0 0px 0">
<span style="color: #999">Email :</span>
{{ scope.row.email }}
</p>
<p style="margin: 0 0 5px 0" v-if="scope.row.title != ''">
<p style="margin: 2px 0 0" v-if="scope.row.type != null && scope.row.type !== ''">
<span style="color: #999">Identity : </span>
<span v-if="scope.row.type == 0" style="color: #409eff">Editor-in-Chief</span>
<span v-if="scope.row.type == 1" style="color: #409eff">Vice Editor-in-Chief</span>
<span v-if="scope.row.type == 2" style="color: #409eff">Editorial Board Members</span>
</p>
<p class="tab_tie_col tab_tie_col--invite tab_tie_col--dialog">
<span>Latest invitation: </span>{{ formatInviteTime(scope.row) }}
</p>
</template>
</el-table-column>
<el-table-column prop="research_areas" label="Research Areas">
<el-table-column label="Other Information" min-width="280">
<template slot-scope="scope">
<p class="tab_tie_col">
<span>Research areas: </span>{{ getBoardResearchAreas(scope.row) }}
</p>
<p class="tab_tie_col">
<span>Field: </span>{{ getBoardField(scope.row) }}
</p>
<p class="tab_tie_col">
<span>AI Field: </span>{{ getBoardFieldAi(scope.row) }}
</p>
</template>
</el-table-column>
<el-table-column label="Research Areas" width="260">
<template slot-scope="scope">
<commonMajorTableList
:user_id="scope.row.user_id"
@@ -363,6 +380,52 @@ export default {
}
},
methods: {
formatBoardText(value) {
const text = value != null ? String(value).trim() : '';
return text || '—';
},
getBoardField(row) {
return this.formatBoardText(row && row.field);
},
getBoardFieldAi(row) {
return this.formatBoardText(row && row.field_ai);
},
getBoardResearchAreas(row) {
return this.formatBoardText(row && row.research_areas);
},
formatInviteTime(row) {
const raw =
row &&
(row.last_time != null
? row.last_time
: row.last_invite_time != null
? row.last_invite_time
: row.latest_invite_time);
if (raw == null || raw === '') {
return '—';
}
if (typeof raw === 'string' && /[-/]/.test(raw) && isNaN(Number(raw))) {
return raw;
}
let ts = Number(raw);
if (!ts) {
return '—';
}
if (ts < 1e12) {
ts *= 1000;
}
const date = new Date(ts);
if (isNaN(date.getTime())) {
return String(raw);
}
const Y = date.getFullYear() + '-';
const M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
const s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
return Y + M + D + ' ' + h + ':' + m + ':' + s;
},
selectEditorialBoardType(type) {
console.log('type at line 525:', type);
this.editorialBoardForm.board_type = type;
@@ -745,6 +808,20 @@ export default {
margin: 0 5px 0 0;
font-size: 13px;
}
.tab_tie_col--invite {
display: block;
margin-top: 4px;
font-size: 12px;
color: #aaa;
}
.tab_tie_col--dialog {
margin: 4px 0 0;
}
.tab_tie_col--multiline {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
::v-deep .editorialBoardDialog .el-dialog__body {
padding-top: 10px !important;
padding-bottom: 0px !important;

View File

@@ -1,7 +1,12 @@
<template>
<div class="ManuscirptList">
<div class="arrlist">
<ul class="catalogue-ul">
<span v-if="zyModeEnabled" class="outline-format-icon" @click.stop="handleStripHeadingNumbers">
<el-tooltip effect="dark" content="Remove heading numbers (e.g. 1, 1.1)" placement="left">
<i :class="strippingHeadingNumbers ? 'el-icon-loading' : 'el-icon-brush'"></i>
</el-tooltip>
</span>
<ul class="catalogue-ul" :class="{ 'catalogue-ul--with-toolbar': zyModeEnabled }">
<li class="catalogue-li">
<div
@click="goToListComment(item.am_id, 'content')"
@@ -21,8 +26,15 @@
<script>
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
import { collectHeadingNumberStripUpdates } from '@/utils/stripHeadingNumber';
import { isZyModeEnabled } from '@/utils/zyMode';
export default {
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content', 'strippingHeadingNumbers'],
computed: {
zyModeEnabled() {
return isZyModeEnabled(this.$route);
}
},
watch: {
content: {
handler(newVal, oldVal) {
@@ -102,6 +114,17 @@ export default {
goToListComment(id, type) {
this.$emit('goToListComment', id, type);
},
handleStripHeadingNumbers() {
if (this.strippingHeadingNumbers) {
return;
}
const updates = collectHeadingNumberStripUpdates(this.content);
if (!updates.length) {
this.$message.info('No heading numbers found to remove.');
return;
}
this.$emit('stripHeadingNumbers', updates);
}
},
async created() {},
@@ -159,9 +182,25 @@ export default {
.arrlist {
flex: 1;
overflow-y: auto;
position: relative;
/* padding-top: 8px; */
}
.outline-format-icon {
position: absolute;
top: 0px;
right: 2px;
z-index: 2;
color: #909399;
font-size: 16px;
line-height: 1;
cursor: pointer;
}
.outline-format-icon:hover {
color: #409eff;
}
.catalogue-ul {
width: 100%;
list-style: none;
@@ -169,6 +208,10 @@ export default {
margin: 0;
}
.catalogue-ul--with-toolbar {
padding-top: 0px;
}
/* 基础条目样式 - 模拟 Word 悬浮效果 */
.catalogue-item {
position: relative;

View File

@@ -37,6 +37,8 @@
<script>
import Tinymce from '@/components/page/components/Tinymce';
import { convertNumericSuperscriptsToBlueCitationsHtml } from '@/utils/convertNumericSuperscriptsToBlue';
import { moveBlueCitationsBeforePeriodHtml } from '@/utils/moveBlueCitationsBeforePeriod';
export default {
props: {
value: {},
@@ -128,6 +130,42 @@ export default {
getContent(type, content) {
this.$emit('getContent', type, content);
},
/** 临时:数字上标 → blue 参考文献标签 */
convertNumericSuperscriptsToBlue() {
const tinymceRef = this.$refs.tinymceChild1;
if (!tinymceRef || !tinymceRef.tinymceId || typeof window.tinymce === 'undefined') {
return 0;
}
const editor = window.tinymce.get(tinymceRef.tinymceId);
if (!editor) {
return 0;
}
const raw = editor.getContent({ format: 'raw' });
const result = convertNumericSuperscriptsToBlueCitationsHtml(raw);
if (result.count > 0) {
editor.setContent(result.html);
editor.fire('change');
}
return result.count;
},
/** 临时blue 引用挪到句号前,重复句号合并为一个 */
moveBlueCitationsBeforePeriod() {
const tinymceRef = this.$refs.tinymceChild1;
if (!tinymceRef || !tinymceRef.tinymceId || typeof window.tinymce === 'undefined') {
return 0;
}
const editor = window.tinymce.get(tinymceRef.tinymceId);
if (!editor) {
return 0;
}
const raw = editor.getContent({ format: 'raw' });
const result = moveBlueCitationsBeforePeriodHtml(raw);
if (result.count > 0) {
editor.setContent(result.html);
editor.fire('change');
}
return result.count;
}
}
};

View File

@@ -1179,7 +1179,7 @@ import { TableUtils } from '@/common/js/TableUtils';
import { debounce, throttle } from '@/common/js/debounce';
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
import LatexDataPanel from './LatexDataPanel.vue';
import { downloadManuscriptWord, fetchManuscriptReferenceList } from '@/utils/exportManuscriptWord';
import { downloadManuscriptWord, fetchManuscriptReferenceList, summarizeManuscriptExportItems } from '@/utils/exportManuscriptWord';
import {
buildReferencesCopyText,
buildReferencesHtmlLabels,
@@ -1768,6 +1768,11 @@ export default {
return;
}
this.exportingManuscriptWord = true;
console.info('[Word export] 用户点击导出按钮', {
articleId: this.articleId,
pArticleId: this.pArticleId,
summary: summarizeManuscriptExportItems(this.wordList)
});
try {
const hasUploaded =
this.uploadedReferenceHtmlItems && this.uploadedReferenceHtmlItems.length;
@@ -1786,7 +1791,12 @@ export default {
: this.$t('commonTable.exportManuscriptSuccess') || 'Word downloaded.'
);
} catch (err) {
console.error(err);
console.error('[Word export] 导出失败', {
articleId: this.articleId,
pArticleId: this.pArticleId,
summary: summarizeManuscriptExportItems(this.wordList),
error: err
});
if (err && err.message === 'NO_CONTENT') {
this.$message.warning(this.$t('commonTable.exportManuscriptEmpty') || 'No content to export.');
} else {

View File

@@ -502,6 +502,40 @@
ref="myTableModal"
/>
<el-dialog
title="导出全部图片 Word"
:visible.sync="figureExportDialogVisible"
width="420px"
append-to-body
:close-on-click-modal="false"
>
<div class="figure-export-dialog">
<p class="figure-export-dialog__hint">高只需填一项即可另一项按原图比例自动计算两项都填则固定尺寸图与图之间仅空 1 </p>
<el-form label-width="88px" size="small" @submit.native.prevent>
<el-form-item label="宽度 (cm)">
<el-input
v-model="figureExportWidthCm"
placeholder="如 8只填宽度时锁定纵横比"
clearable
/>
</el-form-item>
<el-form-item label="高度 (cm)">
<el-input
v-model="figureExportHeightCm"
placeholder="如 6只填高度时锁定纵横比"
clearable
/>
</el-form-item>
</el-form>
</div>
<span slot="footer">
<el-button @click="figureExportDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="exportingAllFiguresWord" @click="confirmDownloadAllFiguresWord">
导出
</el-button>
</span>
</el-dialog>
</div>
</template>
@@ -550,7 +584,10 @@ export default {
exportingAllWord: false,
exportingTableId: null,
exportingAllFiguresWord: false,
exportingFigureId: null
exportingFigureId: null,
figureExportDialogVisible: false,
figureExportWidthCm: '',
figureExportHeightCm: ''
};
},
components: {
@@ -1034,10 +1071,43 @@ if(newData.ami_id) {
this.$message.warning('No figures to export.');
return;
}
this.figureExportWidthCm = '';
this.figureExportHeightCm = '';
this.figureExportDialogVisible = true;
},
parseFigureExportSizeCm(value) {
if (value === undefined || value === null || value === '') {
return null;
}
const num = Number(String(value).trim());
if (!Number.isFinite(num) || num <= 0) {
return null;
}
return num;
},
async confirmDownloadAllFiguresWord() {
const widthCm = this.parseFigureExportSizeCm(this.figureExportWidthCm);
const heightCm = this.parseFigureExportSizeCm(this.figureExportHeightCm);
if (
(this.figureExportWidthCm && widthCm == null) ||
(this.figureExportHeightCm && heightCm == null)
) {
this.$message.warning('请输入有效的厘米数值(大于 0。');
return;
}
this.exportingAllFiguresWord = true;
try {
await exportAllFiguresWordFile(this.images, this.mediaUrl, 'figures');
const exportOptions = {};
if (widthCm != null || heightCm != null) {
exportOptions.figureSize = {
widthCm: widthCm,
heightCm: heightCm
};
}
await exportAllFiguresWordFile(this.images, this.mediaUrl, 'figures', exportOptions);
this.$message.success('Word downloaded.');
this.figureExportDialogVisible = false;
} catch (err) {
console.error(err);
if (err && err.message === 'NO_FIGURES') {
@@ -1370,4 +1440,11 @@ li {
}
.single {
}
.figure-export-dialog__hint {
margin: 0 0 14px;
color: #606266;
font-size: 13px;
line-height: 1.5;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -93,8 +93,15 @@
</div>
<!-- 同意弹出框 -->
<el-dialog title="Agree User" :visible.sync="agreeVisible" width="700px" :close-on-click-modal="false" class="agree_form_box">
<el-form ref="agree_Form" :model="agreeForm" :rules="rules" label-width="140px">
<el-dialog
title="Agree User"
:visible.sync="agreeVisible"
width="700px"
:close-on-click-modal="false"
class="agree_form_box"
@opened="handleAgreeDialogOpened"
>
<el-form ref="agree_Form" :model="agreeForm" :rules="agreeFormRules" label-width="140px">
<el-form-item label="Account :" style="margin-bottom: 2px !important">
<span>{{ agreeForm.account }}</span>
</el-form-item>
@@ -188,6 +195,10 @@
<script>
import commonCv from '../common/cv.vue';
import bus from '../common/bus';
import { isZyModeEnabled } from '@/utils/zyMode';
const AGREE_DETAIL_FIELDS = ['realname', 'phone', 'technical', 'country', 'website', 'field', 'introduction', 'company'];
export default {
components: {
commonCv
@@ -336,6 +347,33 @@ export default {
}
};
},
computed: {
zyModeEnabled() {
return isZyModeEnabled(this.$route);
},
agreeFormRules() {
const rules = JSON.parse(JSON.stringify(this.rules));
if (this.zyModeEnabled) {
rules.phone = [
{
validator: function (rule, value, callback) {
if (!value || String(value).trim() === '') {
callback();
return;
}
if (/^1[34578]\d{9}$/.test(value) == false) {
callback(new Error('Please enter the correct phone format'));
} else {
callback();
}
},
trigger: 'blur'
}
];
}
return rules;
}
},
created() {
this.getDate();
this.getCountry();
@@ -406,16 +444,81 @@ export default {
// 同意
agreeApply(e) {
this.agreeForm = {};
console.log('e at line 405:', e);
this.agreeForm = Object.assign({}, e);
this.applyZyDefaultYear();
this.agreeVisible = true;
this.agreeForm = e;
this.$refs.agree_Form.clearValidate();
this.$nextTick(() => {
this.applyZyDefaultYear();
if (this.$refs.agree_Form) {
this.$refs.agree_Form.clearValidate();
}
});
if (this.zyModeEnabled && e.user_id) {
this.supplementAgreeFormByUserDetail(e.user_id);
}
},
// 提交同意
saveAgreee() {
applyZyDefaultYear() {
if (!this.zyModeEnabled) {
return;
}
this.$set(this.agreeForm, 'year', 1);
},
handleAgreeDialogOpened() {
this.applyZyDefaultYear();
},
isAgreeFieldEmpty(value) {
return value === undefined || value === null || String(value).trim() === '';
},
mergeAgreeFormFromUserDetail(user) {
if (!user) {
return;
}
AGREE_DETAIL_FIELDS.forEach((key) => {
if (this.isAgreeFieldEmpty(this.agreeForm[key]) && !this.isAgreeFieldEmpty(user[key])) {
this.$set(this.agreeForm, key, user[key]);
}
});
this.applyZyDefaultYear();
},
supplementAgreeFormByUserDetail(userId) {
this.$api
.post('api/User/getUserDetail', {
user_id: userId
})
.then((res) => {
if (res.code != 0 || !res.data || !res.data.user) {
if (res.msg) {
this.$message.error(res.msg);
}
return;
}
this.mergeAgreeFormFromUserDetail(res.data.user);
this.$nextTick(() => {
this.tryZyModeAutoAgree();
});
})
.catch((err) => {
console.log(err);
});
},
tryZyModeAutoAgree() {
if (!this.zyModeEnabled || !this.$refs.agree_Form) {
return;
}
this.$refs.agree_Form.validate((valid) => {
if (valid) {
this.submitAgreeForm();
}
});
},
submitAgreeForm() {
var data = {
ap_yboard_id: this.agreeForm.ap_yboard_id,
year: this.agreeForm.year,
@@ -428,27 +531,30 @@ export default {
introduction: this.agreeForm.introduction,
company: this.agreeForm.company
};
console.log('data at line 419:', data);
this.$api
.post('api/User/agreeYboardApplyNew', {
...data
})
.then((res) => {
if (res.code == 0) {
this.$message.success('Add succeeded!');
this.agreeVisible = false;
this.getDate();
} else {
this.$message.error(res.msg);
}
})
.catch((err) => {
console.log(err);
});
},
// 提交同意
saveAgreee() {
this.$refs.agree_Form.validate((valid) => {
if (valid) {
this.$api
.post('api/User/agreeYboardApplyNew', {
...data
})
.then((res) => {
if (res.code == 0) {
this.$message.success('Add succeeded!');
this.agreeVisible = false;
this.getDate();
} else {
this.$message.error(res.msg);
}
})
.catch((err) => {
console.log(err);
});
this.submitAgreeForm();
} else {
this.$message.error('Please complete the information!!');
}

View File

@@ -0,0 +1,113 @@
/** 上标内纯数字引用1 / 1, 2 / 3-5 / 35 等 */
const NUMERIC_CITATION_PLAIN_PATTERN = /^[\d\s,\-–—]+$/;
function normalizeCitationBracketInner(text) {
let inner = String(text || '')
.replace(/\u00a0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
for (let i = 0; i < 3; i++) {
inner = inner.replace(/(\d),(\d)/g, '$1, $2');
}
return inner;
}
/** 上标文本是否全是参考文献数字(含逗号、连字符) */
export function isNumericCitationPlainText(text) {
const plain = String(text || '')
.replace(/\u00a0/g, ' ')
.trim();
if (!plain) {
return false;
}
return NUMERIC_CITATION_PLAIN_PATTERN.test(plain);
}
/** 纯数字引用 → [1, 2] 形式 */
export function formatBlueCitationFromPlainText(text) {
const inner = normalizeCitationBracketInner(text);
if (!inner) {
return '';
}
if (/^\[.+\]$/.test(inner)) {
return inner;
}
return '[' + inner + ']';
}
/** 去掉紧邻节点上的空白(普通空格 / &nbsp;),避免与两侧 &nbsp; 重复 */
function stripAdjacentWhitespace(parent, sup) {
if (!parent || !sup) {
return;
}
const prev = sup.previousSibling;
if (prev && prev.nodeType === Node.TEXT_NODE) {
prev.nodeValue = prev.nodeValue.replace(/[\s\u00a0]+$/, '');
if (!prev.nodeValue) {
parent.removeChild(prev);
}
}
const next = sup.nextSibling;
if (next && next.nodeType === Node.TEXT_NODE) {
next.nodeValue = next.nodeValue.replace(/^[\s\u00a0]+/, '');
if (!next.nodeValue) {
parent.removeChild(next);
}
}
}
/**
* 将 HTML 中「内容全是数字」的 <sup> 转为 &nbsp;<blue>[…]</blue>&nbsp;。
* 上标两侧若已有空格,先去掉再各放一个 &nbsp;,避免出现 &nbsp;+空格。
* @returns {{ html: string, count: number }}
*/
export function convertNumericSuperscriptsToBlueCitationsHtml(html) {
if (!html || typeof document === 'undefined') {
return { html: html || '', count: 0 };
}
const container = document.createElement('div');
container.innerHTML = String(html);
let count = 0;
const superscripts = Array.from(container.querySelectorAll('sup'));
superscripts.forEach(function (sup) {
if (sup.closest('blue')) {
return;
}
const plain = (sup.textContent || '').replace(/\u00a0/g, ' ').trim();
if (!isNumericCitationPlainText(plain)) {
return;
}
const citationText = formatBlueCitationFromPlainText(plain);
if (!citationText) {
return;
}
const parent = sup.parentNode;
if (!parent) {
return;
}
stripAdjacentWhitespace(parent, sup);
const blueEl = document.createElement('blue');
blueEl.textContent = citationText;
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('\u00a0'));
fragment.appendChild(blueEl);
fragment.appendChild(document.createTextNode('\u00a0'));
parent.replaceChild(fragment, sup);
count += 1;
});
return {
html: container.innerHTML,
count: count
};
}

View File

@@ -1,4 +1,4 @@
import { AlignmentType, ExternalHyperlink, LineRuleType, Packer, Paragraph, TextRun } from 'docx';
import { AlignmentType, ExternalHyperlink, ImageRun, LineRuleType, Packer, Paragraph, TextRun } from 'docx';
import { saveAs } from 'file-saver';
import {
appendFigureTitleParagraphs,
@@ -9,11 +9,26 @@ import {
htmlToPlainText,
htmlToTextRuns,
splitHtmlSegments,
TABLE_FONT_SIZE,
TITLE_COLOR
TABLE_FONT_SIZE
} from '@/utils/exportTableWord';
const LINK_COLOR = '0563C1';
const DOCX_IMAGE_EMU_PER_UNIT = 9525;
/** A4 页宽 21cm左右边距各 1.91cm(与 exportTableWord PAGE_MARGINS 一致)→ 正文宽 100% */
const PAGE_WIDTH_CM = 21;
const PAGE_MARGIN_LEFT_CM = 1.91;
const PAGE_MARGIN_RIGHT_CM = 1.91;
const FIGURE_MAX_WIDTH_CM = PAGE_WIDTH_CM - PAGE_MARGIN_LEFT_CM - PAGE_MARGIN_RIGHT_CM;
const FIGURE_IMAGE_FETCH_TIMEOUT_MS = 15000;
/** 批量导出全部图片时,每个图块之间的空行数 */
const ALL_FIGURES_BLOCK_EMPTY_LINE_COUNT = 1;
/** 图片段:单倍行距、段前段后 0对应 Word LineSpacingRule = wdLineSpaceSingle */
const FIGURE_IMAGE_PARAGRAPH_SPACING = {
before: 0,
after: 0,
line: 240,
lineRule: LineRuleType.AUTO
};
function sanitizeFileName(name, fallback) {
const base = String(name || fallback || 'figure')
@@ -28,7 +43,20 @@ function getFigureImagePath(item) {
if (!item) {
return '';
}
return item.url || item.image || '';
if (item.url) {
return item.url;
}
const image = item.image;
if (!image) {
return '';
}
if (typeof image === 'string') {
return image;
}
if (typeof image === 'object' && image.url) {
return image.url;
}
return '';
}
function getFullImageUrl(mediaUrl, imagePath) {
@@ -42,11 +70,291 @@ function getFullImageUrl(mediaUrl, imagePath) {
return String(mediaUrl || '') + path;
}
function getFigureImageType(imagePath) {
const ext = String(imagePath || '')
.split('.')
.pop()
.toLowerCase()
.split('?')[0];
if (ext === 'png') {
return 'png';
}
if (ext === 'jpg' || ext === 'jpeg') {
return 'jpg';
}
if (ext === 'gif') {
return 'gif';
}
if (ext === 'bmp') {
return 'bmp';
}
return null;
}
function getFigureImageMimeType(imageType) {
if (imageType === 'png') {
return 'image/png';
}
if (imageType === 'jpg') {
return 'image/jpeg';
}
if (imageType === 'gif') {
return 'image/gif';
}
if (imageType === 'bmp') {
return 'image/bmp';
}
return 'application/octet-stream';
}
function cmToDocxImageUnits(cm) {
return (cm * 360000) / DOCX_IMAGE_EMU_PER_UNIT;
}
function parseFigureSizeCm(value) {
if (value === undefined || value === null || value === '') {
return null;
}
const num = Number(value);
if (!Number.isFinite(num) || num <= 0) {
return null;
}
return num;
}
function resolveFigureSizeOptions(options) {
const opts = options || {};
const widthCm = parseFigureSizeCm(opts.widthCm);
const heightCm = parseFigureSizeCm(opts.heightCm);
if (widthCm == null && heightCm == null) {
return null;
}
return {
widthCm: widthCm,
heightCm: heightCm,
maxWidthCm: widthCm != null ? widthCm : FIGURE_MAX_WIDTH_CM
};
}
function computeFigureImageTransformation(naturalWidth, naturalHeight, sizeOptions) {
const resolved = resolveFigureSizeOptions(sizeOptions);
const maxWidthCm = resolved && resolved.maxWidthCm != null ? resolved.maxWidthCm : FIGURE_MAX_WIDTH_CM;
const fixedWidthCm = resolved && resolved.widthCm != null ? resolved.widthCm : null;
const fixedHeightCm = resolved && resolved.heightCm != null ? resolved.heightCm : null;
if (fixedWidthCm != null) {
const width = cmToDocxImageUnits(fixedWidthCm);
if (fixedHeightCm != null) {
return {
width: Math.round(width),
height: Math.round(cmToDocxImageUnits(fixedHeightCm))
};
}
if (naturalWidth && naturalHeight) {
return {
width: Math.round(width),
height: Math.round(width * (naturalHeight / naturalWidth))
};
}
return {
width: Math.round(width),
height: Math.round(width * 0.75)
};
}
if (fixedHeightCm != null) {
const height = cmToDocxImageUnits(fixedHeightCm);
if (naturalWidth && naturalHeight) {
return {
width: Math.round(height * (naturalWidth / naturalHeight)),
height: Math.round(height)
};
}
return {
width: Math.round(height * (4 / 3)),
height: Math.round(height)
};
}
const maxWidth = cmToDocxImageUnits(maxWidthCm);
if (!naturalWidth || !naturalHeight) {
return {
width: Math.round(maxWidth),
height: Math.round(maxWidth * 0.75)
};
}
// LockAspectRatio = True仅当宽度超出正文区时等比缩小
let width = naturalWidth;
let height = naturalHeight;
if (width > maxWidth) {
height = Math.round(height * (maxWidth / width));
width = maxWidth;
}
return {
width: Math.round(width),
height: Math.round(height)
};
}
/** 本地开发时把跨域 /public/ 资源改写为同源路径,走 vue.config.js 的 /public 代理 */
function toSameOriginMediaFetchUrl(imageUrl) {
const url = String(imageUrl || '').trim();
if (!url || typeof window === 'undefined') {
return url;
}
if (url.startsWith('/public/')) {
return url;
}
try {
const parsed = new URL(url, window.location.origin);
if (parsed.origin === window.location.origin) {
return parsed.href;
}
const publicPathMatch = parsed.pathname.match(/(\/public\/.*)$/);
if (publicPathMatch) {
return publicPathMatch[1];
}
} catch (e) {
return url;
}
return url;
}
async function loadFigureImageData(imageUrl) {
if (!imageUrl || typeof fetch === 'undefined') {
return null;
}
const fetchUrl = toSameOriginMediaFetchUrl(imageUrl);
try {
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timer =
controller &&
setTimeout(function () {
controller.abort();
}, FIGURE_IMAGE_FETCH_TIMEOUT_MS);
const response = await fetch(fetchUrl, controller ? { signal: controller.signal } : undefined);
if (timer) {
clearTimeout(timer);
}
if (!response.ok) {
return null;
}
const buffer = await response.arrayBuffer();
if (!buffer || !buffer.byteLength) {
return null;
}
return new Uint8Array(buffer);
} catch (e) {
return null;
}
}
function getImageNaturalSize(imageData, imageType) {
if (typeof window === 'undefined' || !window.Image || !window.URL) {
return Promise.resolve(null);
}
return new Promise(function (resolve) {
const blob = new Blob([imageData], { type: getFigureImageMimeType(imageType) });
const objectUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = function () {
resolve({
width: img.naturalWidth,
height: img.naturalHeight
});
URL.revokeObjectURL(objectUrl);
};
img.onerror = function () {
resolve(null);
URL.revokeObjectURL(objectUrl);
};
img.src = objectUrl;
});
}
function appendFigureBlockEmptyLines(children, count) {
const lineCount = Math.max(0, Number(count) || 0);
let i = 0;
for (i = 0; i < lineCount; i += 1) {
children.push(createTableParagraph([new TextRun('')], AlignmentType.JUSTIFIED));
}
}
function createFigureImageParagraph(prepared, imageData, imageType, sizeOptions) {
return getImageNaturalSize(imageData, imageType).then(function (size) {
const transformation = computeFigureImageTransformation(size && size.width, size && size.height, sizeOptions);
try {
return new Paragraph({
alignment: AlignmentType.CENTER,
spacing: FIGURE_IMAGE_PARAGRAPH_SPACING,
children: [
new ImageRun({
type: imageType,
data: imageData,
transformation: transformation,
altText: {
title: htmlToPlainText(prepared.title) || 'Figure',
description: prepared.imageUrl,
name: 'figure-image'
}
})
]
});
} catch (e) {
return createFigureLinkParagraph(prepared);
}
});
}
function createFigureLinkParagraph(prepared) {
return new Paragraph({
alignment: AlignmentType.CENTER,
spacing: FIGURE_IMAGE_PARAGRAPH_SPACING,
children: [
new ExternalHyperlink({
link: prepared.imageUrl,
children: [
new TextRun({
text: prepared.imageUrl,
font: FONT_NAME,
size: TABLE_FONT_SIZE,
color: LINK_COLOR,
underline: {}
})
]
})
]
});
}
export function isExportableFigureItem(item) {
return !!getFigureImagePath(item);
}
export function prepareFigureExportItem(item, mediaUrl) {
if (item && item.image && typeof item.image === 'object' && item.image.url) {
const imagePath = item.image.url;
const imageUrl = getFullImageUrl(mediaUrl, imagePath);
if (!imageUrl) {
return null;
}
return {
title: item.image.title || '',
note: item.image.note || '',
imageUrl: imageUrl,
imagePath: imagePath
};
}
const imagePath = getFigureImagePath(item);
const imageUrl = getFullImageUrl(mediaUrl, imagePath);
if (!imageUrl) {
@@ -55,46 +363,34 @@ export function prepareFigureExportItem(item, mediaUrl) {
return {
title: (item && item.title) || '',
note: (item && item.note) || '',
imageUrl: imageUrl
imageUrl: imageUrl,
imagePath: imagePath
};
}
function buildFigureWordChildren(item, mediaUrl) {
export async function buildEmbeddedFigureChildren(item, mediaUrl, options) {
const opts = options || {};
const appendTrailingEmptyLines = opts.appendTrailingEmptyLines !== false;
const trailingEmptyLineCount =
opts.trailingEmptyLineCount != null ? opts.trailingEmptyLineCount : null;
const figureSize = resolveFigureSizeOptions(opts.figureSize);
const prepared = prepareFigureExportItem(item, mediaUrl);
if (!prepared) {
return [];
}
const children = [];
const imageType = getFigureImageType(prepared.imagePath);
const imageData = imageType ? await loadFigureImageData(prepared.imageUrl) : null;
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 (imageData && imageType) {
children.push(await createFigureImageParagraph(prepared, imageData, imageType, figureSize));
} else if (prepared.imageUrl) {
children.push(createFigureLinkParagraph(prepared));
}
if (prepared.title) {
appendFigureTitleParagraphs(children, prepared.title);
appendFigureTitleParagraphs(children, prepared.title, { note: prepared.note });
}
if (prepared.note) {
@@ -103,22 +399,48 @@ function buildFigureWordChildren(item, mediaUrl) {
});
}
appendTableBlockEmptyLines(children);
if (appendTrailingEmptyLines) {
if (trailingEmptyLineCount != null) {
appendFigureBlockEmptyLines(children, trailingEmptyLineCount);
} else {
appendTableBlockEmptyLines(children);
}
}
return children;
}
export function buildFigureWordDocument(item, mediaUrl) {
return createWordDocument(buildFigureWordChildren(item, mediaUrl));
async function buildFigureWordChildren(item, mediaUrl) {
return buildEmbeddedFigureChildren(item, mediaUrl);
}
export function buildAllFiguresWordDocument(items, mediaUrl) {
export async function buildFigureWordDocument(item, mediaUrl) {
return createWordDocument(await buildFigureWordChildren(item, mediaUrl));
}
export async function buildAllFiguresWordDocument(items, mediaUrl, options) {
const opts = options || {};
const figureSize = resolveFigureSizeOptions(opts.figureSize);
const trailingEmptyLineCount =
opts.trailingEmptyLineCount != null ? opts.trailingEmptyLineCount : ALL_FIGURES_BLOCK_EMPTY_LINE_COUNT;
const exportItems = [];
(items || []).forEach(function (item) {
if (prepareFigureExportItem(item, mediaUrl)) {
exportItems.push(item);
}
});
const children = [];
(items || []).forEach((item) => {
const blockChildren = buildFigureWordChildren(item, mediaUrl);
blockChildren.forEach((child) => {
for (let i = 0; i < exportItems.length; i += 1) {
const isLast = i === exportItems.length - 1;
const blockChildren = await buildEmbeddedFigureChildren(exportItems[i], mediaUrl, {
figureSize: figureSize,
appendTrailingEmptyLines: !isLast,
trailingEmptyLineCount: isLast ? 0 : trailingEmptyLineCount
});
blockChildren.forEach(function (child) {
children.push(child);
});
});
}
return createWordDocument(children);
}
@@ -128,12 +450,12 @@ export async function downloadFigureWord(item, mediaUrl, fileName) {
const error = new Error('NO_FIGURE');
throw error;
}
const doc = buildFigureWordDocument(item, mediaUrl);
const doc = await buildFigureWordDocument(item, mediaUrl);
const blob = await Packer.toBlob(doc);
saveAs(blob, `${sanitizeFileName(fileName || item.title, 'figure')}.docx`);
}
export async function downloadAllFiguresWord(items, mediaUrl, fileName) {
export async function downloadAllFiguresWord(items, mediaUrl, fileName, options) {
const exportItems = [];
(items || []).forEach((item) => {
if (prepareFigureExportItem(item, mediaUrl)) {
@@ -146,7 +468,7 @@ export async function downloadAllFiguresWord(items, mediaUrl, fileName) {
throw error;
}
const doc = buildAllFiguresWordDocument(exportItems, mediaUrl);
const doc = await buildAllFiguresWordDocument(exportItems, mediaUrl, options);
const blob = await Packer.toBlob(doc);
saveAs(blob, `${sanitizeFileName(fileName, 'figures')}.docx`);
}

View File

@@ -17,8 +17,15 @@ import {
import JSZip from 'jszip';
import { htmlToPlainText, TABLE_FONT_SIZE, FONT_KERN_MIN_1PT, FONT_KERN_MIN_XIAO_ER, FONT_NAME, PAGE_MARGINS, WORD_PARAGRAPH_SPACING } from '@/utils/exportTableWord';
import { formatAuthorFirstName, formatAuthorLastName } from '@/utils/productionSubmissionImport';
import { PAGE_HEIGHT_TWIPS, PAGE_WIDTH_TWIPS, getDocumentGridProperties } from '@/utils/exportWordLayout';
import {
PAGE_HEADER_MARGIN_TWIPS,
PAGE_FOOTER_MARGIN_TWIPS,
PAGE_HEIGHT_TWIPS,
PAGE_WIDTH_TWIPS,
getDocumentGridProperties
} from '@/utils/exportWordLayout';
import orcidIconUrl from '@/assets/img/orcid.png';
import { createTmrLogoImageRun } from '@/utils/exportManuscriptWordHeader';
/** 标题:阿里巴巴普惠体 H / Calibri四号 14pt */
const TITLE_FONT_EAST_ASIA = '阿里巴巴普惠体 H';
@@ -378,6 +385,25 @@ export async function fetchManuscriptHeaderData(apiClient, articleId, pArticleId
}
}
let stageInfo = null;
if (production && production.journal_id && production.journal_stage_id) {
const stagesRes = await withPromiseTimeout(
apiClient
.post('api/Typeset/getStagesOnline', { journal_id: production.journal_id })
.catch(function () {
return null;
}),
12000
);
if (stagesRes && stagesRes.code == 0 && stagesRes.data && stagesRes.data.stages) {
const targetStageId = production.journal_stage_id;
stageInfo =
stagesRes.data.stages.find(function (item) {
return String(item.journal_stage_id) === String(targetStageId);
}) || null;
}
}
if (!production && !authors.length && !previewAuthor) {
return {
pArticleId: resolvedId,
@@ -386,7 +412,8 @@ export async function fetchManuscriptHeaderData(apiClient, articleId, pArticleId
previewAuthor: null,
journal: journal,
organList: organList,
finalReview: finalReview
finalReview: finalReview,
stageInfo: stageInfo
};
}
@@ -397,7 +424,8 @@ export async function fetchManuscriptHeaderData(apiClient, articleId, pArticleId
previewAuthor: previewAuthor,
journal: journal,
organList: organList,
finalReview: finalReview
finalReview: finalReview,
stageInfo: stageInfo
};
}
@@ -884,7 +912,19 @@ function createAffiliationParagraph(index, addressText) {
});
}
function createAuthorTitleParagraph(title) {
function buildTitleParagraphChildren(contentRuns, logoImageData) {
const children = [];
const logoRun = logoImageData && createTmrLogoImageRun(logoImageData);
if (logoRun) {
children.push(logoRun);
}
if (contentRuns && contentRuns.length) {
children.push.apply(children, contentRuns);
}
return children;
}
function createAuthorTitleParagraph(title, logoImageData) {
const text = String(title || '').trim();
if (!text) {
return null;
@@ -896,41 +936,49 @@ function createAuthorTitleParagraph(title) {
widowControl: false,
overflowPunctuation: true,
autoSpaceEastAsianText: true,
children: [
createStyledTextRun(text, {
fontEastAsia: TITLE_FONT_EAST_ASIA,
fontAscii: TITLE_FONT_ASCII,
size: TITLE_FONT_SIZE,
bold: true,
kern: FONT_KERN_MIN_XIAO_ER
})
]
children: buildTitleParagraphChildren(
[
createStyledTextRun(text, {
fontEastAsia: TITLE_FONT_EAST_ASIA,
fontAscii: TITLE_FONT_ASCII,
size: TITLE_FONT_SIZE,
bold: true,
kern: FONT_KERN_MIN_XIAO_ER
})
],
logoImageData
)
});
}
function createMissingTitleParagraph() {
function createMissingTitleParagraph(logoImageData) {
return new Paragraph({
alignment: AlignmentType.JUSTIFIED,
spacing: TITLE_PARAGRAPH_SPACING,
widowControl: false,
overflowPunctuation: true,
autoSpaceEastAsianText: true,
children: [
createStyledTextRun(MISSING_DATA_PLACEHOLDER, {
fontEastAsia: TITLE_FONT_EAST_ASIA,
fontAscii: TITLE_FONT_ASCII,
size: TITLE_FONT_SIZE,
bold: true,
color: MISSING_DATA_COLOR,
kern: FONT_KERN_MIN_XIAO_ER
})
]
children: buildTitleParagraphChildren(
[
createStyledTextRun(MISSING_DATA_PLACEHOLDER, {
fontEastAsia: TITLE_FONT_EAST_ASIA,
fontAscii: TITLE_FONT_ASCII,
size: TITLE_FONT_SIZE,
bold: true,
color: MISSING_DATA_COLOR,
kern: FONT_KERN_MIN_XIAO_ER
})
],
logoImageData
)
});
}
function createManuscriptTitleParagraph(headerData) {
function createManuscriptTitleParagraph(headerData, logoImageData) {
const text = resolveManuscriptDisplayTitle(headerData);
return text ? createAuthorTitleParagraph(text) : createMissingTitleParagraph();
return text
? createAuthorTitleParagraph(text, logoImageData)
: createMissingTitleParagraph(logoImageData);
}
function stripParagraphPropertyTag(pPrInner, tagName) {
@@ -1623,7 +1671,8 @@ export function buildMinimalManuscriptHeaderData(pArticleId) {
previewAuthor: null,
journal: null,
organList: [],
finalReview: null
finalReview: null,
stageInfo: null
};
}
@@ -2148,6 +2197,96 @@ function patchMetadataTableBlock(tblBlock) {
return block;
}
function isHomePageSingleColumnSectPrInner(sectPrInner) {
return /<w:cols\b[^>]*\bw:num="1"/i.test(String(sectPrInner || '')) && !/<w:cols\b[^>]*\bw:num="2"/i.test(String(sectPrInner || ''));
}
function isHomePageDualColumnSectPrInner(sectPrInner) {
return /<w:cols\b[^>]*\bw:num="2"/i.test(String(sectPrInner || ''));
}
function replaceSectPrInnerColsWithSingle(sectPrInner) {
let next = String(sectPrInner || '')
.replace(/<w:cols\b[^>]*\/>/g, '')
.replace(/<w:cols\b[^>]*>[\s\S]*?<\/w:cols>/g, '');
return next + '<w:cols w:num="1"/>';
}
/** 首页元数据表格节结束位置:含表格后空行,不含正文双栏分节符 */
export function findHomePageMetadataTableSectionEndIndex(xml, bounds) {
if (!bounds) {
return -1;
}
let end = bounds.end;
let cursor = bounds.end;
const limit = Math.min(xml.length, bounds.end + 8000);
while (cursor < limit) {
const slice = xml.slice(cursor, limit);
const match = slice.match(/^\s*(<w:p\b[^>]*>[\s\S]*?<\/w:p>)/);
if (!match) {
break;
}
const block = match[1];
const blockEnd = cursor + match[0].length;
const sectMatch = block.match(/<w:sectPr>([\s\S]*?)<\/w:sectPr>/);
if (sectMatch && isHomePageDualColumnSectPrInner(sectMatch[1])) {
break;
}
end = blockEnd;
cursor = blockEnd;
if (sectMatch) {
break;
}
if (/\bIntroduction\b/i.test(block) || /<v:roundrect\b[^>]*fillcolor="#DEEBF7"/i.test(block)) {
break;
}
}
return end;
}
/** 强制首页元数据表格Abstract 等)所在节为单栏 */
export function patchHomePageMetadataTableSingleColumnToXml(xml) {
let bounds = findHomePageMetadataTableBounds(xml);
if (!bounds) {
return xml;
}
let result = xml;
const beforeTableSlice = result.slice(Math.max(0, bounds.start - 8000), bounds.start);
const hasSingleBreakBeforeTable = /<w:sectPr>[\s\S]*?<w:cols\b[^>]*\bw:num="1"/i.test(beforeTableSlice);
if (!hasSingleBreakBeforeTable) {
const breakXml = buildHomePageSectionBreakParagraphXml(buildHomePageContinuousSectPrInner());
result = result.slice(0, bounds.start) + breakXml + result.slice(bounds.start);
bounds = findHomePageMetadataTableBounds(result);
if (!bounds) {
return result;
}
}
const corrMatch = findCorrespondenceParagraphMatch(result);
const regionStart = corrMatch ? corrMatch.index + corrMatch[0].length : 0;
const tableSectionEnd = findHomePageMetadataTableSectionEndIndex(result, bounds);
return result.replace(/<w:sectPr>([\s\S]*?)<\/w:sectPr>/g, function (full, inner, offset) {
if (offset < regionStart || offset >= tableSectionEnd) {
return full;
}
if (isHomePageSingleColumnSectPrInner(inner)) {
return full;
}
return '<w:sectPr>' + replaceSectPrInnerColsWithSingle(inner) + '</w:sectPr>';
});
}
function buildHomePageContinuousSectPrInner() {
const grid = getDocumentGridProperties();
return (
@@ -2165,7 +2304,11 @@ function buildHomePageContinuousSectPrInner() {
PAGE_MARGINS.bottom +
'" w:left="' +
PAGE_MARGINS.left +
'" w:header="708" w:footer="708" w:gutter="0"/>' +
'" w:header="' +
PAGE_HEADER_MARGIN_TWIPS +
'" w:footer="' +
PAGE_FOOTER_MARGIN_TWIPS +
'" w:gutter="0"/>' +
'<w:pgNumType/>' +
'<w:cols w:num="1"/>' +
'<w:docGrid w:linePitch="' +
@@ -2219,9 +2362,9 @@ function patchSectPrBetweenCorrespondenceAndHomePageTable(xml) {
return xml;
}
const continuousSectPrInner = buildHomePageContinuousSectPrInner();
const singleSectPrInner = buildHomePageContinuousSectPrInner();
const between = xml.slice(start, tblIdx).replace(/<w:sectPr>[\s\S]*?<\/w:sectPr>/g, function () {
return '<w:sectPr>' + continuousSectPrInner + '</w:sectPr>';
return '<w:sectPr>' + singleSectPrInner + '</w:sectPr>';
});
return xml.slice(0, start) + between + xml.slice(tblIdx);
@@ -2249,11 +2392,11 @@ function patchCorrespondenceDoubleSectionBreaks(xml) {
return patchSectPrBetweenCorrespondenceAndHomePageTable(xml);
}
const continuousSectPrInner = buildHomePageContinuousSectPrInner();
const singleSectPrInner = buildHomePageContinuousSectPrInner();
const doubleBreakBlock =
buildHomePageSectionBreakParagraphXml(continuousSectPrInner) +
buildHomePageSectionBreakParagraphXml(singleSectPrInner) +
buildHomePageEmptyLineParagraphXml() +
buildHomePageSectionBreakParagraphXml(continuousSectPrInner);
buildHomePageSectionBreakParagraphXml(singleSectPrInner);
return xml.slice(0, start) + doubleBreakBlock + xml.slice(tblIdx);
}
@@ -2291,19 +2434,57 @@ export async function patchHomePageDoubleSectionBreaks(blob) {
});
}
function isHomePageMetadataTableBlock(block) {
const text = String(block || '');
return /Author/i.test(text) && /contributions/i.test(text) && /Abstract/i.test(text);
}
/** 线性扫描定位首页表格Abstract + Author contributions避免全文正则灾难性回溯 */
export function findHomePageMetadataTableBounds(xml) {
if (!xml || !/Author/i.test(xml) || !/contributions/i.test(xml) || !/Abstract/.test(xml)) {
return null;
}
const tblToken = '<w:tbl';
const closeTag = '</w:tbl>';
let searchFrom = 0;
while (searchFrom < xml.length) {
const tblStart = xml.indexOf(tblToken, searchFrom);
if (tblStart < 0) {
return null;
}
const tblEnd = xml.indexOf(closeTag, tblStart);
if (tblEnd < 0) {
return null;
}
const tblEndExclusive = tblEnd + closeTag.length;
const block = xml.slice(tblStart, tblEndExclusive);
if (isHomePageMetadataTableBlock(block)) {
return {
start: tblStart,
end: tblEndExclusive,
block: block
};
}
searchFrom = tblEndExclusive;
}
return null;
}
/** 补齐元数据表格Abstract + Author contributionsWord 表格属性(图五) */
export function patchMetadataTablePropertiesToXml(xml) {
if (!xml || !/Author/i.test(xml) || !/contributions/i.test(xml) || !/Abstract/.test(xml)) {
const bounds = findHomePageMetadataTableBounds(xml);
if (!bounds) {
return xml;
}
const match = xml.match(/<w:tbl\b[\s\S]*?>[\s\S]*?Author[\s\S]*?contributions[\s\S]*?Abstract[\s\S]*?<\/w:tbl>/i);
if (!match || match.index == null) {
return xml;
}
const patched = patchMetadataTableBlock(match[0]);
return xml.slice(0, match.index) + patched + xml.slice(match.index + match[0].length);
const patched = patchMetadataTableBlock(bounds.block);
return xml.slice(0, bounds.start) + patched + xml.slice(bounds.end);
}
/** 补齐元数据表格Abstract + Author contributionsWord 表格属性(图五) */
@@ -2329,7 +2510,7 @@ export async function patchMetadataTableProperties(blob) {
}
function buildHomePageTableSectionChildren(headerData) {
return [buildMetadataTable(headerData), createHeaderBlockEmptyLineParagraph(EMPTY_LINE_AFTER_AFFILIATION)];
return [buildMetadataTable(headerData)];
}
function buildMetadataSectionChildren(headerData) {
@@ -2340,14 +2521,18 @@ function buildMetadataColumnChildren(headerData) {
return buildMetadataSectionChildren(headerData);
}
function buildTitleBlockChildren(headerData, orcidImageData) {
function buildTitleBlockChildren(headerData, orcidImageData, tmrLogoImageData) {
const children = [];
const previewAuthor = headerData.previewAuthor || {};
const authors = headerData.authors || [];
const organIndexMap = buildOrganIndexMap(headerData.organList || []);
const addressList = getAddressList(headerData);
pushTitleBlockModule(children, createManuscriptTitleParagraph(headerData), EMPTY_LINE_AFTER_TITLE);
pushTitleBlockModule(
children,
createManuscriptTitleParagraph(headerData, tmrLogoImageData),
EMPTY_LINE_AFTER_TITLE
);
let authorParagraph = null;
if (authors.length) {
@@ -2392,12 +2577,12 @@ function buildTitleBlockChildren(headerData, orcidImageData) {
return children;
}
export function buildAuthorHeaderSectionGroups(headerData, orcidImageData) {
export function buildAuthorHeaderSectionGroups(headerData, orcidImageData, tmrLogoImageData) {
if (!headerData) {
return [];
}
const titleBlockChildren = buildTitleBlockChildren(headerData, orcidImageData);
const titleBlockChildren = buildTitleBlockChildren(headerData, orcidImageData, tmrLogoImageData);
if (!titleBlockChildren.length) {
return [];
}
@@ -2407,7 +2592,7 @@ export function buildAuthorHeaderSectionGroups(headerData, orcidImageData) {
return [];
}
/** 首页:标题块 → 2 个连续分节符(中间空行 ¶)→ 首页表格(栏)→ 连续分节符 → 正文双栏 */
/** 首页:标题块(单栏)→ 连续分节符 → 首页元数据表格(栏)→ 连续分节符 → 正文双栏 */
return [
{
columnCount: 1,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,546 @@
import {
AlignmentType,
Footer,
LineRuleType,
Paragraph,
TextRun
} from 'docx';
import { FONT_NAME, TABLE_FONT_SIZE } from '@/utils/exportTableWord';
import { PAGE_FOOTER_MARGIN_TWIPS } from '@/utils/exportWordLayout';
const PAGE_FOOTER_FONT_SIZE = TABLE_FONT_SIZE;
const PAGE_FOOTER_COLOR = '002060';
const PAGE_FOOTER_PAGE_NUMBER_COLOR = '000000';
const PAGE_FOOTER_SUBMIT_PREFIX = 'Submit a manuscript: ';
const PAGE_FOOTER_PAGE_NUMBER_SHAPE_NAME = 'page-footer-page-number';
/** 页码文本框:宽 0.52cm × 高 0.62cm(绝对值,不随文字缩放) */
const PAGE_FOOTER_PAGE_NUMBER_WIDTH_CM = 0.52;
const PAGE_FOOTER_PAGE_NUMBER_HEIGHT_CM = 0.62;
/** OOXML1cm = 360000 EMU与 H1 形状一致) */
const PAGE_FOOTER_PAGE_NUMBER_WIDTH_EMU = Math.round(PAGE_FOOTER_PAGE_NUMBER_WIDTH_CM * 360000);
const PAGE_FOOTER_PAGE_NUMBER_HEIGHT_EMU = Math.round(PAGE_FOOTER_PAGE_NUMBER_HEIGHT_CM * 360000);
/** 0.52cm → twips用于框内段落制表位WPS 按文字区算宽时撑满目标宽度) */
const PAGE_FOOTER_PAGE_NUMBER_WIDTH_TWIPS = Math.round((PAGE_FOOTER_PAGE_NUMBER_WIDTH_CM * 1440) / 2.54);
const DRAWINGML_NS_WP14 = 'http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing';
const PAGE_FOOTER_PAGE_NUMBER_DOC_PR_ID = 900001;
const DRAWINGML_NS_A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const DRAWINGML_NS_WP = 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing';
const DRAWINGML_NS_WPS = 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape';
const DRAWINGML_NS_WPS_SHAPE_URI = 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape';
function createPageFooterFontOptions() {
return {
ascii: FONT_NAME,
eastAsia: FONT_NAME,
hAnsi: FONT_NAME
};
}
function createPageFooterTextRun(text, overrides) {
return new TextRun(
Object.assign(
{
text: text,
font: createPageFooterFontOptions(),
size: PAGE_FOOTER_FONT_SIZE,
color: PAGE_FOOTER_COLOR
},
overrides || {}
)
);
}
function buildPageFooterPageNumberRunPropertiesXml() {
return (
'<w:rPr>' +
'<w:rFonts w:ascii="' +
FONT_NAME +
'" w:hAnsi="' +
FONT_NAME +
'" w:eastAsia="' +
FONT_NAME +
'" w:cs="' +
FONT_NAME +
'"/>' +
'<w:color w:val="' +
PAGE_FOOTER_PAGE_NUMBER_COLOR +
'"/>' +
'<w:sz w:val="' +
PAGE_FOOTER_FONT_SIZE +
'"/>' +
'<w:szCs w:val="' +
PAGE_FOOTER_FONT_SIZE +
'"/>' +
'</w:rPr>'
);
}
function createPageFooterFirstBlankLineParagraph() {
return new Paragraph({
alignment: AlignmentType.LEFT,
spacing: {
before: 0,
after: 0,
line: 240,
lineRule: LineRuleType.AUTO
},
children: [
new TextRun({
text: '',
font: createPageFooterFontOptions(),
size: PAGE_FOOTER_FONT_SIZE
})
]
});
}
function buildPageFooterBlankLineParagraphXml() {
return (
'<w:p>' +
'<w:pPr>' +
'<w:jc w:val="left"/>' +
'<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>' +
'</w:pPr>' +
'<w:r>' +
'<w:rPr>' +
'<w:rFonts w:ascii="' +
FONT_NAME +
'" w:hAnsi="' +
FONT_NAME +
'" w:eastAsia="' +
FONT_NAME +
'" w:cs="' +
FONT_NAME +
'"/>' +
'<w:sz w:val="' +
PAGE_FOOTER_FONT_SIZE +
'"/>' +
'<w:szCs w:val="' +
PAGE_FOOTER_FONT_SIZE +
'"/>' +
'</w:rPr>' +
'<w:t xml:space="preserve"></w:t>' +
'</w:r>' +
'</w:p>'
);
}
function isBlankFooterParagraphInner(pInner) {
if (/Submit a manuscript:/i.test(pInner) || /<w:drawing\b/.test(pInner)) {
return false;
}
const plainText = pInner.replace(/<[^>]+>/g, '').replace(/[\u200B-\u200D\uFEFF\s]/g, '');
return !plainText;
}
function ensurePageFooterBlankLineParagraph(xml) {
if (!/Submit a manuscript:/i.test(xml)) {
return xml;
}
if (/<w:ftr\b[^>]*>\s*<w:p\b[^>]*>[\s\S]*?Submit a manuscript:/i.test(xml)) {
return xml.replace(/(<w:ftr\b[^>]*>)/, '$1' + buildPageFooterBlankLineParagraphXml());
}
return xml.replace(
/(<w:ftr[\s\S]*?)<w:p\b[^>]*>([\s\S]*?)<\/w:p>(\s*<w:p\b[^>]*>[\s\S]*?Submit a manuscript:)/i,
function (_full, before, pInner, rest) {
if (!isBlankFooterParagraphInner(pInner)) {
return before + buildPageFooterBlankLineParagraphXml() + '<w:p>' + pInner + '</w:p>' + rest;
}
const fixedInner = injectPageFooterParagraphProperties(pInner);
return before + '<w:p>' + fixedInner + '</w:p>' + rest;
}
);
}
/** 页脚左侧文案,数据来自 getProductionPreview → journal.website */
export function resolveManuscriptPageFooterSubmitText(headerData) {
const journal = (headerData && headerData.journal) || {};
const website = String(journal.website || journal.url || journal.journal_url || '').trim();
if (!website) {
return PAGE_FOOTER_SUBMIT_PREFIX;
}
return PAGE_FOOTER_SUBMIT_PREFIX + website;
}
export function buildManuscriptPageFooter(headerData) {
const submitText = resolveManuscriptPageFooterSubmitText(headerData);
return new Footer({
children: [
createPageFooterFirstBlankLineParagraph(),
new Paragraph({
alignment: AlignmentType.LEFT,
spacing: {
before: 0,
after: 0,
line: 240,
lineRule: LineRuleType.AUTO
},
children: [createPageFooterTextRun(submitText)]
})
]
});
}
function buildPageFooterPageNumberFieldRunXml() {
return (
'<w:r>' +
buildPageFooterPageNumberRunPropertiesXml() +
'<w:fldChar w:fldCharType="begin"/>' +
'<w:instrText xml:space="preserve"> PAGE </w:instrText>' +
'<w:fldChar w:fldCharType="separate"/>' +
'<w:t>1</w:t>' +
'<w:fldChar w:fldCharType="end"/>' +
'</w:r>'
);
}
function buildPageFooterPageNumberWidthTabRunXml() {
return '<w:r>' + buildPageFooterPageNumberRunPropertiesXml() + '<w:tab/></w:r>';
}
function buildPageFooterPageNumberParagraphPrXml() {
return (
'<w:pPr>' +
'<w:jc w:val="left"/>' +
'<w:tabs>' +
'<w:tab w:val="left" w:leader="none" w:pos="' +
PAGE_FOOTER_PAGE_NUMBER_WIDTH_TWIPS +
'"/>' +
'</w:tabs>' +
'<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>' +
'<w:snapToGrid w:val="0"/>' +
'</w:pPr>'
);
}
function buildPageFooterPageNumberTxbxParagraphXml() {
return (
'<w:p>' +
buildPageFooterPageNumberParagraphPrXml() +
buildPageFooterPageNumberFieldRunXml() +
buildPageFooterPageNumberWidthTabRunXml() +
'</w:p>'
);
}
function buildPageFooterPageNumberSizeRelXml() {
return (
'<wp14:sizeRelH relativeFrom="margin">' +
'<wp14:pctWidth>0</wp14:pctWidth>' +
'</wp14:sizeRelH>' +
'<wp14:sizeRelV relativeFrom="margin">' +
'<wp14:pctHeight>0</wp14:pctHeight>' +
'</wp14:sizeRelV>'
);
}
function buildPageFooterPageNumberPositionHXml() {
return (
'<wp:positionH relativeFrom="margin">' +
'<wp:align>right</wp:align>' +
'</wp:positionH>'
);
}
function buildPageFooterPageNumberPositionVXml() {
return (
'<wp:positionV relativeFrom="paragraph">' +
'<wp:posOffset>0</wp:posOffset>' +
'</wp:positionV>'
);
}
function buildPageFooterPageNumberAnchorOpenXml() {
return (
'<wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0" relativeHeight="251658240" behindDoc="0" locked="0" layoutInCell="1" allowOverlap="1">'
);
}
function buildPageFooterPageNumberWpsShapeXml() {
const cx = PAGE_FOOTER_PAGE_NUMBER_WIDTH_EMU;
const cy = PAGE_FOOTER_PAGE_NUMBER_HEIGHT_EMU;
return (
'<wps:wsp xmlns:wps="' +
DRAWINGML_NS_WPS +
'">' +
'<wps:cNvSpPr txBox="1"/>' +
'<wps:spPr>' +
'<a:xfrm xmlns:a="' +
DRAWINGML_NS_A +
'">' +
'<a:off x="0" y="0"/>' +
'<a:ext cx="' +
cx +
'" cy="' +
cy +
'"/>' +
'</a:xfrm>' +
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' +
'<a:noFill/>' +
'<a:ln w="0"><a:noFill/></a:ln>' +
'</wps:spPr>' +
'<wps:txbx>' +
'<w:txbxContent>' +
buildPageFooterPageNumberTxbxParagraphXml() +
'</w:txbxContent>' +
'</wps:txbx>' +
'<wps:bodyPr rot="0" spcFirstLastPara="0" vertOverflow="overflow" horzOverflow="overflow" vert="horz" wrap="none" lIns="0" tIns="0" rIns="0" bIns="0" anchor="t" anchorCtr="0">' +
'<a:noAutofit xmlns:a="' +
DRAWINGML_NS_A +
'"/>' +
'</wps:bodyPr>' +
'</wps:wsp>'
);
}
/** 页码DrawingML 文本框wp:extent 固定 0.52×0.62cmWPS/Word 识别更稳定) */
function buildPageFooterPageNumberDrawingRunXml() {
const cx = PAGE_FOOTER_PAGE_NUMBER_WIDTH_EMU;
const cy = PAGE_FOOTER_PAGE_NUMBER_HEIGHT_EMU;
return (
'<w:r>' +
'<w:drawing>' +
buildPageFooterPageNumberAnchorOpenXml() +
'<wp:simplePos x="0" y="0"/>' +
buildPageFooterPageNumberPositionHXml() +
buildPageFooterPageNumberPositionVXml() +
'<wp:extent cx="' +
cx +
'" cy="' +
cy +
'"/>' +
buildPageFooterPageNumberSizeRelXml() +
'<wp:effectExtent l="0" t="0" r="0" b="0"/>' +
'<wp:wrapNone/>' +
'<wp:docPr id="' +
PAGE_FOOTER_PAGE_NUMBER_DOC_PR_ID +
'" name="' +
PAGE_FOOTER_PAGE_NUMBER_SHAPE_NAME +
'"/>' +
'<wp:cNvGraphicFramePr>' +
'<a:graphicFrameLocks xmlns:a="' +
DRAWINGML_NS_A +
'" noChangeAspect="0"/>' +
'</wp:cNvGraphicFramePr>' +
'<a:graphic xmlns:a="' +
DRAWINGML_NS_A +
'">' +
'<a:graphicData uri="' +
DRAWINGML_NS_WPS_SHAPE_URI +
'">' +
buildPageFooterPageNumberWpsShapeXml() +
'</a:graphicData>' +
'</a:graphic>' +
'</wp:anchor>' +
'</w:drawing>' +
'</w:r>'
);
}
function stripFooterParagraphIndentAndSpacing(inner) {
let next = inner;
next = next.replace(/<w:ind\b[^>]*\/>/g, '');
next = next.replace(/<w:ind\b[^>]*>[\s\S]*?<\/w:ind>/g, '');
next = next.replace(/<w:spacing\b[^>]*\/>/g, '');
next = next.replace(/<w:spacing\b[^>]*>[\s\S]*?<\/w:spacing>/g, '');
next = next.replace(/<w:adjustRightInd\b[^>]*\/>/g, '');
next = next.replace(/<w:snapToGrid\b[^>]*\/>/g, '');
next = next.replace(/<w:jc\b[^>]*\/>/g, '');
next = next.replace(/<w:jc\b[^>]*>[\s\S]*?<\/w:jc>/g, '');
next = next.replace(/<w:tabs\b[^>]*>[\s\S]*?<\/w:tabs>/g, '');
return next;
}
function injectPageFooterParagraphProperties(pInner) {
let inner = stripFooterParagraphIndentAndSpacing(pInner);
const inject =
'<w:jc w:val="left"/>' +
'<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>' +
'<w:snapToGrid w:val="1"/>';
if (/<w:pPr\b/.test(inner)) {
return inner.replace(/(<w:pPr[^>]*>)/, '$1' + inject);
}
return '<w:pPr>' + inject + '</w:pPr>' + inner;
}
function stripFooterInlinePageNumberRuns(inner) {
let next = inner.replace(/<w:tab\b[^>]*\/>/g, '');
next = next.replace(/<w:r>\s*<w:drawing>[\s\S]*?page-footer-page-number[\s\S]*?<\/w:drawing>\s*<\/w:r>/gi, '');
next = next.replace(/<w:r>\s*<w:pict>[\s\S]*?page-footer-page-number[\s\S]*?<\/w:pict>\s*<\/w:r>/gi, '');
next = next.replace(/<w:r\b[^>]*>[\s\S]*?<\/w:r>/g, function (runBlock) {
if (/<w:drawing\b/.test(runBlock) || /<w:pict\b/.test(runBlock)) {
return runBlock;
}
if (/<w:instrText[^>]*>[\s\S]*?PAGE/i.test(runBlock) || /<w:fldChar\b/.test(runBlock)) {
return '';
}
return runBlock;
});
return next;
}
function ensureFooterDrawingNamespaces(xml) {
let next = xml;
const namespaces = [
['xmlns:wp', DRAWINGML_NS_WP],
['xmlns:wp14', DRAWINGML_NS_WP14],
['xmlns:a', DRAWINGML_NS_A],
['xmlns:wps', DRAWINGML_NS_WPS]
];
namespaces.forEach(function (pair) {
if (!new RegExp(pair[0].replace(':', '\\:')).test(next)) {
next = next.replace(/<w:ftr\b/, '<w:ftr ' + pair[0] + '="' + pair[1] + '"');
}
});
return next;
}
function isPageFooterPageNumberShapeBlock(block) {
return new RegExp('name="' + PAGE_FOOTER_PAGE_NUMBER_SHAPE_NAME + '"').test(String(block || ''));
}
function patchPageFooterPageNumberAnchorBlock(anchorBlock) {
if (!isPageFooterPageNumberShapeBlock(anchorBlock)) {
return anchorBlock;
}
const cx = PAGE_FOOTER_PAGE_NUMBER_WIDTH_EMU;
const cy = PAGE_FOOTER_PAGE_NUMBER_HEIGHT_EMU;
let patched = anchorBlock;
patched = patched.replace(/<wp:anchor\b[^>]*>/, buildPageFooterPageNumberAnchorOpenXml());
patched = patched.replace(/<wp:positionH[\s\S]*?<\/wp:positionH>/g, buildPageFooterPageNumberPositionHXml());
patched = patched.replace(/<wp:positionV[\s\S]*?<\/wp:positionV>/g, buildPageFooterPageNumberPositionVXml());
patched = patched.replace(/<wp14:sizeRelH[\s\S]*?<\/wp14:sizeRelH>/g, '');
patched = patched.replace(/<wp14:sizeRelV[\s\S]*?<\/wp14:sizeRelV>/g, '');
patched = patched.replace(
/<wp:extent cx="[^"]+" cy="[^"]+"\/>/g,
'<wp:extent cx="' + cx + '" cy="' + cy + '"/>' + buildPageFooterPageNumberSizeRelXml()
);
patched = patched.replace(
/<a:ext cx="[^"]+" cy="[^"]+"\/>/g,
'<a:ext cx="' + cx + '" cy="' + cy + '"/>'
);
patched = patched.replace(/<wp:wrap(?:None|Square|Tight|TopAndBottom|Through)\b[^>]*\/>/g, '<wp:wrapNone/>');
patched = patched.replace(
/<wp:wrap(?:None|Square|Tight|TopAndBottom|Through)\b[^>]*>[\s\S]*?<\/wp:wrap(?:None|Square|Tight|TopAndBottom|Through)>/g,
'<wp:wrapNone/>'
);
patched = patched.replace(/<a:spAutoFit\b[^>]*\/>/g, '<a:noAutofit/>');
patched = patched.replace(/<a:spAutoFit\b[^>]*>[\s\S]*?<\/a:spAutoFit>/g, '<a:noAutofit/>');
if (!/<a:noAutofit\b/.test(patched)) {
patched = patched.replace(/<\/wps:bodyPr>/i, '<a:noAutofit/></wps:bodyPr>');
}
patched = patched.replace(
/(<w:txbxContent>)[\s\S]*?(<\/w:txbxContent>)/g,
function (_match, open, close) {
return open + buildPageFooterPageNumberTxbxParagraphXml() + close;
}
);
return patched;
}
function forcePageFooterPageNumberCanonicalRunXml(xml) {
let next = xml.replace(
/<w:r>\s*<w:pict>[\s\S]*?(?:page-footer-page-number|name="page-footer-page-number")[\s\S]*?<\/w:pict>\s*<\/w:r>/gi,
buildPageFooterPageNumberDrawingRunXml()
);
next = next.replace(
/<w:r>\s*<w:drawing>[\s\S]*?name="page-footer-page-number"[\s\S]*?<\/w:drawing>\s*<\/w:r>/gi,
buildPageFooterPageNumberDrawingRunXml()
);
return next;
}
/** 二次强制页码 DrawingML 文本框绝对尺寸wp:extent / a:ext */
export function applyPageFooterPageNumberAbsoluteSizeToXml(xml) {
if (!xml || !/<w:ftr\b/.test(xml) || !/page-footer-page-number/.test(xml)) {
return xml;
}
let next = forcePageFooterPageNumberCanonicalRunXml(xml);
next = next.replace(/<wp:anchor\b[\s\S]*?<\/wp:anchor>/gi, patchPageFooterPageNumberAnchorBlock);
return next;
}
/** 补丁页脚 XML左对齐投稿链接 + 浮于文字上方的页码文本框 */
export function patchManuscriptPageFootersToXml(xml) {
if (!xml || !/<w:ftr\b/.test(xml)) {
return xml;
}
xml = ensureFooterDrawingNamespaces(xml);
xml = ensurePageFooterBlankLineParagraph(xml);
xml = xml.replace(/<w:p\b[^>]*>([\s\S]*?)<\/w:p>/g, function (fullMatch, pInner) {
if (!/Submit a manuscript:/i.test(pInner)) {
return fullMatch;
}
let inner = stripFooterInlinePageNumberRuns(pInner);
inner = injectPageFooterParagraphProperties(inner);
if (!new RegExp('name="' + PAGE_FOOTER_PAGE_NUMBER_SHAPE_NAME + '"').test(inner)) {
inner = inner + buildPageFooterPageNumberDrawingRunXml();
}
if (inner === pInner) {
return fullMatch;
}
return fullMatch.replace(pInner, inner);
});
xml = forcePageFooterPageNumberCanonicalRunXml(xml);
return applyPageFooterPageNumberAbsoluteSizeToXml(xml);
}
/** 从 document.xml.rels 取 footer1.xml 的关系 id */
export function findPrimaryFooterRelationshipId(relsXml) {
if (!relsXml) {
return '';
}
const patterns = [
/<Relationship\b[^>]*\bId="([^"]+)"[^>]*\bType="[^"]*\/footer"[^>]*\bTarget="footer1\.xml"/i,
/<Relationship\b[^>]*\bTarget="footer1\.xml"[^>]*\bType="[^"]*\/footer"[^>]*\bId="([^"]+)"/i,
/<Relationship\b[^>]*\bType="[^"]*\/footer"[^>]*\bId="([^"]+)"/i
];
for (let i = 0; i < patterns.length; i++) {
const match = relsXml.match(patterns[i]);
if (match && match[1]) {
return match[1];
}
}
return '';
}
/** 所有分节统一引用同一 default 页脚 */
export function patchDocumentSectionFooterReferencesXml(xml, defaultFooterRelId) {
if (!xml || !defaultFooterRelId || !/<w:sectPr\b/.test(xml)) {
return xml;
}
const footerReferenceXml =
'<w:footerReference w:type="default" r:id="' + defaultFooterRelId + '"/>';
return xml.replace(/<w:sectPr\b[^>]*>([\s\S]*?)<\/w:sectPr>/g, function (full, inner) {
let next = inner;
next = next.replace(/<w:titlePg\b[^>]*\/>/g, '');
next = next.replace(/<w:footerReference\b[^>]*\/>/g, '');
return '<w:sectPr>' + footerReferenceXml + next + '</w:sectPr>';
});
}
/** 将所有分节的 w:pgMar w:footer 统一为 1.75cm */
export function patchDocumentFooterBottomMarginXml(xml) {
if (!xml || !/<w:pgMar\b/.test(xml)) {
return xml;
}
return xml.replace(/(<w:pgMar\b[^>]*\bw:footer=")([^"]+)(")/g, '$1' + PAGE_FOOTER_MARGIN_TWIPS + '$3');
}

View File

@@ -0,0 +1,637 @@
import {
AlignmentType,
ExternalHyperlink,
Header,
HorizontalPositionRelativeFrom,
ImageRun,
LineRuleType,
Paragraph,
TextRun,
TextWrappingType,
VerticalPositionRelativeFrom
} from 'docx';
import { FONT_NAME, TABLE_FONT_SIZE } from '@/utils/exportTableWord';
import { PAGE_HEADER_MARGIN_TWIPS } from '@/utils/exportWordLayout';
import tmrLogoUrl from '@/assets/img/tmr-logo.png';
/** 页眉第一行Calibri 五号 10.5pt */
const PAGE_HEADER_TYPE_FONT = 'Calibri';
const PAGE_HEADER_TYPE_FONT_SIZE = 21;
/** 页眉第二行Charis SIL 六号 7.5pt */
const PAGE_HEADER_CITATION_FONT_SIZE = TABLE_FONT_SIZE;
/** 页眉 REVIEW 等类型标签 rgb(0, 32, 96) */
const PAGE_HEADER_TYPE_COLOR = '002060';
/** 页眉引用行 rgb(89, 89, 89) */
const PAGE_HEADER_CITATION_COLOR = '595959';
/** TMR logo宽 1.76cm × 高 0.57cm,锁定纵横比 */
const TMR_LOGO_WIDTH_CM = 1.76;
const TMR_LOGO_HEIGHT_CM = 0.57;
const TMR_LOGO_WIDTH_EMU = Math.round(TMR_LOGO_WIDTH_CM * 360000);
const TMR_LOGO_HEIGHT_EMU = Math.round(TMR_LOGO_HEIGHT_CM * 360000);
/** docx ImageRunextent EMU = transformation 尺寸 × 9525 */
const DOCX_IMAGE_EMU_PER_UNIT = 9525;
/** TMR logo 位置Word 布局 → 位置):页面正文锚定,水平 0.01cm 栏,垂直 -1.14cm 段落下侧 */
const TMR_LOGO_H_OFFSET_CM = 0.01;
const TMR_LOGO_V_OFFSET_CM = -1.14;
const TMR_LOGO_H_OFFSET_EMU = Math.round(TMR_LOGO_H_OFFSET_CM * 360000);
const TMR_LOGO_V_OFFSET_EMU = Math.round(TMR_LOGO_V_OFFSET_CM * 360000);
/** 穿越型环绕:距正文左右 0.32cm */
const TMR_LOGO_WRAP_DIST_CM = 0.32;
const TMR_LOGO_WRAP_DIST_EMU = Math.round(TMR_LOGO_WRAP_DIST_CM * 360000);
const TMR_LOGO_DOC_PROPERTY_NAME = 'tmr-page-logo';
/** 文本之后 0.38 字符 → Word 存为 w:rightChars="38"1/100 字符) */
const PAGE_HEADER_RIGHT_CHARS = Math.round(0.38 * 100);
let tmrLogoImageCache = null;
function getProductionTypesetInfo(production) {
if (!production || typeof production !== 'object') {
return {};
}
return production.u_typeset_info || production.typeset_info || production;
}
function normalizeDoiSuffix(value) {
let text = String(value || '').trim();
if (!text) {
return '';
}
text = text.replace(/^https?:\/\/(dx\.)?doi\.org\//i, '');
text = text.replace(/^10\.53388\//i, '');
return text.replace(/^\/+/, '').replace(/\/+$/, '');
}
/** 页眉第二行文章类型Essential Information → Type如 REVIEW */
export function resolveManuscriptPageHeaderTypeText(headerData) {
const production = (headerData && headerData.production) || {};
const typeset = getProductionTypesetInfo(production);
const raw = String(
production.type ||
typeset.type ||
typeset.info_type ||
typeset.ts_type ||
production.info_type ||
production.ts_type ||
production.article_type ||
production.article_type_name ||
''
).trim();
if (!raw) {
return '';
}
return raw.toUpperCase();
}
/** 由分期 year/vol/issue + Number(npp) 拼装,如 2027;6(3):18 */
export function buildPageHeaderStageCitation(stageInfo, npp) {
if (!stageInfo || typeof stageInfo !== 'object') {
return '';
}
const year = String(stageInfo.stage_year != null ? stageInfo.stage_year : '').trim();
const vol = String(stageInfo.stage_vol != null ? stageInfo.stage_vol : '').trim();
const issue = String(stageInfo.stage_no != null ? stageInfo.stage_no : '').trim();
const number = String(npp != null && npp !== '' ? npp : '').trim();
if (!year || !vol || !issue || !number) {
return '';
}
return year + ';' + vol + '(' + issue + '):' + number;
}
function resolveManuscriptPageHeaderStageInfo(headerData, production) {
const stageInfo = (headerData && headerData.stageInfo) || null;
if (stageInfo && typeof stageInfo === 'object') {
return stageInfo;
}
if (
production &&
production.stage_year != null &&
production.stage_vol != null &&
production.stage_no != null
) {
return {
stage_year: production.stage_year,
stage_vol: production.stage_vol,
stage_no: production.stage_no
};
}
return null;
}
/** 页眉第二行引用,如 Medical Data Mining 2027;6(3):18. https://doi.org/... */
export function resolveManuscriptPageHeaderCitationParts(headerData) {
const production = (headerData && headerData.production) || {};
const journal = (headerData && headerData.journal) || {};
const typeset = getProductionTypesetInfo(production);
const stageInfo = resolveManuscriptPageHeaderStageInfo(headerData, production);
const builtStage = buildPageHeaderStageCitation(stageInfo, production.npp);
const fallbackStage = String(typeset.stage || production.stage || '').trim();
return {
journalTitle: String(journal.title || journal.jname || journal.name || '').trim(),
stage: builtStage || fallbackStage,
doiSuffix: normalizeDoiSuffix(typeset.ts_doi || typeset.doi || production.ts_doi || production.doi)
};
}
/** @deprecated 使用 resolveManuscriptPageHeaderCitationParts */
export function resolveManuscriptPageHeaderCitationText(headerData) {
const parts = resolveManuscriptPageHeaderCitationParts(headerData);
const segments = [];
if (parts.journalTitle && parts.stage) {
segments.push(parts.journalTitle + ' ' + parts.stage + '.');
} else if (parts.journalTitle) {
segments.push(parts.journalTitle + '.');
} else if (parts.stage) {
segments.push(parts.stage + '.');
}
if (parts.doiSuffix) {
segments.push('https://doi.org/10.53388/' + parts.doiSuffix);
}
return segments.join(' ');
}
function createPageHeaderFontOptions(eastAsia, ascii, size) {
return {
ascii: ascii || eastAsia,
eastAsia: eastAsia,
hAnsi: ascii || eastAsia
};
}
function getTmrLogoDocxTransformation() {
return {
width: TMR_LOGO_WIDTH_EMU / DOCX_IMAGE_EMU_PER_UNIT,
height: TMR_LOGO_HEIGHT_EMU / DOCX_IMAGE_EMU_PER_UNIT
};
}
/** 页眉首行:空行(回车),右对齐 */
function createPageHeaderFirstBlankLineParagraph() {
return new Paragraph({
alignment: AlignmentType.RIGHT,
spacing: {
before: 0,
after: 0,
line: 240,
lineRule: LineRuleType.AUTO
},
children: [
new TextRun({
text: '',
font: createPageHeaderFontOptions(PAGE_HEADER_TYPE_FONT, PAGE_HEADER_TYPE_FONT),
size: PAGE_HEADER_TYPE_FONT_SIZE
})
]
});
}
export function createTmrLogoImageRun(logoImageData) {
if (!logoImageData || !logoImageData.length) {
return null;
}
try {
return new ImageRun({
type: 'png',
data: logoImageData,
transformation: getTmrLogoDocxTransformation(),
altText: {
name: TMR_LOGO_DOC_PROPERTY_NAME,
title: 'TMR',
description: 'TMR page logo'
},
floating: {
horizontalPosition: {
relative: HorizontalPositionRelativeFrom.COLUMN,
offset: TMR_LOGO_H_OFFSET_EMU
},
verticalPosition: {
relative: VerticalPositionRelativeFrom.PARAGRAPH,
offset: TMR_LOGO_V_OFFSET_EMU
},
allowOverlap: true,
lockAnchor: false,
layoutInCell: true,
wrap: {
type: TextWrappingType.NONE
}
}
});
} catch (e) {
return null;
}
}
/** 页面级 TMR Logo锚定在正文首段浮动于文字流外非页眉 */
export function createTmrPageLogoAnchorParagraph(logoImageData) {
if (!logoImageData || !logoImageData.length) {
return null;
}
const logoRun = createTmrLogoImageRun(logoImageData);
if (!logoRun) {
return null;
}
return new Paragraph({
spacing: {
before: 0,
after: 0,
line: 240,
lineRule: LineRuleType.AUTO
},
children: [logoRun, new TextRun('')]
});
}
function createPageHeaderCitationTextRun(text, citationFont, overrides) {
return new TextRun(
Object.assign(
{
text: text,
font: citationFont,
size: PAGE_HEADER_CITATION_FONT_SIZE,
color: PAGE_HEADER_CITATION_COLOR
},
overrides || {}
)
);
}
function buildPageHeaderCitationRuns(headerData) {
const parts = resolveManuscriptPageHeaderCitationParts(headerData);
const citationFont = createPageHeaderFontOptions(FONT_NAME, FONT_NAME);
const runs = [];
if (parts.journalTitle) {
runs.push(
createPageHeaderCitationTextRun(parts.journalTitle, citationFont, {
bold: true,
italics: true
})
);
}
if (parts.journalTitle && parts.stage) {
runs.push(createPageHeaderCitationTextRun(' ' + parts.stage + '.', citationFont));
} else if (parts.stage) {
runs.push(createPageHeaderCitationTextRun(parts.stage + '.', citationFont));
} else if (parts.journalTitle) {
runs.push(createPageHeaderCitationTextRun('.', citationFont));
}
if (parts.doiSuffix) {
const doiUrl = 'https://doi.org/10.53388/' + parts.doiSuffix;
if (runs.length) {
runs.push(createPageHeaderCitationTextRun(' ', citationFont));
}
runs.push(
new ExternalHyperlink({
link: doiUrl,
children: [createPageHeaderCitationTextRun(doiUrl, citationFont)]
})
);
}
return runs;
}
/** 页眉第二行起:文章类型 + 软回车 + 引用(同一段落) */
function createPageHeaderTypeAndCitationParagraph(headerData, typeText) {
const typeLine = String(typeText || '').trim();
const citationRuns = buildPageHeaderCitationRuns(headerData);
if (!typeLine && !citationRuns.length) {
return null;
}
const children = [];
if (typeLine) {
children.push(
new TextRun({
text: typeLine,
font: createPageHeaderFontOptions(PAGE_HEADER_TYPE_FONT, PAGE_HEADER_TYPE_FONT),
size: PAGE_HEADER_TYPE_FONT_SIZE,
bold: true,
color: PAGE_HEADER_TYPE_COLOR
})
);
}
if (typeLine && citationRuns.length) {
children.push(new TextRun({ break: 1 }));
}
if (citationRuns.length) {
children.push.apply(children, citationRuns);
}
return new Paragraph({
alignment: AlignmentType.RIGHT,
spacing: {
before: 0,
after: 0,
line: 240,
lineRule: LineRuleType.AUTO
},
children: children
});
}
export function buildManuscriptPageHeader(headerData) {
const typeText = resolveManuscriptPageHeaderTypeText(headerData);
const children = [createPageHeaderFirstBlankLineParagraph()];
const typeAndCitationParagraph = createPageHeaderTypeAndCitationParagraph(headerData, typeText);
if (typeAndCitationParagraph) {
children.push(typeAndCitationParagraph);
}
return new Header({ children: children });
}
export async function loadTmrLogoImageData() {
if (tmrLogoImageCache) {
return tmrLogoImageCache;
}
if (typeof fetch === 'undefined') {
return null;
}
try {
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timer =
controller &&
setTimeout(function () {
controller.abort();
}, 8000);
const response = await fetch(tmrLogoUrl, controller ? { signal: controller.signal } : undefined);
if (timer) {
clearTimeout(timer);
}
if (!response.ok) {
return null;
}
const buffer = await response.arrayBuffer();
if (!buffer || !buffer.byteLength) {
return null;
}
tmrLogoImageCache = new Uint8Array(buffer);
return tmrLogoImageCache;
} catch (e) {
return null;
}
}
function stripPageHeaderParagraphIndentAndSpacing(inner) {
let next = inner;
next = next.replace(/<w:ind\b[^>]*\/>/g, '');
next = next.replace(/<w:ind\b[^>]*>[\s\S]*?<\/w:ind>/g, '');
next = next.replace(/<w:spacing\b[^>]*\/>/g, '');
next = next.replace(/<w:spacing\b[^>]*>[\s\S]*?<\/w:spacing>/g, '');
next = next.replace(/<w:adjustRightInd\b[^>]*\/>/g, '');
next = next.replace(/<w:kinsoku\b[^>]*\/>/g, '');
next = next.replace(/<w:overflowPunct\b[^>]*\/>/g, '');
return next;
}
function buildPageHeaderRightIndentXml() {
return (
'<w:ind w:rightChars="' +
PAGE_HEADER_RIGHT_CHARS +
'"/>' +
'<w:adjustRightInd w:val="1"/>'
);
}
function injectPageHeaderRightAlignParagraphProperties(pInner) {
let inner = stripPageHeaderParagraphIndentAndSpacing(pInner);
inner = inner.replace(/<w:jc\b[^>]*\/>/g, '');
inner = inner.replace(/<w:jc\b[^>]*>[\s\S]*?<\/w:jc>/g, '');
const inject =
'<w:jc w:val="right"/>' +
'<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>' +
buildPageHeaderRightIndentXml();
if (/<w:pPr\b/.test(inner)) {
return inner.replace(/(<w:pPr[^>]*>)/, '$1' + inject);
}
return '<w:pPr>' + inject + '</w:pPr>' + inner;
}
function injectPageHeaderRightIndentParagraphProperties(pInner) {
return injectPageHeaderRightAlignParagraphProperties(pInner);
}
function stripPageHeaderParagraphBorder(inner) {
let next = inner;
next = next.replace(/<w:pBdr\b[^>]*\/>/g, '');
next = next.replace(/<w:pBdr\b[^>]*>[\s\S]*?<\/w:pBdr>/g, '');
return next;
}
function injectPageHeaderCitationParagraphProperties(pInner) {
let inner = stripPageHeaderParagraphBorder(stripPageHeaderParagraphIndentAndSpacing(pInner));
inner = inner.replace(/<w:jc\b[^>]*\/>/g, '');
inner = inner.replace(/<w:jc\b[^>]*>[\s\S]*?<\/w:jc>/g, '');
const inject =
'<w:jc w:val="right"/>' +
'<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>' +
buildPageHeaderRightIndentXml() +
'<w:kinsoku w:val="1"/>' +
'<w:overflowPunct w:val="0"/>';
if (/<w:pPr\b/.test(inner)) {
return inner.replace(/(<w:pPr[^>]*>)/, '$1' + inject);
}
return '<w:pPr>' + inject + '</w:pPr>' + inner;
}
function ensurePicLocksNoChangeAspect(anchorBlock) {
if (/<pic:cNvPicPr[\s\S]*?noChangeAspect="1"/.test(anchorBlock)) {
return anchorBlock;
}
if (/<pic:cNvPicPr\b/.test(anchorBlock)) {
return anchorBlock.replace(/<pic:cNvPicPr\b([^>]*)>([\s\S]*?)<\/pic:cNvPicPr>/, function (full, attrs, inner) {
if (/<a:picLocks\b/.test(inner)) {
return full.replace(/<a:picLocks\b([^>]*)\/>/, function (lockTag, lockAttrs) {
if (/noChangeAspect/.test(lockAttrs)) {
return lockTag;
}
return '<a:picLocks' + lockAttrs + ' noChangeAspect="1"/>';
});
}
return (
'<pic:cNvPicPr' +
attrs +
'><a:picLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>' +
inner +
'</pic:cNvPicPr>'
);
});
}
return anchorBlock.replace(
/(<pic:nvPicPr>[\s\S]*?<\/pic:nvPicPr>)/,
'$1<pic:cNvPicPr><a:picLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></pic:cNvPicPr>'
);
}
function buildTmrLogoPositionHXml() {
return (
'<wp:positionH relativeFrom="column">' +
'<wp:posOffset>' +
TMR_LOGO_H_OFFSET_EMU +
'</wp:posOffset></wp:positionH>'
);
}
function buildTmrLogoPositionVXml() {
return (
'<wp:positionV relativeFrom="paragraph">' +
'<wp:posOffset>' +
TMR_LOGO_V_OFFSET_EMU +
'</wp:posOffset></wp:positionV>'
);
}
function buildTmrLogoWrapThroughXml() {
return (
'<wp:wrapThrough wrapText="bothSides">' +
'<wp:wrapPolygon edited="0">' +
'<wp:start x="0" y="0"/>' +
'<wp:lineTo x="0" y="21600"/>' +
'<wp:lineTo x="21600" y="21600"/>' +
'<wp:lineTo x="21600" y="0"/>' +
'<wp:lineTo x="0" y="0"/>' +
'</wp:wrapPolygon></wp:wrapThrough>'
);
}
function patchTmrPageLogoAnchorBlock(anchorBlock) {
if (!/<a:blip\b/.test(anchorBlock)) {
return anchorBlock;
}
if (!new RegExp('name="' + TMR_LOGO_DOC_PROPERTY_NAME + '"').test(anchorBlock)) {
return anchorBlock;
}
let patched = anchorBlock;
patched = patched.replace(/<wp:anchor\b[^>]*>/, function () {
return (
'<wp:anchor distT="0" distB="0" distL="' +
TMR_LOGO_WRAP_DIST_EMU +
'" distR="' +
TMR_LOGO_WRAP_DIST_EMU +
'" simplePos="0" allowOverlap="1" behindDoc="0" locked="0" layoutInCell="1" relativeHeight="251658240">'
);
});
patched = patched.replace(/<wp:positionH[\s\S]*?<\/wp:positionH>/g, buildTmrLogoPositionHXml());
patched = patched.replace(/<wp:positionV[\s\S]*?<\/wp:positionV>/g, buildTmrLogoPositionVXml());
patched = patched.replace(/<wp:wrap(?:None|Square|Tight|TopAndBottom|Through)\b[^>]*\/>/g, buildTmrLogoWrapThroughXml());
patched = patched.replace(
/<wp:wrap(?:None|Square|Tight|TopAndBottom|Through)\b[^>]*>[\s\S]*?<\/wp:wrap(?:None|Square|Tight|TopAndBottom|Through)>/g,
buildTmrLogoWrapThroughXml()
);
if (!/<wp:wrapThrough\b/.test(patched)) {
patched = patched.replace(/<wp:extent\b/, buildTmrLogoWrapThroughXml() + '<wp:extent');
}
patched = patched.replace(
/<wp:extent cx="[^"]+" cy="[^"]+"\/>/g,
'<wp:extent cx="' + TMR_LOGO_WIDTH_EMU + '" cy="' + TMR_LOGO_HEIGHT_EMU + '"/>'
);
patched = patched.replace(
/<a:ext cx="[^"]+" cy="[^"]+"\/>/g,
'<a:ext cx="' + TMR_LOGO_WIDTH_EMU + '" cy="' + TMR_LOGO_HEIGHT_EMU + '"/>'
);
patched = patched.replace(
/<a:graphicFrameLocks\b([^>]*)\/>/g,
'<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>'
);
patched = ensurePicLocksNoChangeAspect(patched);
return patched;
}
function patchTmrPageLogoDrawingsToDocumentXml(xml) {
if (!xml || !/<w:body\b/.test(xml)) {
return xml;
}
return xml.replace(/<wp:anchor\b[\s\S]*?<\/wp:anchor>/g, function (anchorBlock) {
return patchTmrPageLogoAnchorBlock(anchorBlock);
});
}
/** 补丁页眉 XML引用行右缩进 0.38 字符、单倍行距、kinsoku/overflowPunct */
export function patchManuscriptPageHeadersToXml(xml) {
if (!xml || !/<w:hdr\b/.test(xml)) {
return xml;
}
return xml.replace(/<w:p\b[^>]*>([\s\S]*?)<\/w:p>/g, function (fullMatch, pInner) {
const isCitation =
/https:\/\/doi\.org\//i.test(pInner) ||
(/<w:br\b/.test(pInner) && /<w:color w:val="002060"/.test(pInner));
const patchedInner = isCitation
? injectPageHeaderCitationParagraphProperties(pInner)
: injectPageHeaderRightIndentParagraphProperties(pInner);
if (patchedInner === pInner) {
return fullMatch;
}
return fullMatch.replace(pInner, patchedInner);
});
}
/** 将所有分节的 w:pgMar w:header 统一为 0.70cm */
export function patchDocumentHeaderTopMarginXml(xml) {
if (!xml || !/<w:pgMar\b/.test(xml)) {
return xml;
}
return xml.replace(/(<w:pgMar\b[^>]*\bw:header=")([^"]+)(")/g, '$1' + PAGE_HEADER_MARGIN_TWIPS + '$3');
}
/** 从 document.xml.rels 取 header1.xml 的关系 id */
export function findPrimaryHeaderRelationshipId(relsXml) {
if (!relsXml) {
return '';
}
const patterns = [
/<Relationship\b[^>]*\bId="([^"]+)"[^>]*\bType="[^"]*\/header"[^>]*\bTarget="header1\.xml"/i,
/<Relationship\b[^>]*\bTarget="header1\.xml"[^>]*\bType="[^"]*\/header"[^>]*\bId="([^"]+)"/i,
/<Relationship\b[^>]*\bType="[^"]*\/header"[^>]*\bId="([^"]+)"/i
];
for (let i = 0; i < patterns.length; i++) {
const match = relsXml.match(patterns[i]);
if (match && match[1]) {
return match[1];
}
}
return '';
}
/** 所有分节统一引用同一 default 页眉,并去掉首页/奇偶页差异 */
export function patchDocumentSectionHeaderReferencesXml(xml, defaultHeaderRelId) {
if (!xml || !defaultHeaderRelId || !/<w:sectPr\b/.test(xml)) {
return xml;
}
const headerReferenceXml =
'<w:headerReference w:type="default" r:id="' + defaultHeaderRelId + '"/>';
return xml.replace(/<w:sectPr\b[^>]*>([\s\S]*?)<\/w:sectPr>/g, function (full, inner) {
let next = inner;
next = next.replace(/<w:titlePg\b[^>]*\/>/g, '');
next = next.replace(/<w:headerReference\b[^>]*\/>/g, '');
return '<w:sectPr>' + headerReferenceXml + next + '</w:sectPr>';
});
}
/** 关闭奇偶页不同页眉,避免偶数页丢失页眉 */
export function patchDocumentSettingsEvenHeadersOff(settingsXml) {
if (!settingsXml) {
return settingsXml;
}
if (/<w:evenAndOddHeaders\b/.test(settingsXml)) {
return settingsXml.replace(/<w:evenAndOddHeaders\b[^>]*\/>/g, '<w:evenAndOddHeaders w:val="0"/>');
}
return settingsXml.replace(/<w:settings\b([^>]*)>/, '<w:settings$1><w:evenAndOddHeaders w:val="0"/>');
}

View File

@@ -186,28 +186,105 @@ function applyTableCaptionCenterParagraphPr(pInner) {
return '<w:pPr><w:jc w:val="center"/></w:pPr>' + pInner;
}
/** 从 tbl 前向前收集紧邻的 w:p避免全文正则灾难性回溯 */
function collectParagraphBlocksImmediatelyBefore(xml, tblIdx, minIndex) {
const blocks = [];
let end = tblIdx;
while (end > minIndex && blocks.length < 8) {
let ws = end;
while (ws > minIndex && /\s/.test(xml.charAt(ws - 1))) {
ws -= 1;
}
if (ws <= minIndex) {
break;
}
const closeIdx = xml.lastIndexOf('</w:p>', ws - 1);
if (closeIdx < minIndex) {
break;
}
const openIdx = xml.lastIndexOf('<w:p', closeIdx);
if (openIdx < minIndex) {
break;
}
const raw = xml.slice(openIdx, ws);
if (!/<\/w:p>\s*$/i.test(raw)) {
break;
}
blocks.unshift({
start: openIdx,
raw: raw,
inner: raw.replace(/^<w:p\b[^>]*>/i, '').replace(/<\/w:p>\s*$/i, '')
});
end = openIdx;
}
return blocks;
}
function isHomePageMetadataTableBlock(block) {
const text = String(block || '');
return /Author/i.test(text) && /contributions/i.test(text) && /Abstract/i.test(text);
}
/** 正文表格标题Table N…紧挨 w:tbl 前的段落强制居中,不含首页元数据表格 */
export function patchBodyTableCaptionCenterToXml(xml) {
return xml.replace(/((?:<w:p\b[^>]*>[\s\S]*?<\/w:p>\s*)+)(\s*<w:tbl\b)/gi, function (match, paragraphGroup, tblOpen) {
const blocks = paragraphGroup.match(/<w:p\b[^>]*>[\s\S]*?<\/w:p>/gi) || [];
if (!blocks.length) {
return match;
if (!xml || xml.indexOf('<w:tbl') < 0) {
return xml;
}
const parts = [];
let cursor = 0;
const tblToken = '<w:tbl';
const closeTag = '</w:tbl>';
while (cursor < xml.length) {
const tblIdx = xml.indexOf(tblToken, cursor);
if (tblIdx < 0) {
parts.push(xml.slice(cursor));
break;
}
const firstText = extractTableCaptionParagraphPlainText(
blocks[0].replace(/^<w:p\b[^>]*>/, '').replace(/<\/w:p>$/, '')
);
if (!isBodyTableCaptionPlainText(firstText)) {
return match;
const tblEnd = xml.indexOf(closeTag, tblIdx);
if (tblEnd < 0) {
parts.push(xml.slice(cursor));
break;
}
const centered = blocks.map(function (block) {
return block.replace(/(<w:p\b[^>]*>)([\s\S]*?)(<\/w:p>)/, function (_full, open, inner, close) {
return open + applyTableCaptionCenterParagraphPr(inner) + close;
const tblEndExclusive = tblEnd + closeTag.length;
const tblBlock = xml.slice(tblIdx, tblEndExclusive);
if (isHomePageMetadataTableBlock(tblBlock)) {
parts.push(xml.slice(cursor, tblEndExclusive));
cursor = tblEndExclusive;
continue;
}
const blocks = collectParagraphBlocksImmediatelyBefore(xml, tblIdx, cursor);
const isCaption =
blocks.length &&
isBodyTableCaptionPlainText(extractTableCaptionParagraphPlainText(blocks[0].inner));
if (isCaption) {
parts.push(xml.slice(cursor, blocks[0].start));
blocks.forEach(function (entry) {
parts.push(
entry.raw.replace(/(<w:p\b[^>]*>)([\s\S]*?)(<\/w:p>)/i, function (_full, open, inner, close) {
return open + applyTableCaptionCenterParagraphPr(inner) + close;
})
);
});
});
return centered.join('') + tblOpen;
});
const afterBlocks = blocks[blocks.length - 1].start + blocks[blocks.length - 1].raw.length;
parts.push(xml.slice(afterBlocks, tblIdx));
} else {
parts.push(xml.slice(cursor, tblIdx));
}
parts.push(tblBlock);
cursor = tblEndExclusive;
}
return parts.join('');
}
/** 导出后处理:除首页表格外,正文表格标题居中 */
@@ -248,11 +325,17 @@ function ensureFigureTitleTrailingPeriod(text) {
return trimmed + '.';
}
function createFigureTitleParagraph(children) {
return createWordParagraph(children, { alignment: AlignmentType.JUSTIFIED });
function createFigureTitleParagraph(children, alignment) {
return createWordParagraph(children, { alignment: alignment || AlignmentType.CENTER });
}
function appendFigureTitleParagraphs(children, titleHtml) {
function hasFigureAnnotation(noteHtml) {
return !!htmlToPlainText(String(noteHtml || '')).trim();
}
function appendFigureTitleParagraphs(children, titleHtml, options) {
const opts = options || {};
const alignment = hasFigureAnnotation(opts.note) ? AlignmentType.JUSTIFIED : AlignmentType.CENTER;
const segments = splitHtmlSegments(titleHtml)
.map(function (segment) {
return htmlToPlainText(segment).trim();
@@ -266,16 +349,19 @@ function appendFigureTitleParagraphs(children, titleHtml) {
const text =
index === segments.length - 1 ? ensureFigureTitleTrailingPeriod(segment) : segment;
children.push(
createFigureTitleParagraph([
new TextRun({
text: text,
bold: true,
color: TITLE_COLOR,
font: FONT_NAME,
size: TITLE_FONT_SIZE,
kern: FONT_KERN_MIN_1PT
})
])
createFigureTitleParagraph(
[
new TextRun({
text: text,
bold: true,
color: TITLE_COLOR,
font: FONT_NAME,
size: TITLE_FONT_SIZE,
kern: FONT_KERN_MIN_1PT
})
],
alignment
)
);
});
}
@@ -771,6 +857,28 @@ function buildTableWordChildren(processedItem, options) {
return children;
}
/** 稿件导出:标题 + 表格 + 表注均为单栏(分节符在标题前 / 表前,双栏恢复在表注后 / 表后) */
export function buildTableWordManuscriptParts(processedItem, options) {
const blockChildren = buildTableWordChildren(processedItem, options);
const tableIndex = blockChildren.findIndex(function (child) {
return child instanceof Table;
});
if (tableIndex < 0) {
return {
titleChildren: blockChildren,
tableChildren: [],
noteChildren: []
};
}
return {
titleChildren: blockChildren.slice(0, tableIndex),
tableChildren: blockChildren.slice(tableIndex, tableIndex + 1),
noteChildren: blockChildren.slice(tableIndex + 1)
};
}
export function buildTableWordDocument(processedItem) {
return createWordDocument(buildTableWordChildren(processedItem));
}

View File

@@ -5,6 +5,10 @@ import { PAGE_MARGINS, WORD_PARAGRAPH_SPACING } from '@/utils/exportTableWord';
export const PAGE_WIDTH_TWIPS = 11906;
export const PAGE_HEIGHT_TWIPS = 16838;
export const DUAL_COLUMN_COUNT = 2;
/** 页眉上边距:页顶到页眉区域 0.70cm */
export const PAGE_HEADER_MARGIN_TWIPS = Math.round((0.7 * 1440) / 2.54);
/** 页脚下边距:页底到页脚区域 1.75cm */
export const PAGE_FOOTER_MARGIN_TWIPS = Math.round((1.75 * 1440) / 2.54);
/**
* Word 分栏对话框(两栏预设):
@@ -19,7 +23,8 @@ export const MANUSCRIPT_DUAL_COLUMN = {
equalWidth: false
};
function getTextAreaWidthTwips() {
/** 版心宽度twips页宽 左右页边距,页脚右制表位与此对齐 */
export function getTextAreaWidthTwips() {
return PAGE_WIDTH_TWIPS - PAGE_MARGINS.left - PAGE_MARGINS.right;
}
@@ -86,7 +91,10 @@ export function createWordSectionProperties(columnCount, isFirstSection) {
width: PAGE_WIDTH_TWIPS,
height: PAGE_HEIGHT_TWIPS
},
margin: PAGE_MARGINS
margin: Object.assign({}, PAGE_MARGINS, {
header: PAGE_HEADER_MARGIN_TWIPS,
footer: PAGE_FOOTER_MARGIN_TWIPS
})
},
grid: getDocumentGridProperties()
};
@@ -128,6 +136,37 @@ export function buildDualColumnSectPrColsXml() {
);
}
/** 连续分节符:正文表格单栏 */
export function buildSingleColumnContinuousSectPrInner() {
const grid = getDocumentGridProperties();
return (
'<w:type w:val="continuous"/>' +
'<w:pgSz w:w="' +
PAGE_WIDTH_TWIPS +
'" w:h="' +
PAGE_HEIGHT_TWIPS +
'" w:orient="portrait"/>' +
'<w:pgMar w:top="' +
PAGE_MARGINS.top +
'" w:right="' +
PAGE_MARGINS.right +
'" w:bottom="' +
PAGE_MARGINS.bottom +
'" w:left="' +
PAGE_MARGINS.left +
'" w:header="' +
PAGE_HEADER_MARGIN_TWIPS +
'" w:footer="' +
PAGE_FOOTER_MARGIN_TWIPS +
'" w:gutter="0"/>' +
'<w:pgNumType/>' +
'<w:cols w:num="1"/>' +
'<w:docGrid w:linePitch="' +
grid.linePitch +
'"/>'
);
}
/** 连续分节符:恢复正文双栏 */
export function buildDualColumnContinuousSectPrInner() {
const grid = getDocumentGridProperties();
@@ -146,7 +185,11 @@ export function buildDualColumnContinuousSectPrInner() {
PAGE_MARGINS.bottom +
'" w:left="' +
PAGE_MARGINS.left +
'" w:header="708" w:footer="708" w:gutter="0"/>' +
'" w:header="' +
PAGE_HEADER_MARGIN_TWIPS +
'" w:footer="' +
PAGE_FOOTER_MARGIN_TWIPS +
'" w:gutter="0"/>' +
'<w:pgNumType/>' +
buildDualColumnSectPrColsXml() +
'<w:docGrid w:linePitch="' +

View File

@@ -75,10 +75,8 @@ export function applyMailTemplateVariableMocks(html, options) {
});
}
/** 复审通知邮件模板变量标题、SN、期刊名称、期刊官网 */
export function applyReviewNotifyTemplateVariables(html, options) {
function buildReviewNotifyVariableMap(options) {
const opts = options || {};
if (!html) return '';
const title = opts.title != null ? String(opts.title) : '';
const acceptSn =
opts.accept_sn != null && opts.accept_sn !== ''
@@ -88,7 +86,10 @@ export function applyReviewNotifyTemplateVariables(html, options) {
: '';
const journalName = opts.journal_name != null ? String(opts.journal_name) : '';
const journalUrl = opts.journal_url != null ? String(opts.journal_url) : '';
const map = {
const articleId = opts.article_id != null ? String(opts.article_id) : '';
const name = opts.name != null ? String(opts.name) : '';
const finalRId = opts.final_r_id != null ? String(opts.final_r_id) : '';
return {
title,
article_title: title,
manuscript_title: title,
@@ -96,8 +97,14 @@ export function applyReviewNotifyTemplateVariables(html, options) {
article_sn: acceptSn,
sn: acceptSn,
journal_name: journalName,
journal_url: journalUrl
journal_url: journalUrl,
article_id: articleId,
name,
final_r_id: finalRId
};
}
function replaceReviewNotifyCurlyPlaceholders(html, map) {
return String(html).replace(/\{\{\s*([^}]+?)\s*\}\}/g, (full, rawKey) => {
const key = String(rawKey).trim();
if (!key) return full;
@@ -107,3 +114,146 @@ export function applyReviewNotifyTemplateVariables(html, options) {
return full;
});
}
function replaceReviewNotifyBracketPlaceholders(html, map) {
return String(html).replace(/\[\[\s*([^\]]+?)\s*\]\]/g, (full, rawKey) => {
const key = String(rawKey).trim();
if (!key) return full;
if (Object.prototype.hasOwnProperty.call(map, key)) {
return map[key];
}
return full;
});
}
/** 复审/终审通知邮件模板变量标题、SN、article_id、终审人 name、final_review.id 等 */
export function applyReviewNotifyTemplateVariables(html, options) {
if (!html) return '';
const map = buildReviewNotifyVariableMap(options);
let next = replaceReviewNotifyCurlyPlaceholders(html, map);
next = replaceReviewNotifyBracketPlaceholders(next, map);
return next;
}
export function hasReviewNotifyTemplatePlaceholders(html) {
const text = String(html || '');
if (!text) return false;
if (/\{\{\s*(article_title|article_sn|accept_sn|title|sn|manuscript_title|article_id|final_r_id|name|journal_name|journal_url)\s*\}\}/i.test(text)) {
return true;
}
return /\[\[\s*(name|article_id|final_r_id|title|accept_sn|article_sn|sn)\s*\]\]/i.test(text);
}
const REVIEW_NOTIFY_PLACEHOLDER_ORDER = [
'name',
'article_id',
'final_r_id',
'article_title',
'title',
'manuscript_title',
'accept_sn',
'article_sn',
'sn',
'journal_name',
'journal_url'
];
/** 同一稿件下所有收件人相同的变量 */
export const REVIEW_NOTIFY_SHARED_VAR_KEYS = [
'article_id',
'article_title',
'title',
'manuscript_title',
'accept_sn',
'article_sn',
'sn',
'journal_name',
'journal_url'
];
/** 每位收件人各自不同的变量 */
export const REVIEW_NOTIFY_PER_RECIPIENT_VAR_KEYS = ['name', 'final_r_id'];
/** 左侧公共变量面板展示 */
export const REVIEW_NOTIFY_SHARED_DISPLAY_VAR_KEYS = [
'article_id',
'article_title',
'title',
'manuscript_title',
'accept_sn',
'article_sn',
'sn',
'journal_name',
'journal_url'
];
/** 左侧每人不同表格展示name 已在收件人列展示,不参与表格列) */
export const REVIEW_NOTIFY_PER_RECIPIENT_DISPLAY_VAR_KEYS = ['final_r_id'];
export function isReviewNotifySharedVarKey(key) {
return REVIEW_NOTIFY_SHARED_VAR_KEYS.indexOf(String(key || '').trim()) >= 0;
}
export function isReviewNotifyPerRecipientVarKey(key) {
return REVIEW_NOTIFY_PER_RECIPIENT_VAR_KEYS.indexOf(String(key || '').trim()) >= 0;
}
export function isReviewNotifySharedDisplayVarKey(key) {
return REVIEW_NOTIFY_SHARED_DISPLAY_VAR_KEYS.indexOf(String(key || '').trim()) >= 0;
}
export function isReviewNotifyPerRecipientDisplayVarKey(key) {
return REVIEW_NOTIFY_PER_RECIPIENT_DISPLAY_VAR_KEYS.indexOf(String(key || '').trim()) >= 0;
}
export function splitReviewNotifyTemplatePlaceholderKeys(keys) {
const list = Array.isArray(keys) ? keys : [];
const shared = [];
const perRecipient = [];
const sharedDisplay = [];
const perRecipientDisplay = [];
list.forEach(function (key) {
if (isReviewNotifyPerRecipientVarKey(key)) {
perRecipient.push(key);
if (isReviewNotifyPerRecipientDisplayVarKey(key)) {
perRecipientDisplay.push(key);
}
} else if (isReviewNotifySharedVarKey(key)) {
shared.push(key);
if (isReviewNotifySharedDisplayVarKey(key)) {
sharedDisplay.push(key);
}
} else {
shared.push(key);
sharedDisplay.push(key);
}
});
return {
shared: shared,
perRecipient: perRecipient,
sharedDisplay: sharedDisplay,
perRecipientDisplay: perRecipientDisplay
};
}
/** 从主题/正文提取模板占位符 key去重、按常用顺序排列 */
export function extractReviewNotifyTemplatePlaceholderKeys(html) {
const text = String(html || '');
const keys = new Set();
text.replace(/\{\{\s*([^}]+?)\s*\}\}/g, function (_full, rawKey) {
const key = String(rawKey).trim();
if (key) keys.add(key);
});
text.replace(/\[\[\s*([^\]]+?)\s*\]\]/g, function (_full, rawKey) {
const key = String(rawKey).trim();
if (key) keys.add(key);
});
const ordered = [];
REVIEW_NOTIFY_PLACEHOLDER_ORDER.forEach(function (key) {
if (keys.has(key)) ordered.push(key);
});
keys.forEach(function (key) {
if (ordered.indexOf(key) < 0) ordered.push(key);
});
return ordered;
}

View File

@@ -0,0 +1,229 @@
/** blue 内参考文献标号 [1] / [1, 2] */
const CITATION_BLUE_PATTERN = /\[\d/;
function isCitationBlueElement(el) {
return (
el &&
el.nodeType === Node.ELEMENT_NODE &&
el.tagName.toLowerCase() === 'blue' &&
CITATION_BLUE_PATTERN.test(el.textContent || '')
);
}
function isSpacingTextNode(node) {
if (!node || node.nodeType !== Node.TEXT_NODE) {
return false;
}
return /^[\s\u00a0]+$/.test(node.nodeValue);
}
/** 引用前的正文节点(跳过左侧空白) */
function getTextNodeBeforeCitation(blueEl) {
let node = blueEl.previousSibling;
if (node && isSpacingTextNode(node)) {
node = node.previousSibling;
}
if (node && node.nodeType === Node.TEXT_NODE) {
return node;
}
return null;
}
/** 去掉引用前正文末尾的 , . 及空白,并记录是否含逗号/句号 */
function stripTrailingCitationPunctuationFromTextNode(textNode) {
let val = textNode.nodeValue;
let changed = false;
let hadComma = false;
let hadPeriod = false;
while (val.length) {
const match = val.match(/^(.*?)([,.])([\s\u00a0]*)$/);
if (!match) {
break;
}
if (match[2] === ',') {
hadComma = true;
}
if (match[2] === '.') {
hadPeriod = true;
}
val = match[1];
changed = true;
}
if (changed) {
textNode.nodeValue = val;
}
return { changed: changed, hadComma: hadComma, hadPeriod: hadPeriod };
}
function stripPunctuationBeforeCitation(parent, blueEl) {
const textNode = getTextNodeBeforeCitation(blueEl);
if (!textNode) {
return { changed: false, hadComma: false, hadPeriod: false };
}
const result = stripTrailingCitationPunctuationFromTextNode(textNode);
if (textNode.nodeValue === '') {
parent.removeChild(textNode);
}
return result;
}
function removeSpacingAfterBlue(parent, blueEl) {
let changed = false;
let next = blueEl.nextSibling;
while (next && isSpacingTextNode(next)) {
parent.removeChild(next);
changed = true;
next = blueEl.nextSibling;
}
return changed;
}
/** 左侧保留一个 &nbsp; */
function ensureLeftNbspBeforeBlue(parent, blueEl) {
const prev = blueEl.previousSibling;
if (prev && isSpacingTextNode(prev)) {
if (prev.nodeValue !== '\u00a0') {
prev.nodeValue = '\u00a0';
return true;
}
return false;
}
parent.insertBefore(document.createTextNode('\u00a0'), blueEl);
return true;
}
/** 解析 blue 后已有标点:逗号、句号及后续正文 */
function parsePunctuationAfterBlue(val) {
let s = String(val || '').replace(/^[\s\u00a0]+/, '');
let hadComma = false;
let hadPeriod = false;
if (s.charAt(0) === ',') {
hadComma = true;
s = s.slice(1).replace(/^[\s\u00a0]+/, '');
}
const periodMatch = s.match(/^(\.+)([\s\S]*)$/);
let rest = '';
if (periodMatch) {
hadPeriod = true;
rest = (periodMatch[2] || '').replace(/^[\s\u00a0]+/, '');
} else {
rest = s;
}
return { hadComma: hadComma, hadPeriod: hadPeriod, rest: rest };
}
/**
* blue 后标点:保留逗号;, 与 . 后若有正文均加普通空格。
* 有逗号有句号:,. rest
* 仅逗号:, rest
* 仅句号:. rest
*/
function buildCitationSuffix(hadComma, hadPeriod, rest) {
rest = String(rest || '').replace(/^[\s\u00a0]+/, '');
let suffix = '';
if (hadComma) {
suffix += ',';
}
if (hadPeriod) {
suffix += '.';
}
if (rest && (hadComma || hadPeriod)) {
suffix += ' ' + rest;
}
return suffix;
}
function applyCitationSuffixAfterBlue(parent, blueEl, hadComma, hadPeriod, rest) {
if (!hadComma && !hadPeriod) {
return false;
}
const suffix = buildCitationSuffix(hadComma, hadPeriod, rest);
const next = blueEl.nextSibling;
if (next && next.nodeType === Node.TEXT_NODE) {
if (next.nodeValue !== suffix) {
next.nodeValue = suffix;
return true;
}
return false;
}
parent.insertBefore(document.createTextNode(suffix), blueEl.nextSibling);
return true;
}
function moveOneCitationBeforePeriod(blueEl) {
const parent = blueEl.parentNode;
if (!parent) {
return false;
}
let changed = false;
const before = stripPunctuationBeforeCitation(parent, blueEl);
if (before.changed) {
changed = true;
}
if (removeSpacingAfterBlue(parent, blueEl)) {
changed = true;
}
if (ensureLeftNbspBeforeBlue(parent, blueEl)) {
changed = true;
}
let hadComma = before.hadComma;
let hadPeriod = before.hadPeriod;
let rest = '';
const next = blueEl.nextSibling;
if (next && next.nodeType === Node.TEXT_NODE) {
const parsed = parsePunctuationAfterBlue(next.nodeValue);
hadComma = hadComma || parsed.hadComma;
hadPeriod = hadPeriod || parsed.hadPeriod;
rest = parsed.rest;
}
if (applyCitationSuffixAfterBlue(parent, blueEl, hadComma, hadPeriod, rest)) {
changed = true;
}
return changed;
}
/**
* 将 blue 参考文献挪到 , . 之前;保留逗号,句号后若有正文则加空格。
* 目标text&nbsp;<blue>[1, 2]</blue>,. Following text
* text&nbsp;<blue>[1, 2]</blue>, following text
* @returns {{ html: string, count: number }}
*/
export function moveBlueCitationsBeforePeriodHtml(html) {
if (!html || typeof document === 'undefined') {
return { html: html || '', count: 0 };
}
const container = document.createElement('div');
container.innerHTML = String(html);
const blues = Array.from(container.querySelectorAll('blue')).filter(isCitationBlueElement);
let count = 0;
for (let i = blues.length - 1; i >= 0; i--) {
if (moveOneCitationBeforePeriod(blues[i])) {
count += 1;
}
}
return {
html: container.innerHTML,
count: count
};
}

View File

@@ -0,0 +1,81 @@
/**
* 标题行首序号1 / 1.1 / 1.2.3 / 1. / 1) 等(不含 2024 这类四位年份)
*/
const HEADING_NUMBER_PREFIX_RE =
/^\s*(?:(?:\d+\.)+\d+|\d{1,2}[\.\):]|\d{1,2}(?=\s+[^\d]))[\.\):]?\s+/;
export function stripHeadingNumberPrefix(text) {
return String(text || '').replace(HEADING_NUMBER_PREFIX_RE, '');
}
export function hasHeadingNumberPrefix(text) {
return HEADING_NUMBER_PREFIX_RE.test(String(text || ''));
}
function htmlToPlainText(html) {
if (typeof document !== 'undefined') {
const node = document.createElement('div');
node.innerHTML = String(html || '');
return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ');
}
return String(html || '')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
export function headingHtmlHasNumberPrefix(html) {
return hasHeadingNumberPrefix(htmlToPlainText(html));
}
/** 去掉 HTML 标题内容中首个非空文本节点前的序号 */
export function stripHeadingNumberFromHtml(html) {
if (typeof document === 'undefined') {
const plain = htmlToPlainText(html);
if (!hasHeadingNumberPrefix(plain)) {
return html;
}
return String(html || '').replace(HEADING_NUMBER_PREFIX_RE, '');
}
const div = document.createElement('div');
div.innerHTML = html || '';
const walker = document.createTreeWalker(div, NodeFilter.SHOW_TEXT, null);
let node;
while ((node = walker.nextNode())) {
const raw = node.textContent || '';
if (!raw.trim()) {
continue;
}
const stripped = stripHeadingNumberPrefix(raw);
if (stripped !== raw) {
node.textContent = stripped;
}
break;
}
return div.innerHTML;
}
/** 从正文列表中收集需去掉序号的 H1/H2 标题 */
export function collectHeadingNumberStripUpdates(contentList) {
const list = contentList || [];
const updates = [];
list.forEach(function (item) {
if (!item || (item.is_h1 != 1 && item.is_h2 != 1)) {
return;
}
const original = item.content || '';
const next = stripHeadingNumberFromHtml(original);
if (next !== original) {
updates.push({
am_id: item.am_id,
content: next,
original: original
});
}
});
return updates;
}