Files
BYW-VoiceHub/app/components/Admin/SmtpManager.vue
T

419 lines
15 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-text-primary tracking-tight">{{ locale.title }}</h2>
<p class="text-xs text-text-tertiary mt-1">
{{ locale.desc }}
</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"
:disabled="reloading"
@click="reloadSmtpConfig"
>
<RotateCw :size="14" :class="reloading ? 'animate-spin' : ''" />
{{ reloading ? locale.reloading : locale.reload }}
</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"
:disabled="saving"
@click="saveConfig"
>
<Save :size="14" /> {{ saving ? locale.saving : locale.save }}
</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-bg-secondary-30 border border-border-secondary 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"
>
<Server :size="16" class="text-primary" /> {{ locale.serviceConfig }}
</h3>
<button
class="relative w-10 h-5 rounded-full transition-colors"
:class="config.smtpEnabled ? 'bg-primary-hover' : 'bg-bg-tertiary'"
@click="config.smtpEnabled = !config.smtpEnabled"
>
<div
class="absolute top-1 w-3 h-3 bg-bg-secondary 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-text-disabled uppercase tracking-widest px-1"
>{{ locale.smtpHost }}</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"
>
</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
>
<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"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.secure }}</label
>
<CustomSelect
:model-value="config.smtpSecure ? secureOptionLabels.ssl : secureOptionLabels.none"
:options="[secureOptionLabels.ssl, secureOptionLabels.none]"
class="w-full"
@update:model-value="(val) => (config.smtpSecure = val === secureOptionLabels.ssl)"
/>
</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
>
<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"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.fromEmail }}</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"
>
<p class="text-[9px] text-text-tertiary px-1 italic">
{{ locale.fromEmailHint }}
</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
>
<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"
>
</div>
<div class="space-y-1.5">
<label class="text-[10px] font-black text-text-disabled uppercase tracking-widest px-1"
>{{ locale.fromName }}</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"
>
</div>
</div>
</section>
<!-- SMTP 测试模块 -->
<section class="bg-bg-secondary-30 border border-border-secondary rounded-[2rem] p-6 space-y-6">
<h3
class="text-sm font-black text-text-primary uppercase tracking-widest flex items-center gap-2"
>
<Send :size="16" class="text-success" /> {{ locale.serviceTest }}
</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
>
<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"
>
<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"
:disabled="testing || !testEmail || !config.smtpEnabled"
@click="sendTestEmail"
>
<Check v-if="testResult?.success" :size="14" class="text-success" />
<Send v-else :size="14" />
{{ testing ? locale.sending : testResult?.success ? locale.sent : locale.send }}
</button>
</div>
</div>
<div v-if="testResult">
<div
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'
"
>
<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 { computed, 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({
smtpEnabled: false,
smtpHost: '',
smtpPort: 587,
smtpSecure: false,
smtpUsername: '',
smtpPassword: '',
smtpFromEmail: '',
smtpFromName: getLocaleMessage('defaultFromName')
})
const testEmail = ref('')
const testing = ref(false)
const saving = ref(false)
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 {
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 || getLocaleMessage('defaultFromName')
}
// 保存原始配置用于重置
originalConfig.value = { ...config.value }
} catch (error) {
console.error('加载SMTP配置失败:', error)
showNotification(getLocaleMessage('loadFailed'), 'error')
}
}
// 保存配置
const saveConfig = async () => {
if (config.value.smtpEnabled) {
// 验证必填字段
if (!config.value.smtpHost || !config.value.smtpUsername || !config.value.smtpPassword) {
showNotification(getLocaleMessage('incompleteConfig'), 'error')
return
}
}
saving.value = true
try {
await $fetch('/api/admin/system-settings', {
method: 'POST',
body: config.value
})
originalConfig.value = { ...config.value }
showNotification(getLocaleMessage('saveSuccess'), 'success')
} catch (error) {
console.error('保存SMTP配置失败:', error)
showNotification(getLocalizedServerMessage(error.data?.message) || getLocaleMessage('saveFailed'), 'error')
} finally {
saving.value = false
}
}
// 重置配置
const resetConfig = () => {
config.value = { ...originalConfig.value }
testResult.value = null
showNotification(getLocaleMessage('resetSuccess'), 'info')
}
const reloadSmtpConfig = async () => {
reloading.value = true
try {
const response = await $fetch('/api/admin/smtp/reload', {
method: 'POST'
})
if (!response.success) {
throw new Error(getLocalizedServerMessage(response.message) || getLocaleMessage('reloadFailed'))
}
showNotification(getLocalizedServerMessage(response.message) || getLocaleMessage('reloadSuccess'), 'success')
} catch (error) {
console.error('重载SMTP配置失败:', error)
showNotification(
getLocalizedServerMessage(error.data?.message || error.message) || getLocaleMessage('reloadFailed'),
'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')
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 = localizeSmtpResult(response)
if (response.success) {
showNotification(getLocaleMessage('testEmailSuccess'), 'success')
setTimeout(() => {
testResult.value = null
}, 5000)
}
} catch (error) {
console.error('发送测试邮件失败:', error)
testResult.value = {
success: false,
message: getLocalizedServerMessage(error.data?.message) || getLocaleMessage('testEmailFailed')
}
} finally {
testing.value = false
}
}
// 生命周期
onMounted(() => {
loadConfig()
})
</script>
<style scoped></style>