1168 lines
44 KiB
Vue
1168 lines
44 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-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 :</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 :</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 :</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 :</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>
|