diff --git a/src/api/index.js b/src/api/index.js index 487fb66..525753d 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 442418b..c390035 100644 --- a/src/components/common/langs/en.js +++ b/src/components/common/langs/en.js @@ -916,17 +916,38 @@ colTitle: 'Template title', startConfig: 'Start auto promotion configuration', notConfigured: 'Not configured', searchPlaceholder: 'Name / Email', + statusAll: 'All status', searchBtn: 'Search', index: 'No.', + taskName: 'Task Name', + taskParams: 'Parameters', + templateIdLabel: 'Template ID', + styleIdLabel: 'Style ID', + deliveryStats: 'Delivery Stats', + totalCount: 'Total', + sentCount: 'Sent', + failCount: 'Failed', + bounceCount: 'Bounce', + noDeliveryIssue: 'No delivery issues', + deliveryIssue: 'Delivery issues', + completedText: 'Completed', expertInfo: 'Expert Info', templateStyle: 'Template / Style', stylePrefix: 'Style', runAt: 'Execution time', status: 'Status', + state0: 'Draft', + state1: 'Running', + state2: 'Paused', + state3: 'Completed', + state4: 'Cancelled', + state5: 'Preparing', paused: 'Paused', toRun: 'To run', operation: 'Operation', preview: 'Preview', + previewAction: 'Preview', + editAction: 'Edit', enable: 'Enable', pause: 'Pause', previewEditTitle: 'Preview and edit promotion email', @@ -943,9 +964,50 @@ colTitle: 'Template title', configUpdated: 'Configuration updated', pauseSuccess: 'Paused', enableSuccess: 'Enabled', + operationFailed: 'Operation failed', + selectTaskForLogs: 'Please select a task first (click the view icon in the list)', + pushLogTitle: 'Task list', + logRefresh: 'Refresh', + taskLogStateFilter: 'Status', + taskLogStateAll: 'All', + taskLogState0: 'Pending', + taskLogState1: 'Sent', + taskLogState2: 'Failed', + taskLogState3: 'Bounced', + taskLogState4: 'Cancelled', + logColExpert: 'Expert', + logColSendTime: 'Sent at', + logColPreparedAt: 'Prepared at', + logColStatus: 'Status', + logColAction: 'Action', + emptyLogs: 'No logs', + viewFailureReason: 'Reason', + editLogTip: 'Edit', + deleteLogTip: 'Delete', + previewLogTip: 'Preview', + logFieldAffiliation: 'Affiliation', + logFieldSubject: 'Subject', + logFieldSendTime: 'Sent at', + logFieldExecutionTime: 'Executed at', + saveDetail: 'Save', + failureReasonTitle: 'Failure reason', + saveDetailSuccess: 'Saved', + saveDetailFailed: 'Save failed', + logDetailLoadFailed: 'Failed to load detail', + logIdMissing: 'Missing log id', + logAlreadySent: 'This record is no longer pending; switched to preview', + deleteLogConfirm: 'Delete this log entry?', + tipTitle: 'Tip', + deleteLogSuccess: 'Deleted', + deleteLogFailed: 'Delete failed', + noFailureReason: 'No failure reason', deletedSuccess: 'Deleted', mockPromotionSubject: 'Promotion for {journal}', mockPromotionContent: '

Dear {name},

Check out our latest journal updates...

