diff --git a/frontend/src/views/Dashboard/MailNotificationSettings.css b/frontend/src/views/Dashboard/MailNotificationSettings.css new file mode 100644 index 0000000..6a49a29 --- /dev/null +++ b/frontend/src/views/Dashboard/MailNotificationSettings.css @@ -0,0 +1,91 @@ +.mail-common-fields, +.mail-channels, +.mail-smtp-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px 16px; +} + +.mail-common-fields { + margin-bottom: 14px; +} + +.mail-channels { + grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); +} + +.mail-channel { + min-width: 0; + margin: 0; + padding: 12px 14px; + border: 1px solid #dce6f1; + border-radius: 10px; +} + +.mail-channel legend { + padding: 0 6px; + color: #32405a; + font-size: 13px; + font-weight: 700; +} + +.mail-field { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; + color: #526983; + font-size: 12px; +} + +.mail-channel > .mail-field + .mail-field { + margin-top: 12px; +} + +.mail-field > span { + display: flex; + align-items: center; + min-height: 22px; +} + +.mail-field input, +.mail-field select { + box-sizing: border-box; + width: 100%; + min-width: 0; + min-height: 36px; + padding: 8px 10px; + border: 1px solid #cbd8e7; + border-radius: 8px; + background: #fff; + color: #263650; + font: inherit; + font-size: 13px; +} + +.mail-switch { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 12px; + color: #32405a; + font-size: 12px; +} + +.mail-switch input { + margin: 0; +} + +.mail-auth-switch { + align-self: end; + min-height: 36px; + margin-bottom: 0; +} + +@media (max-width: 700px) { + .mail-common-fields, + .mail-channels, + .mail-smtp-fields { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/frontend/src/views/Dashboard/MailNotificationSettings.vue b/frontend/src/views/Dashboard/MailNotificationSettings.vue new file mode 100644 index 0000000..4bc5a4a --- /dev/null +++ b/frontend/src/views/Dashboard/MailNotificationSettings.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/frontend/src/views/Dashboard/SettingsModal.js b/frontend/src/views/Dashboard/SettingsModal.js index 72153ff..8a6bf47 100644 --- a/frontend/src/views/Dashboard/SettingsModal.js +++ b/frontend/src/views/Dashboard/SettingsModal.js @@ -1,6 +1,6 @@ import { nextTick, ref } from "vue"; -const sections = ["service", "acme", "dns", "http", "smtp"]; +const sections = ["service", "acme", "dns", "notification"]; export function useSettingsModal(saveSettings) { const activeSection = ref(sections[0]); diff --git a/frontend/src/views/Dashboard/SettingsModal.test.js b/frontend/src/views/Dashboard/SettingsModal.test.js index fe375cc..54669f1 100644 --- a/frontend/src/views/Dashboard/SettingsModal.test.js +++ b/frontend/src/views/Dashboard/SettingsModal.test.js @@ -3,10 +3,69 @@ import test from "node:test"; import { readFileSync } from "node:fs"; import { parse, compileScript, compileTemplate } from "@vue/compiler-sfc"; import { useSettingsModal } from "./SettingsModal.js"; +import { settingsPayload } from "./SettingsPayload.js"; const source = readFileSync(new URL("./SettingsModal.vue", import.meta.url), "utf8"); -test("settings.componentCompilesWithFiveAccessiblePanels", () => { +test("邮件通知组件保留两种通道且公共字段只出现一次", () => { + const mailSource = readFileSync(new URL("./MailNotificationSettings.vue", import.meta.url), "utf8"); + const { descriptor, errors } = parse(mailSource); + assert.deepEqual(errors, []); + const script = compileScript(descriptor, { id: "mail-settings-test" }); + const compiled = compileTemplate({ + source: descriptor.template.content, + filename: "MailNotificationSettings.vue", + id: "mail-settings-test", + compilerOptions: { bindingMetadata: script.bindings }, + }); + assert.deepEqual(compiled.errors, []); + for (const field of ["http_enabled", "smtp_enabled", "sender_email", "smtp_password"]) { + assert.equal(mailSource.split(`v-model="notification.${field}"`).length - 1, 1); + } + assert.equal(mailSource.split(':value="notification.recipients.join').length - 1, 1); + assert.match(source, / { + for (const http of [false, true]) { + for (const smtp of [false, true]) { + const notification = { + http_enabled: http, + smtp_enabled: smtp, + endpoint: "https://mail.example.com", + sender_email: "sender@example.com", + recipients: ["recipient@example.com"], + smtp_host: "smtp.example.com", + smtp_port: 587, + smtp_username: "sender", + smtp_password: "", + smtp_mode: "starttls", + smtp_credential_configured: true, + }; + const payload = settingsPayload({ notification }).notification; + const { smtp_credential_configured, ...expected } = notification; + assert.deepEqual(payload, expected); + assert.equal(notification.smtp_credential_configured, true); + } + } +}); + +test("通知字段校验失败时切换到合并后的邮件通知页", async () => { + let saved = false; + const state = useSettingsModal(() => { saved = true; }); + const sections = []; + const invalid = { + closest: () => ({ dataset: { settingsSection: "notification" } }), + focus: () => sections.push(state.activeSection.value), + reportValidity: () => sections.push(state.activeSection.value), + }; + await state.submitSettings({ currentTarget: { querySelector: () => invalid } }); + assert.equal(saved, false); + assert.deepEqual(sections, ["notification", "notification"]); +}); + +test("settings.componentCompilesWithFourAccessiblePanels", () => { const { descriptor, errors } = parse(source); assert.deepEqual(errors, []); const script = compileScript(descriptor, { id: "settings-test" }); @@ -18,8 +77,10 @@ test("settings.componentCompilesWithFiveAccessiblePanels", () => { compilerOptions: { bindingMetadata: script.bindings }, }); assert.deepEqual(result.errors, []); - assert.equal((source.match(/role="tab"/g) || []).length, 5); - assert.equal((source.match(/role="tabpanel"/g) || []).length, 5); + assert.equal((source.match(/role="tab"/g) || []).length, 4); + assert.equal((source.match(/role="tabpanel"/g) || []).length, 4); + assert.match(source, /id="settings-tab-notification"/); + assert.doesNotMatch(source, /settings-tab-(http|smtp)/); assert.match(source, /:required="field.required && dnsVerificationRequired"/); assert.match(source, /
/); assert.ok(!source.includes('

{ } }, }; await state.navigateTabs(event); - assert.equal(state.activeSection.value, "smtp"); - assert.deepEqual(focused, ["#settings-tab-smtp"]); + assert.equal(state.activeSection.value, "notification"); + assert.deepEqual(focused, ["#settings-tab-notification"]); await state.navigateTabs({ ...event, key: "Home" }); assert.equal(state.activeSection.value, "service"); }); diff --git a/frontend/src/views/Dashboard/SettingsModal.vue b/frontend/src/views/Dashboard/SettingsModal.vue index e73f0d0..1fc5922 100644 --- a/frontend/src/views/Dashboard/SettingsModal.vue +++ b/frontend/src/views/Dashboard/SettingsModal.vue @@ -1,6 +1,7 @@