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
+69 -89
View File
@@ -1,48 +1,48 @@
<template>
<div class="bg-bg-secondary-30 border border-border-secondary rounded-[2rem] overflow-hidden">
<div class="bg-zinc-900/30 border border-zinc-800 rounded-[2rem] overflow-hidden">
<!-- 头部 -->
<div class="flex items-center justify-between p-6 border-b border-border-secondary bg-bg-secondary-20">
<div class="flex items-center justify-between p-6 border-b border-zinc-800 bg-zinc-900/20">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-2xl bg-primary-hover-10 flex items-center justify-center border border-primary-20"
class="w-10 h-10 rounded-2xl bg-blue-600/10 flex items-center justify-center border border-blue-500/20"
>
<Mail :size="20" class="text-primary" />
<Mail :size="20" class="text-blue-500" />
</div>
<div>
<h3 class="text-sm font-black text-text-primary uppercase tracking-widest">{{ locale.title }}</h3>
<p class="text-[10px] text-text-tertiary mt-0.5">{{ locale.desc }}</p>
<h3 class="text-sm font-black text-zinc-100 uppercase tracking-widest">邮件模板管理</h3>
<p class="text-[10px] text-zinc-500 mt-0.5">自定义系统邮件的主题与内容,支持变量替换</p>
</div>
</div>
<div class="flex items-center gap-3">
<button
v-if="selected"
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-[11px] font-bold rounded-xl transition-all"
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-[11px] font-bold rounded-xl transition-all"
:disabled="saving"
@click="doPreview"
>
<Eye :size="14" /> {{ previewHtml ? locale.refreshPreview : locale.livePreview }}
<Eye :size="14" /> {{ previewHtml ? '刷新预览' : '实时预览' }}
</button>
<button
v-if="selected"
class="flex items-center gap-2 px-6 py-2 bg-primary-hover hover:bg-primary text-text-primary text-[11px] 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-[11px] font-bold rounded-xl shadow-lg shadow-blue-900/20 transition-all active:scale-95"
:disabled="saving"
@click="save"
>
<Save :size="14" /> {{ saving ? locale.saving : locale.updateTemplate }}
<Save :size="14" /> {{ saving ? '保存中...' : '更新模板' }}
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 min-h-[650px]">
<!-- 左侧:模板列表 -->
<div class="lg:col-span-3 border-r border-border-secondary bg-bg-secondary-10 flex flex-col">
<div class="p-4 border-b border-border-secondary-50">
<div class="lg:col-span-3 border-r border-zinc-800 bg-zinc-900/10 flex flex-col">
<div class="p-4 border-b border-zinc-800/50">
<div class="relative">
<Search :size="14" class="absolute left-3 top-1/2 -translate-y-1/2 text-text-disabled" />
<Search :size="14" class="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600" />
<input
type="text"
:placeholder="locale.searchPlaceholder"
class="w-full bg-bg-primary border border-border-secondary rounded-lg pl-9 pr-3 py-2 text-[10px] text-text-tertiary focus:outline-none focus:border-primary-30"
placeholder="搜索模板..."
class="w-full bg-zinc-950 border border-zinc-800 rounded-lg pl-9 pr-3 py-2 text-[10px] text-zinc-400 focus:outline-none focus:border-blue-500/30"
>
</div>
</div>
@@ -53,8 +53,8 @@
class="w-full group flex flex-col gap-1 p-3 rounded-2xl transition-all text-left border"
:class="[
selectedKey === t.key
? 'bg-primary-hover-10 border-primary-20'
: 'bg-transparent border-transparent hover:bg-bg-tertiary-40'
? 'bg-blue-600/10 border-blue-500/20'
: 'bg-transparent border-transparent hover:bg-zinc-800/40'
]"
@click="select(t)"
>
@@ -63,8 +63,8 @@
class="text-[11px] font-black tracking-tight transition-colors"
:class="
selectedKey === t.key
? 'text-primary'
: 'text-text-secondary group-hover:text-text-primary'
? 'text-blue-400'
: 'text-zinc-300 group-hover:text-zinc-100'
"
>
{{ t.name }}
@@ -72,69 +72,67 @@
<div class="flex gap-1">
<span
v-if="t.isBuiltin && !t.isOverridden"
class="px-1.5 py-0.5 rounded-md bg-bg-tertiary text-[8px] font-black text-text-tertiary uppercase tracking-tighter"
class="px-1.5 py-0.5 rounded-md bg-zinc-800 text-[8px] font-black text-zinc-500 uppercase tracking-tighter"
>
{{ locale.builtin }}
内置
</span>
<span
v-if="t.isOverridden"
class="px-1.5 py-0.5 rounded-md bg-success-10 text-[8px] font-black text-success uppercase tracking-tighter"
class="px-1.5 py-0.5 rounded-md bg-emerald-500/10 text-[8px] font-black text-emerald-500 uppercase tracking-tighter"
>
{{ locale.custom }}
自定义
</span>
</div>
</div>
<span class="text-[9px] font-bold text-text-disabled font-mono">{{ t.key }}</span>
<span class="text-[9px] font-bold text-zinc-600 font-mono">{{ t.key }}</span>
</button>
</div>
</div>
<!-- 右侧:编辑器区域 -->
<div class="lg:col-span-9 flex flex-col bg-bg-primary-20 overflow-hidden">
<div class="lg:col-span-9 flex flex-col bg-zinc-950/20 overflow-hidden">
<div v-if="selected" class="flex-1 overflow-y-auto custom-scrollbar p-8">
<div class="max-w-4xl mx-auto space-y-8">
<!-- 基础信息与主题 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<div class="flex items-center justify-between px-1">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest"
>{{ locale.templateName }}</label
>
</div>
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>模板名称</label
>
<input
v-model="form.name"
type="text"
class="h-11 w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-3 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-3 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between px-1">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest"
>{{ locale.emailSubject }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest"
>邮件主题</label
>
<span class="text-[9px] text-text-tertiary font-bold uppercase">{{ locale.supportsVariables }}</span>
<span class="text-[9px] text-zinc-500 font-bold uppercase">支持变量</span>
</div>
<input
v-model="form.subject"
type="text"
:placeholder="locale.subjectPlaceholder"
class="h-11 w-full bg-bg-primary border border-border-secondary rounded-xl px-4 py-3 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-3 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/30"
>
</div>
</div>
<!-- 变量提示 -->
<div class="bg-primary-hover-5 border border-primary-10 rounded-2xl p-4 flex gap-4">
<div class="bg-blue-600/5 border border-blue-500/10 rounded-2xl p-4 flex gap-4">
<div
class="w-8 h-8 rounded-xl bg-primary-10 flex items-center justify-center shrink-0"
class="w-8 h-8 rounded-xl bg-blue-500/10 flex items-center justify-center shrink-0"
>
<Info :size="16" class="text-primary" />
<Info :size="16" class="text-blue-500" />
</div>
<div class="space-y-1">
<h4 class="text-[10px] font-black text-primary uppercase tracking-widest">
{{ locale.availableVariables }}
<h4 class="text-[10px] font-black text-blue-400 uppercase tracking-widest">
可用变量
</h4>
<p class="text-[11px] text-text-tertiary leading-relaxed font-mono">
<p class="text-[11px] text-zinc-500 leading-relaxed font-mono">
<template v-if="selected.key === 'verification.code'">
<span v-pre>{{ name }}, {{ email }}, {{ code }}, {{ expiresInMinutes }}</span>
</template>
@@ -149,32 +147,32 @@
<div class="space-y-3">
<div class="flex items-center justify-between px-1">
<div class="flex items-center gap-2">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest"
>{{ locale.htmlContent }}</label
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest"
>HTML 内容</label
>
<span
class="px-2 py-0.5 rounded-md bg-bg-secondary text-[8px] font-black text-text-tertiary uppercase"
class="px-2 py-0.5 rounded-md bg-zinc-900 text-[8px] font-black text-zinc-500 uppercase"
>Handlebars</span
>
</div>
<button
v-if="selected.isOverridden"
class="flex items-center gap-1.5 text-[10px] font-bold text-error hover:text-error transition-colors"
class="flex items-center gap-1.5 text-[10px] font-bold text-rose-500 hover:text-rose-400 transition-colors"
@click="restore"
>
<RotateCcw :size="12" /> {{ locale.restoreDefault }}
<RotateCcw :size="12" /> 恢复默认配置
</button>
</div>
<div class="relative group">
<div
class="absolute right-4 top-4 p-2 rounded-lg bg-bg-secondary-50 border border-border-secondary opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
class="absolute right-4 top-4 p-2 rounded-lg bg-zinc-900/50 border border-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Code :size="14" class="text-text-tertiary" />
<Code :size="14" class="text-zinc-500" />
</div>
<textarea
v-model="form.html"
rows="16"
class="w-full bg-bg-primary border border-border-secondary rounded-2xl p-6 text-[11px] text-text-secondary font-mono leading-relaxed focus:outline-none focus:border-primary-30 custom-scrollbar"
class="w-full bg-zinc-950 border border-zinc-800 rounded-2xl p-6 text-[11px] text-zinc-300 font-mono leading-relaxed focus:outline-none focus:border-blue-500/30 custom-scrollbar"
placeholder="<html>..."
/>
</div>
@@ -186,22 +184,22 @@
enter-from-class="transform translate-y-4 opacity-0"
enter-to-class="transform translate-y-0 opacity-100"
>
<div v-if="previewHtml" class="space-y-4 pt-4 border-t border-border-secondary">
<div v-if="previewHtml" class="space-y-4 pt-4 border-t border-zinc-800">
<div class="flex items-center justify-between px-1">
<label
class="text-[10px] font-black text-text-disabled uppercase tracking-widest flex items-center gap-2"
class="text-[10px] font-black text-zinc-600 uppercase tracking-widest flex items-center gap-2"
>
<Eye :size="14" /> {{ locale.livePreviewLabel }}
<span class="text-text-tertiary font-normal normal-case">{{ previewSubject }}</span>
<Eye :size="14" /> 实时预览:
<span class="text-zinc-400 font-normal normal-case">{{ previewSubject }}</span>
</label>
<button
class="text-[10px] font-bold text-text-tertiary hover:text-text-secondary"
class="text-[10px] font-bold text-zinc-500 hover:text-zinc-300"
@click="previewHtml = ''"
>
{{ locale.hidePreview }}
隐藏预览
</button>
</div>
<div class="rounded-2xl overflow-hidden border border-border-secondary bg-bg-secondary shadow-2xl">
<div class="rounded-2xl overflow-hidden border border-zinc-800 bg-white shadow-2xl">
<iframe :srcdoc="previewHtml" class="w-full h-[500px] border-none" />
</div>
</div>
@@ -212,14 +210,14 @@
<!-- 空状态 -->
<div
v-else
class="flex-1 flex flex-col items-center justify-center text-text-secondary space-y-4"
class="flex-1 flex flex-col items-center justify-center text-zinc-700 space-y-4"
>
<div
class="w-16 h-16 rounded-3xl bg-bg-secondary-50 flex items-center justify-center border border-border-secondary-50"
class="w-16 h-16 rounded-3xl bg-zinc-900/50 flex items-center justify-center border border-zinc-800/50"
>
<Mail :size="32" class="text-text-primary" />
<Mail :size="32" class="text-zinc-800" />
</div>
<p class="text-xs font-bold tracking-widest uppercase">{{ locale.emptySelectTemplate }}</p>
<p class="text-xs font-bold tracking-widest uppercase">请在左侧选择一个邮件模板</p>
</div>
</div>
</div>
@@ -229,7 +227,6 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useToast } from '~/composables/useToast'
import { useLocale } from '~/utils/locale'
import { Mail, Info, Save, RotateCcw, Eye, Code, Search, ChevronRight } from '@lucide/vue'
type TemplateItem = {
@@ -242,18 +239,6 @@ type TemplateItem = {
}
const { success, error } = useToast()
const { admin } = useLocale()
const locale = computed(() => {
const base = admin.value?.emailTemplateManager || {}
return {
...base,
messages: { ...(base.messages || {}) },
errors: { ...(base.errors || {}) }
}
})
const getLocaleText = (section: string, key: string, fallback = '') => {
return locale.value?.[section]?.[key] || fallback
}
const templates = ref<TemplateItem[]>([])
const selectedKey = ref<string>('')
const form = ref<{ key: string; name: string; subject: string; html: string }>({
@@ -293,10 +278,10 @@ async function save() {
try {
saving.value = true
await $fetch('/api/admin/email-templates', { method: 'POST', body: form.value })
success(getLocaleText('messages', 'saved', '模板已保存'))
success('模板已保存')
await loadList()
} catch (e: any) {
error(e?.data?.message || getLocaleText('errors', 'saveFailed', '保存失败'))
error(e?.data?.message || '保存失败')
} finally {
saving.value = false
}
@@ -308,10 +293,10 @@ async function restore() {
await $fetch(`/api/admin/email-templates?key=${encodeURIComponent(form.value.key)}`, {
method: 'DELETE'
})
success(getLocaleText('messages', 'restored', '已恢复默认模板'))
success('已恢复默认模板')
await loadList()
} catch (e: any) {
error(e?.data?.message || getLocaleText('errors', 'restoreFailed', '恢复失败'))
error(e?.data?.message || '恢复失败')
} finally {
saving.value = false
}
@@ -321,15 +306,10 @@ async function doPreview() {
try {
const defaultData =
form.value.key === 'verification.code'
? {
name: locale.value?.previewData?.name || 'Example User',
email: 'example@school.edu',
code: '123456',
expiresInMinutes: 5
}
? { name: '张三', email: 'example@school.edu', code: '123456', expiresInMinutes: 5 }
: {
title: locale.value?.previewData?.title || 'System Notification',
message: locale.value?.previewData?.message || 'This is preview content.\nLine breaks and links are supported.',
title: '系统通知',
message: '这是一条预览内容\n支持换行与链接',
actionUrl: 'https://example.com'
}
const res = await $fetch('/api/admin/email-templates/preview', {
@@ -339,7 +319,7 @@ async function doPreview() {
previewHtml.value = res.html
previewSubject.value = res.subject
} catch (e: any) {
error(e?.data?.message || getLocaleText('errors', 'previewFailed', '预览失败'))
error(e?.data?.message || '预览失败')
}
}
@@ -356,10 +336,10 @@ onMounted(() => {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: var(--panel-bg-alt);
background: var(--bg-tertiary, #27272a);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--panel-bg-hover);
background: #3f3f46;
}
</style>