1593 lines
57 KiB
Vue
1593 lines
57 KiB
Vue
<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>
|