fix(email): complete mail settings and templates

This commit is contained in:
kate522 committed 2026-09-04 12:06:42 +08:00
1 parent 4f8ab94fac
commit 4833053a5c
29 files changed
+716 -426

No files matched your search

-2
View File
@@ -3,8 +3,6 @@ export const globalApiMethods = {
* 全局&功能 (Global & Functions)
* ================================================================ */
getMailSettings() { return this.request('/settings/mail') },
saveMailSettings(data) { return this.request('/settings/mail', { method: 'POST', body: JSON.stringify(data) }) },
testMail(to) { return this.request('/mail/test', { method: 'POST', body: JSON.stringify({ to }) }) },
getFooterTabbarSettings() { return this.request('/settings/footer-tabbar') },
+12 -1
View File
@@ -273,6 +273,17 @@
v-html="schema.content || modelValue || ''"
/>
<!-- Action button -->
<n-button
v-else-if="schema.type === 'button'"
:type="schema.buttonType || 'default'"
:loading="schema.loading"
:disabled="schema.disabled"
@click="emit('action', schema.key)"
>
{{ schema.buttonText || schema.label || '执行' }}
</n-button>
<!-- Date picker -->
<n-date-picker
v-else-if="schema.type === 'date'"
@@ -645,7 +656,7 @@ const props = defineProps({
errorMessage: { type: String, default: '' },
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'action'])
const secretMarkedForClear = computed(() => props.schema.secret && props.modelValue === '__AKSRT_CLEAR_SECRET__')
const secretDisplayValue = computed(() => secretMarkedForClear.value ? '' : props.modelValue)
@@ -66,6 +66,7 @@
:model-value="getFieldValue(field.key)"
:error-message="errors?.[field.key]"
@update:model-value="setFieldValue(field.key, $event)"
@action="emit('field-action', $event)"
/>
</slot>
<AksField
@@ -74,6 +75,7 @@
:model-value="getFieldValue(field.key)"
:error-message="errors?.[field.key]"
@update:model-value="setFieldValue(field.key, $event)"
@action="emit('field-action', $event)"
/>
</template>
</template>
@@ -161,7 +163,7 @@ const props = defineProps({
errors: { type: Object, default: () => ({}) },
})
const emit = defineEmits(['update:formData', 'save', 'reload'])
const emit = defineEmits(['update:formData', 'save', 'reload', 'field-action'])
const slots = useSlots()
const searchQuery = ref('')
+24 -5
View File
@@ -14,6 +14,7 @@
@update:form-data="formData = $event"
@save="handleSave"
@reload="handleReload"
@field-action="handleFieldAction"
>
<template
v-if="isPermissionsLogin"
@@ -142,6 +143,7 @@ const captchaProviderTab = ref('tencent')
/** Resolve domain/section from props or route meta (canonicalDomain + section, with legacy fallback) */
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
const resolvedSection = computed(() => props.section || route.meta?.section || route.meta?.schemaSection || resolveSectionFromPath(route.path))
const isEmailPage = computed(() => resolvedDomain.value === 'global' && resolvedSection.value === 'email')
const isPermissionsLogin = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'login')
const isPermissionsModeration = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'moderation')
@@ -158,7 +160,7 @@ function resolveSectionFromPath(path) {
/** Load schema from the Vue-side registry */
const pageMeta = computed(() => {
const schema = resolvedDomain.value === 'page_display'
const schema = (isEmailPage.value || resolvedDomain.value === 'page_display')
? serverSchema.value
: getSchema(resolvedDomain.value, resolvedSection.value)
if (schema && schema.groups && schema.groups.length > 0) {
@@ -180,7 +182,7 @@ const pageMeta = computed(() => {
/** Default values from schema */
const defaults = computed(() => {
const schema = resolvedDomain.value === 'page_display'
const schema = (isEmailPage.value || resolvedDomain.value === 'page_display')
? serverSchema.value
: getSchema(resolvedDomain.value, resolvedSection.value)
if (!schema) return {}
@@ -202,9 +204,7 @@ const {
} = useSectionForm({
fetch: async () => {
const response = await api.getSchemaPage(resolvedDomain.value, resolvedSection.value)
if (resolvedDomain.value === 'page_display') {
serverSchema.value = response?.schema || null
}
serverSchema.value = response?.schema || serverSchema.value
return response
},
submit: async (data) => {
@@ -231,6 +231,25 @@ const {
},
})
async function handleFieldAction(key) {
if (key !== 'email_test_send' || !isEmailPage.value) return
const to = String(formData.value?.email_test_to || '').trim()
if (!to) {
message.warning('请先填写测试收件地址')
return
}
if (dirty.value) {
message.warning('请先保存当前配置,再发送测试邮件')
return
}
try {
const response = await api.testMail(to)
message.success(response?.message || '测试邮件已发送')
} catch (error) {
message.error(error?.message || '测试邮件发送失败')
}
}
async function handleSave() {
try {
await doSave()
@@ -1,109 +0,0 @@
export const emailSchema = {
key: 'global.email',
title: '邮件配置',
icon: 'fa fas fa-envelope',
description: 'SMTP 发信配置、邮件事件开关与模板自定义。',
defaults: {
email_footer_text: '',
email_site_branding: true,
email_template_mode: 'plain',
email_reg_verify_link_enabled: true,
email_reg_verify_code_enabled: true,
email_reg_success_enabled: true,
email_password_reset_enabled: true,
email_change_code_enabled: true,
email_change_notice_enabled: true,
},
groups: [
{
key: 'global',
title: '全局设置',
icon: 'fa fas fa-cog',
fields: [
{ key: 'email_footer_text', label: '邮件页脚文字', type: 'textarea', rows: 2, placeholder: '留空则使用站点名称', help: '附加在每封邮件底部的文字' },
{ key: 'email_site_branding', label: '显示站点品牌', type: 'switch', default: true, help: '在邮件开头显示「[站点名]」前缀' },
{ key: 'email_template_mode', label: '邮件格式', type: 'radio', default: 'plain', options: [
{ label: '纯文本', value: 'plain' },
{ label: 'HTML', value: 'html' },
]},
],
},
{
key: 'branding',
title: '邮件品牌',
icon: 'fa fas fa-pen',
fields: [
{ key: 'mail_showname', label: '发件人名称', type: 'input', default: '', help: '覆盖系统默认发件人名称' },
{ key: 'mail_title_prefix', label: '主题前缀', type: 'input', default: '', help: '自动添加在所有邮件主题前面,如【站点名】' },
{ key: 'mail_description', label: '邮件底部说明', type: 'textarea', rows: 3, default: '', help: '附加在邮件页脚文字之后,留空不显示' },
{ key: 'mail_more_content', label: '邮件附加内容', type: 'textarea', rows: 4, default: '', help: '附加在邮件内容末尾的补充说明' },
],
},
{
key: 'reg_verify_link',
title: '注册验证链接',
icon: 'fa fas fa-link',
fields: [
{ key: 'email_reg_verify_link_enabled', label: '启用', type: 'switch', default: true, help: '注册后发送验证链接邮件' },
{ key: 'email_reg_verify_link_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '验证你的邮箱地址' },
{ key: 'email_reg_verify_link_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {url}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{url} 验证链接' },
{ key: 'email_reg_verify_link_html', label: 'HTML 格式', type: 'switch', default: false },
],
},
{
key: 'reg_verify_code',
title: '注册验证码',
icon: 'fa fas fa-keyboard',
fields: [
{ key: 'email_reg_verify_code_enabled', label: '启用', type: 'switch', default: true, help: '注册后发送 6 位验证码邮件' },
{ key: 'email_reg_verify_code_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '邮箱验证码' },
{ key: 'email_reg_verify_code_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {code}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{code} 验证码' },
{ key: 'email_reg_verify_code_html', label: 'HTML 格式', type: 'switch', default: false },
],
},
{
key: 'reg_success',
title: '注册成功通知',
icon: 'fa fas fa-check-circle',
fields: [
{ key: 'email_reg_success_enabled', label: '启用', type: 'switch', default: true, help: '邮箱验证成功后发送通知邮件' },
{ key: 'email_reg_success_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '邮箱验证成功' },
{ key: 'email_reg_success_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name}', help: '可用占位符:{username} 用户名、{site_name} 站点名' },
{ key: 'email_reg_success_html', label: 'HTML 格式', type: 'switch', default: false },
],
},
{
key: 'password_reset',
title: '密码重置',
icon: 'fa fas fa-unlock-alt',
fields: [
{ key: 'email_password_reset_enabled', label: '启用', type: 'switch', default: true, help: '发送密码重置链接邮件' },
{ key: 'email_password_reset_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '重置密码' },
{ key: 'email_password_reset_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {url}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{url} 重置链接' },
{ key: 'email_password_reset_html', label: 'HTML 格式', type: 'switch', default: false },
],
},
{
key: 'change_code',
title: '邮箱变更验证码',
icon: 'fa fas fa-shield-alt',
fields: [
{ key: 'email_change_code_enabled', label: '启用', type: 'switch', default: true, help: '变更邮箱时发送验证码邮件' },
{ key: 'email_change_code_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '邮箱验证码' },
{ key: 'email_change_code_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {code}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{code} 验证码' },
{ key: 'email_change_code_html', label: 'HTML 格式', type: 'switch', default: false },
],
},
{
key: 'change_notice',
title: '邮箱变更通知',
icon: 'fa fas fa-bell',
fields: [
{ key: 'email_change_notice_enabled', label: '启用', type: 'switch', default: true, help: '邮箱变更成功后向旧邮箱发送通知' },
{ key: 'email_change_notice_subject', label: '主题(留空使用默认)', type: 'input', placeholder: '账号邮箱已变更' },
{ key: 'email_change_notice_content', label: '内容模板(留空使用默认)', type: 'textarea', rows: 4, placeholder: '支持 {username} {site_name} {new_email}', help: '可用占位符:{username} 用户名、{site_name} 站点名、{new_email} 新邮箱' },
{ key: 'email_change_notice_html', label: 'HTML 格式', type: 'switch', default: false },
],
},
],
}
@@ -7,7 +7,7 @@ import { logoSchema } from './logo.js'
import { commonSchema } from './common.js'
import { layoutSchema } from './layout.js'
import { searchSchema } from './search.js'
import { emailSchema } from './email.js'
// Email schema is served by the PHP template catalog to keep defaults and fields canonical.
import { tabbarSchema } from './tabbar.js'
import { floatingSchema } from './floating.js'
import { i18nSchema } from './i18n.js'
@@ -21,7 +21,7 @@ export const globalSchemas = {
common: commonSchema,
layout: layoutSchema,
search: searchSchema,
email: emailSchema,
email: null,
tabbar: tabbarSchema,
floating: floatingSchema,
i18n: i18nSchema,