Files
BYW-VoiceHub/app/components/Admin/SmtpManager.vue
T
Henry_Du 573e168702 feat(admin-ui): 管理后台整体切换为浅色主题(ModernLight) + 新增 lightbulb 图标
1. layout(管理控制台 dashboard.vue): main bg-[#09090b] text-zinc-100 → bg-[#f8fafc] text-zinc-900; header bg-zinc-950/60 border-zinc-800 → bg-white/80 border-zinc-200; 移动菜单按钮/新手引导按钮 text-zinc-300/400 hover:bg-zinc-800 → text-zinc-600 hover:bg-zinc-100 border-zinc-200

2. Admin/Sidebar.vue: bg-[#09090b] border-zinc-800 → bg-white border-zinc-200 + shadow-sm; 标题 text-zinc-100 → 900; activeTab bg-blue-600/10 text-blue-400 → bg-blue-50 text-blue-700 border-blue-200; 未选中 text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800/40 → text-zinc-600 hover:text-zinc-900 hover:bg-zinc-100; 用户卡 avatar/首字母 bg-white/70 border-zinc-200 + 退出按钮 hover:bg-red-50 text-red-600

3. 批量 33 个 Admin 组件硬编码深色原子类 1414 处替换(bg-zinc-9xx / border-zinc-8xx / text-zinc-100,200,300 / shadow-black/ -> 对应 light 版本) + style 块原生 CSS rgba(255,255,255,x) 16 处改为 rgba(0,0,0,x) / var 浅色主题 + 硬编码 bg-[#0c0c0e] / border-white/10 / color:white / surface-primary fallback 对应浅色修正

4. 二轮批量追加: bg-zinc-950 + opacity 变体(80/60/50/40/30/20/10) 共 431 处 → bg-white 对应 opacity 版本;placeholder:text-zinc-800 → 500;text-blue/red/emerald/amber/rose/purple 400 版 → 700/600 深浅一致版;border color /500 /30 → 200;focus:ring /20 → /30;shadow color 阴影降低强度

5. hover:text-white 扫尾: 13 处 DataAnalysisPanel/DatabaseManager/NotificationSender/SemesterManager/SongManagement/UserSongsModal hover 文本从 white → zinc-900;按钮三态从 hover:bg-*600 白字 → hover:bg-*-100/200 对应深字色,和前台 indigo/浅色体系保持一致

6. BlacklistManager 前台可见提示 💡→ <Icon lightbulb amber-600 size=13>;ChangelogManager 徽章色从 emerald-300/blue-300/zinc-400 → emerald-700/blue-700/zinc-600 可读;Icon.vue 新增 lightbulb 分支 stroke 图标

7. 受影响文件: dashboard.vue + Sidebar.vue + Icon.vue + Admin/* (33个组件共 1874 处批量替换 + 20 处手工精细化修正)
2026-09-19 08:30:59 +08:00

349 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="max-w-[1400px] mx-auto space-y-10 pb-20 px-2">
<!-- 头部区域 -->
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div>
<h2 class="text-2xl font-black text-zinc-900 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-white border border-zinc-200 hover:border-zinc-200 text-zinc-600 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 ? '重载中...' : '重载SMTP' }}
</button>
<button
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 ? '保存中...' : '保存配置' }}
</button>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-12 gap-8">
<!-- 左侧栏:SMTP 设置与测试 -->
<div class="xl:col-span-4 space-y-8">
<!-- SMTP 核心设置 -->
<section class="bg-zinc-50/30 border border-zinc-200 rounded-[2rem] p-6 space-y-6">
<div class="flex items-center justify-between">
<h3
class="text-sm font-black text-zinc-900 uppercase tracking-widest flex items-center gap-2"
>
<Server :size="16" class="text-blue-500" /> 服务配置
</h3>
<button
class="relative w-10 h-5 rounded-full transition-colors"
:class="config.smtpEnabled ? 'bg-blue-600' : 'bg-zinc-100'"
@click="config.smtpEnabled = !config.smtpEnabled"
>
<div
class="absolute top-1 w-3 h-3 bg-white rounded-full transition-all"
:class="config.smtpEnabled ? 'left-6' : 'left-1'"
/>
</button>
</div>
<div class="space-y-4">
<div class="space-y-1.5">
<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-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1.5">
<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-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>加密方式</label
>
<CustomSelect
:model-value="config.smtpSecure ? 'SSL/TLS' : '无'"
:options="['SSL/TLS', '无']"
class="w-full"
@update:model-value="(val) => (config.smtpSecure = val === 'SSL/TLS')"
/>
</div>
</div>
<div class="space-y-1.5">
<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-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
</div>
<div class="space-y-1.5">
<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-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
<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-zinc-600 uppercase tracking-widest px-1"
>授权码 / 密码</label
>
<input
v-model="config.smtpPassword"
type="password"
placeholder="••••••••••••"
class="w-full bg-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-zinc-600 uppercase tracking-widest px-1"
>发件人姓名</label
>
<input
v-model="config.smtpFromName"
type="text"
placeholder="校园广播站"
class="w-full bg-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
</div>
</div>
</section>
<!-- SMTP 测试模块 -->
<section class="bg-zinc-50/30 border border-zinc-200 rounded-[2rem] p-6 space-y-6">
<h3
class="text-sm font-black text-zinc-900 uppercase tracking-widest flex items-center gap-2"
>
<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-zinc-600 uppercase tracking-widest px-1"
>测试接收邮箱</label
>
<div class="flex gap-2">
<input
v-model="testEmail"
type="email"
placeholder="输入测试邮箱地址"
class="flex-1 bg-white border border-zinc-200 rounded-xl px-4 py-2.5 text-xs text-zinc-700 focus:outline-none focus:border-blue-200"
>
<button
class="flex items-center gap-2 px-4 py-2 bg-white border border-zinc-200 hover:border-zinc-200 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-emerald-500" />
<Send v-else :size="14" />
{{ testing ? '发送中' : testResult?.success ? '已发送' : '发送' }}
</button>
</div>
</div>
<div v-if="testResult">
<div
class="flex items-center gap-2 p-3 rounded-xl text-xs"
:class="
testResult.success
? '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" />
<XCircle v-else :size="14" />
<div class="flex flex-col gap-0.5">
<p class="font-bold">{{ testResult.message }}</p>
<p v-if="testResult.detail" class="text-[10px] opacity-80 break-all font-mono">
{{ testResult.detail }}
</p>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- 右侧栏:模板编辑器 -->
<div class="xl:col-span-8">
<EmailTemplateManager />
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useToast } from '~/composables/useToast'
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 config = ref({
smtpEnabled: false,
smtpHost: '',
smtpPort: 587,
smtpSecure: false,
smtpUsername: '',
smtpPassword: '',
smtpFromEmail: '',
smtpFromName: '校园广播站'
})
const testEmail = ref('')
const testing = ref(false)
const saving = ref(false)
const reloading = ref(false)
const testResult = ref(null)
const originalConfig = ref({})
// 加载配置
const loadConfig = async () => {
try {
const response = await $fetch('/api/admin/system-settings')
config.value = {
smtpEnabled: response.smtpEnabled || false,
smtpHost: response.smtpHost || '',
smtpPort: response.smtpPort || 587,
smtpSecure: response.smtpSecure || false,
smtpUsername: response.smtpUsername || '',
smtpPassword: response.smtpPassword || '',
smtpFromEmail: response.smtpFromEmail || '',
smtpFromName: response.smtpFromName || '校园广播站'
}
// 保存原始配置用于重置
originalConfig.value = { ...config.value }
} catch (error) {
console.error('加载SMTP配置失败:', error)
showNotification('加载配置失败', 'error')
}
}
// 保存配置
const saveConfig = async () => {
if (config.value.smtpEnabled) {
// 验证必填字段
if (!config.value.smtpHost || !config.value.smtpUsername || !config.value.smtpPassword) {
showNotification('请填写完整的SMTP配置信息', 'error')
return
}
}
saving.value = true
try {
await $fetch('/api/admin/system-settings', {
method: 'POST',
body: config.value
})
originalConfig.value = { ...config.value }
showNotification('SMTP配置保存成功', 'success')
} catch (error) {
console.error('保存SMTP配置失败:', error)
showNotification(error.data?.message || '保存配置失败', 'error')
} finally {
saving.value = false
}
}
// 重置配置
const reloadSmtpConfig = async () => {
reloading.value = true
try {
const response = await $fetch('/api/admin/smtp/reload', {
method: 'POST'
})
if (!response.success) {
throw new Error(response.message || 'SMTP重载失败')
}
showNotification(response.message || 'SMTP配置已重载', 'success')
} catch (error) {
console.error('重载SMTP配置失败:', error)
showNotification(error.data?.message || error.message || '重载SMTP配置失败', 'error')
} finally {
reloading.value = false
}
}
// 测试连接
// 发送测试邮件
const sendTestEmail = async () => {
if (!testEmail.value) {
showNotification('请输入测试邮箱地址', 'error')
return
}
testing.value = true
testResult.value = null
try {
const response = await $fetch('/api/admin/smtp/test-email', {
method: 'POST',
body: {
...config.value,
testEmail: testEmail.value
}
})
testResult.value = response
if (response.success) {
showNotification('测试邮件发送成功', 'success')
setTimeout(() => {
testResult.value = null
}, 5000)
}
} catch (error) {
console.error('发送测试邮件失败:', error)
testResult.value = {
success: false,
message: error.data?.message || '发送测试邮件失败'
}
} finally {
testing.value = false
}
}
// 生命周期
onMounted(() => {
loadConfig()
})
</script>
<style scoped></style>