合并邮件通知设置并统一公共配置
This commit is contained in:
parent
94cc1ce0e7
commit
355c80dbc5
91
frontend/src/views/Dashboard/MailNotificationSettings.css
Normal file
91
frontend/src/views/Dashboard/MailNotificationSettings.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
85
frontend/src/views/Dashboard/MailNotificationSettings.vue
Normal file
85
frontend/src/views/Dashboard/MailNotificationSettings.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
import HelpTip from "../components/HelpTip/HelpTip.vue";
|
||||
|
||||
defineProps({ notification: { type: Object, required: true } });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mail-notification-settings">
|
||||
<div class="mail-common-fields">
|
||||
<label class="mail-field">
|
||||
<span>发件人邮箱</span>
|
||||
<input v-model="notification.sender_email" type="email" />
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>收件人 <HelpTip>多个地址用逗号分隔,两种发送方式共用此配置。</HelpTip></span>
|
||||
<input
|
||||
:value="notification.recipients.join(', ')"
|
||||
@input="notification.recipients = $event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean)"
|
||||
placeholder="多个地址用逗号分隔"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mail-channels">
|
||||
<fieldset class="mail-channel">
|
||||
<legend>HTTP 接口</legend>
|
||||
<label class="mail-switch">
|
||||
<input v-model="notification.http_enabled" type="checkbox" />
|
||||
<span>启用接口发送</span>
|
||||
<HelpTip>通过邮件服务接口发送通知,保留现有接口配置。</HelpTip>
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>通知服务商</span>
|
||||
<input v-model="notification.provider" placeholder="例如 temporary-mail" />
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>通知接口地址</span>
|
||||
<input v-model="notification.endpoint" placeholder="https://..." />
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="mail-channel">
|
||||
<legend>SMTP 服务</legend>
|
||||
<label class="mail-switch">
|
||||
<input v-model="notification.smtp_enabled" type="checkbox" />
|
||||
<span>启用 SMTP 发送</span>
|
||||
<HelpTip>通过 SMTP 服务发送邮件,保留现有通道启用状态。</HelpTip>
|
||||
</label>
|
||||
<div class="mail-smtp-fields">
|
||||
<label class="mail-field">
|
||||
<span>SMTP 主机</span>
|
||||
<input v-model="notification.smtp_host" placeholder="例如 smtp.example.com" />
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>SMTP 端口</span>
|
||||
<input v-model.number="notification.smtp_port" type="number" min="1" max="65535" />
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>SMTP 用户名</span>
|
||||
<input v-model="notification.smtp_username" autocomplete="username" />
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>SMTP 密码
|
||||
<HelpTip>{{ notification.smtp_credential_configured ? "凭据已配置;密码不会回显,留空表示不更新。" : "凭据尚未配置,请按 SMTP 服务要求填写。" }}</HelpTip>
|
||||
</span>
|
||||
<input v-model="notification.smtp_password" type="password" autocomplete="new-password" placeholder="留空保留已保存密码" />
|
||||
</label>
|
||||
<label class="mail-field">
|
||||
<span>连接模式</span>
|
||||
<select v-model="notification.smtp_mode">
|
||||
<option value="plain">明文连接</option>
|
||||
<option value="starttls">STARTTLS</option>
|
||||
<option value="implicit_tls">隐式 TLS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="mail-switch mail-auth-switch">
|
||||
<input v-model="notification.smtp_allow_plaintext_auth" type="checkbox" />
|
||||
<span>允许明文认证</span>
|
||||
<HelpTip>仅在明确确认 SMTP 网络受保护时开启。</HelpTip>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped src="./MailNotificationSettings.css"></style>
|
||||
@ -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]);
|
||||
|
||||
@ -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, /<MailNotificationSettings :notification="settingsForm.notification"/);
|
||||
assert.doesNotMatch(mailSource, /role="tab"/);
|
||||
});
|
||||
|
||||
test("合并通知页面不改变通道开关和已保存配置", () => {
|
||||
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, /<form novalidate @submit.prevent="submitSettings">/);
|
||||
assert.ok(!source.includes('<p class="field-hint"'));
|
||||
@ -57,8 +118,8 @@ test("settings.keyboardNavigationWrapsAndFocusesTabs", async () => {
|
||||
} },
|
||||
};
|
||||
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");
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import HelpTip from "../components/HelpTip/HelpTip.vue";
|
||||
import { useSettingsModal } from "./SettingsModal.js";
|
||||
import MailNotificationSettings from "./MailNotificationSettings.vue";
|
||||
|
||||
const props = defineProps({ state: { type: Object, required: true } });
|
||||
const {
|
||||
@ -73,28 +74,16 @@ const { activeSection, submitSettings, navigateTabs } = useSettingsModal(saveSet
|
||||
DNS API 验证
|
||||
</button>
|
||||
<button
|
||||
id="settings-tab-http"
|
||||
id="settings-tab-notification"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="settings-panel-http"
|
||||
:aria-selected="activeSection === 'http'"
|
||||
:tabindex="activeSection === 'http' ? 0 : -1"
|
||||
@click="activeSection = 'http'"
|
||||
aria-controls="settings-panel-notification"
|
||||
:aria-selected="activeSection === 'notification'"
|
||||
:tabindex="activeSection === 'notification' ? 0 : -1"
|
||||
@click="activeSection = 'notification'"
|
||||
@keydown="navigateTabs"
|
||||
>
|
||||
HTTP 通知
|
||||
</button>
|
||||
<button
|
||||
id="settings-tab-smtp"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="settings-panel-smtp"
|
||||
:aria-selected="activeSection === 'smtp'"
|
||||
:tabindex="activeSection === 'smtp' ? 0 : -1"
|
||||
@click="activeSection = 'smtp'"
|
||||
@keydown="navigateTabs"
|
||||
>
|
||||
SMTP 通知
|
||||
邮件通知
|
||||
</button>
|
||||
</div>
|
||||
<form novalidate @submit.prevent="submitSettings">
|
||||
@ -371,98 +360,15 @@ const { activeSection, submitSettings, navigateTabs } = useSettingsModal(saveSet
|
||||
</HelpTip>
|
||||
</section>
|
||||
<section
|
||||
v-show="activeSection === 'http'"
|
||||
id="settings-panel-http"
|
||||
data-settings-section="http"
|
||||
v-show="activeSection === 'notification'"
|
||||
id="settings-panel-notification"
|
||||
data-settings-section="notification"
|
||||
role="tabpanel"
|
||||
aria-labelledby="settings-tab-http"
|
||||
aria-labelledby="settings-tab-notification"
|
||||
class="settings-panel"
|
||||
>
|
||||
<h3>HTTP 通知</h3>
|
||||
<div class="form-grid">
|
||||
<label class="switch-field">
|
||||
<input v-model="settingsForm.notification.http_enabled" type="checkbox" />
|
||||
<span>
|
||||
<strong>启用 HTTP 通知</strong>
|
||||
<HelpTip>通过外部邮件或消息服务 API 发送通知</HelpTip>
|
||||
</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>通知服务商</span>
|
||||
<input v-model="settingsForm.notification.provider" placeholder="例如 temporary-mail" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span>通知接口地址</span>
|
||||
<input v-model="settingsForm.notification.endpoint" placeholder="https://..." />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>发件人邮箱</span>
|
||||
<input v-model="settingsForm.notification.sender_email" type="email" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span>收件人</span>
|
||||
<input :value="settingsForm.notification.recipients.join(', ')" @input="settingsForm.notification.recipients = $event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean)" placeholder="多个地址用逗号分隔" />
|
||||
</label>
|
||||
</section>
|
||||
<section
|
||||
v-show="activeSection === 'smtp'"
|
||||
id="settings-panel-smtp"
|
||||
data-settings-section="smtp"
|
||||
role="tabpanel"
|
||||
aria-labelledby="settings-tab-smtp"
|
||||
class="settings-panel"
|
||||
>
|
||||
<h3>SMTP 通知</h3>
|
||||
<label class="switch-field">
|
||||
<input v-model="settingsForm.notification.smtp_enabled" type="checkbox" />
|
||||
<span>
|
||||
<strong>启用 SMTP 通知</strong>
|
||||
<HelpTip>使用外部 SMTP 服务发送证书任务通知,不会实现邮件服务器端</HelpTip>
|
||||
</span>
|
||||
</label>
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span>SMTP 主机</span>
|
||||
<input v-model="settingsForm.notification.smtp_host" placeholder="例如 smtp.example.com" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>SMTP 端口</span>
|
||||
<input v-model.number="settingsForm.notification.smtp_port" type="number" min="1" max="65535" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span>SMTP 用户名</span>
|
||||
<input v-model="settingsForm.notification.smtp_username" autocomplete="username" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>SMTP 密码</span>
|
||||
<input v-model="settingsForm.notification.smtp_password" type="password" autocomplete="new-password" placeholder="留空表示不更新已保存密码" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span>连接模式</span>
|
||||
<select v-model="settingsForm.notification.smtp_mode">
|
||||
<option value="plain">明文连接</option>
|
||||
<option value="starttls">STARTTLS</option>
|
||||
<option value="implicit_tls">隐式 TLS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="switch-field">
|
||||
<input v-model="settingsForm.notification.smtp_allow_plaintext_auth" type="checkbox" />
|
||||
<span>
|
||||
<strong>允许明文认证</strong>
|
||||
<HelpTip>仅在明确确认 SMTP 网络受保护时开启</HelpTip>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<HelpTip>
|
||||
{{ settingsForm.notification.smtp_credential_configured ? "SMTP 凭据已配置;密码不会回显。填写新密码后保存即可更新。" : "SMTP 凭据尚未配置;启用 SMTP 时请填写用户名和密码。" }}
|
||||
</HelpTip>
|
||||
<h3>邮件通知</h3>
|
||||
<MailNotificationSettings :notification="settingsForm.notification" />
|
||||
</section>
|
||||
<p v-if="settingsError" class="form-error">{{ settingsError }}</p>
|
||||
<div class="modal-actions">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user