Compare commits

9 Commits

Author SHA1 Message Date
052d0e4ca4 自动化配置 2026-04-27 09:53:19 +08:00
ca1b10c418 滑动选中 2026-04-24 15:05:57 +08:00
bb36bcc645 修改邮件模版预览按钮 2026-04-24 14:51:25 +08:00
fe4bd7c9b0 任务工厂 2026-04-24 11:05:22 +08:00
632913aaad 提交 2026-04-23 09:12:13 +08:00
711a3fe2ec 任务工厂列表版 2026-04-22 11:42:47 +08:00
0d913e90a7 国家 2026-04-17 13:35:56 +08:00
7458beb8b2 批量查询领域 2026-04-17 10:56:44 +08:00
8fbcf39a25 提交 2026-04-17 10:36:48 +08:00
15 changed files with 4664 additions and 683 deletions

View File

@@ -64,8 +64,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 +74,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 +83,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 +169,7 @@
<el-menu-item index="mailboxConfig">
{{ $t('sidebar.mailboxManagement') }}
</el-menu-item>
<el-submenu index="expertDatabaseSub">
<template slot="title">
{{ $t('sidebar.expertDatabase') }}
@@ -175,19 +177,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">
@@ -234,7 +233,6 @@ export default {
index: '1',
title: this.$t('sidebar.author'),
subs: [
{
index: 'articleList',
title: this.$t('sidebar.author1')
@@ -246,10 +244,11 @@ export default {
{
index: 'articleAdd',
title: this.$t('sidebar.author2')
} , {
},
{
index: 'orderListAuthor',
title: this.$t('sidebar.author4')
},
}
]
}
// ,{
@@ -312,7 +311,6 @@ export default {
index: 'Promotionsystem',
title: this.$t('menu.Promotionsystem'),
subs: [
{
index: 'disseMRecord',
title: this.$t('menu.userManSys6')
@@ -363,7 +361,7 @@ export default {
index: '4',
title: this.$t('sidebar.userManSys'),
subs: [
{
{
//论文编辑系统
icon: 'el-icon-lx-copy',
index: 'Userdatabase',
@@ -679,74 +677,61 @@ 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(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 +773,7 @@ export default {
index: 'JournalManagementAll',
title: this.$t('sidebar.journalList')
},
{
index: 'GroupClassification',
title: this.$t('sidebar.GroupClassification')

View File

@@ -281,6 +281,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,17 +308,102 @@ const en = {
},
columns: {
baseInfo: 'Base Information',
country: 'Country',
affiliation: 'Affiliation',
researchAreas: 'Research areas'
},
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'
},
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',
title: 'Mailbox config',
@@ -1016,6 +1102,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 +1133,100 @@ 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',
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',
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',
factoryTaskSelectPlaceholder: 'Select promotion task',
configured: 'Configured',
editConfig: 'Edit auto promotion configuration',
startConfig: 'Start auto promotion configuration',
@@ -1100,6 +1267,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:',

View File

@@ -269,6 +269,7 @@ const zh = {
keywordManagement: '关键词管理',
crawlTasks: '抓取任务',
expertList: '专家列表',
countryManagement: '国家信息',
autoPromotion: '自动推广',
ReArticles: '被拒稿件', // 被拒稿件
editorialBoard: '编委管理',
@@ -296,17 +297,102 @@ const zh = {
},
columns: {
baseInfo: '基础信息',
country: '国家',
affiliation: '单位',
researchAreas: '研究领域'
},
emptyMark: '-',
fields: {
nameLabel: '姓名:',
emailLabel: '邮箱:',
acquisitionTimeLabel: '采集时间:'
},
viewAllInfo: '查看全部信息',
detailDialogTitle: '领域与文章',
detailColField: '研究领域',
detailColPaper: '文章标题',
detailColJournal: '所属期刊',
detailClose: '关闭',
detailCellEmpty: '暂无',
noFieldDetail: '暂无领域对应的文献信息',
exportWarn: '请选择研究领域或输入关键词或领域 field 后再导出。',
exportFailed: '导出失败'
},
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: '邮件系统',
title: '邮箱账号管理',
@@ -1001,6 +1087,8 @@ const zh = {
autoSolicit: '自动约稿',
editConfig: '修改配置',
running: '运行中',
stopped: '已停止',
configure: '配置',
emailTemplate: '邮件模板',
emailStyle: '邮件风格',
notStarted: '未开启自动约稿计划',
@@ -1030,21 +1118,100 @@ 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: '创建自动化推广任务',
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库',
factoryScenario: '场景',
factoryScenarioPlaceholder: '请选择场景',
factoryScenarioSolicit: '约稿',
factoryScenarioPromoteCitation: '推广引用',
factoryScenarioGeneralThanks: '常规感谢',
createdAt: '创建时间',
noFactoryTask: '没有任务',
factoryCreateNow: '立即创建',
emailClientCreateTaskBtn: '创建任务',
emailClientCreateTaskNeedFactory: '请先在下拉框中选择推广工厂任务',
emailClientCreateTaskSuccess: '创建任务成功',
emailClientCreateTaskFailed: '创建任务失败',
emailClientCreateTaskPreparingHint: '创建任务成功,生成发送邮件列表需要几分钟,请耐心等候...'
}
,
autoPromotionLogs: {
detail: '自动推广详情',
factoryTaskSelectPlaceholder: '选择推广任务',
configured: '已配置',
editConfig: '修改期刊自动推广配置',
startConfig: '立即开始期刊自动推广配置',
@@ -1085,6 +1252,8 @@ const zh = {
enable: '开启',
pause: '暂停',
previewEditTitle: '预览并修改推广邮件',
logDetailEditTitle: '编辑推广发送记录',
logDetailPreviewTitle: '预览推广发送记录',
receiver: '收件人:',
receiverImmutablePlaceholder: '收件人邮箱不可更改',
subject: '主题:',

File diff suppressed because it is too large Load Diff

View File

@@ -16,20 +16,59 @@
<div class="left">
<span class="label">{{ $t('autoPromotion.journal') }} : </span>
{{ currentJournalName }}
<el-select
v-if="config.initialized && selectedJournalId"
v-model="headerPromotionFactoryId"
class="header-factory-task-select"
size="small"
filterable
:loading="factoryTasksHeaderLoading"
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
@change="onHeaderFactoryTaskChange"
>
<el-option
v-for="opt in factoryTaskOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-tag
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
:type="headerFactoryTaskRunning ? 'success' : 'info'"
size="small"
effect="plain"
class="header-factory-status-tag"
>
<i v-if="headerFactoryTaskRunning" class="el-icon-circle-check"></i>
<span v-else class="header-factory-status-dot"></span>
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
</el-tag>
<template v-if="config.initialized">
<el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
<!-- <template v-if="config.initialized"> -->
<!-- <el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
</el-tag>
<el-button type="text" size="small" style="margin-left: 10px" @click="openWizardDialog">
</el-tag> -->
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
<i class="el-icon-edit"></i>
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
</el-button>
</template>
<!-- </template> -->
<el-tag v-else type="info" size="small" effect="plain" style="margin-left: 10px">
<!-- <el-tag v-else type="info" size="small" effect="plain" style="margin-left: 10px">
<i class="el-icon-info"></i> {{ $t('autoPromotionLogs.notConfigured') }}
</el-tag>
</el-tag> -->
</div>
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
<el-button
type="primary"
size="small"
icon="el-icon-plus"
:loading="createTaskLoading"
@click="handleCreateEmailClientTask"
>
{{ $t('autoPromotion.emailClientCreateTaskBtn') }}
</el-button>
</div>
</div>
</el-card>
@@ -41,7 +80,9 @@
:config="config"
:wizardStartDate.sync="wizardStartDate"
:selectedFieldIds.sync="selectedFieldIds"
:selectedCountryIds.sync="selectedCountryIds"
:availableFields="availableFields"
:availableCountries="availableCountries"
:fieldsLoading="fieldsLoading"
:fieldsSaving="fieldsSaving"
:currentJournalName="currentJournalName"
@@ -52,6 +93,7 @@
:title="$t('autoPromotion.title')"
@open-template-selector="showTemplateDialog = true"
@confirm-fields="savePromotionFieldsNow"
@confirm-countries="savePromotionCountriesNow"
@confirm="completeInitialization"
/>
</el-card>
@@ -61,22 +103,24 @@
<div class="filter-header-row">
<div class="tmr-capsule-group">
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="$t('autoPromotionLogs.statusAll')" name="all"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state0')" name="0"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state5')" name="5"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state1')" name="1"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state3')" name="3"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state2')" name="2"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state4')" name="4"></el-tab-pane>
</el-tabs>
</div>
<div class="tmr-capsule-group">
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="$t('autoPromotionLogs.statusAll')" name="all"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state0')" name="0"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state5')" name="5"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state1')" name="1"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state3')" name="3"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state2')" name="2"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state4')" name="4"></el-tab-pane>
</el-tabs>
</div>
<!-- <div class="filter-actions">
<el-button type="primary" icon="el-icon-search" @click="handleSearch">{{ $t('autoPromotionLogs.searchBtn') }}</el-button>
</div> -->
</div>
<div class="filter-actions">
<el-button type="primary" plain icon="el-icon-refresh" :loading="loading" @click="handleSearch">
{{ $t('autoPromotionLogs.logRefresh') }}
</el-button>
</div>
</div>
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
<el-table-column
@@ -210,7 +254,9 @@
:config="config"
:wizardStartDate.sync="wizardStartDate"
:selectedFieldIds.sync="selectedFieldIds"
:selectedCountryIds.sync="selectedCountryIds"
:availableFields="availableFields"
:availableCountries="availableCountries"
:fieldsLoading="fieldsLoading"
:fieldsSaving="fieldsSaving"
:currentJournalName="currentJournalName"
@@ -221,6 +267,7 @@
:title="$t('autoPromotion.title')"
@open-template-selector="showTemplateDialog = true"
@confirm-fields="savePromotionFieldsNow"
@confirm-countries="savePromotionCountriesNow"
@cancel="showWizardDialog = false"
@confirm="completeInitialization"
/>
@@ -236,6 +283,16 @@
@confirm="handleTemplateApply"
@close-all-dialogs="closeAllDialogs"
/>
<promotion-factory-task-dialog
:visible.sync="showFactoryTaskDialog"
:initial-journal-id="factoryDialogInitialJournalId"
:initial-task="factoryDialogInitialTask"
@success="
fetchFactoryTasksForHeader();
fetchList();
fetchJournalDetail();
"
/>
<el-dialog :title="$t('autoPromotionLogs.previewEditTitle')" :visible.sync="showPreviewDialog" width="1200px" top="5vh">
<div class="mail-edit-wrapper" v-if="previewForm">
<el-form label-width="120px" size="small">
@@ -281,6 +338,7 @@
import CkeditorMail from '@/components/page/components/email/CkeditorMail.vue';
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue';
import AutoPromotionWizard from '@/components/page/components/autoPromotion/AutoPromotionWizard.vue';
import PromotionFactoryTaskDialog from '@/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue';
import PromotionDetailDrawer from '@/components/page/components/autoPromotion/PromotionDetailDrawer.vue';
// 这里假设你已经定义了 API 地址
const API = {
@@ -294,7 +352,7 @@ const API = {
export default {
name: 'autoPromotion',
components: { TemplateSelectorDialog, AutoPromotionWizard, CkeditorMail, PromotionDetailDrawer },
components: { TemplateSelectorDialog, AutoPromotionWizard, PromotionFactoryTaskDialog, CkeditorMail, PromotionDetailDrawer },
data() {
return {
handleRefreshList: [],
@@ -339,9 +397,19 @@ export default {
templateDialogInitialTemplateId: '',
togglingTaskId: '',
selectedFieldIds: [],
selectedCountryIds: [],
availableFields: [],
availableCountries: [],
fieldsLoading: false,
fieldsSaving: false,
showFactoryTaskDialog: false,
factoryDialogInitialJournalId: '',
factoryDialogInitialTask: null,
routePromotionFactoryId: '',
headerPromotionFactoryId: '',
factoryTaskOptions: [],
factoryTasksHeaderLoading: false,
createTaskLoading: false,
previewForm: {
id: '',
email: '',
@@ -351,7 +419,28 @@ export default {
}
};
},
computed: {},
computed: {
/** 当前选中工厂任务是否运行中(与下拉 options 中 running 一致) */
headerFactoryTaskRunning() {
const id = String(this.headerPromotionFactoryId || '').trim();
if (!id || !this.factoryTaskOptions || !this.factoryTaskOptions.length) return null;
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
if (opt && typeof opt.running === 'boolean') return opt.running;
return null;
}
},
watch: {
'$route.query.promotion_factory_id'(val) {
const s = String(val != null ? val : '').trim();
if (s === String(this.headerPromotionFactoryId || '').trim()) return;
this.headerPromotionFactoryId = s;
this.routePromotionFactoryId = s;
if (this.config.initialized && this.selectedJournalId) {
this.query.pageIndex = 1;
this.fetchList();
}
}
},
created() {
this.initPage();
},
@@ -370,6 +459,7 @@ export default {
closeAllDialogs() {
// 点击“去新增模板”后:关闭当前页面所有可能的弹窗
this.showWizardDialog = false;
this.showFactoryTaskDialog = false;
this.showTemplateDialog = false;
this.showPreviewDialog = false;
this.currentRow = null;
@@ -463,6 +553,12 @@ export default {
async initPage() {
this.hidePage = false;
var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
var pfid =
(this.$route.query && this.$route.query.promotion_factory_id) ||
(this.$route.query && this.$route.query.taskId) ||
'';
this.routePromotionFactoryId = String(pfid || '');
this.headerPromotionFactoryId = this.routePromotionFactoryId;
this.selectedJournalId = String(journal_id);
this.loading = true;
try {
@@ -472,6 +568,7 @@ export default {
}
if (this.config.initialized) {
await this.fetchTemplates();
await this.fetchFactoryTasksForHeader();
await this.fetchList();
}
} finally {
@@ -540,6 +637,150 @@ export default {
}
},
/** 下拉项:场景类型文案(优先 scene否则按 type 映射) */
getFactoryHeaderTaskTypeLabel(task) {
if (!task || typeof task !== 'object') return '';
const scene = String(task.scene || task.scene_name || '').trim();
if (scene) return scene;
const type = String(task.type != null ? task.type : '').trim();
if (type === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
if (type === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
if (type === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
if (type === '4') return this.$t('autoPromotion.autoSolicit');
return String(task.task_name || task.name || '').trim();
},
formatFactoryHeaderTaskCreateTime(task) {
if (!task || typeof task !== 'object') return '';
const raw = task.ctime || task.create_time || task.created_at || task.time || '';
if (raw == null || String(raw).trim() === '') return '';
const n = Number(raw);
let dt = null;
if (!isNaN(n) && n > 0) {
dt = new Date(n > 1e12 ? n : n * 1000);
} else {
dt = new Date(String(raw));
}
if (!dt || isNaN(dt.getTime())) return String(raw).trim();
const pad = (v) => String(v).padStart(2, '0');
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
},
isFactoryHeaderTaskRunning(task) {
if (!task || typeof task !== 'object') return false;
if (task.start_promotion != null && String(task.start_promotion).trim() !== '') {
return String(task.start_promotion) === '1';
}
if (task.state != null && String(task.state).trim() !== '') {
return String(task.state) === '1';
}
return false;
},
/** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */
buildFactoryHeaderOptionMainLabel(task, pidFallback) {
const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—';
const datePart = this.formatFactoryHeaderTaskCreateTime(task);
return datePart ? `${typePart} - ${datePart}` : typePart;
},
replacePromotionFactoryIdInUrl(promotionFactoryId) {
try {
const url = new URL(window.location.href);
url.searchParams.set('promotion_factory_id', String(promotionFactoryId));
window.history.replaceState({}, document.title, url.toString());
} catch (e) {
console.error(e);
}
},
async fetchFactoryTasksForHeader() {
if (!this.selectedJournalId || !this.config.initialized) {
this.factoryTaskOptions = [];
return;
}
this.factoryTasksHeaderLoading = true;
try {
const userId = localStorage.getItem('U_id') || '';
const res = await this.$api.post('api/promotion_factory/getList', {
journal_id: String(this.selectedJournalId),
user_id: String(userId),
state: '-1'
});
const payload = (res && res.data) || {};
const list = this.findArray(payload) || this.findArray(res) || [];
let opts = (Array.isArray(list) ? list : []).map((task, idx) => {
const pid =
task && task.promotion_factory_id != null
? String(task.promotion_factory_id)
: task && task.id != null
? String(task.id)
: '';
if (!pid) return null;
const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
const running = this.isFactoryHeaderTaskRunning(task);
return { value: pid, label, running };
}).filter(Boolean);
let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim();
const ids = new Set(opts.map((o) => o.value));
if (cur && !ids.has(cur)) {
opts = [{ value: cur, label: cur, running: false }].concat(opts);
}
this.factoryTaskOptions = opts;
if (!cur && opts.length) {
cur = opts[0].value;
}
if (cur) {
this.headerPromotionFactoryId = cur;
this.routePromotionFactoryId = cur;
const routePid = String((this.$route.query && this.$route.query.promotion_factory_id) || '').trim();
if (cur !== routePid) {
this.replacePromotionFactoryIdInUrl(cur);
}
} else {
this.headerPromotionFactoryId = '';
}
} catch (e) {
this.factoryTaskOptions = [];
} finally {
this.factoryTasksHeaderLoading = false;
}
},
onHeaderFactoryTaskChange(id) {
const next = String(id != null ? id : '').trim();
if (!next) return;
this.routePromotionFactoryId = next;
this.headerPromotionFactoryId = next;
this.query.pageIndex = 1;
this.replacePromotionFactoryIdInUrl(next);
this.fetchList();
},
async handleCreateEmailClientTask() {
const pid = String(this.headerPromotionFactoryId || this.routePromotionFactoryId || '').trim();
if (!pid) {
this.$message.warning(this.$t('autoPromotion.emailClientCreateTaskNeedFactory'));
return;
}
this.createTaskLoading = true;
try {
const res = await this.$api.post('api/email_client/createTask', { promotion_factory_id: pid });
if (res && Number(res.code) === 0) {
const taskId = String(res.task_id || (res.data && res.data.task_id) || '').trim();
if (taskId) {
// Fire-and-forget: prepare recipient list in background.
this.$api.post('api/email_client/prepareTask', { task_id: taskId }).catch(() => {});
}
this.$message.success(this.$t('autoPromotion.emailClientCreateTaskPreparingHint'));
this.query.pageIndex = 1;
this.fetchList();
} else {
this.$message.error((res && res.msg) || this.$t('autoPromotion.emailClientCreateTaskFailed'));
}
} catch (e) {
this.$message.error(this.$t('autoPromotion.emailClientCreateTaskFailed'));
} finally {
this.createTaskLoading = false;
}
},
// 打开向导弹窗:用于“修改期刊自动推广配置”
findArray(obj) {
if (Array.isArray(obj)) return obj;
@@ -552,10 +793,32 @@ export default {
if (values.length && Array.isArray(values[0])) return values[0];
return null;
},
parseCountryIdsFromPromotionPayload(selectedPayload) {
if (!selectedPayload || typeof selectedPayload !== 'object') return [];
const raw =
selectedPayload.country_fetch_ids != null
? selectedPayload.country_fetch_ids
: selectedPayload.country_ids != null
? selectedPayload.country_ids
: '';
if (typeof raw === 'string' && raw.trim()) {
return raw.split(',').map((s) => s.trim()).filter(Boolean).map(String);
}
return [];
},
journalPromotionFieldsPayload(journalId) {
return {
journal_id: String(journalId),
fetch_ids: (this.selectedFieldIds || []).join(','),
country_fetch_ids: (this.selectedCountryIds || []).join(',')
};
},
async loadPromotionFields(journalId) {
this.fieldsLoading = true;
this.availableFields = [];
this.availableCountries = [];
this.selectedFieldIds = [];
this.selectedCountryIds = [];
try {
const availableRes = await this.$api.post('api/email_client/getAvailableFields', { journal_id: String(journalId) });
const availablePayload = (availableRes && availableRes.data) || availableRes || {};
@@ -566,33 +829,22 @@ export default {
const label = item.field || item.title || item.name || item.label || String(id);
return { id: String(id), label };
});
this.availableCountries = this.availableFields.map((x) => ({ id: String(x.id), label: x.label }));
} catch (e) {
this.availableFields = [];
this.availableCountries = [];
}
try {
const selectedRes = await this.$api.post('api/email_client/getJournalPromotionFields', { journal_id: String(journalId) });
const selectedPayload = (selectedRes && selectedRes.data) || selectedRes || {};
let selectedArr = this.findArray(selectedPayload);
if (selectedArr) {
this.selectedFieldIds = selectedArr.map((it) => String(it.expert_fetch_id || it.fetch_id || it.id || it.field_id || it));
} else if (typeof selectedPayload === 'string') {
this.selectedFieldIds = selectedPayload.split(',').map((s) => s.trim()).filter(Boolean);
} else if (typeof selectedPayload.fetch_ids === 'string') {
this.selectedFieldIds = selectedPayload.fetch_ids.split(',').map((s) => s.trim()).filter(Boolean);
}
} catch (e) {
this.selectedFieldIds = [];
}
// 日志页不请求 getJournalPromotionFields该接口在此场景不可用已选字段/国家由向导内操作或他处回显
this.fieldsLoading = false;
},
async savePromotionFieldsNow() {
if (!this.selectedJournalId) return;
this.fieldsSaving = true;
try {
await this.$api.post('api/email_client/setJournalPromotionFields', {
journal_id: String(this.selectedJournalId),
fetch_ids: (this.selectedFieldIds || []).join(',')
});
await this.$api.post(
'api/email_client/setJournalPromotionFields',
this.journalPromotionFieldsPayload(this.selectedJournalId)
);
this.$message.success(this.$t('autoPromotion.fieldsSaved'));
} catch (e) {
this.$message.error(this.$t('autoPromotion.saveFailed'));
@@ -600,6 +852,21 @@ export default {
this.fieldsSaving = false;
}
},
async savePromotionCountriesNow() {
if (!this.selectedJournalId) return;
this.fieldsSaving = true;
try {
await this.$api.post(
'api/email_client/setJournalPromotionFields',
this.journalPromotionFieldsPayload(this.selectedJournalId)
);
this.$message.success(this.$t('autoPromotion.countriesSaved'));
} catch (e) {
this.$message.error(this.$t('autoPromotion.saveFailed'));
} finally {
this.fieldsSaving = false;
}
},
async openWizardDialog() {
this.wizardStep = 0;
if (this.config && this.config.start_date) {
@@ -610,6 +877,41 @@ export default {
}
this.showWizardDialog = true;
},
openFactoryTaskDialogFromLogs() {
this.factoryDialogInitialJournalId = this.selectedJournalId ? String(this.selectedJournalId) : '';
const routePid = String(this.routePromotionFactoryId || '').trim();
const first = this.list && this.list.length ? this.list[0] : null;
const matched = routePid
? (this.list || []).find((row) => {
const pid = row && row.promotion_factory_id != null ? String(row.promotion_factory_id) : '';
const rid = row && row.id != null ? String(row.id) : '';
const tid = row && row.task_id != null ? String(row.task_id) : '';
return pid === routePid || rid === routePid || tid === routePid;
}) || first
: first;
let task = null;
if (matched) {
task = { ...matched };
if (!routePid) {
task.promotion_factory_id =
matched.promotion_factory_id != null
? String(matched.promotion_factory_id)
: matched.id != null
? String(matched.id)
: matched.task_id != null
? String(matched.task_id)
: '';
}
}
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id避免列表首行 id 与路由不一致
if (routePid) {
task = { ...(task || {}), promotion_factory_id: routePid };
}
this.factoryDialogInitialTask = task && Object.keys(task).length ? task : null;
this.showFactoryTaskDialog = true;
},
// 切换期刊逻辑
async handleJournalChange() {
@@ -702,13 +1004,14 @@ export default {
};
this.config.initialized = true;
this.showWizardDialog = false;
this.fetchList();
await this.$api.post(API.saveConfig, payload);
await this.$api.post('api/email_client/setJournalPromotionFields', {
journal_id: String(this.selectedJournalId || ''),
fetch_ids: (this.selectedFieldIds || []).join(',')
});
await this.$api.post(
'api/email_client/setJournalPromotionFields',
this.journalPromotionFieldsPayload(this.selectedJournalId || '')
);
this.$message.success(this.$t('autoPromotionLogs.configUpdated'));
await this.fetchFactoryTasksForHeader();
await this.fetchList();
} finally {
this.saving = false;
}
@@ -730,6 +1033,7 @@ export default {
try {
const params = {
journal_id: String(this.selectedJournalId || ''),
factory_id: String(this.routePromotionFactoryId || ''),
page: Number(this.query.pageIndex || 1),
per_page: Number(this.query.pageSize || 15)
};
@@ -743,8 +1047,15 @@ export default {
this.list = rawList.map((item, idx) => {
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
const state = String(item.state != null ? item.state : '');
const promotionFactoryId =
item.promotion_factory_id != null
? item.promotion_factory_id
: item.id != null
? item.id
: item.task_id;
return {
id: item.id || item.task_id || `task_${idx + 1}`,
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
task_id: String(item.task_id != null ? item.task_id : item.id || ''),
task_name: item.task_name || item.name || '',
scene: item.scene || '',
@@ -922,6 +1233,36 @@ export default {
justify-content: space-between;
align-items: center;
}
.config-bar .left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 10px;
}
.config-bar .right {
flex-shrink: 0;
margin-left: 12px;
}
.header-factory-task-select {
width: min(380px, 46vw);
min-width: 200px;
}
.header-factory-status-tag {
margin-left: 8px;
vertical-align: middle;
}
.header-factory-status-tag .el-icon-circle-check {
margin-right: 4px;
}
.header-factory-status-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #909399;
margin-right: 6px;
vertical-align: middle;
}
/* 向导样式 */
.wizard-card {
@@ -1504,6 +1845,10 @@ export default {
border-radius: 6px;
transition: all 0.2s;
}
.filter-actions {
margin-left: auto;
}
/* 基础 Badge 样式 */
.status-badge {
display: inline-flex;

View 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;

View File

@@ -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,8 +266,37 @@ export default {
this.$emit('update:selectedFieldIds', val);
}
},
selectedCountryIdsProxy: {
get() {
return this.selectedCountryIds;
},
set(val) {
this.$emit('update:selectedCountryIds', val);
}
},
sortedFilteredFields() {
const kwRaw = String(this.fieldSearchText || '');
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()
@@ -203,10 +308,9 @@ export default {
.map((s) => normalize(s))
.filter(Boolean)
: [];
const list = (this.availableFields || []).filter((item) => {
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 || '')));
@@ -217,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: {
@@ -232,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;
}
}
};
@@ -580,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>

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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, // 期刊官网链接

View File

@@ -1354,6 +1354,7 @@ export default {
displayList: [],
currentTypeText: '',
tinymceId: this.id || 'vue-tinymce-' + +new Date()
};
},
// this.$nextTick(() => window.tinymce.get(this.tinymceId).setContent(newVal));
@@ -1510,6 +1511,51 @@ 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;
// 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]);
// this.currentSelection = {
// // 将 label 设置为包含标签的 HTML 字符串
// 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._onDocumentSelectionChange = this.handleDocumentSelectionChange.bind(this);
this._onDocumentMouseUp = this.handleDocumentMouseUp.bind(this);
this._onManuscriptMouseDown = this.handleManuscriptMouseDown.bind(this);
@@ -1518,6 +1564,7 @@ export default {
if (this.$refs.scroll) {
this.$refs.scroll.addEventListener('mousedown', this._onManuscriptMouseDown);
}
},
activated() {
// 主动触发 MathJax 渲染

View 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>

View File

@@ -68,16 +68,16 @@
</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 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 +85,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 +157,10 @@ export default {
list: [],
total: 0,
loading: false,
exportLoading: false
exportLoading: false,
fieldDetailVisible: false,
fieldDetailExpert: null,
fieldDetailRows: []
};
},
created() {
@@ -228,6 +264,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 +377,18 @@ 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;
}
</style>

View File

@@ -100,6 +100,7 @@
v-model="form.body"
:journalList="journalList"
:journalId="form.journalId"
:language="form.lang"
placeholder=""
/> -->
<CkeditorMail v-model="form.body" />

View File

@@ -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'
}
},
{