' + }, + tmrEmailEditor: { + preview: 'Preview', + placeholder: 'Please enter email content' } diff --git a/src/components/common/langs/zh.js b/src/components/common/langs/zh.js index 443120e..46fb914 100644 --- a/src/components/common/langs/zh.js +++ b/src/components/common/langs/zh.js @@ -901,17 +901,38 @@ const zh = { startConfig: '立即开始期刊自动推广配置', notConfigured: '尚未配置', searchPlaceholder: '姓名 / 邮箱', + statusAll: '全部状态', searchBtn: '搜索', index: '序号', + taskName: '任务名称', + taskParams: '参数内容', + templateIdLabel: '模板ID', + styleIdLabel: '风格ID', + deliveryStats: '投递统计', + totalCount: '总量', + sentCount: '已发送', + failCount: '失败', + bounceCount: '退信', + noDeliveryIssue: '无发送异常', + deliveryIssue: '投递异常', + completedText: '已完成', expertInfo: '专家信息', templateStyle: '模板 / 风格', stylePrefix: '风格', runAt: '执行时间', status: '状态', + state0: '草稿', + state1: '运行中', + state2: '暂停', + state3: '完成', + state4: '取消', + state5: '准备', paused: '已暂停', toRun: '待执行', operation: '操作', preview: '查看预览', + previewAction: '预览', + editAction: '编辑', enable: '开启', pause: '暂停', previewEditTitle: '预览并修改推广邮件', @@ -928,9 +949,50 @@ const zh = { configUpdated: '配置已更新', pauseSuccess: '已暂停', enableSuccess: '已开启', + operationFailed: '操作失败', + selectTaskForLogs: '请先选择一条任务(点击列表「查看」图标)', + pushLogTitle: '任务列表', + logRefresh: '刷新', + taskLogStateFilter: '发送状态', + taskLogStateAll: '全部', + taskLogState0: '待发送', + taskLogState1: '已发送', + taskLogState2: '失败', + taskLogState3: '退信', + taskLogState4: '取消', + logColExpert: '专家信息', + logColSendTime: '发送时间', + logColPreparedAt: '预处理完成时间', + logColStatus: '状态', + logColAction: '操作', + emptyLogs: '暂无日志', + viewFailureReason: '原因', + editLogTip: '编辑', + deleteLogTip: '删除', + previewLogTip: '预览', + logFieldAffiliation: '单位/机构', + logFieldSubject: '主题', + logFieldSendTime: '发送时间', + logFieldExecutionTime: '执行时间', + saveDetail: '保存', + failureReasonTitle: '失败原因', + saveDetailSuccess: '保存成功', + saveDetailFailed: '保存失败', + logDetailLoadFailed: '加载详情失败', + logIdMissing: '缺少日志 ID', + logAlreadySent: '该记录已非待发送状态,已切换为预览', + deleteLogConfirm: '确定删除该条发送记录?', + tipTitle: '提示', + deleteLogSuccess: '删除成功', + deleteLogFailed: '删除失败', + noFailureReason: '暂无失败原因', deletedSuccess: '已删除', mockPromotionSubject: '自动推广:{journal}', mockPromotionContent: '

亲爱的 {name},

请查看我们最新的期刊更新...

