Compare commits
2 Commits
77703a855a
...
b56d5aa105
| Author | SHA1 | Date | |
|---|---|---|---|
| b56d5aa105 | |||
| e57e06be95 |
@@ -643,6 +643,7 @@ const en = {
|
||||
outboxTab: 'Mail sent',
|
||||
draftsTab: 'Drafts',
|
||||
deletedTab: 'Deleted',
|
||||
trashTab: 'Filtered mail',
|
||||
spamTab: 'Spam',
|
||||
searchPlaceholder: 'Please enter name or email',
|
||||
searchBtn: 'Search',
|
||||
@@ -947,7 +948,63 @@ const en = {
|
||||
selectTemplateStyleFirst: 'Please select template and style first',
|
||||
recipientLimit: 'You can add up to {count} recipient(s)',
|
||||
recipientLimitPlaceholder: 'Limit reached (max {count})',
|
||||
sendBatchTitle: 'Send result',
|
||||
sendBatchSuccess: 'Sent successfully:',
|
||||
sendBatchFail: 'Failed to send:',
|
||||
sendBatchConfirm: 'OK',
|
||||
sendProgressTitle: 'Sending mail',
|
||||
sendProgressRunning: 'Sending… {done}/{total} completed',
|
||||
sendProgressAllSuccess: 'All sent successfully ({count})',
|
||||
sendProgressDone: 'Done: {ok} sent, {fail} failed',
|
||||
backToInbox: 'Back to email list',
|
||||
articleContextId: 'Article ID:',
|
||||
articleContextIdPlaceholder: 'Enter article ID',
|
||||
articleContextLoad: 'Load manuscript',
|
||||
articleContextSearch: 'Search',
|
||||
articleContextSearchHint: 'Please enter article ID',
|
||||
articleContextPickTitle: 'Select recipients',
|
||||
articleContextNavAuthors: 'Authors',
|
||||
articleContextNavUnderReview: 'Under review',
|
||||
articleContextNavFinal: 'Final Decision',
|
||||
articleContextNeedId: 'Please enter article ID',
|
||||
articleContextLoadFail: 'Failed to load manuscript info',
|
||||
articleContextTitle: 'Title',
|
||||
articleContextAuthors: 'Authors',
|
||||
articleContextUnderReview: 'First / re-review',
|
||||
articleContextFirstReview: 'First-review reviewers',
|
||||
articleContextReReview: 'Re-review reviewers',
|
||||
articleContextFinalReview: 'Final decision',
|
||||
tableName: 'Name',
|
||||
tableNameScore: 'Name ( score )',
|
||||
tableRole: 'Role',
|
||||
tableAuthorRole: '1st auth. / Corr.',
|
||||
tableEmail: 'Email',
|
||||
articleContextEmptyGroup: 'None',
|
||||
articleContextNoEmail: 'No email',
|
||||
articleContextAddTo: 'Add to To',
|
||||
articleContextSelectAll: 'Select all',
|
||||
articleContextClearAll: 'Clear',
|
||||
articleContextAddSelected: 'Add to To',
|
||||
articleContextAddedTo: 'Added {count} recipient(s)',
|
||||
articleContextOpenDialog: 'Select from manuscript',
|
||||
articleContextDialogTitle: 'Manuscript contacts · {sn}',
|
||||
articleContextSelectedHint: '{count} selected',
|
||||
articleContextSelectedEmpty: 'Select contacts from the left',
|
||||
articleContextRemoveSelected: 'Remove',
|
||||
articleContextAddAllSelected: 'Add all selected',
|
||||
articleContextReplaceVars: 'Replace template vars',
|
||||
articleContextReplaceVarsSuccess: 'Replaced {{article_title}} / {{article_sn}}',
|
||||
articleContextReplaceVarsEmpty: 'Subject or body has no {{article_title}} / {{article_sn}} placeholders',
|
||||
roleFirstAuthor: 'First author',
|
||||
roleCorrespondingAuthor: 'Corresponding author',
|
||||
roleFirstAuthorShort: '1st auth.',
|
||||
roleCorrespondingAuthorShort: 'Corr.',
|
||||
roleFirstReview: 'First review',
|
||||
roleReReview: 'Re-review',
|
||||
roleFinalReview: 'Final decision',
|
||||
copyText: 'Copy',
|
||||
copySuccess: 'Copied',
|
||||
copyFail: 'Copy failed',
|
||||
},
|
||||
home: {
|
||||
authortop: 'Author guide',
|
||||
@@ -1287,6 +1344,8 @@ const en = {
|
||||
addTemplateBtn: 'Go add templates',
|
||||
templateTab: 'Template',
|
||||
styleTab: 'Style',
|
||||
noStyle: 'No style',
|
||||
noStyleDesc: 'Apply template body only, without header/footer',
|
||||
loading: 'Loading...',
|
||||
refresh: 'Refresh'
|
||||
},
|
||||
@@ -1338,7 +1397,7 @@ const en = {
|
||||
clearAll: 'Clear All',
|
||||
selectPromotionFieldsTip: 'Multiple selection supported; leave empty for no field restriction.',
|
||||
selectPromotionCountryTip: 'Multiple selection supported; leave empty for no country restriction. Uses the same API as fields until a dedicated country list is available.',
|
||||
fieldSearchPlaceholder: 'Search promotion fields',
|
||||
fieldSearchPlaceholder: 'Single term: fuzzy; comma/newline list: exact match',
|
||||
countrySearchPlaceholder: 'Search countries',
|
||||
countryQuickZone1: 'Partition 1',
|
||||
countryQuickZone2: 'Partition 2',
|
||||
@@ -1346,6 +1405,9 @@ const en = {
|
||||
countryQuickChina: 'China',
|
||||
countryQuickIndia: 'India',
|
||||
noFieldMatch: 'No matching fields',
|
||||
copySelectedFields: 'Copy selected fields (semicolon-separated)',
|
||||
copySelectedFieldsSuccess: 'Copied {count} field(s)',
|
||||
copySelectedFieldsEmpty: 'No fields selected',
|
||||
noCountryMatch: 'No matching countries',
|
||||
confirm: 'Confirm',
|
||||
fieldsSaved: 'Promotion fields saved',
|
||||
@@ -1433,10 +1495,10 @@ const en = {
|
||||
factoryStepNav2Desc: 'Choose template and style.',
|
||||
factoryStepNav3Title: 'Sending and scenario',
|
||||
factoryStepNav3Desc: 'Choose accounts, send count, and target type.',
|
||||
factoryStepNav4Title: 'Promotion fields',
|
||||
factoryStepNav4Desc: 'Select at least one promotion field.',
|
||||
factoryStepNav5Title: 'Country',
|
||||
factoryStepNav5Desc: 'Select at least one country or partition.',
|
||||
factoryStepNav4Title: 'Country',
|
||||
factoryStepNav4Desc: 'Select at least one country or partition.',
|
||||
factoryStepNav5Title: 'Promotion fields',
|
||||
factoryStepNav5Desc: 'Select at least one promotion field.',
|
||||
factoryStepNav6Title: 'Confirm and enable',
|
||||
factoryStepNav6Desc: 'Choose save only or enable next day.',
|
||||
factoryPromotionFieldsBlockTip: 'Open “Choose fields” and tick at least one item; do not submit with none selected.',
|
||||
@@ -1627,6 +1689,10 @@ const en = {
|
||||
dialogIntroduction: 'Introduction :',
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
articleDetailEditor: {
|
||||
reReviewNotifyBtn: 'Notify re-review',
|
||||
reReviewNotifyTitle: 'Notify re-review'
|
||||
},
|
||||
refRelevance: {
|
||||
startDetect: 'Batch Audit Reference Context',
|
||||
startDetectTip:
|
||||
|
||||
@@ -655,8 +655,9 @@ const zh = {
|
||||
inboxTab: '收件箱',
|
||||
outboxTab: '发件箱',
|
||||
draftsTab: '草稿箱',
|
||||
deletedTab: '已删除',
|
||||
spamTab: '垃圾邮件',
|
||||
deletedTab: '已删除',
|
||||
trashTab: '已过滤的邮件',
|
||||
spamTab: '垃圾邮件',
|
||||
searchPlaceholder: '请输入姓名或邮箱',
|
||||
searchBtn: '搜索',
|
||||
syncBtn: '同步远程邮箱',
|
||||
@@ -932,7 +933,63 @@ const zh = {
|
||||
selectTemplateStyleFirst: '请先选择模板和风格',
|
||||
recipientLimit: '最多只能添加 {count} 个收件人',
|
||||
recipientLimitPlaceholder: '已达上限(最多 {count} 个)',
|
||||
sendBatchTitle: '发送结果',
|
||||
sendBatchSuccess: '发送成功:',
|
||||
sendBatchFail: '发送失败:',
|
||||
sendBatchConfirm: '确定',
|
||||
sendProgressTitle: '正在发送邮件',
|
||||
sendProgressRunning: '正在发送,已完成 {done}/{total}',
|
||||
sendProgressAllSuccess: '全部发送成功,共 {count} 封',
|
||||
sendProgressDone: '发送完成:成功 {ok} 封,失败 {fail} 封',
|
||||
backToInbox: '返回收件箱',
|
||||
articleContextId: '文章 ID:',
|
||||
articleContextIdPlaceholder: '请输入文章 ID',
|
||||
articleContextLoad: '加载稿件信息',
|
||||
articleContextSearch: '查询',
|
||||
articleContextSearchHint: '请输入文章 ID',
|
||||
articleContextPickTitle: '选择收件人',
|
||||
articleContextNavAuthors: '稿件作者',
|
||||
articleContextNavUnderReview: 'Under review',
|
||||
articleContextNavFinal: 'Final Decision',
|
||||
articleContextNeedId: '请输入文章 ID',
|
||||
articleContextLoadFail: '加载稿件信息失败',
|
||||
articleContextTitle: '标题',
|
||||
articleContextAuthors: '作者',
|
||||
articleContextUnderReview: '初审 / 复审',
|
||||
articleContextFirstReview: '初审审稿人',
|
||||
articleContextReReview: '复审审稿人',
|
||||
articleContextFinalReview: '终审',
|
||||
tableName: '姓名',
|
||||
tableNameScore: 'Name ( score )',
|
||||
tableRole: '身份',
|
||||
tableAuthorRole: '一作/通讯',
|
||||
tableEmail: '邮箱',
|
||||
articleContextEmptyGroup: '暂无',
|
||||
articleContextNoEmail: '无邮箱',
|
||||
articleContextAddTo: '加入收件人',
|
||||
articleContextSelectAll: '全选',
|
||||
articleContextClearAll: '取消',
|
||||
articleContextAddSelected: '加入收件人',
|
||||
articleContextAddedTo: '已加入 {count} 个收件人',
|
||||
articleContextOpenDialog: '从稿件选择联系人',
|
||||
articleContextDialogTitle: '稿件联系人 · {sn}',
|
||||
articleContextSelectedHint: '已勾选 {count} 人',
|
||||
articleContextSelectedEmpty: '请从左侧勾选联系人',
|
||||
articleContextRemoveSelected: '移除',
|
||||
articleContextAddAllSelected: '加入全部已选',
|
||||
articleContextReplaceVars: '替换模板变量',
|
||||
articleContextReplaceVarsSuccess: '已替换 {{article_title}} / {{article_sn}}',
|
||||
articleContextReplaceVarsEmpty: '主题或正文不含 {{article_title}}、{{article_sn}} 等占位符',
|
||||
roleFirstAuthor: '第一作者',
|
||||
roleCorrespondingAuthor: '通讯作者',
|
||||
roleFirstAuthorShort: '一作',
|
||||
roleCorrespondingAuthorShort: '通讯',
|
||||
roleFirstReview: '初审',
|
||||
roleReReview: '复审',
|
||||
roleFinalReview: '终审',
|
||||
copyText: '复制',
|
||||
copySuccess: '已复制',
|
||||
copyFail: '复制失败',
|
||||
},
|
||||
home: {
|
||||
authortop: '用户指南',
|
||||
@@ -1267,6 +1324,8 @@ const zh = {
|
||||
addTemplateBtn: '去新增模板',
|
||||
templateTab: '模版',
|
||||
styleTab: '样式',
|
||||
noStyle: '不使用样式',
|
||||
noStyleDesc: '仅应用模板正文,不套页眉页脚',
|
||||
loading: '加载中...',
|
||||
refresh: '刷新'
|
||||
},
|
||||
@@ -1318,7 +1377,7 @@ const zh = {
|
||||
clearAll: '取消全选',
|
||||
selectPromotionFieldsTip: '可多选;未选择则不限制推广领域。',
|
||||
selectPromotionCountryTip: '可多选;未选择则不限制国家。与领域接口一致,后续可对接独立国家数据。',
|
||||
fieldSearchPlaceholder: '搜索推广领域',
|
||||
fieldSearchPlaceholder: '单个词模糊搜索;逗号/换行多个名称需完全匹配',
|
||||
countrySearchPlaceholder: '搜索国家',
|
||||
countryQuickZone1: '1区',
|
||||
countryQuickZone2: '2区',
|
||||
@@ -1326,6 +1385,9 @@ const zh = {
|
||||
countryQuickChina: 'China',
|
||||
countryQuickIndia: 'India',
|
||||
noFieldMatch: '没有匹配的领域',
|
||||
copySelectedFields: '复制已选领域(分号分隔)',
|
||||
copySelectedFieldsSuccess: '已复制 {count} 个领域',
|
||||
copySelectedFieldsEmpty: '暂无已选领域',
|
||||
noCountryMatch: '没有匹配的国家',
|
||||
confirm: '确定',
|
||||
fieldsSaved: '推广领域已保存',
|
||||
@@ -1413,10 +1475,10 @@ const zh = {
|
||||
factoryStepNav2Desc: '选好邮件模板和样式。',
|
||||
factoryStepNav3Title: '发送与场景',
|
||||
factoryStepNav3Desc: '选账号,填发送数量和目标人类型。',
|
||||
factoryStepNav4Title: '推广领域',
|
||||
factoryStepNav4Desc: '至少选择一个推广领域。',
|
||||
factoryStepNav5Title: '国家',
|
||||
factoryStepNav5Desc: '至少选择一个国家或分区。',
|
||||
factoryStepNav4Title: '国家',
|
||||
factoryStepNav4Desc: '至少选择一个国家或分区。',
|
||||
factoryStepNav5Title: '推广领域',
|
||||
factoryStepNav5Desc: '至少选择一个推广领域。',
|
||||
factoryStepNav6Title: '确认并开启',
|
||||
factoryStepNav6Desc: '选择仅保存或次日自动开启。',
|
||||
factoryPromotionFieldsBlockTip: '请打开「选择领域」,在列表中至少勾选一项;不得留空提交。',
|
||||
@@ -1607,6 +1669,10 @@ const zh = {
|
||||
dialogIntroduction: '简介:',
|
||||
cancel: '取消'
|
||||
},
|
||||
articleDetailEditor: {
|
||||
reReviewNotifyBtn: '通知复审',
|
||||
reReviewNotifyTitle: '通知复审'
|
||||
},
|
||||
refRelevance: {
|
||||
startDetect: '全量引文相关性核查',
|
||||
startDetectTip: '一键启动对本篇稿件全部参考文献与正文语境的 AI 一致性核查',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="container">
|
||||
<div class="success-icon">✔</div>
|
||||
<h2 class="success-title">Application submitted successfully!</h2>
|
||||
<p class="success-desc" v-if="isFromChina">
|
||||
We have a WeChat group for academic discussions. Please feel free to scan the QR code below to join.
|
||||
<p class="success-desc success-desc--left" v-if="isFromChina">
|
||||
You may voluntarily scan the QR code to connect with the editorial team and join our editorial board group for future academic exchanges.
|
||||
</p>
|
||||
<p class="success-desc" v-else>
|
||||
Your application is currently under review. We appreciate your patience, and our team will notify you of the final decision via email as soon as possible.
|
||||
@@ -15,8 +15,8 @@
|
||||
<div v-else class="qr-code-box" v-if="qrCodeUrl">
|
||||
<img :src="qrCodeUrl" alt="WeChat Group QR" />
|
||||
</div>
|
||||
<!-- <p class="remark-tip">Please use the following format for group remark:</p>
|
||||
<div class="remark-box">Name - Research Field - Affiliation</div> -->
|
||||
<p class="remark-tip">When joining the group, please use the following remark format:</p>
|
||||
<div class="remark-box">{{ groupRemarkFormat }}</div>
|
||||
</div>
|
||||
|
||||
<router-link to="/login" replace class="back-btn"> Login to the Submission System Now </router-link>
|
||||
@@ -30,10 +30,17 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
qrCodeUrl: '',
|
||||
qrLoading: false
|
||||
qrLoading: false,
|
||||
journalAbbr: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
groupRemarkFormat() {
|
||||
if (this.journalAbbr) {
|
||||
return `${this.journalAbbr} + Name`;
|
||||
}
|
||||
return 'Journal Abbreviation + Name';
|
||||
},
|
||||
isFromChina() {
|
||||
const s = String(this.$route.query.country || '').trim();
|
||||
if (!s) return false;
|
||||
@@ -85,6 +92,7 @@ export default {
|
||||
const raw =
|
||||
(journal && (journal.wechat_yboard_qrcode || journal.yboard_qrcode || journal.qrcode_url)) || '';
|
||||
this.qrCodeUrl = this.buildMediaUrl(raw);
|
||||
this.journalAbbr = journal && journal.abbr ? String(journal.abbr).trim() : '';
|
||||
})
|
||||
.catch(() => {
|
||||
this.qrCodeUrl = '';
|
||||
@@ -147,6 +155,10 @@ export default {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.success-desc--left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #c3dafe;
|
||||
@@ -182,19 +194,22 @@ export default {
|
||||
.remark-tip {
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
margin-top: 10px;
|
||||
margin-top: 16px;
|
||||
text-align: left;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.remark-box {
|
||||
background: #fff;
|
||||
border: 1px solid #feb2b2;
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
border: 1px solid #c3dafe;
|
||||
color: #2b6cb0;
|
||||
font-weight: 600;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
|
||||
@@ -812,9 +812,14 @@
|
||||
>
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- 无数据:补全空内容(可自定义为“-”“无”等) -->
|
||||
|
||||
<span>-</span>
|
||||
<el-button
|
||||
v-if="index1 === 0 && showReReviewNotifyBtn(iken)"
|
||||
type="text"
|
||||
size="mini"
|
||||
style="margin-left: 8px"
|
||||
@click.stop="openReReviewNotifyMail(iken)"
|
||||
>{{ $t('articleDetailEditor.reReviewNotifyBtn') }}</el-button>
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
@@ -1126,6 +1131,16 @@
|
||||
</el-dialog>
|
||||
<!-- 审稿详情 start-->
|
||||
<reviewerDetail ref="reviewerDetail" v-if="reviewerVisible" :reviewerDetail="reviewerDetail" destroy-on-close></reviewerDetail>
|
||||
<re-review-notify-mail-dialog
|
||||
:visible.sync="reReviewNotifyVisible"
|
||||
:reviewer="reReviewNotifyReviewer"
|
||||
:article-id="editform.articleId"
|
||||
:article-title="form.title"
|
||||
:accept-sn="form.accept_sn"
|
||||
:journal-id="form.journal"
|
||||
:journal-name="form.journal_name"
|
||||
:journal-url="form.journal_url"
|
||||
/>
|
||||
<!-- 审稿详情 end-->
|
||||
<el-dialog title="Edit H Index" :visible.sync="HEditVisible" width="400px" :close-on-click-modal="false">
|
||||
<div style="margin: 0 0 20px 10px; font-weight: bold">{{ HIxForm.realname }}</div>
|
||||
@@ -1547,11 +1562,13 @@
|
||||
import timetalk from './time_talk';
|
||||
import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue';
|
||||
import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue';
|
||||
import ReReviewNotifyMailDialog from '../../components/page/components/articleDetail/ReReviewNotifyMailDialog.vue';
|
||||
export default {
|
||||
components: {
|
||||
timetalk,
|
||||
reviewerDetail,
|
||||
FigureCopyright
|
||||
FigureCopyright,
|
||||
ReReviewNotifyMailDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -1561,6 +1578,8 @@ export default {
|
||||
finalDecisionData: {},
|
||||
FinalDecisionVisible: false,
|
||||
reviewerVisible: false,
|
||||
reReviewNotifyVisible: false,
|
||||
reReviewNotifyReviewer: null,
|
||||
expanded: false,
|
||||
corresVisible: false,
|
||||
isShowAuthorInfo: false,
|
||||
@@ -1658,6 +1677,8 @@ export default {
|
||||
articleId: this.$route.query.id,
|
||||
journal: '',
|
||||
journalname: '',
|
||||
journal_name: '',
|
||||
journal_url: '',
|
||||
username: '',
|
||||
title: '',
|
||||
accept_sn: '',
|
||||
@@ -2272,6 +2293,23 @@ export default {
|
||||
this.$refs.reviewerDetail.init(item.art_rev_id, type, repeatItem);
|
||||
});
|
||||
},
|
||||
showReReviewNotifyBtn(reviewer) {
|
||||
if (!this.isReReviewNotifyEnabled()) return false;
|
||||
if (!reviewer) return false;
|
||||
const hasFirstReview =
|
||||
Number(reviewer.state) !== 0 && [1, 2, 3, 4].includes(Number(reviewer.recommend));
|
||||
if (!hasFirstReview) return false;
|
||||
const second = Array.isArray(reviewer.repeat) ? reviewer.repeat[0] : null;
|
||||
if (!second) return true;
|
||||
return ![1, 2, 3].includes(Number(second.recommend));
|
||||
},
|
||||
isReReviewNotifyEnabled() {
|
||||
return Object.prototype.hasOwnProperty.call(this.$route.query, 'zy');
|
||||
},
|
||||
openReReviewNotifyMail(reviewer) {
|
||||
this.reReviewNotifyReviewer = reviewer ? { ...reviewer } : null;
|
||||
this.reReviewNotifyVisible = true;
|
||||
},
|
||||
|
||||
goReviewerDetail(id) {
|
||||
console.log('id at line 1112:', id);
|
||||
@@ -2694,7 +2732,9 @@ export default {
|
||||
: '';
|
||||
this.form.title = res.article.title;
|
||||
this.form.journal = res.article.journal_id;
|
||||
this.form.journalname = res.article.journalname;
|
||||
this.form.journal_name = res.article.journal_name || res.article.journalname || '';
|
||||
this.form.journalname = res.article.journalname || res.article.journal_name || '';
|
||||
this.form.journal_url = res.article.journal_url || res.article.website || res.article.journal_website || '';
|
||||
this.form.abstrart = res.article.abstrart;
|
||||
this.form.accept_sn = res.article.accept_sn;
|
||||
this.form.scoring = res.article.scoring;
|
||||
|
||||
@@ -275,41 +275,40 @@
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center; width: calc(100% - 120px)">
|
||||
<span class="reportIcon" style="cursor: pointer" @click="linkEmail(item, v.art_aut_id)"
|
||||
<span class="reportIcon"
|
||||
><i class="el-icon-s-custom"></i
|
||||
></span>
|
||||
<span
|
||||
@click="linkEmail(item, v.art_aut_id)"
|
||||
style="
|
||||
max-width: calc(100% - 60px);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
>{{ v.realname || v.email || '-' }}
|
||||
<span class="author-name">{{ v.realname || v.email || '-' }}</span>
|
||||
<span class="author-action-icons">
|
||||
<span
|
||||
class="author-action-btn author-action-btn--mail"
|
||||
title="Send email"
|
||||
@click.stop="linkEmail(item, v.art_aut_id)"
|
||||
>
|
||||
<svg class="author-action-svg" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 3.5A1.5 1.5 0 0 1 3.5 2h9A1.5 1.5 0 0 1 14 3.5v9A1.5 1.5 0 0 1 12.5 14h-9A1.5 1.5 0 0 1 2 12.5v-9zm1.2-.5 4.8 3.6L12.8 3H3.2zM3 4.3v7.7h10V4.3l-4.5 3.4a.6.6 0 0 1-.7 0L3 4.3z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="author-action-btn author-action-btn--search"
|
||||
title="Search author"
|
||||
@click.stop="openAuthorSearch(v)"
|
||||
>
|
||||
<svg class="author-action-svg" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 1.25a5.75 5.75 0 1 1 0 11.5 5.75 5.75 0 0 1 0-11.5zm0 1.3a4.45 4.45 0 1 0 0 8.9 4.45 4.45 0 0 0 0-8.9z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11.05 11.05a.65.65 0 0 1 .92 0l2.98 2.98a.65.65 0 1 1-.92.92l-2.98-2.98a.65.65 0 0 1 0-.92z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
@click="linkEmail(item, v.art_aut_id)"
|
||||
t="1763011629734"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="11794"
|
||||
width="24"
|
||||
height="16"
|
||||
style="margin-top: 1.5px; margin-left: 4px; cursor: pointer"
|
||||
>
|
||||
<path
|
||||
d="M780.8 0c134.3168 0 243.2 108.8832 243.2 243.2v537.6c0 134.3168-108.8832 243.2-243.2 243.2H243.2c-134.3168 0-243.2-108.8832-243.2-243.2V243.2C0 108.8832 108.8832 0 243.2 0h537.6z m5.3568 604.4864c-7.9936-6.496-13.4912-8.4928-16.4864-5.9904-3.0016 2.496-4.4992 8.2432-4.4992 17.2352v32.9792c0 8.9984-2.1248 15.2448-6.368 18.7392-4.2496 3.5008-11.3728 5.248-21.3632 5.248h-34.432a455.7504 455.7504 0 0 1-17.664-0.3776 357.76 357.76 0 0 0-15.36-0.3712h-14.9952c-11.4944 0-19.488 1.4976-23.9872 4.4992-4.4928 2.9952-6.7456 11.488-6.7456 25.4848v38.2208c0 8 3.3728 14.496 10.1184 19.488 6.7456 4.9984 13.6192 7.5008 20.6144 7.5008h86.944c7.0016 0 12.6208 2.7456 16.8704 8.2432 4.2432 5.4976 6.368 12.4928 6.368 20.992v33.728c0 13.4848 2.1248 19.9808 6.3744 19.4816 4.2432-0.4992 11.616-4.992 22.1056-13.4912a919.7312 919.7312 0 0 0 28.1088-22.4832c10.24-8.4992 20.736-16.992 31.488-25.4848a1926.9952 1926.9952 0 0 0 31.104-25.1136c9.9904-8.2432 18.7328-15.36 26.2272-21.3568 5.504-4 8.128-10.496 7.872-19.488-0.2496-8.9984-3.6224-15.9936-10.112-20.992a1228.544 1228.544 0 0 1-28.864-22.8608 1902.9184 1902.9184 0 0 0-32.9792-26.2336 2137.6256 2137.6256 0 0 1-32.6016-25.856 968.064 968.064 0 0 0-27.7376-21.7408zM180.5248 389.376c-7.488-5.4976-13.7344-6.8672-18.7328-4.1216-4.992 2.752-7.4944 9.3696-7.4944 19.8656v277.3248c0 10.496 1.248 20.6144 3.744 30.3616a62.72 62.72 0 0 0 13.4976 25.856c6.496 7.4944 15.488 13.6192 26.9824 18.368 11.488 4.7424 26.2336 7.1168 44.224 7.1168h56.16c22.464 0.0192 48.2112 0.1408 77.2544 0.3776 30.976 0.2496 64.2112 0.3712 99.6864 0.3712h103.4368a42.112 42.112 0 0 1-0.7488-8.2432v-61.4656c0-7.9936 0.6272-16.3648 1.8752-25.1072 1.248-8.7424 3.7504-16.6144 7.4944-23.6096a48.5568 48.5568 0 0 1 15.3664-17.2416c6.496-4.4928 14.9888-6.7456 25.4848-6.7456h87.6992c0-1.4976 0.2496-3.4944 0.7488-5.9968 0-2.496 0.128-5.4976 0.3712-8.992 0.256-3.5008 0.3776-8 0.3776-13.4912 0-8 1.376-16.992 4.1216-26.9824 2.752-9.9968 6.7456-18.7392 11.9936-26.24 5.248-7.488 11.9936-12.3648 20.2368-14.6112 8.2432-2.2464 17.8624 0.3712 28.8576 7.872 10.9952 6.9952 23.104 15.488 36.352 25.4848a1266.0096 1266.0096 0 0 1 40.8512 32.2304V396.864c0-7.0016-2.624-11.1232-7.872-12.3712-5.248-1.248-12.6144 1.1264-22.112 7.1232-2.9952 1.9968-8 5.248-14.9888 9.7408a5917.1648 5917.1648 0 0 1-22.8608 14.6176c-8.2432 5.248-16.992 10.8672-26.24 16.864a12420.544 12420.544 0 0 1-25.4784 16.4928c-7.744 4.992-14.24 9.2416-19.488 12.736-5.248 3.5008-8.3712 5.504-9.3696 5.9968l-94.4448 58.464c-3.4944 2.0032-8.8704 5.248-16.1152 9.7472a2571.0336 2571.0336 0 0 1-23.232 14.24c-8.2496 4.992-15.872 9.6192-22.8608 13.8688a3873.9264 3873.9264 0 0 1-14.24 8.6208c-2.496 1.4976-6.2464 3.1232-11.2448 4.864-4.9984 1.7536-10.624 2.88-16.864 3.3792-6.2464 0.4992-12.7424 0.2496-19.488-0.7488a46.4064 46.4064 0 0 1-19.1168-7.4944l-20.2368-13.4912A3314.24 3314.24 0 0 0 432 544.896a2994.4 2994.4 0 0 1-19.8592-13.12 260.5568 260.5568 0 0 0-14.2464-8.992c-5.4912-3.0016-11.8656-3.6224-19.1104-1.8752-7.2448 1.7472-13.12 4.6208-17.6128 8.6208-2.496 1.4976-7.1232 5.8688-13.8688 13.12a2662.8928 2662.8928 0 0 0-21.3632 23.232 654.7392 654.7392 0 0 1-21.3568 22.4832c-6.7456 6.752-10.8736 10.624-12.3712 11.6224-7.4944 4.4928-12.992 5.4976-16.4864 2.9952-3.5008-2.496-4-7.2448-1.504-14.24 1.504-4 4.3776-9.7408 8.6208-17.2416 4.2496-7.488 8.6208-15.36 13.12-23.6096a770.4192 770.4192 0 0 0 12.736-24.3584c4-8 7.0016-14.4896 8.9984-19.488 2.496-4.992 3.2512-10.8672 2.2464-17.6128-0.9984-6.7456-4.992-12.3712-11.9872-16.864-2.0032-0.9984-6.1248-3.5008-12.3712-7.4944a3333.7728 3333.7728 0 0 0-22.4832-14.2464 3710.0032 3710.0032 0 0 1-27.7376-17.6128 1545.984 1545.984 0 0 0-28.1024-17.6128 915.1872 915.1872 0 0 1-23.2384-14.6176 494.88 494.88 0 0 0-13.4912-8.6208z m17.9968-139.4112c-15.9936 0-27.232 3.744-33.728 11.2448-6.5024 7.488-9.7472 17.7344-9.7472 30.7264v6.7456c0 4.4992 0.7488 8 2.2464 10.496 1.504 2.496 4.9984 5.7472 10.496 9.7408l306.56 183.6416c7.4944 4.4928 14.24 8.1216 20.2368 10.8672 5.9968 2.752 10.496 4.1216 13.4912 4.1216 2.496-0.4992 7.3728-2.496 14.6176-5.9968a299.5712 299.5712 0 0 1 18.368-8.2432l306.56-184.384c6.9952-5.504 10.9888-9.12 11.9936-10.8736 0.9984-1.7472 1.4976-4.8704 1.4976-9.3696l-0.7488-8.2432c0-12.4928-3.2512-22.2336-9.7472-29.2288-6.496-7.0016-17.984-10.496-34.4768-10.496z"
|
||||
fill="#409eff"
|
||||
p-id="11795"
|
||||
data-spm-anchor-id="a313x.search_index.0.i17.7fe43a81QbZr1X"
|
||||
class="selected"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 260px; width: auto" v-if="v.user_id">
|
||||
@@ -2758,6 +2757,16 @@ export default {
|
||||
query: data
|
||||
});
|
||||
},
|
||||
openAuthorSearch(author) {
|
||||
const artAutId = author && author.art_aut_id != null && author.art_aut_id !== '' ? author.art_aut_id : '';
|
||||
if (!artAutId) {
|
||||
return;
|
||||
}
|
||||
window.open(
|
||||
'https://submission.tmrjournals.com/api/author/index?artAutId=' + encodeURIComponent(artAutId),
|
||||
'_blank'
|
||||
);
|
||||
},
|
||||
//超出传送文件个数限制
|
||||
alertlimit() {
|
||||
this.$message.error('The maximum number of uploaded files has been exceeded');
|
||||
@@ -3382,6 +3391,58 @@ td {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.author-name {
|
||||
max-width: calc(100% - 100px);
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #303133;
|
||||
}
|
||||
.author-action-icons {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.14);
|
||||
transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.author-action-svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
display: block;
|
||||
}
|
||||
.author-action-btn--mail {
|
||||
background: linear-gradient(180deg, #66b1ff 0%, #409eff 100%);
|
||||
box-shadow: 0 1px 4px rgba(64, 158, 255, 0.32);
|
||||
}
|
||||
.author-action-btn--mail:hover {
|
||||
background: linear-gradient(180deg, #79bbff 0%, #53a8ff 100%);
|
||||
box-shadow: 0 3px 8px rgba(64, 158, 255, 0.38);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.author-action-btn--search {
|
||||
background: linear-gradient(180deg, #0088bf 0%, #006699 100%);
|
||||
box-shadow: 0 1px 4px rgba(0, 102, 153, 0.32);
|
||||
}
|
||||
.author-action-btn--search:hover {
|
||||
background: linear-gradient(180deg, #0099cc 0%, #007bb8 100%);
|
||||
box-shadow: 0 3px 8px rgba(0, 102, 153, 0.38);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.author-information {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -105,6 +105,12 @@
|
||||
>
|
||||
<span class="tpl-name">{{ taskCard.countryScopeLabel || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<i class="el-icon-s-data"
|
||||
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">{{ $t('autoPromotion.factorySendCount') }} :</span></i
|
||||
>
|
||||
<span class="tpl-name">{{ taskCard.sendCountText }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1104,6 +1110,7 @@ export default {
|
||||
fieldCount: fieldCount,
|
||||
fieldCountText: String(fieldCount),
|
||||
countryScopeLabel: task.country_scope_label || '-',
|
||||
sendCountText: this.formatFactorySendCount(task && task.send_count),
|
||||
createdAtText: this.formatTaskCreateTime(task),
|
||||
totalCount: total,
|
||||
showCount: idx === 0 && total > 0,
|
||||
@@ -1189,6 +1196,11 @@ export default {
|
||||
.filter(Boolean);
|
||||
return fetchIds.length;
|
||||
},
|
||||
formatFactorySendCount(value) {
|
||||
if (value == null || String(value).trim() === '') return '-';
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? String(n) : String(value).trim();
|
||||
},
|
||||
formatTaskCreateTime(task) {
|
||||
if (!task || typeof task !== 'object') return '';
|
||||
const raw = task.ctime || task.create_time || task.created_at || task.time || '';
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="$t('articleDetailEditor.reReviewNotifyTitle')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="920px"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@open="handleOpen"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<div v-loading="loading" class="re-review-mail-dialog">
|
||||
<div class="mail_shuru">
|
||||
<span class="mail_tit">{{ $t('mailboxSend.to') }}</span>
|
||||
<el-tag v-if="toEmail" type="info" size="small">{{ toEmail }}</el-tag>
|
||||
<span v-else class="mail-empty-tip">-</span>
|
||||
</div>
|
||||
<div class="mail_shuru">
|
||||
<span class="mail_tit">{{ $t('mailboxSend.subject') }}</span>
|
||||
<el-input v-model="mailForm.subject" class="mail_inp" />
|
||||
</div>
|
||||
<div class="mail-editor-wrap">
|
||||
<ckeditor-mail
|
||||
v-if="editorMounted"
|
||||
ref="mailEditor"
|
||||
:key="editorId"
|
||||
:id="editorId"
|
||||
v-model="mailForm.content"
|
||||
:use-safe-init-content="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="mail-footer-bar">
|
||||
<div class="sender-info">
|
||||
<span class="sender-label">{{ $t('mailboxSend.sender') }}</span>
|
||||
<span class="sender-content">
|
||||
<template v-if="senderName && senderEmail">{{ senderName }} <{{ senderEmail }}></template>
|
||||
<template v-else>-</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button @click="dialogVisible = false">{{ $t('mailboxSend.cancel') || 'Cancel' }}</el-button>
|
||||
<el-button type="primary" icon="el-icon-s-promotion" :loading="sendLoading" @click="handleSend">
|
||||
{{ $t('mailboxSend.send') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CkeditorMail from '@/components/page/components/email/CkeditorMail.vue';
|
||||
import { applyReviewNotifyTemplateVariables } from '@/utils/mailTemplatePreview';
|
||||
|
||||
const RE_REVIEW_TEMPLATE_ID = '82';
|
||||
/** 复审通知固定发件邮箱 j_email_id */
|
||||
const RE_REVIEW_SENDER_J_EMAIL_ID = '23';
|
||||
|
||||
export default {
|
||||
name: 'ReReviewNotifyMailDialog',
|
||||
components: {
|
||||
CkeditorMail
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
reviewer: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
articleId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
articleTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 稿件 SN,对应接口字段 accept_sn */
|
||||
acceptSn: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
journalId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
/** 期刊名称,对应 getArticleDetail 返回字段 journal_name */
|
||||
journalName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 期刊官网,对应 getArticleDetail 返回字段 */
|
||||
journalUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
sendLoading: false,
|
||||
editorMounted: false,
|
||||
editorId: 're-review-mail-' + Date.now(),
|
||||
mailForm: {
|
||||
subject: '',
|
||||
content: ''
|
||||
},
|
||||
toEmail: '',
|
||||
senderName: '',
|
||||
senderEmail: '',
|
||||
jEmailId: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleOpen() {
|
||||
this.editorMounted = false;
|
||||
this.editorId = 're-review-mail-' + Date.now();
|
||||
this.resetForm();
|
||||
this.loadMailData();
|
||||
},
|
||||
handleClosed() {
|
||||
this.editorMounted = false;
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
this.mailForm = { subject: '', content: '' };
|
||||
this.toEmail = '';
|
||||
this.senderName = '';
|
||||
this.senderEmail = '';
|
||||
this.jEmailId = '';
|
||||
},
|
||||
async loadMailData() {
|
||||
this.loading = true;
|
||||
this.editorMounted = false;
|
||||
try {
|
||||
await Promise.all([this.loadReviewerEmail(), this.loadSenderAccount()]);
|
||||
await this.loadTemplate();
|
||||
await this.$nextTick();
|
||||
this.editorMounted = true;
|
||||
} catch (e) {
|
||||
console.error('loadMailData failed', e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadReviewerEmail() {
|
||||
const reviewer = this.reviewer || {};
|
||||
if (reviewer.email) {
|
||||
this.toEmail = reviewer.email;
|
||||
return;
|
||||
}
|
||||
if (!reviewer.art_rev_id || !this.articleId) return;
|
||||
const res = await this.$api.post('api/Workbench/getArticleReviewDetail', {
|
||||
article_id: this.articleId,
|
||||
art_rev_id: reviewer.art_rev_id,
|
||||
account: localStorage.getItem('U_name')
|
||||
});
|
||||
if (res && res.status === 1 && res.data && res.data.article_reviewer) {
|
||||
this.toEmail = res.data.article_reviewer.email || '';
|
||||
}
|
||||
},
|
||||
async loadSenderAccount() {
|
||||
this.jEmailId = RE_REVIEW_SENDER_J_EMAIL_ID;
|
||||
const emailRes = await this.$api.post('api/email_client/getOneEmail', {
|
||||
j_email_id: this.jEmailId
|
||||
});
|
||||
if (emailRes && emailRes.code === 0 && emailRes.data && emailRes.data.email) {
|
||||
const email = emailRes.data.email;
|
||||
this.senderName = email.smtp_from_name || email.smtp_user || '';
|
||||
this.senderEmail = email.smtp_user || '';
|
||||
}
|
||||
},
|
||||
async loadTemplate() {
|
||||
const res = await this.$api.post('api/mail_template/getTemplate', {
|
||||
template_id: RE_REVIEW_TEMPLATE_ID
|
||||
});
|
||||
if (!res || res.code !== 0) {
|
||||
this.$message.error((res && res.msg) || 'Failed to load mail template');
|
||||
return;
|
||||
}
|
||||
const data = (res && res.data) || {};
|
||||
const t = data.template || data.detail || data || {};
|
||||
const vars = {
|
||||
title: this.articleTitle,
|
||||
accept_sn: this.acceptSn,
|
||||
journal_name: this.journalName,
|
||||
journal_url: this.journalUrl
|
||||
};
|
||||
const subjectRaw = t.subject != null ? String(t.subject) : '';
|
||||
const bodyRaw = t.body_html != null ? t.body_html : t.body != null ? t.body : '';
|
||||
this.mailForm.subject = applyReviewNotifyTemplateVariables(subjectRaw, vars);
|
||||
this.mailForm.content = applyReviewNotifyTemplateVariables(String(bodyRaw || ''), vars);
|
||||
},
|
||||
async handleSend() {
|
||||
if (!this.toEmail) {
|
||||
this.$message.warning(this.$t('mailboxSend.validateTo'));
|
||||
return;
|
||||
}
|
||||
if (!this.mailForm.subject) {
|
||||
this.$message.warning(this.$t('mailboxSend.validateSubject'));
|
||||
return;
|
||||
}
|
||||
if (!this.jEmailId) {
|
||||
this.$message.warning(this.$t('mailboxSend.needAccount'));
|
||||
return;
|
||||
}
|
||||
if (!this.journalId) {
|
||||
this.$message.warning(this.$t('mailboxSend.needAccount'));
|
||||
return;
|
||||
}
|
||||
this.sendLoading = true;
|
||||
try {
|
||||
let content = this.mailForm.content || '';
|
||||
const editorRef = this.$refs.mailEditor;
|
||||
if (editorRef && typeof editorRef.getInlinedContent === 'function') {
|
||||
content = editorRef.getInlinedContent() || content;
|
||||
}
|
||||
const res = await this.$api.post('api/email_client/sendOne', {
|
||||
to_email: this.toEmail,
|
||||
subject: this.mailForm.subject,
|
||||
content,
|
||||
j_email_id: this.jEmailId,
|
||||
journal_id: String(this.journalId)
|
||||
});
|
||||
if (res && res.code === 0) {
|
||||
this.$message.success(this.$t('mailboxSend.sendSuccess'));
|
||||
this.dialogVisible = false;
|
||||
this.$emit('sent');
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('mailboxSend.sendFail'));
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('mailboxSend.sendFail'));
|
||||
} finally {
|
||||
this.sendLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.re-review-mail-dialog {
|
||||
min-height: 420px;
|
||||
}
|
||||
.mail_shuru {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
line-height: 40px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.mail_tit {
|
||||
width: 72px;
|
||||
flex-shrink: 0;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mail_inp {
|
||||
flex: 1;
|
||||
}
|
||||
.mail_inp ::v-deep .el-input__inner {
|
||||
border: none;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-radius: 0;
|
||||
}
|
||||
.mail-editor-wrap {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.mail-footer-bar {
|
||||
margin-top: 16px;
|
||||
padding: 10px 15px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.sender-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
.sender-label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.sender-content {
|
||||
color: #006699;
|
||||
}
|
||||
.mail-empty-tip {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -44,15 +44,25 @@
|
||||
<el-option :label="$t('autoPromotionLogs.taskLogState3')" value="3"></el-option>
|
||||
<el-option :label="$t('autoPromotionLogs.taskLogState4')" value="4"></el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="logKeyword"
|
||||
size="mini"
|
||||
clearable
|
||||
:placeholder="$t('autoPromotionLogs.searchPlaceholder')"
|
||||
prefix-icon="el-icon-search"
|
||||
class="log-keyword-input"
|
||||
@keyup.enter.native="handleLogKeywordSearch"
|
||||
@clear="handleLogKeywordSearch"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
icon="el-icon-refresh"
|
||||
icon="el-icon-search"
|
||||
:loading="loading"
|
||||
class="log-refresh-btn"
|
||||
@click="handleRefreshLogs"
|
||||
class="log-search-btn"
|
||||
@click="handleLogKeywordSearch"
|
||||
>
|
||||
{{ $t('autoPromotionLogs.logRefresh') }}
|
||||
{{ $t('autoPromotionLogs.searchBtn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -69,7 +79,7 @@
|
||||
|
||||
<div class="list-wrapper">
|
||||
<div
|
||||
v-for="(item, rowIndex) in fullData"
|
||||
v-for="(item, rowIndex) in displayLogData"
|
||||
:key="item.id"
|
||||
class="log-row"
|
||||
:class="{ 'row-error': item.isErrorRow }"
|
||||
@@ -137,7 +147,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && fullData.length === 0" class="empty-ui">
|
||||
<div v-if="!loading && displayLogData.length === 0" class="empty-ui">
|
||||
<i class="el-icon-document"></i>
|
||||
<p>{{ $t('autoPromotionLogs.emptyLogs') }}</p>
|
||||
</div>
|
||||
@@ -147,7 +157,7 @@
|
||||
<el-pagination
|
||||
:current-page.sync="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:page-sizes="[20, 50, 100, 500]"
|
||||
:total="totalCount"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handlePageSizeChange"
|
||||
@@ -306,10 +316,11 @@ export default {
|
||||
return {
|
||||
loading: false,
|
||||
logState: '',
|
||||
logKeyword: '',
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
totalCount: 0,
|
||||
fullData: [],
|
||||
displayLogData: [],
|
||||
logDetailVisible: false,
|
||||
logDetailLoading: false,
|
||||
logDetailMode: 'preview',
|
||||
@@ -376,6 +387,7 @@ export default {
|
||||
if (oldVal === '' || oldVal == null) return;
|
||||
this.currentPage = 1;
|
||||
this.logState = '';
|
||||
this.logKeyword = '';
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
@@ -383,13 +395,15 @@ export default {
|
||||
handleOpen() {
|
||||
this.currentPage = 1;
|
||||
this.logState = '';
|
||||
this.logKeyword = '';
|
||||
this.loadData();
|
||||
},
|
||||
handleLogStateChange() {
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
handleRefreshLogs() {
|
||||
handleLogKeywordSearch() {
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
handlePageSizeChange(size) {
|
||||
@@ -399,7 +413,7 @@ export default {
|
||||
},
|
||||
async loadData() {
|
||||
if (!this.taskId) {
|
||||
this.fullData = [];
|
||||
this.displayLogData = [];
|
||||
this.totalCount = 0;
|
||||
return;
|
||||
}
|
||||
@@ -413,10 +427,14 @@ export default {
|
||||
if (this.logState !== '' && this.logState != null) {
|
||||
params.state = String(this.logState);
|
||||
}
|
||||
const keyword = String(this.logKeyword || '').trim();
|
||||
if (keyword) {
|
||||
params.keyword = keyword;
|
||||
}
|
||||
const res = await this.$api.post(API_GET_TASK_LOGS, params);
|
||||
if (res && res.code != null && Number(res.code) !== 0) {
|
||||
this.$message.error(res.msg || this.$t('autoPromotionLogs.logLoadFailed'));
|
||||
this.fullData = [];
|
||||
this.displayLogData = [];
|
||||
this.totalCount = 0;
|
||||
return;
|
||||
}
|
||||
@@ -431,9 +449,9 @@ export default {
|
||||
this.totalCount = Number(
|
||||
(payload && (payload.total != null ? payload.total : payload.count)) || rawList.length || 0
|
||||
);
|
||||
this.fullData = rawList.map((row, idx) => this.mapLogItem(row, idx));
|
||||
this.displayLogData = rawList.map((row, idx) => this.mapLogItem(row, idx));
|
||||
} catch (e) {
|
||||
this.fullData = [];
|
||||
this.displayLogData = [];
|
||||
this.totalCount = 0;
|
||||
this.$message.error((e && e.message) || this.$t('autoPromotionLogs.logLoadFailed'));
|
||||
} finally {
|
||||
@@ -771,7 +789,11 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-refresh-btn {
|
||||
.log-keyword-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.log-search-btn {
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -220,19 +220,30 @@
|
||||
</h4>
|
||||
<div
|
||||
class="status-confirm-box is-clickable"
|
||||
:class="{ 'has-selected-fields': selectedFieldLabels.length }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="openFieldDialog"
|
||||
@keyup.enter="openFieldDialog"
|
||||
>
|
||||
<el-tooltip
|
||||
v-if="selectedFieldLabels.length"
|
||||
:content="$t('autoPromotion.copySelectedFields')"
|
||||
placement="top"
|
||||
>
|
||||
<i
|
||||
class="el-icon-document-copy field-copy-icon"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="$t('autoPromotion.copySelectedFields')"
|
||||
@click.stop="copySelectedFieldText"
|
||||
@keyup.enter.stop="copySelectedFieldText"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
<div v-if="selectedFieldLabels.length" class="selected-tags">
|
||||
<el-tag v-for="label in selectedFieldLabels" :key="'pf-' + label" size="mini" type="info" effect="plain">{{ label }}</el-tag>
|
||||
</div>
|
||||
<!-- <div v-else class="empty-inline">
|
||||
<i class="el-icon-collection-tag"></i>
|
||||
<span>{{ $t('autoPromotion.factoryClickConfigureFields') }}</span>
|
||||
</div> -->
|
||||
<div class="field-tip">
|
||||
<div v-else class="field-tip">
|
||||
<span class="factory-required-star" aria-hidden="true">*</span
|
||||
><span>{{ $t('autoPromotion.factoryPromotionFieldsBlockTip') }}</span>
|
||||
</div>
|
||||
@@ -361,6 +372,7 @@
|
||||
},
|
||||
expertTypeCountsLoading: false,
|
||||
expertTypeCountsReqSeq: 0,
|
||||
expertDbCountReqSeq: 0,
|
||||
factoryStartPromotion: true,
|
||||
factoryConfig: { defaultTemplateId: '', defaultStyleId: '' },
|
||||
factoryTemplateName: '',
|
||||
@@ -550,8 +562,9 @@
|
||||
const list = (this.availableFields || []).filter(function (item) {
|
||||
if (!tokens.length) return true;
|
||||
const label = normalize(item.label || '');
|
||||
const fuzzy = tokens.length === 1;
|
||||
return tokens.some(function (t) {
|
||||
return t === label;
|
||||
return fuzzy ? label === t || label.includes(t) : label === t;
|
||||
});
|
||||
});
|
||||
return list.slice().sort(function (a, b) {
|
||||
@@ -1006,12 +1019,7 @@
|
||||
limit: 1
|
||||
})
|
||||
.catch(() => null),
|
||||
this.$api
|
||||
.post('api/expert_manage/getList', {
|
||||
pageIndex: 1,
|
||||
pageSize: 1
|
||||
})
|
||||
.catch(() => null)
|
||||
this.$api.post('api/expert_manage/getList', this.buildExpertDbListParams()).catch(() => null)
|
||||
]);
|
||||
|
||||
const next = {
|
||||
@@ -1058,6 +1066,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
buildExpertDbListParams() {
|
||||
const params = {
|
||||
pageIndex: 1,
|
||||
pageSize: 1
|
||||
};
|
||||
const zoneIds = (this.factoryZoneCountryIds || []).map(String);
|
||||
if (zoneIds.length === 1 && zoneIds[0] === 'country_china') {
|
||||
params.country = 'China';
|
||||
}
|
||||
return params;
|
||||
},
|
||||
async refreshExpertDbCount() {
|
||||
const journalId = this.selectedJournalId;
|
||||
if (!journalId) return;
|
||||
const reqId = ++this.expertDbCountReqSeq;
|
||||
this.expertTypeCountsLoading = true;
|
||||
try {
|
||||
const res = await this.$api.post('api/expert_manage/getList', this.buildExpertDbListParams()).catch(() => null);
|
||||
if (reqId !== this.expertDbCountReqSeq) return;
|
||||
const safeCount = function (v) {
|
||||
return v == null || isNaN(Number(v)) ? 0 : Number(v);
|
||||
};
|
||||
let expertDb = null;
|
||||
if (res && res.code === 0) {
|
||||
const total = (res.data && (res.data.total || res.data.count)) || res.total;
|
||||
expertDb = safeCount(total);
|
||||
}
|
||||
this.expertTypeCounts = Object.assign({}, this.expertTypeCounts, { expertDb: expertDb });
|
||||
} finally {
|
||||
if (reqId === this.expertDbCountReqSeq) {
|
||||
this.expertTypeCountsLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadAccounts(id) {
|
||||
this.accountsLoading = true;
|
||||
try {
|
||||
@@ -1239,6 +1281,20 @@
|
||||
this.syncActiveStep();
|
||||
},
|
||||
selectAllFields() {
|
||||
const kwRaw = String(this.fieldSearchText || '').trim();
|
||||
if (kwRaw) {
|
||||
const filtered = this.sortedFilteredFields || [];
|
||||
if (!filtered.length) {
|
||||
this.$message.warning(this.$t('autoPromotion.noFieldMatch'));
|
||||
return;
|
||||
}
|
||||
const idSet = new Set((this.factoryFieldIds || []).map(String));
|
||||
filtered.forEach(function (f) {
|
||||
idSet.add(String(f.id));
|
||||
});
|
||||
this.factoryFieldIds = Array.from(idSet);
|
||||
return;
|
||||
}
|
||||
this.factoryFieldIds = (this.availableFields || []).map(function (f) {
|
||||
return String(f.id);
|
||||
});
|
||||
@@ -1246,6 +1302,42 @@
|
||||
clearAllFields() {
|
||||
this.factoryFieldIds = [];
|
||||
},
|
||||
copySelectedFieldText() {
|
||||
const labels = this.selectedFieldLabels || [];
|
||||
if (!labels.length) {
|
||||
this.$message.warning(this.$t('autoPromotion.copySelectedFieldsEmpty'));
|
||||
return;
|
||||
}
|
||||
const text = labels.join('; ');
|
||||
const done = function () {
|
||||
this.$message.success(this.$t('autoPromotion.copySelectedFieldsSuccess', { count: labels.length }));
|
||||
}.bind(this);
|
||||
const fail = function () {
|
||||
this.$message.warning(this.$t('autoPromotion.factoryBatchImportCopyFail'));
|
||||
}.bind(this);
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
||||
navigator.clipboard.writeText(text).then(done).catch(function () {
|
||||
this.fallbackCopyFieldText(text, done, fail);
|
||||
}.bind(this));
|
||||
return;
|
||||
}
|
||||
this.fallbackCopyFieldText(text, done, fail);
|
||||
},
|
||||
fallbackCopyFieldText(text, onSuccess, onFail) {
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
onFail();
|
||||
}
|
||||
},
|
||||
selectAllAccounts() {
|
||||
this.selectedEmailIds = this.accountList.map(function (a) {
|
||||
return String(a.j_email_id);
|
||||
@@ -1375,6 +1467,9 @@
|
||||
factoryZoneCountryIds: {
|
||||
handler: function () {
|
||||
this.syncActiveStep();
|
||||
if (this.selectedJournalId) {
|
||||
this.refreshExpertDbCount();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
@@ -1547,6 +1642,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-confirm-box.has-selected-fields {
|
||||
position: relative;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.status-confirm-box-radio {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -1561,7 +1661,23 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-copy-icon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1;
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.field-copy-icon:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
|
||||
@@ -40,6 +40,14 @@
|
||||
|
||||
<el-tab-pane :label="$t('mailTemplate.styleTab')" name="style">
|
||||
<div class="card-grid">
|
||||
<div
|
||||
v-if="styleOptional"
|
||||
:class="['card-item', 'card-item-none', { active: !selectedHeaderId }]"
|
||||
@click="selectedHeaderId = null"
|
||||
>
|
||||
<p class="card-title">{{ $t('mailTemplate.noStyle') }}</p>
|
||||
<p class="card-desc">{{ $t('mailTemplate.noStyleDesc') }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in headerStyles"
|
||||
:key="item.id"
|
||||
@@ -111,6 +119,8 @@ export default {
|
||||
// 可选:打开时回显的风格/模板
|
||||
initialStyleId: { type: [String, Number], default: '' },
|
||||
initialTemplateId: { type: [String, Number], default: '' },
|
||||
// 为 true 时仅选模板即可应用,Style 可选(可不选)
|
||||
styleOptional: { type: Boolean, default: false },
|
||||
// 可选:来源页面标识,用于回跳时决定 mailboxMouldDetail 返回到哪里
|
||||
// 例如:'autoPromotion':返回自动化推广;默认返回邮件模版列表
|
||||
returnSource: { type: String, default: '' }
|
||||
@@ -163,24 +173,36 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
applyDisabled() {
|
||||
return this.initLoading || this.contentLoading || !this.selectedHeaderId || !this.selectedContentId;
|
||||
if (this.initLoading || this.contentLoading) return true;
|
||||
if (!this.selectedContentId) return true;
|
||||
if (!this.styleOptional && !this.selectedHeaderId) return true;
|
||||
return false;
|
||||
},
|
||||
hasValidPreview() {
|
||||
const header = this.headerStyles.find((h) => h.id === this.selectedHeaderId);
|
||||
const content = this.contentTemplates.find((c) => c.id === this.selectedContentId);
|
||||
if (!content) return false;
|
||||
if (this.styleOptional && !this.selectedHeaderId) return true;
|
||||
const header = this.headerStyles.find((h) => h.id === this.selectedHeaderId);
|
||||
return !!(header && content);
|
||||
},
|
||||
// 【关键】拼接 HTML
|
||||
combinedHtml() {
|
||||
if (this.initLoading) return '';
|
||||
if (!this.hasValidPreview) return '';
|
||||
const header = this.headerStyles.find((h) => h.id === this.selectedHeaderId);
|
||||
const content = this.contentTemplates.find((c) => c.id === this.selectedContentId);
|
||||
if (!content) return '';
|
||||
if (this.styleOptional && !this.selectedHeaderId) {
|
||||
return content.bodyHtml || '';
|
||||
}
|
||||
const header = this.headerStyles.find((h) => h.id === this.selectedHeaderId);
|
||||
return `${header.htmlHeader}${content.bodyHtml}${header.htmlFooter}`;
|
||||
},
|
||||
currentSelectionText() {
|
||||
const h = this.headerStyles.find((h) => h.id === this.selectedHeaderId);
|
||||
const c = this.contentTemplates.find((c) => c.id === this.selectedContentId);
|
||||
if (this.styleOptional && !this.selectedHeaderId) {
|
||||
return c ? c.name : '';
|
||||
}
|
||||
return `${c ? c.name : ''} + ${h ? h.name : ''}`;
|
||||
},
|
||||
currentJournalLabel() {
|
||||
@@ -306,8 +328,8 @@ export default {
|
||||
htmlFooter: item.footer_html || ''
|
||||
}));
|
||||
|
||||
// 默认选中第一条
|
||||
if (this.headerStyles.length && !this.selectedHeaderId) {
|
||||
// 默认选中第一条(Style 可选时不自动选中)
|
||||
if (this.headerStyles.length && !this.selectedHeaderId && !this.styleOptional) {
|
||||
this.selectedHeaderId = this.headerStyles[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -382,13 +404,14 @@ export default {
|
||||
},
|
||||
submit() {
|
||||
if (this.applyDisabled) return;
|
||||
// 将拼接好的 HTML 及选择信息抛给父组件(兼容:父组件也可只用 html)
|
||||
const header = this.headerStyles.find((h) => h.id === this.selectedHeaderId);
|
||||
const header = this.selectedHeaderId
|
||||
? this.headerStyles.find((h) => h.id === this.selectedHeaderId)
|
||||
: null;
|
||||
const content = this.contentTemplates.find((c) => c.id === this.selectedContentId);
|
||||
this.$emit('confirm', {
|
||||
html: this.combinedHtml,
|
||||
journal_id: this.selectedJournalId,
|
||||
style_id: this.selectedHeaderId,
|
||||
style_id: this.selectedHeaderId || '',
|
||||
template_id: this.selectedContentId,
|
||||
style: header || null,
|
||||
template: content || null
|
||||
@@ -494,6 +517,12 @@ export default {
|
||||
background: #f5f7ff;
|
||||
outline: 1px solid #6366f1;
|
||||
}
|
||||
.card-item-none {
|
||||
min-height: 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
<ul class="folder-list">
|
||||
<li :class="{ active: currentFolder === 'inbox' }" @click="switchFolder('inbox')">
|
||||
<i class="el-icon-message"></i> {{ $t('mailboxCollect.inboxTab') }}
|
||||
<span class="badge" v-if="queryIn.num > 0">{{ queryIn.num }}</span>
|
||||
<span class="badge" v-if="inboxCount > 0">{{ inboxCount }}</span>
|
||||
</li>
|
||||
<li :class="{ active: currentFolder === 'trash' }" @click="switchFolder('trash')">
|
||||
<i class="el-icon-delete"></i> {{ $t('mailboxCollect.trashTab') }}
|
||||
<span class="badge" v-if="trashCount > 0">{{ trashCount }}</span>
|
||||
</li>
|
||||
<!-- <li :class="{ active: currentFolder === 'sent' }" @click="switchFolder('sent')">
|
||||
<i class="el-icon-position"></i><span style="font-size: 14px">{{ $t('mailboxCollect.outboxTab') }}</span>
|
||||
@@ -194,6 +198,7 @@ import {
|
||||
inboxTotalPages: 1,
|
||||
inboxTotal: 0,
|
||||
inboxLoadingMore: false,
|
||||
inboxLoadingAll: false,
|
||||
inboxLoading: false,
|
||||
listWidth: 350,
|
||||
minWidth: 260,
|
||||
@@ -219,8 +224,22 @@ import {
|
||||
},
|
||||
components: { MailDetail },
|
||||
computed: {
|
||||
inboxList() {
|
||||
return this.tableData_in.filter((item) => !this.isFilteredMail(item));
|
||||
},
|
||||
trashList() {
|
||||
return this.tableData_in.filter((item) => this.isFilteredMail(item));
|
||||
},
|
||||
inboxCount() {
|
||||
return this.inboxList.length;
|
||||
},
|
||||
trashCount() {
|
||||
return this.trashList.length;
|
||||
},
|
||||
displayList() {
|
||||
return this.currentFolder === 'inbox' ? this.tableData_in : this.tableData_out;
|
||||
if (this.currentFolder === 'trash') return this.trashList;
|
||||
if (this.currentFolder === 'sent') return this.tableData_out;
|
||||
return this.inboxList;
|
||||
},
|
||||
selectedAccountEmail() {
|
||||
return this.selectedAccount ? this.selectedAccount.smtp_user : '';
|
||||
@@ -229,6 +248,16 @@ import {
|
||||
created() {
|
||||
this.loadJournals();
|
||||
this.initAccountSelection();
|
||||
this._skipNextActivatedSync = true;
|
||||
},
|
||||
activated() {
|
||||
if (this._skipNextActivatedSync) {
|
||||
this._skipNextActivatedSync = false;
|
||||
return;
|
||||
}
|
||||
if (this.selectedAccount) {
|
||||
this.autoSyncInbox();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopInboxSse(true);
|
||||
@@ -311,7 +340,7 @@ import {
|
||||
: null;
|
||||
if (res && Number(res.code) === 0 && email) {
|
||||
this.selectedAccount = email;
|
||||
this.fetchData();
|
||||
this.autoSyncInbox();
|
||||
this.startInboxSse();
|
||||
return;
|
||||
}
|
||||
@@ -381,8 +410,8 @@ import {
|
||||
localStorage.setItem('mailboxCollect_journal_id', String(row.journal_id));
|
||||
}
|
||||
|
||||
// 不再写入路由 query,直接拉取数据
|
||||
this.fetchData();
|
||||
// 切换账号后自动收信并刷新列表
|
||||
this.autoSyncInbox();
|
||||
this.startInboxSse();
|
||||
},
|
||||
handleAccountDialogBeforeClose(done) {
|
||||
@@ -439,27 +468,114 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
this.insertMailsFromBuffer();
|
||||
});
|
||||
},
|
||||
isFilteredMail(item) {
|
||||
if (!item) return false;
|
||||
const email = String(item.email || item.from_email || '')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
const fromName = String(item.from_name || '').trim();
|
||||
const subject = String(item.subject || '');
|
||||
const subjectLower = subject.toLowerCase();
|
||||
const excerpt = this.stripHtml(item.content || '').toLowerCase();
|
||||
const text = subjectLower + ' ' + excerpt;
|
||||
|
||||
if (email.includes('mailer-daemon')) return true;
|
||||
if (/failure\s+notice/i.test(subjectLower)) return true;
|
||||
|
||||
if (email.includes('mailsupport.aliyun.com') || email.includes('no-reply@mailsupport.aliyun')) {
|
||||
return true;
|
||||
}
|
||||
if (fromName.includes('阿里邮箱')) return true;
|
||||
if (text.includes('的退信')) return true;
|
||||
if (text.includes('退信') && (text.includes('aliyun') || text.includes('mailsupport'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subject.includes('自动回复') || fromName.includes('自动回复')) return true;
|
||||
if (/auto[\s-]?reply/i.test(subject)) return true;
|
||||
if (/automatic\s+reply/i.test(subject)) return true;
|
||||
if (/out\s+of\s+(the\s+)?office/i.test(subjectLower)) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
mapInboxRows(list) {
|
||||
return (Array.isArray(list) ? list : []).map((item) => ({
|
||||
id: item.inbox_id || item.id,
|
||||
inbox_id: item.inbox_id || item.id,
|
||||
email: item.from_email,
|
||||
has_attachment: item.has_attachment,
|
||||
from_name: item.from_name,
|
||||
subject: item.subject,
|
||||
email_date: item.email_date,
|
||||
content: item.content_html || item.content_text || '',
|
||||
is_read: item.is_read
|
||||
}));
|
||||
},
|
||||
appendInboxRows(rows) {
|
||||
const filteredRows = rows.filter(
|
||||
(row) => !this.tableData_in.some((existing) => existing.id === row.id)
|
||||
);
|
||||
if (filteredRows.length) {
|
||||
this.tableData_in = this.tableData_in.concat(filteredRows);
|
||||
}
|
||||
},
|
||||
applyInboxPageMeta(data) {
|
||||
this.inboxPage = data.page != null ? Number(data.page) : this.inboxPage;
|
||||
this.inboxTotalPages = data.total_pages != null ? Number(data.total_pages) : 1;
|
||||
this.inboxTotal = data.total != null ? Number(data.total) : this.tableData_in.length;
|
||||
},
|
||||
loadRemainingInboxPages() {
|
||||
if (!this.selectedAccount || this.inboxLoadingAll) return;
|
||||
if (this.inboxPage >= this.inboxTotalPages) return;
|
||||
|
||||
this.inboxLoadingAll = true;
|
||||
const loadNext = (page) => {
|
||||
if (page > this.inboxTotalPages) {
|
||||
this.inboxLoadingAll = false;
|
||||
this.inboxLoadingMore = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
const params = {
|
||||
j_email_id: this.selectedAccount.j_email_id,
|
||||
journal_id: this.selectedAccount.journal_id,
|
||||
page,
|
||||
per_page: this.inboxPerPage
|
||||
};
|
||||
return this.$api
|
||||
.post(API.getInboxList, params)
|
||||
.then((res) => {
|
||||
const data = res && res.data ? res.data : {};
|
||||
const list = Array.isArray(data.list) ? data.list : Array.isArray(data) ? data : [];
|
||||
this.appendInboxRows(this.mapInboxRows(list));
|
||||
this.applyInboxPageMeta({ ...data, page });
|
||||
return loadNext(page + 1);
|
||||
})
|
||||
.catch(() => {
|
||||
this.inboxLoadingAll = false;
|
||||
this.inboxLoadingMore = false;
|
||||
});
|
||||
};
|
||||
|
||||
this.inboxLoadingMore = true;
|
||||
loadNext(this.inboxPage + 1);
|
||||
},
|
||||
// 拉取收件列表,支持分页:page=1 时替换列表,page>1 时追加;接口返回 total、page、per_page、total_pages
|
||||
fetchData(page) {
|
||||
if (!this.selectedAccount) return;
|
||||
const isFirstPage = page === 1 || page == null;
|
||||
if (isFirstPage) {
|
||||
this.inboxPage = 1;
|
||||
// 首次拉取:显示加载中
|
||||
this.inboxLoadingAll = false;
|
||||
this.inboxLoading = true;
|
||||
// 避免复用旧列表造成“先空后满”的闪烁
|
||||
this.tableData_in = [];
|
||||
}
|
||||
const params = {
|
||||
j_email_id: this.selectedAccount.j_email_id,
|
||||
journal_id: this.selectedAccount.journal_id,
|
||||
// keyword: this.searchKeyword || '',
|
||||
page: isFirstPage ? 1 : page,
|
||||
per_page: this.inboxPerPage
|
||||
};
|
||||
if (isFirstPage) {
|
||||
// 第一页不显示 loadingMore,仅翻页时显示
|
||||
} else {
|
||||
if (!isFirstPage) {
|
||||
this.inboxLoadingMore = true;
|
||||
}
|
||||
this.$api
|
||||
@@ -467,33 +583,18 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
.then((res) => {
|
||||
const data = res && res.data ? res.data : {};
|
||||
const list = Array.isArray(data.list) ? data.list : Array.isArray(data) ? data : [];
|
||||
const rows = list.map((item) => ({
|
||||
id: item.inbox_id || item.id,
|
||||
inbox_id: item.inbox_id || item.id,
|
||||
email: item.from_email,
|
||||
has_attachment: item.has_attachment,
|
||||
from_name: item.from_name,
|
||||
subject: item.subject,
|
||||
email_date: item.email_date,
|
||||
content: item.content_html || item.content_text || '',
|
||||
is_read: item.is_read
|
||||
}));
|
||||
const rows = this.mapInboxRows(list);
|
||||
if (isFirstPage) {
|
||||
this.tableData_in = rows;
|
||||
} else {
|
||||
// 过滤掉已经在列表中存在的 id (可能来自 SSE 之前手动插入的)
|
||||
const filteredRows = rows.filter(row =>
|
||||
!this.tableData_in.some(existing => existing.id === row.id)
|
||||
);
|
||||
this.tableData_in = this.tableData_in.concat(filteredRows);
|
||||
|
||||
this.appendInboxRows(rows);
|
||||
}
|
||||
this.inboxPage = data.page != null ? Number(data.page) : isFirstPage ? 1 : this.inboxPage;
|
||||
this.inboxTotalPages = data.total_pages != null ? Number(data.total_pages) : 1;
|
||||
this.inboxTotal = data.total != null ? Number(data.total) : this.tableData_in.length;
|
||||
this.queryIn.num = this.inboxTotal;
|
||||
this.applyInboxPageMeta(data);
|
||||
this.inboxLoadingMore = false;
|
||||
if (isFirstPage) this.inboxLoading = false;
|
||||
if (isFirstPage) {
|
||||
this.inboxLoading = false;
|
||||
this.loadRemainingInboxPages();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.inboxLoadingMore = false;
|
||||
@@ -502,7 +603,7 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
},
|
||||
switchFolder(f) {
|
||||
this.currentFolder = f;
|
||||
this.activeMailId = null;
|
||||
this.closeDetail();
|
||||
},
|
||||
/** 接口 code 可能是数字 0 或字符串 "0" */
|
||||
isEmailApiSuccess(res) {
|
||||
@@ -651,29 +752,52 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
.catch(() => {});
|
||||
});
|
||||
},
|
||||
// 同步收件箱:api/email_client/syncInbox 参数 j_email_id、journal_id(均为 String),完成后重新拉取 displayList
|
||||
autoSyncInbox() {
|
||||
return this.syncInboxAndRefresh({ showMessage: false, fetchOnFail: true });
|
||||
},
|
||||
handleSyncInbox() {
|
||||
if (!this.selectedAccount) return;
|
||||
this.syncInboxAndRefresh({ showMessage: true, fetchOnFail: false });
|
||||
},
|
||||
syncInboxAndRefresh(options = {}) {
|
||||
const showMessage = options.showMessage !== false;
|
||||
const fetchOnFail = options.fetchOnFail === true;
|
||||
if (!this.selectedAccount) return Promise.resolve(false);
|
||||
if (this.syncLoading) return Promise.resolve(false);
|
||||
this.syncLoading = true;
|
||||
const params = {
|
||||
j_email_id: String(this.selectedAccount.j_email_id),
|
||||
journal_id: String(this.selectedAccount.journal_id)
|
||||
};
|
||||
this.$api
|
||||
return this.$api
|
||||
.post(API.syncInbox, params)
|
||||
.then((res) => {
|
||||
this.syncLoading = false;
|
||||
if (res && res.code === 0) {
|
||||
this.newMailsBuffer=[]
|
||||
this.$message.success(this.$t('mailboxCollect.syncSuccess'));
|
||||
this.newMailsBuffer = [];
|
||||
if (showMessage) {
|
||||
this.$message.success(this.$t('mailboxCollect.syncSuccess'));
|
||||
}
|
||||
this.fetchData();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
if (showMessage) {
|
||||
this.$message.error((res && res.msg) || this.$t('mailboxCollect.syncFail'));
|
||||
}
|
||||
if (fetchOnFail) {
|
||||
this.fetchData();
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.catch(() => {
|
||||
if (showMessage) {
|
||||
this.$message.error(this.$t('mailboxCollect.syncFail'));
|
||||
}
|
||||
if (fetchOnFail) {
|
||||
this.fetchData();
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
this.syncLoading = false;
|
||||
this.$message.error(this.$t('mailboxCollect.syncFail'));
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -738,7 +862,7 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
},
|
||||
onListScroll(e) {
|
||||
const el = e.target;
|
||||
if (!el || this.currentFolder !== 'inbox' || this.inboxLoadingMore) return;
|
||||
if (!el || this.inboxLoadingMore || this.inboxLoadingAll) return;
|
||||
if (this.inboxPage >= this.inboxTotalPages) return;
|
||||
const threshold = 80;
|
||||
if (el.scrollHeight - el.scrollTop - el.clientHeight <= threshold) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
851
src/components/page/nursingExpertInsight.vue
Normal file
851
src/components/page/nursingExpertInsight.vue
Normal file
@@ -0,0 +1,851 @@
|
||||
<template>
|
||||
<div class="nursing-insight-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ currentJournal.title }}</h2>
|
||||
<p class="subtitle">关注:中国专家待联系存量与库内规模</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<span class="journal-label">期刊</span>
|
||||
<el-select
|
||||
v-model="selectedJournal"
|
||||
size="small"
|
||||
style="width: 220px"
|
||||
:disabled="loading"
|
||||
@change="handleJournalChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in journalOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" icon="el-icon-refresh" :loading="loading" @click="loadAll">
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="fetch-progress-panel">
|
||||
<div class="fetch-progress-head">
|
||||
<span class="fetch-progress-title">关键词加载进度</span>
|
||||
<span class="fetch-progress-count">{{ fetchProgress.completed }} / {{ fetchProgress.total }}</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="fetchProgress.percent"
|
||||
:stroke-width="18"
|
||||
:text-inside="true"
|
||||
/>
|
||||
<div v-if="loadingProgress" class="fetch-progress-detail">{{ loadingProgress }}</div>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="focus-panel">
|
||||
<div class="focus-grid">
|
||||
<div class="focus-item focus-item--primary">
|
||||
<div class="focus-label">中国 · 待联系(可发池)</div>
|
||||
<div class="focus-value focus-value--pending">{{ summary.chinaPendingCount }}</div>
|
||||
<div class="focus-ratio">占中国专家 {{ pendingRatioText }}</div>
|
||||
<div class="focus-hint">全量分页拉取后跨关键词去重</div>
|
||||
</div>
|
||||
<div class="focus-item">
|
||||
<div class="focus-label">中国专家(去重)</div>
|
||||
<div class="focus-value">{{ summary.chinaCount }}</div>
|
||||
<div class="focus-hint">{{ activeKeywords.length }} 个关键词汇总</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
class="focus-tip"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="说明:中国专家通过 country=China 筛选;每关键词自动分页拉全量后再统计,汇总时对专家去重。数据量较大时刷新会较慢。"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="16" class="chart-row">
|
||||
<el-col :xs="24">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header">中国专家 · 待联系 / 已联系</div>
|
||||
<div class="pending-ratio-bar">
|
||||
<div class="pending-ratio-head">
|
||||
<span>待联系占比</span>
|
||||
<span class="pending-ratio-value">
|
||||
{{ summary.chinaPendingCount }} / {{ summary.chinaCount }}({{ pendingRatioText }})
|
||||
</span>
|
||||
</div>
|
||||
<el-progress
|
||||
v-if="!pendingRatioIsTiny"
|
||||
:percentage="pendingRatio"
|
||||
:stroke-width="18"
|
||||
color="#f56c6c"
|
||||
:format="() => pendingRatioText"
|
||||
/>
|
||||
<div v-else class="pending-ratio-tiny">
|
||||
<span class="pending-ratio-tiny-value">{{ pendingRatioText }}</span>
|
||||
<span class="pending-ratio-tiny-hint">占比不足 1%,进度条难以直观展示,请以上方数字为准</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="contactPieChart" class="chart-box"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const NURSING_KEYWORDS = [
|
||||
'Infection Control Nursing',
|
||||
'Infectious Disease Nursing',
|
||||
'Pandemic Preparedness',
|
||||
'Community Health Nursing',
|
||||
'Home Health Nursing',
|
||||
'Gerontological Nursing',
|
||||
'Oncology Nursing',
|
||||
'Cardiovascular Nursing',
|
||||
'Diabetes Nursing',
|
||||
'Hematology Nursing',
|
||||
'Hemodialysis Nursing',
|
||||
'Nephrology Nursing',
|
||||
'Critical Care Nursing',
|
||||
'Perinatal Nursing',
|
||||
'Midwifery Care',
|
||||
'Neonatal Intensive Care',
|
||||
'Pediatric Nursing',
|
||||
'Infertility Nursing',
|
||||
'Reproductive Nursing',
|
||||
'Chronic Disease Management',
|
||||
'Transitional Care',
|
||||
'Patient Centered Care',
|
||||
'Nursing Leadership',
|
||||
'Nursing Administration',
|
||||
'Nursing Informatics',
|
||||
'Artificial Intelligence in Nursing',
|
||||
'Psychosocial Nursing',
|
||||
'Hospice and Palliative Care',
|
||||
'Telenursing',
|
||||
'mHealth in Nursing',
|
||||
'Robotic Nursing Care',
|
||||
'Precision Nursing',
|
||||
'Qualitative Nursing Research',
|
||||
'Mixed Methods Nursing',
|
||||
'Psychometric Testing',
|
||||
'Health Literacy in Nursing',
|
||||
'Adherence to Treatment',
|
||||
'Virtual Reality in Nursing Education'
|
||||
];
|
||||
|
||||
const CANCER_KEYWORDS = [
|
||||
'Bladder cancer',
|
||||
'Thyroid cancer',
|
||||
'Renal cell carcinoma',
|
||||
'Kidney cancer',
|
||||
'Endometrial cancer',
|
||||
'Oral cancer',
|
||||
'Nasopharyngeal carcinoma',
|
||||
'Multiple myeloma',
|
||||
'Brain tumor',
|
||||
'Bone tumor',
|
||||
'Tumor',
|
||||
'Tumour',
|
||||
'Neoplasm',
|
||||
'Cancer',
|
||||
'Malignancy',
|
||||
'Carcinoma',
|
||||
'Sarcoma',
|
||||
'Lymphoma',
|
||||
'Leukemia',
|
||||
'Melanoma',
|
||||
'Glioma',
|
||||
'Lung cancer',
|
||||
'Breast cancer',
|
||||
'Gastric cancer',
|
||||
'Hepatocellular carcinoma',
|
||||
'Colorectal cancer',
|
||||
'Pancreatic cancer',
|
||||
'Ovarian cancer',
|
||||
'Prostate cancer',
|
||||
'Esophageal cancer',
|
||||
'Cervical cancer'
|
||||
];
|
||||
|
||||
const BMEC_KEYWORDS = [
|
||||
'Biomechanics',
|
||||
'Nanomedicine',
|
||||
'Nanodrug delivery',
|
||||
'Wound healing engineering',
|
||||
'Metal-organic frameworks',
|
||||
'Cancer therapy',
|
||||
'3D bioprinting',
|
||||
'Biomaterials',
|
||||
'Tissue regeneration',
|
||||
'Stem cells',
|
||||
'Regenerative medicine',
|
||||
'Cancer nanotherapy',
|
||||
'Targeted drug delivery',
|
||||
'Bioactive materials',
|
||||
'Smart polymers',
|
||||
'Biocompatible materials',
|
||||
'Nanostructures',
|
||||
'Drug-loaded nanoparticles',
|
||||
'Nanocarriers',
|
||||
'Immunotherapy',
|
||||
'Molecular imaging',
|
||||
'Precision medicine',
|
||||
'Cancer immunotherapy',
|
||||
'Biomedical engineering',
|
||||
'Advanced drug delivery systems',
|
||||
'Bioprinting technologies',
|
||||
'Tissue engineering',
|
||||
'Bioinks',
|
||||
'Nanoparticles',
|
||||
'Gene therapy',
|
||||
'Biodegradable polymers',
|
||||
'Surface modification',
|
||||
'Biofabrication',
|
||||
'Biosensors'
|
||||
];
|
||||
|
||||
const MDM_KEYWORDS = [
|
||||
'Artificial Intelligence',
|
||||
'Machine Learning',
|
||||
'Deep Learning',
|
||||
'Medical Data Mining',
|
||||
'Biomedical Informatics',
|
||||
'Clinical Decision Support',
|
||||
'Electronic Health Records',
|
||||
'Digital Health',
|
||||
'Medical Imaging',
|
||||
'Radiomics',
|
||||
'Predictive Modeling',
|
||||
'Explainable AI',
|
||||
'Natural Language Processing',
|
||||
'Precision Medicine',
|
||||
'Bioinformatics',
|
||||
'Multi-omics',
|
||||
'Drug Discovery',
|
||||
'Healthcare Analytics',
|
||||
'Federated Learning',
|
||||
'Clinical Validation'
|
||||
];
|
||||
|
||||
const LR_KEYWORDS = [
|
||||
'Molecular Biology',
|
||||
'Gene Expression',
|
||||
'Cell Signaling',
|
||||
'Apoptosis',
|
||||
'Neurodegeneration',
|
||||
'Immune Response',
|
||||
'Inflammation',
|
||||
'Aging Mechanisms',
|
||||
'Oxidative Stress',
|
||||
'Animal Behavior',
|
||||
'Plant Development',
|
||||
'Microbial Communities',
|
||||
'Antibiotic Resistance',
|
||||
'Synthetic Biology',
|
||||
'Evolutionary Biology'
|
||||
];
|
||||
|
||||
const JOURNAL_CONFIGS = {
|
||||
cancer: {
|
||||
title: 'Cancer Communications · Expert Insight',
|
||||
label: 'Cancer Communications',
|
||||
keywords: CANCER_KEYWORDS
|
||||
},
|
||||
bmec: {
|
||||
title: 'BMEC · Expert Insight',
|
||||
label: 'BMEC',
|
||||
keywords: BMEC_KEYWORDS
|
||||
},
|
||||
mdm: {
|
||||
title: 'MDM · Expert Insight',
|
||||
label: 'MDM',
|
||||
keywords: MDM_KEYWORDS
|
||||
},
|
||||
lr: {
|
||||
title: 'LR · Expert Insight',
|
||||
label: 'LR',
|
||||
keywords: LR_KEYWORDS
|
||||
},
|
||||
nursing: {
|
||||
title: 'Nursing Communications · Expert Insight',
|
||||
label: 'Nursing Communications',
|
||||
keywords: NURSING_KEYWORDS
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_JOURNAL = 'cancer';
|
||||
const JOURNAL_STORAGE_KEY = 'nc_expert_insight_journal';
|
||||
|
||||
const CHINA_COUNTRY = 'China';
|
||||
const LIST_PAGE_SIZE = 500;
|
||||
|
||||
export default {
|
||||
name: 'NursingExpertInsight',
|
||||
data() {
|
||||
const savedJournal = localStorage.getItem(JOURNAL_STORAGE_KEY);
|
||||
const selectedJournal = JOURNAL_CONFIGS[savedJournal] ? savedJournal : DEFAULT_JOURNAL;
|
||||
return {
|
||||
loading: false,
|
||||
loadingProgress: '',
|
||||
selectedJournal,
|
||||
fetchProgress: {
|
||||
completed: 0,
|
||||
total: JOURNAL_CONFIGS[selectedJournal].keywords.length,
|
||||
percent: 0
|
||||
},
|
||||
summary: {
|
||||
chinaCount: 0,
|
||||
chinaPendingCount: 0,
|
||||
chinaContactedCount: 0
|
||||
},
|
||||
chartInstances: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
journalOptions() {
|
||||
return Object.keys(JOURNAL_CONFIGS).map((key) => ({
|
||||
value: key,
|
||||
label: JOURNAL_CONFIGS[key].label
|
||||
}));
|
||||
},
|
||||
currentJournal() {
|
||||
return JOURNAL_CONFIGS[this.selectedJournal] || JOURNAL_CONFIGS[DEFAULT_JOURNAL];
|
||||
},
|
||||
activeKeywords() {
|
||||
return this.currentJournal.keywords || [];
|
||||
},
|
||||
pendingRatio() {
|
||||
const total = Number(this.summary.chinaCount || 0);
|
||||
if (!total) return 0;
|
||||
return Math.min(100, (this.summary.chinaPendingCount / total) * 100);
|
||||
},
|
||||
pendingRatioText() {
|
||||
return this.formatRatioPercent(this.summary.chinaPendingCount, this.summary.chinaCount);
|
||||
},
|
||||
pendingRatioIsTiny() {
|
||||
return this.pendingRatio > 0 && this.pendingRatio < 1;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAll();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.disposeCharts();
|
||||
},
|
||||
methods: {
|
||||
formatRatioPercent(part, total) {
|
||||
const numerator = Number(part || 0);
|
||||
const denominator = Number(total || 0);
|
||||
if (!denominator) return '—';
|
||||
const ratio = (numerator / denominator) * 100;
|
||||
if (ratio > 0 && ratio < 1) return `${ratio.toFixed(2)}%`;
|
||||
if (ratio < 10) return `${ratio.toFixed(1)}%`;
|
||||
return `${Math.round(ratio)}%`;
|
||||
},
|
||||
isPendingContact(row) {
|
||||
const text = String((row && row.state_text) || '').trim();
|
||||
if (!text) return true;
|
||||
if (text.includes('待联系')) return true;
|
||||
if (/pending|to contact|not contacted/i.test(text)) return true;
|
||||
if (row.state != null && String(row.state) === '0') return true;
|
||||
return false;
|
||||
},
|
||||
async runPool(items, limit, iterator) {
|
||||
const results = new Array(items.length);
|
||||
let cursor = 0;
|
||||
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||||
while (cursor < items.length) {
|
||||
const current = cursor;
|
||||
cursor += 1;
|
||||
results[current] = await iterator(items[current], current);
|
||||
}
|
||||
});
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
},
|
||||
async fetchChinaExpertList(field, onProgress) {
|
||||
const baseParams = {
|
||||
major_id: null,
|
||||
keyword: '',
|
||||
field,
|
||||
country: CHINA_COUNTRY,
|
||||
pageSize: LIST_PAGE_SIZE
|
||||
};
|
||||
|
||||
const firstRes = await this.$api.post('api/expert_manage/getList', {
|
||||
...baseParams,
|
||||
pageIndex: 1
|
||||
});
|
||||
const firstList = (firstRes && firstRes.data && firstRes.data.list) || [];
|
||||
const total = Number((firstRes && firstRes.data && firstRes.data.total) || firstList.length || 0);
|
||||
let totalPages = Number(
|
||||
(firstRes && firstRes.data && (firstRes.data.totalPages || firstRes.data.total_pages)) || 0
|
||||
);
|
||||
if (!totalPages && total > 0) {
|
||||
totalPages = Math.ceil(total / LIST_PAGE_SIZE);
|
||||
}
|
||||
totalPages = Math.max(1, totalPages);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
return { list: firstList, total };
|
||||
}
|
||||
|
||||
const restPages = [];
|
||||
for (let pageIndex = 2; pageIndex <= totalPages; pageIndex += 1) {
|
||||
restPages.push(pageIndex);
|
||||
}
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(`共 ${totalPages} 页,已加载 1/${totalPages}`);
|
||||
}
|
||||
|
||||
let loadedPages = 1;
|
||||
const restLists = await this.runPool(restPages, 4, async (pageIndex) => {
|
||||
const res = await this.$api.post('api/expert_manage/getList', {
|
||||
...baseParams,
|
||||
pageIndex
|
||||
});
|
||||
loadedPages += 1;
|
||||
if (onProgress) {
|
||||
onProgress(`共 ${totalPages} 页,已加载 ${loadedPages}/${totalPages}`);
|
||||
}
|
||||
return (res && res.data && res.data.list) || [];
|
||||
});
|
||||
|
||||
const list = firstList.concat(...restLists);
|
||||
return { list, total };
|
||||
},
|
||||
async fetchKeywordInsight(field, onProgress) {
|
||||
const listPayload = await this.fetchChinaExpertList(field, onProgress);
|
||||
const list = listPayload.list || [];
|
||||
const total = Number(listPayload.total || list.length || 0);
|
||||
return { field, list, total };
|
||||
},
|
||||
getExpertKey(row) {
|
||||
return String((row && (row.expert_id || row.id || row.uid || row.email)) || '').trim();
|
||||
},
|
||||
resetFetchProgress() {
|
||||
this.fetchProgress = {
|
||||
completed: 0,
|
||||
total: this.activeKeywords.length,
|
||||
percent: 0
|
||||
};
|
||||
this.loadingProgress = '';
|
||||
},
|
||||
handleJournalChange(journal) {
|
||||
localStorage.setItem(JOURNAL_STORAGE_KEY, journal);
|
||||
if (!this.loading) this.loadAll();
|
||||
},
|
||||
markKeywordDone(field) {
|
||||
const completed = Math.min(this.fetchProgress.completed + 1, this.fetchProgress.total);
|
||||
const percent = this.fetchProgress.total
|
||||
? Math.min(100, Math.round((completed / this.fetchProgress.total) * 100))
|
||||
: 0;
|
||||
this.fetchProgress = {
|
||||
...this.fetchProgress,
|
||||
completed,
|
||||
percent
|
||||
};
|
||||
this.loadingProgress = completed >= this.fetchProgress.total ? '正在汇总统计...' : `已完成:${field}`;
|
||||
},
|
||||
async loadAll() {
|
||||
this.loading = true;
|
||||
this.resetFetchProgress();
|
||||
this.loadingProgress = '正在按关键词并行查询...';
|
||||
try {
|
||||
const expertMap = new Map();
|
||||
const keywords = this.activeKeywords;
|
||||
const results = await this.runPool(keywords, 8, async (field) => {
|
||||
this.loadingProgress = `正在查询:${field}`;
|
||||
try {
|
||||
const result = await this.fetchKeywordInsight(field, (pageText) => {
|
||||
this.loadingProgress = `${field} · ${pageText}`;
|
||||
});
|
||||
this.markKeywordDone(field);
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.markKeywordDone(field);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
results.forEach(({ list }) => {
|
||||
list.forEach((row) => {
|
||||
const key = this.getExpertKey(row);
|
||||
if (key && !expertMap.has(key)) expertMap.set(key, row);
|
||||
});
|
||||
});
|
||||
|
||||
this.buildSummary(expertMap);
|
||||
this.$nextTick(() => this.renderCharts());
|
||||
} catch (e) {
|
||||
this.$message.error('加载失败,请稍后重试');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loadingProgress = '';
|
||||
this.resetFetchProgress();
|
||||
}
|
||||
},
|
||||
buildSummary(expertMap) {
|
||||
const summary = {
|
||||
chinaCount: 0,
|
||||
chinaPendingCount: 0,
|
||||
chinaContactedCount: 0
|
||||
};
|
||||
expertMap.forEach((row) => {
|
||||
summary.chinaCount += 1;
|
||||
if (this.isPendingContact(row)) {
|
||||
summary.chinaPendingCount += 1;
|
||||
} else {
|
||||
summary.chinaContactedCount += 1;
|
||||
}
|
||||
});
|
||||
this.summary = summary;
|
||||
},
|
||||
disposeCharts() {
|
||||
this.chartInstances.forEach((chart) => {
|
||||
if (chart) chart.dispose();
|
||||
});
|
||||
this.chartInstances = [];
|
||||
},
|
||||
initChart(refName) {
|
||||
const el = this.$refs[refName];
|
||||
if (!el) return null;
|
||||
const chart = this.$echarts.init(el);
|
||||
this.chartInstances.push(chart);
|
||||
return chart;
|
||||
},
|
||||
renderCharts() {
|
||||
this.disposeCharts();
|
||||
this.renderContactPieChart();
|
||||
},
|
||||
renderContactPieChart() {
|
||||
const chart = this.initChart('contactPieChart');
|
||||
if (!chart) return;
|
||||
const pending = this.summary.chinaPendingCount;
|
||||
const contacted = this.summary.chinaContactedCount;
|
||||
const total = this.summary.chinaCount;
|
||||
const pendingPercent = this.formatRatioPercent(pending, total);
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params) => {
|
||||
const percent =
|
||||
params.name === '待联系'
|
||||
? pendingPercent
|
||||
: this.formatRatioPercent(contacted, total);
|
||||
return `${params.name}: ${params.value}(${percent})`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
bottom: 0,
|
||||
formatter: (name) => {
|
||||
if (name === '待联系') return `待联系 ${pending}(${pendingPercent})`;
|
||||
return `已联系 ${contacted}(${this.formatRatioPercent(contacted, total)})`;
|
||||
}
|
||||
},
|
||||
graphic: [
|
||||
{
|
||||
type: 'group',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: -26,
|
||||
style: {
|
||||
text: String(pending),
|
||||
fill: '#f56c6c',
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 18,
|
||||
style: {
|
||||
text: '待联系',
|
||||
fill: '#606266',
|
||||
fontSize: 14,
|
||||
textAlign: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 40,
|
||||
style: {
|
||||
text: pendingPercent,
|
||||
fill: '#909399',
|
||||
fontSize: 13,
|
||||
textAlign: 'center'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['46%', '72%'],
|
||||
center: ['50%', '46%'],
|
||||
selectedMode: 'single',
|
||||
selectedOffset: 10,
|
||||
minShowLabelAngle: 0,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: (params) => {
|
||||
if (params.name === '待联系') {
|
||||
return `{pending|${params.name}}\n{pending|${params.value}(${pendingPercent})}`;
|
||||
}
|
||||
return `${params.name}\n${params.value}(${this.formatRatioPercent(params.value, total)})`;
|
||||
},
|
||||
rich: {
|
||||
pending: {
|
||||
color: '#f56c6c',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
lineHeight: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 18,
|
||||
length2: 12
|
||||
},
|
||||
emphasis: {
|
||||
scale: true,
|
||||
scaleSize: 12
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: '待联系',
|
||||
value: pending,
|
||||
selected: true,
|
||||
itemStyle: {
|
||||
color: '#f56c6c',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 3,
|
||||
shadowBlur: 12,
|
||||
shadowColor: 'rgba(245, 108, 108, 0.5)'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '已联系',
|
||||
value: contacted,
|
||||
itemStyle: { color: '#dcdfe6' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
handleResize() {
|
||||
this.chartInstances.forEach((chart) => chart && chart.resize());
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nursing-insight-page {
|
||||
padding: 16px 20px 32px;
|
||||
background: #f5f7fa;
|
||||
min-height: calc(100vh - 90px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.journal-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fetch-progress-panel {
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.fetch-progress-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.fetch-progress-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.fetch-progress-count {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.fetch-progress-detail {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.focus-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.focus-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.focus-item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.focus-item--primary {
|
||||
background: #fff5f5;
|
||||
border-color: #f56c6c;
|
||||
box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.08);
|
||||
}
|
||||
|
||||
.focus-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.focus-value {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.focus-value--pending {
|
||||
font-size: 42px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.focus-ratio {
|
||||
margin-top: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.focus-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #a8abb2;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.focus-tip {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pending-ratio-bar {
|
||||
padding: 0 4px 16px;
|
||||
}
|
||||
|
||||
.pending-ratio-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.pending-ratio-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.pending-ratio-tiny {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: #fef0f0;
|
||||
border: 1px solid #fde2e2;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pending-ratio-tiny-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #f56c6c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pending-ratio-tiny-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.focus-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1104,6 +1104,14 @@ export default new Router({
|
||||
title: 'Crawl Task Monitor'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ncNursingExpertInsightPrivate',
|
||||
component: () => import('../components/page/nursingExpertInsight'),
|
||||
meta: {
|
||||
title: 'Nursing Expert Insight'
|
||||
},
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/scholarCrawlersKeywords',
|
||||
component: () => import('../components/page/scholarCrawlersKeywords'),
|
||||
|
||||
@@ -74,3 +74,36 @@ export function applyMailTemplateVariableMocks(html, options) {
|
||||
return full;
|
||||
});
|
||||
}
|
||||
|
||||
/** 复审通知邮件模板变量:标题、SN、期刊名称、期刊官网 */
|
||||
export function applyReviewNotifyTemplateVariables(html, options) {
|
||||
const opts = options || {};
|
||||
if (!html) return '';
|
||||
const title = opts.title != null ? String(opts.title) : '';
|
||||
const acceptSn =
|
||||
opts.accept_sn != null && opts.accept_sn !== ''
|
||||
? String(opts.accept_sn)
|
||||
: opts.sn != null
|
||||
? String(opts.sn)
|
||||
: '';
|
||||
const journalName = opts.journal_name != null ? String(opts.journal_name) : '';
|
||||
const journalUrl = opts.journal_url != null ? String(opts.journal_url) : '';
|
||||
const map = {
|
||||
title,
|
||||
article_title: title,
|
||||
manuscript_title: title,
|
||||
accept_sn: acceptSn,
|
||||
article_sn: acceptSn,
|
||||
sn: acceptSn,
|
||||
journal_name: journalName,
|
||||
journal_url: journalUrl
|
||||
};
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user