feat: 迁移自定义功能至 1.5.9.9 版本

包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
2026-09-12 19:26:42 +08:00
parent 34f0679117
commit 1106aeea6c
161 changed files with 18963 additions and 27900 deletions
+65 -135
View File
@@ -3,26 +3,26 @@
<!-- 头部区域 -->
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div>
<h2 class="text-2xl font-black text-text-primary tracking-tight">{{ locale.title }}</h2>
<p class="text-xs text-text-tertiary mt-1">
{{ locale.desc }}
<h2 class="text-2xl font-black text-zinc-100 tracking-tight">邮件服务配置</h2>
<p class="text-xs text-zinc-500 mt-1">
配置 SMTP 服务以发送系统验证码、投稿通知及其他重要提醒
</p>
</div>
<div class="flex items-center gap-3">
<button
class="flex items-center gap-2 px-6 py-2 bg-bg-secondary border border-border-secondary hover:border-border-tertiary text-text-secondary text-xs font-bold rounded-xl transition-all active:scale-95 disabled:opacity-50"
class="flex items-center gap-2 px-6 py-2 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-zinc-300 text-xs font-bold rounded-xl transition-all active:scale-95 disabled:opacity-50"
:disabled="reloading"
@click="reloadSmtpConfig"
>
<RotateCw :size="14" :class="reloading ? 'animate-spin' : ''" />
{{ reloading ? locale.reloading : locale.reload }}
{{ reloading ? '重载中...' : '重载SMTP' }}
</button>
<button
class="flex items-center gap-2 px-6 py-2 bg-primary-hover hover:bg-primary text-text-primary text-xs font-bold rounded-xl shadow-lg shadow-[var(--primary-glow)] transition-all active:scale-95"
class="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-blue-900/20 transition-all active:scale-95"
:disabled="saving"
@click="saveConfig"
>
<Save :size="14" /> {{ saving ? locale.saving : locale.save }}
<Save :size="14" /> {{ saving ? '保存中...' : '保存配置' }}
</button>
</div>
</div>
@@ -31,20 +31,20 @@
<!-- 左侧栏:SMTP 设置与测试 -->
<div class="xl:col-span-4 space-y-8">
<!-- SMTP 核心设置 -->
<section class="bg-bg-secondary-30 border border-border-secondary rounded-[2rem] p-6 space-y-6">
<section class="bg-zinc-900/30 border border-zinc-800 rounded-[2rem] p-6 space-y-6">
<div class="flex items-center justify-between">
<h3
class="text-sm font-black text-text-primary uppercase tracking-widest flex items-center gap-2"
class="text-sm font-black text-zinc-100 uppercase tracking-widest flex items-center gap-2"
>
<Server :size="16" class="text-primary" /> {{ locale.serviceConfig }}
<Server :size="16" class="text-blue-500" /> 服务配置
</h3>
<button
class="relative w-10 h-5 rounded-full transition-colors"
:class="config.smtpEnabled ? 'bg-primary-hover' : 'bg-bg-tertiary'"
:class="config.smtpEnabled ? 'bg-blue-600' : 'bg-zinc-800'"
@click="config.smtpEnabled = !config.smtpEnabled"
>
<div
class="absolute top-1 w-3 h-3 bg-bg-secondary rounded-full transition-all"
class="absolute top-1 w-3 h-3 bg-white rounded-full transition-all"
:class="config.smtpEnabled ? 'left-6' : 'left-1'"
/>
</button>
@@ -52,118 +52,118 @@
<div class="space-y-4">
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.smtpHost }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>SMTP 服务器</label
>
<input
v-model="config.smtpHost"
type="text"
placeholder="smtp.example.com"
class="w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.port }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>端口</label
>
<input
v-model.number="config.smtpPort"
type="number"
placeholder="587"
class="w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.secure }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>加密方式</label
>
<CustomSelect
:model-value="config.smtpSecure ? secureOptionLabels.ssl : secureOptionLabels.none"
:options="[secureOptionLabels.ssl, secureOptionLabels.none]"
:model-value="config.smtpSecure ? 'SSL/TLS' : '无'"
:options="['SSL/TLS', '无']"
class="w-full"
@update:model-value="(val) => (config.smtpSecure = val === secureOptionLabels.ssl)"
@update:model-value="(val) => (config.smtpSecure = val === 'SSL/TLS')"
/>
</div>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.username }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>发件人账号</label
>
<input
v-model="config.smtpUsername"
type="text"
placeholder="your-email@example.com"
class="w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.fromEmail }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>发件人邮箱 (From Email)</label
>
<input
v-model="config.smtpFromEmail"
type="text"
:placeholder="config.smtpUsername || 'your-email@example.com'"
class="w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
<p class="text-[9px] text-text-tertiary px-1 italic">
{{ locale.fromEmailHint }}
<p class="text-[9px] text-zinc-500 px-1 italic">
通常与发件人账号一致,部分服务商强制要求一致。
</p>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.password }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>授权码 / 密码</label
>
<input
v-model="config.smtpPassword"
type="password"
placeholder="••••••••••••"
class="w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.fromName }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>发件人姓名</label
>
<input
v-model="config.smtpFromName"
type="text"
:placeholder="locale.defaultFromName"
class="w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
placeholder="校园广播站"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
</div>
</section>
<!-- SMTP 测试模块 -->
<section class="bg-bg-secondary-30 border border-border-secondary rounded-[2rem] p-6 space-y-6">
<section class="bg-zinc-900/30 border border-zinc-800 rounded-[2rem] p-6 space-y-6">
<h3
class="text-sm font-black text-text-primary uppercase tracking-widest flex items-center gap-2"
class="text-sm font-black text-zinc-100 uppercase tracking-widest flex items-center gap-2"
>
<Send :size="16" class="text-success" /> {{ locale.serviceTest }}
<Send :size="16" class="text-emerald-500" /> 服务测试
</h3>
<div class="space-y-4">
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.testEmail }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>测试接收邮箱</label
>
<div class="flex gap-2">
<input
v-model="testEmail"
type="email"
:placeholder="locale.testEmailPlaceholder"
class="flex-1 bg-bg-primary border border-border-secondary rounded-xl px-4 py-2.5 text-xs text-text-primary focus:outline-none focus:border-primary-30"
placeholder="输入测试邮箱地址"
class="flex-1 bg-zinc-950 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
<button
class="flex items-center gap-2 px-4 py-2 bg-bg-secondary border border-border-secondary hover:border-border-tertiary text-text-tertiary text-xs font-bold rounded-xl transition-all disabled:opacity-50"
class="flex items-center gap-2 px-4 py-2 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-zinc-400 text-xs font-bold rounded-xl transition-all disabled:opacity-50"
:disabled="testing || !testEmail || !config.smtpEnabled"
@click="sendTestEmail"
>
<Check v-if="testResult?.success" :size="14" class="text-success" />
<Check v-if="testResult?.success" :size="14" class="text-emerald-500" />
<Send v-else :size="14" />
{{ testing ? locale.sending : testResult?.success ? locale.sent : locale.send }}
{{ testing ? '发送中' : testResult?.success ? '已发送' : '发送' }}
</button>
</div>
</div>
@@ -173,8 +173,8 @@
class="flex items-center gap-2 p-3 rounded-xl text-xs"
:class="
testResult.success
? 'bg-success-10 text-success border border-success-20'
: 'bg-error-10 text-error border border-error-20'
? 'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20'
: 'bg-red-500/10 text-red-500 border border-red-500/20'
"
>
<CheckCircle v-if="testResult.success" :size="14" />
@@ -200,26 +200,13 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useToast } from '~/composables/useToast'
import { useLocale } from '~/utils/locale'
import EmailTemplateManager from '~/components/Admin/EmailTemplateManager.vue'
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
import { Server, Save, Check, Send, CheckCircle, XCircle, RotateCw } from '@lucide/vue'
const { showToast: showNotification } = useToast()
const { admin } = useLocale()
const locale = computed(() => admin.value?.smtpManager || {})
const getLocaleMessage = (key) => locale.value?.[key] || `SMTP 配置:${key}`
const getOptionText = (value, fallback) => {
if (typeof value === 'function') return value() || fallback
if (typeof value === 'string') return value || fallback
return fallback
}
const secureOptionLabels = computed(() => ({
ssl: getOptionText(locale.value?.secureOptions?.ssl, 'SSL/TLS'),
none: getOptionText(locale.value?.secureOptions?.none, 'None')
}))
// 响应式数据
const config = ref({
@@ -230,7 +217,7 @@ const config = ref({
smtpUsername: '',
smtpPassword: '',
smtpFromEmail: '',
smtpFromName: getLocaleMessage('defaultFromName')
smtpFromName: '校园广播站'
})
const testEmail = ref('')
@@ -240,30 +227,6 @@ const reloading = ref(false)
const testResult = ref(null)
const originalConfig = ref({})
// 将服务端返回的固定文案映射为当前语言,避免后端中文穿透到前端界面。
const getLocalizedServerMessage = (message) => {
if (!message) {
return ''
}
const serverMessages = locale.value?.serverMessages
if (!serverMessages) return message
const rawMessages = serverMessages.raw
if (rawMessages) {
const key = Object.keys(rawMessages).find((item) => rawMessages[item] === message)
return key ? serverMessages[key] : message
}
const matchedEntry = Object.values(serverMessages).find((entry) => entry?.raw === message)
return matchedEntry?.text || message
}
const localizeSmtpResult = (response) => ({
...response,
message: getLocalizedServerMessage(response?.message) || response?.message
})
// 加载配置
const loadConfig = async () => {
try {
@@ -277,14 +240,14 @@ const loadConfig = async () => {
smtpUsername: response.smtpUsername || '',
smtpPassword: response.smtpPassword || '',
smtpFromEmail: response.smtpFromEmail || '',
smtpFromName: response.smtpFromName || getLocaleMessage('defaultFromName')
smtpFromName: response.smtpFromName || '校园广播站'
}
// 保存原始配置用于重置
originalConfig.value = { ...config.value }
} catch (error) {
console.error('加载SMTP配置失败:', error)
showNotification(getLocaleMessage('loadFailed'), 'error')
showNotification('加载配置失败', 'error')
}
}
@@ -293,7 +256,7 @@ const saveConfig = async () => {
if (config.value.smtpEnabled) {
// 验证必填字段
if (!config.value.smtpHost || !config.value.smtpUsername || !config.value.smtpPassword) {
showNotification(getLocaleMessage('incompleteConfig'), 'error')
showNotification('请填写完整的SMTP配置信息', 'error')
return
}
}
@@ -306,21 +269,17 @@ const saveConfig = async () => {
})
originalConfig.value = { ...config.value }
showNotification(getLocaleMessage('saveSuccess'), 'success')
showNotification('SMTP配置保存成功', 'success')
} catch (error) {
console.error('保存SMTP配置失败:', error)
showNotification(getLocalizedServerMessage(error.data?.message) || getLocaleMessage('saveFailed'), 'error')
showNotification(error.data?.message || '保存配置失败', 'error')
} finally {
saving.value = false
}
}
// 重置配置
const resetConfig = () => {
config.value = { ...originalConfig.value }
testResult.value = null
showNotification(getLocaleMessage('resetSuccess'), 'info')
}
const reloadSmtpConfig = async () => {
reloading.value = true
@@ -329,53 +288,24 @@ const reloadSmtpConfig = async () => {
method: 'POST'
})
if (!response.success) {
throw new Error(getLocalizedServerMessage(response.message) || getLocaleMessage('reloadFailed'))
throw new Error(response.message || 'SMTP重载失败')
}
showNotification(getLocalizedServerMessage(response.message) || getLocaleMessage('reloadSuccess'), 'success')
showNotification(response.message || 'SMTP配置已重载', 'success')
} catch (error) {
console.error('重载SMTP配置失败:', error)
showNotification(
getLocalizedServerMessage(error.data?.message || error.message) || getLocaleMessage('reloadFailed'),
'error'
)
showNotification(error.data?.message || error.message || '重载SMTP配置失败', 'error')
} finally {
reloading.value = false
}
}
// 测试连接
const testConnection = async () => {
if (!config.value.smtpEnabled || !config.value.smtpHost) {
showNotification(getLocaleMessage('serverRequired'), 'error')
return
}
testing.value = true
testResult.value = null
try {
const response = await $fetch('/api/admin/smtp/test-connection', {
method: 'POST',
body: config.value
})
testResult.value = localizeSmtpResult(response)
} catch (error) {
console.error('测试连接失败:', error)
testResult.value = {
success: false,
message:
getLocalizedServerMessage(error.data?.message) || getLocaleMessage('testConnectionFailed')
}
} finally {
testing.value = false
}
}
// 发送测试邮件
const sendTestEmail = async () => {
if (!testEmail.value) {
showNotification(getLocaleMessage('testEmailRequired'), 'error')
showNotification('请输入测试邮箱地址', 'error')
return
}
@@ -391,9 +321,9 @@ const sendTestEmail = async () => {
}
})
testResult.value = localizeSmtpResult(response)
testResult.value = response
if (response.success) {
showNotification(getLocaleMessage('testEmailSuccess'), 'success')
showNotification('测试邮件发送成功', 'success')
setTimeout(() => {
testResult.value = null
}, 5000)
@@ -402,7 +332,7 @@ const sendTestEmail = async () => {
console.error('发送测试邮件失败:', error)
testResult.value = {
success: false,
message: getLocalizedServerMessage(error.data?.message) || getLocaleMessage('testEmailFailed')
message: error.data?.message || '发送测试邮件失败'
}
} finally {
testing.value = false