Compare commits
24 Commits
23b8a1d031
...
similarity
| Author | SHA1 | Date | |
|---|---|---|---|
| d765628bb3 | |||
| be8ea4e486 | |||
| 4426077094 | |||
| 6050dd365d | |||
| 723ec0d190 | |||
| b10de50fdf | |||
| f67d8d5600 | |||
| 67a4875b01 | |||
| 8e59702f0b | |||
| ea1564018e | |||
| 4d7c230abe | |||
| 8b9e35287c | |||
| 9234318139 | |||
| a4cbce2db7 | |||
| 56cda2c232 | |||
| 0c9ff0fe68 | |||
| ca1b10c418 | |||
| bb36bcc645 | |||
| fe4bd7c9b0 | |||
| 632913aaad | |||
| 711a3fe2ec | |||
| 0d913e90a7 | |||
| 7458beb8b2 | |||
| 8fbcf39a25 |
@@ -19,8 +19,8 @@ const service = axios.create({
|
||||
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
||||
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
||||
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
||||
// baseURL: '/api', //本地
|
||||
baseURL: '/', //正式
|
||||
baseURL: '/api', //本地
|
||||
// baseURL: '/', //正式
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -391,7 +391,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");
|
||||
|
||||
@@ -128,6 +128,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.initORCID();
|
||||
bus.$on('editorSessionLocalRestored', this.syncLsUserToHeader);
|
||||
if (this.user_id == 24) {
|
||||
this.daojishi = '2021.9.3 - 2021.9.30';
|
||||
this.curStartTime = '2021-10-01 00:00:00';
|
||||
@@ -162,6 +163,11 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
syncLsUserToHeader() {
|
||||
this.user_id = localStorage.getItem('U_id');
|
||||
this.user_cap = localStorage.getItem('U_role');
|
||||
this.$forceUpdate();
|
||||
},
|
||||
goHome() {
|
||||
this.$router.push('/');
|
||||
},
|
||||
@@ -176,6 +182,7 @@ export default {
|
||||
localStorage.removeItem('U_role');
|
||||
localStorage.removeItem('U_id');
|
||||
localStorage.removeItem('U_name');
|
||||
localStorage.removeItem('U_email');
|
||||
localStorage.removeItem('U_status');
|
||||
localStorage.removeItem('ms_journal_alias');
|
||||
localStorage.removeItem('journalTypeData');
|
||||
@@ -294,6 +301,9 @@ export default {
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('editorSessionLocalRestored', this.syncLsUserToHeader);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -120,6 +120,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.initORCID();
|
||||
bus.$on('editorSessionLocalRestored', this.onEditorSessionLocalRestored);
|
||||
if (this.user_id == 24) {
|
||||
this.daojishi = '2021.9.3 - 2021.9.30';
|
||||
this.curStartTime = '2021-10-01 00:00:00';
|
||||
@@ -155,6 +156,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onEditorSessionLocalRestored() {
|
||||
this.updateUsername();
|
||||
this.$forceUpdate();
|
||||
},
|
||||
updateUsername() {
|
||||
|
||||
this.localUsername = localStorage.getItem('U_name');
|
||||
@@ -174,6 +179,7 @@ export default {
|
||||
localStorage.removeItem('U_role');
|
||||
localStorage.removeItem('U_id');
|
||||
localStorage.removeItem('U_name');
|
||||
localStorage.removeItem('U_email');
|
||||
localStorage.removeItem('U_status');
|
||||
localStorage.removeItem('ms_journal_alias');
|
||||
localStorage.removeItem('journalTypeData');
|
||||
@@ -289,6 +295,7 @@ export default {
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('editorSessionLocalRestored', this.onEditorSessionLocalRestored);
|
||||
// 步骤 C2: 销毁前移除监听器
|
||||
if (this.$bus) {
|
||||
this.$bus.$off('user-name-updated', this.updateUsername);
|
||||
|
||||
@@ -20,8 +20,15 @@
|
||||
<template v-for="subItem in item.subs">
|
||||
<el-submenu v-if="subItem.subs" :index="subItem.index" :key="subItem.index + '-submenu'">
|
||||
<template slot="title">
|
||||
{{ subItem.title }}
|
||||
<!-- <el-badge is-dot :hidden="false">{{ subItem.title }}</el-badge> -->
|
||||
<el-badge
|
||||
v-if="subItem.index === '45'"
|
||||
is-dot
|
||||
:hidden="applyBadgeYouth <= 0"
|
||||
class="sidebar-submenu-apply-badge"
|
||||
>
|
||||
<span>{{ subItem.title }}</span>
|
||||
</el-badge>
|
||||
<template v-else>{{ subItem.title }}</template>
|
||||
</template>
|
||||
|
||||
<template v-for="(threeItem, i) in subItem.subs">
|
||||
@@ -35,8 +42,16 @@
|
||||
{{ fourItem.title }}
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
<el-menu-item v-else :index="threeItem.index" :key="threeItem.index + '-item'"
|
||||
>{{ threeItem.title }}
|
||||
<el-menu-item v-else :index="threeItem.index" :key="threeItem.index + '-item'">
|
||||
<el-badge
|
||||
v-if="threeItem.index === 'youthApplyList'"
|
||||
is-dot
|
||||
:hidden="applyBadgeYouth <= 0"
|
||||
class="sidebar-menu-youth-apply-badge"
|
||||
>
|
||||
<span class="sidebar-menu-youth-apply-badge__text">{{ threeItem.title }}</span>
|
||||
</el-badge>
|
||||
<template v-else>{{ threeItem.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-submenu>
|
||||
@@ -64,8 +79,8 @@
|
||||
</el-submenu>
|
||||
</template>
|
||||
<!-- 编委 -->
|
||||
|
||||
<template v-if="this.user_cap.includes(',board')||this.user_cap.includes('board_editor')">
|
||||
|
||||
<template v-if="this.user_cap.includes(',board') || this.user_cap.includes('board_editor')">
|
||||
<el-submenu index="7">
|
||||
<template slot="title"> <i class="el-icon-notebook-1"></i> {{ $t('sidebar.edit_oria') }} </template>
|
||||
<!-- <el-menu-item index="editorial">
|
||||
@@ -74,7 +89,7 @@
|
||||
<!-- <el-menu-item index="edithistory">
|
||||
{{ $t('sidebar.edit_oria2') }}
|
||||
</el-menu-item> -->
|
||||
<el-menu-item index="editPeerewer">
|
||||
<el-menu-item index="editPeerewer">
|
||||
{{ $t('sidebar.edit_ewer1') }}
|
||||
</el-menu-item>
|
||||
<el-menu-item index="editPerhistory">
|
||||
@@ -83,9 +98,10 @@
|
||||
</el-submenu>
|
||||
</template>
|
||||
<!-- 主编 -->
|
||||
|
||||
<template v-if="this.user_cap.includes('chief')||this.user_cap.includes('chief_editor')||this.user_cap.includes('deputy_editor')">
|
||||
|
||||
<template
|
||||
v-if="this.user_cap.includes('chief') || this.user_cap.includes('chief_editor') || this.user_cap.includes('deputy_editor')"
|
||||
>
|
||||
<el-submenu index="6">
|
||||
<template slot="title"> <i class="el-icon-document-copy"></i> {{ $t('sidebar.man_ing') }} </template>
|
||||
<el-menu-item index="managing">
|
||||
@@ -168,6 +184,7 @@
|
||||
<el-menu-item index="mailboxConfig">
|
||||
{{ $t('sidebar.mailboxManagement') }}
|
||||
</el-menu-item>
|
||||
|
||||
<el-submenu index="expertDatabaseSub">
|
||||
<template slot="title">
|
||||
{{ $t('sidebar.expertDatabase') }}
|
||||
@@ -175,19 +192,16 @@
|
||||
<el-menu-item index="expertDatabase">
|
||||
{{ $t('sidebar.expertList') }}
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="crawlTaskMonitor">
|
||||
{{ $t('sidebar.crawlTasks') }}
|
||||
</el-menu-item>
|
||||
|
||||
</el-submenu>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<el-menu-item index="countryManagement">
|
||||
{{ $t('sidebar.countryManagement') }}
|
||||
</el-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
|
||||
<el-submenu index="tools">
|
||||
<template slot="title"> <i class="el-icon-paperclip"></i> {{ $t('sidebar.tools') }} </template>
|
||||
<el-menu-item index="RejectedArticles">
|
||||
@@ -221,6 +235,8 @@ export default {
|
||||
user_cap: localStorage.getItem('U_role'),
|
||||
|
||||
menuList: [],
|
||||
/** 青年编委申请红点:Young Scientist 父级 + Apply 子项,数据来自 getYboardApplys */
|
||||
applyBadgeYouth: 0,
|
||||
items: [],
|
||||
// 作者
|
||||
author_items: [
|
||||
@@ -234,7 +250,6 @@ export default {
|
||||
index: '1',
|
||||
title: this.$t('sidebar.author'),
|
||||
subs: [
|
||||
|
||||
{
|
||||
index: 'articleList',
|
||||
title: this.$t('sidebar.author1')
|
||||
@@ -246,10 +261,11 @@ export default {
|
||||
{
|
||||
index: 'articleAdd',
|
||||
title: this.$t('sidebar.author2')
|
||||
} , {
|
||||
},
|
||||
{
|
||||
index: 'orderListAuthor',
|
||||
title: this.$t('sidebar.author4')
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
// ,{
|
||||
@@ -312,7 +328,6 @@ export default {
|
||||
index: 'Promotionsystem',
|
||||
title: this.$t('menu.Promotionsystem'),
|
||||
subs: [
|
||||
|
||||
{
|
||||
index: 'disseMRecord',
|
||||
title: this.$t('menu.userManSys6')
|
||||
@@ -363,7 +378,7 @@ export default {
|
||||
index: '4',
|
||||
title: this.$t('sidebar.userManSys'),
|
||||
subs: [
|
||||
{
|
||||
{
|
||||
//论文编辑系统
|
||||
icon: 'el-icon-lx-copy',
|
||||
index: 'Userdatabase',
|
||||
@@ -679,74 +694,64 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// if(this.user_cap.includes(',board')||this.user_cap.includes('board_editor')||this.user_cap.includes('chief')||this.user_cap.includes('chief_editor')||this.user_cap.includes('deputy_editor')){
|
||||
// Promise.all([
|
||||
// this.$api
|
||||
// .post('api/Finalreview/lists', {
|
||||
// 'reviewer_id': localStorage.getItem('U_id'),state:5,
|
||||
// 'page': 1,
|
||||
// 'size': 999999,
|
||||
// })
|
||||
|
||||
// ]).then(([res1]) => {
|
||||
// console.log('res1 at line 770:', res1)
|
||||
// const totalCheck = res1.data.total || 0; // 待审核
|
||||
|
||||
|
||||
// if (totalCheck > 0 ) {
|
||||
// const h = this.$createElement;
|
||||
// const messageNodes = [];
|
||||
|
||||
// if (totalCheck > 0) {
|
||||
// messageNodes.push(
|
||||
// h(
|
||||
// 'p',
|
||||
// {
|
||||
// style: {
|
||||
// color: '#006699',
|
||||
// marginTop: '10px',
|
||||
// marginBottom: '4px',
|
||||
// display: 'block', width: '280px' // 设置宽度
|
||||
|
||||
// }
|
||||
// },
|
||||
// `You have received 【${totalCheck}】 invitations .`
|
||||
// )
|
||||
// );
|
||||
|
||||
// // 第二行
|
||||
// messageNodes.push(
|
||||
// h(
|
||||
// 'p',
|
||||
// {
|
||||
// style: {
|
||||
// color: '#888',
|
||||
// display: 'block', width: '280px' // 设置宽度
|
||||
|
||||
// }
|
||||
// },
|
||||
// 'Please process them promptly in the final review history.'
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// this.$notify({
|
||||
// title: 'Final Review',
|
||||
// message: h('div', {
|
||||
// style: 'width: 200px;'
|
||||
|
||||
// }, messageNodes)
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
if (String(this.userrole) === '1') {
|
||||
this.fetchApplyBadgeSummary();
|
||||
}
|
||||
// if(this.user_cap.includes(',board')||this.user_cap.includes('board_editor')||this.user_cap.includes('chief')||this.user_cap.includes('chief_editor')||this.user_cap.includes('deputy_editor')){
|
||||
// Promise.all([
|
||||
// this.$api
|
||||
// .post('api/Finalreview/lists', {
|
||||
// 'reviewer_id': localStorage.getItem('U_id'),state:5,
|
||||
// 'page': 1,
|
||||
// 'size': 999999,
|
||||
// })
|
||||
// ]).then(([res1]) => {
|
||||
// console.log('res1 at line 770:', res1)
|
||||
// const totalCheck = res1.data.total || 0; // 待审核
|
||||
// if (totalCheck > 0 ) {
|
||||
// const h = this.$createElement;
|
||||
// const messageNodes = [];
|
||||
// if (totalCheck > 0) {
|
||||
// messageNodes.push(
|
||||
// h(
|
||||
// 'p',
|
||||
// {
|
||||
// style: {
|
||||
// color: '#006699',
|
||||
// marginTop: '10px',
|
||||
// marginBottom: '4px',
|
||||
// display: 'block', width: '280px' // 设置宽度
|
||||
// }
|
||||
// },
|
||||
// `You have received 【${totalCheck}】 invitations .`
|
||||
// )
|
||||
// );
|
||||
// // 第二行
|
||||
// messageNodes.push(
|
||||
// h(
|
||||
// 'p',
|
||||
// {
|
||||
// style: {
|
||||
// color: '#888',
|
||||
// display: 'block', width: '280px' // 设置宽度
|
||||
// }
|
||||
// },
|
||||
// 'Please process them promptly in the final review history.'
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// this.$notify({
|
||||
// title: 'Final Review',
|
||||
// message: h('div', {
|
||||
// style: 'width: 200px;'
|
||||
// }, messageNodes)
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
},
|
||||
|
||||
created() {
|
||||
|
||||
localStorage.setItem('collapse', this.collapse);
|
||||
if (this.userrole == 2) {
|
||||
//其余的身份(显示作者)
|
||||
@@ -788,7 +793,7 @@ export default {
|
||||
index: 'JournalManagementAll',
|
||||
title: this.$t('sidebar.journalList')
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
index: 'GroupClassification',
|
||||
title: this.$t('sidebar.GroupClassification')
|
||||
@@ -860,10 +865,33 @@ export default {
|
||||
localStorage.setItem('collapse', this.collapse);
|
||||
bus.$emit('collapse-content', msg);
|
||||
});
|
||||
bus.$on('apply-badge-refresh', () => {
|
||||
this.fetchApplyBadgeSummary();
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('apply-badge-refresh');
|
||||
},
|
||||
methods: {
|
||||
// 获取数据
|
||||
getDate() {}
|
||||
getDate() {},
|
||||
fetchApplyBadgeSummary() {
|
||||
if (String(this.userrole) !== '1') return;
|
||||
const editorId = localStorage.getItem('U_id');
|
||||
if (!editorId) return;
|
||||
this.$api
|
||||
.post('api/User/getYboardApplys', { editor_id: editorId })
|
||||
.then((res) => {
|
||||
if (res && res.code === 0 && res.data && Array.isArray(res.data.applys)) {
|
||||
this.applyBadgeYouth = res.data.applys.length > 0 ? 1 : 0;
|
||||
} else {
|
||||
this.applyBadgeYouth = 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.applyBadgeYouth = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -937,4 +965,38 @@ export default {
|
||||
.linkBar:hover {
|
||||
background: #00527a;
|
||||
}
|
||||
|
||||
/* Young Scientist 父级标题红点 */
|
||||
.sidebar-submenu-apply-badge {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-submenu-apply-badge ::v-deep .el-badge__content.is-dot {
|
||||
top: 28%;
|
||||
margin-top: 6px;
|
||||
right: -4px;
|
||||
border: 0;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Apply 子菜单行上的红点,与菜单行高对齐 */
|
||||
.sidebar-menu-youth-apply-badge {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.sidebar-menu-youth-apply-badge ::v-deep .el-badge__content.is-dot {
|
||||
top: 28%;
|
||||
margin-top: 6px;
|
||||
right: -4px;
|
||||
border: 0;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sidebar-menu-youth-apply-badge__text {
|
||||
color: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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'
|
||||
|
||||
//测试环境
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ const en = {
|
||||
info: {
|
||||
realname: 'English names can only contain uppercase and lowercase letters, "-", and spaces'
|
||||
},
|
||||
articleAdd: {
|
||||
qqEmailAlertLine1: 'The publisher does not currently accept submissions from QQ Mail.',
|
||||
qqEmailAlertLine2Before: 'Please go to',
|
||||
qqEmailDashboardLink: 'Personal Center (Dashboard)',
|
||||
qqEmailAlertLine2After: ' to change your registered email.',
|
||||
qqEmailSubmitBlockedMsg: 'QQ Mail is not supported for submission. Please go to Personal Center (Dashboard) and change your email.'
|
||||
},
|
||||
total: {
|
||||
author: 'author',
|
||||
editor: 'editor',
|
||||
@@ -40,6 +47,13 @@ const en = {
|
||||
status: 'Status',
|
||||
delete: 'Delete',
|
||||
deleteInfo: 'Are you sure you want to delete this journal installment?',
|
||||
plagiarismNotChecked: 'Not checked',
|
||||
plagiarismChecking: 'Checking…',
|
||||
plagiarismRecheck: 'Re-check',
|
||||
plagiarismCheckFailed: 'Failed to start plagiarism check.',
|
||||
plagiarismStatusFailed: 'Failed to load plagiarism status.',
|
||||
plagiarismNoReportUrl: 'Report link is not available yet.',
|
||||
plagiarismReportDetailFailed: 'Could not load manuscript details. Please try again.',
|
||||
},
|
||||
menu: {
|
||||
main: 'Personal Center',
|
||||
@@ -281,6 +295,7 @@ const en = {
|
||||
keywordManagement: 'Keyword Management',
|
||||
crawlTasks: 'Crawl Tasks',
|
||||
expertList: 'Expert List',
|
||||
countryManagement: 'Country Management',
|
||||
autoPromotion: 'Auto Promotion',
|
||||
ReArticles: 'Rejected Manuscripts', // 被拒稿件
|
||||
editorialBoard: 'Boss System',
|
||||
@@ -307,16 +322,109 @@ const en = {
|
||||
},
|
||||
columns: {
|
||||
baseInfo: 'Base Information',
|
||||
country: 'Country',
|
||||
affiliation: 'Affiliation',
|
||||
researchAreas: 'Research areas'
|
||||
researchAreas: 'Research areas',
|
||||
unsubscribeStatus: 'Subscription status'
|
||||
},
|
||||
emptyMark: '-',
|
||||
fields: {
|
||||
nameLabel: 'Name:',
|
||||
emailLabel: 'Email:',
|
||||
acquisitionTimeLabel: 'Acquisition Time:'
|
||||
},
|
||||
viewAllInfo: 'View all details',
|
||||
detailDialogTitle: 'Research areas & Article',
|
||||
detailColField: 'Research areas',
|
||||
detailColPaper: 'Article title',
|
||||
detailColJournal: 'Journal',
|
||||
detailClose: 'Close',
|
||||
detailCellEmpty: 'N/A',
|
||||
noFieldDetail: 'No publication details for these fields',
|
||||
exportWarn: 'Please select a research area, enter a keyword, or enter a field before exporting.',
|
||||
exportFailed: 'Export failed'
|
||||
exportFailed: 'Export failed',
|
||||
unsubscribeNormal: 'Subscribed',
|
||||
unsubscribeUnsubscribed: 'Unsubscribed',
|
||||
unsubscribeSwitchOn: 'Unsub',
|
||||
unsubscribeSwitchOff: 'Subscribed',
|
||||
unsubscribeMissingId: 'Missing expert ID, unable to switch unsubscribe status',
|
||||
unsubscribeUpdateSuccess: 'Unsubscribe status updated',
|
||||
unsubscribeUpdateFailed: 'Failed to update unsubscribe status'
|
||||
},
|
||||
countryManagement: {
|
||||
title: 'Country Management',
|
||||
keywordPlaceholder: 'Chinese / English / Code',
|
||||
partitionLabel: 'Partition',
|
||||
partitionAll: 'All partitions',
|
||||
partition1: 'Partition 1',
|
||||
partition2: 'Partition 2',
|
||||
partition3: 'Partition 3',
|
||||
searchBtn: 'Search',
|
||||
resetBtn: 'Reset',
|
||||
table: {
|
||||
no: 'No.',
|
||||
zhName: 'Chinese name',
|
||||
enName: 'English name',
|
||||
code: 'Code',
|
||||
partition: 'Partition',
|
||||
actions: 'Actions'
|
||||
},
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
editTitle: 'Edit country',
|
||||
form: {
|
||||
zhName: 'Chinese name',
|
||||
enName: 'English name',
|
||||
code: 'Code',
|
||||
partition: 'Partition'
|
||||
},
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
confirm: 'OK',
|
||||
deleteTitle: 'Confirm delete',
|
||||
deleteConfirm: 'Delete this country record? This cannot be undone.',
|
||||
saveSuccess: 'Saved',
|
||||
deleteSuccess: 'Deleted',
|
||||
opFailed: 'Operation failed',
|
||||
loadFailed: 'Failed to load list',
|
||||
missingId: 'Missing country id',
|
||||
ruleZhName: 'Enter Chinese name',
|
||||
ruleEnName: 'Enter English name',
|
||||
ruleCode: 'Enter code',
|
||||
rulePartition: 'Select a partition',
|
||||
batchPartitionBtn: 'Batch update partition',
|
||||
batchPartitionTitle: 'Batch update partition',
|
||||
batchPartitionTargetLabel: 'Target partition',
|
||||
batchPartitionTargetRequired: 'Please select target partition first',
|
||||
batchPartitionHelp:
|
||||
'One record per line: ISO code (3 letters) OR English OR Chinese name. Lines starting with # are comments. Example:\nDNK\nDenmark',
|
||||
batchPartitionPlaceholder: 'Paste data, e.g.:\nDNK\nISR',
|
||||
batchPartitionPreview: 'Preview match',
|
||||
batchPartitionApply: 'Apply partition updates',
|
||||
batchPartitionEmpty: 'Enter valid data (one country key per line)',
|
||||
batchPartitionLoadListFailed: 'Failed to load full country list',
|
||||
batchPartitionPreviewEmpty: 'Nothing to match',
|
||||
batchPartitionColKey: 'Key from input',
|
||||
batchPartitionColCurrentPartition: 'Current partition',
|
||||
batchPartitionColPartition: 'Target partition',
|
||||
batchPartitionColMatch: 'Match',
|
||||
batchPartitionColId: 'country_id',
|
||||
batchPartitionColName: 'English name',
|
||||
batchPartitionMatched: 'Matched',
|
||||
batchPartitionMismatch: 'Partition mismatch',
|
||||
batchPartitionMissing: 'Not found',
|
||||
batchPartitionSkipSame: 'Same partition',
|
||||
batchPartitionApplyConfirm: 'Update partition only for {n} row(s). Continue?',
|
||||
batchPartitionFilterPlaceholder: 'Filter by key / English / code / Chinese (use ; or , for multiple)',
|
||||
batchPartitionSelectAllFiltered: 'Select all (filtered)',
|
||||
batchPartitionClearSelection: 'Clear selection',
|
||||
batchPartitionFilterCount: 'Showing {show} of {total}',
|
||||
batchPartitionSelectionHint:
|
||||
'Filter the table below, then tick rows to update. "Select all" only selects filtered rows that need a partition change. If nothing is ticked, all rows that need an update are submitted.',
|
||||
batchPartitionNoSelection: 'Selection mode is on but no rows are ticked. Tick at least one row, or click Clear selection to submit all.',
|
||||
batchPartitionDone: 'Done: {input} line(s) in batch; {ok} updated, {fail} failed, {miss} not found.',
|
||||
batchPartitionSummaryLine:
|
||||
'Summary: {input} line(s) parsed; {matched} matched; {miss} not found; {same} unchanged (same partition); {will} will be updated.'
|
||||
},
|
||||
mailboxConfig: {
|
||||
mailSystem: 'Mailbox system',
|
||||
@@ -402,7 +510,7 @@ const en = {
|
||||
languagePlaceholder: 'Language',
|
||||
searchBtn: 'Search',
|
||||
createTemplate: 'Create Template',
|
||||
colTitle: 'Template title',
|
||||
colTitle: 'Template title',
|
||||
colSubject: 'Email subject',
|
||||
colScene: 'Scene',
|
||||
colLanguage: 'Language',
|
||||
@@ -418,6 +526,23 @@ colTitle: 'Template title',
|
||||
deleteFail: 'Delete failed',
|
||||
previewTitle: 'Template preview',
|
||||
previewClose: 'Close',
|
||||
batchImportBtn: 'Batch import',
|
||||
batchImportTitle: 'Batch import templates (JSON)',
|
||||
batchImportHint:
|
||||
'Paste a JSON array. Each item is saved via the same API as the editor (omit template_id to create; include template_id to update). Fields: title, subject, scene, language (or lang), version, body_html (or body), variables_json (or variables), is_active.',
|
||||
batchImportCommonTip: 'Journal ID is set in the field below; when non-empty it overrides journal_id / journalId on every row.',
|
||||
batchImportJournalId: 'Journal ID',
|
||||
batchImportJournalPlaceholder: 'Match list filter or type manually',
|
||||
batchImportRun: 'Run import',
|
||||
batchImportBadJson: 'Invalid JSON',
|
||||
batchImportEmpty: 'Array must contain at least one object',
|
||||
batchImportMissingJournal: 'Row {index}: missing journal_id (use the input above or put journal_id in JSON)',
|
||||
batchImportMissingField: 'Row {index}: missing {field}',
|
||||
batchImportRowFail: 'Row {index} failed: {msg}',
|
||||
batchImportRowNetwork: 'Row {index}: request error',
|
||||
batchImportDone: 'Done: {ok} succeeded, {fail} failed',
|
||||
batchImportErrorsTitle: 'Errors (first 8)',
|
||||
batchImportSaveFail: 'Save failed',
|
||||
},
|
||||
mailboxStyle: {
|
||||
title: 'Email Styles',
|
||||
@@ -549,6 +674,72 @@ colTitle: 'Template title',
|
||||
printBtn: 'Print',
|
||||
previewNotSupported: 'This file format cannot be previewed online',
|
||||
downloadToView: 'Download to view locally',
|
||||
registerAuthorBtn: 'Auto submit',
|
||||
registerAuthorConfirm:
|
||||
'Create an account via the same admin API as User Management: login name "{account}", display name "{realname}", email "{email}", initial password 123456qwe (no captcha). Continue?',
|
||||
registerAuthorSuccess: 'Author account created.',
|
||||
registerAuthorSuccessWithEmail: 'Created: {email}, password: 123456qwe',
|
||||
registerAuthorFail: 'Creation failed. Try again later or add the user manually in User Management.',
|
||||
registerAuthorExistsEmail: 'This email is already registered.',
|
||||
registerAuthorExistsAccount: 'This login name is already taken. Edit the sender display name or add the user manually.',
|
||||
registerAuthorNeedEmail: 'Sender email is missing; cannot create an account.',
|
||||
registerAuthorNoQq: 'QQ Mail is not supported for author accounts. Please add the user manually.',
|
||||
registerAuthorConfirmShort: 'Email: {email}\nPassword: {password}',
|
||||
registerAuthorPickEmailFail: 'Could not allocate an available email after several attempts. Try again later or add the user manually.',
|
||||
autoSubmitBtn: 'Submit first .docx as manuscript',
|
||||
autoSubmitTitle: 'Log in as author and upload the first .docx attachment',
|
||||
autoSubmitSessionTip:
|
||||
'Author checkLogin does not change your U_* local account. The server cookie is the author during upload. After finishing, enter your editor password below and log in again to restore the editor server session.',
|
||||
autoSubmitUsername: 'Username',
|
||||
autoSubmitPassword: 'Password',
|
||||
autoSubmitSenderEmailLabel: 'Sender email',
|
||||
autoSubmitSenderEmailPlaceholder: '(No sender email)',
|
||||
autoSubmitCode: 'Captcha',
|
||||
autoSubmitCodePh: 'Fill only if the server requires captcha; often leave empty',
|
||||
autoSubmitCancel: 'Cancel',
|
||||
autoSubmitDialogTitle: 'Auto submit',
|
||||
autoSubmitConfirm: 'Auto submit',
|
||||
autoSubmitExistingAccountTip:
|
||||
'This sender email may already have an account. Enter your password. Re-selecting a local file replaces the previous one.',
|
||||
autoSubmitNotifyMailSubject: '[{journal}] Please complete your submission',
|
||||
autoSubmitNotifyMailFail: 'Could not send the notification email. You can resend from the compose page.',
|
||||
autoSubmitNotifyMailSkipped: 'No sender mailbox (j_email_id) found; skipped automatic email.',
|
||||
autoSubmitNoDocx: 'No .docx attachment in this message (only .docx is supported, same as new submission).',
|
||||
autoSubmitDownloadFail: 'Could not download the attachment. Try again later.',
|
||||
autoSubmitSuccessTitle: 'Manuscript created',
|
||||
autoSubmitDialogClose: 'Close',
|
||||
autoSubmitSuccessLineAccount: 'Account: {account}',
|
||||
autoSubmitSuccessLinePassword: 'Password: {password}',
|
||||
autoSubmitSuccessLineDraft: 'Manuscript ID: {id}. Draft created in staging.',
|
||||
autoSubmitSuccessLineLinkPrefix: 'Submission link: ',
|
||||
autoSubmitSuccessMailSent: 'Notification email sent',
|
||||
autoSubmitSuccessMailSkipped: 'No notification email (journal sender mailbox not configured).',
|
||||
autoSubmitSuccessMailSkippedRecipient: 'No notification email (no valid From address on this message).',
|
||||
autoSubmitSuccessMailFailed: 'Notification email was not sent; try again from the compose page.',
|
||||
autoSubmitSuccessBodyLocalOnly:
|
||||
'Article ID: {id}. The UI still shows your editor account; the server session may still be the author after author login. Refresh or log in again as editor.',
|
||||
autoSubmitSuccessBodyServerRestored:
|
||||
'Article ID: {id}. Logged in again as editor; both local storage and server session should match your editor account.',
|
||||
autoSubmitSuccessNotify:
|
||||
'Article ID: {id}<br/><a href="{link}" target="_blank" rel="noopener noreferrer">Open articleAdd</a>',
|
||||
autoSubmitEditorRestorePwd: 'Editor password (restore session)',
|
||||
autoSubmitEditorRestorePwdPh:
|
||||
'Optional: password for the current editor account shown in the header, used after success to restore the server session',
|
||||
autoSubmitEditorReloginFail:
|
||||
'Could not restore the editor server session; local values were restored where possible. Refresh the page or log in again as editor.',
|
||||
autoSubmitFail: 'Submission failed. Check credentials or network and try again.',
|
||||
autoSubmitUsernameRequired: 'Username is required',
|
||||
autoSubmitPasswordRequired: 'Password is required',
|
||||
autoSubmitJournalLabel: 'Journal',
|
||||
autoSubmitJournalUnknown: 'No journal (switch to a mailbox account that is bound to a journal)',
|
||||
autoSubmitNeedJournal:
|
||||
'This mailbox has no journal ID; staging backfill cannot match the submission page. Switch mailbox account first.',
|
||||
autoSubmitFailPartial: '(If contribute succeeded, article ID may be {id}; please verify in admin.)',
|
||||
autoSubmitManuscriptSource: 'Manuscript file',
|
||||
autoSubmitPickLocalDocx: 'Upload .docx from disk',
|
||||
autoSubmitSourceHint: 'Optional: upload a file; otherwise the first .docx in the email is used. Choosing again replaces the current local file.',
|
||||
autoSubmitLocalPicked: 'Local file: {name}',
|
||||
autoSubmitNeedDocxSource: 'Upload a .docx file, or ensure the email has a .docx attachment.',
|
||||
},
|
||||
crawlerKeywords: {
|
||||
pageTitle: 'Keyword Configuration',
|
||||
@@ -894,8 +1085,8 @@ colTitle: 'Template title',
|
||||
step3: 'References',
|
||||
step: 'step',
|
||||
Information: 'Fill in information',
|
||||
|
||||
|
||||
startPreAccept: 'Start the pre-acceptance process',
|
||||
startPreAcceptWithPayment: 'Start the pre-acceptance process and complete your payment',
|
||||
},
|
||||
Formula: {
|
||||
FormulaTemplate: 'Formula Template'
|
||||
@@ -1016,6 +1207,8 @@ colTitle: 'Template title',
|
||||
autoSolicit: 'Auto Solicitation',
|
||||
editConfig: 'Edit Configuration',
|
||||
running: 'Running',
|
||||
stopped: 'Stopped',
|
||||
configure: 'Configure',
|
||||
emailTemplate: 'Email Template',
|
||||
emailStyle: 'Email Style',
|
||||
notStarted: 'Auto solicitation plan is not enabled',
|
||||
@@ -1045,21 +1238,152 @@ colTitle: 'Template title',
|
||||
changeTemplate: 'Change Template',
|
||||
selectPromotionFields: 'Select Promotion Fields',
|
||||
choosePromotionFields: 'Choose Fields',
|
||||
selectPromotionCountry: 'Select Country',
|
||||
choosePromotionCountry: 'Choose Countries',
|
||||
selectedCount: 'Selected {count}',
|
||||
selectAll: 'Select All',
|
||||
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',
|
||||
countrySearchPlaceholder: 'Search countries',
|
||||
countryQuickZone1: 'Partition 1',
|
||||
countryQuickZone2: 'Partition 2',
|
||||
countryQuickZone3: 'Partition 3',
|
||||
countryQuickChina: 'China',
|
||||
countryQuickIndia: 'India',
|
||||
noFieldMatch: 'No matching fields',
|
||||
noCountryMatch: 'No matching countries',
|
||||
confirm: 'Confirm',
|
||||
fieldsSaved: 'Promotion fields saved',
|
||||
countriesSaved: 'Promotion countries saved',
|
||||
confirmAndEnable: 'Confirm and Enable',
|
||||
onlySaveConfig: 'Save configuration only',
|
||||
enableNowNextDay: 'Enable auto promotion now (starts next day)'
|
||||
enableNowNextDay: 'Enable auto promotion now (starts next day)',
|
||||
factoryCreateBtn: 'Create automated promotion task',
|
||||
factoryBatchImportBtn: 'Batch import (JSON)',
|
||||
factoryBatchImportTitle: 'Batch create tasks (JSON)',
|
||||
factoryBatchImportHintShort: 'Submit a JSON array; non-empty fields merge into each row. You can still edit JSON.',
|
||||
factoryBatchImportHint:
|
||||
'Paste a JSON array… Pick journal (getAllJournal) or type ID; template/style/fetch_ids below override JSON when non-empty. Load promotion fields to tick IDs or type comma-separated fetch_ids; load accounts for j_email_id. expert_type "5" needs partitions/countries. Shorthand: zones, countries, email_id_list.',
|
||||
factoryBatchImportCommonTip: 'Journal from cover or ID field; non-empty template ID, style ID, or fetch_ids override JSON on every row.',
|
||||
factoryBatchImportJournalId: 'Journal ID',
|
||||
factoryBatchImportJournalPick: 'Journal',
|
||||
factoryBatchImportJournalEmpty: 'No journals returned. Check api/Journal/getAllJournal.',
|
||||
factoryBatchImportJournalManualPlaceholder: 'Filled when you pick a cover, or type manually',
|
||||
factoryBatchImportTemplateId: 'Template ID',
|
||||
factoryBatchImportStyleId: 'Style ID',
|
||||
factoryBatchImportJournalPlaceholder: 'Merged into payload',
|
||||
factoryBatchImportTemplatePlaceholder: 'Merged into payload',
|
||||
factoryBatchImportStylePlaceholder: 'Merged into payload',
|
||||
factoryBatchImportFetchIdsLabel: 'Promotion fields (fetch_ids)',
|
||||
factoryBatchImportLoadFields: 'Load available fields',
|
||||
factoryBatchImportFetchTip: 'Uses current journal: pick journal, then load. Search by name or ID (multiple tokens: space or comma). “Select all” selects the filtered list when search is set, otherwise all fields. Checkboxes sync with the comma text; when non-empty, overrides fetch_ids on every row.',
|
||||
factoryBatchImportFetchIdsManual: 'Merged IDs (comma-separated, editable)',
|
||||
factoryBatchImportFetchIdsPlaceholder: 'e.g. 1,2,3 or use checkboxes',
|
||||
factoryBatchImportNeedJournalForFields: 'Select or enter journal ID first',
|
||||
factoryBatchImportLoadAccounts: 'Load accounts for journal',
|
||||
factoryBatchImportAccountsApiTip: 'POST api/email_client/getAccounts with journal_id',
|
||||
factoryBatchImportColEmailId: 'j_email_id',
|
||||
factoryBatchImportColAddress: 'Sender address',
|
||||
factoryBatchImportColQuota: 'Remaining / daily limit',
|
||||
factoryBatchImportCopyEmailIds: 'Copy email_ids (comma)',
|
||||
factoryBatchImportNeedJournalForAccounts: 'Enter journal ID first',
|
||||
factoryBatchImportNoAccounts: 'No mailbox accounts for this journal',
|
||||
factoryBatchImportAccountsFail: 'Failed to load accounts',
|
||||
factoryBatchImportCopyEmailIdsEmpty: 'Load the account list first',
|
||||
factoryBatchImportIdsCopied: 'Copied j_email_id list to clipboard',
|
||||
factoryBatchImportCopyFail: 'Copy failed; select and copy manually',
|
||||
factoryBatchImportSyncToJson: 'Apply top form to JSON',
|
||||
factoryBatchImportSyncFromJson: 'Load first row into form',
|
||||
factoryBatchImportSyncIncludeEmails: 'When applying, set each row email_ids from loaded accounts',
|
||||
factoryBatchImportSyncTip: 'You can still edit JSON manually; non-empty top fields are merged again on submit.',
|
||||
factoryBatchImportJsonFromUiOk: 'JSON updated from the form',
|
||||
factoryBatchImportUiFromJsonOk: 'Form updated from the first JSON row',
|
||||
factoryBatchImportRun: 'Run batch create',
|
||||
factoryBatchImportBadJson: 'Invalid JSON; check brackets and quotes',
|
||||
factoryBatchImportEmpty: 'Array must contain at least one object',
|
||||
factoryBatchImportMissing: 'Row {index}: missing field {field}',
|
||||
factoryBatchImportNeedFetch: 'Row {index}: expert database requires fetch_ids',
|
||||
factoryBatchImportNeedZone: 'Row {index}: expert database requires target_partitions or target_country_ids',
|
||||
factoryBatchImportRowFail: 'Row {index} failed: {msg}',
|
||||
factoryBatchImportRowNetwork: 'Row {index}: request error',
|
||||
factoryBatchImportDone: 'Done: {ok} succeeded, {fail} failed',
|
||||
factoryBatchImportErrorsTitle: 'Errors (first 8)',
|
||||
factoryDialogTitle: 'Create task',
|
||||
factoryJournal: 'Journal',
|
||||
factoryJournalPlaceholder: 'Select a journal',
|
||||
factorySendSettings: 'Sending & scenario',
|
||||
factoryEmails: 'Sender accounts',
|
||||
factoryEmailsPlaceholder: 'Select one or more sender accounts',
|
||||
factorySendCount: 'Send count',
|
||||
factoryType: 'Scenario',
|
||||
factoryTypeEditor: 'Editor',
|
||||
factoryTypeArticle: 'Promote article',
|
||||
factoryExpertType: 'Expert type',
|
||||
factoryExpertTypePlaceholder: 'Optional; follow backend rules',
|
||||
factorySubmit: 'Submit task',
|
||||
factorySubmitSuccess: 'Factory task created',
|
||||
factorySubmitFailed: 'Create failed, please try again later',
|
||||
factoryNeedJournal: 'Please select a journal first',
|
||||
factoryNeedTemplate: 'Please select email template and style',
|
||||
factoryNeedEmails: 'Please select at least one sender account',
|
||||
factoryNeedExpertType: 'Please select target person type',
|
||||
factoryEmailsPickJournal: 'Select a journal to load sender accounts',
|
||||
factoryNoAccounts: 'No mailbox accounts for this journal',
|
||||
factoryAccountRemaining: 'Remaining today',
|
||||
factorySendMaxFromApi: 'limit: up to {max} per day',
|
||||
factorySendMaxFallback: 'using mailbox quota sum ~{max} (or default cap)',
|
||||
factoryStepNav1Title: 'Journal',
|
||||
factoryStepNav1Desc: 'Select a journal first.',
|
||||
factoryStepNav2Title: 'Email template and style',
|
||||
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.',
|
||||
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.',
|
||||
factoryPromotionCountryBlockTip: 'Tick at least one partition or country; do not submit with none selected.',
|
||||
factoryNeedPromotionFields: 'Select at least one promotion field before submitting.',
|
||||
factoryNeedPromotionCountry: 'Select at least one partition or country before submitting.',
|
||||
factoryQuotaLabel: 'Quota',
|
||||
factoryClickSelectTemplate: 'Click to select email template',
|
||||
factoryClickConfigureFields: 'Click to configure subject fields',
|
||||
factoryBtnModify: 'Edit',
|
||||
factoryBtnReset: 'Reset',
|
||||
factoryBtnCancel: 'Cancel',
|
||||
factoryBtnSubmit: 'Submit task',
|
||||
factoryFillRequired: 'Please complete journal, template, and at least one sender account',
|
||||
factoryExpertChief: 'Editor-in-Chief',
|
||||
factoryExpertBoard: 'Editorial board',
|
||||
factoryExpertYoungBoard: 'Young editorial board',
|
||||
factoryExpertAuthor: 'Author',
|
||||
factoryExpertDb: 'Expert database',
|
||||
factoryExpertJump: 'View',
|
||||
factoryOfficialEmailTip: 'For this type, the system uses the official sender email by default. No account selection is required.',
|
||||
factoryScenario: 'Scenario',
|
||||
factoryScenarioPlaceholder: 'Select scenario',
|
||||
factoryScenarioSolicit: 'Invite Submission',
|
||||
factoryScenarioPromoteCitation: 'Promote Citation',
|
||||
factoryScenarioGeneralThanks: 'General Thanks',
|
||||
createdAt: 'Created at',
|
||||
noFactoryTask: 'No tasks',
|
||||
factoryCreateNow: 'Create now',
|
||||
emailClientCreateTaskBtn: 'Create task',
|
||||
emailClientCreateTaskNeedFactory: 'Please select a promotion factory task in the dropdown first',
|
||||
emailClientCreateTaskSuccess: 'Task created',
|
||||
emailClientCreateTaskFailed: 'Failed to create task',
|
||||
emailClientCreateTaskPreparingHint: 'Task created. Generating the mailing list may take a few minutes, please wait...'
|
||||
}
|
||||
,
|
||||
autoPromotionLogs: {
|
||||
detail: 'Auto Promotion Details',
|
||||
pipelineHistory: 'PIPELINE HISTORY',
|
||||
factoryTaskSelectPlaceholder: 'Select promotion task',
|
||||
configured: 'Configured',
|
||||
editConfig: 'Edit auto promotion configuration',
|
||||
startConfig: 'Start auto promotion configuration',
|
||||
@@ -1100,6 +1424,8 @@ colTitle: 'Template title',
|
||||
enable: 'Enable',
|
||||
pause: 'Pause',
|
||||
previewEditTitle: 'Preview and edit promotion email',
|
||||
logDetailEditTitle: 'Edit promotion send log',
|
||||
logDetailPreviewTitle: 'Preview promotion send log',
|
||||
receiver: 'Receiver:',
|
||||
receiverImmutablePlaceholder: 'Receiver email cannot be changed',
|
||||
subject: 'Subject:',
|
||||
@@ -1124,6 +1450,7 @@ colTitle: 'Template title',
|
||||
taskLogState2: 'Failed',
|
||||
taskLogState3: 'Bounced',
|
||||
taskLogState4: 'Cancelled',
|
||||
logColIndex: 'No.',
|
||||
logColExpert: 'Expert',
|
||||
logColSendTime: 'Sent at',
|
||||
logColPreparedAt: 'Prepared at',
|
||||
|
||||
@@ -19,6 +19,13 @@ const zh = {
|
||||
}, info: {
|
||||
realname: '英文名字只能包含大小写英文字母、"-" 、和 空格'
|
||||
},
|
||||
articleAdd: {
|
||||
qqEmailAlertLine1: '出版社暂不支持qq邮箱投稿。',
|
||||
qqEmailAlertLine2Before: '请前往',
|
||||
qqEmailDashboardLink: '个人中心(Dashboard)',
|
||||
qqEmailAlertLine2After: '更换邮箱账号。',
|
||||
qqEmailSubmitBlockedMsg: '出版社暂不支持qq邮箱投稿,请去个人中心更换邮箱账号'
|
||||
},
|
||||
total: {
|
||||
author: '作者',
|
||||
editor: '编辑',
|
||||
@@ -38,6 +45,13 @@ const zh = {
|
||||
status: '状态',
|
||||
delete: '删除',
|
||||
deleteInfo: '您确定要删除该期刊分期吗?',
|
||||
plagiarismNotChecked: '未检测',
|
||||
plagiarismChecking: '正在检测…',
|
||||
plagiarismRecheck: '重新查重',
|
||||
plagiarismCheckFailed: '查重任务启动失败。',
|
||||
plagiarismStatusFailed: '获取查重状态失败。',
|
||||
plagiarismNoReportUrl: '报告链接暂不可用。',
|
||||
plagiarismReportDetailFailed: '获取稿件详情失败,请稍后重试。',
|
||||
},
|
||||
menu: {
|
||||
main: '个人中心',
|
||||
@@ -269,6 +283,7 @@ const zh = {
|
||||
keywordManagement: '关键词管理',
|
||||
crawlTasks: '抓取任务',
|
||||
expertList: '专家列表',
|
||||
countryManagement: '国家信息',
|
||||
autoPromotion: '自动推广',
|
||||
ReArticles: '被拒稿件', // 被拒稿件
|
||||
editorialBoard: '编委管理',
|
||||
@@ -296,16 +311,109 @@ const zh = {
|
||||
},
|
||||
columns: {
|
||||
baseInfo: '基础信息',
|
||||
country: '国家',
|
||||
affiliation: '单位',
|
||||
researchAreas: '研究领域'
|
||||
researchAreas: '研究领域',
|
||||
unsubscribeStatus: '订阅状态'
|
||||
},
|
||||
emptyMark: '-',
|
||||
fields: {
|
||||
nameLabel: '姓名:',
|
||||
emailLabel: '邮箱:',
|
||||
acquisitionTimeLabel: '采集时间:'
|
||||
},
|
||||
viewAllInfo: '查看全部信息',
|
||||
detailDialogTitle: '领域与文章',
|
||||
detailColField: '研究领域',
|
||||
detailColPaper: '文章标题',
|
||||
detailColJournal: '所属期刊',
|
||||
detailClose: '关闭',
|
||||
detailCellEmpty: '暂无',
|
||||
noFieldDetail: '暂无领域对应的文献信息',
|
||||
exportWarn: '请选择研究领域或输入关键词或领域 field 后再导出。',
|
||||
exportFailed: '导出失败'
|
||||
exportFailed: '导出失败',
|
||||
unsubscribeNormal: '已订阅',
|
||||
unsubscribeUnsubscribed: '已退订',
|
||||
unsubscribeSwitchOn: '退订',
|
||||
unsubscribeSwitchOff: '已订阅',
|
||||
unsubscribeMissingId: '缺少专家 ID,无法切换退订状态',
|
||||
unsubscribeUpdateSuccess: '退订状态更新成功',
|
||||
unsubscribeUpdateFailed: '退订状态更新失败'
|
||||
},
|
||||
countryManagement: {
|
||||
title: '国家信息维护',
|
||||
keywordPlaceholder: '中文名 / 英文名 / 代码',
|
||||
partitionLabel: '分区',
|
||||
partitionAll: '全部分区',
|
||||
partition1: '分区 1',
|
||||
partition2: '分区 2',
|
||||
partition3: '分区 3',
|
||||
searchBtn: '搜索',
|
||||
resetBtn: '重置',
|
||||
table: {
|
||||
no: '序号',
|
||||
zhName: '中文名称',
|
||||
enName: '英文名称',
|
||||
code: '代码',
|
||||
partition: '分区',
|
||||
actions: '操作'
|
||||
},
|
||||
edit: '修改',
|
||||
delete: '删除',
|
||||
editTitle: '编辑国家信息',
|
||||
form: {
|
||||
zhName: '中文名称',
|
||||
enName: '英文名称',
|
||||
code: '代码',
|
||||
partition: '分区'
|
||||
},
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
confirm: '确定',
|
||||
deleteTitle: '删除确认',
|
||||
deleteConfirm: '确定删除该国家信息?删除后不可恢复。',
|
||||
saveSuccess: '保存成功',
|
||||
deleteSuccess: '删除成功',
|
||||
opFailed: '操作失败',
|
||||
loadFailed: '加载列表失败',
|
||||
missingId: '缺少国家编号,无法删除',
|
||||
ruleZhName: '请输入中文名称',
|
||||
ruleEnName: '请输入英文名称',
|
||||
ruleCode: '请输入代码',
|
||||
rulePartition: '请选择分区',
|
||||
batchPartitionBtn: '批量修改分区',
|
||||
batchPartitionTitle: '批量修改分区',
|
||||
batchPartitionTargetLabel: '目标分区',
|
||||
batchPartitionTargetRequired: '请先选择目标分区',
|
||||
batchPartitionHelp:
|
||||
'每行一条:国家代码(3位) 或 英文名 或 中文名。# 开头为注释。示例:\nDNK\nDenmark\n丹麦',
|
||||
batchPartitionPlaceholder: '粘贴数据,例如:\nDNK\nISR',
|
||||
batchPartitionPreview: '预览匹配',
|
||||
batchPartitionApply: '确认写入分区',
|
||||
batchPartitionEmpty: '请先填写有效数据(每行一个国家标识)',
|
||||
batchPartitionLoadListFailed: '拉取全量国家列表失败',
|
||||
batchPartitionPreviewEmpty: '没有可匹配的行',
|
||||
batchPartitionColKey: '输入标识',
|
||||
batchPartitionColCurrentPartition: '当前分区',
|
||||
batchPartitionColPartition: '目标分区',
|
||||
batchPartitionColMatch: '匹配结果',
|
||||
batchPartitionColId: 'country_id',
|
||||
batchPartitionColName: '英文名',
|
||||
batchPartitionMatched: '已匹配',
|
||||
batchPartitionMismatch: '分区不一致',
|
||||
batchPartitionMissing: '未匹配',
|
||||
batchPartitionSkipSame: '分区相同',
|
||||
batchPartitionApplyConfirm: '将按当前规则仅更新分区字段,共 {n} 条。是否继续?',
|
||||
batchPartitionFilterPlaceholder: '筛选:输入标识 / 英文名 / 代码 / 中文名(可用分号、逗号分隔多个关键词)',
|
||||
batchPartitionSelectAllFiltered: '全选当前筛选',
|
||||
batchPartitionClearSelection: '取消全选',
|
||||
batchPartitionFilterCount: '当前显示 {show} / 共 {total} 条',
|
||||
batchPartitionSelectionHint:
|
||||
'说明:可先在下方筛选,再勾选要提交的行。「全选」只勾选当前筛选结果中、且分区有变化的行。未勾选任何行时,将提交全部「将提交更新」的行。',
|
||||
batchPartitionNoSelection: '当前为「仅勾选」模式,请至少勾选一行,或点「取消全选」恢复为提交全部。',
|
||||
batchPartitionDone: '批量完成:本次录入 {input} 条;更新成功 {ok} 条,失败 {fail} 条;未匹配 {miss} 条。',
|
||||
batchPartitionSummaryLine:
|
||||
'统计:录入 {input} 条;已匹配 {matched} 条;未匹配 {miss} 条;分区相同跳过 {same} 条;将提交更新 {will} 条。'
|
||||
},
|
||||
mailboxConfig: {
|
||||
mailSystem: '邮件系统',
|
||||
@@ -407,6 +515,23 @@ const zh = {
|
||||
deleteFail: '删除失败',
|
||||
previewTitle: '模板预览',
|
||||
previewClose: '关闭',
|
||||
batchImportBtn: '批量导入',
|
||||
batchImportTitle: '批量导入邮件模板(JSON)',
|
||||
batchImportHint:
|
||||
'粘贴 JSON 数组,每条对应一次保存接口(新建不传 template_id;更新可带 template_id)。字段与编辑页一致:title、subject、scene、language(可用 lang)、version、body_html(可用 body)、variables_json(可用 variables)、is_active。',
|
||||
batchImportCommonTip: '期刊 ID 在下方单独填写;若填写非空,会覆盖每条 JSON 中的 journal_id / journalId。',
|
||||
batchImportJournalId: '期刊 ID',
|
||||
batchImportJournalPlaceholder: '可与列表筛选一致,或手填',
|
||||
batchImportRun: '开始导入',
|
||||
batchImportBadJson: 'JSON 解析失败',
|
||||
batchImportEmpty: '请至少包含一条对象',
|
||||
batchImportMissingJournal: '第 {index} 条:缺少期刊 ID(请填写上方输入框或在 JSON 中提供 journal_id)',
|
||||
batchImportMissingField: '第 {index} 条:缺少字段 {field}',
|
||||
batchImportRowFail: '第 {index} 条保存失败:{msg}',
|
||||
batchImportRowNetwork: '第 {index} 条请求异常',
|
||||
batchImportDone: '完成:成功 {ok},失败 {fail}',
|
||||
batchImportErrorsTitle: '失败明细(最多 8 条)',
|
||||
batchImportSaveFail: '保存失败',
|
||||
},
|
||||
mailboxStyle: {
|
||||
title: '邮件风格',
|
||||
@@ -538,6 +663,68 @@ const zh = {
|
||||
printBtn: '打印',
|
||||
previewNotSupported: '该文件格式无法在线预览',
|
||||
downloadToView: '下载到本地查看',
|
||||
registerAuthorBtn: '自动投稿',
|
||||
registerAuthorConfirm:
|
||||
'将使用推广后台「添加用户」接口创建账号:登录名「{account}」,显示名「{realname}」,邮箱「{email}」,初始密码 123456qwe(无需验证码)。是否继续?',
|
||||
registerAuthorSuccess: '作者账号已创建。',
|
||||
registerAuthorSuccessWithEmail: '已创建:{email},密码:123456qwe',
|
||||
registerAuthorFail: '创建失败,请稍后重试或到用户管理中手动添加。',
|
||||
registerAuthorExistsEmail: '该邮箱已被注册。',
|
||||
registerAuthorExistsAccount: '该登录名已被占用,请人工处理或修改发件人显示名后重试。',
|
||||
registerAuthorNeedEmail: '缺少发件人邮箱,无法创建账号。',
|
||||
registerAuthorNoQq: '本站不支持 QQ 邮箱作为作者账号,请在用户管理中手动添加。',
|
||||
registerAuthorConfirmShort: '邮箱:{email}\n密码:{password}',
|
||||
registerAuthorPickEmailFail: '多次尝试后仍无法分配到可用邮箱,请稍后重试或手动添加用户。',
|
||||
autoSubmitBtn: '附件一键建稿',
|
||||
autoSubmitTitle: '用作者账号登录并上传首份 .docx 附件',
|
||||
autoSubmitSessionTip:
|
||||
'作者 checkLogin 不会修改顶部 U_* 本地账号;上传期间服务端 Cookie 为作者。完成后请填写「编辑密码」再登录一次以恢复编辑服务端会话。',
|
||||
autoSubmitUsername: '登录名',
|
||||
autoSubmitPassword: '密码',
|
||||
autoSubmitSenderEmailLabel: '发件人邮箱',
|
||||
autoSubmitSenderEmailPlaceholder: '(未识别发件人邮箱)',
|
||||
autoSubmitCode: '验证码',
|
||||
autoSubmitCodePh: '若后台要求验证码则填写,一般可留空',
|
||||
autoSubmitCancel: '取消',
|
||||
autoSubmitDialogTitle: '自动投稿',
|
||||
autoSubmitConfirm: '自动投稿',
|
||||
autoSubmitExistingAccountTip:
|
||||
'该发件邮箱可能已有账号。请填写密码;本地文件可多次重选,新文件会替换上一份。',
|
||||
autoSubmitNotifyMailSubject: '【{journal}】投稿完善提醒',
|
||||
autoSubmitNotifyMailFail: '通知邮件发送失败,可稍后在发件页手动补发。',
|
||||
autoSubmitNotifyMailSkipped: '未识别发件邮箱账号,已跳过自动发信。',
|
||||
autoSubmitNoDocx: '邮件中无 .docx 附件(仅支持 docx,与新增稿件一致)。',
|
||||
autoSubmitDownloadFail: '无法下载附件,请稍后重试。',
|
||||
autoSubmitSuccessTitle: '建稿成功',
|
||||
autoSubmitDialogClose: '关闭',
|
||||
autoSubmitSuccessLineAccount: '账号{account}',
|
||||
autoSubmitSuccessLinePassword: '密码{password}',
|
||||
autoSubmitSuccessLineDraft: '稿号id:{id} 已创建草稿箱',
|
||||
autoSubmitSuccessLineLinkPrefix: '稿件链接地址是:',
|
||||
autoSubmitSuccessMailSent: '邮件通知已发送',
|
||||
autoSubmitSuccessMailSkipped: '未发送通知邮件(未配置期刊发件邮箱)',
|
||||
autoSubmitSuccessMailSkippedRecipient: '未发送通知邮件(邮件中缺少有效发件人邮箱)',
|
||||
autoSubmitSuccessMailFailed: '邮件通知未发送,请稍后在发件页补发',
|
||||
autoSubmitSuccessBodyLocalOnly:
|
||||
'文章 ID:{id}。界面仍为当前编辑账号;服务端在作者登录后可能仍为作者会话,请刷新或重新登录编辑账号。',
|
||||
autoSubmitSuccessBodyServerRestored: '文章 ID:{id}。已通过编辑账号重新登录,本地与服务端会话均已恢复为编辑。',
|
||||
autoSubmitSuccessNotify:
|
||||
'文章 ID:{id}<br/><a href="{link}" target="_blank" rel="noopener noreferrer">打开 articleAdd 继续编辑</a>',
|
||||
autoSubmitEditorRestorePwd: '编辑密码(恢复会话)',
|
||||
autoSubmitEditorRestorePwdPh: '选填:与顶部当前登录名对应的编辑密码,用于上传成功后恢复服务端会话',
|
||||
autoSubmitEditorReloginFail: '恢复编辑服务端会话失败,已尽量恢复本地信息,请刷新页面或重新登录。',
|
||||
autoSubmitFail: '建稿失败,请检查账号密码或网络后重试。',
|
||||
autoSubmitUsernameRequired: '请填写登录名',
|
||||
autoSubmitPasswordRequired: '请填写密码',
|
||||
autoSubmitJournalLabel: '目标期刊',
|
||||
autoSubmitJournalUnknown: '未识别期刊(请先在邮件列表切换绑定期刊的邮箱账号)',
|
||||
autoSubmitNeedJournal: '当前邮箱账号没有期刊信息,无法与投稿页一致回填。请通过「切换邮箱账号」选择绑定期刊的账号。',
|
||||
autoSubmitFailPartial: '(若 contribute 已成功,文章 ID 可能为:{id},请到后台核对)',
|
||||
autoSubmitManuscriptSource: '稿件文件',
|
||||
autoSubmitPickLocalDocx: '本地上传 .docx',
|
||||
autoSubmitSourceHint: '可选本地上传;否则使用邮件中第一份 .docx。再次选择会替换当前本机文件。',
|
||||
autoSubmitLocalPicked: '当前本机文件:{name}',
|
||||
autoSubmitNeedDocxSource: '请在本地上传 .docx,或确保邮件中带有 .docx 附件。',
|
||||
},
|
||||
crawlerKeywords: {
|
||||
pageTitle: '关键词配置',
|
||||
@@ -883,8 +1070,8 @@ const zh = {
|
||||
step3: '参考',
|
||||
step: 'step',
|
||||
Information: 'Fill in information',
|
||||
|
||||
|
||||
startPreAccept: '开始预接收流程',
|
||||
startPreAcceptWithPayment: '开始预接收流程并完成支付',
|
||||
},
|
||||
Formula:{
|
||||
FormulaTemplate:'公式模版'
|
||||
@@ -1001,6 +1188,8 @@ const zh = {
|
||||
autoSolicit: '自动约稿',
|
||||
editConfig: '修改配置',
|
||||
running: '运行中',
|
||||
stopped: '已停止',
|
||||
configure: '配置',
|
||||
emailTemplate: '邮件模板',
|
||||
emailStyle: '邮件风格',
|
||||
notStarted: '未开启自动约稿计划',
|
||||
@@ -1030,21 +1219,152 @@ const zh = {
|
||||
changeTemplate: '更换模版',
|
||||
selectPromotionFields: '选择推广领域',
|
||||
choosePromotionFields: '选择领域',
|
||||
selectPromotionCountry: '选择国家',
|
||||
choosePromotionCountry: '选择国家',
|
||||
selectedCount: '已选 {count} 项',
|
||||
selectAll: '全选',
|
||||
clearAll: '取消全选',
|
||||
selectPromotionFieldsTip: '可多选;未选择则不限制推广领域。',
|
||||
selectPromotionCountryTip: '可多选;未选择则不限制国家。与领域接口一致,后续可对接独立国家数据。',
|
||||
fieldSearchPlaceholder: '搜索推广领域',
|
||||
countrySearchPlaceholder: '搜索国家',
|
||||
countryQuickZone1: '1区',
|
||||
countryQuickZone2: '2区',
|
||||
countryQuickZone3: '3区',
|
||||
countryQuickChina: 'China',
|
||||
countryQuickIndia: 'India',
|
||||
noFieldMatch: '没有匹配的领域',
|
||||
noCountryMatch: '没有匹配的国家',
|
||||
confirm: '确定',
|
||||
fieldsSaved: '推广领域已保存',
|
||||
countriesSaved: '推广国家已保存',
|
||||
confirmAndEnable: '确认并开启',
|
||||
onlySaveConfig: '仅保存配置',
|
||||
enableNowNextDay: '立即激活自动推广(次日开始自动推广)'
|
||||
enableNowNextDay: '立即激活自动推广(次日开始自动推广)',
|
||||
factoryCreateBtn: '创建自动化推广任务',
|
||||
factoryBatchImportBtn: '临时批量导入',
|
||||
factoryBatchImportTitle: '批量创建推广任务(JSON)',
|
||||
factoryBatchImportHintShort: '数组提交;上方非空项会合并进每条请求,仍可直接改 JSON。',
|
||||
factoryBatchImportHint:
|
||||
'粘贴 JSON 数组…期刊用封面(getAllJournal)或手填 ID;模板/样式/推广领域 fetch_ids 在下方非空则覆盖 JSON。推广领域可「加载可选领域」勾选或手改逗号 ID;填期刊后可查邮箱 j_email_id(getAccounts)。expert_type 为 5 时须分区或国家。简写:zones、countries、email_id_list。',
|
||||
factoryBatchImportCommonTip: '期刊以封面或下方 ID 为准;模板 ID、样式 ID、推广领域 fetch_ids 任一非空则覆盖每条 JSON 中对应字段后再请求接口。',
|
||||
factoryBatchImportJournalId: '期刊 ID',
|
||||
factoryBatchImportJournalPick: '选择期刊',
|
||||
factoryBatchImportJournalEmpty: '未获取到期刊列表,请检查接口或稍后重试',
|
||||
factoryBatchImportJournalManualPlaceholder: '点击上方封面自动填入,也可手改',
|
||||
factoryBatchImportTemplateId: '模板 ID',
|
||||
factoryBatchImportStyleId: '样式 ID',
|
||||
factoryBatchImportJournalPlaceholder: '与 JSON 合并',
|
||||
factoryBatchImportTemplatePlaceholder: '与 JSON 合并',
|
||||
factoryBatchImportStylePlaceholder: '与 JSON 合并',
|
||||
factoryBatchImportFetchIdsLabel: '推广领域 fetch_ids',
|
||||
factoryBatchImportLoadFields: '加载可选领域',
|
||||
factoryBatchImportFetchTip: '依赖当前期刊:先选期刊再加载。上方可搜索名称或 ID(多关键词用空格或逗号);有搜索时「全选」勾选当前筛选结果,无搜索时「全选」为全部。勾选与下方逗号文本同步,非空则覆盖每条 JSON 的 fetch_ids。',
|
||||
factoryBatchImportFetchIdsManual: '合并用 ID(逗号分隔,可手改)',
|
||||
factoryBatchImportFetchIdsPlaceholder: '例:1,2,3;或与勾选联动',
|
||||
factoryBatchImportNeedJournalForFields: '请先选择或填写期刊 ID',
|
||||
factoryBatchImportLoadAccounts: '查询该期刊邮箱',
|
||||
factoryBatchImportAccountsApiTip: 'POST api/email_client/getAccounts,参数 journal_id',
|
||||
factoryBatchImportColEmailId: 'j_email_id',
|
||||
factoryBatchImportColAddress: '发件地址',
|
||||
factoryBatchImportColQuota: '今日剩余 / 日上限',
|
||||
factoryBatchImportCopyEmailIds: '复制 email_ids(逗号)',
|
||||
factoryBatchImportNeedJournalForAccounts: '请先填写期刊 ID',
|
||||
factoryBatchImportNoAccounts: '该期刊下暂无邮箱账号',
|
||||
factoryBatchImportAccountsFail: '拉取邮箱列表失败',
|
||||
factoryBatchImportCopyEmailIdsEmpty: '请先查询出账号列表',
|
||||
factoryBatchImportIdsCopied: '已复制 j_email_id 列表到剪贴板',
|
||||
factoryBatchImportCopyFail: '复制失败,请手动选中复制',
|
||||
factoryBatchImportSyncToJson: '上方选项写入 JSON',
|
||||
factoryBatchImportSyncFromJson: '首条 JSON 回显到上方',
|
||||
factoryBatchImportSyncIncludeEmails: '写入时用当前邮箱列表覆盖每条 email_ids',
|
||||
factoryBatchImportSyncTip: '写入后仍可单独改 JSON;提交时若上方输入框非空仍会再合并覆盖。',
|
||||
factoryBatchImportJsonFromUiOk: '已根据上方选项更新 JSON',
|
||||
factoryBatchImportUiFromJsonOk: '已用首条 JSON 更新上方表单',
|
||||
factoryBatchImportRun: '开始批量创建',
|
||||
factoryBatchImportBadJson: 'JSON 解析失败,请检查括号与引号',
|
||||
factoryBatchImportEmpty: '请至少包含一条对象',
|
||||
factoryBatchImportMissing: '第 {index} 条缺少字段:{field}',
|
||||
factoryBatchImportNeedFetch: '第 {index} 条:专家库需填写 fetch_ids(推广领域)',
|
||||
factoryBatchImportNeedZone: '第 {index} 条:专家库需至少填写分区或国家(target_partitions / target_country_ids)',
|
||||
factoryBatchImportRowFail: '第 {index} 条创建失败:{msg}',
|
||||
factoryBatchImportRowNetwork: '第 {index} 条请求异常',
|
||||
factoryBatchImportDone: '完成:成功 {ok},失败 {fail}',
|
||||
factoryBatchImportErrorsTitle: '失败明细(最多显示 8 条)',
|
||||
factoryDialogTitle: '创建任务',
|
||||
factoryJournal: '期刊',
|
||||
factoryJournalPlaceholder: '请选择期刊',
|
||||
factorySendSettings: '发送与场景',
|
||||
factoryEmails: '发送邮箱',
|
||||
factoryEmailsPlaceholder: '请选择发送账号(可多选)',
|
||||
factorySendCount: '发送数量',
|
||||
factoryType: '场景类型',
|
||||
factoryTypeEditor: '编辑',
|
||||
factoryTypeArticle: '推广文章',
|
||||
factoryExpertType: '专家类型',
|
||||
factoryExpertTypePlaceholder: '可选,按后端要求填写',
|
||||
factorySubmit: '提交任务',
|
||||
factorySubmitSuccess: '工厂任务已创建',
|
||||
factorySubmitFailed: '创建失败,请稍后重试',
|
||||
factoryNeedJournal: '请先选择期刊',
|
||||
factoryNeedTemplate: '请先选择邮件模板与样式',
|
||||
factoryNeedEmails: '请至少选择一个发送邮箱',
|
||||
factoryNeedExpertType: '请选择目标人类型',
|
||||
factoryEmailsPickJournal: '请先选择期刊以加载邮箱列表',
|
||||
factoryNoAccounts: '该期刊下暂无可用邮箱账号',
|
||||
factoryAccountRemaining: '今日剩余',
|
||||
factorySendMaxFromApi: '接口限制:单日最多 {max} 封',
|
||||
factorySendMaxFallback: '未返回接口上限,当前按邮箱额度合计约 {max} 封(或默认上限)',
|
||||
factoryStepNav1Title: '期刊',
|
||||
factoryStepNav1Desc: '先选期刊,未选不能提交。',
|
||||
factoryStepNav2Title: '邮件模版与样式',
|
||||
factoryStepNav2Desc: '选好邮件模板和样式。',
|
||||
factoryStepNav3Title: '发送与场景',
|
||||
factoryStepNav3Desc: '选账号,填发送数量和目标人类型。',
|
||||
factoryStepNav4Title: '推广领域',
|
||||
factoryStepNav4Desc: '至少选择一个推广领域。',
|
||||
factoryStepNav5Title: '国家',
|
||||
factoryStepNav5Desc: '至少选择一个国家或分区。',
|
||||
factoryStepNav6Title: '确认并开启',
|
||||
factoryStepNav6Desc: '选择仅保存或次日自动开启。',
|
||||
factoryPromotionFieldsBlockTip: '请打开「选择领域」,在列表中至少勾选一项;不得留空提交。',
|
||||
factoryPromotionCountryBlockTip: '请至少勾选一项分区或国家;不得留空提交。',
|
||||
factoryNeedPromotionFields: '请至少选择一项推广领域后再提交。',
|
||||
factoryNeedPromotionCountry: '请至少选择一项分区或国家后再提交。',
|
||||
factoryQuotaLabel: '额度',
|
||||
factoryClickSelectTemplate: '点击选择邮件模板',
|
||||
factoryClickConfigureFields: '点击配置学科字段',
|
||||
factoryBtnModify: '修改',
|
||||
factoryBtnReset: '重置',
|
||||
factoryBtnCancel: '取消',
|
||||
factoryBtnSubmit: '立即提交任务',
|
||||
factoryFillRequired: '请完善必填信息(期刊、模板、账号)',
|
||||
factoryExpertChief: '主编',
|
||||
factoryExpertBoard: '编委',
|
||||
factoryExpertYoungBoard: '青年编委',
|
||||
factoryExpertAuthor: '作者',
|
||||
factoryExpertDb: 'expert库',
|
||||
factoryExpertJump: '查看',
|
||||
factoryOfficialEmailTip: '此类型默认使用系统官方邮箱发送,无需选择邮箱账号。',
|
||||
factoryScenario: '场景',
|
||||
factoryScenarioPlaceholder: '请选择场景',
|
||||
factoryScenarioSolicit: '约稿',
|
||||
factoryScenarioPromoteCitation: '推广引用',
|
||||
factoryScenarioGeneralThanks: '常规感谢',
|
||||
createdAt: '创建时间',
|
||||
noFactoryTask: '没有任务',
|
||||
factoryCreateNow: '立即创建',
|
||||
emailClientCreateTaskBtn: '创建任务',
|
||||
emailClientCreateTaskNeedFactory: '请先在下拉框中选择推广工厂任务',
|
||||
emailClientCreateTaskSuccess: '创建任务成功',
|
||||
emailClientCreateTaskFailed: '创建任务失败',
|
||||
emailClientCreateTaskPreparingHint: '创建任务成功,生成发送邮件列表需要几分钟,请耐心等候...'
|
||||
}
|
||||
,
|
||||
autoPromotionLogs: {
|
||||
detail: '自动推广详情',
|
||||
pipelineHistory: '流水线历史',
|
||||
factoryTaskSelectPlaceholder: '选择推广任务',
|
||||
configured: '已配置',
|
||||
editConfig: '修改期刊自动推广配置',
|
||||
startConfig: '立即开始期刊自动推广配置',
|
||||
@@ -1085,6 +1405,8 @@ const zh = {
|
||||
enable: '开启',
|
||||
pause: '暂停',
|
||||
previewEditTitle: '预览并修改推广邮件',
|
||||
logDetailEditTitle: '编辑推广发送记录',
|
||||
logDetailPreviewTitle: '预览推广发送记录',
|
||||
receiver: '收件人:',
|
||||
receiverImmutablePlaceholder: '收件人邮箱不可更改',
|
||||
subject: '主题:',
|
||||
@@ -1109,6 +1431,7 @@ const zh = {
|
||||
taskLogState2: '失败',
|
||||
taskLogState3: '退信',
|
||||
taskLogState4: '取消',
|
||||
logColIndex: '序号',
|
||||
logColExpert: '专家信息',
|
||||
logColSendTime: '发送时间',
|
||||
logColPreparedAt: '预处理完成时间',
|
||||
|
||||
@@ -859,8 +859,8 @@
|
||||
<el-form-item label="Account :" prop="account">
|
||||
<span>{{ coreForm.account }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="Email :">
|
||||
<span>{{ coreForm.email }}</span>
|
||||
<el-form-item label="Email :" prop="email">
|
||||
<el-input type="text" placeholder="Please enter email..." v-model="coreForm.email" style="width: 320px" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Real name :" prop="realname">
|
||||
@@ -1610,6 +1610,14 @@ this.applyCvitaTable = data;
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.coreTable = res.data.baseInfo;
|
||||
try {
|
||||
if (this.coreTable && this.coreTable.email != null && this.coreTable.email !== undefined) {
|
||||
localStorage.setItem('U_email', String(this.coreTable.email));
|
||||
}
|
||||
if (this.coreTable && this.coreTable.realname != null && this.coreTable.realname !== undefined) {
|
||||
localStorage.setItem('U_relname', String(this.coreTable.realname));
|
||||
}
|
||||
} catch (e) {}
|
||||
this.majorsList=res.data.baseInfo.majors;
|
||||
this.cvitaForm.user_id = res.data.baseInfo.user_id;
|
||||
this.reviewForm.user_id = res.data.baseInfo.user_id;
|
||||
@@ -1901,6 +1909,14 @@ this.applyCvitaTable = data;
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.$message.success('Personal information modified successfully!');
|
||||
try {
|
||||
if (this.coreForm.email != null && this.coreForm.email !== undefined) {
|
||||
localStorage.setItem('U_email', String(this.coreForm.email));
|
||||
}
|
||||
if (this.coreForm.realname != null && this.coreForm.realname !== undefined) {
|
||||
localStorage.setItem('U_relname', String(this.coreForm.realname));
|
||||
}
|
||||
} catch (e) {}
|
||||
this.coreVisible = false;
|
||||
this.tipVisible = false;
|
||||
this.getPersonData();
|
||||
|
||||
@@ -263,7 +263,39 @@
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon" style="line-height: 120px"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item label="Contact Editor WeChat QR Code :" prop="wechat_yboard_qrcode">
|
||||
<div class="wechat-name-split__right">
|
||||
<div class="yboard-qrcode-side__label"></div>
|
||||
<div class="portrait WeChatCode">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
ref="upIconIMgYboard"
|
||||
:action="baseUrl + 'api/Journal/uploadYboardQrcode'"
|
||||
:show-file-list="false"
|
||||
name="qrcode_url"
|
||||
:on-success="handleAvatarSuccessYboard"
|
||||
:on-error="handleAvatarErrorYboard"
|
||||
:before-upload="beforeAvatarUpload2"
|
||||
>
|
||||
<img
|
||||
v-if="detailForm.wechat_yboard_qrcode"
|
||||
:src="
|
||||
/^https?:\/\//.test(detailForm.wechat_yboard_qrcode)
|
||||
? detailForm.wechat_yboard_qrcode
|
||||
: mediaUrl + 'journalyboardqrcode/' + detailForm.wechat_yboard_qrcode
|
||||
"
|
||||
class="avatar"
|
||||
accept=".png,.jpg"
|
||||
/>
|
||||
<i v-else class="el-icon-plus avatar-uploader-icon" style="line-height: 120px"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Journal Topic :" prop="ResearchAreas">
|
||||
<el-button @click="addArea" size="mini" type="primary" plain style="position: absolute; right: 0; margin-left: 10px"
|
||||
>+ Add</el-button
|
||||
@@ -283,8 +315,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Wechat Name :" prop="wechat_name">
|
||||
<el-input v-model="detailForm.wechat_name" placeholder="eg:公众号名称"></el-input>
|
||||
<el-form-item label="Wechat Name :" prop="wechat_name" class="wechat-name-split">
|
||||
<div class="wechat-name-split__inner">
|
||||
<div class="wechat-name-split__left">
|
||||
<el-input v-model="detailForm.wechat_name" placeholder="eg:公众号名称"></el-input>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Wechat APP ID :" prop="wechat_app_id">
|
||||
<el-input v-model="detailForm.wechat_app_id" placeholder="eg:公众号app_id"></el-input>
|
||||
@@ -657,6 +694,17 @@ export default {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
},
|
||||
async handleAvatarSuccessYboard(res, file) {
|
||||
if (res.code == 0) {
|
||||
this.detailForm.wechat_yboard_qrcode = res.upurl;
|
||||
this.$forceUpdate();
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
},
|
||||
handleAvatarErrorYboard(res, file) {
|
||||
// no-op
|
||||
},
|
||||
handleAvatarError2(res, file) {
|
||||
// this.$message.error(res);
|
||||
},
|
||||
@@ -883,7 +931,9 @@ export default {
|
||||
epassword: data.epassword,
|
||||
kfen: data.kfen,
|
||||
fee: data.fee,
|
||||
databases: data.databases || data.database_inclusion || '',
|
||||
editor_qrcode: data.editor_qrcode,
|
||||
wechat_yboard_qrcode: data.wechat_yboard_qrcode || data.yboard_qrcode,
|
||||
scope: data.scope,
|
||||
abstract_chinese: data.abstract_chinese,
|
||||
publish_author: data.publish_author,
|
||||
@@ -1369,6 +1419,29 @@ export default {
|
||||
border-radius: 110px;
|
||||
}
|
||||
|
||||
/* Wechat Name + Apply Youth Board QR:两列对齐到右侧 */
|
||||
.wechat-name-split__inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wechat-name-split__left {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.wechat-name-split__right {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.yboard-qrcode-side__label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.portrait .ptmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -215,6 +215,8 @@
|
||||
localStorage.setItem('U_role', 'superadmin');
|
||||
localStorage.setItem('U_name', res.userinfo.account);
|
||||
localStorage.setItem('U_id', res.userinfo.user_id);
|
||||
localStorage.setItem('U_email', '');
|
||||
|
||||
|
||||
this.$router.push('/');
|
||||
} else if (res.data.roles.includes('editor')) {
|
||||
@@ -222,6 +224,7 @@
|
||||
localStorage.setItem('U_role', res.data.roles);
|
||||
localStorage.setItem('U_name', res.data.userinfo.account);
|
||||
localStorage.setItem('U_id', res.data.userinfo.user_id);
|
||||
localStorage.setItem('U_email', res.data.userinfo.email);
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
localStorage.setItem('U_status', '2'); //其余的身份
|
||||
@@ -229,6 +232,7 @@
|
||||
localStorage.setItem('U_name', res.data.userinfo.account);
|
||||
localStorage.setItem('U_id', res.data.userinfo.user_id);
|
||||
localStorage.setItem('U_relname', res.data.userinfo.realname);
|
||||
localStorage.setItem('U_email', res.data.userinfo.email);
|
||||
this.$router.push('/');
|
||||
// this.roleVisible = true;
|
||||
// this.user_cap = res.data.roles;
|
||||
|
||||
238
src/components/page/YouthBoardSubmitSuccess.vue
Normal file
238
src/components/page/YouthBoardSubmitSuccess.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="submission-page">
|
||||
<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>
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<div v-if="isFromChina" class="qr-section">
|
||||
<div v-if="qrLoading" class="qr-loading">Loading...</div>
|
||||
<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> -->
|
||||
</div>
|
||||
|
||||
<router-link to="/login" replace class="back-btn"> Login to the Submission System Now </router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'YouthBoardSubmitSuccess',
|
||||
data() {
|
||||
return {
|
||||
qrCodeUrl: '',
|
||||
qrLoading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFromChina() {
|
||||
const s = String(this.$route.query.country || '').trim();
|
||||
if (!s) return false;
|
||||
// 兼容:1 / China / 中国 / CN / CHN
|
||||
if (s === '1') return true;
|
||||
if (s === 'China' || s === 'CN' || s === 'CHN') return true;
|
||||
if (s === '中国') return true;
|
||||
if (s === 'US' || s === 'United States' || s === '美国') return false;
|
||||
return s === 'China';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.isFromChina) this.loadApplyBaseInfo();
|
||||
},
|
||||
methods: {
|
||||
normalizeCountry(v) {
|
||||
const s = String(v || '').trim();
|
||||
if (!s) return '';
|
||||
if (s === '中国' || s === 'CN' || s === 'CHN') return 'China';
|
||||
if (s === '美国' || s === 'USA' || s === 'US') return 'United States';
|
||||
if (s === '英国' || s === 'UK' || s === 'GBR') return 'United Kingdom';
|
||||
return s;
|
||||
},
|
||||
buildMediaUrl(raw) {
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) return raw;
|
||||
const mediaBase = (this.Common && this.Common.mediaUrl ? this.Common.mediaUrl : '').replace(/\/+$/, '');
|
||||
if (!mediaBase) return raw;
|
||||
const cleanPath = String(raw).replace(/^\/+/, '');
|
||||
// 与后台上传目录保持一致(JournalManagement/common.vue)
|
||||
return `${mediaBase}/journalyboardqrcode/${cleanPath}`;
|
||||
},
|
||||
loadApplyBaseInfo() {
|
||||
const journal_id = this.$route.query.journal_id || this.$route.query.journalId;
|
||||
const expert_id = this.$route.query.expert_id || this.$route.query.expertId;
|
||||
if (!journal_id || !expert_id) {
|
||||
return;
|
||||
}
|
||||
this.qrLoading = true;
|
||||
|
||||
this.$api
|
||||
.post('api/Ucenter/getApplyYboardForExpertBaseInfo', {
|
||||
journal_id,
|
||||
expert_id
|
||||
})
|
||||
.then((res) => {
|
||||
const data = (res && res.data) || {};
|
||||
const journal = data.journal || data.journal_info || data.journalInfo || null;
|
||||
const raw =
|
||||
(journal && (journal.wechat_yboard_qrcode || journal.yboard_qrcode || journal.qrcode_url)) || '';
|
||||
this.qrCodeUrl = this.buildMediaUrl(raw);
|
||||
})
|
||||
.catch(() => {
|
||||
this.qrCodeUrl = '';
|
||||
})
|
||||
.finally(() => {
|
||||
this.qrLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.submission-page {
|
||||
--primary-blue: #3a91d9;
|
||||
--text-main: #2c3e50;
|
||||
--bg-light: #f7f9fc;
|
||||
--success-green: #48bb78;
|
||||
--danger: #e53e3e;
|
||||
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--bg-light);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
height: fit-content;
|
||||
padding: 60px 40px;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 60px;
|
||||
color: var(--success-green);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 24px;
|
||||
color: var(--text-main);
|
||||
font-weight: 700;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.success-desc {
|
||||
font-size: 16px;
|
||||
color: #718096;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #c3dafe;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.qr-header {
|
||||
color: #2b6cb0;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.qr-code-box {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
margin: 15px auto;
|
||||
border: 1px solid #eee;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-code-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.remark-tip {
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.remark-box {
|
||||
background: #fff;
|
||||
border: 1px solid #feb2b2;
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-block;
|
||||
margin-top: 40px;
|
||||
color: #006699;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
margin: 15px auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #718096;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
612
src/components/page/YouthEditorialBoardRegistration.vue
Normal file
612
src/components/page/YouthEditorialBoardRegistration.vue
Normal file
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<div class="registration-container">
|
||||
<div class="card">
|
||||
<h1>Youth Editorial Board Registration</h1>
|
||||
<div v-if="journalInfoLoaded" class="journal-hero">
|
||||
<div class="journal-cover-wrap" v-if="journalCoverUrl">
|
||||
<img :src="journalCoverUrl" :alt="journalTitle || 'Journal Cover'" class="journal-cover" />
|
||||
</div>
|
||||
<div class="journal-title">{{ journalTitle || 'Journal Information' }}</div>
|
||||
</div>
|
||||
<!-- <p v-if="journalId && expertId" class="link-meta">
|
||||
Journal ID: <strong>{{ journalId }}</strong> · Expert ID: <strong>{{ expertId }}</strong>
|
||||
</p> -->
|
||||
<!-- <p v-else class="link-meta link-meta--warn">
|
||||
Missing <code>journal_id</code> and/or <code>expert_id</code> in the URL. Please use the full invitation link.
|
||||
</p> -->
|
||||
|
||||
<div v-if="submitSuccess" class="success-panel">
|
||||
<h2>Thank you</h2>
|
||||
<p>Your registration has been submitted. We will contact you by email.</p>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="handleSubmit" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> English Name</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.engName"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> Email (QQ mail is not allowed)</label>
|
||||
<input type="email" v-model="formData.email" placeholder="" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> Password</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="formData.password"
|
||||
placeholder=""
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label><span class="required-star">*</span> Upload CV</label>
|
||||
<div v-if="!selectedFile" class="upload-trigger" @click="$refs.fileInput.click()">
|
||||
<div class="icon">📤</div>
|
||||
<div class="hint">Click to upload PDF CV</div>
|
||||
<input type="file" ref="fileInput" hidden accept=".pdf" @change="onFileChange" />
|
||||
</div>
|
||||
<div v-else class="file-display-box">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name">{{ selectedFile.name }}</span>
|
||||
<span class="remove-btn" @click.prevent="removeFile">×</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <transition name="fade">
|
||||
<div v-if="formData.country === 'China'" class="qr-area">
|
||||
<div class="qr-title">Mandatory for Youth Scientists in China</div>
|
||||
<div v-if="wechatQrUrl" class="qr-box">
|
||||
<img :src="wechatQrUrl" alt="Group QR Code" @error="onQrImgError" />
|
||||
</div>
|
||||
<p v-else class="qr-fallback">Please add image file <code>public/youth-board-wechat-qr.png</code> for the group QR code.</p>
|
||||
<p class="qr-hint">Please use the following format for the group remark:</p>
|
||||
<div class="format-tag">Name - Research Field - Affiliation</div>
|
||||
</div>
|
||||
</transition> -->
|
||||
|
||||
<button type="submit" class="submit-btn" :disabled="submitting">
|
||||
{{ submitting ? 'Submitting...' : 'Register Now' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
/** 公开邀请注册:后端实现 multipart 接口后填写此路径(与 Dashboard 青年编委申请字段对齐可再改) */
|
||||
const SUBMIT_URL = '/api/User/youthBoardInviteRegister';
|
||||
|
||||
export default {
|
||||
name: 'YouthEditorialBoardRegistration',
|
||||
data() {
|
||||
return {
|
||||
baseUrl: this.Common.baseUrl,
|
||||
formData: {
|
||||
engName: '',
|
||||
email: '',
|
||||
country: '',
|
||||
password: ''
|
||||
},
|
||||
countryList: [],
|
||||
selectedFile: null,
|
||||
submitting: false,
|
||||
submitSuccess: false,
|
||||
wechatQrUrl: '',
|
||||
journalInfoLoaded: false,
|
||||
journalDetail: null,
|
||||
uploadedCvPath: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
journalId() {
|
||||
const q = this.$route.query || {};
|
||||
return String(q.journal_id || q.journalId || '').trim();
|
||||
},
|
||||
expertId() {
|
||||
const q = this.$route.query || {};
|
||||
return String(q.expert_id || q.expertId || '').trim();
|
||||
},
|
||||
journalTitle() {
|
||||
const j = this.journalDetail || {};
|
||||
return j.full_name || j.journal_name || j.title || j.abbr || '';
|
||||
},
|
||||
journalCoverUrl() {
|
||||
const j = this.journalDetail || {};
|
||||
const raw =
|
||||
j.journal_icon ||
|
||||
j.cover ||
|
||||
j.img ||
|
||||
j.image ||
|
||||
j.icon ||
|
||||
j.logo ||
|
||||
j.thumb ||
|
||||
j.picture ||
|
||||
j.photo ||
|
||||
j.journal_cover ||
|
||||
j.journal_img ||
|
||||
'';
|
||||
if (!raw) return '';
|
||||
if (/^https?:\/\//i.test(raw)) return raw;
|
||||
const mediaBase = (this.Common.mediaUrl || '').replace(/\/+$/, '');
|
||||
const cleanPath = String(raw).replace(/^\/+/, '');
|
||||
if (!mediaBase) return `/${cleanPath}`;
|
||||
if (/^journal\//i.test(cleanPath) || /^reviewer\//i.test(cleanPath)) {
|
||||
return `${mediaBase}/${cleanPath}`;
|
||||
}
|
||||
return `${mediaBase}/journal/${cleanPath}`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const base = process.env.BASE_URL || '/';
|
||||
const prefix = base.endsWith('/') ? base : `${base}/`;
|
||||
this.wechatQrUrl = `${prefix}youth-board-wechat-qr.png`;
|
||||
this.fetchCountries();
|
||||
this.fetchApplyBaseInfo();
|
||||
},
|
||||
methods: {
|
||||
countryOptionKey(item) {
|
||||
return item.name || item.id || item.country || JSON.stringify(item);
|
||||
},
|
||||
countryOptionValue(item) {
|
||||
return item.name != null ? item.name : item.country || item.title || '';
|
||||
},
|
||||
countryOptionLabel(item) {
|
||||
return item.name != null ? item.name : item.country || item.title || '';
|
||||
},
|
||||
fetchCountries() {
|
||||
// this.$api
|
||||
// .post('api/Reviewer/getCountrys')
|
||||
// .then((res) => {
|
||||
// const list = res.countrys || res.data || [];
|
||||
// this.countryList = Array.isArray(list) ? list : [];
|
||||
// })
|
||||
// .catch(() => {
|
||||
// this.countryList = [{ name: 'China' }, { name: 'United States' }, { name: 'United Kingdom' }];
|
||||
// });
|
||||
},
|
||||
fetchApplyBaseInfo() {
|
||||
if (!this.journalId || !this.expertId) return;
|
||||
this.$api
|
||||
.post('api/Ucenter/getApplyYboardForExpertBaseInfo', {
|
||||
journal_id: this.journalId,
|
||||
expert_id: this.expertId
|
||||
})
|
||||
.then((res) => {
|
||||
const data = (res && res.data) || {};
|
||||
const expertCandidate =
|
||||
data.expert_info ||{}
|
||||
|
||||
const journal =
|
||||
data.journal ||
|
||||
data.journal_info ||
|
||||
data.journalInfo ||
|
||||
data.journal_detail ||
|
||||
data.journalDetail ||
|
||||
null;
|
||||
|
||||
// 为了兼容后端返回字段名,这里做多字段兜底取值
|
||||
const pickFirst = (obj, keys) => {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const k = keys[i];
|
||||
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') {
|
||||
return obj[k];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const pickFromCandidateOrData = (keys) => {
|
||||
const v1 = pickFirst(expertCandidate, keys);
|
||||
if (v1) return v1;
|
||||
return pickFirst(data, keys);
|
||||
};
|
||||
|
||||
const normalizeCountry = (v) => {
|
||||
const s = String(v || '').trim();
|
||||
if (!s) return '';
|
||||
// 常见中文/英文归一化(确保和下拉 options.value 匹配)
|
||||
if (s === '中国' || s === 'CN' || s === 'CHN') return 'China';
|
||||
if (s === '美国' || s === 'USA' || s === 'US') return 'United States';
|
||||
if (s === '英国' || s === 'UK' || s === 'GBR') return 'United Kingdom';
|
||||
return s;
|
||||
};
|
||||
|
||||
this.formData.engName = pickFromCandidateOrData([
|
||||
'eng_name',
|
||||
'english_name',
|
||||
'u_eng_name',
|
||||
'U_eng_name',
|
||||
'U_engName',
|
||||
'U_name_en',
|
||||
'realname',
|
||||
'u_relname',
|
||||
'U_relname',
|
||||
'name',
|
||||
'U_name'
|
||||
]);
|
||||
|
||||
this.formData.email = pickFromCandidateOrData([
|
||||
'email',
|
||||
'u_email',
|
||||
'U_email',
|
||||
'U_email_address',
|
||||
'user_email',
|
||||
'username',
|
||||
'U_username'
|
||||
]);
|
||||
|
||||
this.formData.country = normalizeCountry(
|
||||
pickFromCandidateOrData([
|
||||
'country',
|
||||
'country_name',
|
||||
'u_country',
|
||||
'U_country',
|
||||
'countryName',
|
||||
'U_country_name'
|
||||
])
|
||||
);
|
||||
|
||||
if (journal) {
|
||||
this.journalDetail = journal;
|
||||
this.journalInfoLoaded = true;
|
||||
}
|
||||
if (!journal) {
|
||||
this.journalDetail = null;
|
||||
this.journalInfoLoaded = true;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.journalDetail = null;
|
||||
this.journalInfoLoaded = true;
|
||||
});
|
||||
},
|
||||
async onFileChange(e) {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (file && file.type === 'application/pdf') {
|
||||
this.selectedFile = file;
|
||||
const fd = new FormData();
|
||||
fd.append('reviewerCV', file);
|
||||
try {
|
||||
const resp = await axios.post(`${this.baseUrl}api/Ucenter/up_cv_file`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
const body = resp && resp.data ? resp.data : {};
|
||||
if (Number(body.code) === 0 && body.upurl) {
|
||||
this.uploadedCvPath = body.upurl;
|
||||
} else {
|
||||
this.selectedFile = null;
|
||||
this.uploadedCvPath = '';
|
||||
this.alertError((body && body.msg) || 'CV upload failed.');
|
||||
}
|
||||
} catch (err) {
|
||||
this.selectedFile = null;
|
||||
this.uploadedCvPath = '';
|
||||
this.alertError('CV upload failed. Please try again.');
|
||||
}
|
||||
} else if (file) {
|
||||
this.alertError('Please upload a valid PDF file.');
|
||||
}
|
||||
},
|
||||
removeFile() {
|
||||
this.selectedFile = null;
|
||||
this.uploadedCvPath = '';
|
||||
if (this.$refs.fileInput) this.$refs.fileInput.value = '';
|
||||
},
|
||||
onQrImgError() {
|
||||
this.wechatQrUrl = '';
|
||||
},
|
||||
alertError(msg) {
|
||||
this.$message.error(msg);
|
||||
},
|
||||
async handleSubmit() {
|
||||
if (!this.journalId || !this.expertId) {
|
||||
this.alertError('Invalid link: journal_id and expert_id are required in the URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
const engName = (this.formData.engName || '').trim();
|
||||
if (!engName) {
|
||||
this.alertError('Please enter your English name.');
|
||||
return;
|
||||
}
|
||||
|
||||
const emailRaw = (this.formData.email || '').trim();
|
||||
if (!emailRaw) {
|
||||
this.alertError('Please enter your email address.');
|
||||
return;
|
||||
}
|
||||
const email = emailRaw.toLowerCase();
|
||||
if (!/^[-._A-Za-z0-9]+@[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+$/.test(email)) {
|
||||
this.alertError('Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
if (email.endsWith('@qq.com')) {
|
||||
this.alertError('Registration failed: QQ email addresses are not accepted.');
|
||||
return;
|
||||
}
|
||||
if (!this.formData.password) {
|
||||
this.alertError('Please enter your password.');
|
||||
return;
|
||||
}
|
||||
if (!this.selectedFile) {
|
||||
this.alertError('Please upload your CV.');
|
||||
return;
|
||||
}
|
||||
if (!this.uploadedCvPath) {
|
||||
this.alertError('CV upload is not complete. Please upload again.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitting = true;
|
||||
this.$api
|
||||
.post('api/Ucenter/submitApplyYboardForExpert', {
|
||||
journal_id: this.journalId,
|
||||
expert_id: this.expertId,
|
||||
name: engName,
|
||||
email: emailRaw,
|
||||
cv: this.uploadedCvPath,
|
||||
password: this.formData.password
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.code == 0) {
|
||||
this.$router.replace({
|
||||
path: '/youthBoardSubmitSuccess',
|
||||
query: {
|
||||
country: this.formData.country == 'China' ? '1' : '0',
|
||||
journal_id: this.journalId,
|
||||
expert_id: this.expertId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.alertError((res && res.msg) || 'Submission failed.');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.alertError((err && err.msg) || 'Submission failed.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitting = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.registration-container {
|
||||
--primary: #3a91d9;
|
||||
--text: #2c3e50;
|
||||
--border: #e2e8f0;
|
||||
--danger: #e53e3e;
|
||||
--bg: #f7f9fc;
|
||||
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
padding: 40px;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.journal-hero {
|
||||
text-align: center;
|
||||
margin: 6px 0 18px;
|
||||
}
|
||||
|
||||
.journal-cover-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 112px;
|
||||
height: 150px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.journal-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.journal-title {
|
||||
margin-top: 10px;
|
||||
color: #1d4f8c;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: var(--text);
|
||||
font-size: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.link-meta {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.link-meta--warn {
|
||||
color: #c05621;
|
||||
}
|
||||
|
||||
.success-panel {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.success-panel h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required-star {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
border: 1px dashed #cbd5e0;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
}
|
||||
.upload-trigger:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.upload-trigger .icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
.upload-trigger .hint {
|
||||
font-size: 14px;
|
||||
color: #718096;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.file-display-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.remove-btn {
|
||||
color: var(--danger);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.qr-area {
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 16px;
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.qr-title {
|
||||
color: #2b6cb0;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.qr-box {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: #fff;
|
||||
margin: 0 auto 15px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.qr-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.qr-fallback {
|
||||
font-size: 13px;
|
||||
color: #718096;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.qr-hint {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.format-tag {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #feb2b2;
|
||||
border-radius: 8px;
|
||||
color: var(--danger);
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #4da1e6 0%, #3588d1 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -810,9 +810,37 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="articleAddQqEmailSubmitBlocked"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="max-width: 720px; margin: 24px auto 0"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="qq-email-alert-body">
|
||||
<div class="qq-email-alert-line1">{{ $t('articleAdd.qqEmailAlertLine1') }}</div>
|
||||
<div class="qq-email-alert-line2">
|
||||
<span>{{ $t('articleAdd.qqEmailAlertLine2Before') }}</span>
|
||||
<router-link to="/dashboard" class="qq-email-dashboard-link">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
{{ $t('articleAdd.qqEmailDashboardLink') }}
|
||||
</router-link>
|
||||
<span>{{ $t('articleAdd.qqEmailAlertLine2After') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<div style="text-align: center; margin: 40px 0 0 0">
|
||||
<el-button type="warning" @click="onStagingSave(4)" class="pro_stage">Save as draft </el-button>
|
||||
<el-button type="primary" @click="onSubmit(1)" class="pro_ceed">Confirm Submit </el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="onSubmit(1)"
|
||||
class="pro_ceed"
|
||||
:disabled="articleAddQqEmailSubmitBlocked"
|
||||
>
|
||||
Confirm Submit
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1612,6 +1640,17 @@ export default {
|
||||
},
|
||||
upload_qualifications: function () {
|
||||
return this.baseUrl + 'api/Admin/up_file';
|
||||
},
|
||||
/** 投稿页第四步:登录邮箱为 QQ 邮箱时不允许正式提交 */
|
||||
articleAddQqEmailSubmitBlocked() {
|
||||
try {
|
||||
const raw = localStorage.getItem('U_email') || '';
|
||||
const email = String(raw).trim().toLowerCase();
|
||||
if (!email || !email.includes('@')) return false;
|
||||
return /@qq\.com$/i.test(email) || /@vip\.qq\.com$/i.test(email);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -1907,6 +1946,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async onSubmit() {
|
||||
if (this.articleAddQqEmailSubmitBlocked) {
|
||||
this.$message.warning(this.$t('articleAdd.qqEmailSubmitBlockedMsg'));
|
||||
return false;
|
||||
}
|
||||
// 1. 校验协议勾选
|
||||
if (!this.agreechecked) {
|
||||
this.$message.error(
|
||||
@@ -4063,6 +4106,28 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qq-email-alert-body {
|
||||
line-height: 1.55;
|
||||
}
|
||||
.qq-email-alert-line1 {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.qq-email-alert-line2 {
|
||||
display: block;
|
||||
}
|
||||
.qq-email-dashboard-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 0 4px;
|
||||
font-weight: 600;
|
||||
color: #cf9236;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.qq-email-dashboard-link:hover {
|
||||
color: #b8822f;
|
||||
}
|
||||
/deep/.apc_content a{
|
||||
color: rgb(81, 127, 213) !important; cursor: pointer !important; text-decoration: underline !important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="article-detail-editor-page">
|
||||
<div class="crumbs">
|
||||
<div class="art_state_message_id" style="padding-left: 18px">
|
||||
<font
|
||||
@@ -915,9 +915,38 @@
|
||||
}}</b>
|
||||
<!-- <el-button type="text" @click="testedit" icon="el-icon-edit">Change</el-button> -->
|
||||
</div>
|
||||
<div>
|
||||
<span>Repetition : </span>
|
||||
<b>{{ form.repetition }}%</b>
|
||||
<div class="detail-plagiarism-row">
|
||||
<span class="detail-plagiarism-main">
|
||||
<span class="detail-plagiarism-lbl">Repetition :</span>
|
||||
<template v-if="detailPlagiarismUiShowProcessing()">
|
||||
<span class="detail-plagiarism-loading">
|
||||
<i class="el-icon-loading"></i>
|
||||
{{ $t('articleListEditor.plagiarismChecking') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="detailPlagiarismUiShowResult()">
|
||||
<span
|
||||
class="detail-plagiarism-pct-text"
|
||||
:style="detailPlagiarismSimilarityStyle()"
|
||||
@click.stop="detailOpenPlagiarismReport"
|
||||
>
|
||||
{{ detailPlagiarismSimilarityNumber() }}%
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="detail-plagiarism-not-checked-text" @click.stop="detailTriggerCrossrefPlagiarismCheck">
|
||||
{{ $t('articleListEditor.plagiarismNotChecked') }}
|
||||
</span>
|
||||
</template>
|
||||
<el-button
|
||||
v-if="detailPlagiarismShowRecheck()"
|
||||
type="text"
|
||||
class="detail-plagiarism-recheck-btn detail-plagiarism-recheck-inline"
|
||||
@click.stop="detailTriggerCrossrefPlagiarismCheck"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<!--<br clear="both">
|
||||
<el-button type="primary" @click="showResubmit" style="margin: 15px 0 0 0;">Resubmit the manuscript
|
||||
@@ -937,9 +966,38 @@
|
||||
}}</b>
|
||||
<el-button style="padding: 0" type="text" @click="testedit" icon="el-icon-edit">Change</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<span>Repetition : </span>
|
||||
<b>{{ form.repetition }}%</b>
|
||||
<div class="detail-plagiarism-row detail-plagiarism-row--with-actions">
|
||||
<span class="detail-plagiarism-main">
|
||||
<span class="detail-plagiarism-lbl">Repetition :</span>
|
||||
<template v-if="detailPlagiarismUiShowProcessing()">
|
||||
<span class="detail-plagiarism-loading">
|
||||
<i class="el-icon-loading"></i>
|
||||
{{ $t('articleListEditor.plagiarismChecking') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="detailPlagiarismUiShowResult()">
|
||||
<span
|
||||
class="detail-plagiarism-pct-text"
|
||||
:style="detailPlagiarismSimilarityStyle()"
|
||||
@click.stop="detailOpenPlagiarismReport"
|
||||
>
|
||||
{{ detailPlagiarismSimilarityNumber() }}%
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="detail-plagiarism-not-checked-text" @click.stop="detailTriggerCrossrefPlagiarismCheck">
|
||||
{{ $t('articleListEditor.plagiarismNotChecked') }}
|
||||
</span>
|
||||
</template>
|
||||
<el-button
|
||||
v-if="detailPlagiarismShowRecheck()"
|
||||
type="text"
|
||||
class="detail-plagiarism-recheck-btn detail-plagiarism-recheck-inline"
|
||||
@click.stop="detailTriggerCrossrefPlagiarismCheck"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||
</el-button>
|
||||
</span>
|
||||
<a :href="mediaUrl + form.repeurl" v-if="form.repeurl" class="zip_load" target="_blank">
|
||||
<img src="../../assets/img/icon_0.png" />
|
||||
<span>Duplicate check file</span>
|
||||
@@ -1426,6 +1484,8 @@
|
||||
import timetalk from './time_talk';
|
||||
import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue';
|
||||
import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue';
|
||||
import axios from 'axios';
|
||||
import { getSimilarityStyle } from '../../utils/ithenticateSimilarityStyle';
|
||||
export default {
|
||||
components: {
|
||||
timetalk,
|
||||
@@ -1549,6 +1609,9 @@ export default {
|
||||
approval_content: '',
|
||||
is_figure_copyright: '',
|
||||
repetition: '',
|
||||
plagiarism_similarity: '',
|
||||
plagiarism_report_url: '',
|
||||
plagiarism_job_state: '',
|
||||
manuscirpt: '',
|
||||
remarks: '',
|
||||
state: '',
|
||||
@@ -1743,7 +1806,9 @@ export default {
|
||||
underReview: ['1'],
|
||||
finalDecision: ['1'],
|
||||
is_figure_copyright: '',
|
||||
figurecopyright_file: ''
|
||||
figurecopyright_file: '',
|
||||
plagiarismDetailPollTimer: null,
|
||||
plagiarismDetailPending: false
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
@@ -2541,6 +2606,26 @@ export default {
|
||||
this.form.approval_file = res.article.approval_file;
|
||||
this.form.approval_content = res.article.approval_content;
|
||||
this.form.repetition = res.article.repetition;
|
||||
this.$set(
|
||||
this.form,
|
||||
'plagiarism_similarity',
|
||||
res.article.plagiarism_similarity != null && res.article.plagiarism_similarity !== ''
|
||||
? res.article.plagiarism_similarity
|
||||
: res.article.crossref_similarity != null && res.article.crossref_similarity !== ''
|
||||
? res.article.crossref_similarity
|
||||
: ''
|
||||
);
|
||||
this.$set(
|
||||
this.form,
|
||||
'plagiarism_report_url',
|
||||
res.article.plagiarism_report_url || res.article.crossref_report_url || ''
|
||||
);
|
||||
this.$set(
|
||||
this.form,
|
||||
'plagiarism_job_state',
|
||||
res.article.plagiarism_job_state || res.article.crossref_status || ''
|
||||
);
|
||||
this.detailInitPlagiarismAfterLoad();
|
||||
this.form.remarks = res.article.remarks;
|
||||
this.form.repeurl = res.article.repeurl;
|
||||
this.repeform.repefen = res.article.repetition;
|
||||
@@ -2856,11 +2941,188 @@ export default {
|
||||
// 关闭弹窗
|
||||
closeResubmit() {
|
||||
(this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields();
|
||||
},
|
||||
|
||||
/* ---------- Crossref plagiarism(详情页右侧) ---------- */
|
||||
detailPlagiarismArticleKey() {
|
||||
return String(this.form.articleId || this.editform.articleId || this.$route.query.id || '');
|
||||
},
|
||||
detailNormalizePlagiarismStatusPayload(body) {
|
||||
const root = body && typeof body === 'object' ? body : {};
|
||||
if (root.code != null && Number(root.code) !== 0) {
|
||||
return { status: 'api_error', similarity: null, reportUrl: '' };
|
||||
}
|
||||
let d = root.data != null ? root.data : root;
|
||||
if (typeof d === 'string') {
|
||||
try {
|
||||
d = JSON.parse(d);
|
||||
} catch (e) {
|
||||
d = {};
|
||||
}
|
||||
}
|
||||
if (!d || typeof d !== 'object') d = {};
|
||||
const statusRaw = d.status || d.state || d.job_status || d.plagiarism_status || '';
|
||||
const status = String(statusRaw).toLowerCase();
|
||||
let sim =
|
||||
d.similarity != null
|
||||
? d.similarity
|
||||
: d.percent != null
|
||||
? d.percent
|
||||
: d.similarity_percent != null
|
||||
? d.similarity_percent
|
||||
: d.crossref_similarity != null
|
||||
? d.crossref_similarity
|
||||
: null;
|
||||
if (sim === '' || sim === undefined) sim = null;
|
||||
const reportUrl = d.report_url || d.reportUrl || d.url || d.report_link || '';
|
||||
return { status, similarity: sim, reportUrl: String(reportUrl || '') };
|
||||
},
|
||||
detailPlagiarismSimilarityRaw() {
|
||||
const f = this.form;
|
||||
if (!f) return null;
|
||||
if (f.plagiarism_similarity != null && f.plagiarism_similarity !== '') return f.plagiarism_similarity;
|
||||
if (f.crossref_similarity != null && f.crossref_similarity !== '') return f.crossref_similarity;
|
||||
const st = String(f.plagiarism_job_state || f.crossref_status || '').toLowerCase();
|
||||
if (['completed', 'done', 'success', 'complete'].includes(st) && (f.plagiarism_similarity === 0 || f.plagiarism_similarity === '0')) {
|
||||
return 0;
|
||||
}
|
||||
const rep = f.repetition;
|
||||
if (rep != null && rep !== '' && Number(rep) > 0) return rep;
|
||||
return null;
|
||||
},
|
||||
detailPlagiarismSimilarityNumber() {
|
||||
const n = this.detailPlagiarismSimilarityRaw();
|
||||
if (n == null || n === '') return 0;
|
||||
const x = Number(n);
|
||||
return isNaN(x) ? 0 : Math.round(x * 10) / 10;
|
||||
},
|
||||
detailPlagiarismUiShowProcessing() {
|
||||
if (this.form._plagiarismLocalLoading) return true;
|
||||
if (this.plagiarismDetailPending) return true;
|
||||
const st = String(this.form.plagiarism_job_state || this.form.crossref_status || '').toLowerCase();
|
||||
return ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st);
|
||||
},
|
||||
detailPlagiarismUiShowResult() {
|
||||
if (this.detailPlagiarismUiShowProcessing()) return false;
|
||||
const st = String(this.form.plagiarism_job_state || this.form.crossref_status || '').toLowerCase();
|
||||
const done = ['completed', 'done', 'success', 'complete'].includes(st);
|
||||
const raw = this.detailPlagiarismSimilarityRaw();
|
||||
if (raw != null && raw !== '' && !isNaN(Number(raw))) {
|
||||
if (Number(raw) === 0 && !done && !(this.form.plagiarism_report_url || this.form.crossref_report_url)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (done && (this.form.plagiarism_similarity === 0 || this.form.plagiarism_similarity === '0')) return true;
|
||||
return !!(this.form.plagiarism_report_url || this.form.crossref_report_url);
|
||||
},
|
||||
detailPlagiarismSimilarityStyle() {
|
||||
const n = Number(this.detailPlagiarismSimilarityNumber());
|
||||
return { color: getSimilarityStyle(n).color };
|
||||
},
|
||||
detailPlagiarismShowRecheck() {
|
||||
if (this.detailPlagiarismUiShowProcessing()) return false;
|
||||
return Number(this.form.state) === 6;
|
||||
},
|
||||
detailInitPlagiarismAfterLoad() {
|
||||
const st = String(this.form.plagiarism_job_state || '').toLowerCase();
|
||||
if (['pending', 'processing', 'queued', 'running', 'submitted'].includes(st)) {
|
||||
this.plagiarismDetailPending = true;
|
||||
this.detailEnsurePlagiarismPoll();
|
||||
}
|
||||
},
|
||||
detailEnsurePlagiarismPoll() {
|
||||
if (!this.plagiarismDetailPending) {
|
||||
this.detailStopPlagiarismPolling();
|
||||
return;
|
||||
}
|
||||
if (!this.plagiarismDetailPollTimer) {
|
||||
this.detailPollPlagiarismOnce();
|
||||
this.plagiarismDetailPollTimer = setInterval(() => this.detailPollPlagiarismOnce(), 60000);
|
||||
}
|
||||
},
|
||||
detailStopPlagiarismPolling() {
|
||||
if (this.plagiarismDetailPollTimer) {
|
||||
clearInterval(this.plagiarismDetailPollTimer);
|
||||
this.plagiarismDetailPollTimer = null;
|
||||
}
|
||||
},
|
||||
async detailPollPlagiarismOnce() {
|
||||
const key = this.detailPlagiarismArticleKey();
|
||||
if (!key || !this.plagiarismDetailPending) return;
|
||||
try {
|
||||
const res = await axios.get('/api/plagiarism/status', { params: { article_id: key } });
|
||||
const body = res && res.data;
|
||||
const norm = this.detailNormalizePlagiarismStatusPayload(body);
|
||||
if (norm.status === 'api_error') {
|
||||
this.plagiarismDetailPending = false;
|
||||
this.detailStopPlagiarismPolling();
|
||||
return;
|
||||
}
|
||||
if (norm.similarity != null && norm.similarity !== '') {
|
||||
this.$set(this.form, 'plagiarism_similarity', norm.similarity);
|
||||
}
|
||||
if (norm.reportUrl) this.$set(this.form, 'plagiarism_report_url', norm.reportUrl);
|
||||
if (norm.status) this.$set(this.form, 'plagiarism_job_state', norm.status);
|
||||
const active = ['pending', 'processing', 'queued', 'running', 'submitted'];
|
||||
const terminal = ['completed', 'done', 'success', 'complete', 'failed', 'error', 'fail', 'cancelled'];
|
||||
const isActive = active.includes(norm.status);
|
||||
let clearPending =
|
||||
terminal.includes(norm.status) ||
|
||||
norm.status === 'error' ||
|
||||
(!isActive && norm.reportUrl && norm.similarity != null && norm.similarity !== '');
|
||||
if (clearPending) {
|
||||
this.plagiarismDetailPending = false;
|
||||
}
|
||||
this.detailEnsurePlagiarismPoll();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$message.error(this.$t('articleListEditor.plagiarismStatusFailed'));
|
||||
}
|
||||
},
|
||||
async detailTriggerCrossrefPlagiarismCheck() {
|
||||
const key = this.detailPlagiarismArticleKey();
|
||||
if (!key) return;
|
||||
if (this.detailPlagiarismUiShowProcessing()) return;
|
||||
this.$set(this.form, '_plagiarismLocalLoading', true);
|
||||
try {
|
||||
const res = await this.$api.post('api/plagiarism/check', { article_id: key });
|
||||
if (res && Number(res.code) === 0) {
|
||||
this.plagiarismDetailPending = true;
|
||||
this.$set(this.form, 'plagiarism_job_state', 'pending');
|
||||
this.detailEnsurePlagiarismPoll();
|
||||
await this.detailPollPlagiarismOnce();
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$message.error(this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||
} finally {
|
||||
this.$set(this.form, '_plagiarismLocalLoading', false);
|
||||
}
|
||||
},
|
||||
detailOpenPlagiarismReport() {
|
||||
const raw = (this.form.plagiarism_report_url || this.form.crossref_report_url || '').trim();
|
||||
if (!raw) {
|
||||
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||
return;
|
||||
}
|
||||
let full = raw;
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
const base = (this.mediaUrl || '').replace(/\/+$/, '');
|
||||
const path = raw.replace(/^\/+/, '');
|
||||
full = base ? `${base}/${path}` : `/${path}`;
|
||||
}
|
||||
window.open(full, '_blank');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.opname = this.$route.query.mark;
|
||||
this.resubmitJournal.manuscriptId = this.$route.query.id;
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.detailStopPlagiarismPolling();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -3345,4 +3607,80 @@ td {
|
||||
.copyright-declaration-wrapper :deep(.el-radio.is-checked .el-radio__label) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 详情页:去掉全局 main.css 中 .art_caozuo_ 的浅蓝底;查重行无额外底色 */
|
||||
.article-detail-editor-page .art_caozuo_ {
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #ebeef5 !important;
|
||||
}
|
||||
.detail-plagiarism-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 8px;
|
||||
background: transparent !important;
|
||||
padding: 4px 0 8px 0;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
/* 合并「Repetition :」与数值,避免命中 .art_caozuo_ > div > span 的 min-width:65px 造成巨大空隙 */
|
||||
.article-detail-editor-page .art_caozuo_ > div > span.detail-plagiarism-main {
|
||||
min-width: 0 !important;
|
||||
width: auto !important;
|
||||
max-width: 100%;
|
||||
margin-right: 8px !important;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.article-detail-editor-page .art_caozuo_ .detail-plagiarism-lbl {
|
||||
color: #777;
|
||||
margin: 0 2px 0 0 !important;
|
||||
min-width: 0 !important;
|
||||
width: auto !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.detail-plagiarism-loading {
|
||||
color: #409eff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.detail-plagiarism-loading .el-icon-loading {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.detail-plagiarism-pct-text {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.detail-plagiarism-pct-text:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
.detail-plagiarism-not-checked-text {
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.detail-plagiarism-not-checked-text:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
.detail-plagiarism-recheck-btn {
|
||||
margin-left: 4px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
/* 紧跟在百分比 / 未检测 文案后,避免继承整行大间距 */
|
||||
.detail-plagiarism-main .detail-plagiarism-recheck-inline.el-button--text {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 2px !important;
|
||||
vertical-align: baseline;
|
||||
line-height: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -107,11 +107,11 @@
|
||||
</span>
|
||||
|
||||
|
||||
<span v-if="item.state == 6" style="text-decoration: none;margin-left: 20px;">
|
||||
<span v-if="item.state == 6 && preAcceptButtonReady(item)" style="text-decoration: none;margin-left: 20px;">
|
||||
|
||||
<span @click="goPre_ingested(item.article_id)" class="preButton">
|
||||
<!-- <el-badge is-dot class="item" > -->
|
||||
<i class="el-icon-bank-card"></i>Start the pre-acceptance process and complete your payment
|
||||
<i class="el-icon-bank-card"></i>{{ preAcceptEntryButtonText(item) }}
|
||||
<!-- </el-badge> -->
|
||||
</span>
|
||||
</span>
|
||||
@@ -539,6 +539,61 @@
|
||||
this.getdate();
|
||||
},
|
||||
methods: {
|
||||
/** 与 Complete_profile.vue getDetail 中订单/支付展示逻辑一致:preOrderDetail → is_buy、期刊费、折后费、isFree */
|
||||
_feeNum(v) {
|
||||
if (v == null || v === '') return 0;
|
||||
const n = Number(String(v).replace(/,/g, ''));
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
},
|
||||
computePreacceptShortButton(articleInfo, journalInfo) {
|
||||
if (!articleInfo || !journalInfo) return false;
|
||||
const journalFee = journalInfo.fee;
|
||||
const articleFee = articleInfo.fee;
|
||||
const isBuy = Number(articleInfo.is_buy) === 1;
|
||||
const tableFee = journalFee ? this._feeNum(articleFee) : 0;
|
||||
const isFree = isBuy && tableFee === 0;
|
||||
const noJournalFee = !journalFee || this._feeNum(journalFee) === 0 || String(journalFee) === '0.00';
|
||||
// Complete_profile: active=1 当已付,或 未付但期刊无 APC
|
||||
if (isFree) return true;
|
||||
if (isBuy) return true;
|
||||
if (!isBuy && noJournalFee) return true;
|
||||
return false;
|
||||
},
|
||||
preAcceptButtonReady(item) {
|
||||
const p = item._preacceptPay;
|
||||
return !!(p && p.loading === false);
|
||||
},
|
||||
async hydratePreacceptPaymentForList(rows) {
|
||||
if (!rows || !rows.length) return;
|
||||
const targets = rows.filter((r) => Number(r.state) === 6);
|
||||
await Promise.all(
|
||||
targets.map(async (row) => {
|
||||
try {
|
||||
const res = await this.$api.post('api/Order/preOrderDetail', {
|
||||
article_id: row.article_id
|
||||
});
|
||||
if (res && Number(res.code) === 0) {
|
||||
const article = res.data.article_detail || {};
|
||||
const journal = res.data.journal_detail || {};
|
||||
const shortButton = this.computePreacceptShortButton(article, journal);
|
||||
this.$set(row, '_preacceptPay', { loading: false, shortButton });
|
||||
} else {
|
||||
this.$set(row, '_preacceptPay', { loading: false, shortButton: false });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$set(row, '_preacceptPay', { loading: false, shortButton: false });
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
preAcceptEntryButtonText(item) {
|
||||
const pay = item._preacceptPay;
|
||||
if (!pay || pay.loading) return '';
|
||||
return pay.shortButton
|
||||
? this.$t('PreAccept.startPreAccept')
|
||||
: this.$t('PreAccept.startPreAcceptWithPayment');
|
||||
},
|
||||
formatToHtml(val) {
|
||||
|
||||
if (!val) return '';
|
||||
@@ -612,8 +667,12 @@ return processedText;
|
||||
1 + '-';
|
||||
let D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||
res.data[i].ctime = Y + M + D;
|
||||
if (Number(res.data[i].state) === 6) {
|
||||
this.$set(res.data[i], '_preacceptPay', { loading: true, shortButton: false });
|
||||
}
|
||||
}
|
||||
this.tableData = res.data
|
||||
this.tableData = res.data;
|
||||
this.hydratePreacceptPaymentForList(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
|
||||
@@ -116,9 +116,36 @@
|
||||
<i class="el-icon-data-line"></i> Manuscript Tracking
|
||||
</b>
|
||||
|
||||
<span style="float: right">
|
||||
<span style="float: right" class="plagiarism-header-bar">
|
||||
<span class="labelTitle" style="font-weight: 500; font-size: 13px">Plagiarism Check :</span>
|
||||
<font style="margin-right: 16px; font-size: 13px; font-weight: 700"> {{ item.repetition }} % </font>
|
||||
<template v-if="plagiarismUiShowProcessing(item)">
|
||||
<span class="plagiarism-inline-loading plagiarism-on-blue">
|
||||
<i class="el-icon-loading"></i>
|
||||
<span>{{ $t('articleListEditor.plagiarismChecking') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="plagiarismUiShowResult(item)">
|
||||
<span
|
||||
class="plagiarism-pct-text plagiarism-on-blue"
|
||||
@click.stop="openPlagiarismReport(item)"
|
||||
>
|
||||
{{ plagiarismSimilarityNumber(item) }}%
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="plagiarism-not-checked-text plagiarism-on-blue" @click.stop="triggerCrossrefPlagiarismCheck(item)">
|
||||
{{ $t('articleListEditor.plagiarismNotChecked') }}
|
||||
</span>
|
||||
</template>
|
||||
<el-button
|
||||
v-if="plagiarismShowRecheck(item)"
|
||||
type="text"
|
||||
size="mini"
|
||||
class="plagiarism-recheck-btn"
|
||||
@click.native.stop="triggerCrossrefPlagiarismCheck(item)"
|
||||
>
|
||||
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||
</el-button>
|
||||
</span>
|
||||
<span style="margin: 0 10px; float: right">| </span>
|
||||
|
||||
@@ -1065,6 +1092,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { Loading } from 'element-ui';
|
||||
import timetalk from './time_talk';
|
||||
import commonRemarkList from './articleListEditor_A_list.vue';
|
||||
@@ -1432,12 +1460,18 @@ export default {
|
||||
editVisible1: false,
|
||||
bankVisible: false,
|
||||
majorData: {},
|
||||
googleSearchInfo: ''
|
||||
googleSearchInfo: '',
|
||||
/** Crossref / plagiarism async polling */
|
||||
plagiarismPollTimer: null,
|
||||
plagiarismPendingIds: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getdate();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopPlagiarismPolling();
|
||||
},
|
||||
computed: {
|
||||
upload_zip: function () {
|
||||
return this.baseUrl + 'api/Article/up_file/type/repezip';
|
||||
@@ -2072,6 +2106,7 @@ export default {
|
||||
this.tableData[i].reportList = this.tableData[i].reportList.slice(0, 3);
|
||||
this.$forceUpdate();
|
||||
}
|
||||
this.initPlagiarismFromList();
|
||||
|
||||
loading.close();
|
||||
})
|
||||
@@ -2658,6 +2693,234 @@ export default {
|
||||
return str;
|
||||
},
|
||||
|
||||
/* ---------- Crossref plagiarism (list header) ---------- */
|
||||
_plagiarismArticleKey(item) {
|
||||
return item && item.article_id != null ? String(item.article_id) : '';
|
||||
},
|
||||
_plagiarismPending(articleKey) {
|
||||
return !!(articleKey && this.plagiarismPendingIds[articleKey]);
|
||||
},
|
||||
plagiarismSimilarityNumber(item) {
|
||||
const n = this.plagiarismSimilarityRaw(item);
|
||||
if (n == null || n === '') return 0;
|
||||
const x = Number(n);
|
||||
return isNaN(x) ? 0 : Math.round(x * 10) / 10;
|
||||
},
|
||||
plagiarismSimilarityRaw(item) {
|
||||
if (!item) return null;
|
||||
if (item.plagiarism_similarity != null && item.plagiarism_similarity !== '') return item.plagiarism_similarity;
|
||||
if (item.crossref_similarity != null && item.crossref_similarity !== '') return item.crossref_similarity;
|
||||
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
|
||||
if (['completed', 'done', 'success', 'complete'].includes(st) && (item.plagiarism_similarity === 0 || item.plagiarism_similarity === '0')) {
|
||||
return 0;
|
||||
}
|
||||
const rep = item.repetition;
|
||||
if (rep != null && rep !== '' && Number(rep) > 0) return rep;
|
||||
return null;
|
||||
},
|
||||
plagiarismUiShowProcessing(item) {
|
||||
const key = this._plagiarismArticleKey(item);
|
||||
if (!key) return false;
|
||||
if (item._plagiarismLocalLoading) return true;
|
||||
if (this._plagiarismPending(key)) return true;
|
||||
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
|
||||
return ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st);
|
||||
},
|
||||
plagiarismUiShowResult(item) {
|
||||
if (this.plagiarismUiShowProcessing(item)) return false;
|
||||
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
|
||||
const done = ['completed', 'done', 'success', 'complete'].includes(st);
|
||||
const raw = this.plagiarismSimilarityRaw(item);
|
||||
if (raw != null && raw !== '' && !isNaN(Number(raw))) {
|
||||
if (Number(raw) === 0 && !done && !(item.plagiarism_report_url || item.crossref_report_url)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (done && (item.plagiarism_similarity === 0 || item.plagiarism_similarity === '0')) return true;
|
||||
return !!(item.plagiarism_report_url || item.crossref_report_url);
|
||||
},
|
||||
plagiarismShowRecheck(item) {
|
||||
if (this.plagiarismUiShowProcessing(item)) return false;
|
||||
return Number(item.state) === 6;
|
||||
},
|
||||
initPlagiarismFromList() {
|
||||
if (!this.tableData || !this.tableData.length) {
|
||||
this.reconcilePlagiarismPollTimer();
|
||||
return;
|
||||
}
|
||||
this.tableData.forEach((row) => {
|
||||
if (row.crossref_report_url && !row.plagiarism_report_url) {
|
||||
this.$set(row, 'plagiarism_report_url', row.crossref_report_url);
|
||||
}
|
||||
if (row.crossref_similarity != null && row.plagiarism_similarity == null) {
|
||||
this.$set(row, 'plagiarism_similarity', row.crossref_similarity);
|
||||
}
|
||||
if (row.crossref_status && !row.plagiarism_job_state) {
|
||||
this.$set(row, 'plagiarism_job_state', row.crossref_status);
|
||||
}
|
||||
const key = this._plagiarismArticleKey(row);
|
||||
const st = String(row.plagiarism_job_state || '').toLowerCase();
|
||||
if (key && ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st)) {
|
||||
this.$set(this.plagiarismPendingIds, key, true);
|
||||
}
|
||||
});
|
||||
this.reconcilePlagiarismPollTimer();
|
||||
},
|
||||
reconcilePlagiarismPollTimer() {
|
||||
const hasPending = Object.keys(this.plagiarismPendingIds).some((k) => this.plagiarismPendingIds[k]);
|
||||
if (hasPending) {
|
||||
if (!this.plagiarismPollTimer) {
|
||||
this.pollPlagiarismPendingOnce();
|
||||
this.plagiarismPollTimer = setInterval(() => this.pollPlagiarismPendingOnce(), 60000);
|
||||
}
|
||||
} else if (this.plagiarismPollTimer) {
|
||||
clearInterval(this.plagiarismPollTimer);
|
||||
this.plagiarismPollTimer = null;
|
||||
}
|
||||
},
|
||||
stopPlagiarismPolling() {
|
||||
if (this.plagiarismPollTimer) {
|
||||
clearInterval(this.plagiarismPollTimer);
|
||||
this.plagiarismPollTimer = null;
|
||||
}
|
||||
this.plagiarismPendingIds = {};
|
||||
},
|
||||
pollPlagiarismPendingOnce() {
|
||||
Object.keys(this.plagiarismPendingIds).forEach((key) => {
|
||||
if (this.plagiarismPendingIds[key]) this.fetchPlagiarismStatusByArticleId(key);
|
||||
});
|
||||
},
|
||||
_normalizePlagiarismStatusPayload(body) {
|
||||
const root = body && typeof body === 'object' ? body : {};
|
||||
if (root.code != null && Number(root.code) !== 0) {
|
||||
return { status: 'api_error', similarity: null, reportUrl: '' };
|
||||
}
|
||||
let d = root.data != null ? root.data : root;
|
||||
if (typeof d === 'string') {
|
||||
try {
|
||||
d = JSON.parse(d);
|
||||
} catch (e) {
|
||||
d = {};
|
||||
}
|
||||
}
|
||||
if (!d || typeof d !== 'object') d = {};
|
||||
const statusRaw = d.status || d.state || d.job_status || d.plagiarism_status || '';
|
||||
const status = String(statusRaw).toLowerCase();
|
||||
let sim =
|
||||
d.similarity != null
|
||||
? d.similarity
|
||||
: d.percent != null
|
||||
? d.percent
|
||||
: d.similarity_percent != null
|
||||
? d.similarity_percent
|
||||
: d.crossref_similarity != null
|
||||
? d.crossref_similarity
|
||||
: null;
|
||||
if (sim === '' || sim === undefined) sim = null;
|
||||
const reportUrl = d.report_url || d.reportUrl || d.url || d.report_link || '';
|
||||
return { status, similarity: sim, reportUrl: String(reportUrl || '') };
|
||||
},
|
||||
findTableRowByArticleId(articleId) {
|
||||
const id = String(articleId);
|
||||
return (this.tableData || []).find((r) => String(r.article_id) === id) || null;
|
||||
},
|
||||
async fetchPlagiarismStatusByArticleId(articleId) {
|
||||
const key = String(articleId);
|
||||
try {
|
||||
const res = await axios.get('/api/plagiarism/status', { params: { article_id: key } });
|
||||
const body = res && res.data;
|
||||
const norm = this._normalizePlagiarismStatusPayload(body);
|
||||
if (norm.status === 'api_error') {
|
||||
this.$set(this.plagiarismPendingIds, key, false);
|
||||
this.reconcilePlagiarismPollTimer();
|
||||
return;
|
||||
}
|
||||
const row = this.findTableRowByArticleId(key);
|
||||
if (row) {
|
||||
if (norm.similarity != null && norm.similarity !== '') {
|
||||
this.$set(row, 'plagiarism_similarity', norm.similarity);
|
||||
}
|
||||
if (norm.reportUrl) this.$set(row, 'plagiarism_report_url', norm.reportUrl);
|
||||
if (norm.status) this.$set(row, 'plagiarism_job_state', norm.status);
|
||||
}
|
||||
const active = ['pending', 'processing', 'queued', 'running', 'submitted'];
|
||||
const terminal = ['completed', 'done', 'success', 'complete', 'failed', 'error', 'fail', 'cancelled'];
|
||||
const isActive = active.includes(norm.status);
|
||||
let clearPending =
|
||||
terminal.includes(norm.status) ||
|
||||
norm.status === 'error' ||
|
||||
(!isActive && norm.reportUrl && norm.similarity != null && norm.similarity !== '');
|
||||
if (clearPending) {
|
||||
this.$set(this.plagiarismPendingIds, key, false);
|
||||
}
|
||||
this.reconcilePlagiarismPollTimer();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$message.error(this.$t('articleListEditor.plagiarismStatusFailed'));
|
||||
}
|
||||
},
|
||||
async triggerCrossrefPlagiarismCheck(item) {
|
||||
const key = this._plagiarismArticleKey(item);
|
||||
if (!key) return;
|
||||
if (this.plagiarismUiShowProcessing(item)) return;
|
||||
this.$set(item, '_plagiarismLocalLoading', true);
|
||||
try {
|
||||
const res = await this.$api.post('api/plagiarism/check', { article_id: key });
|
||||
if (res && Number(res.code) === 0) {
|
||||
this.$set(this.plagiarismPendingIds, key, true);
|
||||
this.$set(item, 'plagiarism_job_state', 'pending');
|
||||
this.reconcilePlagiarismPollTimer();
|
||||
await this.fetchPlagiarismStatusByArticleId(key);
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$message.error(this.$t('articleListEditor.plagiarismCheckFailed'));
|
||||
} finally {
|
||||
this.$set(item, '_plagiarismLocalLoading', false);
|
||||
}
|
||||
},
|
||||
async openPlagiarismReport(item) {
|
||||
const articleId = item && item.article_id;
|
||||
if (!articleId) {
|
||||
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||
return;
|
||||
}
|
||||
let raw = '';
|
||||
try {
|
||||
const res = await this.$api.post('api/Article/getArticleDetail', {
|
||||
articleId,
|
||||
human: 'editor'
|
||||
});
|
||||
console.log("🚀 ~ openPlagiarismReport ~ res:", res);
|
||||
|
||||
const a = res && res.article;
|
||||
if (a) {
|
||||
raw = String(
|
||||
a.repeurl || ''
|
||||
).trim();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$message.error(this.$t('articleListEditor.plagiarismReportDetailFailed'));
|
||||
return;
|
||||
}
|
||||
const url = String(raw).trim();
|
||||
if (!url) {
|
||||
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||
return;
|
||||
}
|
||||
let full = url;
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
const base = (this.mediaUrl || '').replace(/\/+$/, '');
|
||||
const path = url.replace(/^\/+/, '');
|
||||
full = base ? `${base}/${path}` : `/${path}`;
|
||||
}
|
||||
window.open(full, '_blank');
|
||||
},
|
||||
|
||||
//文章类型
|
||||
|
||||
|
||||
@@ -3426,4 +3689,65 @@ td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
/* box-shadow: 0 2px 8px rgba(0,0,0,0.1); */
|
||||
}
|
||||
|
||||
/* Crossref plagiarism strip (blue header bar) */
|
||||
.plagiarism-header-bar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 8px;
|
||||
margin-right: 4px;
|
||||
max-width: 48%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.plagiarism-on-blue {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-inline-loading {
|
||||
color: #e6f4ff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-inline-loading .el-icon-loading {
|
||||
font-size: 14px;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-pct-text {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-pct-text:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-not-checked-text {
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #e6f4ff;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-not-checked-text:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-recheck-btn {
|
||||
color: #e6f4ff !important;
|
||||
padding: 0 4px !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.articleTopBaseInfo .plagiarism-recheck-btn:hover {
|
||||
color: #fff !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -198,8 +198,8 @@
|
||||
<el-form-item label="Account :" prop="account">
|
||||
<span>{{coreForm.account}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="Email :">
|
||||
<span>{{coreForm.email}}</span>
|
||||
<el-form-item label="Email :" prop="email">
|
||||
<el-input type="text" placeholder="Please enter email..." v-model="coreForm.email" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="head portrait :">
|
||||
<el-upload class="avatar-uploader" :action="baseUrl+'master/Journal/up_topic_file'"
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
:selectedTemplateName="selectedTemplateName"
|
||||
:selectedStyleName="selectedStyleName"
|
||||
:availableFields="availableFields"
|
||||
:availableCountries="availableCountries"
|
||||
:fieldsLoading="fieldsLoading"
|
||||
:fieldsSaving="fieldsSaving"
|
||||
:selectedFieldIds.sync="selectedFieldIdsProxy"
|
||||
:selectedCountryIds.sync="selectedCountryIdsProxy"
|
||||
@open-template-selector="emitOpenTemplateSelector"
|
||||
@confirm-fields="emitConfirmFields"
|
||||
@confirm-countries="emitConfirmCountries"
|
||||
@update:wizardStartDate="onWizardStartDateUpdate"
|
||||
/>
|
||||
|
||||
@@ -49,11 +52,14 @@
|
||||
:selectedTemplateName="selectedTemplateName"
|
||||
:selectedStyleName="selectedStyleName"
|
||||
:availableFields="availableFields"
|
||||
:availableCountries="availableCountries"
|
||||
:fieldsLoading="fieldsLoading"
|
||||
:fieldsSaving="fieldsSaving"
|
||||
:selectedFieldIds.sync="selectedFieldIdsProxy"
|
||||
:selectedCountryIds.sync="selectedCountryIdsProxy"
|
||||
@open-template-selector="emitOpenTemplateSelector"
|
||||
@confirm-fields="emitConfirmFields"
|
||||
@confirm-countries="emitConfirmCountries"
|
||||
@update:wizardStartDate="onWizardStartDateUpdate"
|
||||
/>
|
||||
<div class="dialog-footer">
|
||||
@@ -89,9 +95,11 @@ export default {
|
||||
selectedStyleName: { type: String, default: '' },
|
||||
saving: { type: Boolean, default: false },
|
||||
availableFields: { type: Array, default: () => [] },
|
||||
availableCountries: { type: Array, default: () => [] },
|
||||
fieldsLoading: { type: Boolean, default: false },
|
||||
fieldsSaving: { type: Boolean, default: false },
|
||||
selectedFieldIds: { type: Array, default: () => [] }
|
||||
selectedFieldIds: { type: Array, default: () => [] },
|
||||
selectedCountryIds: { type: Array, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
@@ -118,6 +126,14 @@ export default {
|
||||
this.$emit('update:selectedFieldIds', val);
|
||||
}
|
||||
},
|
||||
selectedCountryIdsProxy: {
|
||||
get() {
|
||||
return this.selectedCountryIds;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:selectedCountryIds', val);
|
||||
}
|
||||
},
|
||||
canConfirm() {
|
||||
const id = this.config && this.config.defaultTemplateId != null ? String(this.config.defaultTemplateId) : '';
|
||||
return id !== '' && id !== '0';
|
||||
@@ -130,6 +146,9 @@ export default {
|
||||
emitConfirmFields() {
|
||||
this.$emit('confirm-fields');
|
||||
},
|
||||
emitConfirmCountries() {
|
||||
this.$emit('confirm-countries');
|
||||
},
|
||||
onWizardStartDateUpdate(val) {
|
||||
// 由内容组件回传日期,继续走父组件的 .sync 链路
|
||||
this.wizardStartDateProxy = val;
|
||||
|
||||
@@ -103,7 +103,44 @@
|
||||
|
||||
<section class="form-section">
|
||||
<h4 class="section-title">
|
||||
<i class="el-icon-finished"></i> 3. {{ $t('autoPromotion.confirmAndEnable') }}
|
||||
<i class="el-icon-location-outline"></i> 3. {{ $t('autoPromotion.selectPromotionCountry') }}
|
||||
<!-- <span class="selected-count">
|
||||
{{ $t('autoPromotion.selectedCount', { count: selectedCountryIdsProxy.length }) }}
|
||||
</span> -->
|
||||
<!-- <el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-edit-outline"
|
||||
class="section-action-btn"
|
||||
@click="countryDialogVisible = true"
|
||||
>
|
||||
{{ $t('autoPromotion.choosePromotionCountry') }}
|
||||
</el-button> -->
|
||||
</h4>
|
||||
<div class="status-confirm-box">
|
||||
<div class="country-quick-checks">
|
||||
<div class="field-tip" style="margin-bottom: 10px;">{{ $t('autoPromotion.selectPromotionCountryTip') }}</div>
|
||||
<el-checkbox-group v-model="selectedCountryIdsProxy" size="small">
|
||||
<el-checkbox label="Partition1">{{ $t('autoPromotion.countryQuickZone1') }}</el-checkbox>
|
||||
<el-checkbox label="Partition2">{{ $t('autoPromotion.countryQuickZone2') }}</el-checkbox>
|
||||
<el-checkbox label="Partition3">{{ $t('autoPromotion.countryQuickZone3') }}</el-checkbox>
|
||||
<el-checkbox label="country_china" value="239">{{ $t('autoPromotion.countryQuickChina') }}</el-checkbox>
|
||||
<el-checkbox label="country_india" value="228">{{ $t('autoPromotion.countryQuickIndia') }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<!-- <div v-if="selectedCountryTagRows.length" class="selected-tags">
|
||||
<el-tag v-for="row in selectedCountryTagRows" :key="'c-' + row.id" size="mini" type="info" effect="plain">{{ row.text }}</el-tag>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<section class="form-section">
|
||||
<h4 class="section-title">
|
||||
<i class="el-icon-finished"></i> 4. {{ $t('autoPromotion.confirmAndEnable') }}
|
||||
</h4>
|
||||
|
||||
<div class="status-confirm-box">
|
||||
@@ -150,6 +187,41 @@
|
||||
<el-button size="small" type="primary" :loading="fieldsSaving" @click="emitConfirmFields">{{ $t('autoPromotion.confirm') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('autoPromotion.selectPromotionCountry')"
|
||||
:visible.sync="countryDialogVisible"
|
||||
width="1200px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="field-dialog-toolbar">
|
||||
<el-input
|
||||
v-model="countrySearchText"
|
||||
size="small"
|
||||
clearable
|
||||
class="field-search-input"
|
||||
prefix-icon="el-icon-search"
|
||||
:placeholder="$t('autoPromotion.countrySearchPlaceholder')"
|
||||
/>
|
||||
<el-button size="mini" @click="selectAllCountries">{{ $t('autoPromotion.selectAll') }}</el-button>
|
||||
<el-button size="mini" @click="clearAllCountries">{{ $t('autoPromotion.clearAll') }}</el-button>
|
||||
</div>
|
||||
<div class="field-dialog-body" v-loading="fieldsLoading">
|
||||
<el-checkbox-group v-model="selectedCountryIdsProxy" class="field-check-group">
|
||||
<el-checkbox v-for="c in sortedFilteredCountries" :key="'country-' + String(c.id)" :label="String(c.id)">
|
||||
{{ c.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<div v-if="!fieldsLoading && sortedFilteredCountries.length === 0" class="field-empty-tip">
|
||||
{{ $t('autoPromotion.noCountryMatch') }}
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer">
|
||||
<el-button size="small" @click="countryDialogVisible = false">{{ $t('autoPromotion.cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="fieldsSaving" @click="emitConfirmCountries">{{ $t('autoPromotion.confirm') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -159,7 +231,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
fieldSearchText: '',
|
||||
fieldDialogVisible: false
|
||||
fieldDialogVisible: false,
|
||||
countrySearchText: '',
|
||||
countryDialogVisible: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@@ -170,9 +244,11 @@ export default {
|
||||
selectedTemplateName: { type: String, default: '' },
|
||||
selectedStyleName: { type: String, default: '' },
|
||||
availableFields: { type: Array, default: () => [] },
|
||||
availableCountries: { type: Array, default: () => [] },
|
||||
fieldsLoading: { type: Boolean, default: false },
|
||||
fieldsSaving: { type: Boolean, default: false },
|
||||
selectedFieldIds: { type: Array, default: () => [] }
|
||||
selectedFieldIds: { type: Array, default: () => [] },
|
||||
selectedCountryIds: { type: Array, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
hasSelectedTemplate() {
|
||||
@@ -190,11 +266,52 @@ export default {
|
||||
this.$emit('update:selectedFieldIds', val);
|
||||
}
|
||||
},
|
||||
selectedCountryIdsProxy: {
|
||||
get() {
|
||||
return this.selectedCountryIds;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:selectedCountryIds', val);
|
||||
}
|
||||
},
|
||||
sortedFilteredFields() {
|
||||
const kw = (this.fieldSearchText || '').trim().toLowerCase();
|
||||
const list = (this.availableFields || []).filter((item) => {
|
||||
if (!kw) return true;
|
||||
return String(item.label || '').toLowerCase().includes(kw);
|
||||
const kwRaw = String(this.fieldSearchText || '');
|
||||
const normalize = (s) =>
|
||||
String(s || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.toLowerCase();
|
||||
const tokens = kwRaw
|
||||
? kwRaw
|
||||
.split(/[\r\n,,;;]+/g)
|
||||
.map((s) => normalize(s))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const list = (this.availableFields || []).filter((item) => {
|
||||
if (!tokens.length) return true;
|
||||
const label = normalize(item.label || '');
|
||||
// 严格匹配:必须与字段名完全一致(忽略大小写与空白差异)
|
||||
return tokens.some((t) => t === label);
|
||||
});
|
||||
return list.slice().sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
|
||||
},
|
||||
sortedFilteredCountries() {
|
||||
const kwRaw = String(this.countrySearchText || '');
|
||||
const normalize = (s) =>
|
||||
String(s || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.toLowerCase();
|
||||
const tokens = kwRaw
|
||||
? kwRaw
|
||||
.split(/[\r\n,,;;]+/g)
|
||||
.map((s) => normalize(s))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const list = (this.availableCountries || []).filter((item) => {
|
||||
if (!tokens.length) return true;
|
||||
const label = normalize(item.label || '');
|
||||
return tokens.some((t) => t === label);
|
||||
});
|
||||
return list.slice().sort((a, b) => String(a.label || '').localeCompare(String(b.label || '')));
|
||||
},
|
||||
@@ -204,6 +321,30 @@ export default {
|
||||
return (this.selectedFieldIdsProxy || [])
|
||||
.map((id) => map[String(id)])
|
||||
.filter(Boolean);
|
||||
},
|
||||
selectedCountryTagRows() {
|
||||
const map = {};
|
||||
(this.availableCountries || []).forEach((i) => {
|
||||
map[String(i.id)] = i.label;
|
||||
});
|
||||
const quick = {
|
||||
zone_1: this.$t('autoPromotion.countryQuickZone1'),
|
||||
zone_2: this.$t('autoPromotion.countryQuickZone2'),
|
||||
zone_3: this.$t('autoPromotion.countryQuickZone3'),
|
||||
country_china: this.$t('autoPromotion.countryQuickChina'),
|
||||
country_india: this.$t('autoPromotion.countryQuickIndia')
|
||||
};
|
||||
return (this.selectedCountryIdsProxy || []).map((id) => {
|
||||
const sid = String(id);
|
||||
const fromList = map[sid];
|
||||
const text =
|
||||
fromList != null && fromList !== ''
|
||||
? fromList
|
||||
: quick[sid] != null
|
||||
? quick[sid]
|
||||
: sid;
|
||||
return { id: sid, text };
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -219,6 +360,16 @@ export default {
|
||||
emitConfirmFields() {
|
||||
this.$emit('confirm-fields');
|
||||
this.fieldDialogVisible = false;
|
||||
},
|
||||
selectAllCountries() {
|
||||
this.selectedCountryIdsProxy = (this.availableCountries || []).map((c) => String(c.id));
|
||||
},
|
||||
clearAllCountries() {
|
||||
this.selectedCountryIdsProxy = [];
|
||||
},
|
||||
emitConfirmCountries() {
|
||||
this.$emit('confirm-countries');
|
||||
this.countryDialogVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -567,5 +718,17 @@ export default {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.country-quick-checks {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.country-quick-checks >>> .el-checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px 20px;
|
||||
}
|
||||
.country-quick-checks >>> .el-checkbox {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -59,20 +59,22 @@
|
||||
|
||||
<div class="drawer-body" v-loading="loading">
|
||||
<div class="list-header">
|
||||
<div class="col-index">{{ $t('autoPromotionLogs.logColIndex') }}</div>
|
||||
<div class="col-info">{{ $t('autoPromotionLogs.logColExpert') }}</div>
|
||||
<div class="col-send" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColSendTime') }}</div>
|
||||
<div class="col-prepared" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColPreparedAt') }}</div>
|
||||
<div class="col-status">{{ $t('autoPromotionLogs.logColStatus') }}</div>
|
||||
<div class="col-action">{{ $t('autoPromotionLogs.logColAction') }}</div>
|
||||
<div class="col-action"></div>
|
||||
</div>
|
||||
|
||||
<div class="list-wrapper">
|
||||
<div
|
||||
v-for="item in fullData"
|
||||
v-for="(item, rowIndex) in fullData"
|
||||
:key="item.id"
|
||||
class="log-row"
|
||||
:class="{ 'row-error': item.isErrorRow }"
|
||||
>
|
||||
<div class="col-index">{{ (currentPage - 1) * pageSize + rowIndex + 1 }}</div>
|
||||
<div class="col-info">
|
||||
<div class="expert-main">
|
||||
<span class="name">{{ item.expertName }}</span>
|
||||
@@ -782,6 +784,14 @@ export default {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.col-index {
|
||||
flex: 0 0 52px;
|
||||
width: 52px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.col-info {
|
||||
flex: 1.8;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,29 @@
|
||||
<h1 class="mail-subject-top">{{ $t('mailboxCollect.subject') }}:{{ mailData.subject }}</h1>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button
|
||||
v-if="hasWordAttachment"
|
||||
type="primary"
|
||||
size="small"
|
||||
plain
|
||||
icon="el-icon-upload2"
|
||||
:loading="registerAuthorLoading"
|
||||
class="register-author-btn"
|
||||
@click="registerAuthorFromMail"
|
||||
>
|
||||
{{ $t('mailboxCollect.registerAuthorBtn') }}
|
||||
</el-button>
|
||||
<!-- <el-button
|
||||
v-if="resolvedJournalId"
|
||||
type="success"
|
||||
size="small"
|
||||
plain
|
||||
icon="el-icon-upload2"
|
||||
class="register-author-btn"
|
||||
@click="openAutoSubmitDialog"
|
||||
>
|
||||
{{ $t('mailboxCollect.autoSubmitBtn') }}
|
||||
</el-button> -->
|
||||
<!-- <i class="el-icon-star-off action-icon"></i> -->
|
||||
<i class="el-icon-close action-icon" @click="$emit('close')"></i>
|
||||
</div>
|
||||
@@ -74,7 +97,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mail-body-content" v-html="mailData.content_html"></div>
|
||||
<div class="mail-body-content" v-html="mailBodyHtml"></div>
|
||||
|
||||
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
|
||||
<div class="attachment-header">
|
||||
@@ -118,14 +141,98 @@
|
||||
ref="previewDialog"
|
||||
@download="downloadAttachment"
|
||||
/>
|
||||
<el-dialog
|
||||
:title="
|
||||
autoSubmitSuccessMode
|
||||
? $t('mailboxCollect.autoSubmitSuccessTitle')
|
||||
: $t('mailboxCollect.autoSubmitDialogTitle')
|
||||
"
|
||||
:visible.sync="autoSubmitDialogVisible"
|
||||
width="580px"
|
||||
append-to-body
|
||||
@close="onAutoSubmitDialogClose"
|
||||
>
|
||||
<div v-show="!autoSubmitSuccessMode">
|
||||
<el-alert
|
||||
v-if="autoSubmitPrefillFromRegisterConflict"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="auto-submit-existing-tip"
|
||||
:title="$t('mailboxCollect.autoSubmitExistingAccountTip')"
|
||||
/>
|
||||
<el-form ref="autoSubmitFormRef" :model="autoSubmitForm" label-width="120px" class="auto-submit-form">
|
||||
<el-form-item :label="$t('mailboxCollect.autoSubmitSenderEmailLabel')">
|
||||
<el-input
|
||||
class="auto-submit-email-readonly"
|
||||
:value="autoSubmitReadonlyEmailDisplay || $t('mailboxCollect.autoSubmitSenderEmailPlaceholder')"
|
||||
readonly
|
||||
tabindex="-1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('mailboxCollect.autoSubmitPassword')">
|
||||
<el-input
|
||||
v-model="autoSubmitForm.password"
|
||||
type="password"
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('mailboxCollect.autoSubmitManuscriptSource')">
|
||||
<el-upload
|
||||
:key="'mail-auto-docx-' + autoSubmitUploadKey"
|
||||
ref="autoSubmitLocalUpload"
|
||||
class="auto-submit-local-upload"
|
||||
action=""
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
:on-change="onAutoSubmitLocalDocxChange"
|
||||
:on-remove="onAutoSubmitLocalDocxRemove"
|
||||
:on-exceed="onAutoSubmitLocalDocxExceed"
|
||||
>
|
||||
<el-button size="small" type="default" icon="el-icon-folder-open">{{
|
||||
$t('mailboxCollect.autoSubmitPickLocalDocx')
|
||||
}}</el-button>
|
||||
</el-upload>
|
||||
<p class="auto-submit-source-hint">{{ $t('mailboxCollect.autoSubmitSourceHint') }}</p>
|
||||
<p v-if="localDocxDisplayName" class="auto-submit-local-picked">
|
||||
{{ $t('mailboxCollect.autoSubmitLocalPicked', { name: localDocxDisplayName }) }}
|
||||
</p>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-show="autoSubmitSuccessMode" class="auto-submit-success-body" v-html="autoSubmitSuccessBodyHtml"></div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<template v-if="autoSubmitSuccessMode">
|
||||
<el-button type="primary" @click="autoSubmitDialogVisible = false">{{
|
||||
$t('mailboxCollect.autoSubmitDialogClose')
|
||||
}}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="autoSubmitDialogVisible = false">{{ $t('mailboxCollect.autoSubmitCancel') }}</el-button>
|
||||
<el-button type="primary" :loading="autoSubmitLoading" @click="submitAutoSubmitManuscript">{{
|
||||
$t('mailboxCollect.autoSubmitConfirm')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Common from '@/components/common/common';
|
||||
import bus from '@/components/common/bus';
|
||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
||||
import { runMailManuscriptPipeline, clearAuthorShadowSession } from '@/utils/mailManuscriptAutoSubmit';
|
||||
import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import FilePreviewDialog from './FilePreviewDialog.vue';
|
||||
|
||||
/** 投稿页对外链接域名(邮件与成功通知统一用线上地址,避免本地 origin) */
|
||||
const SUBMISSION_SITE_ORIGIN = 'https://submission.tmrjournals.com';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilePreviewDialog
|
||||
@@ -144,6 +251,20 @@ export default {
|
||||
content: '',
|
||||
attachments: []
|
||||
})
|
||||
},
|
||||
/** 当前邮件列表所选邮箱账号的期刊 ID(与 mailboxCollect 一致) */
|
||||
journalId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
journalTitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 当前邮箱账号 j_email_id,与 mailboxSend 发信参数一致 */
|
||||
jEmailId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -151,17 +272,580 @@ export default {
|
||||
mediaUrl: Common.mediaUrl,
|
||||
isDetailExpanded: false,
|
||||
downloadingIndex: -1,
|
||||
packingAll: false
|
||||
packingAll: false,
|
||||
registerAuthorLoading: false,
|
||||
autoSubmitDialogVisible: false,
|
||||
autoSubmitLoading: false,
|
||||
/** 建稿成功后在同一弹窗内展示摘要,仅保留关闭 */
|
||||
autoSubmitSuccessMode: false,
|
||||
autoSubmitSuccessBodyHtml: '',
|
||||
autoSubmitForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
/** 弹窗内本机选择的 .docx,优先于邮件附件下载 */
|
||||
autoSubmitLocalDocxFile: null,
|
||||
/** 因「邮箱/账号已存在」打开建稿弹窗时展示提示条 */
|
||||
autoSubmitPrefillFromRegisterConflict: false,
|
||||
/** 本地上传组件强制重建 key,超限选新文件时替换 */
|
||||
autoSubmitUploadKey: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 存在 Word 附件(.doc / .docx)时显示「创建作者账号」 */
|
||||
hasWordAttachment() {
|
||||
const att = (this.mailData && this.mailData.attachments) || [];
|
||||
if (!att.length) return false;
|
||||
return att.some((f) => {
|
||||
const n = (f && f.name) || '';
|
||||
return /\.(doc|docx)$/i.test(n);
|
||||
});
|
||||
},
|
||||
/** 新增稿件上传链路仅接受 .docx */
|
||||
hasDocxAttachment() {
|
||||
const att = (this.mailData && this.mailData.attachments) || [];
|
||||
if (!att.length) return false;
|
||||
return att.some((f) => {
|
||||
const n = (f && f.name) || '';
|
||||
return /\.docx$/i.test(n);
|
||||
});
|
||||
},
|
||||
resolvedJournalId() {
|
||||
if (this.journalId != null && String(this.journalId).trim() !== '') {
|
||||
const n = Number(this.journalId);
|
||||
if (!Number.isNaN(n) && n > 0) return n;
|
||||
}
|
||||
try {
|
||||
const s = localStorage.getItem('mailboxCollect_journal_id');
|
||||
if (s != null && String(s).trim() !== '') {
|
||||
const n = Number(s);
|
||||
if (!Number.isNaN(n) && n > 0) return n;
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
},
|
||||
resolvedJournalLabel() {
|
||||
const id = this.resolvedJournalId;
|
||||
if (id == null) return this.$t('mailboxCollect.autoSubmitJournalUnknown');
|
||||
const t = this.journalTitle && String(this.journalTitle).trim();
|
||||
return t ? `${t}(ID: ${id})` : `ID: ${id}`;
|
||||
},
|
||||
/** 发件接口 sendOne 所需 j_email_id */
|
||||
resolvedJEmailId() {
|
||||
if (this.jEmailId != null && String(this.jEmailId).trim() !== '') {
|
||||
return String(this.jEmailId).trim();
|
||||
}
|
||||
try {
|
||||
const s = localStorage.getItem('mailboxCollect_j_email_id');
|
||||
if (s != null && String(s).trim() !== '') return String(s).trim();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/** 通知邮件正文中的期刊名称(不含「ID:」后缀) */
|
||||
resolvedNotifyJournalName() {
|
||||
const t = this.journalTitle && String(this.journalTitle).trim();
|
||||
if (t) return t;
|
||||
const id = this.resolvedJournalId;
|
||||
return id != null ? `ID ${id}` : this.$t('mailboxCollect.autoSubmitJournalUnknown');
|
||||
},
|
||||
totalAttachmentSize() {
|
||||
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
|
||||
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||||
return this.formatFileSize(total);
|
||||
},
|
||||
localDocxDisplayName() {
|
||||
const f = this.autoSubmitLocalDocxFile;
|
||||
if (!f || !f.name) return '';
|
||||
return f.name;
|
||||
},
|
||||
/** 弹窗内只读展示:当前邮件发件人邮箱 */
|
||||
autoSubmitReadonlyEmailDisplay() {
|
||||
return this.getPrimaryMailSenderEmail() || '';
|
||||
},
|
||||
/** 正文:兼容 content_html / body / html,纯文本时包一层 pre */
|
||||
mailBodyHtml() {
|
||||
const m = this.mailData || {};
|
||||
let raw = m.content_html || m.body_html || m.html || m.body || m.content || '';
|
||||
raw = normalizeEmailHtmlForInlineDisplay(raw);
|
||||
if (raw) return raw;
|
||||
const text = m.content_text;
|
||||
if (text != null && String(text).trim() !== '') {
|
||||
return `<pre class="mail-plain-pre">${this.escapeHtml(String(text))}</pre>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stripHtml(html) {
|
||||
if (html == null || html === '') return '';
|
||||
const s = String(html);
|
||||
const d = typeof document !== 'undefined' ? document.createElement('div') : null;
|
||||
if (d) {
|
||||
d.innerHTML = s;
|
||||
return (d.textContent || d.innerText || '').trim();
|
||||
}
|
||||
return s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
},
|
||||
/** 与 partyList 添加用户一致:从正文尝试提取手机号(国内 11 位或含 + 的国际号) */
|
||||
extractPhoneFromMailBody() {
|
||||
const m = this.mailData || {};
|
||||
let blob = '';
|
||||
['content_text', 'content', 'body', 'content_html', 'html', 'body_html'].forEach((k) => {
|
||||
const v = m[k];
|
||||
if (v != null && String(v).trim() !== '') blob += `\n${String(v)}`;
|
||||
});
|
||||
const text = this.stripHtml(blob);
|
||||
const cn = text.match(/(?:^|\D)(1[3-9]\d{9})(?:\D|$)/);
|
||||
if (cn) return cn[1];
|
||||
const intl = text.match(/\+\d{1,3}[\s\-]?\d[\d\s\-]{8,18}\d/);
|
||||
if (intl) return intl[0].replace(/\s+/g, ' ').trim().slice(0, 32);
|
||||
const labeled = text.match(/(?:Tel|Phone|Mobile|MW)[\s::]*([+()\d][\d\s\-().]{10,40})/i);
|
||||
if (labeled) return labeled[1].trim().slice(0, 32);
|
||||
return '';
|
||||
},
|
||||
/** 发件人显示名(用于 realname),与 partyList 校验规则一致 */
|
||||
deriveRealnameFromMail() {
|
||||
const m = this.mailData || {};
|
||||
const fromEmail = String(m.from_email || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const localPart = fromEmail.split('@')[0] || 'user';
|
||||
let rawName = String(m.from_name || '').trim();
|
||||
let realname = rawName || localPart;
|
||||
if (this.$validateString && !this.$validateString(realname)) {
|
||||
realname = localPart;
|
||||
if (this.$validateString && !this.$validateString(realname)) {
|
||||
realname = 'Author';
|
||||
}
|
||||
}
|
||||
return realname;
|
||||
},
|
||||
/** 当前邮件发件人邮箱(来稿真实地址),用于注册作者与通知收件人 */
|
||||
getPrimaryMailSenderEmail() {
|
||||
const raw = this.mailData && this.mailData.from_email != null ? String(this.mailData.from_email).trim() : '';
|
||||
if (!raw) return '';
|
||||
const lower = raw.toLowerCase();
|
||||
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(lower)) return lower;
|
||||
return '';
|
||||
},
|
||||
/** 由邮箱推导登录名(与 checkUserByAccount / addUser 常用规则一致) */
|
||||
deriveLoginAccountFromEmail(email) {
|
||||
const e = String(email || '').toLowerCase().trim();
|
||||
const at = e.indexOf('@');
|
||||
if (at < 1) return 'author';
|
||||
let acc = e
|
||||
.slice(0, at)
|
||||
.replace(/[^a-z0-9_]/gi, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
if (!acc || acc.length < 2) {
|
||||
acc = 'u_' + e.replace(/[^\w]/g, '').slice(0, 24);
|
||||
}
|
||||
if (!acc || acc.length < 2) {
|
||||
acc = 'author_' + String(Date.now()).slice(-8);
|
||||
}
|
||||
if (acc.length > 32) acc = acc.slice(0, 32);
|
||||
return acc;
|
||||
},
|
||||
openAutoSubmitDialogWithCredentialsPrefill({ usernameHint }) {
|
||||
if (this.resolvedJournalId == null) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedJournal'));
|
||||
return;
|
||||
}
|
||||
this.autoSubmitPrefillFromRegisterConflict = true;
|
||||
this.autoSubmitSuccessMode = false;
|
||||
this.autoSubmitSuccessBodyHtml = '';
|
||||
this.autoSubmitForm.username = usernameHint != null ? String(usernameHint).trim() : '';
|
||||
this.autoSubmitForm.password = '';
|
||||
this.autoSubmitDialogVisible = true;
|
||||
},
|
||||
/** addUser 返回「邮箱已存在」类文案时改走手动录入 */
|
||||
isRegisterDuplicateResponse(res, msg) {
|
||||
const s = String(msg || '');
|
||||
const lower = s.toLowerCase();
|
||||
if (lower.includes('already registered') || lower.includes('email is already')) return true;
|
||||
if (s.includes('已注册') && (s.includes('邮箱') || s.includes('邮件'))) return true;
|
||||
const hint = String(this.$t('mailboxCollect.registerAuthorExistsEmail') || '').toLowerCase();
|
||||
if (hint && lower.includes(hint.slice(0, 12))) return true;
|
||||
return false;
|
||||
},
|
||||
async registerAuthorFromMail() {
|
||||
if (!this.hasWordAttachment) return;
|
||||
const realname = this.deriveRealnameFromMail();
|
||||
const email2 = this.getPrimaryMailSenderEmail();
|
||||
if (!email2) {
|
||||
this.$message.error(this.$t('mailboxCollect.registerAuthorNeedEmail'));
|
||||
return;
|
||||
}
|
||||
const account2 = this.deriveLoginAccountFromEmail(email2);
|
||||
this.registerAuthorLoading = true;
|
||||
try {
|
||||
const r1 = await this.$api.post('api/User/checkUserByEmail', { email: email2, account: account2 });
|
||||
const r2 = await this.$api.post('api/User/checkUserByAccount', { email: email2, account: account2 });
|
||||
if (!(r1 && r2 && Number(r1.code) === 0 && Number(r2.code) === 0)) {
|
||||
this.$message.error((r1 && r1.msg) || (r2 && r2.msg) || this.$t('mailboxCollect.registerAuthorFail'));
|
||||
return;
|
||||
}
|
||||
if (r1.data && Number(r1.data.has) !== 0) {
|
||||
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: email2 });
|
||||
return;
|
||||
}
|
||||
if (r2.data && Number(r2.data.has) !== 0) {
|
||||
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: account2 });
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.$message.error(this.$t('mailboxCollect.registerAuthorFail'));
|
||||
return;
|
||||
} finally {
|
||||
this.registerAuthorLoading = false;
|
||||
}
|
||||
const pwd = '123456qwe';
|
||||
this.$notify({
|
||||
title: this.$t('mailboxCollect.registerAuthorBtn'),
|
||||
message: this.$t('mailboxCollect.registerAuthorConfirmShort', { email: email2, password: pwd }),
|
||||
type: 'info',
|
||||
duration: 8000
|
||||
});
|
||||
this.registerAuthorLoading = true;
|
||||
try {
|
||||
const phone2 = this.extractPhoneFromMailBody() || '';
|
||||
const addForm = {
|
||||
email: email2,
|
||||
account: account2,
|
||||
password: pwd,
|
||||
repassword: pwd,
|
||||
realname,
|
||||
phone: phone2 || ''
|
||||
};
|
||||
const res = await this.$api.post('api/User/addUser', addForm);
|
||||
if (res && Number(res.code) === 0) {
|
||||
this.autoSubmitForm.username = account2;
|
||||
this.autoSubmitForm.password = pwd;
|
||||
this.$nextTick(() => {
|
||||
if (this.resolvedJournalId != null) {
|
||||
this.openAutoSubmitDialog();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const msg = (res && res.msg) || '';
|
||||
if (this.isRegisterDuplicateResponse(res, msg)) {
|
||||
this.openAutoSubmitDialogWithCredentialsPrefill({ usernameHint: email2 });
|
||||
} else {
|
||||
this.$message.error(msg || this.$t('mailboxCollect.registerAuthorFail'));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.$message.error(this.$t('mailboxCollect.registerAuthorFail'));
|
||||
} finally {
|
||||
this.registerAuthorLoading = false;
|
||||
}
|
||||
},
|
||||
escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
},
|
||||
buildAbsoluteArticleAddUrl(articleId) {
|
||||
const id = articleId == null ? '' : String(articleId);
|
||||
const base = SUBMISSION_SITE_ORIGIN.replace(/\/+$/, '');
|
||||
return `${base}/articleAdd?id=${encodeURIComponent(id)}`;
|
||||
},
|
||||
buildAuthorNotifyMailHtml(articleUrl, username, passwordPlain) {
|
||||
const j = this.escapeHtml(this.resolvedNotifyJournalName);
|
||||
const a = this.escapeHtml(username);
|
||||
const p = this.escapeHtml(passwordPlain);
|
||||
const href = this.escapeHtml(articleUrl);
|
||||
const link = `<a href="${href}" target="_blank" rel="noopener noreferrer">${href}</a>`;
|
||||
const loc = (this.$i18n && this.$i18n.locale) || '';
|
||||
if (String(loc).toLowerCase().startsWith('en')) {
|
||||
return `<p>I am an editor of <strong>${j}</strong>. <br/>Thank you for your submission. An author account has been created for you: username <strong>${a}</strong>, default password <strong>${p}</strong>. <br/>Please complete your manuscript here: ${link}</p>`;
|
||||
}
|
||||
return `<p>我是《${j}》的编辑,很高兴收到你的投稿。<br/>已帮您创建好了投稿账号,登录名为 <strong>${a}</strong>,默认密码为 <strong>${p}</strong>。<br/>请您进一步完善您的稿件,投稿地址:${link}</p>`;
|
||||
},
|
||||
async getAutoSubmitNotifyMailResult({ articleUrl, username, passwordPlain }) {
|
||||
const jEmailId = this.resolvedJEmailId;
|
||||
const journalId = this.resolvedJournalId;
|
||||
if (!jEmailId || journalId == null) {
|
||||
return 'skipped_config';
|
||||
}
|
||||
const toEmail = this.getPrimaryMailSenderEmail();
|
||||
if (!toEmail) {
|
||||
return 'skipped_recipient';
|
||||
}
|
||||
try {
|
||||
const journalName = this.resolvedNotifyJournalName;
|
||||
const subject = this.$t('mailboxCollect.autoSubmitNotifyMailSubject', { journal: journalName });
|
||||
const content = this.buildAuthorNotifyMailHtml(articleUrl, username, passwordPlain);
|
||||
const res = await this.$api.post('api/email_client/sendOne', {
|
||||
to_email: toEmail,
|
||||
subject: String(subject || '').trim(),
|
||||
content,
|
||||
j_email_id: String(jEmailId),
|
||||
journal_id: String(journalId)
|
||||
});
|
||||
if (!res || Number(res.code) !== 0) {
|
||||
return 'failed';
|
||||
}
|
||||
return 'sent';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 'failed';
|
||||
}
|
||||
},
|
||||
buildAutoSubmitSuccessDialogHtml({ username, passwordPlain, articleId, articleUrl, mailResult }) {
|
||||
const href = this.escapeHtml(articleUrl);
|
||||
const linkHtml = `<a href="${href}" target="_blank" rel="noopener noreferrer">${href}</a>`;
|
||||
const line1 = this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessLineAccount', { account: username })
|
||||
);
|
||||
const line2 = this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessLinePassword', { password: passwordPlain })
|
||||
);
|
||||
const line3 = this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessLineDraft', { id: String(articleId == null ? '' : articleId) })
|
||||
);
|
||||
const linkPrefix = this.escapeHtml(this.$t('mailboxCollect.autoSubmitSuccessLineLinkPrefix'));
|
||||
let line5;
|
||||
if (mailResult === 'sent') {
|
||||
line5 = `<p class="auto-submit-success-mail-sent">${this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessMailSent')
|
||||
)}</p>`;
|
||||
} else if (mailResult === 'skipped_config') {
|
||||
line5 = `<p class="auto-submit-success-mail-muted">${this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessMailSkipped')
|
||||
)}</p>`;
|
||||
} else if (mailResult === 'skipped_recipient') {
|
||||
line5 = `<p class="auto-submit-success-mail-muted">${this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessMailSkippedRecipient')
|
||||
)}</p>`;
|
||||
} else {
|
||||
line5 = `<p class="auto-submit-success-mail-muted">${this.escapeHtml(
|
||||
this.$t('mailboxCollect.autoSubmitSuccessMailFailed')
|
||||
)}</p>`;
|
||||
}
|
||||
return `<div class="auto-submit-success-lines"><p>${line1}</p><p>${line2}</p><p>${line3}</p><p>${linkPrefix}${linkHtml}</p>${line5}</div>`;
|
||||
},
|
||||
openAutoSubmitDialog() {
|
||||
if (this.resolvedJournalId == null) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedJournal'));
|
||||
return;
|
||||
}
|
||||
this.autoSubmitPrefillFromRegisterConflict = false;
|
||||
this.autoSubmitSuccessMode = false;
|
||||
this.autoSubmitSuccessBodyHtml = '';
|
||||
this.autoSubmitDialogVisible = true;
|
||||
},
|
||||
onAutoSubmitLocalDocxChange(file, fileList) {
|
||||
const ref = this.$refs.autoSubmitLocalUpload;
|
||||
if (ref && fileList && fileList.length > 1) {
|
||||
try {
|
||||
ref.handleRemove(fileList[0]);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
return;
|
||||
}
|
||||
const raw = file && file.raw;
|
||||
if (!raw) return;
|
||||
const name = raw.name || '';
|
||||
if (!/\.docx$/i.test(name)) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNoDocx'));
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.autoSubmitLocalUpload) {
|
||||
this.$refs.autoSubmitLocalUpload.clearFiles();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.autoSubmitLocalDocxFile = raw;
|
||||
},
|
||||
onAutoSubmitLocalDocxExceed(files) {
|
||||
const wrap = files && files[0];
|
||||
const raw = wrap && wrap.raw;
|
||||
if (!raw || !/\.docx$/i.test(raw.name || '')) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNoDocx'));
|
||||
return;
|
||||
}
|
||||
if (this.$refs.autoSubmitLocalUpload) {
|
||||
this.$refs.autoSubmitLocalUpload.clearFiles();
|
||||
}
|
||||
this.autoSubmitLocalDocxFile = raw;
|
||||
this.autoSubmitUploadKey += 1;
|
||||
},
|
||||
onAutoSubmitLocalDocxRemove() {
|
||||
this.autoSubmitLocalDocxFile = null;
|
||||
},
|
||||
onAutoSubmitDialogClose() {
|
||||
clearAuthorShadowSession();
|
||||
this.autoSubmitPrefillFromRegisterConflict = false;
|
||||
this.autoSubmitSuccessMode = false;
|
||||
this.autoSubmitSuccessBodyHtml = '';
|
||||
this.autoSubmitLocalDocxFile = null;
|
||||
this.autoSubmitUploadKey = 0;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.autoSubmitLocalUpload) {
|
||||
this.$refs.autoSubmitLocalUpload.clearFiles();
|
||||
}
|
||||
if (this.$refs.autoSubmitFormRef) {
|
||||
this.$refs.autoSubmitFormRef.clearValidate();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 从邮件附件公网路径拉取 docx(axios blob + withCredentials),再走 manuscirpt 上传。
|
||||
*/
|
||||
async fetchMailDocxBlobFromAttachment(mailFile) {
|
||||
const rel = await this.ensureFileUrl(mailFile);
|
||||
if (!rel) {
|
||||
throw new Error('NO_ATTACHMENT_URL');
|
||||
}
|
||||
const url = this.resolvePublicFileUrl(rel);
|
||||
const res = await axios.get(url, {
|
||||
responseType: 'blob',
|
||||
withCredentials: true,
|
||||
timeout: 180000
|
||||
});
|
||||
if (!res || res.status < 200 || res.status >= 300 || !res.data) {
|
||||
throw new Error('DOWNLOAD_HTTP');
|
||||
}
|
||||
const blob = res.data;
|
||||
if (!(blob instanceof Blob) || blob.size === 0) {
|
||||
throw new Error('EMPTY_BLOB');
|
||||
}
|
||||
return blob;
|
||||
},
|
||||
getFirstDocxAttachment() {
|
||||
const att = (this.mailData && this.mailData.attachments) || [];
|
||||
for (let i = 0; i < att.length; i++) {
|
||||
const f = att[i];
|
||||
const n = (f && f.name) || '';
|
||||
if (/\.docx$/i.test(n)) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
submitAutoSubmitManuscript() {
|
||||
let username = String((this.autoSubmitForm && this.autoSubmitForm.username) || '').trim();
|
||||
if (!username) {
|
||||
const em = this.getPrimaryMailSenderEmail();
|
||||
if (em) username = em;
|
||||
}
|
||||
if (!username) {
|
||||
this.$message.warning(this.$t('mailboxCollect.registerAuthorNeedEmail'));
|
||||
return;
|
||||
}
|
||||
this.autoSubmitForm.username = username;
|
||||
if (!(this.autoSubmitForm && this.autoSubmitForm.password)) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitPasswordRequired'));
|
||||
return;
|
||||
}
|
||||
if (this.resolvedJournalId == null) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedJournal'));
|
||||
return;
|
||||
}
|
||||
const mailDocx = this.getFirstDocxAttachment();
|
||||
const localFile = this.autoSubmitLocalDocxFile;
|
||||
if (!localFile && !mailDocx) {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNeedDocxSource'));
|
||||
return;
|
||||
}
|
||||
this.autoSubmitLoading = true;
|
||||
(async () => {
|
||||
try {
|
||||
const docxMime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
let blob;
|
||||
let fileName;
|
||||
if (localFile) {
|
||||
blob = localFile;
|
||||
fileName = localFile.name || 'manuscript.docx';
|
||||
} else {
|
||||
blob = await this.fetchMailDocxBlobFromAttachment(mailDocx);
|
||||
fileName = mailDocx.name || 'manuscript.docx';
|
||||
}
|
||||
const r = await runMailManuscriptPipeline({
|
||||
api: this.$api,
|
||||
baseUrl: Common.baseUrl,
|
||||
preserveEditorLocalStorage: true,
|
||||
journal_id: this.resolvedJournalId,
|
||||
extractWordTables: (consumer) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const f = new File([blob], fileName, { type: docxMime });
|
||||
this.$commonJS.extractWordTablesToArrays(f, async (wordTables) => {
|
||||
try {
|
||||
await consumer(wordTables || []);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
login: {
|
||||
username,
|
||||
password: this.autoSubmitForm.password,
|
||||
code: ''
|
||||
},
|
||||
blob,
|
||||
fileName
|
||||
});
|
||||
if (r.ok) {
|
||||
bus.$emit('editorSessionLocalRestored');
|
||||
const link = this.buildAbsoluteArticleAddUrl(r.article_id);
|
||||
const passwordPlain = this.autoSubmitForm.password;
|
||||
const mailResult = await this.getAutoSubmitNotifyMailResult({
|
||||
articleUrl: link,
|
||||
username,
|
||||
passwordPlain
|
||||
});
|
||||
if (mailResult === 'failed') {
|
||||
this.$message.warning(this.$t('mailboxCollect.autoSubmitNotifyMailFail'));
|
||||
}
|
||||
this.autoSubmitSuccessBodyHtml = this.buildAutoSubmitSuccessDialogHtml({
|
||||
username,
|
||||
passwordPlain,
|
||||
articleId: r.article_id,
|
||||
articleUrl: link,
|
||||
mailResult
|
||||
});
|
||||
this.autoSubmitSuccessMode = true;
|
||||
} else {
|
||||
const base = r.msg || this.$t('mailboxCollect.autoSubmitFail');
|
||||
const tail =
|
||||
r.article_id != null
|
||||
? ' ' + this.$t('mailboxCollect.autoSubmitFailPartial', { id: r.article_id })
|
||||
: '';
|
||||
this.$message.error(base + tail);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const msg = (err && err.message) || '';
|
||||
const httpErr = err && err.response && err.response.status;
|
||||
if (
|
||||
httpErr ||
|
||||
msg === 'NO_ATTACHMENT_URL' ||
|
||||
msg === 'DOWNLOAD_HTTP' ||
|
||||
msg === 'EMPTY_BLOB'
|
||||
) {
|
||||
this.$message.error(this.$t('mailboxCollect.autoSubmitDownloadFail'));
|
||||
} else {
|
||||
this.$message.error(this.$t('mailboxCollect.autoSubmitFail'));
|
||||
}
|
||||
} finally {
|
||||
this.autoSubmitLoading = false;
|
||||
}
|
||||
})();
|
||||
},
|
||||
scrollToAttachments() {
|
||||
// 1. 获取目标元素的 DOM
|
||||
const target = this.$el.querySelector('.attachment-section');
|
||||
@@ -213,6 +897,22 @@ export default {
|
||||
}
|
||||
return '';
|
||||
},
|
||||
/**
|
||||
* 将 Common.mediaUrl 与后端返回的 file_url 拼成可访问 URL。
|
||||
* file_url 常为 /attachments/…,mediaUrl 常为 …/public/,直接拼接会得到 …/public//attachments/… 导致 404。
|
||||
* 稿件上传仍走 up_file/type/manuscirpt;此处仅用于先把附件下载为 Blob。
|
||||
*/
|
||||
resolvePublicFileUrl(pathOrUrl) {
|
||||
const p = pathOrUrl == null ? '' : String(pathOrUrl).trim();
|
||||
if (!p) return '';
|
||||
if (/^https?:\/\//i.test(p)) return p;
|
||||
let base = String(this.mediaUrl || '').replace(/\/+$/, '');
|
||||
let path = p.replace(/^\/+/, '');
|
||||
if (/\/public$/i.test(base) && /^public\//i.test(path)) {
|
||||
path = path.replace(/^public\//i, '');
|
||||
}
|
||||
return `${base}/${path}`;
|
||||
},
|
||||
async downloadAll() {
|
||||
if (this.packingAll) return;
|
||||
this.packingAll = true;
|
||||
@@ -221,7 +921,7 @@ export default {
|
||||
const promises = this.mailData.attachments.map(async (file) => {
|
||||
const url = await this.ensureFileUrl(file);
|
||||
if (!url) return;
|
||||
const fileUrl = this.mediaUrl + url;
|
||||
const fileUrl = this.resolvePublicFileUrl(url);
|
||||
const resp = await fetch(fileUrl);
|
||||
const blob = await resp.blob();
|
||||
zip.file(file.name || 'attachment', blob);
|
||||
@@ -240,7 +940,7 @@ export default {
|
||||
console.log("🚀 ~ previewAttachment ~ file:", file);
|
||||
|
||||
var that = this;
|
||||
that.$refs.previewDialog.init(file, this.mediaUrl+file.file_url || '');
|
||||
that.$refs.previewDialog.init(file, this.resolvePublicFileUrl(file.file_url) || '');
|
||||
// 1. 获取后端返回的 URL
|
||||
const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
inbox_id: String(this.mailData.inbox_id),
|
||||
@@ -248,7 +948,7 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
});
|
||||
|
||||
if (res.data && res.data.file_url) {
|
||||
const fullUrl = this.mediaUrl + res.data.file_url;
|
||||
const fullUrl = this.resolvePublicFileUrl(res.data.file_url);
|
||||
|
||||
// 2. 调用弹窗组件的 init 方法
|
||||
// 注意:微软预览要求 fullUrl 必须是公网可访问的!
|
||||
@@ -266,7 +966,7 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
this.$message.warning(this.$t('mailboxCollect.downloadFail') || '下载失败');
|
||||
return;
|
||||
}
|
||||
const fileUrl = this.mediaUrl + url;
|
||||
const fileUrl = this.resolvePublicFileUrl(url);
|
||||
const fileName = file.name || 'attachment';
|
||||
const resp = await fetch(fileUrl);
|
||||
const blob = await resp.blob();
|
||||
@@ -362,6 +1062,72 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.register-author-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.auto-submit-email-readonly /deep/ .el-input__inner {
|
||||
color: #606266;
|
||||
cursor: default;
|
||||
background-color: #f5f7fa;
|
||||
border-color: #e4e7ed;
|
||||
}
|
||||
.auto-submit-existing-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.auto-submit-local-picked {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: #67c23a;
|
||||
word-break: break-all;
|
||||
}
|
||||
.auto-submit-source-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.auto-submit-success-body {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.65;
|
||||
word-break: break-word;
|
||||
}
|
||||
.auto-submit-success-body p {
|
||||
margin: 0;
|
||||
}
|
||||
.auto-submit-success-body a {
|
||||
color: #409eff;
|
||||
}
|
||||
.auto-submit-success-lines p {
|
||||
margin: 0 0 10px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.auto-submit-success-lines p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.auto-submit-success-mail-sent {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
.auto-submit-success-mail-muted {
|
||||
color: #e6a23c;
|
||||
font-size: 13px;
|
||||
}
|
||||
.auto-submit-local-upload {
|
||||
display: inline-block;
|
||||
}
|
||||
.auto-submit-journal-readonly {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
.action-icon:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
@@ -441,6 +1207,15 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* v-html 注入的正文无 scoped 属性,用 deep 命中内部的 pre */
|
||||
.mail-body-content >>> .mail-plain-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 附件卡片 */
|
||||
.attachment-section {
|
||||
border-top: 1px solid #eee;
|
||||
@@ -635,7 +1410,9 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
}
|
||||
.attachment-brief-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
@@ -644,14 +1421,17 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
|
||||
}
|
||||
.attachment-brief-bar .brief-info {
|
||||
|
||||
}
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.attachment-brief-bar .first-file-name {
|
||||
color: #909399;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.jump-link {
|
||||
margin-left: 15px;
|
||||
margin-left: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.attachment-section {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:close-on-click-modal="false"
|
||||
width="90%"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:before-close="handleClose"
|
||||
custom-class="template-modal"
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button type="button" @click="openPreview(false)" class="preview-trigger-btn">
|
||||
<!-- <button type="button" @click="openPreview(false)" class="preview-trigger-btn">
|
||||
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.preview') }}
|
||||
</button>
|
||||
</button> -->
|
||||
<button type="button" @click="openPreview(true)" class="preview-trigger-btn preview-with-vars-btn">
|
||||
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.previewWithVariables') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
<!-- <textarea
|
||||
ref="editorRef"
|
||||
class="tmr-textarea"
|
||||
:value="plainText"
|
||||
@input="handleInput"
|
||||
:placeholder="resolvedPlaceholder"
|
||||
></textarea>
|
||||
></textarea> -->
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="showModal" class="tmr-modal-mask" @click.self="closePreviewModal">
|
||||
@@ -62,6 +62,10 @@ export default {
|
||||
|
||||
default: ''
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -87,13 +91,29 @@ export default {
|
||||
expert_name: "John Doe", // 专家姓名
|
||||
expert_field: "Biomedical Engineering", // 专家研究领域
|
||||
representative_work_title: "Advanced Applications of AI in Medical Imaging", // 专家代表作标题
|
||||
// ai_content_analysis: "", // AI 约稿理由分析
|
||||
ai_content_analysis: "【AI分析文章,一句话总结】", // AI solicitation rationale
|
||||
ai_advised_topics: "Based on your research expertise, we would particularly welcome submissions on topics such as 【这里是AI针对学者领域给特定约稿主题】, or other closely related areas that align with your work.", // AI suggested directions
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isZhLanguage() {
|
||||
return String(this.language || '').toLowerCase() === 'zh';
|
||||
},
|
||||
localizedAiMockData() {
|
||||
if (this.isZhLanguage) {
|
||||
return {
|
||||
ai_content_analysis: '【AI分析这篇文章,一句话总结】。【我们希望也关注个领域】',
|
||||
ai_advised_topics: '我们尤其关注如【方向/题目建议1】、【方向/题目建议2】以及【方向/题目建议3】等相关议题的研究进展。'
|
||||
};
|
||||
}
|
||||
return {
|
||||
ai_content_analysis: '【AI分析文章,一句话总结】',
|
||||
ai_advised_topics: 'Based on your research expertise, we would particularly welcome submissions on topics such as 【这里是AI针对学者领域给特定约稿主题】, or other closely related areas that align with your work.'
|
||||
};
|
||||
},
|
||||
resolvedPlaceholder() {
|
||||
return this.placeholder || (this.$t && this.$t('tmrEmailEditor.placeholder')) || '请输入邮件内容...';
|
||||
},
|
||||
@@ -141,6 +161,7 @@ const deadlineStr = oneMonthLater.toISOString().split('T')[0];
|
||||
|
||||
const map = {
|
||||
...this.variableMockData,
|
||||
...this.localizedAiMockData,
|
||||
journal_abbr: journal_info.jabbr, // 期刊缩写
|
||||
journal_name: journal_info.title,// 期刊全称
|
||||
journal_url: journal_info.website, // 期刊官网链接
|
||||
|
||||
@@ -1208,10 +1208,16 @@ export default {
|
||||
hasChange: false,
|
||||
hasInit: false,
|
||||
selectedIds: [],
|
||||
isMouseSelecting: false,
|
||||
_selectionSyncToCheckboxesTimer: null,
|
||||
_onDocumentSelectionChange: null,
|
||||
_onDocumentMouseUp: null,
|
||||
_onManuscriptMouseDown: null,
|
||||
|
||||
displayList: [],
|
||||
currentTypeText: '',
|
||||
tinymceId: this.id || 'vue-tinymce-' + +new Date()
|
||||
|
||||
};
|
||||
},
|
||||
// this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(newVal));
|
||||
@@ -1274,51 +1280,60 @@ export default {
|
||||
});
|
||||
this.$refs.scrollDiv.addEventListener('scroll', this.divOnScroll, { passive: true });
|
||||
|
||||
document.addEventListener('selectionchange', () => {
|
||||
if(this.isPreview)return;
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount === 0) return;
|
||||
// document.addEventListener('selectionchange', () => {
|
||||
// if(this.isPreview)return;
|
||||
// const selection = window.getSelection();
|
||||
// if (selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
// 依然保留 trim() 后的文本判断,用来决定是否显示气泡
|
||||
const plainText = selection.toString().trim();
|
||||
// const range = selection.getRangeAt(0);
|
||||
// // 依然保留 trim() 后的文本判断,用来决定是否显示气泡
|
||||
// const plainText = selection.toString().trim();
|
||||
|
||||
if (plainText !== '' && selection.rangeCount > 0) {
|
||||
// --- 1. 获取包含标签的 HTML 内容 ---
|
||||
const fragment = range.cloneContents();
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.appendChild(fragment);
|
||||
// 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 <myfigure>)
|
||||
const htmlLabel = tempDiv.innerHTML;
|
||||
const allPMainElements = this.getInvolvedPMain(range);
|
||||
const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
|
||||
if (allIds.length > 0) {
|
||||
this.updateBubblePosition(range);
|
||||
const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
|
||||
// if (plainText !== '' && selection.rangeCount > 0) {
|
||||
// // --- 1. 获取包含标签的 HTML 内容 ---
|
||||
// const fragment = range.cloneContents();
|
||||
// const tempDiv = document.createElement('div');
|
||||
// tempDiv.appendChild(fragment);
|
||||
// // 关键点:这个 label 变量现在包含了完整的 HTML 结构(如 <myfigure>)
|
||||
// const htmlLabel = tempDiv.innerHTML;
|
||||
// const allPMainElements = this.getInvolvedPMain(range);
|
||||
// const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
|
||||
// if (allIds.length > 0) {
|
||||
// this.updateBubblePosition(range);
|
||||
// const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
|
||||
|
||||
this.currentSelection = {
|
||||
// 将 label 设置为包含标签的 HTML 字符串
|
||||
label: htmlLabel,
|
||||
mainId: allIds[0],
|
||||
index: this.wordList.indexOf(rootItem),
|
||||
// this.currentSelection = {
|
||||
// // 将 label 设置为包含标签的 HTML 字符串
|
||||
// label: htmlLabel,
|
||||
// mainId: allIds[0],
|
||||
// index: this.wordList.indexOf(rootItem),
|
||||
|
||||
content: rootItem ? rootItem.content : ''
|
||||
};
|
||||
// content: rootItem ? rootItem.content : ''
|
||||
// };
|
||||
|
||||
this.currentId = allIds[0];
|
||||
this.currentData = rootItem;
|
||||
}
|
||||
} else {
|
||||
this.currentTag = '';
|
||||
this.currentTagData = {};
|
||||
this.currentSelection = {
|
||||
label: '',
|
||||
mainId: '',
|
||||
index: 0,
|
||||
content: {}
|
||||
};
|
||||
}
|
||||
});
|
||||
// this.currentId = allIds[0];
|
||||
// this.currentData = rootItem;
|
||||
// }
|
||||
// } else {
|
||||
// this.currentTag = '';
|
||||
// this.currentTagData = {};
|
||||
// this.currentSelection = {
|
||||
// label: '',
|
||||
// mainId: '',
|
||||
// index: 0,
|
||||
// content: {}
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
this._onDocumentSelectionChange = this.handleDocumentSelectionChange.bind(this);
|
||||
this._onDocumentMouseUp = this.handleDocumentMouseUp.bind(this);
|
||||
this._onManuscriptMouseDown = this.handleManuscriptMouseDown.bind(this);
|
||||
document.addEventListener('selectionchange', this._onDocumentSelectionChange);
|
||||
document.addEventListener('mouseup', this._onDocumentMouseUp);
|
||||
if (this.$refs.scroll) {
|
||||
this.$refs.scroll.addEventListener('mousedown', this._onManuscriptMouseDown);
|
||||
}
|
||||
|
||||
},
|
||||
activated() {
|
||||
// 主动触发 MathJax 渲染
|
||||
@@ -1378,6 +1393,115 @@ export default {
|
||||
|
||||
return rangePs;
|
||||
},
|
||||
handleManuscriptMouseDown(event) {
|
||||
if (this.isPreview) return;
|
||||
const root = this.$refs && this.$refs.scroll;
|
||||
if (!root) return;
|
||||
if (root.contains(event.target)) {
|
||||
this.isMouseSelecting = true;
|
||||
}
|
||||
},
|
||||
handleDocumentMouseUp() {
|
||||
this.isMouseSelecting = false;
|
||||
},
|
||||
handleDocumentSelectionChange() {
|
||||
if (this.isPreview) return;
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const plainText = selection.toString().trim();
|
||||
|
||||
if (plainText !== '' && selection.rangeCount > 0) {
|
||||
const fragment = range.cloneContents();
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.appendChild(fragment);
|
||||
const htmlLabel = tempDiv.innerHTML;
|
||||
const allPMainElements = this.getInvolvedPMain(range);
|
||||
const allIds = [...new Set(allPMainElements.map((el) => el.getAttribute('data-id')))];
|
||||
if (allIds.length > 0) {
|
||||
this.updateBubblePosition(range);
|
||||
const rootItem = this.wordList.find((item) => item.am_id == allIds[0]);
|
||||
|
||||
this.currentSelection = {
|
||||
label: htmlLabel,
|
||||
mainId: allIds[0],
|
||||
index: this.wordList.indexOf(rootItem),
|
||||
content: rootItem ? rootItem.content : ''
|
||||
};
|
||||
|
||||
this.currentId = allIds[0];
|
||||
this.currentData = rootItem;
|
||||
}
|
||||
} else {
|
||||
this.currentTag = '';
|
||||
this.currentTagData = {};
|
||||
this.currentSelection = {
|
||||
label: '',
|
||||
mainId: '',
|
||||
index: 0,
|
||||
content: {}
|
||||
};
|
||||
}
|
||||
this.scheduleSyncSelectedIdsFromRange();
|
||||
},
|
||||
scheduleSyncSelectedIdsFromRange() {
|
||||
if (this._selectionSyncToCheckboxesTimer) {
|
||||
clearTimeout(this._selectionSyncToCheckboxesTimer);
|
||||
}
|
||||
this._selectionSyncToCheckboxesTimer = setTimeout(() => {
|
||||
this.syncSelectedIdsFromRangeInternal();
|
||||
}, 80);
|
||||
},
|
||||
syncSelectedIdsFromRangeInternal() {
|
||||
if (this.isPreview || this.isInternalAction) return;
|
||||
const scrollRoot = this.$refs && this.$refs.scroll;
|
||||
if (!scrollRoot) return;
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
|
||||
const range = selection.getRangeAt(0);
|
||||
const common = range.commonAncestorContainer;
|
||||
const commonElement = common && common.nodeType === 1 ? common : common && common.parentElement;
|
||||
if (!commonElement || !scrollRoot.contains(commonElement)) return;
|
||||
|
||||
const nodes = Array.from(scrollRoot.querySelectorAll('.drop-target[main-id]'));
|
||||
const touchedMainIds = [];
|
||||
nodes.forEach((node) => {
|
||||
const id = node.getAttribute('main-id');
|
||||
if (!id || id === 'References') return;
|
||||
try {
|
||||
if (range.intersectsNode(node)) {
|
||||
touchedMainIds.push(id);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
if (!touchedMainIds.length) return;
|
||||
|
||||
const indexMap = {};
|
||||
this.wordList.forEach((item, idx) => {
|
||||
if (item && item.am_id != null) {
|
||||
indexMap[String(item.am_id)] = idx;
|
||||
}
|
||||
});
|
||||
const indices = [...new Set(touchedMainIds.map((id) => indexMap[String(id)]).filter((v) => Number.isInteger(v)))];
|
||||
if (!indices.length) return;
|
||||
const lo = Math.min(...indices);
|
||||
const hi = Math.max(...indices);
|
||||
const rangeIds = this.wordList
|
||||
.slice(lo, hi + 1)
|
||||
.map((item) => (item && item.am_id != null ? item.am_id : null))
|
||||
.filter((id) => id != null);
|
||||
if (!rangeIds.length) return;
|
||||
|
||||
const existingSet = new Set(this.selectedIds || []);
|
||||
rangeIds.forEach((id) => existingSet.add(id));
|
||||
const ordered = this.wordList
|
||||
.map((item) => (item && item.am_id != null ? item.am_id : null))
|
||||
.filter((id) => id != null && existingSet.has(id));
|
||||
this.selectedIds = ordered;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
handleUnbindLink(type) {
|
||||
const rootItem = this.wordList.find((item) => item.am_id == this.currentTagData.main_id);
|
||||
|
||||
932
src/components/page/countryManagement.vue
Normal file
932
src/components/page/countryManagement.vue
Normal file
@@ -0,0 +1,932 @@
|
||||
<template>
|
||||
<div class="country-manage">
|
||||
<div class="crumbs">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item> <i class="el-icon-place"></i> {{ $t('countryManagement.title') }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-form :inline="true" :model="query" size="small">
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
:placeholder="$t('countryManagement.keywordPlaceholder')"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="query.partition"
|
||||
clearable
|
||||
:placeholder="$t('countryManagement.partitionAll')"
|
||||
style="width: 140px"
|
||||
@change="onPartitionChange"
|
||||
>
|
||||
<el-option :label="$t('countryManagement.partitionAll')" value="" />
|
||||
<el-option :label="$t('countryManagement.partition1')" value="1" />
|
||||
<el-option :label="$t('countryManagement.partition2')" value="2" />
|
||||
<el-option :label="$t('countryManagement.partition3')" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleSearch">
|
||||
{{ $t('countryManagement.searchBtn') }}
|
||||
</el-button>
|
||||
<el-button @click="handleReset">{{ $t('countryManagement.resetBtn') }}</el-button>
|
||||
<!-- 批量修改分区按钮 -->
|
||||
<!-- <el-button type="warning" plain icon="el-icon-upload2" @click="openBatchPartitionDialog">
|
||||
|
||||
{{ $t('countryManagement.batchPartitionBtn') }}
|
||||
</el-button> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="table-card">
|
||||
<el-table :data="list" border stripe v-loading="loading" header-row-class-name="dark-table-header">
|
||||
<el-table-column type="index" :label="$t('countryManagement.table.no')" width="70" align="center" />
|
||||
<el-table-column prop="zh_name" :label="$t('countryManagement.table.zhName')" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="en_name" :label="$t('countryManagement.table.enName')" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="code" :label="$t('countryManagement.table.code')" width="100" align="center" />
|
||||
<el-table-column prop="partition" :label="$t('countryManagement.table.partition')" width="100" align="center" />
|
||||
<el-table-column :label="$t('countryManagement.table.actions')" width="220" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<div class="table-row-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
size="mini"
|
||||
icon="el-icon-edit"
|
||||
class="action-btn action-btn--edit"
|
||||
@click="openEdit(scope.row)"
|
||||
>
|
||||
{{ $t('countryManagement.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
size="mini"
|
||||
icon="el-icon-delete"
|
||||
class="action-btn action-btn--delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
{{ $t('countryManagement.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="query.page"
|
||||
:page-size="query.per_page"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('countryManagement.editTitle')"
|
||||
:visible.sync="editVisible"
|
||||
width="580px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="editForm" :model="form" :rules="rules" label-width="150px" size="small" class="country-edit-form">
|
||||
<el-form-item :label="$t('countryManagement.form.zhName')" prop="zh_name">
|
||||
<el-input v-model="form.zh_name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('countryManagement.form.enName')" prop="en_name">
|
||||
<el-input v-model="form.en_name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('countryManagement.form.code')" prop="code">
|
||||
<el-input v-model="form.code" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('countryManagement.form.partition')" prop="partition">
|
||||
<el-select v-model="form.partition" style="width: 100%">
|
||||
<el-option :label="$t('countryManagement.partition1')" value="1" />
|
||||
<el-option :label="$t('countryManagement.partition2')" value="2" />
|
||||
<el-option :label="$t('countryManagement.partition3')" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="editVisible = false">{{ $t('countryManagement.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" @click="submitEdit">{{ $t('countryManagement.save') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('countryManagement.batchPartitionTitle')"
|
||||
:visible.sync="batchPartitionVisible"
|
||||
width="80vw"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="resetBatchPartitionDialog"
|
||||
>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between">
|
||||
<div class="batch-partition-content" style="width: 100%">
|
||||
<el-form label-width="120px" size="small" class="batch-partition-target-form">
|
||||
<el-form-item :label="$t('countryManagement.batchPartitionTargetLabel')">
|
||||
<el-select
|
||||
v-model="batchPartitionTargetPartition"
|
||||
style="width: 220px"
|
||||
@change="handleBatchPartitionDraftChange"
|
||||
>
|
||||
<el-option :label="$t('countryManagement.partition1')" value="1" />
|
||||
<el-option :label="$t('countryManagement.partition2')" value="2" />
|
||||
<el-option :label="$t('countryManagement.partition3')" value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p class="batch-partition-help">{{ $t('countryManagement.batchPartitionHelp') }}</p>
|
||||
<el-input
|
||||
v-model="batchPartitionInput"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
:placeholder="$t('countryManagement.batchPartitionPlaceholder')"
|
||||
@input="handleBatchPartitionDraftChange"
|
||||
/>
|
||||
<div class="batch-partition-actions">
|
||||
<el-button type="primary" plain :loading="batchPartitionPreviewLoading" @click="previewBatchPartition">
|
||||
{{ $t('countryManagement.batchPartitionPreview') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="batchPartitionToApplyBaseRows.length > 0"
|
||||
type="success"
|
||||
:loading="batchPartitionApplyLoading"
|
||||
:disabled="!batchPartitionPreviewRows.length"
|
||||
@click="applyBatchPartition"
|
||||
>
|
||||
{{ $t('countryManagement.batchPartitionApply') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="batchPartitionStats.input > 0" class="batch-partition-summary-container" style="margin-left: 20px">
|
||||
<div v-if="batchPartitionStats.input > 0" class="batch-partition-summary">
|
||||
{{
|
||||
$t('countryManagement.batchPartitionSummaryLine', {
|
||||
input: String(batchPartitionStats.input),
|
||||
matched: String(batchPartitionStats.matched),
|
||||
miss: String(batchPartitionStats.miss),
|
||||
same: String(batchPartitionStats.skipSame),
|
||||
will: String(batchPartitionStats.willUpdate)
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<p v-if="batchPartitionPreviewRows.length" class="batch-partition-selection-hint">
|
||||
{{ $t('countryManagement.batchPartitionSelectionHint') }}
|
||||
</p>
|
||||
|
||||
<el-table
|
||||
v-if="batchPartitionPreviewRows.length"
|
||||
:data="batchPartitionFilteredPreviewRows"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
max-height="400"
|
||||
class="batch-partition-table"
|
||||
:row-class-name="batchPartitionRowClassName"
|
||||
>
|
||||
<el-table-column type="index" :label="$t('countryManagement.table.no')" width="64" align="center" />
|
||||
|
||||
<el-table-column
|
||||
prop="key"
|
||||
:label="$t('countryManagement.batchPartitionColKey')"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column prop="country_id" :label="$t('countryManagement.batchPartitionColId')" width="90" align="center" />
|
||||
<el-table-column
|
||||
prop="en_name"
|
||||
:label="$t('countryManagement.batchPartitionColName')"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column :label="$t('countryManagement.batchPartitionColCurrentPartition')" width="140" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="['partition-chip', scope.row.status === 'diff' ? 'partition-chip--warn' : '']">
|
||||
{{ scope.row.currentPartition || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('countryManagement.batchPartitionColMatch')" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span
|
||||
:class="[
|
||||
'match-chip',
|
||||
scope.row.status === 'not_found'
|
||||
? 'match-chip--danger'
|
||||
: scope.row.status === 'diff'
|
||||
? 'match-chip--warn'
|
||||
: 'match-chip--ok'
|
||||
]"
|
||||
>
|
||||
{{ scope.row.matchLabel }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'countryManagement',
|
||||
data() {
|
||||
return {
|
||||
query: {
|
||||
keyword: '',
|
||||
partition: '',
|
||||
page: 1,
|
||||
per_page: 20
|
||||
},
|
||||
list: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
editVisible: false,
|
||||
saveLoading: false,
|
||||
form: {
|
||||
country_id: '',
|
||||
zh_name: '',
|
||||
en_name: '',
|
||||
code: '',
|
||||
partition: '1'
|
||||
},
|
||||
rules: {},
|
||||
batchPartitionVisible: false,
|
||||
batchPartitionTargetPartition: '1',
|
||||
batchPartitionInput: '',
|
||||
batchPartitionPreviewRows: [],
|
||||
batchPartitionPreviewLoading: false,
|
||||
batchPartitionApplyLoading: false,
|
||||
batchPartitionAllRows: [],
|
||||
/** 预览后的统计:录入、匹配、将更新等 */
|
||||
batchPartitionStats: {
|
||||
input: 0,
|
||||
matched: 0,
|
||||
miss: 0,
|
||||
skipSame: 0,
|
||||
willUpdate: 0
|
||||
},
|
||||
/** 预览表筛选关键词(分号、逗号、换行分隔多词) */
|
||||
batchPartitionTableFilter: '',
|
||||
/** none:未勾选视为提交全部可更新行;explicit:仅提交已勾选 */
|
||||
batchPartitionSelectionMode: 'none',
|
||||
batchPartitionSelectedIds: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
batchPartitionFilteredPreviewRows() {
|
||||
const rows = this.batchPartitionPreviewRows || [];
|
||||
const tokens = this.batchPartitionFilterTokens(this.batchPartitionTableFilter);
|
||||
if (!tokens.length) return rows;
|
||||
return rows.filter((r) => this.batchPartitionRowMatchesTokens(r, tokens));
|
||||
},
|
||||
batchPartitionToApplyBaseRows() {
|
||||
return (this.batchPartitionPreviewRows || []).filter(
|
||||
(r) => r._row && String(r._row.partition != null ? r._row.partition : '') !== String(r.partition)
|
||||
);
|
||||
},
|
||||
batchPartitionEffectiveToApplyRows() {
|
||||
const base = this.batchPartitionToApplyBaseRows;
|
||||
if (this.batchPartitionSelectionMode === 'none') return base;
|
||||
const ids = new Set((this.batchPartitionSelectedIds || []).map(String));
|
||||
return base.filter((r) => ids.has(this.countryRowId(r._row)));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.rules = {
|
||||
zh_name: [{ required: true, message: this.$t('countryManagement.ruleZhName'), trigger: 'blur' }],
|
||||
en_name: [{ required: true, message: this.$t('countryManagement.ruleEnName'), trigger: 'blur' }],
|
||||
code: [{ required: true, message: this.$t('countryManagement.ruleCode'), trigger: 'blur' }],
|
||||
partition: [{ required: true, message: this.$t('countryManagement.rulePartition'), trigger: 'change' }]
|
||||
};
|
||||
this.fetchList();
|
||||
},
|
||||
methods: {
|
||||
countryRowId(row) {
|
||||
if (!row) return '';
|
||||
return row.country_id != null ? String(row.country_id) : row.id != null ? String(row.id) : '';
|
||||
},
|
||||
normalizeListResponse(res) {
|
||||
const d = res && res.data;
|
||||
if (!d) return { list: [], total: 0 };
|
||||
const list = d.list || d.data || d.rows || [];
|
||||
const total = Number(d.total != null ? d.total : d.count != null ? d.count : list.length) || 0;
|
||||
return { list: Array.isArray(list) ? list : [], total };
|
||||
},
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = {
|
||||
keyword: this.query.keyword || '',
|
||||
partition: this.query.partition === '' || this.query.partition == null ? '' : String(this.query.partition),
|
||||
page: this.query.page,
|
||||
per_page: this.query.per_page
|
||||
};
|
||||
const res = await this.$api.post('api/Country/getList', params);
|
||||
if (res && res.code === 0) {
|
||||
const { list, total } = this.normalizeListResponse(res);
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
} else {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
if (res && res.msg) this.$message.warning(res.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
this.list = [];
|
||||
this.total = 0;
|
||||
this.$message.error(this.$t('countryManagement.loadFailed'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.query.page = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
/** 切换分区即请求列表,无需再点搜索 */
|
||||
onPartitionChange() {
|
||||
this.query.page = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
handleReset() {
|
||||
this.query = {
|
||||
keyword: '',
|
||||
partition: '',
|
||||
page: 1,
|
||||
per_page: 20
|
||||
};
|
||||
this.fetchList();
|
||||
},
|
||||
handleSizeChange(size) {
|
||||
this.query.per_page = size;
|
||||
this.query.page = 1;
|
||||
this.fetchList();
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.query.page = page;
|
||||
this.fetchList();
|
||||
},
|
||||
openEdit(row) {
|
||||
const id = this.countryRowId(row);
|
||||
this.form = {
|
||||
country_id: id,
|
||||
zh_name: (row && row.zh_name) || '',
|
||||
en_name: (row && row.en_name) || '',
|
||||
code: (row && row.code) || '',
|
||||
partition: row && row.partition != null && row.partition !== '' ? String(row.partition) : '1'
|
||||
};
|
||||
this.editVisible = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.editForm) this.$refs.editForm.clearValidate();
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
country_id: '',
|
||||
zh_name: '',
|
||||
en_name: '',
|
||||
code: '',
|
||||
partition: '1'
|
||||
};
|
||||
},
|
||||
submitEdit() {
|
||||
this.$refs.editForm.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
this.saveLoading = true;
|
||||
try {
|
||||
const params = {
|
||||
country_id: String(this.form.country_id),
|
||||
zh_name: this.form.zh_name,
|
||||
en_name: this.form.en_name,
|
||||
code: this.form.code,
|
||||
partition: String(this.form.partition)
|
||||
};
|
||||
const res = await this.$api.post('api/Country/edit', params);
|
||||
if (res && res.code === 0) {
|
||||
this.$message.success(this.$t('countryManagement.saveSuccess'));
|
||||
this.editVisible = false;
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('countryManagement.opFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('countryManagement.opFailed'));
|
||||
} finally {
|
||||
this.saveLoading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDelete(row) {
|
||||
const id = this.countryRowId(row);
|
||||
if (!id) {
|
||||
this.$message.warning(this.$t('countryManagement.missingId'));
|
||||
return;
|
||||
}
|
||||
this.$confirm(this.$t('countryManagement.deleteConfirm'), this.$t('countryManagement.deleteTitle'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$t('countryManagement.confirm'),
|
||||
cancelButtonText: this.$t('countryManagement.cancel')
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await this.$api.post('api/Country/delete', { country_id: String(id) });
|
||||
if (res && res.code === 0) {
|
||||
this.$message.success(this.$t('countryManagement.deleteSuccess'));
|
||||
this.fetchList();
|
||||
} else {
|
||||
this.$message.error((res && res.msg) || this.$t('countryManagement.opFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('countryManagement.opFailed'));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
openBatchPartitionDialog() {
|
||||
this.batchPartitionVisible = true;
|
||||
},
|
||||
resetBatchPartitionDialog() {
|
||||
this.batchPartitionTargetPartition = '1';
|
||||
this.batchPartitionInput = '';
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.batchPartitionAllRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
this.batchPartitionTableFilter = '';
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
},
|
||||
handleBatchPartitionDraftChange() {
|
||||
if (!this.batchPartitionPreviewRows.length && !(this.batchPartitionStats && this.batchPartitionStats.input > 0)) return;
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.batchPartitionAllRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
this.batchPartitionTableFilter = '';
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
},
|
||||
batchPartitionFilterTokens(text) {
|
||||
return String(text || '')
|
||||
.toLowerCase()
|
||||
.split(/[;\n,,]+/g)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
batchPartitionRowMatchesTokens(pr, tokens) {
|
||||
if (!tokens || !tokens.length) return true;
|
||||
const parts = [
|
||||
pr.key,
|
||||
pr.en_name,
|
||||
pr.country_id,
|
||||
pr.matchLabel,
|
||||
pr._row && pr._row.code,
|
||||
pr._row && pr._row.zh_name,
|
||||
pr._row && pr._row.en_name
|
||||
]
|
||||
.filter((x) => x != null && String(x).trim() !== '')
|
||||
.map((x) => String(x).toLowerCase());
|
||||
const hay = parts.join(' ');
|
||||
return tokens.some((t) => hay.indexOf(t) !== -1);
|
||||
},
|
||||
isBatchPartitionRowUpdatable(pr) {
|
||||
if (!pr || !pr._row) return false;
|
||||
return String(pr._row.partition != null ? pr._row.partition : '') !== String(pr.partition);
|
||||
},
|
||||
isBatchPartitionRowSelected(pr) {
|
||||
if (!pr._row || this.batchPartitionSelectionMode === 'none') return false;
|
||||
const id = this.countryRowId(pr._row);
|
||||
return (this.batchPartitionSelectedIds || []).map(String).includes(String(id));
|
||||
},
|
||||
onBatchPartitionNativeCheckboxChange(pr, ev) {
|
||||
const checked = !!(ev && ev.target && ev.target.checked);
|
||||
const id = pr._row ? this.countryRowId(pr._row) : '';
|
||||
if (!id || !this.isBatchPartitionRowUpdatable(pr)) return;
|
||||
let ids = (this.batchPartitionSelectedIds || []).map(String);
|
||||
if (this.batchPartitionSelectionMode === 'none') {
|
||||
if (checked) {
|
||||
this.batchPartitionSelectionMode = 'explicit';
|
||||
ids = [String(id)];
|
||||
}
|
||||
} else {
|
||||
if (checked) {
|
||||
if (!ids.includes(String(id))) ids.push(String(id));
|
||||
} else {
|
||||
ids = ids.filter((x) => x !== String(id));
|
||||
}
|
||||
}
|
||||
if (this.batchPartitionSelectionMode === 'explicit' && ids.length === 0) {
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
}
|
||||
this.batchPartitionSelectedIds = ids;
|
||||
},
|
||||
selectAllFilteredBatchPartitionRows() {
|
||||
const ids = [];
|
||||
(this.batchPartitionFilteredPreviewRows || []).forEach((r) => {
|
||||
if (!this.isBatchPartitionRowUpdatable(r)) return;
|
||||
const id = this.countryRowId(r._row);
|
||||
if (id) ids.push(String(id));
|
||||
});
|
||||
if (!ids.length) {
|
||||
this.$message.info(this.$t('countryManagement.batchPartitionSkipSame'));
|
||||
return;
|
||||
}
|
||||
this.batchPartitionSelectionMode = 'explicit';
|
||||
this.batchPartitionSelectedIds = [...new Set(ids)];
|
||||
},
|
||||
clearBatchPartitionRowSelection() {
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
},
|
||||
batchPartitionRowClassName({ row }) {
|
||||
if (!row) return '';
|
||||
if (row.status === 'not_found') return 'batch-row-not-found';
|
||||
if (row.status === 'diff') return 'batch-row-mismatch';
|
||||
return '';
|
||||
},
|
||||
resetBatchPartitionStats() {
|
||||
this.batchPartitionStats = {
|
||||
input: 0,
|
||||
matched: 0,
|
||||
miss: 0,
|
||||
skipSame: 0,
|
||||
willUpdate: 0
|
||||
};
|
||||
},
|
||||
recomputeBatchPartitionStats() {
|
||||
const rows = this.batchPartitionPreviewRows || [];
|
||||
let matched = 0;
|
||||
let miss = 0;
|
||||
let skipSame = 0;
|
||||
let willUpdate = 0;
|
||||
rows.forEach((r) => {
|
||||
if (!r._row) {
|
||||
miss += 1;
|
||||
return;
|
||||
}
|
||||
matched += 1;
|
||||
const cur = String(r._row.partition != null ? r._row.partition : '');
|
||||
if (cur === String(r.partition)) {
|
||||
skipSame += 1;
|
||||
} else {
|
||||
willUpdate += 1;
|
||||
}
|
||||
});
|
||||
this.batchPartitionStats = {
|
||||
input: rows.length,
|
||||
matched,
|
||||
miss,
|
||||
skipSame,
|
||||
willUpdate
|
||||
};
|
||||
},
|
||||
parseBatchPartitionLines(text) {
|
||||
const lines = String(text || '')
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && !l.startsWith('#'));
|
||||
return lines.map((key) => ({ key }));
|
||||
},
|
||||
findCountryRowForBatch(rows, keyRaw) {
|
||||
const key = String(keyRaw || '').trim();
|
||||
if (!key || !rows || !rows.length) return null;
|
||||
const upper = key.toUpperCase();
|
||||
if (upper.length === 3) {
|
||||
const byCode = rows.find((r) => String(r.code || '').toUpperCase() === upper);
|
||||
if (byCode) return byCode;
|
||||
}
|
||||
const lower = key.toLowerCase();
|
||||
return (
|
||||
rows.find(
|
||||
(r) =>
|
||||
String(r.en_name || '')
|
||||
.trim()
|
||||
.toLowerCase() === lower
|
||||
) ||
|
||||
rows.find((r) => String(r.zh_name || '').trim() === key) ||
|
||||
null
|
||||
);
|
||||
},
|
||||
async fetchAllCountryRowsForBatch() {
|
||||
const all = [];
|
||||
let page = 1;
|
||||
const per_page = 300;
|
||||
while (true) {
|
||||
const res = await this.$api.post('api/Country/getList', {
|
||||
keyword: '',
|
||||
partition: '',
|
||||
page,
|
||||
per_page
|
||||
});
|
||||
if (!res || res.code !== 0) {
|
||||
throw new Error('getList');
|
||||
}
|
||||
const { list, total } = this.normalizeListResponse(res);
|
||||
if (!Array.isArray(list) || list.length === 0) break;
|
||||
all.push(...list);
|
||||
if (all.length >= total) break;
|
||||
page += 1;
|
||||
if (page > 500) break;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
async previewBatchPartition() {
|
||||
const targetPartition = String(this.batchPartitionTargetPartition || '').trim();
|
||||
if (!['1', '2', '3'].includes(targetPartition)) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionTargetRequired'));
|
||||
return;
|
||||
}
|
||||
const parsed = this.parseBatchPartitionLines(this.batchPartitionInput);
|
||||
if (!parsed.length) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionEmpty'));
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
return;
|
||||
}
|
||||
this.batchPartitionPreviewLoading = true;
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
try {
|
||||
this.batchPartitionAllRows = await this.fetchAllCountryRowsForBatch();
|
||||
} catch (e) {
|
||||
this.batchPartitionAllRows = [];
|
||||
this.batchPartitionPreviewRows = [];
|
||||
this.resetBatchPartitionStats();
|
||||
this.$message.error(this.$t('countryManagement.batchPartitionLoadListFailed'));
|
||||
this.batchPartitionPreviewLoading = false;
|
||||
return;
|
||||
}
|
||||
const rows = this.batchPartitionAllRows;
|
||||
this.batchPartitionPreviewRows = parsed.map((item) => {
|
||||
const row = this.findCountryRowForBatch(rows, item.key);
|
||||
const id = row ? this.countryRowId(row) : '';
|
||||
const curPart = row && row.partition != null && row.partition !== '' ? String(row.partition) : '';
|
||||
let matchLabel = this.$t('countryManagement.batchPartitionMissing');
|
||||
let status = 'not_found';
|
||||
if (row) {
|
||||
if (curPart === targetPartition) {
|
||||
matchLabel = this.$t('countryManagement.batchPartitionSkipSame');
|
||||
status = 'same';
|
||||
} else {
|
||||
matchLabel = this.$t('countryManagement.batchPartitionMismatch');
|
||||
status = 'diff';
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: item.key,
|
||||
partition: targetPartition,
|
||||
currentPartition: curPart || '—',
|
||||
matchLabel,
|
||||
status,
|
||||
country_id: id || '—',
|
||||
en_name: (row && row.en_name) || '—',
|
||||
_row: row
|
||||
};
|
||||
});
|
||||
if (!this.batchPartitionPreviewRows.length) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionPreviewEmpty'));
|
||||
}
|
||||
this.recomputeBatchPartitionStats();
|
||||
this.batchPartitionTableFilter = '';
|
||||
this.batchPartitionSelectionMode = 'none';
|
||||
this.batchPartitionSelectedIds = [];
|
||||
this.batchPartitionPreviewLoading = false;
|
||||
},
|
||||
async applyBatchPartition() {
|
||||
if (!this.batchPartitionPreviewRows.length) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionPreviewEmpty'));
|
||||
return;
|
||||
}
|
||||
const toApply = this.batchPartitionEffectiveToApplyRows.slice();
|
||||
if (this.batchPartitionSelectionMode === 'explicit' && toApply.length === 0) {
|
||||
this.$message.warning(this.$t('countryManagement.batchPartitionNoSelection'));
|
||||
return;
|
||||
}
|
||||
if (!toApply.length) {
|
||||
this.$message.info(this.$t('countryManagement.batchPartitionSkipSame'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t('countryManagement.batchPartitionApplyConfirm', { n: String(toApply.length) }),
|
||||
this.$t('countryManagement.batchPartitionTitle'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: this.$t('countryManagement.confirm'),
|
||||
cancelButtonText: this.$t('countryManagement.cancel')
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
this.batchPartitionApplyLoading = true;
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
for (let i = 0; i < toApply.length; i++) {
|
||||
const pr = toApply[i];
|
||||
const row = pr._row;
|
||||
const params = {
|
||||
country_id: String(this.countryRowId(row)),
|
||||
zh_name: (row && row.zh_name) || '',
|
||||
en_name: (row && row.en_name) || '',
|
||||
code: (row && row.code) || '',
|
||||
partition: String(pr.partition)
|
||||
};
|
||||
try {
|
||||
const res = await this.$api.post('api/Country/edit', params);
|
||||
if (res && res.code === 0) ok += 1;
|
||||
else fail += 1;
|
||||
} catch (e) {
|
||||
fail += 1;
|
||||
}
|
||||
}
|
||||
const miss = this.batchPartitionPreviewRows.filter((r) => !r._row).length;
|
||||
const inputN = String(this.batchPartitionStats.input || this.batchPartitionPreviewRows.length);
|
||||
this.$message.success(
|
||||
this.$t('countryManagement.batchPartitionDone', {
|
||||
ok: String(ok),
|
||||
fail: String(fail),
|
||||
miss: String(miss),
|
||||
input: inputN
|
||||
})
|
||||
);
|
||||
this.batchPartitionApplyLoading = false;
|
||||
this.batchPartitionVisible = false;
|
||||
this.fetchList();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.country-manage {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.crumbs {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.toolbar {
|
||||
/* margin-bottom: 15px; */
|
||||
margin-top: 20px;
|
||||
}
|
||||
.table-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
::v-deep .dark-table-header th {
|
||||
background-color: #f5f7fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
.table-row-actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-row-actions .action-btn {
|
||||
margin: 0;
|
||||
padding: 7px 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.table-row-actions .action-btn--edit {
|
||||
background-color: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.table-row-actions .action-btn--edit:hover {
|
||||
background-color: #d9ecff;
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
.table-row-actions .action-btn--delete {
|
||||
background-color: #fef0f0;
|
||||
border-color: #fbc4c4;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.table-row-actions .action-btn--delete:hover {
|
||||
background-color: #fde2e2;
|
||||
border-color: #f56c6c;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.batch-partition-help {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
white-space: pre-line;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.batch-partition-target-form {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.batch-partition-actions {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.batch-partition-summary {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
line-height: 1.55;
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 12px;
|
||||
background: #f4f6f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
.batch-partition-selection-hint {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.55;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.batch-partition-table-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.batch-partition-filter-input {
|
||||
width: min(320px, 100%);
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.batch-partition-filter-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.batch-partition-checkbox-placeholder {
|
||||
color: #dcdfe6;
|
||||
font-size: 12px;
|
||||
}
|
||||
.batch-partition-native-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
accent-color: #409eff;
|
||||
}
|
||||
.batch-partition-table {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.partition-chip,
|
||||
.match-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 62px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.partition-chip--warn,
|
||||
.match-chip--warn {
|
||||
color: #8a5700;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd591;
|
||||
}
|
||||
.match-chip--danger {
|
||||
color: #a8071a;
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
}
|
||||
.match-chip--ok {
|
||||
color: #237804;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
::v-deep .batch-row-not-found td {
|
||||
background: #fff5f5 !important;
|
||||
}
|
||||
::v-deep .batch-row-mismatch td {
|
||||
background: #fffdf0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -771,6 +771,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -1188,6 +1189,43 @@ export default {
|
||||
this.getjour();
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump');
|
||||
if (!raw) return '';
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
if (!obj || obj.from !== 'promotionFactory') return '';
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return '';
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : '';
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
return id;
|
||||
} catch (e) {
|
||||
try {
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
} catch (e2) {}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
forceClearJumpQuery() {
|
||||
const q = (this.$route && this.$route.query) || {};
|
||||
const has = q && (q._ap_from || q.journal_id || q.journalId || q.journalIdd);
|
||||
if (!has) return;
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : '';
|
||||
const nextQuery = Object.assign({}, q);
|
||||
delete nextQuery._ap_from;
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery.journalIdd;
|
||||
// 关闭“中间态 fullPath”对应的标签页,避免残留在顶部 Tags
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true });
|
||||
}
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => {});
|
||||
try {
|
||||
const cleanUrl = window.location.origin + this.$route.path;
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
} catch (e) {}
|
||||
},
|
||||
unplIcon() {
|
||||
this.$refs['upIconIMg'].$refs['upload-inner'].handleClick();
|
||||
},
|
||||
@@ -1314,9 +1352,28 @@ export default {
|
||||
if (res.code == 0) {
|
||||
this.df_jour = res.data.journals;
|
||||
this.add_jour = res.data.journals;
|
||||
this.query.journal_id = this.df_jour[0].journal_id;
|
||||
this.query.journal_title = this.df_jour[0].title;
|
||||
const fromQueryId = this.$route && this.$route.query ? this.$route.query.journal_id : '';
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId();
|
||||
const pickedId = fromQueryId || fromSessionId;
|
||||
const matched = pickedId ? this.df_jour.find((j) => String(j.journal_id) === String(pickedId)) : null;
|
||||
const first = this.df_jour[0];
|
||||
const picked = matched || first;
|
||||
this.query.journal_id = picked ? picked.journal_id : 0;
|
||||
this.query.journal_title = picked ? picked.title : '';
|
||||
this.getgroup();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {});
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery._ap_from;
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => {});
|
||||
// 双保险:路由稳定后再强制清参一次,避免地址栏残留“中间态”
|
||||
setTimeout(() => {
|
||||
this.forceClearJumpQuery();
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -68,16 +68,42 @@
|
||||
</p>
|
||||
<p class="info-row" style="margin-top: 10px; font-size: 12px">
|
||||
<span class="label">{{ $t('expertDatabase.fields.acquisitionTimeLabel') }}</span>
|
||||
<span class="value time">{{ scope.row.ctime_text ? scope.row.ctime_text : '-' }}</span>
|
||||
<span class="value time">{{ scope.row.ctime_text ? scope.row.ctime_text : $t('expertDatabase.emptyMark') }}</span>
|
||||
</p>
|
||||
|
||||
<span class="custom-tag">{{ scope.row.state_text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="affiliation" :label="$t('expertDatabase.columns.affiliation')" min-width="260" />
|
||||
<el-table-column prop="fieldDisplay" :label="$t('expertDatabase.columns.researchAreas')" min-width="200">
|
||||
<el-table-column prop="country" :label="$t('expertDatabase.columns.country')" min-width="100" />
|
||||
<el-table-column prop="affiliation" :label="$t('expertDatabase.columns.affiliation')" min-width="460" />
|
||||
<el-table-column :label="$t('expertDatabase.columns.unsubscribeStatus')" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div class="unsubscribe-cell">
|
||||
<el-tag
|
||||
size="mini"
|
||||
:type="getUnsubscribeValue(scope.row) === 1 ? 'warning' : 'success'"
|
||||
effect="plain"
|
||||
class="unsubscribe-tag"
|
||||
>
|
||||
{{
|
||||
getUnsubscribeValue(scope.row) === 1
|
||||
? $t('expertDatabase.unsubscribeUnsubscribed')
|
||||
: $t('expertDatabase.unsubscribeNormal')
|
||||
}}
|
||||
</el-tag>
|
||||
<el-switch
|
||||
class="unsubscribe-switch"
|
||||
:value="getUnsubscribeValue(scope.row) === 0"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#dcdfe6"
|
||||
:disabled="switchingExpertId === getExpertId(scope.row)"
|
||||
@change="(val) => handleUnsubscribeSwitch(scope.row, val)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fieldDisplay" :label="$t('expertDatabase.columns.researchAreas')" min-width="260">
|
||||
<template slot-scope="scope">
|
||||
<div v-for="(field, index) in scope.row.fields" :key="index">
|
||||
<span>
|
||||
@@ -85,10 +111,43 @@
|
||||
{{ field.field }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="scope.row.fields && scope.row.fields.length"
|
||||
type="text"
|
||||
size="small"
|
||||
class="view-all-btn"
|
||||
@click.stop="openFieldDetail(scope.row)"
|
||||
>
|
||||
{{ $t('expertDatabase.viewAllInfo') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('expertDatabase.detailDialogTitle')"
|
||||
:visible.sync="fieldDetailVisible"
|
||||
width="1200px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="field-detail-dialog"
|
||||
>
|
||||
<p v-if="fieldDetailExpert" class="field-detail-name">
|
||||
<span class="label">{{ $t('expertDatabase.fields.nameLabel') }}</span>
|
||||
<span class="value bold">{{ fieldDetailExpert.name }}</span>
|
||||
</p>
|
||||
<el-table v-if="fieldDetailRows.length" :data="fieldDetailRows" border stripe size="small" max-height="420">
|
||||
<el-table-column type="index" :label="$t('expertDatabase.table.no')" width="56" align="center" />
|
||||
<el-table-column prop="field" :label="$t('expertDatabase.detailColField')" min-width="140" />
|
||||
<el-table-column prop="paper_title" :label="$t('expertDatabase.detailColPaper')" min-width="220" />
|
||||
<el-table-column prop="paper_journal" :label="$t('expertDatabase.detailColJournal')" min-width="140" />
|
||||
</el-table>
|
||||
<p v-else class="field-detail-empty">{{ $t('expertDatabase.noFieldDetail') }}</p>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="fieldDetailVisible = false">{{ $t('expertDatabase.detailClose') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
background
|
||||
@@ -124,7 +183,11 @@ export default {
|
||||
list: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
exportLoading: false
|
||||
exportLoading: false,
|
||||
switchingExpertId: '',
|
||||
fieldDetailVisible: false,
|
||||
fieldDetailExpert: null,
|
||||
fieldDetailRows: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -187,8 +250,12 @@ export default {
|
||||
this.list = rawList.map((item) => {
|
||||
const fieldArray = item.fields || [];
|
||||
const fieldNames = fieldArray.map((f) => f.field).join(', ');
|
||||
const unsubscribed = this.normalizeUnsubscribeValue(item);
|
||||
return {
|
||||
...item,
|
||||
// 以 unsubscribed 作为页面主判断字段
|
||||
unsubscribed,
|
||||
unsubscribe: unsubscribed,
|
||||
fieldDisplay: fieldNames
|
||||
};
|
||||
});
|
||||
@@ -204,6 +271,57 @@ export default {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
getExpertId(row) {
|
||||
if (!row) return '';
|
||||
return String(row.expert_id || row.id || row.user_id || row.uid || '').trim();
|
||||
},
|
||||
normalizeUnsubscribeValue(row) {
|
||||
const raw =
|
||||
row && row.unsubscribed != null
|
||||
? row.unsubscribed
|
||||
: row && row.unsubscribe != null
|
||||
? row.unsubscribe
|
||||
: row && row.is_unsubscribe != null
|
||||
? row.is_unsubscribe
|
||||
: 0;
|
||||
return Number(raw) === 1 ? 1 : 0;
|
||||
},
|
||||
getUnsubscribeValue(row) {
|
||||
return this.normalizeUnsubscribeValue(row);
|
||||
},
|
||||
async handleUnsubscribeSwitch(row, checked) {
|
||||
const expertId = this.getExpertId(row);
|
||||
if (!expertId) {
|
||||
this.$message.warning(this.$t('expertDatabase.unsubscribeMissingId'));
|
||||
return;
|
||||
}
|
||||
const oldValue = this.getUnsubscribeValue(row);
|
||||
// switch: ON(checked)=正常(0), OFF=退订(1)
|
||||
const nextValue = checked ? 0 : 1;
|
||||
if (oldValue === nextValue) return;
|
||||
row.unsubscribed = nextValue;
|
||||
row.unsubscribe = nextValue;
|
||||
this.switchingExpertId = expertId;
|
||||
try {
|
||||
const apiUrl = nextValue === 1 ? 'api/expert_manage/unsubscribe' : 'api/expert_manage/resubscribe';
|
||||
const res = await this.$api.post(apiUrl, {
|
||||
expert_ids: String(expertId)
|
||||
});
|
||||
if (res && Number(res.code) === 0) {
|
||||
this.$message.success(this.$t('expertDatabase.unsubscribeUpdateSuccess'));
|
||||
} else {
|
||||
row.unsubscribed = oldValue;
|
||||
row.unsubscribe = oldValue;
|
||||
this.$message.error((res && res.msg) || this.$t('expertDatabase.unsubscribeUpdateFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
row.unsubscribed = oldValue;
|
||||
row.unsubscribe = oldValue;
|
||||
this.$message.error(this.$t('expertDatabase.unsubscribeUpdateFailed'));
|
||||
} finally {
|
||||
this.switchingExpertId = '';
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
this.query.pageIndex = 1;
|
||||
this.fetchList();
|
||||
@@ -228,6 +346,17 @@ export default {
|
||||
this.query.pageIndex = page;
|
||||
this.fetchList();
|
||||
},
|
||||
openFieldDetail(row) {
|
||||
const empty = this.$t('expertDatabase.detailCellEmpty');
|
||||
this.fieldDetailExpert = row || null;
|
||||
const fields = (row && row.fields) || [];
|
||||
this.fieldDetailRows = fields.map((f) => ({
|
||||
field: (f && f.field) || empty,
|
||||
paper_title: (f && (f.paper_title || f.title)) || empty,
|
||||
paper_journal: (f && (f.paper_journal || f.journal)) || empty
|
||||
}));
|
||||
this.fieldDetailVisible = true;
|
||||
},
|
||||
async handleExport() {
|
||||
if (!this.query.major_id && !this.query.keyword && !this.query.field) {
|
||||
this.$message.warning(this.$t('expertDatabase.exportWarn'));
|
||||
@@ -330,5 +459,37 @@ export default {
|
||||
.value.time {
|
||||
color: #888;
|
||||
}
|
||||
.view-all-btn {
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.field-detail-name {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.field-detail-empty {
|
||||
margin: 16px 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.unsubscribe-cell {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.unsubscribe-tag {
|
||||
min-width: 56px;
|
||||
height: 22px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.unsubscribe-switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -109,7 +109,14 @@
|
||||
<div class="mail-content-panel" v-if="selectedAccount" v-loading="detailLoading">
|
||||
<template v-if="activeMailId && !detailLoading">
|
||||
<div class="mail-page">
|
||||
<mail-detail v-if="detailMail" :mailData="detailMail" @close="closeDetail" />
|
||||
<mail-detail
|
||||
v-if="detailMail && String(detailMail.inbox_id || '') !== ''"
|
||||
:mailData="detailMail"
|
||||
:journal-id="mailDetailJournalId"
|
||||
:journal-title="mailDetailJournalTitle"
|
||||
:j-email-id="mailDetailJEmailId"
|
||||
@close="closeDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -168,9 +175,11 @@ const API = {
|
||||
getAllJournal: 'api/Article/getJournal'
|
||||
};
|
||||
import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
baseUrl: this.Common.baseUrl,
|
||||
currentFolder: 'inbox',
|
||||
searchKeyword: '',
|
||||
syncLoading: false,
|
||||
@@ -214,6 +223,39 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||
},
|
||||
selectedAccountEmail() {
|
||||
return this.selectedAccount ? this.selectedAccount.smtp_user : '';
|
||||
},
|
||||
/** 当前邮箱账号绑定的期刊,供邮件内一键建稿与 articleAdd 回填一致 */
|
||||
mailDetailJournalId() {
|
||||
const a = this.selectedAccount;
|
||||
if (a && a.journal_id != null && String(a.journal_id).trim() !== '') {
|
||||
return a.journal_id;
|
||||
}
|
||||
try {
|
||||
const s = localStorage.getItem('mailboxCollect_journal_id');
|
||||
return s != null && String(s).trim() !== '' ? s : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
mailDetailJournalTitle() {
|
||||
const id = this.mailDetailJournalId;
|
||||
if (id == null) return '';
|
||||
const list = this.journalList || [];
|
||||
const j = list.find((x) => String(x.journal_id) === String(id));
|
||||
return j && j.title ? j.title : '';
|
||||
},
|
||||
/** 当前发件/收件箱账号,供 sendOne 与 mailboxSend 一致 */
|
||||
mailDetailJEmailId() {
|
||||
const a = this.selectedAccount;
|
||||
if (a && a.j_email_id != null && String(a.j_email_id).trim() !== '') {
|
||||
return a.j_email_id;
|
||||
}
|
||||
try {
|
||||
const s = localStorage.getItem('mailboxCollect_j_email_id');
|
||||
return s != null && String(s).trim() !== '' ? s : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -292,8 +334,14 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||
this.$api
|
||||
.post(API.getOneEmail, { j_email_id: jEmailId })
|
||||
.then((res) => {
|
||||
const email = res && res.data ? res.data.email : null;
|
||||
if (res && res.code === 0 && email) {
|
||||
const raw = res && res.data != null ? res.data : null;
|
||||
const email =
|
||||
raw && typeof raw === 'object' && raw.email && typeof raw.email === 'object'
|
||||
? raw.email
|
||||
: raw && typeof raw === 'object' && (raw.j_email_id != null || raw.smtp_user)
|
||||
? raw
|
||||
: null;
|
||||
if (res && Number(res.code) === 0 && email) {
|
||||
this.selectedAccount = email;
|
||||
this.fetchData();
|
||||
this.startInboxSse();
|
||||
@@ -488,28 +536,103 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
this.currentFolder = f;
|
||||
this.activeMailId = null;
|
||||
},
|
||||
selectMail(item,index) {
|
||||
/** 接口 code 可能是数字 0 或字符串 "0" */
|
||||
isEmailApiSuccess(res) {
|
||||
if (!res || res.code === undefined || res.code === null) return false;
|
||||
return Number(res.code) === 0;
|
||||
},
|
||||
escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
},
|
||||
/** 解析 getEmailDetail 返回体,统一 content 字段;失败时用列表行兜底 */
|
||||
buildDetailMailFromResponse(res, item, inboxId) {
|
||||
const ok = this.isEmailApiSuccess(res);
|
||||
let d = res && res.data != null ? res.data : null;
|
||||
if (d && typeof d === 'string') {
|
||||
try {
|
||||
d = JSON.parse(d);
|
||||
} catch (e) {
|
||||
d = null;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(d) && d.length) {
|
||||
d = d[0];
|
||||
}
|
||||
if (ok && d && typeof d === 'object' && d.data && typeof d.data === 'object' && !d.content_html && !d.content_text) {
|
||||
d = { ...d, ...d.data };
|
||||
}
|
||||
if (ok && (!d || typeof d !== 'object') && res && (res.content_html != null || res.content_text != null || res.subject != null)) {
|
||||
d = { ...res };
|
||||
delete d.code;
|
||||
delete d.msg;
|
||||
if (Object.prototype.hasOwnProperty.call(d, 'data')) delete d.data;
|
||||
}
|
||||
const inboxKey = String(inboxId);
|
||||
const listSnippet = (item && item.content) || '';
|
||||
if (!ok || !d || typeof d !== 'object') {
|
||||
return {
|
||||
...item,
|
||||
subject: (item && item.subject) || '',
|
||||
from_name: (item && item.from_name) || '',
|
||||
from_email: (item && item.email) || '',
|
||||
email_date: item && item.email_date,
|
||||
content_html: listSnippet,
|
||||
inbox_id: inboxKey,
|
||||
attachments: [],
|
||||
has_attachment: (item && item.has_attachment) || 0
|
||||
};
|
||||
}
|
||||
const html = d.content_html || d.body_html || d.html || d.body || '';
|
||||
const text = d.content_text || '';
|
||||
let content_html =
|
||||
html ||
|
||||
(text ? `<pre class="mail-plain-pre">${this.escapeHtml(text)}</pre>` : '') ||
|
||||
listSnippet;
|
||||
if (content_html && typeof content_html === 'string' && /^<!DOCTYPE|^<\s*html[\s>]/i.test(content_html.trim())) {
|
||||
const normalized = normalizeEmailHtmlForInlineDisplay(content_html);
|
||||
if (normalized && normalized.trim()) content_html = normalized;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
...d,
|
||||
content_html,
|
||||
inbox_id: inboxKey,
|
||||
attachments: []
|
||||
};
|
||||
},
|
||||
selectMail(item, index) {
|
||||
this.activeMailId = item.id;
|
||||
this.detailLoading = true;
|
||||
this.detailMail = {};
|
||||
const inboxId = item.inbox_id || item.id;
|
||||
this.$api
|
||||
.post(API.getEmailDetail, { inbox_id: inboxId })
|
||||
.then((res) => {
|
||||
const d = res && res.data;
|
||||
if (res && res.code === 0 && d) {
|
||||
this.detailMail = { ...d, inbox_id: String(inboxId), attachments: [] };
|
||||
item.state = 0;
|
||||
this.displayList[index].is_read = d.is_read;
|
||||
if (Number(d.has_attachment) === 1) {
|
||||
this.fetchAttachments(String(inboxId));
|
||||
} else {
|
||||
this.detailLoading = false;
|
||||
try {
|
||||
const merged = this.buildDetailMailFromResponse(res, item, inboxId);
|
||||
this.detailMail = merged;
|
||||
if (item) item.state = 0;
|
||||
const row = this.displayList[index];
|
||||
if (row && merged && merged.is_read !== undefined && merged.is_read !== null) {
|
||||
this.$set(row, 'is_read', merged.is_read);
|
||||
}
|
||||
} else {
|
||||
this.detailLoading = false;
|
||||
if (Number(merged.has_attachment) === 1) {
|
||||
this.fetchAttachments(String(inboxId));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
|
||||
}
|
||||
this.detailLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
|
||||
this.detailLoading = false;
|
||||
});
|
||||
},
|
||||
@@ -645,7 +768,7 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
},
|
||||
buildSseUrl(jEmailId) {
|
||||
// 与现有 axios baseURL=/api + 相对路径 规则一致;GET + query 传参
|
||||
const base = `/api/${API.inboxSse}`;
|
||||
const base = `${this.baseUrl}${API.inboxSse}`;
|
||||
const q = new URLSearchParams({ j_email_id: String(jEmailId) }).toString();
|
||||
return `${base}?${q}`;
|
||||
},
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
</el-button>
|
||||
|
||||
<div class="right-actions">
|
||||
<el-button type="warning" plain icon="el-icon-upload2" @click="openTemplateBatchImportDialog">{{
|
||||
$t('mailboxMould.batchImportBtn')
|
||||
}}</el-button>
|
||||
<el-button type="primary" plain icon="el-icon-plus" @click="handleCreateTemplate">{{ $t('mailboxMould.createTemplate') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,6 +139,36 @@
|
||||
<el-button @click="previewVisible = false">{{ $t('mailboxMould.previewClose') }}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('mailboxMould.batchImportTitle')"
|
||||
:visible.sync="batchTplImportVisible"
|
||||
width="720px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
custom-class="mailbox-mould-batch-import-dialog"
|
||||
@closed="batchTplImporting = false"
|
||||
>
|
||||
<p class="batch-tpl-hint">{{ $t('mailboxMould.batchImportHint') }}</p>
|
||||
<p class="batch-tpl-tip">{{ $t('mailboxMould.batchImportCommonTip') }}</p>
|
||||
<div class="batch-tpl-journal-row">
|
||||
<span class="batch-tpl-label">{{ $t('mailboxMould.batchImportJournalId') }}</span>
|
||||
<el-input
|
||||
v-model="batchTplImportJournalId"
|
||||
clearable
|
||||
size="small"
|
||||
:placeholder="$t('mailboxMould.batchImportJournalPlaceholder')"
|
||||
class="batch-tpl-journal-input"
|
||||
/>
|
||||
</div>
|
||||
<el-input v-model="batchTplImportText" type="textarea" :rows="14" class="batch-tpl-textarea" spellcheck="false" />
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button size="small" @click="batchTplImportVisible = false">{{ $t('mailboxMould.cancel') }}</el-button>
|
||||
<el-button type="primary" size="small" :loading="batchTplImporting" @click="runTemplateBatchImport">{{
|
||||
$t('mailboxMould.batchImportRun')
|
||||
}}</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -145,7 +178,14 @@ const API = {
|
||||
listStyles: 'api/mail_template/listStyles',
|
||||
getAllJournal: 'api/Article/getJournal',
|
||||
deleteTemplate: 'api/mail_template/deleteTemplate',
|
||||
deleteStyle: 'api/mail_template/deleteStyle'
|
||||
deleteStyle: 'api/mail_template/deleteStyle',
|
||||
saveTemplate: 'api/mail_template/saveTemplate'
|
||||
};
|
||||
// 仅在当前 SPA 会话内记忆筛选(刷新页面即重置)
|
||||
const mailboxMouldSessionMemory = {
|
||||
journalId: '',
|
||||
scene: '',
|
||||
language: ''
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -171,7 +211,13 @@ export default {
|
||||
|
||||
// --- 共用预览 ---
|
||||
previewVisible: false,
|
||||
previewContent: ''
|
||||
previewContent: '',
|
||||
|
||||
/** 邮件模板 JSON 批量导入(期刊 ID 单独输入,与每条合并后调 saveTemplate) */
|
||||
batchTplImportVisible: false,
|
||||
batchTplImportText: '',
|
||||
batchTplImportJournalId: '',
|
||||
batchTplImporting: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -203,8 +249,15 @@ export default {
|
||||
}));
|
||||
this.journalList = mapped;
|
||||
if (mapped.length > 0) {
|
||||
this.tplFilters.journalId = String(mapped[0].journal_id);
|
||||
const rememberedJournalId = String(mailboxMouldSessionMemory.journalId || '').trim();
|
||||
const hasRemembered = rememberedJournalId && mapped.some(j => String(j.journal_id) === rememberedJournalId);
|
||||
this.tplFilters.journalId = hasRemembered
|
||||
? rememberedJournalId
|
||||
: String(mapped[0].journal_id);
|
||||
}
|
||||
this.tplFilters.scene = String(mailboxMouldSessionMemory.scene || '');
|
||||
this.tplFilters.language = String(mailboxMouldSessionMemory.language || '');
|
||||
this.syncTplFilterMemory();
|
||||
if (this.activeTab === 'styles') {
|
||||
this.fetchStyles();
|
||||
} else {
|
||||
@@ -226,6 +279,11 @@ export default {
|
||||
},
|
||||
|
||||
// ========== Templates ==========
|
||||
syncTplFilterMemory() {
|
||||
mailboxMouldSessionMemory.journalId = String((this.tplFilters && this.tplFilters.journalId) || '');
|
||||
mailboxMouldSessionMemory.scene = String((this.tplFilters && this.tplFilters.scene) || '');
|
||||
mailboxMouldSessionMemory.language = String((this.tplFilters && this.tplFilters.language) || '');
|
||||
},
|
||||
fetchTemplates() {
|
||||
this.tplLoading = true;
|
||||
const params = {
|
||||
@@ -233,6 +291,7 @@ export default {
|
||||
scene: this.tplFilters.scene || '',
|
||||
language: this.tplFilters.language || ''
|
||||
};
|
||||
this.syncTplFilterMemory();
|
||||
this.$api
|
||||
.post(API.listTemplates, params)
|
||||
.then(res => {
|
||||
@@ -257,11 +316,154 @@ export default {
|
||||
});
|
||||
},
|
||||
handleCreateTemplate() {
|
||||
this.syncTplFilterMemory();
|
||||
// 传入当前模板列表选中的期刊,详情页用于默认回填
|
||||
const journalId = this.tplFilters && this.tplFilters.journalId ? String(this.tplFilters.journalId) : '';
|
||||
this.$router.push({ path: '/mailboxMouldDetail', query: journalId ? { journal_id: journalId } : {} });
|
||||
},
|
||||
openTemplateBatchImportDialog() {
|
||||
const cur = String((this.tplFilters && this.tplFilters.journalId) || '').trim();
|
||||
if (cur) this.batchTplImportJournalId = cur;
|
||||
if (!this.batchTplImportText || !String(this.batchTplImportText).trim()) {
|
||||
this.batchTplImportText = this.defaultTemplateBatchImportSample();
|
||||
}
|
||||
this.batchTplImportVisible = true;
|
||||
},
|
||||
defaultTemplateBatchImportSample() {
|
||||
return (
|
||||
'[\n' +
|
||||
' {\n' +
|
||||
' "title": "示例标题",\n' +
|
||||
' "subject": "示例主题",\n' +
|
||||
' "scene": "invite_submission",\n' +
|
||||
' "language": "en",\n' +
|
||||
' "version": "1.0.0",\n' +
|
||||
' "body_html": "<p>正文 HTML</p>",\n' +
|
||||
' "variables_json": "",\n' +
|
||||
' "is_active": 1\n' +
|
||||
' }\n' +
|
||||
']\n'
|
||||
);
|
||||
},
|
||||
/** 与 mailboxMouldDetail._doSave 提交 saveTemplate 字段对齐;支持 body、lang、variables 别名 */
|
||||
normalizeMailTemplateSavePayload(row) {
|
||||
if (!row || typeof row !== 'object') return {};
|
||||
const bodyHtml =
|
||||
row.body_html != null
|
||||
? String(row.body_html)
|
||||
: row.body != null
|
||||
? String(row.body)
|
||||
: '';
|
||||
const bodyTextRaw = row.body_text != null ? String(row.body_text) : '';
|
||||
const bodyText =
|
||||
bodyTextRaw.trim() ||
|
||||
bodyHtml
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const lang = String(row.language != null ? row.language : row.lang != null ? row.lang : 'en').toLowerCase();
|
||||
let isActive = '1';
|
||||
if (row.is_active === 0 || row.is_active === '0' || row.is_active === false) isActive = '0';
|
||||
const out = {
|
||||
journal_id: String(row.journal_id != null ? row.journal_id : row.journalId != null ? row.journalId : '').trim(),
|
||||
scene: String(row.scene || 'invite_submission'),
|
||||
language: lang,
|
||||
title: String(row.title || ''),
|
||||
subject: String(row.subject || ''),
|
||||
body_html: bodyHtml,
|
||||
body_text: bodyText,
|
||||
variables_json: String(
|
||||
row.variables_json != null ? row.variables_json : row.variables != null ? row.variables : ''
|
||||
),
|
||||
version: String(row.version != null ? row.version : '1.0.0'),
|
||||
is_active: isActive
|
||||
};
|
||||
const tid = row.template_id != null ? row.template_id : row.id;
|
||||
if (tid != null && String(tid).trim() !== '') out.template_id = String(tid).trim();
|
||||
return out;
|
||||
},
|
||||
applyTemplateBatchImportJournal(payload) {
|
||||
const jid = String(this.batchTplImportJournalId || '').trim();
|
||||
if (jid) payload.journal_id = jid;
|
||||
},
|
||||
validateMailTemplateSavePayload(p, index) {
|
||||
if (!p.journal_id || !String(p.journal_id).trim()) {
|
||||
return this.$t('mailboxMould.batchImportMissingJournal', { index: index + 1 });
|
||||
}
|
||||
if (!p.title || !String(p.title).trim()) {
|
||||
return this.$t('mailboxMould.batchImportMissingField', { index: index + 1, field: 'title' });
|
||||
}
|
||||
if (!p.subject || !String(p.subject).trim()) {
|
||||
return this.$t('mailboxMould.batchImportMissingField', { index: index + 1, field: 'subject' });
|
||||
}
|
||||
if (!p.body_html || !String(p.body_html).trim()) {
|
||||
return this.$t('mailboxMould.batchImportMissingField', { index: index + 1, field: 'body_html' });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
async runTemplateBatchImport() {
|
||||
let rows;
|
||||
try {
|
||||
rows = JSON.parse(this.batchTplImportText || '[]');
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('mailboxMould.batchImportBadJson'));
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(rows) || !rows.length) {
|
||||
this.$message.warning(this.$t('mailboxMould.batchImportEmpty'));
|
||||
return;
|
||||
}
|
||||
this.batchTplImporting = true;
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
const errLines = [];
|
||||
try {
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const payload = this.normalizeMailTemplateSavePayload(rows[i]);
|
||||
this.applyTemplateBatchImportJournal(payload);
|
||||
const ve = this.validateMailTemplateSavePayload(payload, i);
|
||||
if (ve) {
|
||||
fail++;
|
||||
errLines.push(ve);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const res = await this.$api.post(API.saveTemplate, payload);
|
||||
if (res && res.code === 0) {
|
||||
ok++;
|
||||
} else {
|
||||
fail++;
|
||||
errLines.push(
|
||||
this.$t('mailboxMould.batchImportRowFail', {
|
||||
index: i + 1,
|
||||
msg: (res && res.msg) || this.$t('mailboxMould.batchImportSaveFail')
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
fail++;
|
||||
errLines.push(this.$t('mailboxMould.batchImportRowNetwork', { index: i + 1 }));
|
||||
}
|
||||
}
|
||||
this.$message.success(this.$t('mailboxMould.batchImportDone', { ok, fail }));
|
||||
if (errLines.length) {
|
||||
this.$notify({
|
||||
title: this.$t('mailboxMould.batchImportErrorsTitle'),
|
||||
message: errLines.slice(0, 8).join('\n'),
|
||||
type: fail && !ok ? 'error' : 'warning',
|
||||
duration: 12000
|
||||
});
|
||||
}
|
||||
this.batchTplImportVisible = false;
|
||||
this.syncTplFilterMemory();
|
||||
this.fetchTemplates();
|
||||
} finally {
|
||||
this.batchTplImporting = false;
|
||||
}
|
||||
},
|
||||
handleEditTemplate(row) {
|
||||
this.syncTplFilterMemory();
|
||||
const templateId = row && (row.template_id || row.id);
|
||||
const journalId = this.tplFilters && this.tplFilters.journalId ? String(this.tplFilters.journalId) : '';
|
||||
const query = templateId ? { template_id: String(templateId) } : {};
|
||||
@@ -274,6 +476,7 @@ export default {
|
||||
this.previewVisible = true;
|
||||
},
|
||||
handleDeleteTemplate(row) {
|
||||
this.syncTplFilterMemory();
|
||||
const templateId = row && (row.template_id || row.id);
|
||||
if (!templateId) return;
|
||||
this.$confirm(this.$t('mailboxMould.deleteConfirm'), this.$t('mailboxMould.colActions'), {
|
||||
@@ -397,4 +600,39 @@ export default {
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.batch-tpl-hint {
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: #606266;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.batch-tpl-tip {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #909399;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.batch-tpl-journal-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.batch-tpl-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
min-width: 72px;
|
||||
}
|
||||
.batch-tpl-journal-input {
|
||||
flex: 1;
|
||||
max-width: 360px;
|
||||
}
|
||||
.mailbox-mould-batch-import-dialog .batch-tpl-textarea >>> textarea {
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -100,9 +100,10 @@
|
||||
v-model="form.body"
|
||||
:journalList="journalList"
|
||||
:journalId="form.journalId"
|
||||
:language="form.lang"
|
||||
placeholder=""
|
||||
/>
|
||||
<!-- <CkeditorMail v-model="form.body" /> -->
|
||||
<CkeditorMail v-model="form.body" />
|
||||
</div>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
@@ -424,6 +424,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -435,8 +436,7 @@ export default {
|
||||
baseUrl: this.Common.baseUrl,
|
||||
mediaUrl: this.Common.mediaUrl,
|
||||
query: {
|
||||
// journal_id: '',
|
||||
// year: 0,
|
||||
journal_id: 0,
|
||||
keywords: '',
|
||||
page: 1,
|
||||
limit: 15
|
||||
@@ -472,6 +472,23 @@ export default {
|
||||
this.initMajor();
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump');
|
||||
if (!raw) return '';
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
if (!obj || obj.from !== 'promotionFactory') return '';
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return '';
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : '';
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
return id;
|
||||
} catch (e) {
|
||||
try {
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
} catch (e2) {}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
goDetail(row) {
|
||||
console.log('row at line 460:', row);
|
||||
this.$router.push({
|
||||
@@ -518,8 +535,31 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.jourList = res.data.journals;
|
||||
this.query.journal_id = this.jourList[0].journal_id;
|
||||
const fromQueryId = this.$route && this.$route.query ? this.$route.query.journal_id : '';
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId();
|
||||
const pickedId = fromQueryId || fromSessionId;
|
||||
const matched = pickedId ? this.jourList.find((j) => String(j.journal_id) === String(pickedId)) : null;
|
||||
this.query.journal_id = matched ? matched.journal_id : this.jourList[0].journal_id;
|
||||
this.getData();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : '';
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {});
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery._ap_from;
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true });
|
||||
}
|
||||
this.$router.replace({ query: nextQuery }).catch(() => {});
|
||||
// 兜底:某些场景 router.replace 不更新地址栏,这里强制清掉 query
|
||||
try {
|
||||
const u = new URL(window.location.href);
|
||||
['_ap_from', 'journal_id', 'journalId', 'journalIdd'].forEach((k) => u.searchParams.delete(k));
|
||||
window.history.replaceState({}, '', u.toString());
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -478,6 +478,7 @@ const currentYear = new Date().getFullYear();
|
||||
var ReviewTime = `(${currentYear - 2}–${currentYear})`;
|
||||
import commonReviewer from '../page/components/reviewerList/add.vue';
|
||||
import commonMajorTableList from '../page/components/major/tableList.vue';
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
components: {
|
||||
commonReviewer,
|
||||
@@ -657,11 +658,27 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.getDate();
|
||||
this.getContent();
|
||||
this.loadFields();
|
||||
// this.initMajor()
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump');
|
||||
if (!raw) return '';
|
||||
const obj = JSON.parse(raw || '{}');
|
||||
if (!obj || obj.from !== 'promotionFactory') return '';
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return '';
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : '';
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
return id;
|
||||
} catch (e) {
|
||||
try {
|
||||
sessionStorage.removeItem('promotionFactoryJump');
|
||||
} catch (e2) {}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
getProps() {
|
||||
return {
|
||||
value: 'value',
|
||||
@@ -716,6 +733,32 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.code == 0) {
|
||||
this.df_jour = res.data.journals;
|
||||
const fromQueryId = (this.$route && this.$route.query && (this.$route.query.journalId || this.$route.query.journal_id)) || '';
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId();
|
||||
const pickedId = fromQueryId || fromSessionId;
|
||||
if (pickedId) {
|
||||
this.query.journalId = Number(pickedId) || 0;
|
||||
}
|
||||
this.getContent();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : '';
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {});
|
||||
delete nextQuery.journal_id;
|
||||
delete nextQuery.journalId;
|
||||
delete nextQuery._ap_from;
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true });
|
||||
}
|
||||
this.$router.replace({ query: nextQuery }).catch(() => {});
|
||||
// 兜底:某些场景 router.replace 不更新地址栏,这里强制清掉 query
|
||||
try {
|
||||
const u = new URL(window.location.href);
|
||||
['_ap_from', 'journal_id', 'journalId', 'journalIdd'].forEach((k) => u.searchParams.delete(k));
|
||||
window.history.replaceState({}, '', u.toString());
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
|
||||
<script>
|
||||
import commonCv from '../common/cv.vue';
|
||||
import bus from '../common/bus';
|
||||
export default {
|
||||
components: {
|
||||
commonCv
|
||||
@@ -362,6 +363,7 @@ export default {
|
||||
for (var i = 0; i < this.tableData.length; i++) {
|
||||
this.getScoreData(i, this.tableData[i].score);
|
||||
}
|
||||
bus.$emit('apply-badge-refresh');
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../common/bus'
|
||||
export default {
|
||||
name: 'youthList',
|
||||
data() {
|
||||
@@ -326,6 +327,21 @@
|
||||
this.yearData()
|
||||
},
|
||||
methods: {
|
||||
readPromotionFactoryJumpJournalId() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('promotionFactoryJump')
|
||||
if (!raw) return ''
|
||||
const obj = JSON.parse(raw || '{}')
|
||||
if (!obj || obj.from !== 'promotionFactory') return ''
|
||||
if (obj.targetPath && String(obj.targetPath) !== String(this.$route.path)) return ''
|
||||
const id = obj.journal_id != null ? String(obj.journal_id) : ''
|
||||
sessionStorage.removeItem('promotionFactoryJump')
|
||||
return id
|
||||
} catch (e) {
|
||||
try { sessionStorage.removeItem('promotionFactoryJump') } catch (e2) {}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
// 获取当年时间
|
||||
yearData() {
|
||||
this.YearThis = new Date().getFullYear()
|
||||
@@ -347,8 +363,33 @@
|
||||
.then(res => {
|
||||
if (res.code == 0) {
|
||||
this.df_jour = res.data.journals;
|
||||
this.query.journal_id = this.df_jour[0].journal_id
|
||||
const fromQueryId =
|
||||
(this.$route && this.$route.query && (this.$route.query.journal_id || this.$route.query.journalId)) || ''
|
||||
const fromSessionId = this.readPromotionFactoryJumpJournalId()
|
||||
const pickedId = fromQueryId || fromSessionId
|
||||
const matched = pickedId ? this.df_jour.find(j => String(j.journal_id) === String(pickedId)) : null
|
||||
this.query.journal_id = matched ? matched.journal_id : this.df_jour[0].journal_id
|
||||
this.getDate();
|
||||
|
||||
// 仅首次接收参数:应用后立刻清掉 URL,避免刷新仍然固定某一本期刊
|
||||
if (fromQueryId) {
|
||||
const oldFullPath = this.$route && this.$route.fullPath ? this.$route.fullPath : ''
|
||||
const nextQuery = Object.assign({}, (this.$route && this.$route.query) || {})
|
||||
delete nextQuery.journal_id
|
||||
delete nextQuery.journalId
|
||||
delete nextQuery.journalIdd
|
||||
delete nextQuery._ap_from
|
||||
if (oldFullPath) {
|
||||
bus.$emit('close_tag_by_path', { path: oldFullPath, silent: true })
|
||||
}
|
||||
this.$router.replace({ path: this.$route.path, query: nextQuery }).catch(() => {})
|
||||
// 兜底:某些场景 router.replace 不更新地址栏,这里强制清掉 query
|
||||
try {
|
||||
const u = new URL(window.location.href)
|
||||
;['_ap_from', 'journal_id', 'journalId', 'journalIdd'].forEach(k => u.searchParams.delete(k))
|
||||
window.history.replaceState({}, '', u.toString())
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ const i18n = new VueI18n({
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const currentRoute = to; // 获取当前路由路径,例如 "/home"
|
||||
|
||||
if (currentRoute.meta.hideJournal) {
|
||||
if (currentRoute.meta.hideJournal || currentRoute.meta.public) {
|
||||
|
||||
} else {
|
||||
try {
|
||||
@@ -239,11 +239,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
|
||||
// 无论接口成功/失败,都执行原有跳转逻辑
|
||||
document.title = `${to.meta.title} | Traditional Medicine Research`;
|
||||
document.title = `${to.meta.title || 'TMR'} | Traditional Medicine Research`;
|
||||
const role = localStorage.getItem('U_name');
|
||||
const userrole = localStorage.getItem('U_status');
|
||||
|
||||
if (!role && to.path != '/register' && to.path !== '/submission' && to.path !== '/verification' && to.path !== '/orcidLink' && to.path !== '/img' && to.path !== '/reviewer' && to.path !== '/thanks' && to.path !== '/login' && to.path !== '/refuse' && to.path !== '/managing' && to.path.search(/retrieve/i) < 0) {
|
||||
if (!role && to.meta.public !== true && to.path != '/register' && to.path !== '/submission' && to.path !== '/verification' && to.path !== '/orcidLink' && to.path !== '/img' && to.path !== '/reviewer' && to.path !== '/thanks' && to.path !== '/login' && to.path !== '/refuse' && to.path !== '/managing' && to.path.search(/retrieve/i) < 0) {
|
||||
next('/login');
|
||||
} else {
|
||||
if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') {
|
||||
|
||||
@@ -1115,7 +1115,16 @@ export default new Router({
|
||||
path: '/expertDatabase', //专家库
|
||||
component: () => import('../components/page/expertDatabase'),
|
||||
meta: {
|
||||
title: 'Expert Database'
|
||||
title: 'Expert Database',
|
||||
titleKey: 'sidebar.expertList'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/countryManagement', // 专家库-国家信息维护
|
||||
component: () => import('../components/page/countryManagement'),
|
||||
meta: {
|
||||
title: 'Country Management',
|
||||
titleKey: 'sidebar.countryManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1468,6 +1477,24 @@ export default new Router({
|
||||
title: 'img'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/youthBoardRegister',
|
||||
component: () => import( /* webpackChunkName: "youthBoardRegister" */ '../components/page/YouthEditorialBoardRegistration.vue'),
|
||||
meta: {
|
||||
title: 'Youth Editorial Board Registration',
|
||||
public: true,
|
||||
hideJournal: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/youthBoardSubmitSuccess',
|
||||
component: () => import( /* webpackChunkName: "youthBoardSubmitSuccess" */ '../components/page/YouthBoardSubmitSuccess.vue'),
|
||||
meta: {
|
||||
title: 'Submission Success',
|
||||
public: true,
|
||||
hideJournal: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
redirect: '/404'
|
||||
|
||||
26
src/utils/emailHtmlView.js
Normal file
26
src/utils/emailHtmlView.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 将完整 HTML 邮件转为适合 div[v-html] 的片段:
|
||||
* 取 body.innerHTML,并前置 head 内 <style>,避免版式与改版前不一致。
|
||||
*/
|
||||
export function normalizeEmailHtmlForInlineDisplay(html) {
|
||||
if (!html || typeof html !== 'string') return '';
|
||||
const t = html.trim();
|
||||
if (!/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) return html;
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const body = doc && doc.body;
|
||||
if (!body) return html;
|
||||
const inner = body.innerHTML;
|
||||
if (!inner || !inner.trim()) return html;
|
||||
let headInject = '';
|
||||
const head = doc.head;
|
||||
if (head) {
|
||||
head.querySelectorAll('style').forEach((node) => {
|
||||
headInject += node.outerHTML;
|
||||
});
|
||||
}
|
||||
return headInject + inner;
|
||||
} catch (e) {
|
||||
return html;
|
||||
}
|
||||
}
|
||||
15
src/utils/ithenticateSimilarityStyle.js
Normal file
15
src/utils/ithenticateSimilarityStyle.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 基于 iThenticate 标准的颜色映射(与编辑端常见分档一致)
|
||||
* @param {number|string} score 相似度百分比(可为小数,内部会规范化)
|
||||
* @returns {{ color: string, label: string }}
|
||||
*/
|
||||
export function getSimilarityStyle(score) {
|
||||
let n = Number(score);
|
||||
if (!Number.isFinite(n) || n < 0) n = 0;
|
||||
const s = Math.round(n);
|
||||
if (s === 0) return { color: '#0000FF', label: 'Blue' };
|
||||
if (s <= 24) return { color: '#008000', label: 'Green' };
|
||||
if (s <= 49) return { color: '#EDBD3E', label: 'Yellow' };
|
||||
if (s <= 74) return { color: '#FF8C00', label: 'Orange' };
|
||||
return { color: '#FF0000', label: 'Red' };
|
||||
}
|
||||
381
src/utils/mailManuscriptAutoSubmit.js
Normal file
381
src/utils/mailManuscriptAutoSubmit.js
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* 邮件 Word → checkLogin(作者)→ multipart manuscirpt → contribute → 回填。
|
||||
* 默认 preserveEditorLocalStorage:不覆盖 U_*,作者 user_id 仅用接口返回值;作者账号写入 sessionStorage 备用键。
|
||||
* 服务端会话在作者 checkLogin 后仍为作者,需用编辑密码再次 checkLogin 恢复(见 MailDetail)。
|
||||
*/
|
||||
import axios from 'axios';
|
||||
|
||||
const LS_KEYS = ['U_status', 'U_role', 'U_name', 'U_id', 'U_email', 'U_relname'];
|
||||
|
||||
export function backupEditorLocalStorage() {
|
||||
const b = {};
|
||||
LS_KEYS.forEach((k) => {
|
||||
b[k] = localStorage.getItem(k);
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
export function restoreLocalStorage(backup) {
|
||||
if (!backup) return;
|
||||
LS_KEYS.forEach((k) => {
|
||||
if (backup[k] != null) localStorage.setItem(k, backup[k]);
|
||||
else localStorage.removeItem(k);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} api - this.$api
|
||||
* @param {{ username: string, password: string, code?: string, baseUrl?: string }} login
|
||||
*/
|
||||
export async function postCheckLogin(api, login) {
|
||||
const baseUrl = login.baseUrl != null ? login.baseUrl : '/api';
|
||||
const random_num = Math.random();
|
||||
const image = `${baseUrl}api/User/retrieveCaptcha?a=${random_num}`;
|
||||
return api.post('api/User/checkLogin', {
|
||||
username: login.username,
|
||||
password: login.password,
|
||||
random_num,
|
||||
image,
|
||||
code: login.code != null && login.code !== '' ? login.code : ''
|
||||
});
|
||||
}
|
||||
|
||||
/** 校验作者 checkLogin 响应,不写 localStorage */
|
||||
export function validateAuthorLoginResponse(res) {
|
||||
if (!res || Number(res.code) === 1) {
|
||||
return { ok: false, msg: (res && res.msg) || 'Login failed' };
|
||||
}
|
||||
const roles = res.data && res.data.roles;
|
||||
const userinfo = res.data && res.data.userinfo;
|
||||
if (!userinfo) {
|
||||
return { ok: false, msg: 'Invalid login response' };
|
||||
}
|
||||
if (roles && String(roles).includes('editor')) {
|
||||
return { ok: false, msg: 'Editor accounts cannot use manuscript auto-submit in this flow' };
|
||||
}
|
||||
return { ok: true, userinfo };
|
||||
}
|
||||
|
||||
const SHADOW_AUTHOR_ID = 'mailManuscript_author_id';
|
||||
const SHADOW_AUTHOR_ACCOUNT = 'mailManuscript_author_account';
|
||||
|
||||
export function persistAuthorShadowSession(userinfo) {
|
||||
if (!userinfo || typeof sessionStorage === 'undefined') return;
|
||||
try {
|
||||
sessionStorage.setItem(SHADOW_AUTHOR_ID, String(userinfo.user_id));
|
||||
sessionStorage.setItem(SHADOW_AUTHOR_ACCOUNT, String(userinfo.account || ''));
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuthorShadowSession() {
|
||||
if (typeof sessionStorage === 'undefined') return;
|
||||
try {
|
||||
sessionStorage.removeItem(SHADOW_AUTHOR_ID);
|
||||
sessionStorage.removeItem(SHADOW_AUTHOR_ACCOUNT);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function applyAuthorSessionFromLoginResponse(res) {
|
||||
const v = validateAuthorLoginResponse(res);
|
||||
if (!v.ok) {
|
||||
return { ok: false, msg: v.msg };
|
||||
}
|
||||
const { userinfo } = v;
|
||||
const roles = res.data && res.data.roles;
|
||||
localStorage.setItem('U_status', '2');
|
||||
localStorage.setItem('U_role', roles != null && roles !== '' ? roles : '');
|
||||
localStorage.setItem('U_name', userinfo.account);
|
||||
localStorage.setItem('U_id', userinfo.user_id);
|
||||
localStorage.setItem('U_relname', userinfo.realname || '');
|
||||
localStorage.setItem('U_email', userinfo.email || '');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** 与 Login.vue 编辑账号成功分支一致,用于一键建稿后恢复编辑服务端会话 */
|
||||
export function applyEditorSessionFromLoginResponse(res) {
|
||||
if (!res || Number(res.code) === 1) {
|
||||
return { ok: false, msg: (res && res.msg) || 'Login failed' };
|
||||
}
|
||||
const roles = res.data && res.data.roles;
|
||||
const userinfo = res.data && res.data.userinfo;
|
||||
if (!userinfo) {
|
||||
return { ok: false, msg: 'Invalid login response' };
|
||||
}
|
||||
const roleStr = Array.isArray(roles) ? roles.join(',') : String(roles || '');
|
||||
if (!roleStr.includes('editor')) {
|
||||
return { ok: false, msg: 'Not an editor account' };
|
||||
}
|
||||
localStorage.setItem('U_status', '1');
|
||||
localStorage.setItem('U_role', roles != null && roles !== '' ? roles : '');
|
||||
localStorage.setItem('U_name', userinfo.account);
|
||||
localStorage.setItem('U_id', userinfo.user_id);
|
||||
localStorage.setItem('U_email', userinfo.email || '');
|
||||
if (userinfo.realname) {
|
||||
localStorage.setItem('U_relname', userinfo.realname);
|
||||
} else {
|
||||
localStorage.removeItem('U_relname');
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** 与 articleAdd.upload_manuscirpt:`baseUrl + 'api/Article/up_file/type/manuscirpt'` */
|
||||
export async function uploadManuscriptDocxMultipart(baseUrl, blob, filename) {
|
||||
const url = `${baseUrl}api/Article/up_file/type/manuscirpt`;
|
||||
const fd = new FormData();
|
||||
let name = filename || 'manuscript.docx';
|
||||
if (!String(name).toLowerCase().endsWith('.docx')) {
|
||||
name = `${name}.docx`;
|
||||
}
|
||||
fd.append('manuscirpt', blob, name);
|
||||
const { data } = await axios.post(url, fd, {
|
||||
withCredentials: true,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function postContribute(api, { upurl, user_id, article_id }) {
|
||||
const payload = {
|
||||
file_url: '/public/manuscirpt/' + upurl,
|
||||
user_id: String(user_id)
|
||||
};
|
||||
if (article_id != null && article_id !== '') {
|
||||
payload.article_id = String(article_id);
|
||||
}
|
||||
return api.post('api/Contribute/contribute', payload);
|
||||
}
|
||||
|
||||
function buildKeywordsString(article) {
|
||||
if (!article || article.keywords == null || String(article.keywords).trim() === '') {
|
||||
return '';
|
||||
}
|
||||
return String(article.keywords)
|
||||
.split(',')
|
||||
.map((k) => k.trim())
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
/** 与 articleAdd.addWordTablesList → api/Article/addArticleTable */
|
||||
export async function postAddArticleTableFromWordTables(api, article_id, wordTables) {
|
||||
if (!wordTables || !wordTables.length) return { code: 0 };
|
||||
const data = {
|
||||
article_id,
|
||||
list: wordTables.map((e) => ({
|
||||
table: JSON.stringify([...e]),
|
||||
type: 0,
|
||||
html_data: ''
|
||||
}))
|
||||
};
|
||||
return api.post('api/Article/addArticleTable', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} api
|
||||
* @param {{ user_id: string, article_id: string, journal_id: number, article: object, upurl: string, staging_username?: string, extractWordTables?: (consumer: (tables: any[]) => Promise<void>) => Promise<void> }} opts
|
||||
*/
|
||||
export async function runPostContributeArticleAddBackfill(api, opts) {
|
||||
const { user_id, article_id, journal_id, article, upurl, extractWordTables, staging_username } = opts;
|
||||
const relPath = 'manuscirpt/' + upurl;
|
||||
const jid = journal_id != null && String(journal_id).trim() !== '' ? Number(journal_id) : NaN;
|
||||
const hasJournal = !Number.isNaN(jid) && jid > 0;
|
||||
|
||||
if (hasJournal) {
|
||||
const cj = await api.post('api/Article/changeJournal', {
|
||||
article_id,
|
||||
journal_id: jid
|
||||
});
|
||||
if (cj && Number(cj.code) !== 0) {
|
||||
return { ok: false, step: 'changeJournal', msg: (cj && cj.msg) || 'changeJournal failed' };
|
||||
}
|
||||
}
|
||||
|
||||
const keyWords = buildKeywordsString(article);
|
||||
const stagingPayload = {
|
||||
article_id,
|
||||
journal: hasJournal ? jid : article.journal_id != null ? article.journal_id : '',
|
||||
title: (article && article.title) || '',
|
||||
keywords: keyWords,
|
||||
abstrart: (article && article.abstrart) || '',
|
||||
type: article && article.type != null ? article.type : '',
|
||||
username:
|
||||
staging_username != null && String(staging_username).trim() !== ''
|
||||
? String(staging_username).trim()
|
||||
: (typeof localStorage !== 'undefined' && localStorage.getItem('U_name')) || '',
|
||||
user_id,
|
||||
approval: '',
|
||||
approval_file: '',
|
||||
approval_content: '',
|
||||
fund: (article && article.fund) || ''
|
||||
};
|
||||
|
||||
const st = await api.post('api/Article/addArticleStaging', stagingPayload);
|
||||
if (!st || Number(st.status) !== 1) {
|
||||
return {
|
||||
ok: false,
|
||||
step: 'addArticleStaging',
|
||||
msg: (st && st.msg) || 'addArticleStaging failed'
|
||||
};
|
||||
}
|
||||
|
||||
const af = await api.post('api/Article/addArticlefile', {
|
||||
article_id,
|
||||
type: 'manuscirpt',
|
||||
url: relPath
|
||||
});
|
||||
if (!af || Number(af.code) !== 0) {
|
||||
return { ok: false, step: 'addArticlefile', msg: (af && af.msg) || 'addArticlefile failed' };
|
||||
}
|
||||
|
||||
await api.post('api/Article/reloadArticleImages', { article_id });
|
||||
await api.post('api/Article/reloadArticleTable', { article_id });
|
||||
|
||||
if (typeof extractWordTables === 'function') {
|
||||
try {
|
||||
await extractWordTables(async (tables) => {
|
||||
if (tables && tables.length > 0) {
|
||||
await postAddArticleTableFromWordTables(api, article_id, tables);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
step: 'addArticleTable',
|
||||
msg: (e && e.message) || String(e)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await api.post('api/Article/getAuthors', { article_id });
|
||||
await api.post('api/Article/getArticleState', { article_id, user_id });
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {*} ctx.api
|
||||
* @param {string} ctx.baseUrl
|
||||
* @param {object} ctx.login
|
||||
* @param {Blob} ctx.blob
|
||||
* @param {string} ctx.fileName
|
||||
* @param {number|string} [ctx.journal_id] — 当前邮件账号期刊(必填,与 mailbox 一致)
|
||||
* @param {(consumer: (tables: any[]) => Promise<void>) => Promise<void>} [ctx.extractWordTables]
|
||||
* @param {boolean} [ctx.preserveEditorLocalStorage=true] 为 true 时不改 U_*,仅用作者登录返回的 user_id
|
||||
*/
|
||||
export async function runMailManuscriptPipeline(ctx) {
|
||||
const preserve = ctx.preserveEditorLocalStorage !== false;
|
||||
const backup = backupEditorLocalStorage();
|
||||
const { api, baseUrl, login, blob, fileName, journal_id, extractWordTables } = ctx;
|
||||
|
||||
const restoreIfNeeded = () => {
|
||||
if (!preserve) {
|
||||
restoreLocalStorage(backup);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const loginRes = await postCheckLogin(api, { ...login, baseUrl });
|
||||
let authorUserId;
|
||||
let authorAccount = '';
|
||||
|
||||
if (preserve) {
|
||||
const v = validateAuthorLoginResponse(loginRes);
|
||||
if (!v.ok) {
|
||||
return { ok: false, msg: v.msg, backup };
|
||||
}
|
||||
authorUserId = String(v.userinfo.user_id);
|
||||
authorAccount = String(v.userinfo.account || '').trim();
|
||||
persistAuthorShadowSession(v.userinfo);
|
||||
} else {
|
||||
const applied = applyAuthorSessionFromLoginResponse(loginRes);
|
||||
if (!applied.ok) {
|
||||
restoreIfNeeded();
|
||||
return { ok: false, msg: applied.msg, backup };
|
||||
}
|
||||
authorUserId = localStorage.getItem('U_id');
|
||||
authorAccount = String(localStorage.getItem('U_name') || '').trim();
|
||||
}
|
||||
|
||||
if (!authorUserId) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return { ok: false, msg: 'Missing user_id after login', backup };
|
||||
}
|
||||
|
||||
const uploadRes = await uploadManuscriptDocxMultipart(baseUrl, blob, fileName);
|
||||
if (!uploadRes || Number(uploadRes.code) !== 0) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return {
|
||||
ok: false,
|
||||
msg: (uploadRes && uploadRes.msg) || 'Manuscript upload failed',
|
||||
backup
|
||||
};
|
||||
}
|
||||
const upurl = uploadRes.upurl;
|
||||
if (!upurl) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return { ok: false, msg: 'Upload response missing upurl', backup };
|
||||
}
|
||||
|
||||
const cr = await postContribute(api, { upurl, user_id: authorUserId });
|
||||
if (!cr || Number(cr.status) !== 1 || !cr.article || cr.article.article_id == null) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return {
|
||||
ok: false,
|
||||
msg: (cr && cr.msg) || 'Contribute failed',
|
||||
backup,
|
||||
contributeRes: cr
|
||||
};
|
||||
}
|
||||
|
||||
const articleId = String(cr.article.article_id);
|
||||
const jid = journal_id != null && String(journal_id).trim() !== '' ? Number(journal_id) : NaN;
|
||||
if (Number.isNaN(jid) || jid <= 0) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return {
|
||||
ok: false,
|
||||
msg: 'Missing journal_id for post-contribute backfill',
|
||||
backup,
|
||||
contributeRes: cr
|
||||
};
|
||||
}
|
||||
|
||||
const bf = await runPostContributeArticleAddBackfill(api, {
|
||||
user_id: authorUserId,
|
||||
article_id: articleId,
|
||||
journal_id: jid,
|
||||
article: cr.article,
|
||||
upurl,
|
||||
staging_username: authorAccount,
|
||||
extractWordTables
|
||||
});
|
||||
if (!bf.ok) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return {
|
||||
ok: false,
|
||||
msg: bf.msg || 'Post-contribute backfill failed',
|
||||
backup,
|
||||
step: bf.step,
|
||||
article_id: articleId,
|
||||
contributeRes: cr
|
||||
};
|
||||
}
|
||||
|
||||
clearAuthorShadowSession();
|
||||
return { ok: true, article_id: articleId, backup };
|
||||
} catch (e) {
|
||||
restoreIfNeeded();
|
||||
clearAuthorShadowSession();
|
||||
return { ok: false, msg: (e && e.message) || String(e), backup };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user