feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.

This commit is contained in:
2026-01-30 07:40:35 +08:00
commit f4448bbef2
106 changed files with 19282 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
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'
const API_BASE = 'http://localhost:8088'
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_BASE}/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_BASE}/api/mail/services/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
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>
)
}