diff --git a/src/api/index.js b/src/api/index.js index 525753d..487fb66 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -19,8 +19,8 @@ const service = axios.create({ // baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换 // baseURL: 'http://www.tougao.com/', //测试本地 记得切换 // baseURL: 'http://192.168.110.110/tougao/public/index.php/', - // baseURL: '/api', //本地 - baseURL: '/', //正式 + baseURL: '/api', //本地 + // baseURL: '/', //正式 }); diff --git a/src/components/common/langs/en.js b/src/components/common/langs/en.js index e9a8701..fa1ee9f 100644 --- a/src/components/common/langs/en.js +++ b/src/components/common/langs/en.js @@ -1107,6 +1107,11 @@ colTitle: 'Template title', selectPromotionCountryTip: 'Multiple selection supported; leave empty for no country restriction. Uses the same API as fields until a dedicated country list is available.', fieldSearchPlaceholder: 'Search promotion fields', countrySearchPlaceholder: 'Search countries', + countryQuickZone1: 'Partition 1', + countryQuickZone2: 'Partition 2', + countryQuickZone3: 'Partition 3', + countryQuickChina: 'China', + countryQuickIndia: 'India', noFieldMatch: 'No matching fields', noCountryMatch: 'No matching countries', confirm: 'Confirm', @@ -1114,7 +1119,68 @@ colTitle: 'Template title', countriesSaved: 'Promotion countries saved', confirmAndEnable: 'Confirm and Enable', onlySaveConfig: 'Save configuration only', - enableNowNextDay: 'Enable auto promotion now (starts next day)' + enableNowNextDay: 'Enable auto promotion now (starts next day)', + factoryCreateBtn: 'Create automated promotion task', + factoryDialogTitle: 'Create task', + factoryJournal: 'Journal', + factoryJournalPlaceholder: 'Select a journal', + factorySendSettings: 'Sending & scenario', + factoryEmails: 'Sender accounts', + factoryEmailsPlaceholder: 'Select one or more sender accounts', + factorySendCount: 'Send count', + factoryType: 'Scenario', + factoryTypeEditor: 'Editor', + factoryTypeArticle: 'Promote article', + factoryExpertType: 'Expert type', + factoryExpertTypePlaceholder: 'Optional; follow backend rules', + factorySubmit: 'Submit task', + factorySubmitSuccess: 'Factory task created', + factorySubmitFailed: 'Create failed, please try again later', + factoryNeedJournal: 'Please select a journal first', + factoryNeedTemplate: 'Please select email template and style', + factoryNeedEmails: 'Please select at least one sender account', + factoryNeedExpertType: 'Please select target person type', + factoryEmailsPickJournal: 'Select a journal to load sender accounts', + factoryNoAccounts: 'No mailbox accounts for this journal', + factoryAccountRemaining: 'Remaining today', + factorySendMaxFromApi: 'limit: up to {max} per day', + factorySendMaxFallback: 'using mailbox quota sum ~{max} (or default cap)', + factoryStepNav1Title: 'Journal', + factoryStepNav1Desc: 'Select a journal first.', + factoryStepNav2Title: 'Email template and style', + factoryStepNav2Desc: 'Choose template and style.', + factoryStepNav3Title: 'Sending and scenario', + factoryStepNav3Desc: 'Choose accounts, send count, and target type.', + factoryStepNav4Title: 'Promotion fields', + factoryStepNav4Desc: 'Select at least one promotion field.', + factoryStepNav5Title: 'Country', + factoryStepNav5Desc: 'Select at least one country or partition.', + factoryStepNav6Title: 'Confirm and enable', + factoryStepNav6Desc: 'Choose save only or enable next day.', + factoryPromotionFieldsBlockTip: 'Open “Choose fields” and tick at least one item; do not submit with none selected.', + factoryPromotionCountryBlockTip: 'Tick at least one partition or country; do not submit with none selected.', + factoryNeedPromotionFields: 'Select at least one promotion field before submitting.', + factoryNeedPromotionCountry: 'Select at least one partition or country before submitting.', + factoryQuotaLabel: 'Quota', + factoryClickSelectTemplate: 'Click to select email template', + factoryClickConfigureFields: 'Click to configure subject fields', + factoryBtnModify: 'Edit', + factoryBtnReset: 'Reset', + factoryBtnCancel: 'Cancel', + factoryBtnSubmit: 'Submit task', + factoryFillRequired: 'Please complete journal, template, and at least one sender account', + factoryExpertChief: 'Editor-in-Chief', + factoryExpertBoard: 'Editorial board', + factoryExpertYoungBoard: 'Young editorial board', + factoryExpertAuthor: 'Author', + factoryExpertDb: 'Expert database', + factoryScenario: 'Scenario', + factoryScenarioPlaceholder: 'Select scenario', + factoryScenarioSolicit: 'Invite Submission', + factoryScenarioPromoteCitation: 'Promote Citation', + factoryScenarioGeneralThanks: 'General Thanks', + noFactoryTask: 'No tasks', + factoryCreateNow: 'Create now' } , autoPromotionLogs: { diff --git a/src/components/common/langs/zh.js b/src/components/common/langs/zh.js index eb1dd68..8280c7a 100644 --- a/src/components/common/langs/zh.js +++ b/src/components/common/langs/zh.js @@ -1092,6 +1092,11 @@ const zh = { selectPromotionCountryTip: '可多选;未选择则不限制国家。与领域接口一致,后续可对接独立国家数据。', fieldSearchPlaceholder: '搜索推广领域', countrySearchPlaceholder: '搜索国家', + countryQuickZone1: '1区', + countryQuickZone2: '2区', + countryQuickZone3: '3区', + countryQuickChina: 'China', + countryQuickIndia: 'India', noFieldMatch: '没有匹配的领域', noCountryMatch: '没有匹配的国家', confirm: '确定', @@ -1099,7 +1104,68 @@ const zh = { countriesSaved: '推广国家已保存', confirmAndEnable: '确认并开启', onlySaveConfig: '仅保存配置', - enableNowNextDay: '立即激活自动推广(次日开始自动推广)' + enableNowNextDay: '立即激活自动推广(次日开始自动推广)', + factoryCreateBtn: '创建自动化推广任务', + factoryDialogTitle: '创建任务', + factoryJournal: '期刊', + factoryJournalPlaceholder: '请选择期刊', + factorySendSettings: '发送与场景', + factoryEmails: '发送邮箱', + factoryEmailsPlaceholder: '请选择发送账号(可多选)', + factorySendCount: '发送数量', + factoryType: '场景类型', + factoryTypeEditor: '编辑', + factoryTypeArticle: '推广文章', + factoryExpertType: '专家类型', + factoryExpertTypePlaceholder: '可选,按后端要求填写', + factorySubmit: '提交任务', + factorySubmitSuccess: '工厂任务已创建', + factorySubmitFailed: '创建失败,请稍后重试', + factoryNeedJournal: '请先选择期刊', + factoryNeedTemplate: '请先选择邮件模板与样式', + factoryNeedEmails: '请至少选择一个发送邮箱', + factoryNeedExpertType: '请选择目标人类型', + factoryEmailsPickJournal: '请先选择期刊以加载邮箱列表', + factoryNoAccounts: '该期刊下暂无可用邮箱账号', + factoryAccountRemaining: '今日剩余', + factorySendMaxFromApi: '接口限制:单日最多 {max} 封', + factorySendMaxFallback: '未返回接口上限,当前按邮箱额度合计约 {max} 封(或默认上限)', + factoryStepNav1Title: '期刊', + factoryStepNav1Desc: '先选期刊,未选不能提交。', + factoryStepNav2Title: '邮件模版与样式', + factoryStepNav2Desc: '选好邮件模板和样式。', + factoryStepNav3Title: '发送与场景', + factoryStepNav3Desc: '选账号,填发送数量和目标人类型。', + factoryStepNav4Title: '推广领域', + factoryStepNav4Desc: '至少选择一个推广领域。', + factoryStepNav5Title: '国家', + factoryStepNav5Desc: '至少选择一个国家或分区。', + factoryStepNav6Title: '确认并开启', + factoryStepNav6Desc: '选择仅保存或次日自动开启。', + factoryPromotionFieldsBlockTip: '请打开「选择领域」,在列表中至少勾选一项;不得留空提交。', + factoryPromotionCountryBlockTip: '请至少勾选一项分区或国家;不得留空提交。', + factoryNeedPromotionFields: '请至少选择一项推广领域后再提交。', + factoryNeedPromotionCountry: '请至少选择一项分区或国家后再提交。', + factoryQuotaLabel: '额度', + factoryClickSelectTemplate: '点击选择邮件模板', + factoryClickConfigureFields: '点击配置学科字段', + factoryBtnModify: '修改', + factoryBtnReset: '重置', + factoryBtnCancel: '取消', + factoryBtnSubmit: '立即提交任务', + factoryFillRequired: '请完善必填信息(期刊、模板、账号)', + factoryExpertChief: '主编', + factoryExpertBoard: '编委', + factoryExpertYoungBoard: '青年编委', + factoryExpertAuthor: '作者', + factoryExpertDb: 'expert库', + factoryScenario: '场景', + factoryScenarioPlaceholder: '请选择场景', + factoryScenarioSolicit: '约稿', + factoryScenarioPromoteCitation: '推广引用', + factoryScenarioGeneralThanks: '常规感谢', + noFactoryTask: '没有任务', + factoryCreateNow: '立即创建' } , autoPromotionLogs: { diff --git a/src/components/page/autoPromotion.vue b/src/components/page/autoPromotion.vue index 425231c..c258996 100644 --- a/src/components/page/autoPromotion.vue +++ b/src/components/page/autoPromotion.vue @@ -1,610 +1,951 @@ \ No newline at end of file +/* 容器 */ +.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; +} + +/* 统计卡片基础样式 */ +.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 #409eff; +} + +.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; +} + +.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; +} + +/* 统计项 */ +.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; + gap: 8px; +} + +.action-main-btn { + flex: 1; + height: 30px; + background: #006699; + color: white; + border: none; + border-radius: 6px; + font-weight: 700; + font-size: 12px; + cursor: pointer; + transition: background 0.2s; +} + +.action-main-btn:hover { + background: #006699; +} + +.action-main-btn.secondary { + background: transparent; + border: 2px solid #1a1a1a; + color: #1a1a1a; +} + +.action-edit-btn { + width: 30px; + height: 30px; + background: white; + border: 1px solid #e1e4e8; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + color: #6a737d; + display: flex; + align-items: center; + justify-content: center; +} + +.action-edit-btn:hover { + background: #f6f8fa; + color: #409eff; + border-color: #409eff; +} + +/* 响应式 */ +@media (max-width: 1200px) { + .cards-grid { + grid-template-columns: 1fr; + } +} + diff --git a/src/components/page/autoPromotionLogs.vue b/src/components/page/autoPromotionLogs.vue index b420452..d058d20 100644 --- a/src/components/page/autoPromotionLogs.vue +++ b/src/components/page/autoPromotionLogs.vue @@ -21,7 +21,7 @@ {{ $t('autoPromotionLogs.configured') }} - + {{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }} @@ -242,6 +242,15 @@ @confirm="handleTemplateApply" @close-all-dialogs="closeAllDialogs" /> +
@@ -287,6 +296,7 @@ import CkeditorMail from '@/components/page/components/email/CkeditorMail.vue'; import TemplateSelectorDialog from '@/components/page/components/email/TemplateSelectorDialog.vue'; import AutoPromotionWizard from '@/components/page/components/autoPromotion/AutoPromotionWizard.vue'; +import PromotionFactoryTaskDialog from '@/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue'; import PromotionDetailDrawer from '@/components/page/components/autoPromotion/PromotionDetailDrawer.vue'; // 这里假设你已经定义了 API 地址 const API = { @@ -300,7 +310,7 @@ const API = { export default { name: 'autoPromotion', - components: { TemplateSelectorDialog, AutoPromotionWizard, CkeditorMail, PromotionDetailDrawer }, + components: { TemplateSelectorDialog, AutoPromotionWizard, PromotionFactoryTaskDialog, CkeditorMail, PromotionDetailDrawer }, data() { return { handleRefreshList: [], @@ -350,6 +360,9 @@ export default { availableCountries: [], fieldsLoading: false, fieldsSaving: false, + showFactoryTaskDialog: false, + factoryDialogInitialJournalId: '', + factoryDialogInitialTask: null, previewForm: { id: '', email: '', @@ -378,6 +391,7 @@ export default { closeAllDialogs() { // 点击“去新增模板”后:关闭当前页面所有可能的弹窗 this.showWizardDialog = false; + this.showFactoryTaskDialog = false; this.showTemplateDialog = false; this.showPreviewDialog = false; this.currentRow = null; @@ -659,6 +673,11 @@ export default { } this.showWizardDialog = true; }, + openFactoryTaskDialogFromLogs() { + this.factoryDialogInitialJournalId = this.selectedJournalId ? String(this.selectedJournalId) : ''; + this.factoryDialogInitialTask = this.list && this.list.length ? { ...this.list[0] } : null; + this.showFactoryTaskDialog = true; + }, // 切换期刊逻辑 async handleJournalChange() { diff --git a/src/components/page/components/autoPromotion/AutoPromotionWizardContent.vue b/src/components/page/components/autoPromotion/AutoPromotionWizardContent.vue index a053aae..8c03c81 100644 --- a/src/components/page/components/autoPromotion/AutoPromotionWizardContent.vue +++ b/src/components/page/components/autoPromotion/AutoPromotionWizardContent.vue @@ -101,13 +101,13 @@ - +
-
- {{ row.text }} +
+
{{ $t('autoPromotion.selectPromotionCountryTip') }}
+ + {{ $t('autoPromotion.countryQuickZone1') }} + {{ $t('autoPromotion.countryQuickZone2') }} + {{ $t('autoPromotion.countryQuickZone3') }} + {{ $t('autoPromotion.countryQuickChina') }} + {{ $t('autoPromotion.countryQuickIndia') }} +
-
{{ $t('autoPromotion.selectPromotionCountryTip') }}
+ +
- --> +

- 3. {{ $t('autoPromotion.confirmAndEnable') }} + 4. {{ $t('autoPromotion.confirmAndEnable') }}

@@ -317,10 +327,24 @@ export default { (this.availableCountries || []).forEach((i) => { map[String(i.id)] = i.label; }); - return (this.selectedCountryIdsProxy || []).map((id) => ({ - id: String(id), - text: map[String(id)] != null && map[String(id)] !== '' ? map[String(id)] : String(id) - })); + const quick = { + zone_1: this.$t('autoPromotion.countryQuickZone1'), + zone_2: this.$t('autoPromotion.countryQuickZone2'), + zone_3: this.$t('autoPromotion.countryQuickZone3'), + country_china: this.$t('autoPromotion.countryQuickChina'), + country_india: this.$t('autoPromotion.countryQuickIndia') + }; + return (this.selectedCountryIdsProxy || []).map((id) => { + const sid = String(id); + const fromList = map[sid]; + const text = + fromList != null && fromList !== '' + ? fromList + : quick[sid] != null + ? quick[sid] + : sid; + return { id: sid, text }; + }); } }, methods: { @@ -694,5 +718,17 @@ export default { font-size: 12px; color: #909399; } +.country-quick-checks { + margin-bottom: 12px; +} +.country-quick-checks >>> .el-checkbox-group { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 20px; +} +.country-quick-checks >>> .el-checkbox { + margin-right: 0; +} diff --git a/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue b/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue new file mode 100644 index 0000000..d4ad0bc --- /dev/null +++ b/src/components/page/components/autoPromotion/PromotionFactoryTaskDialog.vue @@ -0,0 +1,1353 @@ + + + + + \ No newline at end of file diff --git a/src/components/page/components/email/TemplateSelectorDialog.vue b/src/components/page/components/email/TemplateSelectorDialog.vue index fae3e44..ce65894 100644 --- a/src/components/page/components/email/TemplateSelectorDialog.vue +++ b/src/components/page/components/email/TemplateSelectorDialog.vue @@ -5,6 +5,7 @@ :close-on-click-modal="false" width="90%" top="5vh" + append-to-body destroy-on-close :before-close="handleClose" custom-class="template-modal"