251 lines
12 KiB
TypeScript
251 lines
12 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { CheckCircle, Save, Mail, Plus, Trash2, TestTube, Loader2, Settings, Server } from 'lucide-react'
|
||
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
|
||
import { useConfig } from '../hooks/useConfig'
|
||
import type { MailServiceConfig } from '../types'
|
||
|
||
export default function EmailConfig() {
|
||
const { config, updateEmailConfig } = useConfig()
|
||
|
||
const [saved, setSaved] = useState(false)
|
||
const [services, setServices] = useState<MailServiceConfig[]>(config.email?.services || [])
|
||
const [testingIndex, setTestingIndex] = useState<number | null>(null)
|
||
const [testResults, setTestResults] = useState<Record<number, { success: boolean; message: string }>>({})
|
||
|
||
// 同步配置变化
|
||
useEffect(() => {
|
||
if (config.email?.services) {
|
||
setServices(config.email.services)
|
||
}
|
||
}, [config.email?.services])
|
||
|
||
const handleSave = async () => {
|
||
// 保存到前端 context
|
||
updateEmailConfig({ services })
|
||
|
||
// 保存到后端
|
||
try {
|
||
const res = await fetch('/api/mail/services', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ services }),
|
||
})
|
||
|
||
if (res.ok) {
|
||
setSaved(true)
|
||
setTimeout(() => setSaved(false), 2000)
|
||
}
|
||
} catch (error) {
|
||
console.error('保存失败:', error)
|
||
}
|
||
}
|
||
|
||
const handleAddService = () => {
|
||
setServices([
|
||
...services,
|
||
{
|
||
name: `邮箱服务 ${services.length + 1}`,
|
||
apiBase: '',
|
||
apiToken: '',
|
||
domain: '',
|
||
},
|
||
])
|
||
}
|
||
|
||
const handleRemoveService = (index: number) => {
|
||
if (services.length <= 1) {
|
||
return // 至少保留一个服务
|
||
}
|
||
const newServices = services.filter((_, i) => i !== index)
|
||
setServices(newServices)
|
||
}
|
||
|
||
const handleUpdateService = (index: number, updates: Partial<MailServiceConfig>) => {
|
||
const newServices = [...services]
|
||
newServices[index] = { ...newServices[index], ...updates }
|
||
setServices(newServices)
|
||
}
|
||
|
||
const handleTestService = async (index: number) => {
|
||
const service = services[index]
|
||
setTestingIndex(index)
|
||
setTestResults(prev => ({ ...prev, [index]: { success: false, message: '测试中...' } }))
|
||
|
||
try {
|
||
const res = await fetch('/api/mail/services/test', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: service.name,
|
||
api_base: service.apiBase,
|
||
api_token: service.apiToken,
|
||
domain: service.domain,
|
||
email_path: service.emailPath,
|
||
}),
|
||
})
|
||
|
||
const data = await res.json()
|
||
|
||
if (res.ok && data.code === 0) {
|
||
setTestResults(prev => ({ ...prev, [index]: { success: true, message: '连接成功' } }))
|
||
} else {
|
||
setTestResults(prev => ({ ...prev, [index]: { success: false, message: data.message || '连接失败' } }))
|
||
}
|
||
} catch (error) {
|
||
setTestResults(prev => ({ ...prev, [index]: { success: false, message: '网络错误' } }))
|
||
} finally {
|
||
setTestingIndex(null)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
|
||
<Mail className="h-7 w-7 text-purple-500" />
|
||
邮箱服务配置
|
||
</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||
配置多个邮箱服务用于自动注册和验证码接收
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleAddService}
|
||
icon={<Plus className="h-4 w-4" />}
|
||
>
|
||
添加服务
|
||
</Button>
|
||
<Button
|
||
onClick={handleSave}
|
||
icon={saved ? <CheckCircle className="h-4 w-4" /> : <Save className="h-4 w-4" />}
|
||
>
|
||
{saved ? '已保存' : '保存配置'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Service Cards */}
|
||
<div className="space-y-4">
|
||
{services.map((service, index) => (
|
||
<Card key={index}>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Server className="h-5 w-5 text-purple-500" />
|
||
<span>{service.name || `服务 ${index + 1}`}</span>
|
||
<span className="text-sm font-normal text-slate-500">
|
||
(@{service.domain || '未设置域名'})
|
||
</span>
|
||
</CardTitle>
|
||
<div className="flex items-center gap-2">
|
||
{testResults[index] && (
|
||
<span className={`text-sm ${testResults[index].success ? 'text-green-500' : 'text-red-500'}`}>
|
||
{testResults[index].message}
|
||
</span>
|
||
)}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleTestService(index)}
|
||
disabled={testingIndex === index || !service.apiBase}
|
||
icon={testingIndex === index ? <Loader2 className="h-4 w-4 animate-spin" /> : <TestTube className="h-4 w-4" />}
|
||
>
|
||
测试
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleRemoveService(index)}
|
||
disabled={services.length <= 1}
|
||
icon={<Trash2 className="h-4 w-4" />}
|
||
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||
>
|
||
删除
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<Input
|
||
label="服务名称"
|
||
placeholder="如:主邮箱服务"
|
||
value={service.name}
|
||
onChange={(e) => handleUpdateService(index, { name: e.target.value })}
|
||
hint="用于识别不同的邮箱服务"
|
||
/>
|
||
<Input
|
||
label="邮箱域名"
|
||
placeholder="如:example.com"
|
||
value={service.domain}
|
||
onChange={(e) => handleUpdateService(index, { domain: e.target.value })}
|
||
hint="生成邮箱地址的域名后缀"
|
||
/>
|
||
</div>
|
||
<Input
|
||
label="API 地址"
|
||
placeholder="https://mail.example.com"
|
||
value={service.apiBase}
|
||
onChange={(e) => handleUpdateService(index, { apiBase: e.target.value })}
|
||
hint="邮箱服务 API 地址"
|
||
/>
|
||
<Input
|
||
label="API Token"
|
||
type="password"
|
||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||
value={service.apiToken}
|
||
onChange={(e) => handleUpdateService(index, { apiToken: e.target.value })}
|
||
hint="邮箱服务的 API 认证令牌"
|
||
/>
|
||
|
||
{/* Advanced Settings (Collapsed by default) */}
|
||
<details className="group">
|
||
<summary className="cursor-pointer text-sm text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 flex items-center gap-1">
|
||
<Settings className="h-4 w-4" />
|
||
高级设置
|
||
</summary>
|
||
<div className="mt-4 space-y-4 pl-5 border-l-2 border-slate-200 dark:border-slate-700">
|
||
<Input
|
||
label="邮件列表 API 路径"
|
||
placeholder="/api/public/emailList (默认)"
|
||
value={service.emailPath || ''}
|
||
onChange={(e) => handleUpdateService(index, { emailPath: e.target.value })}
|
||
hint="获取邮件列表的 API 路径"
|
||
/>
|
||
<Input
|
||
label="创建用户 API 路径"
|
||
placeholder="/api/public/addUser (默认)"
|
||
value={service.addUserApi || ''}
|
||
onChange={(e) => handleUpdateService(index, { addUserApi: e.target.value })}
|
||
hint="创建邮箱用户的 API 路径"
|
||
/>
|
||
</div>
|
||
</details>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
|
||
{/* Help Info */}
|
||
<Card>
|
||
<CardContent>
|
||
<div className="text-sm text-slate-500 dark:text-slate-400">
|
||
<p className="font-medium mb-2">配置说明:</p>
|
||
<ul className="list-disc list-inside space-y-1">
|
||
<li>可以添加多个邮箱服务,系统会轮询使用各个服务</li>
|
||
<li>每个服务需要配置独立的 API 地址、Token 和域名</li>
|
||
<li>邮箱域名决定生成的邮箱地址后缀(如 xxx@esyteam.edu.kg)</li>
|
||
<li>验证码会自动从配置的邮箱服务获取</li>
|
||
<li>高级设置通常不需要修改,使用默认值即可</li>
|
||
</ul>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|