2172 lines
81 KiB
Vue
2172 lines
81 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="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 :</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"
|
||
/>
|
||
|
||
<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>
|