自动化推广【约稿】

This commit is contained in:
2026-03-23 09:28:56 +08:00
parent f44b3910a4
commit 12760aaf44
21 changed files with 3482 additions and 559 deletions

View File

@@ -0,0 +1,457 @@
<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-breadcrumb-item>
</el-breadcrumb>
<el-button
type="text"
icon="el-icon-refresh"
class="auto-promo-refresh-btn"
:disabled="loading"
:title="$t('autoPromotion.refresh')"
@click="refreshAll"
/>
</div>
<div class="journal-grid">
<div v-for="journal in allJournals" :key="journal.journal_id" class="journal-item">
<div class="item-header">
<div class="journal-info">
<div class="icon-box">
<i class="el-icon-notebook-2"></i>
</div>
<span class="journal-name">{{ journal.title }}</span>
</div>
</div>
<div class="module-wrapper">
<div :class="['module-card', journal.solicit.initialized ? 'is-solicit-active' : 'is-empty']">
<div class="module-top">
<div class="module-top-left">
<span class="module-title">{{ $t('autoPromotion.autoSolicit') }}</span>
<el-button
v-if="journal.solicit.initialized"
type="text"
size="mini"
icon="el-icon-refresh"
class="config-inline-btn"
:disabled="promotionUpdating && String(promotionUpdatingJournalId) === String(journal.journal_id)"
@click="openWizardForJournal(journal)"
>
{{ $t('autoPromotion.editConfig') }}
</el-button>
</div>
<el-switch
v-model="journal.solicit.enabled"
active-color="#409EFF"
@change="handleSwitch(journal, 'solicit', $event)"
size="small"
:disabled="promotionUpdating && String(promotionUpdatingJournalId) === String(journal.journal_id)"
/>
</div>
<div class="module-content">
<template v-if="journal.solicit.initialized">
<div class="status-row" v-if="journal.solicit.enabled">
<span class="dot-running"></span>
<span class="status-text">{{ $t('autoPromotion.running') }}</span>
</div>
<div class="template-info-row">
<div>
<div class="template-info blue-text">{{ $t('autoPromotion.emailTemplate') }}: {{ journal.solicit.templateName || '-' }}</div>
<div class="template-info blue-text">{{ $t('autoPromotion.emailStyle') }}: {{ journal.solicit.styleName || '-' }}</div>
</div>
</div>
</template>
<div v-else class="empty-text">
{{ isSolicitConfigured(journal) ? $t('autoPromotion.notStarted') : $t('autoPromotion.notInitializedTip') }}
</div>
</div>
<div class="module-footer">
<button
class="action-btn btn-blue"
:disabled="promotionUpdating && String(promotionUpdatingJournalId) === String(journal.journal_id)"
@click="handleSolicitAction(journal)"
>
<span v-if="promotionUpdating && String(promotionUpdatingJournalId) === String(journal.journal_id)">
{{ $t('autoPromotion.loading') }}
</span>
<span v-else>
{{ getSolicitActionText(journal) }}
</span>
</button>
</div>
</div>
</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"
:currentJournalName="wizardJournal ? wizardJournal.title : ''"
:selectedTemplateThumbHtml="selectedTemplateThumbHtml"
:selectedTemplateName="selectedTemplateName"
:selectedStyleName="selectedStyleName"
:saving="saving"
:title="`${$t('autoPromotion.journalManage')}: ${wizardJournal ? wizardJournal.title : ''}`"
@open-template-selector="showTemplateDialog = true"
@cancel="showWizardDialog = false"
@confirm="saveWizardConfig"
/>
<template-selector-dialog
v-if="showTemplateDialog"
:visible.sync="showTemplateDialog"
:journalId="wizardJournal ? wizardJournal.journal_id : ''"
:journalLabel="wizardJournal ? wizardJournal.title : ''"
:return-source="'autoPromotion'"
@confirm="handleTemplateApply"
@close-all-dialogs="closeAllDialogs"
/>
</div>
</template>
<script>
import JournalDetailDialog from './components/autoPromotion/JournalDetailDialog.vue';
import AutoPromotionWizard from './components/autoPromotion/AutoPromotionWizard.vue';
import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue';
export default {
components: { JournalDetailDialog, AutoPromotionWizard, TemplateSelectorDialog },
data() {
return {
loading: true,
detailVisible: false,
currentJournal: null,
showWizardDialog: false,
showTemplateDialog: false,
saving: false,
promotionUpdating: false,
promotionUpdatingJournalId: '',
wizardJournal: null,
wizardStartDate: '',
wizardConfig: {
defaultTemplateId: '',
defaultStyleId: '',
enabled: false
},
selectedTemplateThumbHtml: '',
selectedTemplateName: '',
selectedStyleName: '',
templateNameMap: {},
allJournals: []
};
},
created() {
this.fetchPromotionJournals();
},
methods: {
closeAllDialogs() {
// 进入“新增模板/跳转列表页”前,先关闭当前页所有弹窗,减少卡顿
this.showWizardDialog = false;
this.showTemplateDialog = false;
this.detailVisible = 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 || (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 userId = localStorage.getItem('U_id') || '';
const res = await this.$api.post('api/email_client/getPromotionJournalList', { user_id: userId });
const raw = (res && res.data && (res.data.list || res.data.journals || res.data)) || [];
if (Array.isArray(raw) && raw.length) {
this.allJournals = await Promise.all(raw.map(async (item) => {
const journalId = item.journal_id || item.id;
const tplId = String(item.default_template_id || item.template_id || '0');
const styleId = String(item.default_style_id || item.style_id || '0');
const initialized = tplId !== '0' && styleId !== '0';
const enabled = String(item.start_promotion || item.enabled || '0') === '1';
// 初始简单结构:先用列表接口回填基础状态
let journalObj = {
journal_id: journalId,
title: item.title || item.journal_title || item.name || `Journal ${journalId}`,
solicit: {
enabled,
initialized,
templateId: tplId,
styleId: styleId,
templateName: '',
styleName: '',
html: ''
}
};
// 只有在“有模板id和风格id”时才拉取详情避免未初始化期刊的无意义请求
if (initialized) {
await this.refreshJournalByDetail(journalObj);
}
return journalObj;
}));
} else {
this.allJournals = [];
}
} catch (e) {
this.$message.error(this.$t('autoPromotion.loadListFailed'));
} finally {
this.loading = false;
}
},
// 右上角刷新:重新拉取列表,并重新拉取每个期刊对应的模版/风格详情
async refreshAll() {
this.templateNameMap = {};
this.allJournals = [];
await this.fetchPromotionJournals();
},
async refreshJournalByDetail(journal) {
if (!journal || !journal.journal_id) return;
const journalId = String(journal.journal_id);
try {
const res = await this.$api.post('api/email_client/getPromotionJournalDetail', { journal_id: journalId });
const detail = this._parseJournalDetail(res.data);
// 更新缓存(供原本的 fetchJournalTemplateAndStyleNames 逻辑兼容)
this.$set(this.templateNameMap, 'j_' + journalId, detail);
// 使用 $set 确保响应式更新
this.$set(journal, 'title', detail.title || journal.title);
this.$set(journal, 'solicit', {
...(journal.solicit || {}),
enabled: detail.enabled,
initialized: detail.initialized,
templateId: detail.templateId,
styleId: detail.styleId,
templateName: detail.templateName,
styleName: detail.styleName,
html: detail.html
});
} catch (e) {
console.error('Refresh detail failed:', e);
}
},
// 保持此方法以兼容 wizard 内的逻辑(实际上逻辑已被 _parseJournalDetail 整合)
async fetchJournalTemplateAndStyleNames(journalId) {
const cacheKey = 'j_' + journalId;
if (this.templateNameMap[cacheKey]) return this.templateNameMap[cacheKey];
try {
const res = await this.$api.post('api/email_client/getPromotionJournalDetail', { journal_id: String(journalId) });
const detail = this._parseJournalDetail(res.data);
this.$set(this.templateNameMap, cacheKey, detail);
return detail;
} catch (e) {
return { templateName: '', styleName: '', html: '' };
}
},
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 && journal.solicit && journal.solicit.enabled
? this.$t('autoPromotion.goManagePlan')
: this.$t('autoPromotion.startPlan');
},
async handleSolicitAction(journal) {
if (!this.isSolicitConfigured(journal)) {
this.openWizardForJournal(journal);
return;
}
// 未开启时:必须等待 setDefaultPromotion 成功后才继续打开详情
if (!journal.solicit.enabled) {
const journalId = journal && journal.journal_id ? String(journal.journal_id) : '';
this.promotionUpdating = true;
this.promotionUpdatingJournalId = journalId;
try {
const userId = localStorage.getItem('U_id') || '';
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: '1',
user_id: userId
});
this.$message.success(`${journal.title} ${this.$t('autoPromotion.startedPlan')}`);
await this.refreshJournalByDetail(journal);
this.openDetail(journal);
} catch (e) {
// 接口失败时不要做后续动作(避免进入详情后数据未就绪)
this.$message.error(this.$t('autoPromotion.updateRetryFailed'));
return;
} finally {
this.promotionUpdating = false;
this.promotionUpdatingJournalId = '';
}
return;
}
// 已开启:直接打开详情
this.openDetail(journal);
},
openWizardForJournal(journal) {
this.wizardJournal = journal;
const s = journal.solicit || {};
this.wizardConfig = {
defaultTemplateId: s.templateId !== '0' ? s.templateId : '',
defaultStyleId: s.styleId !== '0' ? s.styleId : '',
enabled: !!s.enabled
};
this.selectedTemplateName = s.templateName || '';
this.selectedStyleName = s.styleName || '';
this.selectedTemplateThumbHtml = `<div style="zoom:0.18; pointer-events:none; user-select:none;">${s.html || ''}</div>`;
this.showWizardDialog = true;
},
handleTemplateApply(payload) {
if (!payload || !payload.template_id || !payload.style_id) {
this.$message.warning(this.$t('autoPromotion.selectTemplateStyleFirst'));
return;
}
this.wizardConfig.defaultTemplateId = String(payload.template_id);
this.wizardConfig.defaultStyleId = String(payload.style_id);
this.selectedTemplateName = payload.template && payload.template.name ? payload.template.name : '';
this.selectedStyleName = payload.style && payload.style.name ? payload.style.name : '';
this.selectedTemplateThumbHtml = `<div style="zoom:0.18; pointer-events:none; user-select:none;">${payload.html || ''}</div>`;
this.showTemplateDialog = false;
},
async saveWizardConfig() {
if (!this.wizardJournal || !this.wizardJournal.solicit) return;
if (!this.wizardConfig.defaultTemplateId || !this.wizardConfig.defaultStyleId) {
this.$message.warning(this.$t('autoPromotion.selectTemplateStyleFirst'));
return;
}
this.saving = true;
try {
const userId = localStorage.getItem('U_id') || '';
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: userId
});
await this.refreshJournalByDetail(this.wizardJournal);
this.$message.success(this.$t('autoPromotion.configSaved'));
this.showWizardDialog = false;
} catch (e) {
this.$message.error(this.$t('autoPromotion.saveFailed'));
} finally {
this.saving = false;
}
},
openDetail(journal) {
this.$router.push({
path: '/autoPromotionLogs',
query: { journal_id: journal && journal.journal_id ? String(journal.journal_id) : '' }
});
},
async handleSwitch(journal, type, nextVal) {
if (!this.isSolicitConfigured(journal)) {
this.$set(journal.solicit, 'enabled', false);
this.$message.warning(this.$t('autoPromotion.notInitializedSwitchTip'));
return;
}
try {
this.promotionUpdating = true;
this.promotionUpdatingJournalId = journal && journal.journal_id ? String(journal.journal_id) : '';
const userId = localStorage.getItem('U_id') || '';
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: userId
});
this.$message.success(nextVal ? this.$t('autoPromotion.planEnabled') : this.$t('autoPromotion.planDisabled'));
} catch (e) {
this.$set(journal.solicit, 'enabled', !nextVal);
this.$message.error(this.$t('autoPromotion.updateFailed'));
} finally {
this.promotionUpdating = false;
this.promotionUpdatingJournalId = '';
}
}
}
};
</script>
<style scoped>
/* 保持您原始的 CSS 样式不变 */
.auto-promo-container { padding: 20px 24px; background-color: #f5f7f9; min-height: 100vh; font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto; }
.page-header { margin-bottom: 16px; display: flex; justify-content: flex-start; align-items: center; }
.auto-promo-refresh-btn { margin-left: 12px; }
.journal-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.journal-item { background: #fff; border-radius: 10px; padding: 16px; border: 1px solid #ebedf0; box-shadow: 0 2px 6px rgba(0,0,0,0.03); }
.item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.journal-info { display: flex; align-items: center; }
.icon-box { width: 28px; height: 28px; background: #edf5ff; border-radius: 4px; display: flex; align-items: center; justify-content: center; margin-right: 10px; }
.icon-box i { color: #409eff; font-size: 14px; }
.journal-name { font-size: 16px; font-weight: 600; color: #1f2d3d; }
.module-wrapper { display: grid; grid-template-columns: 1fr; }
.module-card { border-radius: 6px; padding: 14px; display: flex; flex-direction: column; min-height: 160px; border: 1px solid transparent; }
.is-solicit-active { background: #f4f9ff; border-color: #e1eeff; }
.is-empty { background: #fafafa; border-color: #f0f0f0; }
.module-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.module-top-left { display: flex; align-items: center; gap: 8px; }
.module-title { font-size: 14px; font-weight: 600; color: #333; }
.config-inline-btn { padding: 0; }
.module-content { flex: 1; }
.status-row { display: flex; align-items: center; margin-bottom: 4px; }
.status-text { font-size: 12px; font-weight: 600; color: #52c41a; }
.template-info { font-size: 12px; color: #bbb; margin-bottom: 2px; }
.blue-text { color: #409eff; }
.template-info-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.empty-text { font-size: 12px; color: #bbb; height: 60px; display: flex; align-items: center; }
.dot-running { width: 6px; height: 6px; background: #52c41a; border-radius: 50%; margin-right: 6px; }
.module-footer { margin-top: 12px; }
.action-btn { width: 100%; padding: 6px 0; border: none; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; transition: 0.2s; }
.btn-blue { background: #e8f3ff; color: #409eff; }
.btn-blue:hover { background: #409eff; color: #fff; }
@media (min-width: 1280px) { .journal-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1680px) { .journal-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
</style>