' + }, + tmrEmailEditor: { + preview: '预览效果', + placeholder: '请输入邮件内容' } diff --git a/src/components/page/autoPromotion.vue b/src/components/page/autoPromotion.vue index 90139a5..270a8d8 100644 --- a/src/components/page/autoPromotion.vue +++ b/src/components/page/autoPromotion.vue @@ -104,7 +104,7 @@ :selectedStyleName="selectedStyleName" :saving="saving" :title="`${$t('autoPromotion.journalManage')}: ${wizardJournal ? wizardJournal.title : ''}`" - @open-template-selector="showTemplateDialog = true" + @open-template-selector="openTemplateSelector" @cancel="showWizardDialog = false" @confirm="saveWizardConfig" /> @@ -114,6 +114,8 @@ :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" @@ -148,6 +150,8 @@ export default { selectedTemplateThumbHtml: '', selectedTemplateName: '', selectedStyleName: '', + templateDialogInitialStyleId: '', + templateDialogInitialTemplateId: '', templateNameMap: {}, allJournals: [] }; @@ -156,6 +160,16 @@ export default { this.fetchPromotionJournals(); }, methods: { + openTemplateSelector() { + // 更换模板时优先回显当前已选 style/template;没有则由选择器回落到第一项 + 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; diff --git a/src/components/page/autoPromotionLogs.vue b/src/components/page/autoPromotionLogs.vue index 0e9a29e..4281b68 100644 --- a/src/components/page/autoPromotionLogs.vue +++ b/src/components/page/autoPromotionLogs.vue @@ -1,892 +1,1510 @@ \ No newline at end of file +/* 表格整体精致化 */ +.exquisite-log-table { + border-radius: 4px; + overflow: hidden; +} + +/* 任务列样式 */ +.task-column .task-name { + font-weight: bold; + color: #334155; + margin-bottom: 4px; +} +.task-id-tags { + display: flex; + gap: 8px; +} +.id-tag { + font-size: 11px; + background: #f1f5f9; + color: #64748b; + padding: 1px 6px; + border-radius: 4px; +} + +/* 策略列样式 */ +.strategy-item { + font-size: 13px; + color: #475569; + margin-bottom: 2px; +} +.strategy-item i { + color: #3b82f6; + margin-right: 4px; +} +.mini-text { + font-size: 11px; + color: #94a3b8; +} + +/* 进度条与数据展示统计 */ +.delivery-stats-wrapper { + padding: 4px 0; +} +.stats-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 6px; + font-size: 12px; +} +.percent-text b { + font-size: 14px; + color: #1e293b; +} +.count-text { + color: #94a3b8; +} +.stats-footer { + margin-top: 6px; + display: flex; + gap: 12px; + font-size: 11px; +} +.error-label.red { + color: #f43f5e; + font-weight: 500; +} +.error-label.orange { + color: #f59e0b; + font-weight: 500; +} +.success-label { + color: #10b981; +} + +/* 状态标签样式(根据你现有的类名微调) */ +.status-badge { + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; +} + +/* 操作按钮 */ +.delete-btn { + color: #f43f5e !important; +} +.delete-btn:hover { + color: #be123c !important; +} +.delivery-dashboard { + padding: 0px 0; +} + +/* 数字网格布局 */ +.num-grid { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + background: #f8fafc; + border-radius: 6px; + padding: 0px 10px 2px; + border: 1px solid #edf2f7; +} + +.num-item { + text-align: center; + flex: 1; +} + +.num-item .label { + font-size: 12px; + color: #333; + /* text-transform: uppercase; */ +} + +.num-item .value { + font-size: 15px; + font-weight: 700; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +/* 颜色体系 */ +.value.primary { + color: #3b82f6; +} /* 蓝色 */ +.value.success { + color: #10b981; +} /* 绿色 */ +.value.danger { + color: #ef4444; +} /* 红色 */ +.value.warning { + color: #f59e0b; +} /* 橙色 */ +.value.neutral { + color: #cbd5e1; +} /* 灰色(0的时候) */ + +/* 进度条微调 */ +.progress-container { + margin-bottom: 8px; + padding: 0 4px; +} + +/* 底部标签 */ +.stats-status { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 4px; +} + +.safe-tag { + font-size: 11px; + color: #10b981; + background: #ecfdf5; + padding: 2px 8px; + border-radius: 10px; +} + +.warning-tag { + font-size: 11px; + color: #f59e0b; + background: #fffbe6; + padding: 2px 8px; + border-radius: 10px; +} + +.percent-tag { + font-size: 11px; + color: #64748b; + font-style: italic; +} +::v-deep .custom-table .el-table__cell { + padding: 6px 0 !important; +} + +.filter-header-row { + display: flex; + align-items: center; /* 垂直居中对齐 */ + gap: 16px; /* 胶囊和Search按钮之间的间距 */ + margin-bottom: 16px; + background-color: transparent; /* 容器透明,不厚重 */ +} + +/* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */ +.tmr-capsule-group { + flex: 1; /* 占据左侧空间 */ +} + +/* 强制隐藏默认灰色横线和卡片灰边 */ +.tmr-capsule-group .el-tabs__header { + margin: 0 !important; + border-bottom: none !important; +} + +.tmr-capsule-group .el-tabs__nav { + border: none !important; +} + +/* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */ +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item { + height: 32px !important; /* 紧凑高度 */ + line-height: 32px !important; + font-size: 13px; /* 紧凑字体 */ + border: none !important; /* 彻底隐藏原生卡片边框 */ + background-color: transparent; /* 默认状态下透明背景 */ + color: #515a6e; /* 默认状态深灰文字,更专业 */ + transition: all 0.2s ease-in-out; + padding: 0 16px !important; /* 适当内边距 */ + margin-right: 8px; /* 每个 Tab 之间的间距 */ + border-radius: 6px !important; /* 先统一圆角 */ + overflow: visible; /* 确保选中的阴影显示完全 */ +} + +/* 首尾 Tab 的圆角处理 (形成整体感) */ +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + margin-right: 0; +} + +/* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */ +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:first-child):not(:last-child) { + border-radius: 0; +} + +/* 4. **选中效果 (Active) - 胶囊浮动核心** */ +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active { + background-color: #ffffff !important; /* 白色底色 */ + color: #409eff !important; /* 主题蓝色文字 */ + font-weight: 500; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */ + border-radius: 6px; /* 选中时恢复圆角 */ + position: relative; + z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */ +} + +/* 5. 处理 Tab 之间的断层 (像素级微调) */ +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item { + position: relative; +} + +/* 首尾 Tab 之间的像素级处理 */ +.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after { + content: ''; + position: absolute; + right: -5px; /* 让 Tab 之间紧密无缝 */ + top: 0; + height: 100%; + width: 1px; + background: transparent; /* 移除灰线,不丑 */ +} + +/* 6. 搜索按钮对齐微调 */ +.filter-actions .el-button { + height: 32px; /* 与 Tab 高度一致 */ + padding: 0 16px; + border-radius: 6px; + transition: all 0.2s; +} +/* 基础 Badge 样式 */ +.status-badge { + display: inline-flex; + align-items: center; + padding: 0 10px; + height: 24px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +/* 状态圆点 (利用伪元素实现) */ +.status-badge::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + margin-right: 6px; +} + +/* 基础 Badge 结构 */ +.status-badge { + display: inline-flex; + align-items: center; + padding: 0 12px; + height: 26px; + border-radius: 13px; + font-size: 12px; + font-weight: 500; +} + +/* 状态点基础样式 */ +.status-badge::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + margin-right: 8px; +} + +/* 0: Draft - 浅灰色 */ +.status-draft { + background-color: #f1f5f9; + color: #64748b; +} +.status-draft::before { background-color: #94a3b8; } + +/* 5: Preparing - 橘色 */ +.status-preparing { + background-color: #fff7ed; + color: #c2410c; +} +.status-preparing::before { background-color: #f97316; } + +/* 1: Running - 深蓝色 */ +.status-running { + background-color: #eff6ff; + color: #1d4ed8; +} +.status-running::before { + background-color: #3b82f6; + box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */ +} + +/* 2: Paused - 深灰色 */ +.status-paused { + background-color: #f8fafc; + color: #334155; + border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */ +} +.status-paused::before { background-color: #475569; } + +/* 3: Completed - 绿色 */ +.status-completed { + background-color: #f0fdf4; + color: #15803d; +} +.status-completed::before { background-color: #22c55e; } + +/* 4: Cancelled - 浅红色 */ +.status-cancelled { + background-color: #fef2f2; + color: #b91c1c; +} +.status-cancelled::before { background-color: #ef4444; } + diff --git a/src/components/page/components/autoPromotion/PromotionDetailDrawer.vue b/src/components/page/components/autoPromotion/PromotionDetailDrawer.vue new file mode 100644 index 0000000..017a5c7 --- /dev/null +++ b/src/components/page/components/autoPromotion/PromotionDetailDrawer.vue @@ -0,0 +1,1197 @@ + + + + + + + diff --git a/src/components/page/components/email/CkeditorMail.vue b/src/components/page/components/email/CkeditorMail.vue index 1ec4362..67e2936 100644 --- a/src/components/page/components/email/CkeditorMail.vue +++ b/src/components/page/components/email/CkeditorMail.vue @@ -12,7 +12,9 @@ export default { props: { value: { type: String, default: '' }, id: { type: String, default: () => 'tiny-' + +new Date() }, - showSelectTemplateButton: { type: Boolean, default: false } + showSelectTemplateButton: { type: Boolean, default: false }, + // 仅在需要的页面开启“安全初始化内容” + useSafeInitContent: { type: Boolean, default: false } }, data() { return { @@ -125,7 +127,7 @@ autoInlineStyles(htmlContent) { height: 500, // 核心配置:允许所有 HTML 标签和样式,这对比邮件模板至关重要 valid_children: '+body[tr],+body[thead],+body[tbody],+body[table],+body[style],+p[style],+div[style]', - extended_valid_elements: 'style,meta,title', + extended_valid_elements: 'div[*],table[*],tr[*],td[*],img[*],style', custom_elements: 'style,meta,title', verify_html: false, // 关闭 HTML 校验,防止自动删掉你的邮件结构 forced_root_block: '', // 停止自动包裹

