2105 lines
80 KiB
Vue
2105 lines
80 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 }}
|
||
<el-select
|
||
v-if="config.initialized && selectedJournalId"
|
||
v-model="headerPromotionFactoryId"
|
||
class="header-factory-task-select custom-pipeline-select"
|
||
size="small"
|
||
filterable
|
||
:loading="factoryTasksHeaderLoading"
|
||
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
|
||
@change="onHeaderFactoryTaskChange"
|
||
popper-class="pipeline-popper"
|
||
>
|
||
|
||
|
||
<el-option-group >
|
||
|
||
<el-option
|
||
v-for="opt in factoryTaskOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
class="pipeline-option"
|
||
>
|
||
<div class="option-content">
|
||
<div class="row-top">
|
||
<span class="task-title">{{ mapFactoryTaskTypeLabel(opt.task.type) }}</span>
|
||
<el-tag :type="getStatusType(opt.task.start_promotion)" size="mini" effect="plain" class="status-tag">
|
||
{{ opt.task.start_promotion==1 ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||
|
||
</el-tag>
|
||
</div>
|
||
<div class="row-bottom">
|
||
<span class="meta-item database">{{ mapFactoryExpertTypeLabel(opt.task.expert_type) }}</span>
|
||
<template v-if="opt.task.expert_type==5">
|
||
<span class="separator">•</span>
|
||
<span class="meta-item region">{{ opt.task.country_scope_label }}</span>
|
||
</template>
|
||
|
||
<span class="separator">•</span>
|
||
<span class="meta-item time">{{ opt.task.ctime_text }}</span>
|
||
</div>
|
||
</div>
|
||
</el-option>
|
||
</el-option-group>
|
||
</el-select>
|
||
<el-tag
|
||
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
|
||
:type="headerFactoryTaskRunning ? 'success' : 'info'"
|
||
size="small"
|
||
effect="plain"
|
||
class="header-factory-status-tag"
|
||
>
|
||
<i v-if="headerFactoryTaskRunning" class="el-icon-circle-check"></i>
|
||
<span v-else class="header-factory-status-dot"></span>
|
||
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||
</el-tag>
|
||
|
||
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
||
<i class="el-icon-edit"></i>
|
||
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
||
</el-button>
|
||
</div>
|
||
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
icon="el-icon-plus"
|
||
:loading="createTaskLoading"
|
||
@click="handleCreateEmailClientTask"
|
||
>
|
||
{{ $t('autoPromotion.emailClientCreateTaskBtn') }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<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"
|
||
:selectedCountryIds.sync="selectedCountryIds"
|
||
:availableFields="availableFields"
|
||
:availableCountries="availableCountries"
|
||
: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-countries="savePromotionCountriesNow"
|
||
@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" plain icon="el-icon-refresh" :loading="loading" @click="handleSearch">
|
||
{{ $t('autoPromotionLogs.logRefresh') }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table :data="list" border stripe size="small" class="custom-table exquisite-log-table">
|
||
<el-table-column
|
||
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"
|
||
:selectedCountryIds.sync="selectedCountryIds"
|
||
:availableFields="availableFields"
|
||
:availableCountries="availableCountries"
|
||
: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-countries="savePromotionCountriesNow"
|
||
@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"
|
||
/>
|
||
<promotion-factory-task-dialog
|
||
:visible.sync="showFactoryTaskDialog"
|
||
:initial-journal-id="factoryDialogInitialJournalId"
|
||
:initial-task="factoryDialogInitialTask"
|
||
@success="
|
||
fetchFactoryTasksForHeader();
|
||
fetchList();
|
||
fetchJournalDetail();
|
||
"
|
||
/>
|
||
<el-dialog :title="$t('autoPromotionLogs.previewEditTitle')" :visible.sync="showPreviewDialog" width="1200px" top="5vh">
|
||
<div class="mail-edit-wrapper" v-if="previewForm">
|
||
<el-form label-width="120px" size="small">
|
||
<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 PromotionFactoryTaskDialog from '@/components/page/components/autoPromotion/PromotionFactoryTaskDialog.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, PromotionFactoryTaskDialog, 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: 10 },
|
||
list: [],
|
||
total: 0,
|
||
|
||
// 状态控制
|
||
loading: false,
|
||
saving: false,
|
||
currentJournalName: '',
|
||
// 向导弹窗(用于编辑已初始化配置)
|
||
showWizardDialog: false,
|
||
showPreviewDialog: false,
|
||
currentRow: null,
|
||
templateDialogInitialStyleId: '',
|
||
templateDialogInitialTemplateId: '',
|
||
togglingTaskId: '',
|
||
selectedFieldIds: [],
|
||
selectedCountryIds: [],
|
||
availableFields: [],
|
||
availableCountries: [],
|
||
fieldsLoading: false,
|
||
fieldsSaving: false,
|
||
showFactoryTaskDialog: false,
|
||
factoryDialogInitialJournalId: '',
|
||
factoryDialogInitialTask: null,
|
||
routePromotionFactoryId: '',
|
||
headerPromotionFactoryId: '',
|
||
factoryTaskOptions: [],
|
||
factoryTasksHeaderLoading: false,
|
||
createTaskLoading: false,
|
||
previewForm: {
|
||
id: '',
|
||
email: '',
|
||
run_at: '',
|
||
subject: '',
|
||
content: ''
|
||
}
|
||
};
|
||
},
|
||
computed: {
|
||
/** 当前选中工厂任务是否运行中(与下拉 options 中 running 一致) */
|
||
headerFactoryTaskRunning() {
|
||
const id = String(this.headerPromotionFactoryId || '').trim();
|
||
if (!id || !this.factoryTaskOptions || !this.factoryTaskOptions.length) return null;
|
||
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||
if (opt && typeof opt.running === 'boolean') return opt.running;
|
||
return null;
|
||
},
|
||
/**
|
||
* 顶部 select 前缀展示用:与 headerPromotionFactoryId 对应的 option(含 task)。
|
||
* URL 回退插入的占位项可能没有 task,这里统一成可安全渲染的对象。
|
||
*/
|
||
currentSelectedTask() {
|
||
const emptyTask = { type: '', ctime_text: '' };
|
||
const id = String(this.headerPromotionFactoryId || '').trim();
|
||
if (!id || !Array.isArray(this.factoryTaskOptions) || !this.factoryTaskOptions.length) {
|
||
return { value: '', label: '', running: false, task: { ...emptyTask } };
|
||
}
|
||
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||
if (!opt) {
|
||
return { value: id, label: id, running: false, task: { ...emptyTask } };
|
||
}
|
||
const raw = opt.task && typeof opt.task === 'object' ? opt.task : {};
|
||
const ctimeText =
|
||
raw.ctime_text != null && String(raw.ctime_text).trim() !== ''
|
||
? String(raw.ctime_text).trim()
|
||
: this.formatFactoryHeaderTaskCreateTime(raw);
|
||
return {
|
||
value: opt.value,
|
||
label: opt.label,
|
||
running: typeof opt.running === 'boolean' ? opt.running : this.isFactoryHeaderTaskRunning(raw),
|
||
task: {
|
||
...raw,
|
||
type: raw.type != null ? String(raw.type) : '',
|
||
ctime_text: ctimeText || ''
|
||
}
|
||
};
|
||
}
|
||
},
|
||
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();
|
||
},
|
||
methods: {
|
||
mapFactoryTaskTypeLabel(type) {
|
||
const t = String(type || '');
|
||
if (t === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
|
||
if (t === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
|
||
if (t === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
|
||
if (t === '4') return this.$t('autoPromotion.autoSolicit');
|
||
return this.$t('autoPromotion.autoSolicit');
|
||
},
|
||
mapFactoryExpertTypeLabel(expertType) {
|
||
const t = String(expertType || '').trim();
|
||
if (t === '1') return this.$t('autoPromotion.factoryExpertChief');
|
||
if (t === '2') return this.$t('autoPromotion.factoryExpertBoard');
|
||
if (t === '3') return this.$t('autoPromotion.factoryExpertYoungBoard');
|
||
if (t === '4') return this.$t('autoPromotion.factoryExpertAuthor');
|
||
if (t === '5') return this.$t('autoPromotion.factoryExpertDb');
|
||
return '-';
|
||
},
|
||
getStatusType(status) {
|
||
|
||
return status==1?'success':'info';
|
||
},
|
||
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.showFactoryTaskDialog = 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) || '';
|
||
var pfid =
|
||
(this.$route.query && this.$route.query.promotion_factory_id) || (this.$route.query && this.$route.query.taskId) || '';
|
||
this.routePromotionFactoryId = String(pfid || '');
|
||
this.headerPromotionFactoryId = this.routePromotionFactoryId;
|
||
this.selectedJournalId = String(journal_id);
|
||
this.loading = true;
|
||
try {
|
||
await this.fetchJournalDetail();
|
||
if (this.selectedJournalId) {
|
||
this.loadPromotionFields(this.selectedJournalId);
|
||
}
|
||
if (this.config.initialized) {
|
||
await this.fetchTemplates();
|
||
await this.fetchFactoryTasksForHeader();
|
||
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;
|
||
}
|
||
},
|
||
|
||
/** 下拉项:场景类型文案(优先 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())}`;
|
||
},
|
||
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 expertTypePart = this.mapFactoryExpertTypeLabel(task.expert_type);
|
||
const datePart = this.formatFactoryHeaderTaskCreateTime(task);
|
||
return datePart ? `${typePart} | ${expertTypePart}${task.expert_type==5 ? ` | ${task.country_scope_label} ` :''} | ${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,task:task }
|
||
})
|
||
.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;
|
||
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;
|
||
},
|
||
parseCountryIdsFromPromotionPayload(selectedPayload) {
|
||
if (!selectedPayload || typeof selectedPayload !== 'object') return [];
|
||
const raw =
|
||
selectedPayload.country_fetch_ids != null
|
||
? selectedPayload.country_fetch_ids
|
||
: selectedPayload.country_ids != null
|
||
? selectedPayload.country_ids
|
||
: '';
|
||
if (typeof raw === 'string' && raw.trim()) {
|
||
return raw
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
.map(String);
|
||
}
|
||
return [];
|
||
},
|
||
journalPromotionFieldsPayload(journalId) {
|
||
return {
|
||
journal_id: String(journalId),
|
||
fetch_ids: (this.selectedFieldIds || []).join(','),
|
||
country_fetch_ids: (this.selectedCountryIds || []).join(',')
|
||
};
|
||
},
|
||
async loadPromotionFields(journalId) {
|
||
this.fieldsLoading = true;
|
||
this.availableFields = [];
|
||
this.availableCountries = [];
|
||
this.selectedFieldIds = [];
|
||
this.selectedCountryIds = [];
|
||
try {
|
||
const availableRes = await this.$api.post('api/email_client/getAvailableFields', { journal_id: String(journalId) });
|
||
const availablePayload = (availableRes && availableRes.data) || availableRes || {};
|
||
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 };
|
||
});
|
||
this.availableCountries = this.availableFields.map((x) => ({ id: String(x.id), label: x.label }));
|
||
} catch (e) {
|
||
this.availableFields = [];
|
||
this.availableCountries = [];
|
||
}
|
||
// 日志页不请求 getJournalPromotionFields(该接口在此场景不可用);已选字段/国家由向导内操作或他处回显
|
||
this.fieldsLoading = false;
|
||
},
|
||
async savePromotionFieldsNow() {
|
||
if (!this.selectedJournalId) return;
|
||
this.fieldsSaving = true;
|
||
try {
|
||
await this.$api.post(
|
||
'api/email_client/setJournalPromotionFields',
|
||
this.journalPromotionFieldsPayload(this.selectedJournalId)
|
||
);
|
||
this.$message.success(this.$t('autoPromotion.fieldsSaved'));
|
||
} catch (e) {
|
||
this.$message.error(this.$t('autoPromotion.saveFailed'));
|
||
} finally {
|
||
this.fieldsSaving = false;
|
||
}
|
||
},
|
||
async savePromotionCountriesNow() {
|
||
if (!this.selectedJournalId) return;
|
||
this.fieldsSaving = true;
|
||
try {
|
||
await this.$api.post(
|
||
'api/email_client/setJournalPromotionFields',
|
||
this.journalPromotionFieldsPayload(this.selectedJournalId)
|
||
);
|
||
this.$message.success(this.$t('autoPromotion.countriesSaved'));
|
||
} catch (e) {
|
||
this.$message.error(this.$t('autoPromotion.saveFailed'));
|
||
} finally {
|
||
this.fieldsSaving = false;
|
||
}
|
||
},
|
||
async openWizardDialog() {
|
||
this.wizardStep = 0;
|
||
if (this.config && this.config.start_date) {
|
||
this.wizardStartDate = this.config.start_date;
|
||
}
|
||
if (this.selectedJournalId) {
|
||
await this.loadPromotionFields(this.selectedJournalId);
|
||
}
|
||
this.showWizardDialog = true;
|
||
},
|
||
openFactoryTaskDialogFromLogs() {
|
||
this.factoryDialogInitialJournalId = this.selectedJournalId ? String(this.selectedJournalId) : '';
|
||
const routePid = String(this.routePromotionFactoryId || '').trim();
|
||
const first = this.list && this.list.length ? this.list[0] : null;
|
||
const matched = routePid
|
||
? (this.list || []).find((row) => {
|
||
const pid = row && row.promotion_factory_id != null ? String(row.promotion_factory_id) : '';
|
||
const rid = row && row.id != null ? String(row.id) : '';
|
||
const tid = row && row.task_id != null ? String(row.task_id) : '';
|
||
return pid === routePid || rid === routePid || tid === routePid;
|
||
}) || first
|
||
: first;
|
||
|
||
let task = null;
|
||
if (matched) {
|
||
task = { ...matched };
|
||
if (!routePid) {
|
||
task.promotion_factory_id =
|
||
matched.promotion_factory_id != null
|
||
? String(matched.promotion_factory_id)
|
||
: matched.id != null
|
||
? String(matched.id)
|
||
: matched.task_id != null
|
||
? String(matched.task_id)
|
||
: '';
|
||
}
|
||
}
|
||
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id,避免列表首行 id 与路由不一致
|
||
if (routePid) {
|
||
task = { ...(task || {}), promotion_factory_id: routePid };
|
||
}
|
||
|
||
this.factoryDialogInitialTask = task && Object.keys(task).length ? task : null;
|
||
this.showFactoryTaskDialog = true;
|
||
},
|
||
|
||
// 切换期刊逻辑
|
||
async handleJournalChange() {
|
||
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;
|
||
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;
|
||
}
|
||
},
|
||
|
||
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 || ''),
|
||
factory_id: String(this.routePromotionFactoryId || ''),
|
||
page: Number(this.query.pageIndex || 1),
|
||
per_page: Number(this.query.pageSize || 10)
|
||
};
|
||
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 : '');
|
||
const promotionFactoryId =
|
||
item.promotion_factory_id != null ? item.promotion_factory_id : item.id != null ? item.id : item.task_id;
|
||
return {
|
||
id: item.id || item.task_id || `task_${idx + 1}`,
|
||
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
||
task_id: String(item.task_id != null ? item.task_id : item.id || ''),
|
||
task_name: item.task_name || item.name || '',
|
||
scene: item.scene || '',
|
||
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 {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.config-bar .left {
|
||
width: calc(100% - 100px);
|
||
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 {
|
||
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;
|
||
}
|
||
|
||
.filter-actions {
|
||
margin-left: auto;
|
||
}
|
||
/* 基础 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;
|
||
}
|
||
/* 基础 Select 宽度 */
|
||
.custom-pipeline-select {
|
||
width: 580px;
|
||
}
|
||
|
||
/* 分组标题(el-option-group,避免在 ul 内放 div) */
|
||
.pipeline-popper .el-select-group__title {
|
||
padding: 10px 20px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
color: #909399;
|
||
letter-spacing: 1px;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
/* 单个选项容器 */
|
||
.pipeline-popper .el-select-dropdown__item {
|
||
height: auto; /* 允许高度自适应 */
|
||
padding: 12px 20px;
|
||
line-height: normal;
|
||
border-left: 3px solid transparent; /* 预留边框位置防止抖动 */
|
||
}
|
||
|
||
/* 选中状态 */
|
||
.pipeline-popper .el-select-dropdown__item.selected {
|
||
background-color: #f5f7fa;
|
||
border-left: 3px solid #409eff; /* 选中时的左侧蓝条 */
|
||
color: #606266; /* 覆盖 Element 默认高亮色 */
|
||
}
|
||
|
||
/* 选项内容布局 */
|
||
.pipeline-popper .option-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
/* 第一行:标题 + 状态标签 */
|
||
.pipeline-popper .option-content .row-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.pipeline-popper .option-content .row-top .task-title {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.pipeline-popper .option-content .row-top .status-tag {
|
||
border-radius: 12px;
|
||
padding: 0 8px;
|
||
height: 20px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
/* 第二行:元数据信息 */
|
||
.pipeline-popper .option-content .row-bottom {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 11px;
|
||
color: #909399;
|
||
}
|
||
|
||
.pipeline-popper .option-content .row-bottom .meta-item.database {
|
||
color: #5856d6;
|
||
font-weight: bold;
|
||
/* text-transform: uppercase; */
|
||
}
|
||
|
||
.pipeline-popper .option-content .row-bottom .separator {
|
||
margin: 0 6px;
|
||
color: #dcdfe6;
|
||
}
|
||
|
||
|
||
|
||
</style>
|