diff --git a/src/api/system/batchEmail.ts b/src/api/system/batchEmail.ts index 251a9f4..e8e1f45 100644 --- a/src/api/system/batchEmail.ts +++ b/src/api/system/batchEmail.ts @@ -1,9 +1,16 @@ import { axiosInstance } from '@/api/rbac' -import { assertBusinessOk, omsUrl } from '@/api/oms/common' +import { assertBusinessOk, omsUrl, unwrapData } from '@/api/oms/common' /** 与桌面版 emailAuto.py 一致:阿里企业邮等 */ export type BatchEmailServerType = 'qiye' | 'qiye_ssl' | 'aliyun' | 'aliyun_ssl' | 'mxhichina' +/** 与后端 BatchEmailSmtpHelper.SecretMask 一致,用于线上已保存密码占位 */ +export const SMTP_SECRET_MASK = '******' + +export function isSmtpSecretMask(value?: string | null): boolean { + return String(value ?? '').trim() === SMTP_SECRET_MASK +} + export interface BatchEmailTemplate { name: string subject: string @@ -106,10 +113,11 @@ function unwrapList(resBody: unknown): unknown[] { return unwrapPageResult(resBody).items } -function unwrapObject(resBody: unknown): unknown { - assertBusinessOk(resBody) - const root = (resBody as Record) ?? {} - return root.data ?? root.Data ?? null +/** GET /system/batch-email/account */ +export async function getBatchEmailAccount(): Promise { + const res = await axiosInstance.get(omsUrl('/system/batch-email/account')) + const raw = unwrapData | null>(res.data) + return raw ? normalizeAccount(raw) : null } /** GET /system/batch-email/templates/list */ @@ -136,13 +144,6 @@ export async function deleteBatchEmailTemplate(body: { name: string }) { assertBusinessOk(res.data) } -/** GET /system/batch-email/account */ -export async function getBatchEmailAccount(): Promise { - const res = await axiosInstance.get(omsUrl('/system/batch-email/account')) - const raw = unwrapObject(res.data) - return raw ? normalizeAccount(raw) : null -} - /** POST /system/batch-email/account/save */ export async function saveBatchEmailAccount(body: BatchEmailAccount) { const res = await axiosInstance.post(omsUrl('/system/batch-email/account/save'), body) diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 98a5fef..76950a7 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -786,11 +786,13 @@ export default { 'order.placeOrder.single.stepBasic': 'Basic info', 'order.placeOrder.single.stepSender': 'Sender', 'order.placeOrder.single.stepRecipient': 'Recipient', - 'order.placeOrder.single.stepDeclaration': 'Declaration & submit', + 'order.placeOrder.single.stepDeclaration': 'Declaration info', 'order.placeOrder.single.tip1': 'Fields marked * are required. Fill in each section in order.', 'order.placeOrder.single.tip2': 'Use “Generate sample” to try the full flow quickly.', 'order.placeOrder.single.submitHint': 'Complete required fields and accept the agreement to submit', 'order.placeOrder.single.submitReady': 'Ready to submit', + 'order.placeOrder.single.viewCustomerGuide': 'View customer guide', + 'order.placeOrder.single.declaredCurrencyHint': 'Default is USD; please enter declared prices carefully', 'order.placeOrder.standardTemplates': 'Standard templates', 'order.placeOrder.customTemplates': 'Custom templates', @@ -1716,6 +1718,10 @@ export default { 'system.batchEmail.emailConfig': 'Mailbox', 'system.batchEmail.account': 'Account', 'system.batchEmail.password': 'App password', + 'system.batchEmail.passwordPlaceholder': 'Enter SMTP app password / authorization code', + 'system.batchEmail.passwordPlaceholderEdit': 'Leave blank to keep the saved password', + 'system.batchEmail.passwordSavedHint': 'Password is stored on the server; leave blank unless you want to change it', + 'system.batchEmail.warnPassword': 'Enter app password (e.g. Aliyun authorization code)', 'system.batchEmail.serverType': 'SMTP', 'system.batchEmail.server.qiye': 'Aliyun enterprise 25', 'system.batchEmail.server.qiyeSsl': 'Aliyun enterprise SSL 465', @@ -1765,6 +1771,21 @@ export default { 'system.batchEmail.msgTestOk': 'SMTP test succeeded', 'system.batchEmail.errUnknown': 'Request failed', 'system.batchEmail.logTestFail': 'Connection test failed', + 'system.batchEmail.recipients': 'Recipients', + 'system.batchEmail.recipientMode.manual': 'Manual entry', + 'system.batchEmail.recipientMode.excel': 'Excel import', + 'system.batchEmail.manualEmailsPlaceholder': 'One email per line, or separated by comma, semicolon, or space', + 'system.batchEmail.parseEmails': 'Parse emails', + 'system.batchEmail.clearRecipients': 'Clear', + 'system.batchEmail.recipientCount': '{n} recipient(s)', + 'system.batchEmail.invalidEmailCount': 'Skipped {n} invalid email(s)', + 'system.batchEmail.recipientPreview': 'Recipient preview', + 'system.batchEmail.recipientPreviewMore': '{n} more not shown', + 'system.batchEmail.previewOrderNo': 'Order no.', + 'system.batchEmail.previewTrack': 'Latest tracking', + 'system.batchEmail.excelImportHint': 'Supports .xlsx / .xls with email and order id columns', + 'system.batchEmail.msgManualParseOk': 'Parsed {n} valid email(s)', + 'system.batchEmail.warnNoRecipients': 'Add recipient emails first', 'system.batchEmail.warnNoOrders': 'Import an Excel file first', 'system.batchEmail.warnNoTemplate': 'Select or enter a template', 'system.batchEmail.errNoAccount': 'Configure mailbox account and password', @@ -1778,8 +1799,12 @@ export default { 'system.batchEmail.msgSendDone': 'Done: {successCount} succeeded, {failCount} failed', // System - Announcement - 'system.announcement.search.placeholder': 'Enter keyword', + 'system.announcement.search.placeholder': 'Search title or content', + 'system.announcement.search.reset': 'Reset', 'system.announcement.actions.add': 'Add announcement', + 'system.announcement.actions.addNotice': 'Add notice', + 'system.announcement.actions.addMessage': 'Add message', + 'system.announcement.actions.refresh': 'Refresh', 'system.announcement.actions.batchDelete': 'Delete selected', 'system.announcement.actions.edit': 'Edit', 'system.announcement.actions.view': 'View', @@ -1794,6 +1819,10 @@ export default { 'system.announcement.popup.no': 'No', 'system.announcement.filter.noticeType': 'Notice Type', 'system.announcement.filter.receiverType': 'Receiver Type', + 'system.announcement.tab.notice': 'Notice', + 'system.announcement.tab.message': 'Message', + 'system.announcement.desc.notice': 'System notices for all or selected recipients, with optional popup', + 'system.announcement.desc.message': 'Direct messages without a required title', 'system.announcement.sendWay.now': 'Send now', 'system.announcement.sendWay.delay': 'Delay send', 'system.announcement.sendWay.schedule': 'Schedule send', @@ -1824,6 +1853,13 @@ export default { 'system.announcement.form.receiverIdsPlaceholderUser': 'Search and select internal users', 'system.announcement.form.receiverIdsPlaceholderCustomer': 'Search and select customers', 'system.announcement.form.receiverIdsFallback': 'Selected #{id}', + 'system.announcement.form.section.basic': 'Basic info', + 'system.announcement.form.section.receiver': 'Recipients', + 'system.announcement.form.messageTitleHint': 'Messages do not require a title; content is shown directly', + 'system.announcement.form.popupHint': 'When enabled, recipients see a popup on the workplace', + 'system.announcement.drawer.close': 'Close', + 'system.announcement.modal.addNoticeTitle': 'Add notice', + 'system.announcement.modal.addMessageTitle': 'Add message', 'system.announcement.placeholder.input': 'Please enter', 'system.announcement.placeholder.select': 'Please select', 'system.announcement.modal.addTitle': 'Add Announcement', @@ -2743,7 +2779,7 @@ export default { 'channel.autoTrigger.form.promptRequired': 'Please enter a prompt message', 'channel.autoTrigger.form.ruleNamePlaceholder': 'e.g. UK oversize intercept', 'channel.autoTrigger.form.promptPlaceholder': 'e.g. Oversize — order blocked', - 'channel.autoTrigger.form.notifierPlaceholder': 'Optional user IDs for warn/problem, comma-separated', + 'channel.autoTrigger.form.notifierPlaceholder': 'Optional notifier users for warn/problem', 'channel.autoTrigger.form.loadExample': 'Load oversize example', 'channel.autoTrigger.form.legacyTitle': 'Legacy plain-text condition detected — rebuild with the builder below', 'channel.autoTrigger.form.testFormula': 'Test formula', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index dcb0400..e0edc1c 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -671,11 +671,13 @@ export default { 'order.placeOrder.single.stepBasic': '基本信息', 'order.placeOrder.single.stepSender': '发件人', 'order.placeOrder.single.stepRecipient': '收件人', - 'order.placeOrder.single.stepDeclaration': '申报并提交', + 'order.placeOrder.single.stepDeclaration': '申报信息', 'order.placeOrder.single.tip1': '带 * 号为必填项,请按区块依次填写订单信息。', 'order.placeOrder.single.tip2': '可先点击「生成示例订单」快速体验完整流程。', 'order.placeOrder.single.submitHint': '请完善必填信息并勾选协议后提交', 'order.placeOrder.single.submitReady': '信息已完善,可提交下单', + 'order.placeOrder.single.viewCustomerGuide': '查看对客说明', + 'order.placeOrder.single.declaredCurrencyHint': '默认是美元,请注意填写申报价格', 'order.placeOrder.standardTemplates': '标准模板', 'order.placeOrder.customTemplates': '自定义模板', @@ -1734,6 +1736,10 @@ export default { 'system.batchEmail.emailConfig': '邮箱配置', 'system.batchEmail.account': '账号', 'system.batchEmail.password': '安全密码', + 'system.batchEmail.passwordPlaceholder': '请填写邮箱客户端授权码/安全密码', + 'system.batchEmail.passwordPlaceholderEdit': '留空则沿用已保存的安全密码', + 'system.batchEmail.passwordSavedHint': '已保存在服务器,无需重复填写;仅修改账号或 SMTP 类型时可直接保存', + 'system.batchEmail.warnPassword': '请填写安全密码(阿里邮箱请使用客户端授权码)', 'system.batchEmail.serverType': 'SMTP', 'system.batchEmail.server.qiye': '企业邮 25', 'system.batchEmail.server.qiyeSsl': '企业邮 SSL 465', @@ -1783,6 +1789,21 @@ export default { 'system.batchEmail.msgTestOk': '邮箱连接测试成功', 'system.batchEmail.errUnknown': '请求失败', 'system.batchEmail.logTestFail': '连接测试失败', + 'system.batchEmail.recipients': '收件人', + 'system.batchEmail.recipientMode.manual': '手动填入', + 'system.batchEmail.recipientMode.excel': 'Excel 导入', + 'system.batchEmail.manualEmailsPlaceholder': '每行一个邮箱,或用逗号、分号、空格分隔', + 'system.batchEmail.parseEmails': '解析邮箱', + 'system.batchEmail.clearRecipients': '清空', + 'system.batchEmail.recipientCount': '已识别 {n} 个邮箱', + 'system.batchEmail.invalidEmailCount': '已跳过 {n} 个无效邮箱', + 'system.batchEmail.recipientPreview': '收件人预览', + 'system.batchEmail.recipientPreviewMore': '另有 {n} 个收件人未展示', + 'system.batchEmail.previewOrderNo': '客户单号', + 'system.batchEmail.previewTrack': '最新轨迹', + 'system.batchEmail.excelImportHint': '支持 .xlsx / .xls,需包含邮箱列与订单号列', + 'system.batchEmail.msgManualParseOk': '已解析 {n} 个有效邮箱', + 'system.batchEmail.warnNoRecipients': '请先添加收件人邮箱', 'system.batchEmail.warnNoOrders': '请先导入订单文件', 'system.batchEmail.warnNoTemplate': '请先选择或填写发送模板', 'system.batchEmail.errNoAccount': '请配置邮箱账号和密码', @@ -1796,8 +1817,12 @@ export default { 'system.batchEmail.msgSendDone': '发送完成:成功 {successCount},失败 {failCount}', // 系统 - 公告通知 - 'system.announcement.search.placeholder': '请输入关键字', + 'system.announcement.search.placeholder': '搜索标题或正文', + 'system.announcement.search.reset': '重置', 'system.announcement.actions.add': '新增公告通知', + 'system.announcement.actions.addNotice': '新增公告', + 'system.announcement.actions.addMessage': '新增站内信', + 'system.announcement.actions.refresh': '刷新', 'system.announcement.actions.batchDelete': '删除所选', 'system.announcement.actions.edit': '编辑', 'system.announcement.actions.view': '查看', @@ -1812,6 +1837,10 @@ export default { 'system.announcement.popup.no': '否', 'system.announcement.filter.noticeType': '通知类型', 'system.announcement.filter.receiverType': '接收对象', + 'system.announcement.tab.notice': '公告', + 'system.announcement.tab.message': '站内信', + 'system.announcement.desc.notice': '面向全体或指定对象的系统公告,支持弹窗提醒', + 'system.announcement.desc.message': '点对点站内消息,无需填写标题', 'system.announcement.sendWay.now': '立即发送', 'system.announcement.sendWay.delay': '延迟发送', 'system.announcement.sendWay.schedule': '定时发送', @@ -1842,6 +1871,13 @@ export default { 'system.announcement.form.receiverIdsPlaceholderUser': '请搜索并选择内部用户', 'system.announcement.form.receiverIdsPlaceholderCustomer': '请搜索并选择客户', 'system.announcement.form.receiverIdsFallback': '已选 #{id}', + 'system.announcement.form.section.basic': '基本信息', + 'system.announcement.form.section.receiver': '接收设置', + 'system.announcement.form.messageTitleHint': '站内信无需填写标题,将直接展示正文内容', + 'system.announcement.form.popupHint': '开启后,接收人登录工作台时将弹窗展示', + 'system.announcement.drawer.close': '关闭', + 'system.announcement.modal.addNoticeTitle': '新增公告', + 'system.announcement.modal.addMessageTitle': '新增站内信', 'system.announcement.placeholder.input': '请输入', 'system.announcement.placeholder.select': '请选择', 'system.announcement.modal.addTitle': '新增公告通知', @@ -2779,7 +2815,7 @@ export default { 'channel.autoTrigger.form.promptRequired': '请填写提示语', 'channel.autoTrigger.form.ruleNamePlaceholder': '例如:英国阿里超尺寸拦截', 'channel.autoTrigger.form.promptPlaceholder': '例如:超尺寸,禁止下单', - 'channel.autoTrigger.form.notifierPlaceholder': '警告/问题件可选,用户 ID,逗号分隔', + 'channel.autoTrigger.form.notifierPlaceholder': '警告/问题件可选,请选择通知人', 'channel.autoTrigger.form.loadExample': '载入超尺寸示例', 'channel.autoTrigger.form.legacyTitle': '检测到旧版纯文本条件,请用下方构建器重新配置', 'channel.autoTrigger.form.testFormula': '测试公式', diff --git a/src/views/system/batchEmail/index.vue b/src/views/system/batchEmail/index.vue index a61c52d..d726247 100644 --- a/src/views/system/batchEmail/index.vue +++ b/src/views/system/batchEmail/index.vue @@ -6,34 +6,82 @@
- - {{ t('system.batchEmail.import') }} - {{ t('system.batchEmail.template') }} {{ name }} + + {{ t('system.batchEmail.recipientCount', { n: recipientCount }) }} +
+ + {{ t('system.batchEmail.emailConfig') }} + {{ t('system.batchEmail.send') }} +
+
+
{{ t('system.batchEmail.recipients') }}
+ + + + +
+ + + {{ t('system.batchEmail.import') }} + + {{ t('system.batchEmail.excelImportHint') }} + +
+
+
+ +
+
+ {{ t('system.batchEmail.recipientPreview') }} + + {{ t('system.batchEmail.clearRecipients') }} + +
+ +
+ {{ t('system.batchEmail.recipientPreviewMore', { n: recipientCount - recipientPreviewLimit }) }} +
+
+
+
{{ t('system.batchEmail.preview') }}
@@ -56,30 +104,6 @@ />
-
-
{{ t('system.batchEmail.emailConfig') }}
-
- - {{ t('system.batchEmail.account') }} - - {{ t('system.batchEmail.password') }} - - {{ t('system.batchEmail.serverType') }} - - {{ t('system.batchEmail.server.qiye') }} - {{ t('system.batchEmail.server.qiyeSsl') }} - {{ t('system.batchEmail.server.aliyun') }} - {{ t('system.batchEmail.server.aliyunSsl') }} - {{ t('system.batchEmail.server.mxhichina') }} - - {{ t('system.batchEmail.saveConfig') }} - - {{ t('system.batchEmail.testConnection') }} - - -
-
-
- - - +
+ + + +
@@ -132,6 +158,54 @@ > + + + + @@ -146,11 +220,13 @@ import PaginationPro from '@/components/PaginationPro/index.vue' import { deleteBatchEmailTemplate, getBatchEmailAccount, + isSmtpSecretMask, listBatchEmailRecords, listBatchEmailTemplates, saveBatchEmailAccount, saveBatchEmailTemplate, sendBatchEmailOne, + SMTP_SECRET_MASK, testBatchEmailConnection, type BatchEmailSendRecord, type BatchEmailServerType, @@ -202,17 +278,26 @@ Alibaba.com Logistics 一代国际深圳仓客户服务中心`, } +const EMAIL_SPLIT_RE = /[\n,;,;\s]+/ +const EMAIL_VALID_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const fileInputRef = ref(null) const orderData = ref([]) +const recipientMode = ref<'manual' | 'excel'>('manual') +const manualEmailsText = ref('') const templates = ref>({}) const selectedTemplateName = ref('') const contentDraft = ref('') const subjectDraft = ref('') const accountUsername = ref('') const accountPassword = ref('') +const accountConfigured = ref(false) const serverType = ref('qiye') const sending = ref(false) const testing = ref(false) +const savingConfig = ref(false) +const accountLoading = ref(false) +const emailConfigModalVisible = ref(false) const saveTplModalVisible = ref(false) const newTemplateName = ref('') @@ -223,10 +308,38 @@ const recordTotal = ref(0) const recordPage = ref(1) const recordPageSize = ref(30) const recordLoading = ref(false) -const recordTableHeight = '100%' +const recipientPreviewLimit = 8 const templateNames = computed(() => Object.keys(templates.value)) +const accountPasswordPlaceholder = computed(() => + accountConfigured.value + ? t('system.batchEmail.passwordPlaceholderEdit') + : t('system.batchEmail.passwordPlaceholder'), +) + +const recipientCount = computed(() => { + if (recipientMode.value === 'manual') { + return parseEmailTokens(manualEmailsText.value).valid.length + } + return orderData.value.length +}) + +const recipientColumns = computed(() => [ + { title: t('system.batchEmail.record.toEmail'), dataIndex: 'email', ellipsis: true, tooltip: true }, + { title: t('system.batchEmail.previewOrderNo'), dataIndex: 'orderNo', width: 140, ellipsis: true, tooltip: true }, + { title: t('system.batchEmail.previewTrack'), dataIndex: 'track', ellipsis: true, tooltip: true }, +]) + +const recipientPreviewRows = computed(() => + orderData.value.slice(0, recipientPreviewLimit).map((row, index) => ({ + key: `${row.寄件人邮箱}-${index}`, + email: row.寄件人邮箱, + orderNo: row.客户单号 || '-', + track: row.最新轨迹 || '-', + })), +) + const recordColumns = computed(() => [ { title: t('system.batchEmail.record.toEmail'), dataIndex: 'toEmail', width: 160, ellipsis: true, tooltip: true }, { title: t('system.batchEmail.record.subject'), dataIndex: 'subject', width: 140, ellipsis: true, tooltip: true }, @@ -243,6 +356,48 @@ function templatesToMap(list: Array<{ name: string; subject: string; content: st return out } +function parseEmailTokens(text: string) { + const tokens = text + .split(EMAIL_SPLIT_RE) + .map((item) => item.trim()) + .filter(Boolean) + const valid: string[] = [] + const seen = new Set() + let invalid = 0 + tokens.forEach((token) => { + if (!EMAIL_VALID_RE.test(token)) { + invalid += 1 + return + } + const key = token.toLowerCase() + if (seen.has(key)) return + seen.add(key) + valid.push(token) + }) + return { valid, invalid } +} + +function buildOrderRowsFromEmails(emails: string[]): OrderRow[] { + return emails.map((email) => ({ + 客户单号: '', + 最新轨迹: '', + 换单号: '', + 寄件人邮箱: email, + })) +} + +function getRecipientsForSend(): OrderRow[] { + if (recipientMode.value === 'manual') { + return buildOrderRowsFromEmails(parseEmailTokens(manualEmailsText.value).valid) + } + return orderData.value +} + +function clearRecipients() { + orderData.value = [] + manualEmailsText.value = '' +} + async function loadTemplatesFromApi() { try { const list = await listBatchEmailTemplates() @@ -258,17 +413,47 @@ async function loadTemplatesFromApi() { } async function loadAccountFromApi() { + accountLoading.value = true try { const account = await getBatchEmailAccount() - if (!account) return + if (!account) { + accountConfigured.value = false + accountUsername.value = '' + accountPassword.value = '' + serverType.value = 'qiye' + return + } + accountConfigured.value = true accountUsername.value = account.smtpUsername - accountPassword.value = account.smtpPassword + accountPassword.value = account.smtpPassword || SMTP_SECRET_MASK if (account.serverType) serverType.value = account.serverType - } catch { - /* ignore */ + } catch (e: unknown) { + const msg = (e as Error)?.message || t('system.batchEmail.errUnknown') + Message.error(msg) + } finally { + accountLoading.value = false } } +function resolvePasswordForRequest(): string { + const trimmed = accountPassword.value.trim() + if (trimmed && !isSmtpSecretMask(trimmed)) return trimmed + if (accountConfigured.value) return SMTP_SECRET_MASK + return trimmed +} + +function hasUsableSmtpAuth(): boolean { + if (!accountUsername.value.trim()) return false + const trimmed = accountPassword.value.trim() + if (trimmed && !isSmtpSecretMask(trimmed)) return true + return accountConfigured.value +} + +async function openEmailConfigModal() { + emailConfigModalVisible.value = true + await loadAccountFromApi() +} + function formatSendTime(value?: string) { if (!value) return '-' const parsed = dayjs(value) @@ -300,7 +485,7 @@ function handleRecordPageSizeChange(size: number) { loadRecords() } -function applyTemplateByName(name: string | undefined, silent?: boolean) { +function applyTemplateByName(name: string | undefined) { if (!name || !templates.value[name]) return const tpl = templates.value[name] contentDraft.value = tpl.content @@ -308,7 +493,7 @@ function applyTemplateByName(name: string | undefined, silent?: boolean) { } function onTemplateChange(name: string | undefined) { - applyTemplateByName(name, false) + applyTemplateByName(name) } function triggerImport() { @@ -390,6 +575,7 @@ function onExcelSelected(ev: Event) { }) orderData.value = list + recipientMode.value = 'excel' Message.success(t('system.batchEmail.msgImportOk', { n: list.length })) } catch (err) { const msg = err instanceof Error ? err.message : String(err) @@ -486,7 +672,7 @@ function deleteCurrentTemplate() { await loadTemplatesFromApi() const keys = Object.keys(templates.value) selectedTemplateName.value = keys[0] ?? '' - applyTemplateByName(keys[0], true) + applyTemplateByName(keys[0]) Message.success(t('system.batchEmail.msgDeleteTplOk', { name })) } catch (e: unknown) { const msg = (e as Error)?.message || t('system.batchEmail.errUnknown') @@ -498,36 +684,54 @@ function deleteCurrentTemplate() { async function saveAccountConfig() { const u = accountUsername.value.trim() - const p = accountPassword.value - if (!u || !p) { + const p = resolvePasswordForRequest() + if (!u) { Message.warning(t('system.batchEmail.warnAccount')) - return + return false + } + if (!accountConfigured.value && (!p || isSmtpSecretMask(p))) { + Message.warning(t('system.batchEmail.warnPassword')) + return false } try { await saveBatchEmailAccount({ smtpUsername: u, - smtpPassword: p, + smtpPassword: p || SMTP_SECRET_MASK, serverType: serverType.value, }) + accountConfigured.value = true + if (!accountPassword.value.trim() || isSmtpSecretMask(accountPassword.value)) { + accountPassword.value = SMTP_SECRET_MASK + } Message.success(t('system.batchEmail.msgSaveConfigOk')) + return true } catch (e: unknown) { const msg = (e as Error)?.message || t('system.batchEmail.errUnknown') Message.error(msg) + return false + } +} + +async function saveAccountConfigFromModal() { + savingConfig.value = true + try { + const ok = await saveAccountConfig() + if (ok) emailConfigModalVisible.value = false + } finally { + savingConfig.value = false } } async function testConnection() { - const u = accountUsername.value.trim() - const p = accountPassword.value - if (!u || !p) { + if (!hasUsableSmtpAuth()) { Message.warning(t('system.batchEmail.warnAccount')) return } testing.value = true try { await testBatchEmailConnection({ - smtpUsername: u, - smtpPassword: p, + smtpUsername: accountUsername.value.trim(), + smtpPassword: resolvePasswordForRequest(), serverType: serverType.value, }) Message.success(t('system.batchEmail.msgTestOk')) @@ -543,30 +747,35 @@ async function testConnection() { } } -async function startBatchSend() { - if (!orderData.value.length) { - Message.warning(t('system.batchEmail.warnNoOrders')) - return +function ensureRecipientsReady(): boolean { + if (!getRecipientsForSend().length) { + Message.warning(t('system.batchEmail.warnNoRecipients')) + return false } + return true +} + +async function startBatchSend() { + if (!ensureRecipientsReady()) return + const recipients = getRecipientsForSend() const bodyContent = contentDraft.value.trim() if (!bodyContent) { Message.warning(t('system.batchEmail.warnNoTemplate')) return } const u = accountUsername.value.trim() - const p = accountPassword.value - if (!u || !p) { + if (!hasUsableSmtpAuth()) { Message.error(t('system.batchEmail.errNoAccount')) return } + const p = resolvePasswordForRequest() sending.value = true let successCount = 0 let failCount = 0 - const total = orderData.value.length - for (let i = 0; i < orderData.value.length; i += 1) { - const order = orderData.value[i] + for (let i = 0; i < recipients.length; i += 1) { + const order = recipients[i] const email = order.寄件人邮箱?.trim() ?? '' try { if (!email || !email.includes('@')) { @@ -605,7 +814,7 @@ onMounted(async () => { const names = templateNames.value if (names.length) { selectedTemplateName.value = names[0] - applyTemplateByName(names[0], true) + applyTemplateByName(names[0]) } }) @@ -614,7 +823,7 @@ onMounted(async () => { .batch-email-container { width: 100%; box-sizing: border-box; - padding: 16px; + padding: 16px 16px 24px; background-color: #f0f3f4; flex: 1; min-height: 0; @@ -632,7 +841,7 @@ onMounted(async () => { } .batch-email-left { - flex: 1 1 52%; + flex: 1 1 54%; min-width: 480px; min-height: 0; display: flex; @@ -640,8 +849,8 @@ onMounted(async () => { } .batch-email-right { - flex: 1 1 48%; - min-width: 420px; + flex: 1 1 46%; + min-width: 400px; min-height: 0; display: flex; flex-direction: column; @@ -653,6 +862,7 @@ onMounted(async () => { height: 100%; display: flex; flex-direction: column; + overflow: hidden; } .batch-email-card { @@ -664,10 +874,14 @@ onMounted(async () => { } .record-card { - padding: 16px; + padding: 0; box-sizing: border-box; } +.recipient-section { + flex-shrink: 0; +} + .preview-section { flex: 1; min-height: 0; @@ -676,22 +890,24 @@ onMounted(async () => { overflow: auto; } -.section-config { - flex-shrink: 0; - margin-top: auto; - border-top: 1px solid #f0f0f0; - padding-top: 16px; -} - .record-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; - margin-bottom: 12px; + padding: 16px 16px 0; + margin-bottom: 16px; flex-shrink: 0; } +.list-wrap { + flex: 1; + min-height: 0; + padding: 0 16px 16px; + display: flex; + flex-direction: column; +} + .record-table { flex: 1; min-height: 0; @@ -736,6 +952,10 @@ onMounted(async () => { color: #262626; } +.recipient-tag { + margin-left: 4px; +} + .toolbar-btn { height: 32px; border-radius: 4px; @@ -763,6 +983,56 @@ onMounted(async () => { margin-bottom: 8px; } +.recipient-tabs { + :deep(.arco-tabs-content) { + padding-top: 12px; + } +} + +.manual-email-textarea { + :deep(textarea) { + font-family: inherit; + } +} + +.excel-import-panel { + display: flex; + flex-direction: column; + gap: 8px; +} + +.recipient-preview { + margin-top: 12px; + padding: 12px; + background: #f7f8fa; + border-radius: 4px; +} + +.preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.preview-title { + font-size: 13px; + color: #4e5969; + font-weight: 500; +} + +.preview-more { + margin-top: 8px; + font-size: 12px; + color: #86909c; +} + +.recipient-preview-table { + :deep(.arco-table-th) { + background: #f2f3f5; + } +} + .preview-toolbar { margin-bottom: 8px; } @@ -772,24 +1042,41 @@ onMounted(async () => { color: #86909c; } +.warn-text { + color: rgb(var(--orange-6)); +} + .content-textarea { margin-top: 4px; flex: 1; - min-height: 160px; + min-height: 140px; :deep(textarea) { height: 100% !important; - min-height: 160px; + min-height: 140px; } } -.config-row { - margin-top: 8px; +.password-hint { + margin-top: 4px; } -.config-label { - font-size: 14px; - color: #262626; +.email-config-spin { + display: block; + width: 100%; +} + +.email-config-form { + :deep(.arco-form-item) { + margin-bottom: 16px; + } +} + +.email-config-footer { + display: flex; + justify-content: flex-end; + padding-top: 8px; + border-top: 1px solid var(--color-border-2); } .subject-row { @@ -801,7 +1088,7 @@ onMounted(async () => { } .subject-label { - flex: 0 0 96px; + flex: 0 0 72px; font-size: 14px; color: #262626; } @@ -810,4 +1097,18 @@ onMounted(async () => { flex: 1; min-width: 240px; } + +@media (max-width: 1200px) { + .batch-email-layout { + flex-direction: column; + overflow-y: auto; + } + + .batch-email-left, + .batch-email-right { + min-width: 0; + flex: none; + min-height: 480px; + } +}