任务工厂
This commit is contained in:
@@ -369,7 +369,40 @@ const en = {
|
|||||||
ruleZhName: 'Enter Chinese name',
|
ruleZhName: 'Enter Chinese name',
|
||||||
ruleEnName: 'Enter English name',
|
ruleEnName: 'Enter English name',
|
||||||
ruleCode: 'Enter code',
|
ruleCode: 'Enter code',
|
||||||
rulePartition: 'Select a partition'
|
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: {
|
mailboxConfig: {
|
||||||
mailSystem: 'Mailbox system',
|
mailSystem: 'Mailbox system',
|
||||||
@@ -1183,11 +1216,17 @@ colTitle: 'Template title',
|
|||||||
factoryScenarioGeneralThanks: 'General Thanks',
|
factoryScenarioGeneralThanks: 'General Thanks',
|
||||||
createdAt: 'Created at',
|
createdAt: 'Created at',
|
||||||
noFactoryTask: 'No tasks',
|
noFactoryTask: 'No tasks',
|
||||||
factoryCreateNow: 'Create now'
|
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: {
|
autoPromotionLogs: {
|
||||||
detail: 'Auto Promotion Details',
|
detail: 'Auto Promotion Details',
|
||||||
|
factoryTaskSelectPlaceholder: 'Select promotion task',
|
||||||
configured: 'Configured',
|
configured: 'Configured',
|
||||||
editConfig: 'Edit auto promotion configuration',
|
editConfig: 'Edit auto promotion configuration',
|
||||||
startConfig: 'Start auto promotion configuration',
|
startConfig: 'Start auto promotion configuration',
|
||||||
@@ -1228,6 +1267,8 @@ colTitle: 'Template title',
|
|||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
pause: 'Pause',
|
pause: 'Pause',
|
||||||
previewEditTitle: 'Preview and edit promotion email',
|
previewEditTitle: 'Preview and edit promotion email',
|
||||||
|
logDetailEditTitle: 'Edit promotion send log',
|
||||||
|
logDetailPreviewTitle: 'Preview promotion send log',
|
||||||
receiver: 'Receiver:',
|
receiver: 'Receiver:',
|
||||||
receiverImmutablePlaceholder: 'Receiver email cannot be changed',
|
receiverImmutablePlaceholder: 'Receiver email cannot be changed',
|
||||||
subject: 'Subject:',
|
subject: 'Subject:',
|
||||||
|
|||||||
@@ -358,7 +358,40 @@ const zh = {
|
|||||||
ruleZhName: '请输入中文名称',
|
ruleZhName: '请输入中文名称',
|
||||||
ruleEnName: '请输入英文名称',
|
ruleEnName: '请输入英文名称',
|
||||||
ruleCode: '请输入代码',
|
ruleCode: '请输入代码',
|
||||||
rulePartition: '请选择分区'
|
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: {
|
mailboxConfig: {
|
||||||
mailSystem: '邮件系统',
|
mailSystem: '邮件系统',
|
||||||
@@ -1168,11 +1201,17 @@ const zh = {
|
|||||||
factoryScenarioGeneralThanks: '常规感谢',
|
factoryScenarioGeneralThanks: '常规感谢',
|
||||||
createdAt: '创建时间',
|
createdAt: '创建时间',
|
||||||
noFactoryTask: '没有任务',
|
noFactoryTask: '没有任务',
|
||||||
factoryCreateNow: '立即创建'
|
factoryCreateNow: '立即创建',
|
||||||
|
emailClientCreateTaskBtn: '创建任务',
|
||||||
|
emailClientCreateTaskNeedFactory: '请先在下拉框中选择推广工厂任务',
|
||||||
|
emailClientCreateTaskSuccess: '创建任务成功',
|
||||||
|
emailClientCreateTaskFailed: '创建任务失败',
|
||||||
|
emailClientCreateTaskPreparingHint: '创建任务成功,生成发送邮件列表需要几分钟,请耐心等候...'
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
autoPromotionLogs: {
|
autoPromotionLogs: {
|
||||||
detail: '自动推广详情',
|
detail: '自动推广详情',
|
||||||
|
factoryTaskSelectPlaceholder: '选择推广任务',
|
||||||
configured: '已配置',
|
configured: '已配置',
|
||||||
editConfig: '修改期刊自动推广配置',
|
editConfig: '修改期刊自动推广配置',
|
||||||
startConfig: '立即开始期刊自动推广配置',
|
startConfig: '立即开始期刊自动推广配置',
|
||||||
@@ -1213,6 +1252,8 @@ const zh = {
|
|||||||
enable: '开启',
|
enable: '开启',
|
||||||
pause: '暂停',
|
pause: '暂停',
|
||||||
previewEditTitle: '预览并修改推广邮件',
|
previewEditTitle: '预览并修改推广邮件',
|
||||||
|
logDetailEditTitle: '编辑推广发送记录',
|
||||||
|
logDetailPreviewTitle: '预览推广发送记录',
|
||||||
receiver: '收件人:',
|
receiver: '收件人:',
|
||||||
receiverImmutablePlaceholder: '收件人邮箱不可更改',
|
receiverImmutablePlaceholder: '收件人邮箱不可更改',
|
||||||
subject: '主题:',
|
subject: '主题:',
|
||||||
|
|||||||
@@ -83,12 +83,12 @@
|
|||||||
<span class="tpl-name tpl-name-single-line">{{ taskCard.templateName || 'No Template Configured' }}</span>
|
<span class="tpl-name tpl-name-single-line">{{ taskCard.templateName || 'No Template Configured' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="meta-row">
|
<!-- <div class="meta-row">
|
||||||
<i class="el-icon-collection-tag"
|
<i class="el-icon-collection-tag"
|
||||||
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">Promotion Fields :</span></i
|
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">Promotion Fields :</span></i
|
||||||
>
|
>
|
||||||
<span class="tpl-name">{{ taskCard.fieldCountText }}</span>
|
<span class="tpl-name">{{ taskCard.fieldCountText }}</span>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
<i class="el-icon-location-outline"
|
<i class="el-icon-location-outline"
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-container">
|
<!-- <div class="stats-container">
|
||||||
<div class="stat-box">
|
<div class="stat-box">
|
||||||
<span class="stat-label">SENT</span>
|
<span class="stat-label">SENT</span>
|
||||||
<span class="stat-value"><i class="el-icon-circle-check"></i> 1,240</span>
|
<span class="stat-value"><i class="el-icon-circle-check"></i> 1,240</span>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<span class="stat-label">PENDING</span>
|
<span class="stat-label">PENDING</span>
|
||||||
<span class="stat-value warning"><i class="el-icon-time"></i> 150</span>
|
<span class="stat-value warning"><i class="el-icon-time"></i> 150</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
@@ -125,8 +125,20 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>{{ getFactoryTaskActionText(taskCard) }}</span>
|
<span v-else>{{ getFactoryTaskActionText(taskCard) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="taskCard.createdAtText" class="task-create-time">
|
<div class="task-card-footer-row">
|
||||||
{{ $t('autoPromotion.createdAt') }}: {{ taskCard.createdAtText }}
|
<button
|
||||||
|
v-if="!taskCard.enabled && taskCard.taskId"
|
||||||
|
type="button"
|
||||||
|
class="promo-history-link"
|
||||||
|
@click.stop="openDetail(journal, taskCard)"
|
||||||
|
>
|
||||||
|
<span>{{ $t('autoPromotion.logs') }}</span>
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
</button>
|
||||||
|
<span v-else class="promo-history-spacer"></span>
|
||||||
|
<div v-if="taskCard.createdAtText" class="task-create-time">
|
||||||
|
{{ $t('autoPromotion.createdAt') }}: {{ taskCard.createdAtText }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,6 +376,10 @@ export default {
|
|||||||
this.allJournals = [];
|
this.allJournals = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const journalIdsForTpl = raw
|
||||||
|
.map((item) => item.journal_id || item.id)
|
||||||
|
.filter((id) => id != null && String(id).trim() !== '');
|
||||||
|
await this.prefetchFactoryTemplateNameMapsOnce(journalIdsForTpl);
|
||||||
this.allJournals = await Promise.all(
|
this.allJournals = await Promise.all(
|
||||||
raw.map(async (item) => {
|
raw.map(async (item) => {
|
||||||
const journalId = item.journal_id || item.id;
|
const journalId = item.journal_id || item.id;
|
||||||
@@ -404,7 +420,6 @@ export default {
|
|||||||
async loadFactoryTaskSummaryByJournal(journal, userId) {
|
async loadFactoryTaskSummaryByJournal(journal, userId) {
|
||||||
if (!journal || !journal.journal_id) return;
|
if (!journal || !journal.journal_id) return;
|
||||||
try {
|
try {
|
||||||
await this.loadFactoryTemplateNamesByJournal(journal.journal_id);
|
|
||||||
const res = await this.$api.post('api/promotion_factory/getList', {
|
const res = await this.$api.post('api/promotion_factory/getList', {
|
||||||
journal_id: String(journal.journal_id),
|
journal_id: String(journal.journal_id),
|
||||||
user_id: String(userId || ''),
|
user_id: String(userId || ''),
|
||||||
@@ -466,29 +481,47 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadFactoryTemplateNamesByJournal(journalId) {
|
/**
|
||||||
const key = String(journalId || '');
|
* 全页只请求一次 listTemplatesAll,再按期刊写入 factoryTemplateNameMap。
|
||||||
if (!key) return {};
|
* 若列表项带 journal_id 则按期刊拆分;否则用同一份 id->name 映射到当前页各期刊(与原先「每刊各请求一次」相比只发 1 次 HTTP)。
|
||||||
if (this.factoryTemplateNameMap[key]) {
|
*/
|
||||||
return this.factoryTemplateNameMap[key];
|
async prefetchFactoryTemplateNameMapsOnce(journalIds) {
|
||||||
}
|
const ids = [...new Set((journalIds || []).map((id) => String(id).trim()).filter(Boolean))];
|
||||||
let map = {};
|
if (!ids.length) return;
|
||||||
try {
|
try {
|
||||||
const res = await this.$api.post('api/mail_template/listTemplatesAll', { journal_id: key });
|
const res = await this.$api.post('api/mail_template/listTemplatesAll', { journal_id: ids[0] });
|
||||||
const payload = (res && res.data) || {};
|
const payload = (res && res.data) || {};
|
||||||
const list = this.findArray(payload) || this.findArray(res) || [];
|
const list = this.findArray(payload) || this.findArray(res) || [];
|
||||||
map = (Array.isArray(list) ? list : []).reduce((acc, item) => {
|
const flat = {};
|
||||||
|
const byJournal = {};
|
||||||
|
(Array.isArray(list) ? list : []).forEach((item) => {
|
||||||
const id = item && (item.template_id != null ? item.template_id : item.id);
|
const id = item && (item.template_id != null ? item.template_id : item.id);
|
||||||
if (id == null) return acc;
|
if (id == null) return;
|
||||||
const name = item.title || item.name || '';
|
const name = String(item.title || item.name || '').trim();
|
||||||
if (name) acc[String(id)] = name;
|
if (!name) return;
|
||||||
return acc;
|
const tid = String(id);
|
||||||
}, {});
|
flat[tid] = name;
|
||||||
|
const jid =
|
||||||
|
item.journal_id != null
|
||||||
|
? String(item.journal_id).trim()
|
||||||
|
: item.journalId != null
|
||||||
|
? String(item.journalId).trim()
|
||||||
|
: item.j_id != null
|
||||||
|
? String(item.j_id).trim()
|
||||||
|
: '';
|
||||||
|
if (jid) {
|
||||||
|
if (!byJournal[jid]) byJournal[jid] = {};
|
||||||
|
byJournal[jid][tid] = name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const perItemJournal = Object.keys(byJournal).length > 0;
|
||||||
|
ids.forEach((jid) => {
|
||||||
|
const m = perItemJournal ? byJournal[jid] || {} : { ...flat };
|
||||||
|
this.$set(this.factoryTemplateNameMap, jid, m);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
map = {};
|
console.error(e);
|
||||||
}
|
}
|
||||||
this.$set(this.factoryTemplateNameMap, key, map);
|
|
||||||
return map;
|
|
||||||
},
|
},
|
||||||
getFactoryTemplateName(task, journalId) {
|
getFactoryTemplateName(task, journalId) {
|
||||||
const tplId = task && task.template_id != null ? String(task.template_id) : '';
|
const tplId = task && task.template_id != null ? String(task.template_id) : '';
|
||||||
@@ -1002,7 +1035,47 @@ export default {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-card-footer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-history-spacer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-history-link {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-history-link i {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-history-link:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.task-create-time {
|
.task-create-time {
|
||||||
|
flex-shrink: 0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@@ -16,20 +16,59 @@
|
|||||||
<div class="left">
|
<div class="left">
|
||||||
<span class="label">{{ $t('autoPromotion.journal') }} : </span>
|
<span class="label">{{ $t('autoPromotion.journal') }} : </span>
|
||||||
{{ currentJournalName }}
|
{{ 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">
|
<!-- <template v-if="config.initialized"> -->
|
||||||
<el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
|
<!-- <el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
|
||||||
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
|
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
|
||||||
</el-tag>
|
</el-tag> -->
|
||||||
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
||||||
<i class="el-icon-edit"></i>
|
<i class="el-icon-edit"></i>
|
||||||
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
||||||
</el-button>
|
</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') }}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -64,22 +103,24 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="filter-header-row">
|
<div class="filter-header-row">
|
||||||
<div class="tmr-capsule-group">
|
<div class="tmr-capsule-group">
|
||||||
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
|
<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.statusAll')" name="all"></el-tab-pane>
|
||||||
<el-tab-pane :label="$t('autoPromotionLogs.state0')" name="0"></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.state5')" name="5"></el-tab-pane>
|
||||||
<el-tab-pane :label="$t('autoPromotionLogs.state1')" name="1"></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.state3')" name="3"></el-tab-pane>
|
||||||
<el-tab-pane :label="$t('autoPromotionLogs.state2')" name="2"></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-tab-pane :label="$t('autoPromotionLogs.state4')" name="4"></el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<el-button type="primary" icon="el-icon-search" @click="handleSearch">{{ $t('autoPromotionLogs.searchBtn') }}</el-button>
|
<el-button type="primary" plain icon="el-icon-refresh" :loading="loading" @click="handleSearch">
|
||||||
</div> -->
|
{{ $t('autoPromotionLogs.logRefresh') }}
|
||||||
</div>
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
|
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -247,6 +288,7 @@
|
|||||||
:initial-journal-id="factoryDialogInitialJournalId"
|
:initial-journal-id="factoryDialogInitialJournalId"
|
||||||
:initial-task="factoryDialogInitialTask"
|
:initial-task="factoryDialogInitialTask"
|
||||||
@success="
|
@success="
|
||||||
|
fetchFactoryTasksForHeader();
|
||||||
fetchList();
|
fetchList();
|
||||||
fetchJournalDetail();
|
fetchJournalDetail();
|
||||||
"
|
"
|
||||||
@@ -364,6 +406,10 @@ export default {
|
|||||||
factoryDialogInitialJournalId: '',
|
factoryDialogInitialJournalId: '',
|
||||||
factoryDialogInitialTask: null,
|
factoryDialogInitialTask: null,
|
||||||
routePromotionFactoryId: '',
|
routePromotionFactoryId: '',
|
||||||
|
headerPromotionFactoryId: '',
|
||||||
|
factoryTaskOptions: [],
|
||||||
|
factoryTasksHeaderLoading: false,
|
||||||
|
createTaskLoading: false,
|
||||||
previewForm: {
|
previewForm: {
|
||||||
id: '',
|
id: '',
|
||||||
email: '',
|
email: '',
|
||||||
@@ -373,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() {
|
created() {
|
||||||
this.initPage();
|
this.initPage();
|
||||||
},
|
},
|
||||||
@@ -491,6 +558,7 @@ export default {
|
|||||||
(this.$route.query && this.$route.query.taskId) ||
|
(this.$route.query && this.$route.query.taskId) ||
|
||||||
'';
|
'';
|
||||||
this.routePromotionFactoryId = String(pfid || '');
|
this.routePromotionFactoryId = String(pfid || '');
|
||||||
|
this.headerPromotionFactoryId = this.routePromotionFactoryId;
|
||||||
this.selectedJournalId = String(journal_id);
|
this.selectedJournalId = String(journal_id);
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -500,6 +568,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.config.initialized) {
|
if (this.config.initialized) {
|
||||||
await this.fetchTemplates();
|
await this.fetchTemplates();
|
||||||
|
await this.fetchFactoryTasksForHeader();
|
||||||
await this.fetchList();
|
await this.fetchList();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -568,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) {
|
findArray(obj) {
|
||||||
if (Array.isArray(obj)) return obj;
|
if (Array.isArray(obj)) return obj;
|
||||||
@@ -621,22 +834,7 @@ export default {
|
|||||||
this.availableFields = [];
|
this.availableFields = [];
|
||||||
this.availableCountries = [];
|
this.availableCountries = [];
|
||||||
}
|
}
|
||||||
try {
|
// 日志页不请求 getJournalPromotionFields(该接口在此场景不可用);已选字段/国家由向导内操作或他处回显
|
||||||
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);
|
|
||||||
}
|
|
||||||
this.selectedCountryIds = this.parseCountryIdsFromPromotionPayload(selectedPayload);
|
|
||||||
} catch (e) {
|
|
||||||
this.selectedFieldIds = [];
|
|
||||||
this.selectedCountryIds = [];
|
|
||||||
}
|
|
||||||
this.fieldsLoading = false;
|
this.fieldsLoading = false;
|
||||||
},
|
},
|
||||||
async savePromotionFieldsNow() {
|
async savePromotionFieldsNow() {
|
||||||
@@ -681,29 +879,37 @@ export default {
|
|||||||
},
|
},
|
||||||
openFactoryTaskDialogFromLogs() {
|
openFactoryTaskDialogFromLogs() {
|
||||||
this.factoryDialogInitialJournalId = this.selectedJournalId ? String(this.selectedJournalId) : '';
|
this.factoryDialogInitialJournalId = this.selectedJournalId ? String(this.selectedJournalId) : '';
|
||||||
const targetTaskId = String(this.routePromotionFactoryId || '').trim();
|
const routePid = String(this.routePromotionFactoryId || '').trim();
|
||||||
const first = this.list && this.list.length ? this.list[0] : null;
|
const first = this.list && this.list.length ? this.list[0] : null;
|
||||||
const matched = targetTaskId
|
const matched = routePid
|
||||||
? (this.list || []).find((row) => {
|
? (this.list || []).find((row) => {
|
||||||
const pid = row && row.promotion_factory_id != null ? String(row.promotion_factory_id) : '';
|
const pid = row && row.promotion_factory_id != null ? String(row.promotion_factory_id) : '';
|
||||||
const rid = row && row.id != null ? String(row.id) : '';
|
const rid = row && row.id != null ? String(row.id) : '';
|
||||||
const tid = row && row.task_id != null ? String(row.task_id) : '';
|
const tid = row && row.task_id != null ? String(row.task_id) : '';
|
||||||
return pid === targetTaskId || rid === targetTaskId || tid === targetTaskId;
|
return pid === routePid || rid === routePid || tid === routePid;
|
||||||
}) || first
|
}) || first
|
||||||
: first;
|
: first;
|
||||||
this.factoryDialogInitialTask = matched
|
|
||||||
? {
|
let task = null;
|
||||||
...matched,
|
if (matched) {
|
||||||
promotion_factory_id:
|
task = { ...matched };
|
||||||
matched.promotion_factory_id != null
|
if (!routePid) {
|
||||||
? String(matched.promotion_factory_id)
|
task.promotion_factory_id =
|
||||||
: matched.id != null
|
matched.promotion_factory_id != null
|
||||||
? String(matched.id)
|
? String(matched.promotion_factory_id)
|
||||||
: matched.task_id != null
|
: matched.id != null
|
||||||
? String(matched.task_id)
|
? String(matched.id)
|
||||||
: ''
|
: matched.task_id != null
|
||||||
}
|
? String(matched.task_id)
|
||||||
: null;
|
: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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;
|
this.showFactoryTaskDialog = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -798,13 +1004,14 @@ export default {
|
|||||||
};
|
};
|
||||||
this.config.initialized = true;
|
this.config.initialized = true;
|
||||||
this.showWizardDialog = false;
|
this.showWizardDialog = false;
|
||||||
this.fetchList();
|
|
||||||
await this.$api.post(API.saveConfig, payload);
|
await this.$api.post(API.saveConfig, payload);
|
||||||
await this.$api.post(
|
await this.$api.post(
|
||||||
'api/email_client/setJournalPromotionFields',
|
'api/email_client/setJournalPromotionFields',
|
||||||
this.journalPromotionFieldsPayload(this.selectedJournalId || '')
|
this.journalPromotionFieldsPayload(this.selectedJournalId || '')
|
||||||
);
|
);
|
||||||
this.$message.success(this.$t('autoPromotionLogs.configUpdated'));
|
this.$message.success(this.$t('autoPromotionLogs.configUpdated'));
|
||||||
|
await this.fetchFactoryTasksForHeader();
|
||||||
|
await this.fetchList();
|
||||||
} finally {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
@@ -826,6 +1033,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
journal_id: String(this.selectedJournalId || ''),
|
journal_id: String(this.selectedJournalId || ''),
|
||||||
|
factory_id: String(this.routePromotionFactoryId || ''),
|
||||||
page: Number(this.query.pageIndex || 1),
|
page: Number(this.query.pageIndex || 1),
|
||||||
per_page: Number(this.query.pageSize || 15)
|
per_page: Number(this.query.pageSize || 15)
|
||||||
};
|
};
|
||||||
@@ -1025,6 +1233,36 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
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 {
|
.wizard-card {
|
||||||
@@ -1607,6 +1845,10 @@ export default {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
/* 基础 Badge 样式 */
|
/* 基础 Badge 样式 */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -334,7 +334,9 @@
|
|||||||
editingTaskId: '',
|
editingTaskId: '',
|
||||||
templateNameMap: {},
|
templateNameMap: {},
|
||||||
showFactoryTemplateDialog: false,
|
showFactoryTemplateDialog: false,
|
||||||
_emailIdsSendLimitTimer: null
|
_emailIdsSendLimitTimer: null,
|
||||||
|
/** 编辑态标题:合并 getDetail / 列表 回显后的快照,用于「期刊-场景-时间」 */
|
||||||
|
factoryTitleDetail: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeDestroy: function () {
|
||||||
@@ -367,10 +369,14 @@
|
|||||||
},
|
},
|
||||||
dialogTitle() {
|
dialogTitle() {
|
||||||
if (!this.isEditMode) return this.$t('autoPromotion.factoryDialogTitle');
|
if (!this.isEditMode) return this.$t('autoPromotion.factoryDialogTitle');
|
||||||
const task = this.initialTask || {};
|
const merged = Object.assign({}, this.initialTask || {}, this.factoryTitleDetail || {});
|
||||||
const journalAbbr = this.getTaskJournalAbbr(task);
|
const journalAbbr = this.getTaskJournalAbbr(merged);
|
||||||
const taskTypeLabel = this.getTaskTypeLabel(task.type);
|
const sceneText = String(merged.scene || merged.scene_name || '').trim();
|
||||||
const taskTime = this.getTaskTimeText(task);
|
const typeFromMerged =
|
||||||
|
merged.type != null && String(merged.type).trim() !== '' ? String(merged.type).trim() : '';
|
||||||
|
const typeForLabel = typeFromMerged || String(this.factoryType || '').trim();
|
||||||
|
const taskTypeLabel = sceneText || this.getTaskTypeLabel(typeForLabel);
|
||||||
|
const taskTime = this.getTaskTimeText(merged);
|
||||||
const titleParts = [journalAbbr, taskTypeLabel, taskTime].filter(function (s) {
|
const titleParts = [journalAbbr, taskTypeLabel, taskTime].filter(function (s) {
|
||||||
return String(s || '').trim() !== '';
|
return String(s || '').trim() !== '';
|
||||||
});
|
});
|
||||||
@@ -589,9 +595,11 @@
|
|||||||
} else if (task.state != null) {
|
} else if (task.state != null) {
|
||||||
this.factoryStartPromotion = String(task.state) === '1';
|
this.factoryStartPromotion = String(task.state) === '1';
|
||||||
}
|
}
|
||||||
|
this.factoryTitleDetail = task ? { ...task } : null;
|
||||||
},
|
},
|
||||||
getTaskTypeLabel(type) {
|
getTaskTypeLabel(type) {
|
||||||
const t = String(type || '').trim();
|
const t = String(type || '').trim();
|
||||||
|
if (t === '') return '';
|
||||||
if (t === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
|
if (t === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
|
||||||
if (t === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
|
if (t === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
|
||||||
if (t === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
|
if (t === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
|
||||||
@@ -600,6 +608,8 @@
|
|||||||
},
|
},
|
||||||
getTaskJournalAbbr(task) {
|
getTaskJournalAbbr(task) {
|
||||||
const candidateKeys = [
|
const candidateKeys = [
|
||||||
|
'journal_title',
|
||||||
|
'journal_name',
|
||||||
'journal_abbr',
|
'journal_abbr',
|
||||||
'journal_jabbr',
|
'journal_jabbr',
|
||||||
'jabbr',
|
'jabbr',
|
||||||
@@ -607,7 +617,7 @@
|
|||||||
'journal_short_name',
|
'journal_short_name',
|
||||||
'journal_short',
|
'journal_short',
|
||||||
'journal_code',
|
'journal_code',
|
||||||
'journal_name'
|
'title'
|
||||||
];
|
];
|
||||||
for (let i = 0; i < candidateKeys.length; i++) {
|
for (let i = 0; i < candidateKeys.length; i++) {
|
||||||
const key = candidateKeys[i];
|
const key = candidateKeys[i];
|
||||||
@@ -618,7 +628,17 @@
|
|||||||
return currentTitle || this.$t('autoPromotion.factoryJournal');
|
return currentTitle || this.$t('autoPromotion.factoryJournal');
|
||||||
},
|
},
|
||||||
getTaskTimeText(task) {
|
getTaskTimeText(task) {
|
||||||
const candidateKeys = ['task_time', 'ctime', 'create_time', 'time', 'created_at', 'updated_at', 'update_time'];
|
const candidateKeys = [
|
||||||
|
'task_time',
|
||||||
|
'ctime',
|
||||||
|
'create_time',
|
||||||
|
'time',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'update_time',
|
||||||
|
'add_time',
|
||||||
|
'created_at_str'
|
||||||
|
];
|
||||||
let raw = '';
|
let raw = '';
|
||||||
for (let i = 0; i < candidateKeys.length; i++) {
|
for (let i = 0; i < candidateKeys.length; i++) {
|
||||||
const key = candidateKeys[i];
|
const key = candidateKeys[i];
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<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') }}
|
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.preview') }}
|
||||||
</button>
|
</button> -->
|
||||||
<button type="button" @click="openPreview(true)" class="preview-trigger-btn preview-with-vars-btn">
|
<button type="button" @click="openPreview(true)" class="preview-trigger-btn preview-with-vars-btn">
|
||||||
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.previewWithVariables') }}
|
<i class="icon-eye"></i> {{ $t('tmrEmailEditor.previewWithVariables') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea
|
<!-- <textarea
|
||||||
ref="editorRef"
|
ref="editorRef"
|
||||||
class="tmr-textarea"
|
class="tmr-textarea"
|
||||||
:value="plainText"
|
:value="plainText"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
:placeholder="resolvedPlaceholder"
|
:placeholder="resolvedPlaceholder"
|
||||||
></textarea>
|
></textarea> -->
|
||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="showModal" class="tmr-modal-mask" @click.self="closePreviewModal">
|
<div v-if="showModal" class="tmr-modal-mask" @click.self="closePreviewModal">
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
{{ $t('countryManagement.searchBtn') }}
|
{{ $t('countryManagement.searchBtn') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleReset">{{ $t('countryManagement.resetBtn') }}</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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
:current-page="query.page"
|
:current-page="query.page"
|
||||||
:page-size="query.per_page"
|
:page-size="query.per_page"
|
||||||
:page-sizes="[10, 20, 50]"
|
:page-sizes="[20, 50, 100]"
|
||||||
:total="total"
|
:total="total"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
@@ -120,6 +123,111 @@
|
|||||||
<el-button type="primary" :loading="saveLoading" @click="submitEdit">{{ $t('countryManagement.save') }}</el-button>
|
<el-button type="primary" :loading="saveLoading" @click="submitEdit">{{ $t('countryManagement.save') }}</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -132,7 +240,7 @@ export default {
|
|||||||
keyword: '',
|
keyword: '',
|
||||||
partition: '',
|
partition: '',
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: 10
|
per_page: 20
|
||||||
},
|
},
|
||||||
list: [],
|
list: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -146,9 +254,50 @@ export default {
|
|||||||
code: '',
|
code: '',
|
||||||
partition: '1'
|
partition: '1'
|
||||||
},
|
},
|
||||||
rules: {}
|
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() {
|
created() {
|
||||||
this.rules = {
|
this.rules = {
|
||||||
zh_name: [{ required: true, message: this.$t('countryManagement.ruleZhName'), trigger: 'blur' }],
|
zh_name: [{ required: true, message: this.$t('countryManagement.ruleZhName'), trigger: 'blur' }],
|
||||||
@@ -211,7 +360,7 @@ export default {
|
|||||||
keyword: '',
|
keyword: '',
|
||||||
partition: '',
|
partition: '',
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: 10
|
per_page: 20
|
||||||
};
|
};
|
||||||
this.fetchList();
|
this.fetchList();
|
||||||
},
|
},
|
||||||
@@ -299,6 +448,312 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -359,4 +814,101 @@ export default {
|
|||||||
border-color: #f56c6c;
|
border-color: #f56c6c;
|
||||||
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>
|
</style>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
:journalId="form.journalId"
|
:journalId="form.journalId"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
/>
|
/>
|
||||||
<!-- <CkeditorMail v-model="form.body" /> -->
|
<CkeditorMail v-model="form.body" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user