diff --git a/src/components/common/langs/en.js b/src/components/common/langs/en.js index a81d9e3..adbad11 100644 --- a/src/components/common/langs/en.js +++ b/src/components/common/langs/en.js @@ -369,7 +369,40 @@ const en = { ruleZhName: 'Enter Chinese name', ruleEnName: 'Enter English name', 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: { mailSystem: 'Mailbox system', @@ -1183,11 +1216,17 @@ colTitle: 'Template title', factoryScenarioGeneralThanks: 'General Thanks', createdAt: 'Created at', 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: { detail: 'Auto Promotion Details', + factoryTaskSelectPlaceholder: 'Select promotion task', configured: 'Configured', editConfig: 'Edit auto promotion configuration', startConfig: 'Start auto promotion configuration', @@ -1228,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:', diff --git a/src/components/common/langs/zh.js b/src/components/common/langs/zh.js index 31d0d0e..3358a51 100644 --- a/src/components/common/langs/zh.js +++ b/src/components/common/langs/zh.js @@ -358,7 +358,40 @@ const zh = { ruleZhName: '请输入中文名称', ruleEnName: '请输入英文名称', 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: { mailSystem: '邮件系统', @@ -1168,11 +1201,17 @@ const zh = { factoryScenarioGeneralThanks: '常规感谢', createdAt: '创建时间', noFactoryTask: '没有任务', - factoryCreateNow: '立即创建' + factoryCreateNow: '立即创建', + emailClientCreateTaskBtn: '创建任务', + emailClientCreateTaskNeedFactory: '请先在下拉框中选择推广工厂任务', + emailClientCreateTaskSuccess: '创建任务成功', + emailClientCreateTaskFailed: '创建任务失败', + emailClientCreateTaskPreparingHint: '创建任务成功,生成发送邮件列表需要几分钟,请耐心等候...' } , autoPromotionLogs: { detail: '自动推广详情', + factoryTaskSelectPlaceholder: '选择推广任务', configured: '已配置', editConfig: '修改期刊自动推广配置', startConfig: '立即开始期刊自动推广配置', @@ -1213,6 +1252,8 @@ const zh = { enable: '开启', pause: '暂停', previewEditTitle: '预览并修改推广邮件', + logDetailEditTitle: '编辑推广发送记录', + logDetailPreviewTitle: '预览推广发送记录', receiver: '收件人:', receiverImmutablePlaceholder: '收件人邮箱不可更改', subject: '主题:', diff --git a/src/components/page/autoPromotion.vue b/src/components/page/autoPromotion.vue index f015d67..77c0a8c 100644 --- a/src/components/page/autoPromotion.vue +++ b/src/components/page/autoPromotion.vue @@ -83,12 +83,12 @@ {{ taskCard.templateName || 'No Template Configured' }} -
+
-
+
@@ -125,8 +125,20 @@ {{ getFactoryTaskActionText(taskCard) }} -
- {{ $t('autoPromotion.createdAt') }}: {{ taskCard.createdAtText }} +
@@ -364,6 +376,10 @@ export default { this.allJournals = []; 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( raw.map(async (item) => { const journalId = item.journal_id || item.id; @@ -404,7 +420,6 @@ export default { async loadFactoryTaskSummaryByJournal(journal, userId) { if (!journal || !journal.journal_id) return; try { - await this.loadFactoryTemplateNamesByJournal(journal.journal_id); const res = await this.$api.post('api/promotion_factory/getList', { journal_id: String(journal.journal_id), user_id: String(userId || ''), @@ -466,29 +481,47 @@ export default { }); } }, - async loadFactoryTemplateNamesByJournal(journalId) { - const key = String(journalId || ''); - if (!key) return {}; - if (this.factoryTemplateNameMap[key]) { - return this.factoryTemplateNameMap[key]; - } - let map = {}; + /** + * 全页只请求一次 listTemplatesAll,再按期刊写入 factoryTemplateNameMap。 + * 若列表项带 journal_id 则按期刊拆分;否则用同一份 id->name 映射到当前页各期刊(与原先「每刊各请求一次」相比只发 1 次 HTTP)。 + */ + async prefetchFactoryTemplateNameMapsOnce(journalIds) { + const ids = [...new Set((journalIds || []).map((id) => String(id).trim()).filter(Boolean))]; + if (!ids.length) return; 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 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); - if (id == null) return acc; - const name = item.title || item.name || ''; - if (name) acc[String(id)] = name; - return acc; - }, {}); + if (id == null) return; + const name = String(item.title || item.name || '').trim(); + if (!name) return; + 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) { - map = {}; + console.error(e); } - this.$set(this.factoryTemplateNameMap, key, map); - return map; }, getFactoryTemplateName(task, journalId) { const tplId = task && task.template_id != null ? String(task.template_id) : ''; @@ -1002,7 +1035,47 @@ export default { 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 { + flex-shrink: 0; font-size: 11px; color: #909399; text-align: right; diff --git a/src/components/page/autoPromotionLogs.vue b/src/components/page/autoPromotionLogs.vue index 87cb326..0635b9a 100644 --- a/src/components/page/autoPromotionLogs.vue +++ b/src/components/page/autoPromotionLogs.vue @@ -16,20 +16,59 @@
{{ $t('autoPromotion.journal') }} : {{ currentJournalName }} + + + + + + + {{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }} + - + - + +
+
+ + {{ $t('autoPromotion.emailClientCreateTaskBtn') }} +
@@ -64,22 +103,24 @@
-
- - - - - - - - - -
+
+ + + + + + + + + +
- -
+
+ + {{ $t('autoPromotionLogs.logRefresh') }} + +
+
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(); }, @@ -491,6 +558,7 @@ export default { (this.$route.query && this.$route.query.taskId) || ''; this.routePromotionFactoryId = String(pfid || ''); + this.headerPromotionFactoryId = this.routePromotionFactoryId; this.selectedJournalId = String(journal_id); this.loading = true; try { @@ -500,6 +568,7 @@ export default { } if (this.config.initialized) { await this.fetchTemplates(); + await this.fetchFactoryTasksForHeader(); await this.fetchList(); } } 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) { if (Array.isArray(obj)) return obj; @@ -621,22 +834,7 @@ export default { 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); - } - this.selectedCountryIds = this.parseCountryIdsFromPromotionPayload(selectedPayload); - } catch (e) { - this.selectedFieldIds = []; - this.selectedCountryIds = []; - } + // 日志页不请求 getJournalPromotionFields(该接口在此场景不可用);已选字段/国家由向导内操作或他处回显 this.fieldsLoading = false; }, async savePromotionFieldsNow() { @@ -681,29 +879,37 @@ export default { }, openFactoryTaskDialogFromLogs() { 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 matched = targetTaskId + 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 === targetTaskId || rid === targetTaskId || tid === targetTaskId; + return pid === routePid || rid === routePid || tid === routePid; }) || first : first; - this.factoryDialogInitialTask = matched - ? { - ...matched, - 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) - : '' - } - : null; + + 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; }, @@ -798,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', this.journalPromotionFieldsPayload(this.selectedJournalId || '') ); this.$message.success(this.$t('autoPromotionLogs.configUpdated')); + await this.fetchFactoryTasksForHeader(); + await this.fetchList(); } finally { this.saving = false; } @@ -826,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) }; @@ -1025,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 { @@ -1607,6 +1845,10 @@ export default { border-radius: 6px; transition: all 0.2s; } + +.filter-actions { + margin-left: auto; +} /* 基础 Badge 样式 */ .status-badge { display: inline-flex; diff --git a/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue b/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue index 7df0c78..5275d6e 100644 --- a/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue +++ b/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue @@ -334,7 +334,9 @@ editingTaskId: '', templateNameMap: {}, showFactoryTemplateDialog: false, - _emailIdsSendLimitTimer: null + _emailIdsSendLimitTimer: null, + /** 编辑态标题:合并 getDetail / 列表 回显后的快照,用于「期刊-场景-时间」 */ + factoryTitleDetail: null }; }, beforeDestroy: function () { @@ -367,10 +369,14 @@ }, dialogTitle() { if (!this.isEditMode) return this.$t('autoPromotion.factoryDialogTitle'); - const task = this.initialTask || {}; - const journalAbbr = this.getTaskJournalAbbr(task); - const taskTypeLabel = this.getTaskTypeLabel(task.type); - const taskTime = this.getTaskTimeText(task); + const merged = Object.assign({}, this.initialTask || {}, this.factoryTitleDetail || {}); + const journalAbbr = this.getTaskJournalAbbr(merged); + const sceneText = String(merged.scene || merged.scene_name || '').trim(); + 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) { return String(s || '').trim() !== ''; }); @@ -589,9 +595,11 @@ } else if (task.state != null) { this.factoryStartPromotion = String(task.state) === '1'; } + this.factoryTitleDetail = task ? { ...task } : null; }, getTaskTypeLabel(type) { const t = String(type || '').trim(); + if (t === '') return ''; if (t === '1') return this.$t('autoPromotion.factoryScenarioSolicit'); if (t === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation'); if (t === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks'); @@ -600,6 +608,8 @@ }, getTaskJournalAbbr(task) { const candidateKeys = [ + 'journal_title', + 'journal_name', 'journal_abbr', 'journal_jabbr', 'jabbr', @@ -607,7 +617,7 @@ 'journal_short_name', 'journal_short', 'journal_code', - 'journal_name' + 'title' ]; for (let i = 0; i < candidateKeys.length; i++) { const key = candidateKeys[i]; @@ -618,7 +628,17 @@ return currentTitle || this.$t('autoPromotion.factoryJournal'); }, 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 = ''; for (let i = 0; i < candidateKeys.length; i++) { const key = candidateKeys[i]; diff --git a/src/components/page/components/email/TmrEmailEditor.vue b/src/components/page/components/email/TmrEmailEditor.vue index 7e8510c..9a2915a 100644 --- a/src/components/page/components/email/TmrEmailEditor.vue +++ b/src/components/page/components/email/TmrEmailEditor.vue @@ -5,22 +5,22 @@
-
- + > -->
diff --git a/src/components/page/countryManagement.vue b/src/components/page/countryManagement.vue index f8c2881..644a1b6 100644 --- a/src/components/page/countryManagement.vue +++ b/src/components/page/countryManagement.vue @@ -36,6 +36,9 @@ {{ $t('countryManagement.searchBtn') }} {{ $t('countryManagement.resetBtn') }} + + {{ $t('countryManagement.batchPartitionBtn') }} +
@@ -81,7 +84,7 @@ layout="total, sizes, prev, pager, next, jumper" :current-page="query.page" :page-size="query.per_page" - :page-sizes="[10, 20, 50]" + :page-sizes="[20, 50, 100]" :total="total" @size-change="handleSizeChange" @current-change="handlePageChange" @@ -120,6 +123,111 @@ {{ $t('countryManagement.save') }} + + +
+
+ + + + + + + + + +

{{ $t('countryManagement.batchPartitionHelp') }}

+ +
+ + {{ $t('countryManagement.batchPartitionPreview') }} + + + {{ $t('countryManagement.batchPartitionApply') }} + +
+
+ +
+
+ {{ + $t('countryManagement.batchPartitionSummaryLine', { + input: String(batchPartitionStats.input), + matched: String(batchPartitionStats.matched), + miss: String(batchPartitionStats.miss), + same: String(batchPartitionStats.skipSame), + will: String(batchPartitionStats.willUpdate) + }) + }} +
+

{{ $t('countryManagement.batchPartitionSelectionHint') }}

+ + + + + + + + + + + + + + + + + +
+
+ + +
@@ -132,7 +240,7 @@ export default { keyword: '', partition: '', page: 1, - per_page: 10 + per_page: 20 }, list: [], total: 0, @@ -146,9 +254,50 @@ export default { code: '', 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() { this.rules = { zh_name: [{ required: true, message: this.$t('countryManagement.ruleZhName'), trigger: 'blur' }], @@ -211,7 +360,7 @@ export default { keyword: '', partition: '', page: 1, - per_page: 10 + per_page: 20 }; this.fetchList(); }, @@ -299,6 +448,312 @@ export default { } }) .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; 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; +} diff --git a/src/components/page/mailboxMouldDetail.vue b/src/components/page/mailboxMouldDetail.vue index b93b017..5358d77 100644 --- a/src/components/page/mailboxMouldDetail.vue +++ b/src/components/page/mailboxMouldDetail.vue @@ -102,7 +102,7 @@ :journalId="form.journalId" placeholder="" /> - +