标签 @@ -162,7 +164,10 @@ autoInlineStyles(htmlContent) { init_instance_callback: (editor) => { this.editor = editor; if (this.value) { - editor.setContent(this.value); + const initContent = this.useSafeInitContent + ? this.prepareContentForEditor(this.value) + : this.value; + editor.setContent(initContent); } // 监听内容变化 @@ -172,6 +177,21 @@ autoInlineStyles(htmlContent) { } }); }, + + // 假设 rawHtml 是你那一大串完整的 HTML +prepareContentForEditor(rawHtml) { + // 1. 分离出 Head 及其之前的部分 (包含 doctype, html, head) + const headMatch = rawHtml.match(/([\s\S]*?)/i); + this.originalFullHead = headMatch ? headMatch[0] : ''; // 备份头部 + + // 2. 提取 Body 内部内容,并将 换成

+ // 这样 TinyMCE 就不会因为 fullpage 插件失效而删掉它 + let bodyContent = rawHtml.replace(/([\s\S]*?)/i, '') + .replace(/<\/body>([\s\S]*?)<\/html>/i, ''); + + // 返回给编辑器的内容:用一个特殊的 div 包裹 + return `
${bodyContent}
`; +}, // 获取经过行内样式转换后的内容 getInlinedContent() { if (!this.editor) return ''; diff --git a/src/components/page/components/email/TmrEmailEditor.vue b/src/components/page/components/email/TmrEmailEditor.vue new file mode 100644 index 0000000..5842507 --- /dev/null +++ b/src/components/page/components/email/TmrEmailEditor.vue @@ -0,0 +1,174 @@ + + + + + \ No newline at end of file diff --git a/src/components/page/mailboxMouldDetail.vue b/src/components/page/mailboxMouldDetail.vue index 5110040..e742118 100644 --- a/src/components/page/mailboxMouldDetail.vue +++ b/src/components/page/mailboxMouldDetail.vue @@ -96,7 +96,11 @@
{{ $t('mailboxMouldDetail.emailBody') }}:
- + +
@@ -106,7 +110,7 @@