feat: Implement core backend API server with configuration, S2A proxy, and mail service APIs, and introduce EmailConfig frontend page.
This commit is contained in:
@@ -5,21 +5,36 @@ import { useConfig } from '../hooks/useConfig'
|
||||
import type { MailServiceConfig } from '../types'
|
||||
|
||||
export default function EmailConfig() {
|
||||
const { config, updateEmailConfig } = useConfig()
|
||||
const { updateEmailConfig } = useConfig()
|
||||
const toast = useToast()
|
||||
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [services, setServices] = useState<MailServiceConfig[]>(config.email?.services || [])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [services, setServices] = useState<MailServiceConfig[]>([])
|
||||
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)
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/mail/services')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.code === 0 && Array.isArray(data.data)) {
|
||||
setServices(data.data)
|
||||
updateEmailConfig({ services: data.data })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载邮箱配置失败:', e)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
}, [config.email?.services])
|
||||
loadConfig()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (services.length === 0) {
|
||||
@@ -146,106 +161,115 @@ export default function EmailConfig() {
|
||||
</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}
|
||||
{loading ? (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-slate-400" />
|
||||
<span className="ml-2 text-slate-500">加载中...</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
</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>
|
||||
</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="用于识别不同的邮箱服务"
|
||||
label="API 地址"
|
||||
placeholder="https://mail.example.com"
|
||||
value={service.apiBase}
|
||||
onChange={(e) => handleUpdateService(index, { apiBase: e.target.value })}
|
||||
hint="邮箱服务 API 地址"
|
||||
/>
|
||||
<Input
|
||||
label="邮箱域名"
|
||||
placeholder="如:example.com"
|
||||
value={service.domain}
|
||||
onChange={(e) => handleUpdateService(index, { domain: e.target.value })}
|
||||
hint="生成邮箱地址的域名后缀"
|
||||
label="API Token"
|
||||
type="password"
|
||||
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
value={service.apiToken}
|
||||
onChange={(e) => handleUpdateService(index, { apiToken: e.target.value })}
|
||||
hint="邮箱服务的 API 认证令牌"
|
||||
/>
|
||||
</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>
|
||||
{/* 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>
|
||||
|
||||
Reference in New Issue
Block a user