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

2172 lines
81 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="warning" plain size="small" @click="openFactoryBatchImportDialog" style="display: none;">
{{ $t('autoPromotion.factoryBatchImportBtn') }}
</el-button>
<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"
/>
<el-dialog
:title="$t('autoPromotion.factoryBatchImportTitle')"
:visible.sync="batchImportVisible"
width="1200px"
append-to-body
:close-on-click-modal="false"
custom-class="factory-batch-import-dialog"
@closed="onFactoryBatchImportDialogClosed"
>
<div class="batch-import-layout">
<aside class="batch-import-journal-rail" v-loading="batchImportJournalPickerLoading">
<div class="batch-import-rail-title">{{ $t('autoPromotion.factoryBatchImportJournalPick') }}</div>
<div class="batch-import-journal-scroll">
<div v-if="!batchImportJournalPickerLoading && !batchImportJournalPickerList.length" class="batch-import-journal-empty">
{{ $t('autoPromotion.factoryBatchImportJournalEmpty') }}
</div>
<div v-else class="batch-import-journal-grid batch-import-journal-grid--rail">
<div
v-for="j in batchImportJournalPickerList"
:key="'batch_j_' + j.journal_id"
class="batch-import-journal-card"
:class="{ 'is-active': String(batchImportJournalId) === String(j.journal_id) }"
@click="onBatchImportJournalCardClick(j)"
>
<div class="batch-import-mini-cover-wrapper">
<el-image :src="j.journal_icon" fit="cover" class="batch-import-mini-cover">
<div slot="error" class="batch-import-image-slot"><i class="el-icon-picture-outline"></i></div>
</el-image>
<div v-if="String(batchImportJournalId) === String(j.journal_id)" class="batch-import-mini-badge">
<i class="el-icon-check"></i>
</div>
</div>
<div
class="batch-import-mini-abbr"
:class="{ 'is-active': String(batchImportJournalId) === String(j.journal_id) }"
>
{{ j.abbr || j.title }}
</div>
</div>
</div>
</div>
<div class="batch-import-journal-id-row batch-import-journal-id-row--rail">
<span class="batch-import-id-label">{{ $t('autoPromotion.factoryBatchImportJournalId') }}</span>
<el-input
v-model="batchImportJournalId"
clearable
size="small"
class="batch-import-journal-id-input"
:placeholder="$t('autoPromotion.factoryBatchImportJournalManualPlaceholder')"
@input="onBatchImportJournalIdInput"
/>
</div>
</aside>
<div class="batch-import-main">
<p class="batch-import-hint-compact">{{ $t('autoPromotion.factoryBatchImportHintShort') }}</p>
<el-row :gutter="12" class="batch-import-common-fields">
<el-col :span="12">
<div class="batch-import-field-label">{{ $t('autoPromotion.factoryBatchImportTemplateId') }}</div>
<el-input v-model="batchImportTemplateId" clearable size="small" :placeholder="$t('autoPromotion.factoryBatchImportTemplatePlaceholder')" />
</el-col>
<el-col :span="12">
<div class="batch-import-field-label">{{ $t('autoPromotion.factoryBatchImportStyleId') }}</div>
<el-input v-model="batchImportStyleId" clearable size="small" :placeholder="$t('autoPromotion.factoryBatchImportStylePlaceholder')" />
</el-col>
</el-row>
<div class="batch-import-fetch-block">
<div class="batch-import-field-row-head">
<span class="batch-import-field-label">{{ $t('autoPromotion.factoryBatchImportFetchIdsLabel') }}</span>
<span class="batch-import-fetch-count">{{ $t('autoPromotion.selectedCount', { count: batchImportSelectedFieldIds.length }) }}</span>
<el-button
type="primary"
plain
size="mini"
icon="el-icon-refresh"
:disabled="!String(batchImportJournalId || '').trim()"
:loading="batchImportFieldsLoading"
@click="loadBatchImportAvailableFields"
>
{{ $t('autoPromotion.factoryBatchImportLoadFields') }}
</el-button>
</div>
<div v-if="batchImportAvailableFields.length" class="batch-import-field-toolbar">
<el-input
v-model="batchImportFieldSearchText"
size="small"
clearable
class="batch-import-field-search"
prefix-icon="el-icon-search"
:placeholder="$t('autoPromotion.fieldSearchPlaceholder')"
/>
<el-button size="mini" @click="selectBatchImportFieldsBulk">{{ $t('autoPromotion.selectAll') }}</el-button>
<el-button size="mini" @click="clearBatchImportFieldSelection">{{ $t('autoPromotion.clearAll') }}</el-button>
</div>
<div v-if="batchImportAvailableFields.length" class="batch-import-field-checkbox-wrap">
<el-checkbox-group v-model="batchImportSelectedFieldIds" @change="onBatchImportFieldIdsGroupChange">
<el-checkbox v-for="f in batchImportFilteredFields" :key="'bf_' + f.id" :label="f.id">
{{ f.label }} ({{ f.id }})
</el-checkbox>
</el-checkbox-group>
<div
v-if="String(batchImportFieldSearchText || '').trim() && !batchImportFilteredFields.length"
class="batch-import-field-empty"
>
{{ $t('autoPromotion.noFieldMatch') }}
</div>
</div>
<div class="batch-import-field-label batch-import-fetch-text-label">{{ $t('autoPromotion.factoryBatchImportFetchIdsManual') }}</div>
<el-input
v-model="batchImportFetchIdsOverride"
type="textarea"
:rows="2"
size="small"
class="batch-import-fetch-textarea"
:placeholder="$t('autoPromotion.factoryBatchImportFetchIdsPlaceholder')"
@blur="syncBatchImportSelectionFromFetchIdsString"
/>
</div>
<el-table
v-if="batchImportAccounts.length"
:data="batchImportAccounts"
:show-header="false"
border
size="mini"
class="batch-import-accounts-table"
max-height="220"
>
<el-table-column prop="j_email_id" :label="$t('autoPromotion.factoryBatchImportColEmailId')" width="100" />
<el-table-column :label="$t('autoPromotion.factoryBatchImportColAddress')" min-width="220">
<template slot-scope="scope">{{ batchImportAccountDisplay(scope.row) }}</template>
</el-table-column>
</el-table>
<div class="batch-import-json-toolbar">
<el-button size="small" type="primary" plain @click="syncBatchImportUiToJson">{{
$t('autoPromotion.factoryBatchImportSyncToJson')
}}</el-button>
<el-button size="small" @click="syncBatchImportJsonToUiFirst">{{ $t('autoPromotion.factoryBatchImportSyncFromJson') }}</el-button>
<el-checkbox v-model="batchImportSyncIncludeEmails" class="batch-import-sync-email-check">{{
$t('autoPromotion.factoryBatchImportSyncIncludeEmails')
}}</el-checkbox>
</div>
<el-input
v-model="batchImportText"
type="textarea"
:rows="14"
class="batch-import-textarea"
spellcheck="false"
/>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="small" @click="batchImportVisible = false">{{ $t('autoPromotion.cancel') }}</el-button>
<el-button type="primary" size="small" :loading="batchImporting" @click="runPromotionFactoryBatchImport">{{
$t('autoPromotion.factoryBatchImportRun')
}}</el-button>
</span>
</el-dialog>
</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,
/** 临时JSON 数组批量创建 promotion_factory与图二提交字段一致 */
batchImportVisible: false,
batchImportText: '',
batchImporting: false,
/** 批量创建:与 JSON 合并进接口(非空则覆盖每条里的同名字段) */
batchImportJournalId: '',
batchImportTemplateId: '',
batchImportStyleId: '',
/** 批量弹窗按期刊拉取邮箱账号api/email_client/getAccounts */
batchImportAccounts: [],
batchImportAccountsLoading: false,
/** 批量弹窗期刊封面选择api/Journal/getAllJournal */
batchImportJournalPickerList: [],
batchImportJournalPickerLoading: false,
/** 推广领域 fetch_ids勾选 + 文本合并覆盖每条 JSON */
batchImportAvailableFields: [],
batchImportSelectedFieldIds: [],
batchImportFetchIdsOverride: '',
batchImportFieldsLoading: false,
batchImportFieldSearchText: '',
_batchImportAccountsFetchTimer: null,
/** 写入 JSON 时是否用当前已加载邮箱列表覆盖每条 email_ids */
batchImportSyncIncludeEmails: true
};
},
created() {
this.fetchPromotionJournals();
},
computed: {
/** 推广领域列表:按名称 / ID 关键字筛选(支持空格、逗号多关键词 OR */
batchImportFilteredFields() {
const raw = String(this.batchImportFieldSearchText || '').trim();
const list = this.batchImportAvailableFields || [];
if (!raw) return list;
const tokens = raw
.split(/[,\s]+/)
.map((t) => t.trim().toLowerCase())
.filter(Boolean);
if (!tokens.length) return list;
return list.filter((f) => {
const label = String(f.label || '').toLowerCase();
const id = String(f.id || '').toLowerCase();
return tokens.some((t) => label.indexOf(t) !== -1 || id.indexOf(t) !== -1);
});
}
},
watch: {
batchImportJournalId() {
this.scheduleBatchImportAccountsFetch();
},
batchImportVisible(val) {
if (val) {
this.$nextTick(() => {
this.scheduleBatchImportAccountsFetch();
});
} else {
this.clearBatchImportAccountsFetchTimer();
}
}
},
methods: {
async savePromotionCountriesNow() {
if (!this.wizardJournal || !this.wizardJournal.journal_id) return;
await this.$api.post(
'api/email_client/setJournalPromotionFields',
this.journalPromotionFieldsPayload(this.wizardJournal.journal_id)
);
this.$message.success(this.$t('autoPromotion.countriesSaved'));
},
openFactoryBatchImportDialog() {
if (!this.batchImportText || !String(this.batchImportText).trim()) {
this.batchImportText = this.defaultFactoryBatchImportSample();
}
this.batchImportVisible = true;
this.loadBatchImportJournalPicker();
},
async loadBatchImportJournalPicker() {
this.batchImportJournalPickerLoading = true;
try {
const res = await this.$api.post('api/Journal/getAllJournal', {});
let raw = [];
if (res && Number(res.code) === 0) {
if (res.data && Array.isArray(res.data.journals)) raw = res.data.journals;
else if (Array.isArray(res.data)) raw = res.data;
}
let mapped = (Array.isArray(raw) ? raw : []).map((j) => ({
journal_id: j.journal_id != null ? j.journal_id : j.id,
title: j.title || j.name || '',
abbr: j.abbr || j.short_name || j.abbreviation || j.code || '',
journal_icon: j.journal_icon || j.cover_url || j.cover || j.icon || ''
}));
if (!mapped.length && Array.isArray(this.allJournals) && this.allJournals.length) {
mapped = this.allJournals.map((j) => ({
journal_id: j.journal_id,
title: j.title || '',
abbr: j.abbr || '',
journal_icon: j.journal_icon || ''
}));
}
this.batchImportJournalPickerList = mapped;
const cur = String(this.batchImportJournalId || '').trim();
if (!cur && mapped.length) {
this.batchImportJournalId = String(mapped[0].journal_id);
}
} catch (e) {
console.error(e);
this.batchImportJournalPickerList = [];
} finally {
this.batchImportJournalPickerLoading = false;
}
},
onBatchImportJournalCardClick(j) {
if (!j || j.journal_id == null) return;
const id = String(j.journal_id);
if (id === String(this.batchImportJournalId || '')) return;
this.batchImportJournalId = id;
this.batchImportAccounts = [];
this.clearBatchImportFieldPicker();
},
onBatchImportJournalIdInput() {
this.batchImportAccounts = [];
this.clearBatchImportFieldPicker();
},
clearBatchImportFieldPicker() {
this.batchImportAvailableFields = [];
this.batchImportSelectedFieldIds = [];
this.batchImportFetchIdsOverride = '';
this.batchImportFieldSearchText = '';
},
async loadBatchImportAvailableFields() {
const jid = String(this.batchImportJournalId || '').trim();
if (!jid) {
this.$message.warning(this.$t('autoPromotion.factoryBatchImportNeedJournalForFields'));
return;
}
this.batchImportFieldsLoading = true;
try {
const res = await this.$api.post('api/email_client/getAvailableFields', { journal_id: jid });
const availableArr = this.findArray(res.data || res) || [];
this.batchImportAvailableFields = 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.syncBatchImportSelectionFromFetchIdsString();
} catch (e) {
console.error(e);
this.batchImportAvailableFields = [];
} finally {
this.batchImportFieldsLoading = false;
}
},
onBatchImportFieldIdsGroupChange() {
this._syncBatchImportFetchIdsStringFromSelected();
},
_syncBatchImportFetchIdsStringFromSelected() {
const order = (this.batchImportAvailableFields || []).map((f) => String(f.id));
const set = new Set((this.batchImportSelectedFieldIds || []).map(String));
const ordered = order.filter((id) => set.has(id));
const extra = (this.batchImportSelectedFieldIds || []).map(String).filter((id) => order.indexOf(id) === -1);
this.batchImportFetchIdsOverride = [...ordered, ...extra].join(',');
},
syncBatchImportSelectionFromFetchIdsString() {
const raw = String(this.batchImportFetchIdsOverride || '');
const parts = raw.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
if (!this.batchImportAvailableFields.length) return;
const availIds = new Set((this.batchImportAvailableFields || []).map((f) => String(f.id)));
this.batchImportSelectedFieldIds = parts.filter((id) => availIds.has(String(id)));
},
/** 有搜索时全选当前筛选结果,无搜索时全选全部(与创建任务里「选择领域」一致) */
selectBatchImportFieldsBulk() {
const hasSearch = String(this.batchImportFieldSearchText || '').trim();
const source = hasSearch ? this.batchImportFilteredFields || [] : this.batchImportAvailableFields || [];
const set = new Set((this.batchImportSelectedFieldIds || []).map(String));
source.forEach((f) => set.add(String(f.id)));
this.batchImportSelectedFieldIds = Array.from(set);
this._syncBatchImportFetchIdsStringFromSelected();
},
clearBatchImportFieldSelection() {
this.batchImportSelectedFieldIds = [];
this.batchImportFetchIdsOverride = '';
},
onFactoryBatchImportDialogClosed() {
this.batchImporting = false;
this.batchImportAccounts = [];
this.batchImportAccountsLoading = false;
this.batchImportJournalPickerList = [];
this.clearBatchImportAccountsFetchTimer();
this.clearBatchImportFieldPicker();
},
clearBatchImportAccountsFetchTimer() {
if (this._batchImportAccountsFetchTimer) {
clearTimeout(this._batchImportAccountsFetchTimer);
this._batchImportAccountsFetchTimer = null;
}
},
/** 期刊 ID 变化或弹窗打开后,防抖自动拉取邮箱账号 */
scheduleBatchImportAccountsFetch() {
this.clearBatchImportAccountsFetchTimer();
this._batchImportAccountsFetchTimer = setTimeout(() => {
this._batchImportAccountsFetchTimer = null;
if (!this.batchImportVisible) return;
const jid = String(this.batchImportJournalId || '').trim();
if (!jid) {
this.batchImportAccounts = [];
return;
}
this.fetchBatchImportAccounts(true);
}, 400);
},
async fetchBatchImportAccounts(silentEmpty) {
const jid = String(this.batchImportJournalId || '').trim();
if (!jid) {
if (!silentEmpty) {
this.$message.warning(this.$t('autoPromotion.factoryBatchImportNeedJournalForAccounts'));
}
this.batchImportAccounts = [];
return;
}
this.batchImportAccountsLoading = true;
try {
const res = await this.$api.post('api/email_client/getAccounts', { journal_id: jid });
let list = [];
if (res && Number(res.code) === 0) {
if (Array.isArray(res.data)) list = res.data;
else if (res.data && Array.isArray(res.data.list)) list = res.data.list;
}
this.batchImportAccounts = list;
if (!list.length && !silentEmpty) {
this.$message.info(this.$t('autoPromotion.factoryBatchImportNoAccounts'));
}
} catch (e) {
console.error(e);
this.batchImportAccounts = [];
this.$message.error(this.$t('autoPromotion.factoryBatchImportAccountsFail'));
} finally {
this.batchImportAccountsLoading = false;
}
},
batchImportAccountDisplay(acc) {
if (!acc) return '-';
return acc.smtp_user || acc.account || '-';
},
defaultFactoryBatchImportSample() {
return (
'[\n' +
' {\n' +
' "type": "1",\n' +
' "expert_type": "5",\n' +
' "email_ids": "12",\n' +
' "send_count": "1",\n' +
' "fetch_ids": "1,2",\n' +
' "target_partitions": "1,2",\n' +
' "target_country_ids": "",\n' +
' "start_promotion": "0"\n' +
' }\n' +
']\n'
);
},
/** 将对话框顶部的期刊 / 模板 / 样式 ID 合并进单条 payload输入框非空则覆盖 JSON */
applyBatchImportDialogOverrides(payload) {
const jid = String(this.batchImportJournalId || '').trim();
const tid = String(this.batchImportTemplateId || '').trim();
const sid = String(this.batchImportStyleId || '').trim();
const fetchOv = String(this.batchImportFetchIdsOverride || '').trim();
if (jid) payload.journal_id = jid;
if (tid) payload.template_id = tid;
if (sid) payload.style_id = sid;
if (fetchOv) payload.fetch_ids = fetchOv;
},
/** 与 PromotionFactoryTaskDialog.submitFactory 提交体一致;可选简写 zones / countries / email_id_list */
normalizePromotionFactoryRow(row) {
if (!row || typeof row !== 'object') return {};
const o = { ...row };
const partitionMap = { Partition1: '1', Partition2: '2', Partition3: '3' };
const countryMap = { country_china: '239', country_india: '228' };
if (Array.isArray(o.zones) && (o.target_partitions == null || String(o.target_partitions).trim() === '')) {
o.target_partitions = o.zones
.map((z) => partitionMap[String(z)])
.filter(Boolean)
.join(',');
delete o.zones;
}
if (Array.isArray(o.countries) && (o.target_country_ids == null || String(o.target_country_ids).trim() === '')) {
o.target_country_ids = o.countries
.map((c) => countryMap[String(c)])
.filter(Boolean)
.join(',');
delete o.countries;
}
if (Array.isArray(o.email_id_list)) {
o.email_ids = o.email_id_list.map((id) => String(id)).join(',');
delete o.email_id_list;
}
if (Array.isArray(o.email_ids)) {
o.email_ids = o.email_ids.map((id) => String(id)).join(',');
}
const keys = [
'journal_id',
'type',
'expert_type',
'email_ids',
'send_count',
'template_id',
'style_id',
'fetch_ids',
'target_partitions',
'target_country_ids',
'start_promotion'
];
const out = {};
keys.forEach((k) => {
if (o[k] === undefined) return;
out[k] = o[k] === null || o[k] === '' ? '' : String(o[k]);
});
if (out.start_promotion === undefined || out.start_promotion === '') out.start_promotion = '0';
if (out.fetch_ids === undefined) out.fetch_ids = '';
if (out.target_partitions === undefined) out.target_partitions = '';
if (out.target_country_ids === undefined) out.target_country_ids = '';
return out;
},
/** 将上方期刊 / 模板 / 样式 / 领域 /(可选)邮箱列表写入下方 JSON 数组的每一条 */
syncBatchImportUiToJson() {
let rows;
try {
rows = JSON.parse(this.batchImportText || '[]');
} catch (e) {
this.$message.error(this.$t('autoPromotion.factoryBatchImportBadJson'));
return;
}
if (!Array.isArray(rows)) {
this.$message.warning(this.$t('autoPromotion.factoryBatchImportEmpty'));
return;
}
if (!rows.length) {
rows = [
{
type: '1',
expert_type: '5',
email_ids: '',
send_count: '1',
fetch_ids: '',
target_partitions: '1,2',
target_country_ids: '',
start_promotion: '0'
}
];
}
const jid = String(this.batchImportJournalId || '').trim();
const tid = String(this.batchImportTemplateId || '').trim();
const sid = String(this.batchImportStyleId || '').trim();
const fetchOv = String(this.batchImportFetchIdsOverride || '').trim();
const includeEmails = this.batchImportSyncIncludeEmails !== false;
const emailIdsFromAccounts =
includeEmails && this.batchImportAccounts && this.batchImportAccounts.length
? this.batchImportAccounts
.map((a) => (a && a.j_email_id != null ? String(a.j_email_id) : ''))
.filter(Boolean)
.join(',')
: null;
const next = rows.map((row) => {
const o = row && typeof row === 'object' ? { ...row } : {};
if (jid) o.journal_id = jid;
if (tid) o.template_id = tid;
if (sid) o.style_id = sid;
if (fetchOv) o.fetch_ids = fetchOv;
if (emailIdsFromAccounts) o.email_ids = emailIdsFromAccounts;
return o;
});
this.batchImportText = JSON.stringify(next, null, 2) + '\n';
this.$message.success(this.$t('autoPromotion.factoryBatchImportJsonFromUiOk'));
},
/** 用下方 JSON 首条回显上方表单(并尝试加载领域勾选、触发邮箱拉取) */
async syncBatchImportJsonToUiFirst() {
let rows;
try {
rows = JSON.parse(this.batchImportText || '[]');
} catch (e) {
this.$message.error(this.$t('autoPromotion.factoryBatchImportBadJson'));
return;
}
if (!Array.isArray(rows) || !rows.length) {
this.$message.warning(this.$t('autoPromotion.factoryBatchImportEmpty'));
return;
}
const first = rows[0] && typeof rows[0] === 'object' ? rows[0] : {};
const jid =
first.journal_id != null && String(first.journal_id).trim() !== ''
? String(first.journal_id).trim()
: first.journalId != null && String(first.journalId).trim() !== ''
? String(first.journalId).trim()
: '';
if (jid) this.batchImportJournalId = jid;
this.batchImportTemplateId =
first.template_id != null && String(first.template_id).trim() !== '' ? String(first.template_id).trim() : '';
this.batchImportStyleId =
first.style_id != null && String(first.style_id).trim() !== '' ? String(first.style_id).trim() : '';
this.batchImportFetchIdsOverride =
first.fetch_ids != null && String(first.fetch_ids).trim() !== '' ? String(first.fetch_ids).trim() : '';
if (jid) {
await this.loadBatchImportAvailableFields();
} else {
this.batchImportAvailableFields = [];
this.batchImportSelectedFieldIds = [];
}
this.syncBatchImportSelectionFromFetchIdsString();
this.scheduleBatchImportAccountsFetch();
this.$message.success(this.$t('autoPromotion.factoryBatchImportUiFromJsonOk'));
},
validatePromotionFactoryPayload(p, index) {
const req = ['journal_id', 'type', 'expert_type', 'email_ids', 'send_count', 'template_id', 'style_id'];
for (let i = 0; i < req.length; i++) {
const k = req[i];
if (p[k] == null || String(p[k]).trim() === '') {
return this.$t('autoPromotion.factoryBatchImportMissing', { index: index + 1, field: k });
}
}
if (String(p.expert_type) === '5') {
if (!String(p.fetch_ids || '').trim()) {
return this.$t('autoPromotion.factoryBatchImportNeedFetch', { index: index + 1 });
}
if (!String(p.target_partitions || '').trim() && !String(p.target_country_ids || '').trim()) {
return this.$t('autoPromotion.factoryBatchImportNeedZone', { index: index + 1 });
}
}
return '';
},
async runPromotionFactoryBatchImport() {
let rows;
try {
rows = JSON.parse(this.batchImportText || '[]');
} catch (e) {
this.$message.error(this.$t('autoPromotion.factoryBatchImportBadJson'));
return;
}
if (!Array.isArray(rows) || !rows.length) {
this.$message.warning(this.$t('autoPromotion.factoryBatchImportEmpty'));
return;
}
this.batchImporting = true;
let ok = 0;
let fail = 0;
const errLines = [];
try {
for (let i = 0; i < rows.length; i++) {
const payload = this.normalizePromotionFactoryRow(rows[i]);
this.applyBatchImportDialogOverrides(payload);
const ve = this.validatePromotionFactoryPayload(payload, i);
if (ve) {
fail++;
errLines.push(ve);
continue;
}
try {
const res = await this.$api.post('api/promotion_factory/add', payload);
if (res && Number(res.code) === 0) {
ok++;
} else {
fail++;
errLines.push(
this.$t('autoPromotion.factoryBatchImportRowFail', {
index: i + 1,
msg: (res && res.msg) || this.$t('autoPromotion.factorySubmitFailed')
})
);
}
} catch (e) {
console.error(e);
fail++;
errLines.push(this.$t('autoPromotion.factoryBatchImportRowNetwork', { index: i + 1 }));
}
}
this.$message.success(this.$t('autoPromotion.factoryBatchImportDone', { ok, fail }));
if (errLines.length) {
this.$notify({
title: this.$t('autoPromotion.factoryBatchImportErrorsTitle'),
message: errLines.slice(0, 8).join('\n'),
type: fail && !ok ? 'error' : 'warning',
duration: 12000
});
}
this.batchImportVisible = false;
await this.fetchPromotionJournals();
} finally {
this.batchImporting = false;
}
},
// --- 逻辑部分完全保留你原有的实现 ---
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');
if (t === '6') return this.$t('autoPromotion.factoryExpertYoungBoardBefore2025');
if (t === '7') return this.$t('autoPromotion.factoryExpertAuthorBefore2025');
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;
}
}
},
beforeDestroy() {
this.clearBatchImportAccountsFetchTimer();
}
};
</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;
}
/* 临时批量导入 JSON 对话框 */
.factory-batch-import-dialog .el-dialog__body {
padding-top: 12px;
}
.batch-import-layout {
display: flex;
align-items: flex-start;
gap: 16px;
min-height: 0;
}
.batch-import-journal-rail {
flex: 0 0 74px;
width: 74px;
padding: 8px 8px 8px 0;
border-right: 1px solid #ebeef5;
align-self: stretch;
max-height: 72vh;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
.batch-import-journal-scroll {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.batch-import-rail-title {
font-size: 11px;
font-weight: 600;
color: #606266;
margin-bottom: 6px;
text-align: center;
line-height: 1.25;
flex-shrink: 0;
}
.batch-import-journal-rail .batch-import-journal-empty {
text-align: center;
font-size: 10px;
color: #909399;
padding: 4px 0 8px;
line-height: 1.3;
}
.batch-import-journal-rail .batch-import-journal-grid--rail {
flex-direction: column;
flex-wrap: nowrap;
align-items: center;
gap: 8px;
margin-bottom: 0;
flex: none;
min-height: 0;
overflow: visible;
padding-right: 0;
}
.batch-import-journal-rail .batch-import-journal-card {
width: 35px;
}
.batch-import-journal-rail .batch-import-mini-cover-wrapper {
width: 35px;
height: 45px;
}
.batch-import-journal-rail .batch-import-mini-badge {
width: 14px;
height: 14px;
font-size: 8px;
top: -3px;
right: -3px;
}
.batch-import-journal-rail .batch-import-mini-abbr {
font-size: 9px;
margin-top: 4px;
max-width: 35px;
}
.batch-import-journal-rail .batch-import-journal-card.is-active .batch-import-mini-cover {
transform: translateY(-1px);
}
.batch-import-journal-id-row--rail {
flex-direction: column;
align-items: stretch;
gap: 6px;
padding-top: 4px;
border-top: 1px solid #ebeef5;
}
.batch-import-journal-id-row--rail .batch-import-id-label {
font-size: 11px;
}
.batch-import-journal-id-row--rail .batch-import-journal-id-input {
max-width: none;
width: 100%;
min-width: 0;
}
.batch-import-main {
flex: 1;
min-width: 0;
}
.batch-import-hint-compact {
font-size: 12px;
line-height: 1.45;
color: #909399;
margin: 0 0 12px;
}
.batch-import-hint {
font-size: 12px;
line-height: 1.55;
color: #606266;
margin: 0 0 10px;
white-space: pre-wrap;
}
.batch-import-common-tip {
font-size: 12px;
line-height: 1.5;
color: #909399;
margin: 0 0 10px;
}
.batch-import-common-fields {
margin-bottom: 12px;
}
.batch-import-journal-block {
margin-bottom: 14px;
}
.batch-import-journal-panel {
background: #f8fafc;
border: 1px solid #ebf0f5;
border-radius: 8px;
padding: 12px;
}
.batch-import-journal-empty {
font-size: 12px;
color: #909399;
padding: 8px 0;
}
.batch-import-journal-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
}
.batch-import-journal-card {
width: 70px;
text-align: center;
cursor: pointer;
}
.batch-import-mini-cover-wrapper {
position: relative;
width: 70px;
height: 90px;
}
.batch-import-mini-cover {
width: 100%;
height: 100%;
border-radius: 4px;
border: 2px solid transparent;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: 0.25s;
}
.batch-import-journal-card.is-active .batch-import-mini-cover {
border-color: #409eff;
transform: translateY(-2px);
}
.batch-import-mini-badge {
position: absolute;
top: -6px;
right: -6px;
background: #67c23a;
color: #fff;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.batch-import-image-slot {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: #f5f7fa;
color: #c0c4cc;
}
.batch-import-mini-abbr {
font-size: 11px;
margin-top: 6px;
color: #606266;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.batch-import-mini-abbr.is-active {
color: #409eff;
font-weight: 600;
}
.batch-import-journal-id-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.batch-import-id-label {
font-size: 12px;
color: #606266;
font-weight: 600;
flex-shrink: 0;
}
.batch-import-journal-id-input {
flex: 1;
min-width: 160px;
max-width: 360px;
}
.batch-import-fetch-block {
margin-bottom: 12px;
padding: 10px 12px;
background: #fafafa;
border: 1px solid #eef0f3;
border-radius: 6px;
}
.batch-import-field-row-head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 6px;
}
.batch-import-fetch-count {
font-size: 12px;
color: #909399;
flex: 1;
min-width: 80px;
}
.batch-import-fetch-tip {
font-size: 12px;
color: #c27a00;
margin: 0 0 8px;
line-height: 1.45;
}
.batch-import-field-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.batch-import-field-search {
flex: 1;
min-width: 180px;
max-width: 100%;
}
.batch-import-field-empty {
font-size: 12px;
color: #909399;
padding: 8px 4px;
}
.batch-import-field-checkbox-wrap {
max-height: 160px;
overflow: auto;
margin-bottom: 8px;
padding: 8px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.batch-import-field-checkbox-wrap .el-checkbox {
display: block;
margin-right: 0;
margin-bottom: 6px;
}
.batch-import-fetch-text-label {
margin-top: 4px;
margin-bottom: 4px;
}
.factory-batch-import-dialog .batch-import-fetch-textarea >>> textarea {
font-family: Consolas, 'Courier New', monospace;
font-size: 12px;
line-height: 1.45;
}
.batch-import-field-label {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
font-weight: 600;
}
.batch-import-accounts-table {
margin-bottom: 12px;
}
.batch-import-json-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
.batch-import-sync-email-check {
margin-right: 0;
}
.batch-import-json-toolbar-tip {
font-size: 12px;
color: #909399;
flex: 1;
min-width: 200px;
line-height: 1.45;
}
.factory-batch-import-dialog .batch-import-textarea >>> textarea {
font-family: Consolas, 'Courier New', monospace;
font-size: 12px;
line-height: 1.45;
}
/* 响应式 */
@media (max-width: 1200px) {
.cards-grid {
grid-template-columns: 1fr;
}
}
</style>