Files
tougao_web/src/components/page/autoPromotionLogs.vue
2026-03-30 13:07:33 +08:00

1593 lines
57 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="auto-promo-container">
<div class="crumbs">
<div class="page-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item><i class="el-icon-s-promotion"></i> {{ $t('autoPromotion.title') }}</el-breadcrumb-item>
<el-breadcrumb-item>
{{ $t('autoPromotionLogs.detail') }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<div v-if="!hidePage">
<el-card shadow="never" class="journal-header-card">
<div class="config-bar">
<div class="left">
<span class="label">{{ $t('autoPromotion.journal') }} : </span>
{{ currentJournalName }}
<template v-if="config.initialized">
<el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
</el-tag>
<el-button type="text" size="small" style="margin-left: 10px" @click="openWizardDialog">
<i class="el-icon-edit"></i>
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
</el-button>
</template>
<el-tag v-else type="info" size="small" effect="plain" style="margin-left: 10px">
<i class="el-icon-info"></i> {{ $t('autoPromotionLogs.notConfigured') }}
</el-tag>
</div>
</div>
</el-card>
<div v-loading="loading" class="main-body">
<el-card v-if="!config.initialized" shadow="never" class="wizard-card">
<auto-promotion-wizard
mode="inline"
:config="config"
:wizardStartDate.sync="wizardStartDate"
:selectedFieldIds.sync="selectedFieldIds"
:availableFields="availableFields"
:fieldsLoading="fieldsLoading"
:fieldsSaving="fieldsSaving"
:currentJournalName="currentJournalName"
:selectedTemplateThumbHtml="selectedTemplateThumbHtml"
:selectedTemplateName="selectedTemplateName"
:selectedStyleName="selectedStyleName"
:saving="saving"
:title="$t('autoPromotion.title')"
@open-template-selector="showTemplateDialog = true"
@confirm-fields="savePromotionFieldsNow"
@confirm="completeInitialization"
/>
</el-card>
<div v-else class="manage-mode">
<el-card shadow="never" class="list-card">
<div class="filter-header-row">
<div class="tmr-capsule-group">
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="$t('autoPromotionLogs.statusAll')" name="all"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state0')" name="0"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state5')" name="5"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state1')" name="1"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state3')" name="3"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state2')" name="2"></el-tab-pane>
<el-tab-pane :label="$t('autoPromotionLogs.state4')" name="4"></el-tab-pane>
</el-tabs>
</div>
<!-- <div class="filter-actions">
<el-button type="primary" icon="el-icon-search" @click="handleSearch">{{ $t('autoPromotionLogs.searchBtn') }}</el-button>
</div> -->
</div>
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
<el-table-column
type="index"
:label="$t('autoPromotionLogs.index')"
width="60"
align="center"
></el-table-column>
<el-table-column :label="$t('autoPromotionLogs.taskName')" min-width="240">
<template slot-scope="scope">
<div class="task-column">
<div class="task-name">{{ scope.row.task_name || '-' }}</div>
<div class="task-id-tags">
<span class="id-tag">{{ $t('autoPromotionLogs.templateIdLabel') }}: {{ scope.row.template_id }}</span>
<span class="id-tag">{{ $t('autoPromotionLogs.styleIdLabel') }}: {{ scope.row.style_id }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('autoPromotionLogs.taskParams')" width="200">
<template slot-scope="scope">
<div class="strategy-column">
<div class="strategy-item">
<i class="el-icon-collection-tag"></i>
<span>{{ scope.row.scene || '-' }}</span>
</div>
<div class="strategy-item mini-text">
<i class="el-icon-timer"></i>
<span
>{{ scope.row.send_start_hour }}~{{ scope.row.send_end_hour }}h /
{{ scope.row.min_interval }}-{{ scope.row.max_interval }}m</span
>
</div>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('autoPromotionLogs.deliveryStats')" min-width="320">
<template slot-scope="scope">
<div class="delivery-dashboard">
<div class="num-grid">
<div class="num-item">
<div class="label">{{ $t('autoPromotionLogs.totalCount') }}</div>
<div class="value primary">{{ scope.row.total_count }}</div>
</div>
<div class="num-item">
<div class="label">{{ $t('autoPromotionLogs.sentCount') }}</div>
<div class="value success">{{ scope.row.sent_count }}</div>
</div>
<div class="num-item">
<div class="label">{{ $t('autoPromotionLogs.failCount') }}</div>
<div :class="['value', scope.row.fail_count > 0 ? 'danger' : 'neutral']">
{{ scope.row.fail_count }}
</div>
</div>
<div class="num-item">
<div class="label">{{ $t('autoPromotionLogs.bounceCount') }}</div>
<div :class="['value', scope.row.bounce_count > 0 ? 'warning' : 'neutral']">
{{ scope.row.bounce_count }}
</div>
</div>
</div>
<!-- <div class="progress-container">
<el-progress
:percentage="getPercent(scope.row)"
:status="scope.row.fail_count > 0 || scope.row.bounce_count > 0 ? 'warning' : 'success'"
:stroke-width="6"
:show-text="false"
></el-progress>
</div> -->
<!-- <div class="stats-status">
<span v-if="scope.row.fail_count === 0 && scope.row.bounce_count === 0" class="safe-tag">
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.noDeliveryIssue') }}
</span>
<span v-else class="warning-tag"> <i class="el-icon-warning-outline"></i> {{ $t('autoPromotionLogs.deliveryIssue') }} </span>
<span class="percent-tag">{{ getPercent(scope.row) }}% {{ $t('autoPromotionLogs.completedText') }}</span>
</div> -->
</div>
</template>
</el-table-column>
<el-table-column prop="run_at" :label="$t('autoPromotionLogs.runAt')" width="160">
<template slot-scope="scope">
<span class="time-cell"> <i class="el-icon-date"></i> {{ scope.row.run_at }} </span>
</template>
</el-table-column>
<el-table-column :label="$t('autoPromotionLogs.status')" width="130" align="center">
<template slot-scope="scope">
<span :class="['status-badge', getTaskStatusClass(scope.row.state)]">
{{ getTaskStatusText(scope.row.state) }}
</span>
</template>
</el-table-column>
<el-table-column label="" width="100" align="center">
<template slot-scope="scope">
<div class="action-btns">
<el-button
type="text"
:icon="String(scope.row.state) === '5' ? 'el-icon-edit-outline' : 'el-icon-view'"
@click="previewRow(scope.row)"
>
{{ String(scope.row.state) === '5' ? $t('autoPromotionLogs.editAction') : $t('autoPromotionLogs.previewAction') }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
layout="total, sizes, prev, pager, next"
:current-page="query.pageIndex"
:page-size="query.pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</div>
</div>
<auto-promotion-wizard
mode="dialog"
:visible.sync="showWizardDialog"
:config="config"
:wizardStartDate.sync="wizardStartDate"
:selectedFieldIds.sync="selectedFieldIds"
:availableFields="availableFields"
:fieldsLoading="fieldsLoading"
:fieldsSaving="fieldsSaving"
:currentJournalName="currentJournalName"
:selectedTemplateThumbHtml="selectedTemplateThumbHtml"
:selectedTemplateName="selectedTemplateName"
:selectedStyleName="selectedStyleName"
:saving="saving"
:title="$t('autoPromotion.title')"
@open-template-selector="showTemplateDialog = true"
@confirm-fields="savePromotionFieldsNow"
@cancel="showWizardDialog = false"
@confirm="completeInitialization"
/>
<template-selector-dialog
v-if="showTemplateDialog"
:visible.sync="showTemplateDialog"
:journalId="selectedJournalId"
:journalLabel="currentJournalName"
:initial-style-id="templateDialogInitialStyleId"
:initial-template-id="templateDialogInitialTemplateId"
:return-source="'autoPromotion'"
@confirm="handleTemplateApply"
@close-all-dialogs="closeAllDialogs"
/>
<el-dialog :title="$t('autoPromotionLogs.previewEditTitle')" :visible.sync="showPreviewDialog" width="1200px" top="5vh">
<div class="mail-edit-wrapper" v-if="previewForm">
<el-form label-width="120px" size="small">
<el-form-item :label="$t('autoPromotionLogs.receiver')">
<el-input
v-model="previewForm.email"
disabled
:placeholder="$t('autoPromotionLogs.receiverImmutablePlaceholder')"
/>
</el-form-item>
<el-form-item :label="$t('autoPromotionLogs.subject')">
<el-input v-model="previewForm.subject" :placeholder="$t('autoPromotionLogs.subjectPlaceholder')" />
</el-form-item>
<el-form-item :label="$t('autoPromotionLogs.runAt') + ':'">
<el-date-picker
v-model="previewForm.run_at"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
:placeholder="$t('autoPromotionLogs.runAtPlaceholder')"
style="width: 100%"
/>
</el-form-item>
<CkeditorMail v-model="previewForm.content" />
</el-form>
</div>
<div slot="footer">
<el-button size="small" @click="showPreviewDialog = false">{{ $t('autoPromotionLogs.cancel') }}</el-button>
<el-button size="small" type="primary" @click="saveMailContent" :loading="saving">{{
$t('autoPromotionLogs.confirmEdit')
}}</el-button>
</div>
</el-dialog>
<PromotionDetailDrawer
v-model="drawerVisible"
:detail-data="currentRowData"
:is-conflict="currentRowData.isNotMyJournal"
@appealed="handleRefreshList"
/>
</div>
</template>
<script>
import CkeditorMail from '@/components/page/components/email/CkeditorMail.vue';
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue';
import AutoPromotionWizard from '@/components/page/components/autoPromotion/AutoPromotionWizard.vue';
import PromotionDetailDrawer from '@/components/page/components/autoPromotion/PromotionDetailDrawer.vue';
// 这里假设你已经定义了 API 地址
const API = {
getAllJournal: 'api/email_client/getPromotionJournalList',
listTemplates: 'api/mail_template/listTemplates',
getConfig: 'api/auto_promotion/getConfig',
saveConfig: 'api/email_client/setDefaultPromotion',
list: 'api/email_client/getTaskList',
toggleTaskState: 'api/email_client/setTaskState'
};
export default {
name: 'autoPromotion',
components: { TemplateSelectorDialog, AutoPromotionWizard, CkeditorMail, PromotionDetailDrawer },
data() {
return {
handleRefreshList: [],
drawerVisible: false,
currentRowData: {},
hidePage: false,
// 基础数据
journalOptions: [],
selectedJournalId: '',
templateOptions: [],
// 配置对象
config: {
initialized: false, // 核心:控制向导与列表的显示
enabled: false,
defaultTemplateId: '',
defaultTime: '09:00:00'
},
// 向导控制
wizardStep: 0,
wizardStartDate: '',
showTemplateDialog: false,
selectedTemplateThumbHtml: '',
selectedTemplateName: '',
selectedStyleName: '',
// 列表数据
query: { keyword: '', state: 'all', pageIndex: 1, pageSize: 15 },
list: [],
total: 0,
// 状态控制
loading: false,
saving: false,
currentJournalName: '',
// 向导弹窗(用于编辑已初始化配置)
showWizardDialog: false,
showPreviewDialog: false,
currentRow: null,
templateDialogInitialStyleId: '',
templateDialogInitialTemplateId: '',
togglingTaskId: '',
selectedFieldIds: [],
availableFields: [],
fieldsLoading: false,
fieldsSaving: false,
previewForm: {
id: '',
email: '',
run_at: '',
subject: '',
content: ''
}
};
},
computed: {},
created() {
this.initPage();
},
methods: {
handleTabClick(tab) {
// tab.name 对应的就是原来的 value ("0", "1" 等)
// 注意el-tabs 的 v-model 绑定的是字符串
this.query.state = tab.name;
this.handleStateChange(); // 触发你原有的搜索逻辑
},
getPercent(row) {
if (!row.total_count || row.total_count === 0) return 0;
// 计算已发送占比
return Math.floor((row.sent_count / row.total_count) * 100);
},
closeAllDialogs() {
// 点击“去新增模板”后:关闭当前页面所有可能的弹窗
this.showWizardDialog = false;
this.showTemplateDialog = false;
this.showPreviewDialog = false;
this.currentRow = null;
},
openLogs() {
const row = this.currentRowData || {};
const tid = row.task_id != null && row.task_id !== '' ? row.task_id : row.id;
if (tid == null || tid === '') {
this.$message.warning(this.$t('autoPromotionLogs.selectTaskForLogs'));
return;
}
this.drawerVisible = true;
},
// 点击列表“查看”图标:打开推送历史日志抽屉(与顶部「日志」按钮一致)
previewRow(row) {
this.currentRowData = row ? { ...row } : {};
this.openLogs();
},
// 保存修改后的邮件内容
async saveMailContent() {
this.saving = true;
try {
// 模拟调用 API
// await this.$api.post(API.updateMailDetail, this.previewForm);
// 同步到本地 list 中Mock 效果)
const index = this.list.findIndex((item) => item.id === this.previewForm.id);
if (index !== -1) {
this.$set(this.list[index], 'run_at', this.previewForm.run_at);
this.$set(this.list[index], 'subject', this.previewForm.subject);
this.$set(this.list[index], 'mail_content', this.previewForm.content);
}
this.$message.success(this.$t('autoPromotionLogs.mailContentSaved'));
this.showPreviewDialog = false;
} finally {
this.saving = false;
}
},
// 修改 handleEditRowTemplate 使其调用你已有的选择器
handleEditRowTemplate(row) {
this.currentRow = row;
this.showTemplateDialog = true;
},
// 处理表格内点击编辑图标
handleEditRowTemplate(row) {
this.currentRow = row; // 记录当前正在操作哪一行
this.templateDialogInitialStyleId = row && row.style_id ? String(row.style_id) : '';
this.templateDialogInitialTemplateId = row && row.template_id ? String(row.template_id) : '';
this.showTemplateDialog = true;
},
// 修改原有的 handleTemplateApply 方法,使其兼容向导和表格修改
handleTemplateApply(payload) {
const templateObj = payload && payload.template ? payload.template : {};
const styleObj = payload && payload.style ? payload.style : {};
const templateId = templateObj.id || (payload && payload.template_id) || '';
const templateName = templateObj.name || '';
const styleName = styleObj.name || '';
if (this.currentRow) {
// 说明是修改表格里的某一行
this.currentRow.template_id = String(templateId);
this.currentRow.template_name = templateName;
this.currentRow.style_name = styleName;
this.updateRow(this.currentRow); // 提交保存
this.currentRow = null; // 清空引用
} else {
// 说明是向导模式在选择
this.config.defaultTemplateId = String(templateId);
this.selectedTemplateName = templateName;
this.selectedStyleName = styleName;
this.selectedTemplateThumbHtml = `<div style="zoom:0.18; pointer-events:none;">${payload.html}</div>`;
}
this.showTemplateDialog = false;
},
getTemplateName(id) {
const found = this.templateOptions.find((t) => String(t.template_id) === String(id));
return found ? found.title : id || this.$t('autoPromotionLogs.templateNotSelected');
},
getStyleDisplay(row) {
if (!row) return '-';
if (row.style_name) return row.style_name;
if (row.style_id) return String(row.style_id);
return '-';
},
async initPage() {
this.hidePage = false;
var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
this.selectedJournalId = String(journal_id);
this.loading = true;
try {
await this.fetchJournalDetail();
if (this.selectedJournalId) {
this.loadPromotionFields(this.selectedJournalId);
}
if (this.config.initialized) {
await this.fetchTemplates();
await this.fetchList();
}
} finally {
this.loading = false;
}
},
_parseJournalDetail(data) {
const journalInfo = data.journal || data || {};
const tpl = journalInfo.template || data.template || {};
const style = journalInfo.style || data.style || {};
const tplId = String(journalInfo.default_template_id || '0');
const styleId = String(journalInfo.default_style_id || '0');
return {
title: journalInfo.title || journalInfo.journal_title || '',
templateId: tplId,
styleId: styleId,
templateName: tpl.name || tpl.title || journalInfo.default_template_name || '',
styleName: style.name || style.title || journalInfo.default_style_name || '',
html: `${style.header_html || ''}${tpl.body_html || ''}${style.footer_html || ''}`,
enabled: String(journalInfo.start_promotion || '0') === '1',
initialized: tplId !== '0' && styleId !== '0'
};
},
async fetchJournalDetail() {
if (!this.selectedJournalId) {
this.config.initialized = false;
return;
}
try {
var res = await this.$api.post('api/email_client/getPromotionJournalDetail', {
journal_id: this.selectedJournalId
});
var data = (res && res.data) || {};
if (!data.journal) {
this.$message.error('数据获取失败');
this.hidePage = true;
return;
}
var detail = this._parseJournalDetail(data);
var journalInfo = data.journal || data || {};
this.currentJournalName = detail.title || this.currentJournalName || '';
this.config = {
initialized: detail.initialized,
enabled: detail.enabled, // 回显“是否立即推广”
defaultTemplateId: detail.initialized ? detail.templateId : '',
defaultStyleId: detail.initialized ? detail.styleId : '',
defaultTime: journalInfo.default_time || this.config.defaultTime || ''
};
// 回显默认模板/风格(用于“修改默认配置”)
this.selectedTemplateName = detail.initialized ? detail.templateName || '' : '';
this.selectedStyleName = detail.initialized ? detail.styleName || '' : '';
this.selectedTemplateThumbHtml =
detail.initialized && detail.html
? `<div style="zoom:0.18; pointer-events:none; user-select:none;">${detail.html}</div>`
: '';
// 打开模板选择弹窗时,默认选中当前配置
this.templateDialogInitialTemplateId = detail.initialized ? String(detail.templateId || '') : '';
this.templateDialogInitialStyleId = detail.initialized ? String(detail.styleId || '') : '';
} catch (e) {
this.config.initialized = false;
}
},
// 打开向导弹窗:用于“修改期刊自动推广配置”
findArray(obj) {
if (Array.isArray(obj)) return obj;
if (!obj || typeof obj !== 'object') return null;
const keys = ['list', 'fields', 'rows', 'items', 'data', 'result'];
for (const k of keys) {
if (Array.isArray(obj[k])) return obj[k];
}
const values = Object.values(obj);
if (values.length && Array.isArray(values[0])) return values[0];
return null;
},
async loadPromotionFields(journalId) {
this.fieldsLoading = true;
this.availableFields = [];
this.selectedFieldIds = [];
try {
const availableRes = await this.$api.post('api/email_client/getAvailableFields', { journal_id: String(journalId) });
const availablePayload = (availableRes && availableRes.data) || availableRes || {};
let availableArr = this.findArray(availablePayload);
if (!availableArr) availableArr = Array.isArray(availablePayload) ? availablePayload : [];
this.availableFields = availableArr.map((item, idx) => {
const id = item.expert_fetch_id || item.fetch_id || item.id || item.field_id || (idx + 1);
const label = item.field || item.title || item.name || item.label || String(id);
return { id: String(id), label };
});
} catch (e) {
this.availableFields = [];
}
try {
const selectedRes = await this.$api.post('api/email_client/getJournalPromotionFields', { journal_id: String(journalId) });
const selectedPayload = (selectedRes && selectedRes.data) || selectedRes || {};
let selectedArr = this.findArray(selectedPayload);
if (selectedArr) {
this.selectedFieldIds = selectedArr.map((it) => String(it.expert_fetch_id || it.fetch_id || it.id || it.field_id || it));
} else if (typeof selectedPayload === 'string') {
this.selectedFieldIds = selectedPayload.split(',').map((s) => s.trim()).filter(Boolean);
} else if (typeof selectedPayload.fetch_ids === 'string') {
this.selectedFieldIds = selectedPayload.fetch_ids.split(',').map((s) => s.trim()).filter(Boolean);
}
} catch (e) {
this.selectedFieldIds = [];
}
this.fieldsLoading = false;
},
async savePromotionFieldsNow() {
if (!this.selectedJournalId) return;
this.fieldsSaving = true;
try {
await this.$api.post('api/email_client/setJournalPromotionFields', {
journal_id: String(this.selectedJournalId),
fetch_ids: (this.selectedFieldIds || []).join(',')
});
this.$message.success(this.$t('autoPromotion.fieldsSaved'));
} catch (e) {
this.$message.error(this.$t('autoPromotion.saveFailed'));
} finally {
this.fieldsSaving = false;
}
},
async openWizardDialog() {
this.wizardStep = 0;
if (this.config && this.config.start_date) {
this.wizardStartDate = this.config.start_date;
}
if (this.selectedJournalId) {
await this.loadPromotionFields(this.selectedJournalId);
}
this.showWizardDialog = true;
},
// 切换期刊逻辑
async handleJournalChange() {
this.loading = true;
// 切换期刊时:先清空“已选模板/缩略图/风格名”,再判断该期刊是否初始化
this.showTemplateDialog = false;
this.selectedTemplateThumbHtml = '';
this.selectedTemplateName = '';
this.selectedStyleName = '';
this.config.defaultTemplateId = '';
this.wizardStartDate = '';
this.wizardStep = 0;
try {
await this.fetchTemplates(); // 获取对应期刊的模版
await this.loadConfig(); // 获取该期刊的初始化状态
if (!this.config.initialized) {
// 未初始化:确保默认模板为空(保持向导第一步)
this.config.defaultTemplateId = '';
} else if (this.config.initialized) {
await this.fetchList(); // 已初始化的直接加载列表
}
} finally {
this.loading = false;
}
},
async fetchJournals() {
const res = await this.$api.post(API.getAllJournal, { user_id: localStorage.getItem('U_id') });
this.journalOptions = res.data.journals || [];
},
async fetchTemplates() {
const res = await this.$api.post(API.listTemplates, { journal_id: this.selectedJournalId });
this.templateOptions = (res.data.list || []).map((t) => ({
template_id: t.template_id || t.id,
title: t.title || t.name
}));
},
async loadConfig() {
await this.fetchJournalDetail();
},
// 接收模版选择:兼容向导与列表行编辑
handleTemplateApply(payload) {
// payload 兼容:可能是 string(html) 或对象
const html = payload && typeof payload === 'object' ? payload.html : String(payload || '');
const templateId =
payload && typeof payload === 'object'
? payload.template_id || payload.id || (payload.template && (payload.template.id || payload.template.template_id)) || ''
: '';
const styleId = payload && typeof payload === 'object' ? payload.style_id || (payload.style && payload.style.id) || '' : '';
const templateName = payload && typeof payload === 'object' && payload.template ? payload.template.name : '';
const styleName = payload && typeof payload === 'object' && payload.style ? payload.style.name : '';
// 必须选到模板template_id才允许“下一步”解除置灰
if (!templateId) {
this.$message.warning(this.$t('autoPromotionLogs.selectTemplateWarning'));
return;
}
if (this.currentRow) {
this.currentRow.template_id = String(templateId);
this.currentRow.template_name = templateName || this.getTemplateName(templateId);
this.currentRow.style_id = styleId ? String(styleId) : this.currentRow.style_id;
this.currentRow.style_name = styleName || this.currentRow.style_name || '';
this.updateRow(this.currentRow);
this.currentRow = null;
} else {
this.config.defaultTemplateId = String(templateId);
this.selectedTemplateName = templateName || '';
this.selectedStyleName = styleName || '';
// 生成缩略图 HTML禁用交互避免点击跳转
this.selectedTemplateThumbHtml = `<div style="zoom:0.18; pointer-events:none; user-select:none;">${html}</div>`;
}
this.templateDialogInitialStyleId = '';
this.templateDialogInitialTemplateId = '';
this.showTemplateDialog = false;
// this.$message.success(this.$t('autoPromotion.saved') || 'Saved');
},
// 最终提交初始化
async completeInitialization() {
this.saving = true;
try {
const payload = {
journal_id: String(this.selectedJournalId || ''),
default_template_id: String(this.config.defaultTemplateId || ''),
default_style_id: String(this.config.defaultStyleId || ''),
start_promotion: this.config.enabled ? '1' : '0'
};
this.config.initialized = true;
this.showWizardDialog = false;
this.fetchList();
await this.$api.post(API.saveConfig, payload);
await this.$api.post('api/email_client/setJournalPromotionFields', {
journal_id: String(this.selectedJournalId || ''),
fetch_ids: (this.selectedFieldIds || []).join(',')
});
this.$message.success(this.$t('autoPromotionLogs.configUpdated'));
} finally {
this.saving = false;
}
},
async saveConfig() {
// 用于管理模式下的快捷修改保存
await this.$api.post(API.saveConfig, {
journal_id: String(this.selectedJournalId || ''),
default_template_id: String(this.config.defaultTemplateId || ''),
default_style_id: String(this.config.defaultStyleId || ''),
start_promotion: this.config.enabled ? '1' : '0'
});
this.$message.success(this.$t('autoPromotionLogs.configUpdated'));
},
async fetchList() {
this.loading = true;
try {
const params = {
journal_id: String(this.selectedJournalId || ''),
page: Number(this.query.pageIndex || 1),
per_page: Number(this.query.pageSize || 15)
};
if (this.query.state !== 'all' && this.query.state !== '' && this.query.state != null) {
params.state = String(this.query.state);
}
const res = await this.$api.post(API.list, params);
const data = (res && res.data) || {};
const rawList = Array.isArray(data.list) ? data.list : [];
this.list = rawList.map((item, idx) => {
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
const state = String(item.state != null ? item.state : '');
return {
id: item.id || item.task_id || `task_${idx + 1}`,
task_id: String(item.task_id != null ? item.task_id : item.id || ''),
task_name: item.task_name || item.name || '',
scene: item.scene || '',
name: item.name || item.expert_name || '',
email: item.email || item.to_email || '',
template_id: String(item.template_id || item.default_template_id || ''),
style_id: String(item.style_id || item.default_style_id || ''),
style_name: item.style_name || item.default_style_name || '',
state: state,
paused: state === '2',
run_at: runAt,
total_count: Number(item.total_count || 0),
sent_count: Number(item.sent_count || 0),
fail_count: Number(item.fail_count || 0),
bounce_count: Number(item.bounce_count || 0),
min_interval: Number(item.min_interval || 0),
max_interval: Number(item.max_interval || 0),
send_start_hour: item.send_start_hour != null ? item.send_start_hour : '-',
send_end_hour: item.send_end_hour != null ? item.send_end_hour : '-'
};
});
this.total = Number(data.total || data.count || this.list.length || 0);
// 兼容后端返回 page/per_page
if (data.page != null) this.query.pageIndex = Number(data.page);
if (data.per_page != null) this.query.pageSize = Number(data.per_page);
} catch (e) {
this.list = [];
this.total = 0;
} finally {
this.loading = false;
}
},
// 生成用于页面展示的假数据mock
generateMockList() {
const tpl1 = this.templateOptions && this.templateOptions.length ? String(this.templateOptions[0].template_id) : '1';
const tpl2 = this.templateOptions && this.templateOptions.length > 1 ? String(this.templateOptions[1].template_id) : tpl1;
const baseDate = new Date();
// 使用固定的偏移,保证每次打开都有“未来”的时间
const d1 = new Date(baseDate.getTime() + 86400000 * 2);
const d2 = new Date(baseDate.getTime() + 86400000 * 3);
const d3 = new Date(baseDate.getTime() + 86400000 * 4);
const d4 = new Date(baseDate.getTime() + 86400000 * 5);
const d5 = new Date(baseDate.getTime() + 86400000 * 6);
const fmt = (d) => {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd} 09:00:00`;
};
const mk = (idx, name, email, template_id, style_id, style_name, paused, run_at) => ({
id: `mock_${idx}`,
name,
email,
template_id: String(template_id),
style_id: String(style_id),
style_name: style_name || '',
paused: Boolean(paused),
run_at
});
const list = [
mk(1, '张三', 'zhangsan@example.com', tpl1, 1, '简约风格', false, fmt(d1)),
mk(2, '李四', 'lisi@example.com', tpl2, 2, '商务风格', true, fmt(d2)),
mk(3, '王五', 'wangwu@example.com', tpl1, 1, '简约风格', false, fmt(d3)),
mk(4, '赵六', 'zhaoliu@example.com', tpl2, 3, '学术风格', false, fmt(d4)),
mk(5, '钱七', 'qianqi@example.com', tpl1, 2, '商务风格', true, fmt(d5))
];
return { list, total: list.length };
},
// 模板切换mock仅更新本行数据不做后端请求
updateRow(row) {
if (!row) return;
row.template_id = row.template_id == null ? '' : String(row.template_id);
this.$message.success(this.$t('autoPromotion.saved') || 'Saved');
},
// 暂停/开启(走接口)
async togglePause(row) {
if (!row) return;
const prevPaused = !!row.paused;
const prevState = String(row.state == null ? '' : row.state);
const nextPaused = !prevPaused;
const nextState = nextPaused ? '2' : '1';
const taskId = String(row.id || row.task_id || '');
if (!taskId) return;
try {
this.togglingTaskId = taskId;
row.paused = nextPaused;
row.state = nextState;
await this.$api.post(API.toggleTaskState, {
task_id: taskId,
journal_id: String(this.selectedJournalId || row.journal_id || ''),
state: nextState
});
this.$message.success(nextPaused ? this.$t('autoPromotionLogs.pauseSuccess') : this.$t('autoPromotionLogs.enableSuccess'));
await this.fetchList();
} catch (e) {
row.paused = prevPaused;
row.state = prevState;
this.$message.error((e && e.msg) || (e && e.message) || this.$t('autoPromotionLogs.operationFailed'));
} finally {
this.togglingTaskId = '';
}
},
// 删除mock从列表移除
deleteRow(row) {
if (!row) return;
this.list = (this.list || []).filter((item) => item !== row);
this.total = this.list.length;
this.$message.success(this.$t('autoPromotionLogs.deletedSuccess'));
},
handlePageChange(p) {
this.query.pageIndex = p;
this.fetchList();
},
handlePageSizeChange(size) {
this.query.pageSize = Number(size || 10);
this.query.pageIndex = 1;
this.fetchList();
},
handleSearch() {
this.query.pageIndex = 1;
this.fetchList();
},
handleStateChange() {
this.query.pageIndex = 1;
this.fetchList();
},
getTaskStatusText(state) {
const stateTextMap = {
0: this.$t('autoPromotionLogs.state0'),
1: this.$t('autoPromotionLogs.state1'),
2: this.$t('autoPromotionLogs.state2'),
3: this.$t('autoPromotionLogs.state3'),
4: this.$t('autoPromotionLogs.state4'),
5: this.$t('autoPromotionLogs.state5')
};
const key = Number(state);
return Object.prototype.hasOwnProperty.call(stateTextMap, key) ? stateTextMap[key] : '-';
},
getTaskStatusClass(state) {
const stateClassMap = {
0: 'status-draft', // Draft - 浅灰色
5: 'status-preparing', // Preparing - 橘色
1: 'status-running', // Running - 深蓝色
2: 'status-paused', // Paused - 深灰色
3: 'status-completed', // Completed - 绿色
4: 'status-cancelled' // Cancelled - 浅红色
};
return stateClassMap[Number(state)] || '';
}
}
};
</script>
<style scoped>
.auto-promo-container {
background: #f9fafc;
min-height: 90%;
}
.journal-header-card {
margin-bottom: 15px;
}
.config-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 向导样式 */
.wizard-card {
margin-top: 10px;
min-height: 500px;
}
.wizard-header {
padding: 20px 0 40px;
border-bottom: 1px solid #f0f2f5;
}
.wizard-body {
padding: 40px 0;
}
.step-content {
text-align: center;
}
.step-tips {
margin-bottom: 30px;
}
.step-tips i {
font-size: 40px;
color: #409eff;
margin-bottom: 15px;
}
.template-placeholder {
width: 520px;
max-width: 100%;
border: 2px dashed #dcdfe6;
margin: 0 auto;
border-radius: 8px;
padding: 14px;
cursor: pointer;
transition: 0.3s;
}
.template-placeholder:hover {
border-color: #409eff;
color: #409eff;
background: #f5f7fa;
}
.template-placeholder .inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 150px;
}
.template-placeholder .inner.selected {
display: block;
min-height: unset;
color: #333;
}
.template-placeholder .inner i {
font-size: 30px;
}
.thumb-box {
border: 1px solid #ebeef5;
border-radius: 6px;
overflow: hidden;
background: #fff;
height: 120px;
margin-bottom: 10px;
}
.thumb-content {
transform-origin: top left;
}
.selected-meta {
display: flex;
align-items: center;
gap: 10px;
}
.selected-text {
flex: 1;
min-width: 0;
}
.tpl-line {
color: #606266;
line-height: 20px;
}
.style-line {
color: #909399;
font-size: 12px;
line-height: 18px;
margin-top: 2px;
}
.wizard-footer {
text-align: center;
padding-top: 30px;
border-top: 1px solid #f0f2f5;
}
/* 管理模式样式 */
.config-card {
margin-bottom: 15px;
background: #fff;
}
.list-card {
background: #fff;
}
.list-header {
margin-bottom: 15px;
}
.pagination {
margin-top: 20px;
text-align: right;
}
.final-check {
margin-top: 15px;
line-height: 2;
}
/* 弹窗容器布局 */
.vertical-wizard-container {
display: flex;
gap: 30px;
padding: 10px;
}
.side-nav {
width: 120px;
flex-shrink: 0;
padding-top: 10px;
}
.main-form-content {
flex: 1;
max-height: 60vh; /* 开启滚动条防止内容过长 */
overflow-y: auto;
padding-right: 15px;
}
/* 每一个小节的样式 */
.form-section {
margin-bottom: 10px;
}
.section-title {
margin: 0 0 20px 0;
font-size: 15px;
color: #303133;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 8px;
color: #409eff;
font-size: 18px;
}
/* 优化后的模板选择框 */
.template-placeholder.mini-mode {
width: 100%;
padding: 15px;
min-height: 60px;
background: #f8f9fb;
}
.thumb-box-mini {
width: 100px;
height: 60px;
border: 1px solid #ddd;
overflow: hidden;
background: #fff;
border-radius: 4px;
margin-right: 15px;
}
.status-confirm-box {
padding: 15px;
background: #f0f9eb;
border-radius: 4px;
border: 1px inset #e1f3d8;
}
.selected-meta {
display: flex;
align-items: center;
}
/* 调整分割线间距 */
.el-divider--horizontal {
margin: 24px 0;
}
/* 表格内容排版 */
.user-info .name {
font-weight: bold;
color: #303133;
margin-bottom: 2px;
}
.user-info .email {
color: #909399;
font-size: 12px;
}
.template-info-cell {
display: flex;
align-items: center;
justify-content: space-between;
}
.tpl-main {
font-weight: 600;
color: #409eff;
font-size: 13px;
}
.tpl-sub {
color: #909399;
font-size: 12px;
margin-top: 2px;
}
.edit-icon-btn {
font-size: 16px;
margin-left: 8px;
color: #909399;
}
.edit-icon-btn:hover {
color: #409eff;
}
.time-cell {
color: #606266;
display: flex;
align-items: center;
gap: 5px;
}
/* 状态标签 (带圆点) */
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
}
.status-badge::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 6px;
}
.is-waiting {
color: #e67e22;
background: #fff3e6;
}
.is-waiting::before {
background: #e67e22;
}
.is-paused {
color: #909399;
background: #f4f4f5;
}
.is-paused::before {
background: #909399;
}
/* 操作按钮 */
.action-btns .el-button {
font-size: 13px;
padding: 0 4px;
color: #606266;
}
.action-btns .el-button [class*='el-icon-'] {
font-size: 12px;
}
.action-btns .el-button:hover {
color: #409eff;
}
.action-btns .delete-btn {
color: #f56c6c;
}
.action-btns .delete-btn:hover {
color: #f56c6c;
}
.action-btns .delete-btn.el-button--text {
color: #f56c6c !important;
}
/* 深度选择器处理表格边距 */
.custom-table >>> .el-table__cell {
padding: 12px 0;
}
/* 邮件编辑弹窗专用样式 */
.mail-edit-wrapper {
padding: 10px 20px;
}
.editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
}
/* 模拟富文本编辑器的工具栏感 (如果你没有引入富文本) */
.editor-container >>> .el-textarea__inner {
border: none;
padding: 15px;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', sans-serif;
line-height: 1.6;
}
/* 让 Dialog 的 footer 更靠右 */
.el-dialog__footer {
padding: 20px;
border-top: 1px solid #f0f2f5;
}
/* 列表内的主题预览小字(可选) */
.tpl-sub.mail-subject {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #8c939d;
}
/* 表格整体精致化 */
.exquisite-log-table {
border-radius: 4px;
overflow: hidden;
}
/* 任务列样式 */
.task-column .task-name {
font-weight: bold;
color: #334155;
margin-bottom: 4px;
}
.task-id-tags {
display: flex;
gap: 8px;
}
.id-tag {
font-size: 11px;
background: #f1f5f9;
color: #64748b;
padding: 1px 6px;
border-radius: 4px;
}
/* 策略列样式 */
.strategy-item {
font-size: 13px;
color: #475569;
margin-bottom: 2px;
}
.strategy-item i {
color: #3b82f6;
margin-right: 4px;
}
.mini-text {
font-size: 11px;
color: #94a3b8;
}
/* 进度条与数据展示统计 */
.delivery-stats-wrapper {
padding: 4px 0;
}
.stats-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 6px;
font-size: 12px;
}
.percent-text b {
font-size: 14px;
color: #1e293b;
}
.count-text {
color: #94a3b8;
}
.stats-footer {
margin-top: 6px;
display: flex;
gap: 12px;
font-size: 11px;
}
.error-label.red {
color: #f43f5e;
font-weight: 500;
}
.error-label.orange {
color: #f59e0b;
font-weight: 500;
}
.success-label {
color: #10b981;
}
/* 状态标签样式(根据你现有的类名微调) */
.status-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
/* 操作按钮 */
.delete-btn {
color: #f43f5e !important;
}
.delete-btn:hover {
color: #be123c !important;
}
.delivery-dashboard {
padding: 0px 0;
}
/* 数字网格布局 */
.num-grid {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
background: #f8fafc;
border-radius: 6px;
padding: 0px 10px 2px;
border: 1px solid #edf2f7;
}
.num-item {
text-align: center;
flex: 1;
}
.num-item .label {
font-size: 12px;
color: #333;
/* text-transform: uppercase; */
}
.num-item .value {
font-size: 15px;
font-weight: 700;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
/* 颜色体系 */
.value.primary {
color: #3b82f6;
} /* 蓝色 */
.value.success {
color: #10b981;
} /* 绿色 */
.value.danger {
color: #ef4444;
} /* 红色 */
.value.warning {
color: #f59e0b;
} /* 橙色 */
.value.neutral {
color: #cbd5e1;
} /* 灰色(0的时候) */
/* 进度条微调 */
.progress-container {
margin-bottom: 8px;
padding: 0 4px;
}
/* 底部标签 */
.stats-status {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
}
.safe-tag {
font-size: 11px;
color: #10b981;
background: #ecfdf5;
padding: 2px 8px;
border-radius: 10px;
}
.warning-tag {
font-size: 11px;
color: #f59e0b;
background: #fffbe6;
padding: 2px 8px;
border-radius: 10px;
}
.percent-tag {
font-size: 11px;
color: #64748b;
font-style: italic;
}
::v-deep .custom-table .el-table__cell {
padding: 6px 0 !important;
}
.filter-header-row {
display: flex;
align-items: center; /* 垂直居中对齐 */
gap: 16px; /* 胶囊和Search按钮之间的间距 */
margin-bottom: 16px;
background-color: transparent; /* 容器透明,不厚重 */
}
/* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */
.tmr-capsule-group {
flex: 1; /* 占据左侧空间 */
}
/* 强制隐藏默认灰色横线和卡片灰边 */
.tmr-capsule-group .el-tabs__header {
margin: 0 !important;
border-bottom: none !important;
}
.tmr-capsule-group .el-tabs__nav {
border: none !important;
}
/* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
height: 32px !important; /* 紧凑高度 */
line-height: 32px !important;
font-size: 13px; /* 紧凑字体 */
border: none !important; /* 彻底隐藏原生卡片边框 */
background-color: transparent; /* 默认状态下透明背景 */
color: #515a6e; /* 默认状态深灰文字,更专业 */
transition: all 0.2s ease-in-out;
padding: 0 16px !important; /* 适当内边距 */
margin-right: 8px; /* 每个 Tab 之间的间距 */
border-radius: 6px !important; /* 先统一圆角 */
overflow: visible; /* 确保选中的阴影显示完全 */
}
/* 首尾 Tab 的圆角处理 (形成整体感) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
margin-right: 0;
}
/* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:first-child):not(:last-child) {
border-radius: 0;
}
/* 4. **选中效果 (Active) - 胶囊浮动核心** */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
background-color: #ffffff !important; /* 白色底色 */
color: #409eff !important; /* 主题蓝色文字 */
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */
border-radius: 6px; /* 选中时恢复圆角 */
position: relative;
z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */
}
/* 5. 处理 Tab 之间的断层 (像素级微调) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
position: relative;
}
/* 首尾 Tab 之间的像素级处理 */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after {
content: '';
position: absolute;
right: -5px; /* 让 Tab 之间紧密无缝 */
top: 0;
height: 100%;
width: 1px;
background: transparent; /* 移除灰线,不丑 */
}
/* 6. 搜索按钮对齐微调 */
.filter-actions .el-button {
height: 32px; /* 与 Tab 高度一致 */
padding: 0 16px;
border-radius: 6px;
transition: all 0.2s;
}
/* 基础 Badge 样式 */
.status-badge {
display: inline-flex;
align-items: center;
padding: 0 10px;
height: 24px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
/* 状态圆点 (利用伪元素实现) */
.status-badge::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 6px;
}
/* 基础 Badge 结构 */
.status-badge {
display: inline-flex;
align-items: center;
padding: 0 12px;
height: 26px;
border-radius: 13px;
font-size: 12px;
font-weight: 500;
}
/* 状态点基础样式 */
.status-badge::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
}
/* 0: Draft - 浅灰色 */
.status-draft {
background-color: #f1f5f9;
color: #64748b;
}
.status-draft::before { background-color: #94a3b8; }
/* 5: Preparing - 橘色 */
.status-preparing {
background-color: #fff7ed;
color: #c2410c;
}
.status-preparing::before { background-color: #f97316; }
/* 1: Running - 深蓝色 */
.status-running {
background-color: #eff6ff;
color: #1d4ed8;
}
.status-running::before {
background-color: #3b82f6;
box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */
}
/* 2: Paused - 深灰色 */
.status-paused {
background-color: #f8fafc;
color: #334155;
border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */
}
.status-paused::before { background-color: #475569; }
/* 3: Completed - 绿色 */
.status-completed {
background-color: #f0fdf4;
color: #15803d;
}
.status-completed::before { background-color: #22c55e; }
/* 4: Cancelled - 浅红色 */
.status-cancelled {
background-color: #fef2f2;
color: #b91c1c;
}
.status-cancelled::before { background-color: #ef4444; }
</style>