feat: establish core backend API server with S2A proxy, configuration, and mail services, alongside frontend email configuration page and toast component.
This commit is contained in:
@@ -355,7 +355,61 @@ func handleMailServices(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
api.Success(w, safeServices)
|
api.Success(w, safeServices)
|
||||||
case "POST":
|
case "POST":
|
||||||
api.Error(w, http.StatusNotImplemented, "更新邮箱服务配置暂未实现")
|
var req struct {
|
||||||
|
Services []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
APIBase string `json:"apiBase"`
|
||||||
|
APIToken string `json:"apiToken"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
EmailPath string `json:"emailPath"`
|
||||||
|
AddUserAPI string `json:"addUserApi"`
|
||||||
|
} `json:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("解析邮箱服务配置失败: %v", err), "", "mail")
|
||||||
|
api.Error(w, http.StatusBadRequest, "解析请求失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 config.MailServiceConfig
|
||||||
|
var services []config.MailServiceConfig
|
||||||
|
for _, s := range req.Services {
|
||||||
|
emailPath := s.EmailPath
|
||||||
|
if emailPath == "" {
|
||||||
|
emailPath = "/api/public/emailList"
|
||||||
|
}
|
||||||
|
addUserAPI := s.AddUserAPI
|
||||||
|
if addUserAPI == "" {
|
||||||
|
addUserAPI = "/api/public/addUser"
|
||||||
|
}
|
||||||
|
services = append(services, config.MailServiceConfig{
|
||||||
|
Name: s.Name,
|
||||||
|
APIBase: s.APIBase,
|
||||||
|
APIToken: s.APIToken,
|
||||||
|
Domain: s.Domain,
|
||||||
|
EmailPath: emailPath,
|
||||||
|
AddUserAPI: addUserAPI,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新邮箱服务配置
|
||||||
|
mail.Init(services)
|
||||||
|
|
||||||
|
// 保存到全局配置
|
||||||
|
if config.Global != nil {
|
||||||
|
config.Global.MailServices = services
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Success(fmt.Sprintf("邮箱服务配置已保存: %d 个服务", len(services)), "", "mail")
|
||||||
|
for _, s := range services {
|
||||||
|
logger.Info(fmt.Sprintf(" - %s (%s) @ %s", s.Name, s.Domain, s.APIBase), "", "mail")
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Success(w, map[string]interface{}{
|
||||||
|
"message": fmt.Sprintf("已保存 %d 个邮箱服务配置", len(services)),
|
||||||
|
"count": len(services),
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
|
api.Error(w, http.StatusMethodNotAllowed, "不支持的方法")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
|
|||||||
if (toasts.length === 0) return null
|
if (toasts.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-6 right-6 z-[9999] space-y-3">
|
<div className="fixed top-6 right-6 z-[9999] space-y-3">
|
||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => (
|
||||||
<ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
|
<ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { CheckCircle, Save, Mail, Plus, Trash2, TestTube, Loader2, Settings, Server } from 'lucide-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 { Card, CardHeader, CardTitle, CardContent, Button, Input, useToast } from '../components/common'
|
||||||
import { useConfig } from '../hooks/useConfig'
|
import { useConfig } from '../hooks/useConfig'
|
||||||
import type { MailServiceConfig } from '../types'
|
import type { MailServiceConfig } from '../types'
|
||||||
|
|
||||||
export default function EmailConfig() {
|
export default function EmailConfig() {
|
||||||
const { config, updateEmailConfig } = useConfig()
|
const { config, updateEmailConfig } = useConfig()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
const [services, setServices] = useState<MailServiceConfig[]>(config.email?.services || [])
|
const [services, setServices] = useState<MailServiceConfig[]>(config.email?.services || [])
|
||||||
const [testingIndex, setTestingIndex] = useState<number | null>(null)
|
const [testingIndex, setTestingIndex] = useState<number | null>(null)
|
||||||
const [testResults, setTestResults] = useState<Record<number, { success: boolean; message: string }>>({})
|
const [testResults, setTestResults] = useState<Record<number, { success: boolean; message: string }>>({})
|
||||||
@@ -20,6 +22,12 @@ export default function EmailConfig() {
|
|||||||
}, [config.email?.services])
|
}, [config.email?.services])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
if (services.length === 0) {
|
||||||
|
toast.warning('请至少添加一个邮箱服务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaving(true)
|
||||||
// 保存到前端 context
|
// 保存到前端 context
|
||||||
updateEmailConfig({ services })
|
updateEmailConfig({ services })
|
||||||
|
|
||||||
@@ -31,13 +39,20 @@ export default function EmailConfig() {
|
|||||||
body: JSON.stringify({ services }),
|
body: JSON.stringify({ services }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (res.ok && data.code === 0) {
|
||||||
setSaved(true)
|
setSaved(true)
|
||||||
setTimeout(() => setSaved(false), 2000)
|
setTimeout(() => setSaved(false), 2000)
|
||||||
|
toast.success(`保存成功: ${data.data.message}`)
|
||||||
|
} else {
|
||||||
|
toast.error('保存失败: ' + (data.message || '未知错误'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存失败:', error)
|
console.error('保存失败:', error)
|
||||||
|
toast.error('保存失败: ' + (error instanceof Error ? error.message : '网络错误'))
|
||||||
}
|
}
|
||||||
|
setSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddService = () => {
|
const handleAddService = () => {
|
||||||
@@ -121,9 +136,11 @@ export default function EmailConfig() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
loading={saving}
|
||||||
icon={saved ? <CheckCircle className="h-4 w-4" /> : <Save className="h-4 w-4" />}
|
icon={saved ? <CheckCircle className="h-4 w-4" /> : <Save className="h-4 w-4" />}
|
||||||
>
|
>
|
||||||
{saved ? '已保存' : '保存配置'}
|
{saved ? '已保存' : saving ? '保存中...' : '保存配置'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user