This commit is contained in:
2026-06-09 17:46:33 +08:00
parent b56d5aa105
commit ec4a59eedb
7 changed files with 600 additions and 220 deletions

View File

@@ -964,6 +964,7 @@ str = str.replace(regex, function (match, content, offset, fullString) {
const documentFile = zip.file("word/document.xml");
if (!documentFile) {
console.error("❌ 找不到 word/document.xml无法解析 Word 文件");
callback([]);
return;
}
const relsFile = zip.file("word/_rels/document.xml.rels");
@@ -997,7 +998,8 @@ str = str.replace(regex, function (match, content, offset, fullString) {
const allTables = [];
if (!tables || tables.length === 0) {
console.warn("未找到表格内容,请检查 XML 结构");
return [];
callback([]);
return;
}
for (const table of tables) {
const rows = table.getElementsByTagNameNS(namespace, "tr");

View File

@@ -2,14 +2,14 @@
//记得切换
//正式
const mediaUrl = '/public/';
const baseUrl = '/';
// const mediaUrl = '/public/';
// const baseUrl = '/';
//正式环境
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
// const baseUrl = '/api'
const mediaUrl = 'https://submission.tmrjournals.com/public/';
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
const baseUrl = '/api'
//测试环境

View File

@@ -977,7 +977,7 @@ const en = {
tableName: 'Name',
tableNameScore: 'Name ( score )',
tableRole: 'Role',
tableAuthorRole: '1st auth. / Corr.',
tableAuthorRole: 'Author role',
tableEmail: 'Email',
articleContextEmptyGroup: 'None',
articleContextNoEmail: 'No email',
@@ -991,7 +991,12 @@ const en = {
articleContextSelectedHint: '{count} selected',
articleContextSelectedEmpty: 'Select contacts from the left',
articleContextRemoveSelected: 'Remove',
articleContextTypeSwitched: 'Contact type changed; previous selection cleared',
articleContextAddAllSelected: 'Add all selected',
articleContextReviewerScore: 'Score {score}',
articleContextFirstReviewLine: '1st review: {label}',
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',
@@ -1698,6 +1703,9 @@ const en = {
startDetectTip:
'Launch AI semantic consistency verification for all references against in-text citations.',
checkComplete: 'Relevance check completed',
auditDoneTitle: 'Reference context audit complete',
doneNotFoundHint: '{count} reference(s) not found in text',
doneAllClearHint: 'Review results in the table below. Re-check after editing in-text citations.',
detectingAi: 'Proofreading',
auditProgressTitle: 'Batch reference context audit in progress',
queuePositionLabel: 'Queue position:',
@@ -1722,7 +1730,19 @@ const en = {
copyFailed: 'Copy failed',
copyEmpty: 'Nothing to copy',
redetect: 'Re-check',
check: 'Check',
redetectFailed: 'Re-check failed',
redetectNotFound: 'Not-found references only',
redetectAll: 'All references',
redetectDialogTip: 'What would you like to re-check?',
redetectNotFoundCount: '{count} reference(s) not found in text',
redetectAllDesc:
'Re-run the audit for every reference. Recommended if reference-related content in the manuscript HTML has changed.',
redetectAllConfirm: 'Re-check all references?',
redetectNotFoundConfirm: 'Re-check {count} reference(s) not found in text?',
redetectConfirmTitle: 'Re-check references',
redetectConfirmOk: 'Start re-check',
redetectConfirmCancel: 'Cancel',
noReason: '(No details)',
aiAnalysis: 'AI analysis',
expandReason: 'Expand',

View File

@@ -962,7 +962,7 @@ const zh = {
tableName: '姓名',
tableNameScore: 'Name ( score )',
tableRole: '身份',
tableAuthorRole: '一作/通讯',
tableAuthorRole: '作者身份',
tableEmail: '邮箱',
articleContextEmptyGroup: '暂无',
articleContextNoEmail: '无邮箱',
@@ -976,7 +976,12 @@ const zh = {
articleContextSelectedHint: '已勾选 {count} 人',
articleContextSelectedEmpty: '请从左侧勾选联系人',
articleContextRemoveSelected: '移除',
articleContextTypeSwitched: '已切换联系人类型,原选择已清空',
articleContextAddAllSelected: '加入全部已选',
articleContextReviewerScore: '评分 {score}',
articleContextFirstReviewLine: '初审:{label}',
articleContextRepeatReviewLine: '第 {n} 次复审:{label}',
articleContextFinalReviewLine: '终审:{decision}',
articleContextReplaceVars: '替换模板变量',
articleContextReplaceVarsSuccess: '已替换 {{article_title}} / {{article_sn}}',
articleContextReplaceVarsEmpty: '主题或正文不含 {{article_title}}、{{article_sn}} 等占位符',
@@ -1677,6 +1682,9 @@ const zh = {
startDetect: '全量引文相关性核查',
startDetectTip: '一键启动对本篇稿件全部参考文献与正文语境的 AI 一致性核查',
checkComplete: '相关性检测已完成',
auditDoneTitle: '引文相关性核查已完成',
doneNotFoundHint: '有 {count} 条参考文献未在正文中匹配,请在下方表格中核对。',
doneAllClearHint: '请在下方表格查看核查结果;若修改了正文引文,可点击重新核查。',
detectingAi: '校对中',
auditProgressTitle: '全量引文相关性核查中',
queuePositionLabel: '当前排队位置:',
@@ -1700,8 +1708,19 @@ const zh = {
copySuccess: '已复制',
copyFailed: '复制失败',
copyEmpty: '暂无可复制内容',
redetect: '重新检测',
redetectFailed: '重新检测失败',
redetect: '重新核查',
check: '核查',
redetectFailed: '重新核查失败',
redetectNotFound: '仅核查未匹配项',
redetectAll: '全部参考文献',
redetectDialogTip: '请选择要重新核查的范围',
redetectNotFoundCount: '共 {count} 条未在正文中匹配',
redetectAllDesc: '对全部参考文献重新核查;若 HTML 中与参考文献相关的内容有变更,建议选择此项。',
redetectAllConfirm: '是否对全部参考文献重新核查?',
redetectNotFoundConfirm: '是否对 {count} 条未匹配的参考文献重新核查?',
redetectConfirmTitle: '重新核查',
redetectConfirmOk: '开始核查',
redetectConfirmCancel: '取消',
noReason: '(暂无说明)',
aiAnalysis: 'AI 分析',
expandReason: '展开',

View File

@@ -529,12 +529,14 @@ import { buildArticleAddTableListFromWordFile } from '@/utils/mathFormulaModule'
export default {
data() {
const routeArticleId = Number(this.$route.query.id) || 0;
return {
journal_id:'',
questionform:{},
dialogFormVisible:false,
baseUrl: this.Common.baseUrl,
mediaUrl: this.Common.mediaUrl,
articleId: this.$route.query.id,
articleId: routeArticleId,
userName: localStorage.getItem('U_name'),
loading: false,
Detailvisible: false,
@@ -543,7 +545,7 @@ import { buildArticleAddTableListFromWordFile } from '@/utils/mathFormulaModule'
activeNames: ['2'],
authorList_name: '',
artMes: {
articleId: this.$route.query.id
articleId: routeArticleId
},
fileMesForm: {
manuscirpt: '',
@@ -551,7 +553,7 @@ import { buildArticleAddTableListFromWordFile } from '@/utils/mathFormulaModule'
picturesAndTables: '',
responseFile: '',
supplementary:'', //
articleId: this.$route.query.id,
articleId: routeArticleId,
username: localStorage.getItem('U_name')
},
msgform: {
@@ -938,7 +940,12 @@ if(this.comentDeploy.length){
this.loading = true;
if (this.wordTableParsePromise) {
await this.wordTableParsePromise;
try {
await this.wordTableParsePromise;
} catch (err) {
console.warn('parseManuscriptWordTables failed:', err);
this.wordTablePayloadList = [];
}
}
await this.$api
.post('api/Article/RepairBack', this.fileMesForm)
@@ -963,11 +970,13 @@ if(this.comentDeploy.length){
},500)
} else {
this.loading = false;
this.$message.error('Failed to submit, please contact administrator!');
console.log(res.msg);
}
})
.catch((err) => {
this.loading = false;
console.log(err);
});
},
@@ -1031,6 +1040,11 @@ if(this.comentDeploy.length){
this.wordTablePayloadList = list || [];
return this.wordTablePayloadList;
})
.catch((err) => {
console.warn('parseManuscriptWordTables failed:', err);
this.wordTablePayloadList = [];
return [];
})
.finally(() => {
this.wordTableParsePromise = null;
});

View File

@@ -152,10 +152,84 @@
></div>
</div>
</div>
<span v-else-if="refRelevanceActionState === 'done'" class="ref-relevance-done-badge">
<i class="el-icon-circle-check"></i>
<span>{{ refRelevanceDoneLabel }}</span>
</span>
<div v-else-if="refRelevanceActionState === 'done'" class="ref-audit-done-bar">
<div class="done-meta">
<div class="done-status">
<span class="status-title status-title--done">
<i class="el-icon-circle-check"></i>
<span>{{ $t('refRelevance.auditDoneTitle') }}</span>
</span>
<p class="done-subtext">{{ refRelevanceDoneSubtext }}</p>
</div>
<div class="done-side">
<span v-if="refAuditProgressTotal" class="status-counter">
<span class="count">
{{ refAuditProgressFinished }}/{{ refAuditProgressTotal }}
</span>
</span>
<el-button
v-if="showRefRelevanceRedetectButton"
size="small"
type="primary"
plain
icon="el-icon-refresh-right"
:disabled="refRelevanceStatusLoading || refRelevanceGlobalDetecting"
@click="openRefRelevanceRedetectDialog"
>
{{ $t('refRelevance.redetect') }}
</el-button>
</div>
</div>
</div>
<el-dialog
:title="$t('refRelevance.redetectConfirmTitle')"
:visible.sync="refRelevanceRedetectDialogVisible"
width="440px"
append-to-body
:close-on-click-modal="false"
custom-class="ref-relevance-redetect-dialog"
@closed="resetRefRelevanceRedetectDialog"
>
<p class="ref-relevance-redetect-dialog-tip">
{{ $t('refRelevance.redetectDialogTip') }}
</p>
<el-radio-group
v-model="refRelevanceRedetectChoice"
class="ref-relevance-redetect-options"
>
<el-radio
label="notfound"
:disabled="!hasRefRelevanceNotFound"
class="ref-relevance-redetect-option"
>
<span class="ref-relevance-redetect-option__label">
{{ $t('refRelevance.redetectNotFound') }}
</span>
<span
v-if="hasRefRelevanceNotFound"
class="ref-relevance-redetect-option__meta"
>
{{ $t('refRelevance.redetectNotFoundCount', { count: refRelevanceNotFoundRows.length }) }}
</span>
</el-radio>
<el-radio label="all" class="ref-relevance-redetect-option">
<span class="ref-relevance-redetect-option__label">
{{ $t('refRelevance.redetectAll') }}
</span>
<span class="ref-relevance-redetect-option__meta">
{{ $t('refRelevance.redetectAllDesc') }}
</span>
</el-radio>
</el-radio-group>
<span slot="footer" class="dialog-footer">
<el-button @click="refRelevanceRedetectDialogVisible = false">
{{ $t('refRelevance.redetectConfirmCancel') }}
</el-button>
<el-button type="primary" @click="submitRefRelevanceRedetectDialog">
{{ $t('refRelevance.redetectConfirmOk') }}
</el-button>
</span>
</el-dialog>
</div>
<!-- <div v-if="showB_step == 2" class="ref-ai-filter-group">
<el-radio-group v-model="refAiFilter" size="small" @change="onRefAiFilterChange">
@@ -987,7 +1061,11 @@ export default {
/** 单条参考文献重新检测 loadingp_refer_id -> true */
refRelevanceRedetectingIds: {},
/** 单条参考文献详情轮询 timerp_refer_id -> timerId */
refRelevanceDetailsPollTimers: {}
refRelevanceDetailsPollTimers: {},
refRelevanceRedetectDialogVisible: false,
refRelevanceRedetectChoice: 'all',
/** 重新核查入口(暂隐藏) */
showRefRelevanceRedetectButton: false
};
},
computed: {
@@ -1044,6 +1122,20 @@ export default {
if (status === 2 && label) return label;
return this.$t('refRelevance.checkComplete');
},
refRelevanceDoneSubtext() {
if (this.hasRefRelevanceNotFound) {
return this.$t('refRelevance.doneNotFoundHint', {
count: this.refRelevanceNotFoundRows.length
});
}
return this.$t('refRelevance.doneAllClearHint');
},
refRelevanceNotFoundRows() {
return (this.chanFerForm || []).filter((row) => this.isRefRowRelevanceNotFound(row));
},
hasRefRelevanceNotFound() {
return this.refRelevanceNotFoundRows.length > 0;
},
refRelevanceScanSubtext() {
const label = (this.refRelevanceArticleStatus.status_label || '').trim();
if (label && label !== this.$t('refRelevance.detectingAi')) return label;
@@ -2635,27 +2727,10 @@ export default {
if (this.isRefRowRelevanceRedetecting(row)) return;
const referId = String(row.p_refer_id);
this.stopRefRelevancePoll();
this.$set(this.refRelevanceRedetectingIds, referId, true);
if (this.refRelevanceDetailsCache[referId]) {
this.$delete(this.refRelevanceDetailsCache, referId);
}
this.$set(row, '_refAiItemStatus', 1);
this.$set(row, '_refAiState', 'detecting');
this.$set(row, 'ai_relevance_list', []);
this.$set(row, '_refAiVisibleCount', 0);
this.$set(row, '_refAiPendingCites', 0);
this.prepareRefRowRelevanceRecheck(row);
this.refreshRefAiTableLayout();
this.$api
.post('api/References/referenceCheckRecheckFailedAI', {
p_article_id: this.p_article_id,
p_refer_id: String(row.p_refer_id)
})
.then((res) => {
if (res.code !== 0) {
return Promise.reject(new Error(res.msg || this.$t('refRelevance.redetectFailed')));
}
return this.pollRefRelevanceOnce();
})
this.requestRefRowRelevanceRecheck(row)
.then(() => this.pollRefRelevanceOnce())
.then((finished) => {
if (!finished && this.isRefRelevanceStatusRunning()) {
this.continueRefRelevancePoll();
@@ -2672,6 +2747,98 @@ export default {
this.refreshRefAiTableLayout();
});
},
prepareRefRowRelevanceRecheck(row) {
if (!row || row.p_refer_id == null) return '';
const referId = String(row.p_refer_id);
if (this.refRelevanceDetailsCache[referId]) {
this.$delete(this.refRelevanceDetailsCache, referId);
}
this.$set(row, '_refAiItemStatus', 1);
this.$set(row, '_refAiState', 'detecting');
this.$set(row, 'ai_relevance_list', []);
this.$set(row, '_refAiVisibleCount', 0);
this.$set(row, '_refAiPendingCites', 0);
return referId;
},
requestRefRowRelevanceRecheck(row) {
if (!row || row.p_refer_id == null || !this.p_article_id) {
return Promise.reject(new Error(this.$t('refRelevance.noReferId')));
}
const referId = String(row.p_refer_id);
this.$set(this.refRelevanceRedetectingIds, referId, true);
return this.$api
.post('api/References/referenceCheckRecheckFailedAI', {
p_article_id: this.p_article_id,
p_refer_id: referId
})
.then((res) => {
if (res.code !== 0) {
return Promise.reject(new Error(res.msg || this.$t('refRelevance.redetectFailed')));
}
return res;
});
},
openRefRelevanceRedetectDialog() {
if (this.refRelevanceStatusLoading || this.refRelevanceGlobalDetecting) return;
this.refRelevanceRedetectChoice = this.hasRefRelevanceNotFound ? 'notfound' : 'all';
this.refRelevanceRedetectDialogVisible = true;
},
resetRefRelevanceRedetectDialog() {
this.refRelevanceRedetectChoice = this.hasRefRelevanceNotFound ? 'notfound' : 'all';
},
submitRefRelevanceRedetectDialog() {
this.refRelevanceRedetectDialogVisible = false;
if (this.refRelevanceRedetectChoice === 'notfound') {
if (!this.hasRefRelevanceNotFound) return;
this.startRefRelevanceRedetectNotFound();
return;
}
this.startRefRelevanceDetect();
},
startRefRelevanceRedetectNotFound() {
const rows = this.refRelevanceNotFoundRows.slice();
if (!rows.length || !this.p_article_id) return;
this.stopRefRelevancePoll();
this.applyRefRelevanceArticleStatus({
status: 1,
status_label: this.$t('refRelevance.detectingAi'),
progress_percent: 0,
total: 0,
done: 0,
pending: 0,
failed: 0
});
rows.forEach((row) => this.prepareRefRowRelevanceRecheck(row));
this.refreshRefAiTableLayout();
const self = this;
rows
.reduce(function (chain, row) {
return chain.then(function () {
return self.requestRefRowRelevanceRecheck(row);
});
}, Promise.resolve())
.then(function () {
return self.pollRefRelevanceOnce();
})
.then(function (finished) {
if (!finished && self.isRefRelevanceStatusRunning()) {
self.continueRefRelevancePoll();
}
})
.catch(function (err) {
const msg = err && err.message ? err.message : String(err);
self.$message.error(msg || self.$t('refRelevance.redetectFailed'));
return self.pollRefRelevanceOnce();
})
.finally(function () {
rows.forEach(function (row) {
if (row && row.p_refer_id != null) {
self.$delete(self.refRelevanceRedetectingIds, String(row.p_refer_id));
}
});
self.refreshRefAiTableLayout();
});
},
isRefRowRelevancePending(row) {
if (!row || this.isRefRelevanceUncited(row)) return false;
const itemSt = this.getRefRowProgressStatus(row);
@@ -5906,6 +6073,61 @@ export default {
border-radius: 2px;
transition: width 0.3s ease;
}
.ref-audit-done-bar {
width: auto;
min-width: 400px;
max-width: 560px;
padding: 8px 12px;
background: #f0f9eb;
border: 1px solid #e1f3d8;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
flex-shrink: 0;
}
.ref-audit-done-bar .done-meta {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.ref-audit-done-bar .done-status {
flex: 1 1 auto;
min-width: 0;
}
.ref-audit-done-bar .status-title--done {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: bold;
color: #529b2e;
line-height: 1.4;
}
.ref-audit-done-bar .status-title--done .el-icon-circle-check {
flex-shrink: 0;
font-size: 14px;
color: #67c23a;
}
.ref-audit-done-bar .done-subtext {
margin: 4px 0 0;
padding: 0;
font-size: 12px;
line-height: 1.5;
color: #909399;
font-weight: normal;
}
.ref-audit-done-bar .done-side {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.ref-audit-done-bar .status-counter {
font-family: 'JetBrains Mono', Consolas, monospace, sans-serif;
font-size: 11px;
color: #606266;
white-space: nowrap;
}
@keyframes ref-relevance-spin {
from {
transform: rotate(0deg);
@@ -5917,20 +6139,41 @@ export default {
.ref-relevance-spin {
animation: ref-relevance-spin 1s linear infinite;
}
.ref-relevance-done-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
.ref-relevance-redetect-dialog-tip {
margin: 0 0 16px;
font-size: 13px;
line-height: 1;
color: #67c23a;
background: #f0f9eb;
border: 1px solid #e1f3d8;
border-radius: 4px;
line-height: 1.6;
color: #606266;
}
.ref-relevance-done-badge .el-icon-circle-check {
font-size: 18px;
.ref-relevance-redetect-options {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.ref-relevance-redetect-option {
display: flex;
align-items: flex-start;
margin-right: 0;
padding: 12px 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
white-space: normal;
}
.ref-relevance-redetect-option.is-checked {
border-color: #409eff;
background: #ecf5ff;
}
.ref-relevance-redetect-option__label {
display: block;
font-size: 14px;
color: #303133;
}
.ref-relevance-redetect-option__meta {
display: block;
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.ref-ai-cell--loading {
display: flex;

View File

@@ -1,20 +1,20 @@
<template>
<div>
<div class="crumbs">
<el-button type="text" icon="el-icon-arrow-left" @click="goBackInbox" class="back-inbox-btn">
{{ $t('mailboxSend.backToInbox') }}
</el-button>
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<i class="el-icon-message"></i> {{ $t('mailboxSend.title') }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="container" style="padding-top: 0px;" v-if="showSendEmail">
<!-- 阻止回车在收件人/主题等输入框触发表单隐式提交误点发送 -->
<form class="mail-compose-form" @submit.prevent @keydown.enter="preventComposeEnterSubmit">
<div v-if="articleContext.loaded && hasArticleTemplatePlaceholders" class="article-template-vars-bar mail_shuru">
<div class="mail-compose-header">
<div class="crumbs">
<el-button type="text" icon="el-icon-arrow-left" @click="goBackInbox" class="back-inbox-btn">
{{ $t('mailboxSend.backToInbox') }}
</el-button>
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<i class="el-icon-message"></i> {{ $t('mailboxSend.title') }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div
v-if="showSendEmail && articleContext.loaded && hasArticleTemplatePlaceholders"
class="article-template-vars-bar"
>
<span class="article-loaded-sn">SN {{ articleContext.acceptSn || '—' }}</span>
<el-button
size="mini"
@@ -26,7 +26,11 @@
{{ $t('mailboxSend.articleContextReplaceVars') }}
</el-button>
</div>
</div>
<div class="container" style="padding-top: 0px;" v-if="showSendEmail">
<!-- 阻止回车在收件人/主题等输入框触发表单隐式提交误点发送 -->
<form class="mail-compose-form" @submit.prevent @keydown.enter="preventComposeEnterSubmit">
<el-dialog
:title="articleContextDialogTitle"
:visible.sync="articleContextDialogVisible"
@@ -63,9 +67,10 @@
</div>
<div v-if="articleContext.loaded" class="article-context-loaded-hint">
<span class="article-loaded-sn">SN {{ articleContext.acceptSn || '—' }}</span>
<span class="article-loaded-divider">|</span>
<span class="article-loaded-title" :title="articleContext.title">{{ articleContextTitleBrief }}</span>
<div class="article-loaded-line">
<span class="article-loaded-sn">SN {{ articleContext.acceptSn || '—' }}</span>
</div>
<div v-if="articleContext.title" class="article-loaded-title">{{ articleContext.title }}</div>
</div>
<div v-else-if="!articleContextLoading" class="article-contact-empty article-contact-empty--hint">
{{ $t('mailboxSend.articleContextSearchHint') }}
@@ -78,7 +83,7 @@
:class="{ 'is-active': articleContactActiveTab === 'authors' }"
@click="switchArticleContactTab('authors')"
>
{{ $t('mailboxSend.articleContextNavAuthors') }}
{{ $t('mailboxSend.articleContextNavAuthors') }} ({{ authorContactCount }})
</div>
<div
class="article-context-nav-item"
@@ -104,22 +109,6 @@
<div class="article-context-main">
<div v-show="articleContactActiveTab === 'authors'" class="article-block">
<div class="article-block-head">
<div class="article-block-title">{{ $t('mailboxSend.articleContextNavAuthors') }}</div>
<div v-if="(articleContext.authors || []).length" class="article-block-actions">
<el-button type="text" size="mini" @click="selectAllArticleContacts('authors')">{{ $t('mailboxSend.articleContextSelectAll') }}</el-button>
<el-button type="text" size="mini" @click="clearArticleContacts('authors')">{{ $t('mailboxSend.articleContextClearAll') }}</el-button>
<el-button
type="primary"
size="mini"
plain
:disabled="!(articleContactSelected.authors || []).length"
@click="addSelectedArticleContacts('authors')"
>
{{ $t('mailboxSend.articleContextAddSelected') }} ({{ (articleContactSelected.authors || []).length }})
</el-button>
</div>
</div>
<div v-if="!(articleContext.authors || []).length" class="article-contact-empty">{{ $t('mailboxSend.articleContextEmptyGroup') }}</div>
<div v-else class="overflow-x-auto">
<table class="review_table">
@@ -135,31 +124,34 @@
</th>
<th class="review_table_index">No.</th>
<th>{{ $t('mailboxSend.tableName') }}</th>
<th>{{ $t('mailboxSend.tableAuthorRole') }}</th>
<th>{{ $t('mailboxSend.tableEmail') }}</th>
<th class="review_table_role">{{ $t('mailboxSend.tableAuthorRole') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(author, idx) in articleContext.authors" :key="author.key">
<tr
v-for="(author, idx) in articleContext.authors"
:key="author.key"
:class="{ 'review_table_row--disabled': !hasContactEmail(author) }"
>
<td class="review_table_check">
<el-checkbox
v-if="hasContactEmail(author)"
:value="isContactSelected('authors', author.key)"
:disabled="!author.email"
@change="toggleContactSelected('authors', author.key, $event)"
/>
</td>
<td class="review_table_index">{{ idx + 1 }}</td>
<td>{{ author.name || '—' }}</td>
<td>{{ formatAuthorRolesShort(author.roles) || '—' }}</td>
<td>
<div class="reviewer-email-cell">
<template v-if="author.email">
<div class="author-name-email-cell">
<div class="author-name-email-cell__name">{{ author.name || '—' }}</div>
<div v-if="author.email" class="author-name-email-cell__email">
<span class="is-clickable" @click="addContactToTo(author)">{{ author.email }}</span>
<i class="el-icon-document-copy article-copy-icon" :title="$t('mailboxSend.copyText')" @click.stop="copyContactText(author.email)"></i>
</template>
<span v-else>{{ $t('mailboxSend.articleContextNoEmail') }}</span>
</div>
<div v-else class="author-name-email-cell__email reviewer-no-email">{{ $t('mailboxSend.articleContextNoEmail') }}</div>
</div>
</td>
<td class="review_table_role">{{ formatAuthorRoles(author.roles) }}</td>
</tr>
</tbody>
</table>
@@ -167,22 +159,6 @@
</div>
<div v-show="articleContactActiveTab === 'review'" class="article-block">
<div class="article-block-head">
<div class="article-block-title">{{ $t('mailboxSend.articleContextNavUnderReview') }}</div>
<div v-if="hasReviewTab" class="article-block-actions">
<el-button type="text" size="mini" @click="selectAllArticleContacts('review')">{{ $t('mailboxSend.articleContextSelectAll') }}</el-button>
<el-button type="text" size="mini" @click="clearArticleContacts('review')">{{ $t('mailboxSend.articleContextClearAll') }}</el-button>
<el-button
type="primary"
size="mini"
plain
:disabled="!(articleContactSelected.review || []).length"
@click="addSelectedArticleContacts('review')"
>
{{ $t('mailboxSend.articleContextAddSelected') }} ({{ (articleContactSelected.review || []).length }})
</el-button>
</div>
</div>
<div v-if="!hasReviewTab" class="article-contact-empty">{{ $t('mailboxSend.articleContextEmptyGroup') }}</div>
<div v-else class="overflow-x-auto">
<table class="review_table">
@@ -245,22 +221,6 @@
</div>
<div v-show="articleContactActiveTab === 'final'" class="article-block">
<div class="article-block-head">
<div class="article-block-title">{{ $t('mailboxSend.articleContextNavFinal') }}</div>
<div v-if="hasFinalTab" class="article-block-actions">
<el-button type="text" size="mini" @click="selectAllArticleContacts('final')">{{ $t('mailboxSend.articleContextSelectAll') }}</el-button>
<el-button type="text" size="mini" @click="clearArticleContacts('final')">{{ $t('mailboxSend.articleContextClearAll') }}</el-button>
<el-button
type="primary"
size="mini"
plain
:disabled="!(articleContactSelected.final || []).length"
@click="addSelectedArticleContacts('final')"
>
{{ $t('mailboxSend.articleContextAddSelected') }} ({{ (articleContactSelected.final || []).length }})
</el-button>
</div>
</div>
<div v-if="!hasFinalTab" class="article-contact-empty">{{ $t('mailboxSend.articleContextEmptyGroup') }}</div>
<div v-else class="overflow-x-auto">
<table class="review_table">
@@ -321,7 +281,8 @@
>
<div class="article-context-selected-text">
<div class="article-context-selected-name" :title="item.display">{{ item.display }}</div>
<div v-if="item.subtitle" class="article-context-selected-sub">{{ item.subtitle }}</div>
<div v-if="item.subtitle" class="article-context-selected-sub" :title="item.subtitle">{{ item.subtitle }}</div>
<div v-if="item.identity" class="article-context-selected-identity">{{ item.identity }}</div>
</div>
<i
class="el-icon-close article-context-selected-remove"
@@ -345,7 +306,7 @@
</el-button>
</span>
</el-dialog>
<div class="mail_shuru" style="position: relative; display: flex; align-items: center;">
<div class="mail_shuru mail_shuru--to" style="position: relative; display: flex; align-items: center;">
<span class="mail_tit">{{ $t('mailboxSend.to') }}</span>
<div style="flex: 1; display: flex; flex-wrap: wrap; align-items: center;">
@@ -694,8 +655,9 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
return this.$t('mailboxSend.articleContextPickTitle');
},
articleSelectedTotalCount() {
const sel = this.articleContactSelected || {};
return (sel.authors || []).length + (sel.review || []).length + (sel.final || []).length;
const activeModule = this.getActiveArticleContactModule();
if (!activeModule) return 0;
return (this.articleContactSelected[activeModule] || []).length;
},
hasArticleTemplatePlaceholders() {
const text = this.getMailTextForTemplateScan();
@@ -710,30 +672,33 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
reviewContactCount() {
return (this.articleContext.reviewRows || []).length;
},
authorContactCount() {
return (this.articleContext.authors || []).length;
},
finalContactCount() {
return (this.articleContext.finalReviewers || []).length;
},
articleSelectedContactList() {
const self = this;
const modules = ['authors', 'review', 'final'];
const activeModule = this.getActiveArticleContactModule();
if (!activeModule) return [];
const list = [];
modules.forEach(function (module) {
const keys = self.articleContactSelected[module] || [];
const map = {};
self.getModuleContactList(module).forEach(function (item) {
map[item.key] = item;
});
keys.forEach(function (key) {
const contact = map[key];
if (!contact || !contact.email) return;
const name = String(contact.name || '').trim();
const email = String(contact.email || '').trim();
list.push({
module: module,
key: key,
display: name || email,
subtitle: name && name !== email ? email : ''
});
const keys = self.articleContactSelected[activeModule] || [];
const map = {};
self.getModuleContactList(activeModule).forEach(function (item) {
map[item.key] = item;
});
keys.forEach(function (key) {
const contact = map[key];
if (!contact || !contact.email) return;
const name = String(contact.name || '').trim();
const email = String(contact.email || '').trim();
list.push({
module: activeModule,
key: key,
display: name || email,
subtitle: email,
identity: self.formatArticleContactIdentity(activeModule, contact)
});
});
return list;
@@ -791,19 +756,31 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
this.loadArticleContext(idStr, { silent: true });
},
openArticleContactDialog() {
this.articleContactActiveTab = 'authors';
this.resetArticleContactDialog();
this.articleContextDialogVisible = true;
},
resetArticleContactDialog() {
this.articleContextInput = '';
this.articleContextLoading = false;
this.articleContext = {
loaded: false,
articleId: '',
acceptSn: '',
title: '',
authors: [],
reviewRows: [],
finalReviewers: []
};
this.resetArticleContactSelection();
this.articleContactActiveTab = 'authors';
},
switchArticleContactTab(tab) {
if (tab === 'review' && !this.hasReviewTab) return;
if (tab === 'final' && !this.hasFinalTab) return;
this.articleContactActiveTab = tab;
},
handleArticleContactDialogOpen() {
if (!this.articleContactActiveTab) {
this.articleContactActiveTab = 'authors';
}
if (this.articleContextInput) return;
this.articleContactActiveTab = 'authors';
const qId =
this.$route.query.article_id ||
this.$route.query.articleId ||
@@ -868,6 +845,48 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
if ((roles || []).indexOf('correspondingAuthor') >= 0) parts.push(this.$t('mailboxSend.roleCorrespondingAuthorShort'));
return parts.join('/');
},
formatArticleContactIdentity(module, contact) {
if (!contact) return '';
if (module === 'authors') {
const roles = this.formatAuthorRoles(contact.roles);
return roles && roles !== '—' ? roles : this.$t('mailboxSend.articleContextNavAuthors');
}
if (module === 'review') {
const parts = [this.$t('mailboxSend.articleContextNavUnderReview')];
if (contact.score != null && contact.score !== '') {
parts.push(this.$t('mailboxSend.articleContextReviewerScore', { score: contact.score }));
}
if (this.hasFirstReviewDisplay(contact.firstReview)) {
const label = this.formatFirstReviewLabel(contact.firstReview);
if (label && label !== '—') {
parts.push(this.$t('mailboxSend.articleContextFirstReviewLine', { label: label }));
}
}
const repeats = contact.repeats || [];
for (let i = repeats.length - 1; i >= 0; i--) {
if (!this.hasRepeatReviewDisplay(repeats[i])) continue;
const repLabel = this.formatRepeatReviewLabel(repeats[i]);
if (repLabel && repLabel !== '—') {
parts.push(
this.$t('mailboxSend.articleContextRepeatReviewLine', {
n: i + 2,
label: repLabel
})
);
break;
}
}
return parts.join(' · ');
}
if (module === 'final') {
const decision = this.formatFinalDecisionLabel(contact.finalState);
if (decision && decision !== '—') {
return this.$t('mailboxSend.articleContextFinalReviewLine', { decision: decision });
}
return this.$t('mailboxSend.articleContextNavFinal');
}
return '';
},
formatFirstReviewLabel(review) {
if (!review) return '—';
if (Number(review.state) === 0) return '—';
@@ -1115,6 +1134,23 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
resetArticleContactSelection() {
this.articleContactSelected = { authors: [], review: [], final: [] };
},
getActiveArticleContactModule() {
const modules = ['authors', 'review', 'final'];
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
if ((this.articleContactSelected[module] || []).length) {
return module;
}
}
return null;
},
ensureSingleArticleContactModule(module) {
const active = this.getActiveArticleContactModule();
if (active && active !== module) {
this.resetArticleContactSelection();
this.$message.info(this.$t('mailboxSend.articleContextTypeSwitched'));
}
},
sanitizeArticleContactSelection() {
const self = this;
['authors', 'review', 'final'].forEach(function (module) {
@@ -1150,6 +1186,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
return item.key === key;
});
if (!this.hasContactEmail(contact)) return;
this.ensureSingleArticleContactModule(module);
}
const arr = (this.articleContactSelected[module] || []).slice();
const idx = arr.indexOf(key);
@@ -1184,6 +1221,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
}
},
selectAllArticleContacts(module) {
this.ensureSingleArticleContactModule(module);
const keys = this.getModuleContacts(module).map(function (c) {
return c.key;
});
@@ -1217,10 +1255,9 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
return added;
},
addAllSelectedArticleContacts() {
let total = 0;
total += this.addSelectedArticleContacts('authors', true) || 0;
total += this.addSelectedArticleContacts('review', true) || 0;
total += this.addSelectedArticleContacts('final', true) || 0;
const activeModule = this.getActiveArticleContactModule();
if (!activeModule) return;
const total = this.addSelectedArticleContacts(activeModule, true) || 0;
if (total > 0) {
this.$message.success(this.$t('mailboxSend.articleContextAddedTo', { count: total }));
this.articleContextDialogVisible = false;
@@ -1927,10 +1964,21 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
</script>
<style scoped>
.mail-compose-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.crumbs {
display: flex;
align-items: center;
margin-bottom: 16px;
margin-bottom: 0;
flex: 1;
min-width: 0;
}
.back-inbox-btn {
margin-right: 12px;
@@ -1968,15 +2016,30 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
padding-right: 10px;
}
.article-context-loaded-hint,
.article-template-vars-bar {
.article-context-loaded-hint {
display: flex;
align-items: center;
gap: 8px;
align-items: flex-start;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 13px;
color: #606266;
flex-direction: column;
width: 100%;
}
.article-template-vars-bar {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
margin-left: auto;
font-size: 13px;
color: #606266;
}
.article-loaded-line {
width: 100%;
}
.article-loaded-sn {
@@ -1984,17 +2047,12 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
color: #303133;
}
.article-loaded-divider {
color: #dcdfe6;
}
.article-loaded-title {
flex: 1;
min-width: 120px;
max-width: 520px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
color: #606266;
line-height: 1.5;
word-break: break-word;
white-space: normal;
}
.article-contact-empty--hint {
@@ -2013,7 +2071,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
}
.article-context-nav {
width: 148px;
width: 172px;
flex-shrink: 0;
background: #fafafa;
border-right: 1px solid #ebeef5;
@@ -2021,13 +2079,14 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
}
.article-context-nav-item {
padding: 12px 16px;
padding: 12px 14px;
font-size: 13px;
color: #606266;
cursor: pointer;
border-left: 3px solid transparent;
transition: all 0.2s;
line-height: 1.4;
white-space: nowrap;
}
.article-context-nav-item:hover:not(.is-disabled) {
@@ -2056,7 +2115,7 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
}
.article-context-selected {
width: 240px;
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
@@ -2131,6 +2190,19 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
word-break: break-all;
}
.article-context-selected-identity {
display: inline-block;
margin-top: 4px;
padding: 2px 6px;
max-width: 100%;
font-size: 11px;
line-height: 1.4;
color: #606266;
background: #f4f4f5;
border-radius: 3px;
word-break: break-word;
}
.article-context-selected-remove {
flex-shrink: 0;
margin-top: 2px;
@@ -2148,24 +2220,28 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-left: 8px;
border: 1px solid #dcdfe6;
width: 24px;
height: 24px;
margin-left: 6px;
border: none;
border-radius: 4px;
color: #006699;
background: transparent;
color: #c0c4cc;
cursor: pointer;
transition: all 0.2s;
opacity: 0.55;
transition: color 0.2s, background 0.2s, opacity 0.2s;
}
.mail_shuru--to:hover .mail-to-pick-btn,
.mail-to-pick-btn:hover {
border-color: #006699;
background: #f0f7fa;
opacity: 1;
color: #006699;
background: #f5f7fa;
}
.mail-to-pick-btn .el-icon-plus {
font-size: 16px;
font-weight: bold;
font-size: 14px;
font-weight: normal;
}
.article-context-id-input {
@@ -2266,31 +2342,6 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
margin-top: 0;
}
.article-block-head {
display: flex;
justify-content: space-between;
align-items: center;
background: #f5f7fa;
padding: 10px;
margin-bottom: 12px;
border-radius: 4px;
flex-wrap: wrap;
gap: 8px;
}
.article-block-actions {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.article-block-title {
font-size: 16px;
font-weight: 700;
color: #333;
}
.review_table_check {
width: 42px;
min-width: 42px;
@@ -2319,6 +2370,9 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
border-bottom: 1px solid #ebeef5;
text-align: left;
font-size: 14px;
word-break: break-word;
white-space: normal;
vertical-align: top;
}
.review_table th {
@@ -2353,10 +2407,21 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
}
.review_table_index {
min-width: 90px;
width: 44px;
min-width: 44px;
max-width: 44px;
padding-left: 8px;
padding-right: 8px;
text-align: center;
white-space: nowrap;
}
.review_table_role {
min-width: 220px;
white-space: nowrap;
word-break: normal;
}
.reviewer-name-cell {
min-width: 220px;
}
@@ -2364,9 +2429,30 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.reviewer-name-cell--inline {
display: flex;
flex-wrap: wrap;
align-items: center;
align-items: flex-start;
gap: 4px 8px;
line-height: 1.4;
line-height: 1.5;
}
.author-name-email-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
line-height: 1.5;
}
.author-name-email-cell__name {
font-weight: 500;
color: #303133;
}
.author-name-email-cell__email {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
word-break: break-all;
}
.reviewer-email-cell--inline {
@@ -2709,8 +2795,4 @@ import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview'
.article-context-dialog .el-dialog__footer {
padding: 10px 16px 14px;
}
.article-context-dialog .article-block-title {
font-size: 14px;
}
</style>