Files
tougao_web/src/components/page/autoPromotion.vue

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