Files
tougao_web/src/components/page/autoPromotion.vue
2026-04-29 09:20:47 +08:00

1168 lines
44 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="auto-promo-container" v-loading="loading" :element-loading-text="$t('autoPromotion.loading')">
<div class="page-header">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<i class="el-icon-s-promotion"></i> {{ $t('autoPromotion.title') }}
<el-button
type="text"
icon="el-icon-refresh"
class="auto-promo-refresh-btn"
:disabled="loading"
:title="$t('autoPromotion.refresh')"
@click="refreshAll"
/>
</el-breadcrumb-item>
</el-breadcrumb>
<div class="page-header-actions">
<el-button
type="primary"
size="small"
@click="openFactoryTaskDialogForJournal(null)"
>
{{ $t('autoPromotion.factoryCreateBtn') }}
</el-button>
</div>
</div>
<div class="journal-list-wrapper">
<div v-for="journal in allJournals" :key="journal.journal_id" class="journal-group-section">
<div class="journal-main-header">
<div class="journal-brand">
<div class="brand-icon">
<i class="el-icon-notebook-2"></i>
</div>
<h2 class="journal-title-text">{{ journal.title }}</h2>
</div>
</div>
<div v-if="getJournalDisplayTasks(journal).length" class="cards-grid">
<div
v-for="taskCard in getJournalDisplayTasks(journal)"
:key="taskCard.cardKey"
:class="['stat-card', taskCard.enabled ? 'is-active' : 'is-stopped']"
>
<div class="card-inner-header">
<div class="card-label-area">
<div class="card-main-title">
{{ taskCard.typeLabel }}
<!-- <span v-if="taskCard.showCount" class="scene-task-count">
({{ taskCard.totalCount }})
</span> -->
<el-button
v-if="taskCard.initialized"
type="text"
size="mini"
icon="el-icon-edit"
class="config-inline-btn"
@click="openFactoryTaskDialogForTask(journal, taskCard)"
>
{{ $t('autoPromotion.editConfig') }}
</el-button>
</div>
<div class="status-tag">
<span class="dot"></span>
{{ taskCard.enabled ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
</div>
</div>
<el-switch
:value="taskCard.enabled"
size="small"
active-color="#3DBB6A"
:disabled="!taskCard.taskId || !!taskCard.switchLoading"
@change="onFactoryTaskSwitchChange(journal, taskCard, $event)"
/>
</div>
<div class="card-content">
<div class="template-preview-box">
<div class="meta-row">
<i class="el-icon-message"
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">Template&nbsp;:</span></i
>
<span class="tpl-name tpl-name-single-line">{{ taskCard.templateName || 'No Template Configured' }}</span>
</div>
<div class="meta-row">
<i class="el-icon-user"
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">Type&nbsp;:</span></i
>
<span class="tpl-name">{{ taskCard.expertTypeLabel || '-' }}</span>
</div>
<!-- <div class="meta-row">
<i class="el-icon-collection-tag"
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">Promotion Fields&nbsp;:</span></i
>
<span class="tpl-name">{{ taskCard.fieldCountText }}</span>
</div> -->
<div class="meta-row" v-if="String(taskCard.expertType || '') === '5'">
<i class="el-icon-location-outline"
><span style="font-size: 11px; margin-left: 3px; margin-right: 3px">Country&nbsp;:</span></i
>
<span class="tpl-name">{{ taskCard.countryScopeLabel || '-' }}</span>
</div>
</div>
<!-- <div class="stats-container">
<div class="stat-box">
<span class="stat-label">SENT</span>
<span class="stat-value"><i class="el-icon-circle-check"></i> 1,240</span>
</div>
<div class="stat-box">
<span class="stat-label">PENDING</span>
<span class="stat-value warning"><i class="el-icon-time"></i> 150</span>
</div>
</div> -->
</div>
<div class="card-actions">
<button class="action-main-btn" @click="handleFactoryTaskAction(journal, taskCard)">
<span
v-if="
promotionUpdating &&
String(promotionUpdatingJournalId) === String(journal.journal_id) &&
String(promotionUpdatingTaskId) === String(taskCard.taskId || '')
"
>
<i class="el-icon-loading"></i>
</span>
<span v-else>{{ getFactoryTaskActionText(taskCard) }}</span>
</button>
<div class="task-card-footer-row">
<button
v-if="!taskCard.enabled && taskCard.taskId"
type="button"
class="promo-history-link"
@click.stop="openDetail(journal, taskCard)"
>
<span>{{ $t('autoPromotion.logs') }}</span>
<i class="el-icon-document"></i>
</button>
<span v-else class="promo-history-spacer"></span>
<div v-if="taskCard.createdAtText" class="task-create-time">
{{ $t('autoPromotion.createdAt') }}: {{ taskCard.createdAtText }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-task-tip">
<span>{{ $t('autoPromotion.noFactoryTask') }}</span>
<el-button type="text" size="mini" class="no-task-create-btn" @click="openFactoryTaskDialogForJournal(journal)">
<i class="el-icon-plus"></i>
{{ $t('autoPromotion.factoryCreateNow') }}
</el-button>
</div>
</div>
</div>
<journal-detail-dialog v-if="detailVisible" :visible.sync="detailVisible" :journal="currentJournal" />
<auto-promotion-wizard
mode="dialog"
:visible.sync="showWizardDialog"
:config="wizardConfig"
:wizardStartDate.sync="wizardStartDate"
:selectedFieldIds.sync="selectedFieldIds"
:selectedCountryIds.sync="selectedCountryIds"
:availableFields="availableFields"
:availableCountries="availableCountries"
:fieldsLoading="fieldsLoading"
:fieldsSaving="fieldsSaving"
:currentJournalName="wizardJournal ? wizardJournal.title : ''"
:selectedTemplateThumbHtml="selectedTemplateThumbHtml"
:selectedTemplateName="selectedTemplateName"
:selectedStyleName="selectedStyleName"
:saving="saving"
:title="`${$t('autoPromotion.journalManage')}: ${wizardJournal ? wizardJournal.title : ''}`"
@open-template-selector="openTemplateSelector"
@confirm-fields="savePromotionFieldsNow"
@confirm-countries="savePromotionCountriesNow"
@cancel="showWizardDialog = false"
@confirm="saveWizardConfig"
/>
<template-selector-dialog
v-if="showTemplateDialog"
:visible.sync="showTemplateDialog"
:journalId="wizardJournal ? wizardJournal.journal_id : ''"
:journalLabel="wizardJournal ? wizardJournal.title : ''"
: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"
:journal-options="allJournals"
@success="refreshAll"
/>
</div>
</template>
<script>
import JournalDetailDialog from './components/autoPromotion/JournalDetailDialog.vue';
import AutoPromotionWizard from './components/autoPromotion/AutoPromotionWizard.vue';
import PromotionFactoryTaskDialog from './components/autoPromotion/PromotionFactoryTaskDialog.vue';
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue';
export default {
components: { JournalDetailDialog, AutoPromotionWizard, PromotionFactoryTaskDialog, TemplateSelectorDialog },
data() {
return {
loading: true,
detailVisible: false,
currentJournal: null,
showWizardDialog: false,
showTemplateDialog: false,
saving: false,
promotionUpdating: false,
promotionUpdatingJournalId: '',
promotionUpdatingTaskId: '',
wizardJournal: null,
wizardStartDate: '',
wizardConfig: {
defaultTemplateId: '',
defaultStyleId: '',
enabled: false
},
selectedTemplateThumbHtml: '',
selectedTemplateName: '',
selectedStyleName: '',
selectedFieldIds: [],
selectedCountryIds: [],
availableFields: [],
availableCountries: [],
fieldsLoading: false,
fieldsSaving: false,
templateDialogInitialStyleId: '',
templateDialogInitialTemplateId: '',
templateNameMap: {},
factoryTemplateNameMap: {},
allJournals: [],
showFactoryTaskDialog: false,
factoryDialogInitialJournalId: '',
factoryDialogInitialTask: null
};
},
created() {
this.fetchPromotionJournals();
},
methods: {
savePromotionCountriesNow(){
},
// --- 逻辑部分完全保留你原有的实现 ---
openTemplateSelector() {
this.templateDialogInitialStyleId =
this.wizardConfig && this.wizardConfig.defaultStyleId ? String(this.wizardConfig.defaultStyleId) : '';
this.templateDialogInitialTemplateId =
this.wizardConfig && this.wizardConfig.defaultTemplateId ? String(this.wizardConfig.defaultTemplateId) : '';
this.showTemplateDialog = true;
},
closeAllDialogs() {
this.showWizardDialog = false;
this.showTemplateDialog = false;
this.detailVisible = false;
},
openFactoryTaskDialogForTask(journal, taskCard) {
this.factoryDialogInitialJournalId = journal && journal.journal_id != null ? String(journal.journal_id) : '';
this.factoryDialogInitialTask = taskCard && taskCard.rawTask ? { ...taskCard.rawTask } : null;
this.showFactoryTaskDialog = true;
},
openFactoryTaskDialogForJournal(journal) {
this.factoryDialogInitialJournalId = journal && journal.journal_id != null ? String(journal.journal_id) : '';
this.factoryDialogInitialTask = null;
this.showFactoryTaskDialog = true;
},
async onFactoryTaskSwitchChange(journal, taskCard, nextEnabled) {
const taskId =
taskCard && taskCard.taskId
? String(taskCard.taskId)
: taskCard && taskCard.rawTask && taskCard.rawTask.promotion_factory_id != null
? String(taskCard.rawTask.promotion_factory_id)
: '';
if (!taskId) return;
this.$set(taskCard, 'switchLoading', true);
try {
const res = await this.$api.post('api/promotion_factory/changePromotionAct', {
promotion_factory_id: taskId,
start_promotion: nextEnabled ? '1' : '0'
});
if (res && Number(res.code) === 0) {
this.$set(taskCard, 'enabled', !!nextEnabled);
if (taskCard.rawTask) {
this.$set(taskCard.rawTask, 'state', nextEnabled ? '1' : '0');
this.$set(taskCard.rawTask, 'start_promotion', nextEnabled ? '1' : '0');
}
return;
}
this.$message.error((res && res.msg) || this.$t('autoPromotion.saveFailed'));
await this.loadFactoryTaskSummaryByJournal(journal, localStorage.getItem('U_id') || '');
} catch (e) {
this.$message.error(this.$t('autoPromotion.saveFailed'));
await this.loadFactoryTaskSummaryByJournal(journal, localStorage.getItem('U_id') || '');
} finally {
this.$set(taskCard, 'switchLoading', false);
}
},
getFactoryTaskActionText(taskCard) {
return taskCard && taskCard.enabled ? this.$t('autoPromotion.goManagePlan') : this.$t('autoPromotion.startPlan');
},
async handleFactoryTaskAction(journal, taskCard) {
if (!journal || !taskCard) return;
const taskId = taskCard.taskId != null ? String(taskCard.taskId) : '';
this.promotionUpdating = true;
this.promotionUpdatingJournalId = journal.journal_id;
this.promotionUpdatingTaskId = taskId;
try {
if (!taskCard.enabled && taskId) {
const res = await this.$api.post('api/promotion_factory/changePromotionAct', {
promotion_factory_id: taskId,
start_promotion: '1'
});
if (!res || Number(res.code) !== 0) {
this.$message.error((res && res.msg) || this.$t('autoPromotion.saveFailed'));
return;
}
this.$set(taskCard, 'enabled', true);
if (taskCard.rawTask) {
this.$set(taskCard.rawTask, 'state', '1');
this.$set(taskCard.rawTask, 'start_promotion', '1');
}
}
this.openDetail(journal, taskCard);
} finally {
this.promotionUpdating = false;
this.promotionUpdatingJournalId = '';
this.promotionUpdatingTaskId = '';
}
},
_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 || (tplId !== '0' ? `模板#${tplId}` : ''),
styleName: style.name || style.title || journalInfo.default_style_name || (styleId !== '0' ? `风格#${styleId}` : ''),
html: `${style.header_html || ''}${tpl.body_html || ''}${style.footer_html || ''}`,
enabled: String(journalInfo.start_promotion || '0') === '1',
initialized: tplId !== '0' && styleId !== '0'
};
},
async fetchPromotionJournals() {
this.loading = true;
try {
const username = localStorage.getItem('U_name') || '';
const userId = localStorage.getItem('U_id') || '';
const res = await this.$api.post('api/Article/getJournal', { username: username });
let raw = [];
if (res && Number(res.code) === 0 && res.data) {
if (Array.isArray(res.data.journals)) {
raw = res.data.journals;
} else if (Array.isArray(res.data)) {
raw = res.data;
}
} else if (Array.isArray(res)) {
raw = res;
}
if (!Array.isArray(raw) || !raw.length) {
this.allJournals = [];
return;
}
const journalIdsForTpl = raw
.map((item) => item.journal_id || item.id)
.filter((id) => id != null && String(id).trim() !== '');
await this.prefetchFactoryTemplateNameMapsOnce(journalIdsForTpl);
this.allJournals = await Promise.all(
raw.map(async (item) => {
const journalId = item.journal_id || item.id;
let journalObj = {
journal_id: journalId,
abbr: item.abbr || '',
journal_icon: item.journal_icon || '',
title: item.title || item.journal_title || item.name || `Journal ${journalId}`,
solicit: {
enabled: false,
initialized: false,
templateId: '0',
styleId: '0',
templateName: '',
styleName: '',
html: ''
},
country_scope_label:'',
factoryTask: {
count: 0,
type: '',
typeLabel: this.$t('autoPromotion.autoSolicit')
},
factoryTasks: []
};
await Promise.all([
// this.refreshJournalByDetail(journalObj),
this.loadFactoryTaskSummaryByJournal(journalObj, userId)
]);
return journalObj;
})
);
} catch (e) {
this.$message.error(this.$t('autoPromotion.loadListFailed'));
} finally {
this.loading = false;
}
},
async loadFactoryTaskSummaryByJournal(journal, userId) {
if (!journal || !journal.journal_id) return;
try {
const res = await this.$api.post('api/promotion_factory/getList', {
journal_id: String(journal.journal_id),
user_id: String(userId || ''),
state: '-1'
});
const payload = (res && res.data) || {};
const list = this.findArray(payload) || this.findArray(res) || [];
const totalRaw = payload.total != null ? payload.total : res && res.total != null ? res.total : list.length;
const total = Number(totalRaw) || 0;
let latest = null;
if (Array.isArray(list) && list.length) {
latest = list
.slice()
.sort((a, b) => Number(b.ctime || b.create_time || b.time || 0) - Number(a.ctime || a.create_time || a.time || 0))[0];
}
const normalizedTasks = (Array.isArray(list) ? list : []).map((task, idx) => {
const type = task && task.type != null ? String(task.type) : '';
const fieldCount = this.getFactoryTaskFieldCount(task);
const startPromotion =
task && task.start_promotion != null && String(task.start_promotion).trim() !== ''
? String(task.start_promotion)
: null;
const state = task && task.state != null && String(task.state).trim() !== '' ? String(task.state) : null;
const enabled = String(startPromotion != null ? startPromotion : state != null ? state : '0') === '1';
return {
cardKey: `${journal.journal_id}_${task.promotion_factory_id || idx}`,
taskId: task.promotion_factory_id || '',
type: type,
typeLabel: this.mapFactoryTaskTypeLabel(type),
expertType: task && task.expert_type != null ? String(task.expert_type) : '',
expertTypeLabel: this.mapFactoryExpertTypeLabel(task && task.expert_type != null ? String(task.expert_type) : ''),
enabled: enabled,
switchLoading: false,
initialized: true,
templateName: this.getFactoryTemplateName(task, journal.journal_id),
fieldCount: fieldCount,
fieldCountText: String(fieldCount),
countryScopeLabel: task.country_scope_label || '-',
createdAtText: this.formatTaskCreateTime(task),
totalCount: total,
showCount: idx === 0 && total > 0,
rawTask: task
};
});
const firstTask = normalizedTasks.length ? normalizedTasks[0] : null;
this.$set(journal, 'factoryTasks', normalizedTasks);
this.$set(journal, 'factoryTask', {
count: total,
type: firstTask ? firstTask.type : '',
typeLabel: firstTask ? firstTask.typeLabel : this.$t('autoPromotion.autoSolicit'),
fieldCount: firstTask ? firstTask.fieldCount : 0
});
} catch (e) {
this.$set(journal, 'factoryTasks', []);
this.$set(journal, 'factoryTask', {
count: 0,
type: '',
typeLabel: this.$t('autoPromotion.autoSolicit'),
fieldCount: 0
});
}
},
/**
* 全页只请求一次 listTemplatesAll再按期刊写入 factoryTemplateNameMap。
* 若列表项带 journal_id 则按期刊拆分;否则用同一份 id->name 映射到当前页各期刊(与原先「每刊各请求一次」相比只发 1 次 HTTP
*/
async prefetchFactoryTemplateNameMapsOnce(journalIds) {
const ids = [...new Set((journalIds || []).map((id) => String(id).trim()).filter(Boolean))];
if (!ids.length) return;
try {
const res = await this.$api.post('api/mail_template/listTemplatesAll', { journal_id: ids[0] });
const payload = (res && res.data) || {};
const list = this.findArray(payload) || this.findArray(res) || [];
const flat = {};
const byJournal = {};
(Array.isArray(list) ? list : []).forEach((item) => {
const id = item && (item.template_id != null ? item.template_id : item.id);
if (id == null) return;
const name = String(item.title || item.name || '').trim();
if (!name) return;
const tid = String(id);
flat[tid] = name;
const jid =
item.journal_id != null
? String(item.journal_id).trim()
: item.journalId != null
? String(item.journalId).trim()
: item.j_id != null
? String(item.j_id).trim()
: '';
if (jid) {
if (!byJournal[jid]) byJournal[jid] = {};
byJournal[jid][tid] = name;
}
});
const perItemJournal = Object.keys(byJournal).length > 0;
ids.forEach((jid) => {
const m = perItemJournal ? byJournal[jid] || {} : { ...flat };
this.$set(this.factoryTemplateNameMap, jid, m);
});
} catch (e) {
console.error(e);
}
},
getFactoryTemplateName(task, journalId) {
const tplId = task && task.template_id != null ? String(task.template_id) : '';
const journalMap = this.factoryTemplateNameMap[String(journalId || '')] || {};
if (tplId && journalMap[tplId]) return journalMap[tplId];
if (task && task.template_name) return task.template_name;
return tplId ? `Template #${tplId}` : 'No Template Configured';
},
getFactoryTaskFieldCount(task) {
if (!task) return 0;
if (task.fetch_fields && typeof task.fetch_fields === 'object') {
const keys = Object.keys(task.fetch_fields).filter(Boolean);
if (keys.length) return keys.length;
}
const fetchIds = String(task.fetch_ids || '')
.split(',')
.map((s) => String(s).trim())
.filter(Boolean);
return fetchIds.length;
},
formatTaskCreateTime(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);
const pad = (v) => String(v).padStart(2, '0');
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(
dt.getMinutes()
)}:${pad(dt.getSeconds())}`;
},
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 '-';
},
getJournalDisplayTasks(journal) {
const list = journal && Array.isArray(journal.factoryTasks) ? journal.factoryTasks : [];
return list;
},
getFactoryTaskTypeLabel(journal) {
if (journal && journal.factoryTask && journal.factoryTask.typeLabel) {
return journal.factoryTask.typeLabel;
}
return this.$t('autoPromotion.autoSolicit');
},
async refreshAll() {
this.templateNameMap = {};
this.factoryTemplateNameMap = {};
this.allJournals = [];
await this.fetchPromotionJournals();
},
async refreshJournalByDetail(journal) {
if (!journal || !journal.journal_id) return;
try {
const res = await this.$api.post('api/email_client/getPromotionJournalDetail', { journal_id: String(journal.journal_id) });
const detail = this._parseJournalDetail(res.data);
this.$set(this.templateNameMap, 'j_' + journal.journal_id, detail);
this.$set(journal, 'title', detail.title || journal.title);
this.$set(journal, 'solicit', { ...(journal.solicit || {}), ...detail });
} catch (e) {
console.error(e);
}
},
isSolicitConfigured(journal) {
const s = journal && journal.solicit ? journal.solicit : {};
return !!(s.initialized && s.templateId !== '0' && s.styleId !== '0');
},
getSolicitActionText(journal) {
if (!this.isSolicitConfigured(journal)) return this.$t('autoPromotion.goConfig');
return journal.solicit.enabled ? this.$t('autoPromotion.goManagePlan') : this.$t('autoPromotion.startPlan');
},
findArray(obj) {
if (Array.isArray(obj)) return obj;
const keys = ['list', 'fields', 'rows', 'items', 'data', 'result'];
for (const k of keys) {
if (obj && Array.isArray(obj[k])) return obj[k];
}
return null;
},
async loadPromotionFields(journalId) {
this.fieldsLoading = true;
try {
const availableRes = await this.$api.post('api/email_client/getAvailableFields', { journal_id: String(journalId) });
let availableArr = this.findArray(availableRes.data || availableRes) || [];
this.availableFields = availableArr.map((item, idx) => ({
id: String(item.expert_fetch_id || item.fetch_id || item.id || idx),
label: item.field || item.title || item.name || 'Field'
}));
this.availableCountries = [...this.availableFields];
const selectedRes = await this.$api.post('api/email_client/getJournalPromotionFields', { journal_id: String(journalId) });
const selectedPayload = 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));
}
if (selectedPayload.country_fetch_ids) {
this.selectedCountryIds = selectedPayload.country_fetch_ids.split(',').filter(Boolean);
}
} catch (e) {
console.error(e);
}
this.fieldsLoading = false;
},
async savePromotionFieldsNow() {
await this.$api.post(
'api/email_client/setJournalPromotionFields',
this.journalPromotionFieldsPayload(this.wizardJournal.journal_id)
);
this.$message.success(this.$t('autoPromotion.fieldsSaved'));
},
journalPromotionFieldsPayload(journalId) {
return {
journal_id: String(journalId),
fetch_ids: (this.selectedFieldIds || []).join(','),
country_fetch_ids: (this.selectedCountryIds || []).join(',')
};
},
openWizardForJournal(journal) {
this.wizardJournal = journal;
const s = journal.solicit || {};
this.wizardConfig = { defaultTemplateId: s.templateId, defaultStyleId: s.styleId, enabled: !!s.enabled };
this.selectedTemplateName = s.templateName;
this.selectedStyleName = s.styleName;
this.selectedTemplateThumbHtml = `<div style="zoom:0.18;">${s.html || ''}</div>`;
this.loadPromotionFields(journal.journal_id);
this.showWizardDialog = true;
},
handleTemplateApply(payload) {
this.wizardConfig.defaultTemplateId = String(payload.template_id);
this.wizardConfig.defaultStyleId = String(payload.style_id);
this.selectedTemplateName = payload.template.name;
this.selectedTemplateThumbHtml = `<div style="zoom:0.18;">${payload.html || ''}</div>`;
this.showTemplateDialog = false;
},
async saveWizardConfig() {
this.saving = true;
try {
await this.$api.post('api/email_client/setDefaultPromotion', {
journal_id: String(this.wizardJournal.journal_id),
default_template_id: this.wizardConfig.defaultTemplateId,
default_style_id: this.wizardConfig.defaultStyleId,
start_promotion: this.wizardConfig.enabled ? '1' : '0',
user_id: localStorage.getItem('U_id')
});
await this.refreshJournalByDetail(this.wizardJournal);
this.showWizardDialog = false;
} catch (e) {
this.$message.error('Save failed');
} finally {
this.saving = false;
}
},
openDetail(journal,taskCard) {
const taskId = taskCard && taskCard.taskId != null ? String(taskCard.taskId) : '';
this.$router.push({
path: '/autoPromotionLogs',
query: {
journal_id: String(journal.journal_id),
promotion_factory_id: taskId
}
});
},
async handleSwitch(journal, type, nextVal) {
if (!this.isSolicitConfigured(journal)) {
this.$set(journal.solicit, 'enabled', false);
return;
}
this.promotionUpdating = true;
try {
await this.$api.post('api/email_client/setDefaultPromotion', {
journal_id: String(journal.journal_id),
default_template_id: String(journal.solicit.templateId),
default_style_id: String(journal.solicit.styleId),
start_promotion: nextVal ? '1' : '0',
user_id: localStorage.getItem('U_id')
});
} catch (e) {
this.$set(journal.solicit, 'enabled', !nextVal);
} finally {
this.promotionUpdating = false;
}
}
}
};
</script>
<style scoped>
/* 容器 */
.auto-promo-container {
padding: 0 10px 6px;
min-height: 100vh;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.journal-list-wrapper {
max-width: 1600px;
margin: 0 auto;
}
/* 顶部 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
/* 期刊分组区间 */
.journal-group-section {
margin-bottom: 20px;
}
.journal-main-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-left: 2px;
}
.brand-icon {
width: 22px;
height: 22px;
background: #3a579a;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 15px;
}
.journal-title-text {
font-size: 15px;
font-weight: 700;
color: #1a1a1a;
margin: 0;
margin-left: 6px;
}
.active-badge {
font-size: 12px;
font-weight: 800;
color: #6a737d;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.pulse-dot {
width: 8px;
height: 8px;
background-color: #409eff;
border-radius: 50%;
box-shadow: 0 0 0 rgba(64, 158, 255, 0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 6px rgba(64, 158, 255, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
}
}
.journal-brand {
display: flex;
align-items: center;
}
/* 卡片网格布局 */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 12px;
}
.no-task-tip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 14px 12px;
border: 1px dashed #dcdfe6;
border-radius: 8px;
color: #909399;
font-size: 13px;
background: #fafafa;
}
.no-task-create-btn {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
/* 统计卡片基础样式 */
.stat-card {
background: #ffffff;
border-radius: 8px;
border: 1px solid #e1e4e8;
padding: 10px;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.06);
}
.stat-card.is-active {
border-top: 3px solid #67c23a;
}
.stat-card.is-stopped {
border-top: 3px solid #909399;
}
.stat-card.is-disabled-style {
opacity: 0.8;
background: #fafbfc;
}
/* 卡片内部头部 */
.card-inner-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
width: 100%;
box-sizing: border-box;
gap: 8px;
}
.card-label-area {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
flex: 1;
min-width: 0;
overflow: hidden;
}
.card-main-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 700;
color: #1a1a1a;
min-width: 0;
white-space: nowrap;
overflow: hidden;
}
.config-inline-btn {
flex-shrink: 0;
white-space: nowrap;
}
.scene-task-count {
font-size: 12px;
color: #909399;
font-weight: 600;
}
.status-tag {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 700;
margin-top: 0px;
margin-left: 0;
flex-shrink: 0;
white-space: nowrap;
}
.is-active .status-tag {
color: #52c41a;
}
.is-stopped .status-tag {
color: #909399;
}
.status-tag .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
margin-right: 6px;
}
/* 内容区:与底部按钮同宽对齐 */
.card-content {
flex: 1;
width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
box-sizing: border-box;
}
.template-preview-box {
background: #f1f4f9;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 3px 8px;
/* display: flex;
align-items: center; */
gap: 0px;
margin-bottom: 8px;
color: #409eff;
width: 100%;
box-sizing: border-box;
}
.meta-row {
display: flex;
align-items: center;
min-width: 0;
}
.template-preview-box.is-empty {
color: #909399;
font-style: italic;
}
.tpl-name {
font-size: 12px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tpl-name-single-line {
flex: 1;
min-width: 0;
}
/* 统计项 */
.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 8px;
}
.stat-box {
background: #f8f9fa;
padding: 6px 8px;
border-radius: 4px;
border: 1px solid #f0f1f2;
}
.stat-label {
display: block;
font-size: 10px;
font-weight: 800;
color: #6a737d;
margin-bottom: 4px;
letter-spacing: 0.4px;
}
.stat-value {
font-size: 13px;
font-weight: 700;
color: #24292e;
display: flex;
align-items: center;
gap: 4px;
}
.stat-value i {
font-size: 15px;
color: #52c41a;
}
.stat-value.warning i {
color: #e6a23c;
}
/* 底部按钮区 */
.card-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-card-footer-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 16px;
}
.promo-history-spacer {
flex: 0 0 auto;
min-width: 0;
}
.promo-history-link {
flex: 0 1 auto;
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0;
padding: 0;
border: none;
background: transparent;
font-size: 11px;
font-weight: 600;
color: #409eff;
cursor: pointer;
line-height: 1.3;
text-align: left;
}
.promo-history-link i {
font-size: 12px;
}
.promo-history-link:hover {
color: #66b1ff;
text-decoration: underline;
}
.task-create-time {
flex-shrink: 0;
font-size: 11px;
color: #909399;
text-align: right;
}
.action-main-btn {
flex: 1;
height: 24px;
background: #eaf3ff;
color: #409eff;
border: 1px solid #d7e7ff;
border-radius: 4px;
font-weight: 600;
font-size: 12px;
line-height: 22px;
cursor: pointer;
transition: all 0.2s;
}
.action-main-btn:hover {
background: #dcecff;
border-color: #c4ddff;
color: #2f89ea;
}
.is-active .action-main-btn {
background: #3dbb6a;
color: #ffffff;
border-color: #3dbb6a;
}
.is-active .action-main-btn:hover {
background: #31a85b;
border-color: #31a85b;
color: #ffffff;
}
.action-main-btn.secondary {
background: transparent;
border: 2px solid #1a1a1a;
color: #1a1a1a;
}
.action-edit-btn {
width: 30px;
height: 30px;
background: white;
border: 1px solid #e1e4e8;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
color: #6a737d;
display: flex;
align-items: center;
justify-content: center;
}
.action-edit-btn:hover {
background: #f6f8fa;
color: #409eff;
border-color: #409eff;
}
/* 响应式 */
@media (max-width: 1200px) {
.cards-grid {
grid-template-columns: 1fr;
}
}
</style>