485 lines
15 KiB
Vue
485 lines
15 KiB
Vue
<script setup lang="ts">
|
||
import { Icon } from '@iconify/vue'
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { toast } from 'vue-sonner'
|
||
|
||
import { BasicPage } from '@/components/global-layout'
|
||
|
||
const route = useRoute()
|
||
|
||
const API_BASE = 'http://localhost:8080/api/v1'
|
||
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const testing = ref(false)
|
||
const sendingCode = ref(false)
|
||
|
||
interface Permission {
|
||
allow_email_verify: boolean
|
||
allow_password_reset: boolean
|
||
allow_custom_smtp: boolean
|
||
allow_custom_template: boolean
|
||
}
|
||
|
||
interface SMTPConfig {
|
||
id: number
|
||
host: string
|
||
port: number
|
||
user: string
|
||
from_name: string
|
||
from_email: string
|
||
use_ssl: boolean
|
||
status: string
|
||
}
|
||
|
||
interface EmailConfig {
|
||
enable_email_verify: boolean
|
||
require_email_verify: boolean
|
||
enable_password_reset: boolean
|
||
permission: Permission
|
||
smtp_config: SMTPConfig | null
|
||
}
|
||
|
||
interface EmailTemplate {
|
||
id: number
|
||
type: string
|
||
name: string
|
||
subject: string
|
||
content: string
|
||
is_default: boolean
|
||
status: string
|
||
}
|
||
|
||
const config = ref<EmailConfig | null>(null)
|
||
const templates = ref<EmailTemplate[]>([])
|
||
|
||
const form = ref({
|
||
enable_email_verify: false,
|
||
require_email_verify: false,
|
||
enable_password_reset: false,
|
||
smtp_host: '',
|
||
smtp_port: 465,
|
||
smtp_user: '',
|
||
smtp_password: '',
|
||
smtp_from_name: '',
|
||
smtp_from_email: '',
|
||
smtp_use_ssl: true,
|
||
})
|
||
|
||
const testEmail = ref('')
|
||
const sendCodeEmail = ref('')
|
||
const sendCodePurpose = ref('register')
|
||
|
||
const canUseEmailVerify = computed(() => config.value?.permission?.allow_email_verify)
|
||
const canUsePasswordReset = computed(() => config.value?.permission?.allow_password_reset)
|
||
const canUseCustomSMTP = computed(() => config.value?.permission?.allow_custom_smtp)
|
||
const canUseCustomTemplate = computed(() => config.value?.permission?.allow_custom_template)
|
||
|
||
async function fetchConfig() {
|
||
loading.value = true
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch(`${API_BASE}/dev/applications/${route.params.id}/email-config`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
})
|
||
const data = await response.json()
|
||
if (data.code === 200) {
|
||
config.value = data.data
|
||
form.value.enable_email_verify = data.data.enable_email_verify || false
|
||
form.value.require_email_verify = data.data.require_email_verify || false
|
||
form.value.enable_password_reset = data.data.enable_password_reset || false
|
||
|
||
if (data.data.smtp_config) {
|
||
form.value.smtp_host = data.data.smtp_config.host || ''
|
||
form.value.smtp_port = data.data.smtp_config.port || 465
|
||
form.value.smtp_user = data.data.smtp_config.user || ''
|
||
form.value.smtp_from_name = data.data.smtp_config.from_name || ''
|
||
form.value.smtp_from_email = data.data.smtp_config.from_email || ''
|
||
form.value.smtp_use_ssl = data.data.smtp_config.use_ssl ?? true
|
||
}
|
||
}
|
||
else {
|
||
toast.error(data.message || '获取配置失败')
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('获取配置失败:', error)
|
||
toast.error('获取配置失败')
|
||
}
|
||
finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function fetchTemplates() {
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch(`${API_BASE}/dev/applications/${route.params.id}/email-templates`, {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
})
|
||
const data = await response.json()
|
||
if (data.code === 200) {
|
||
templates.value = data.data || []
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('获取模板失败:', error)
|
||
}
|
||
}
|
||
|
||
async function handleSave() {
|
||
saving.value = true
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch(`${API_BASE}/dev/applications/${route.params.id}/email-config`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
enable_email_verify: form.value.enable_email_verify,
|
||
require_email_verify: form.value.require_email_verify,
|
||
enable_password_reset: form.value.enable_password_reset,
|
||
smtp_config: {
|
||
host: form.value.smtp_host,
|
||
port: form.value.smtp_port,
|
||
user: form.value.smtp_user,
|
||
password: form.value.smtp_password,
|
||
from_name: form.value.smtp_from_name,
|
||
from_email: form.value.smtp_from_email,
|
||
use_ssl: form.value.smtp_use_ssl,
|
||
},
|
||
}),
|
||
})
|
||
const data = await response.json()
|
||
if (data.code === 200) {
|
||
toast.success('保存成功')
|
||
fetchConfig()
|
||
}
|
||
else {
|
||
toast.error(data.message || '保存失败')
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('保存失败:', error)
|
||
toast.error('保存失败')
|
||
}
|
||
finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function handleTestEmail() {
|
||
if (!testEmail.value) {
|
||
toast.error('请输入测试邮箱地址')
|
||
return
|
||
}
|
||
|
||
testing.value = true
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch(`${API_BASE}/dev/applications/${route.params.id}/email-config/test`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
email: testEmail.value,
|
||
}),
|
||
})
|
||
const data = await response.json()
|
||
if (data.code === 200) {
|
||
toast.success('测试邮件已发送,请检查收件箱')
|
||
}
|
||
else {
|
||
toast.error(data.message || '发送失败')
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('发送失败:', error)
|
||
toast.error('发送失败')
|
||
}
|
||
finally {
|
||
testing.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSendVerifyCode() {
|
||
if (!sendCodeEmail.value) {
|
||
toast.error('请输入邮箱地址')
|
||
return
|
||
}
|
||
|
||
sendingCode.value = true
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const response = await fetch(`${API_BASE}/dev/applications/${route.params.id}/send-verify-code`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
email: sendCodeEmail.value,
|
||
purpose: sendCodePurpose.value,
|
||
}),
|
||
})
|
||
const data = await response.json()
|
||
if (data.code === 200) {
|
||
toast.success('验证码已发送')
|
||
}
|
||
else {
|
||
toast.error(data.message || '发送失败')
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('发送失败:', error)
|
||
toast.error('发送失败')
|
||
}
|
||
finally {
|
||
sendingCode.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchConfig()
|
||
fetchTemplates()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<BasicPage
|
||
title="邮箱设置"
|
||
description="配置应用的邮箱验证相关设置"
|
||
:breadcrumbs="[
|
||
{ title: '应用管理', href: '/admin/applications' },
|
||
{ title: '邮箱设置' },
|
||
]"
|
||
sticky
|
||
>
|
||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-muted-foreground" />
|
||
</div>
|
||
|
||
<div v-else class="space-y-6">
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle>邮箱验证设置</UiCardTitle>
|
||
<UiCardDescription>配置应用的邮箱验证功能</UiCardDescription>
|
||
</UiCardHeader>
|
||
<UiCardContent class="space-y-4">
|
||
<div class="flex items-center justify-between">
|
||
<div class="space-y-0.5">
|
||
<UiLabel>启用邮箱验证</UiLabel>
|
||
<p class="text-sm text-muted-foreground">
|
||
允许用户使用邮箱注册和验证
|
||
</p>
|
||
</div>
|
||
<UiSwitch v-model:checked="form.enable_email_verify" />
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between">
|
||
<div class="space-y-0.5">
|
||
<UiLabel>强制邮箱验证</UiLabel>
|
||
<p class="text-sm text-muted-foreground">
|
||
注册时必须验证邮箱才能完成注册
|
||
</p>
|
||
</div>
|
||
<UiSwitch v-model:checked="form.require_email_verify" :disabled="!form.enable_email_verify" />
|
||
</div>
|
||
|
||
<div v-if="canUsePasswordReset" class="flex items-center justify-between">
|
||
<div class="space-y-0.5">
|
||
<UiLabel>启用密码重置</UiLabel>
|
||
<p class="text-sm text-muted-foreground">
|
||
允许用户通过邮箱验证码重置密码
|
||
</p>
|
||
</div>
|
||
<UiSwitch v-model:checked="form.enable_password_reset" />
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle>SMTP 配置</UiCardTitle>
|
||
<UiCardDescription>配置邮件发送服务器</UiCardDescription>
|
||
</UiCardHeader>
|
||
<UiCardContent class="space-y-4">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div class="space-y-2">
|
||
<UiLabel for="smtp_host">
|
||
SMTP 服务器
|
||
</UiLabel>
|
||
<UiInput id="smtp_host" v-model="form.smtp_host" placeholder="smtp.example.com" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="smtp_port">
|
||
端口
|
||
</UiLabel>
|
||
<UiInput id="smtp_port" v-model.number="form.smtp_port" type="number" placeholder="465" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="smtp_user">
|
||
用户名
|
||
</UiLabel>
|
||
<UiInput id="smtp_user" v-model="form.smtp_user" placeholder="your@email.com" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="smtp_password">
|
||
密码/授权码
|
||
</UiLabel>
|
||
<UiInput id="smtp_password" v-model="form.smtp_password" type="password" placeholder="••••••••" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="smtp_from_name">
|
||
发件人名称
|
||
</UiLabel>
|
||
<UiInput id="smtp_from_name" v-model="form.smtp_from_name" placeholder="应用名称" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="smtp_from_email">
|
||
发件人邮箱
|
||
</UiLabel>
|
||
<UiInput id="smtp_from_email" v-model="form.smtp_from_email" placeholder="noreply@example.com" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between">
|
||
<div class="space-y-0.5">
|
||
<UiLabel>使用 SSL</UiLabel>
|
||
<p class="text-sm text-muted-foreground">
|
||
推荐开启,端口 465 通常需要 SSL
|
||
</p>
|
||
</div>
|
||
<UiSwitch v-model:checked="form.smtp_use_ssl" />
|
||
</div>
|
||
|
||
<div class="flex items-end gap-4 pt-4 border-t">
|
||
<div class="flex-1 space-y-2">
|
||
<UiLabel for="test_email">
|
||
测试邮箱
|
||
</UiLabel>
|
||
<UiInput id="test_email" v-model="testEmail" type="email" placeholder="test@example.com" />
|
||
</div>
|
||
<UiButton :disabled="testing" @click="handleTestEmail">
|
||
<Icon v-if="testing" icon="lucide:loader-2" class="mr-2 size-4 animate-spin" />
|
||
发送测试邮件
|
||
</UiButton>
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<UiCard>
|
||
<UiCardHeader>
|
||
<UiCardTitle>发送验证码测试</UiCardTitle>
|
||
<UiCardDescription>测试验证码发送功能</UiCardDescription>
|
||
</UiCardHeader>
|
||
<UiCardContent class="space-y-4">
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div class="space-y-2">
|
||
<UiLabel for="send_code_email">
|
||
邮箱地址
|
||
</UiLabel>
|
||
<UiInput id="send_code_email" v-model="sendCodeEmail" type="email" placeholder="user@example.com" />
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<UiLabel for="send_code_purpose">
|
||
用途
|
||
</UiLabel>
|
||
<UiSelect v-model="sendCodePurpose">
|
||
<UiSelectTrigger>
|
||
<UiSelectValue placeholder="选择用途" />
|
||
</UiSelectTrigger>
|
||
<UiSelectContent>
|
||
<UiSelectItem value="register">
|
||
注册验证
|
||
</UiSelectItem>
|
||
<UiSelectItem value="reset_password">
|
||
重置密码
|
||
</UiSelectItem>
|
||
<UiSelectItem value="change_email">
|
||
更换邮箱
|
||
</UiSelectItem>
|
||
</UiSelectContent>
|
||
</UiSelect>
|
||
</div>
|
||
|
||
<div class="flex items-end">
|
||
<UiButton :disabled="sendingCode" @click="handleSendVerifyCode">
|
||
<Icon v-if="sendingCode" icon="lucide:loader-2" class="mr-2 size-4 animate-spin" />
|
||
发送验证码
|
||
</UiButton>
|
||
</div>
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<UiCard v-if="canUseCustomTemplate">
|
||
<UiCardHeader>
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<UiCardTitle>邮件模板</UiCardTitle>
|
||
<UiCardDescription>自定义邮件模板</UiCardDescription>
|
||
</div>
|
||
<UiButton size="sm">
|
||
<Icon icon="lucide:plus" class="mr-2 size-4" />
|
||
新建模板
|
||
</UiButton>
|
||
</div>
|
||
</UiCardHeader>
|
||
<UiCardContent>
|
||
<div v-if="templates.length === 0" class="text-center py-8 text-muted-foreground">
|
||
<Icon icon="lucide:mail" class="size-12 mx-auto mb-4 opacity-50" />
|
||
<p>暂无自定义模板,将使用系统默认模板</p>
|
||
</div>
|
||
<div v-else class="space-y-4">
|
||
<div
|
||
v-for="template in templates"
|
||
:key="template.id"
|
||
class="flex items-center justify-between p-4 rounded-lg border"
|
||
>
|
||
<div>
|
||
<p class="font-medium">
|
||
{{ template.name }}
|
||
</p>
|
||
<p class="text-sm text-muted-foreground">
|
||
{{ template.subject }}
|
||
</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<UiBadge v-if="template.is_default" variant="secondary">
|
||
默认
|
||
</UiBadge>
|
||
<UiButton variant="ghost" size="icon">
|
||
<Icon icon="lucide:pencil" class="size-4" />
|
||
</UiButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</UiCardContent>
|
||
</UiCard>
|
||
|
||
<div class="flex justify-end">
|
||
<UiButton :disabled="saving" @click="handleSave">
|
||
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 size-4 animate-spin" />
|
||
保存设置
|
||
</UiButton>
|
||
</div>
|
||
</div>
|
||
</BasicPage>
|
||
</template>
|