457 lines
19 KiB
Vue
457 lines
19 KiB
Vue
<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> |