终审编委信息
This commit is contained in:
BIN
src/assets/img/tmr-logo.png
Normal file
BIN
src/assets/img/tmr-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -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.',
|
||||
|
||||
@@ -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: '一作',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 article’s 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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!!');
|
||||
}
|
||||
|
||||
113
src/utils/convertNumericSuperscriptsToBlue.js
Normal file
113
src/utils/convertNumericSuperscriptsToBlue.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/** 上标内纯数字引用:1 / 1, 2 / 3-5 / 3–5 等 */
|
||||
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 + ']';
|
||||
}
|
||||
|
||||
/** 去掉紧邻节点上的空白(普通空格 / ),避免与两侧 重复 */
|
||||
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> 转为 <blue>[…]</blue> 。
|
||||
* 上标两侧若已有空格,先去掉再各放一个 ,避免出现 +空格。
|
||||
* @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
|
||||
};
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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 contributions)Word 表格属性(图五) */
|
||||
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 contributions)Word 表格属性(图五) */
|
||||
@@ -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
546
src/utils/exportManuscriptWordFooter.js
Normal file
546
src/utils/exportManuscriptWordFooter.js
Normal 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;
|
||||
/** OOXML:1cm = 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.62cm,WPS/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');
|
||||
}
|
||||
637
src/utils/exportManuscriptWordHeader.js
Normal file
637
src/utils/exportManuscriptWordHeader.js
Normal 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 ImageRun:extent 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"/>');
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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="' +
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
229
src/utils/moveBlueCitationsBeforePeriod.js
Normal file
229
src/utils/moveBlueCitationsBeforePeriod.js
Normal 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;
|
||||
}
|
||||
|
||||
/** 左侧保留一个 */
|
||||
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 <blue>[1, 2]</blue>,. Following text
|
||||
* text <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
|
||||
};
|
||||
}
|
||||
81
src/utils/stripHeadingNumber.js
Normal file
81
src/utils/stripHeadingNumber.js
Normal 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(/ /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;
|
||||
}
|
||||
Reference in New Issue
Block a user