mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
prettier
This commit is contained in:
@@ -1,152 +1,178 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from "react"
|
||||
import {useMutation, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||
import {Globe, Loader2, Mail, Search, Server, Sparkles} from "lucide-react"
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
type UseQueryResult,
|
||||
} from '@tanstack/react-query';
|
||||
import { Globe, Loader2, Mail, Search, Server, Sparkles } from 'lucide-react';
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Label} from "@/components/ui/label"
|
||||
import {Switch} from "@/components/ui/switch"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import services from "@/lib/services"
|
||||
import type {SystemConfig} from "@/lib/services/admin"
|
||||
import {toast} from "sonner"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import services from '@/lib/services';
|
||||
import type { SystemConfig } from '@/lib/services/admin';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface SystemTabProps {
|
||||
configs: Record<string, SystemConfig>
|
||||
systemConfigsQuery: UseQueryResult<SystemConfig[], Error>
|
||||
configs: Record<string, SystemConfig>;
|
||||
systemConfigsQuery: UseQueryResult<SystemConfig[], Error>;
|
||||
}
|
||||
|
||||
export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [serverAddress, setServerAddress] = useState("")
|
||||
const [siteName, setSiteName] = useState("")
|
||||
const [smtpHost, setSmtpHost] = useState("")
|
||||
const [smtpPort, setSmtpPort] = useState("")
|
||||
const [smtpUsername, setSmtpUsername] = useState("")
|
||||
const [smtpPassword, setSmtpPassword] = useState("")
|
||||
const [smtpTestOpen, setSmtpTestOpen] = useState(false)
|
||||
const [smtpTestTo, setSmtpTestTo] = useState("")
|
||||
const [smtpTestLog, setSmtpTestLog] = useState("")
|
||||
const [smtpTestSuccess, setSmtpTestSuccess] = useState<boolean | null>(null)
|
||||
const [smtpTestError, setSmtpTestError] = useState("")
|
||||
const queryClient = useQueryClient();
|
||||
const [serverAddress, setServerAddress] = useState('');
|
||||
const [siteName, setSiteName] = useState('');
|
||||
const [smtpHost, setSmtpHost] = useState('');
|
||||
const [smtpPort, setSmtpPort] = useState('');
|
||||
const [smtpUsername, setSmtpUsername] = useState('');
|
||||
const [smtpPassword, setSmtpPassword] = useState('');
|
||||
const [smtpTestOpen, setSmtpTestOpen] = useState(false);
|
||||
const [smtpTestTo, setSmtpTestTo] = useState('');
|
||||
const [smtpTestLog, setSmtpTestLog] = useState('');
|
||||
const [smtpTestSuccess, setSmtpTestSuccess] = useState<boolean | null>(null);
|
||||
const [smtpTestError, setSmtpTestError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (systemConfigsQuery.data) {
|
||||
setServerAddress(configs["server_address"]?.value || "")
|
||||
setSiteName(configs["site_name"]?.value || "")
|
||||
setSmtpHost(configs["smtp_host"]?.value || "")
|
||||
setSmtpPort(configs["smtp_port"]?.value || "587")
|
||||
setSmtpUsername(configs["smtp_username"]?.value || "")
|
||||
setSmtpPassword(configs["smtp_password"]?.value || "")
|
||||
setServerAddress(configs['server_address']?.value || '');
|
||||
setSiteName(configs['site_name']?.value || '');
|
||||
setSmtpHost(configs['smtp_host']?.value || '');
|
||||
setSmtpPort(configs['smtp_port']?.value || '587');
|
||||
setSmtpUsername(configs['smtp_username']?.value || '');
|
||||
setSmtpPassword(configs['smtp_password']?.value || '');
|
||||
}
|
||||
}, [systemConfigsQuery.data, configs])
|
||||
}, [systemConfigsQuery.data, configs]);
|
||||
|
||||
const handleDetectAddress = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
setServerAddress(window.location.origin)
|
||||
toast.success("已自动获取当前域名并填充")
|
||||
if (typeof window !== 'undefined') {
|
||||
setServerAddress(window.location.origin);
|
||||
toast.success('已自动获取当前域名并填充');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfigMutation = useMutation({
|
||||
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
||||
const config = configs[key]
|
||||
const config = configs[key];
|
||||
if (!config) {
|
||||
throw new Error(`缺少配置项: ${key}`)
|
||||
throw new Error(`缺少配置项: ${key}`);
|
||||
}
|
||||
await services.adminSystemConfig.updateSystemConfig(key, {
|
||||
value: value ? "true" : "false",
|
||||
value: value ? 'true' : 'false',
|
||||
description: config.description,
|
||||
})
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["public-config"] })
|
||||
toast.success("配置已更新")
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin', 'system-configs'],
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: ['public-config'] });
|
||||
toast.success('配置已更新');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "更新配置失败")
|
||||
toast.error(error.message || '更新配置失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const saveSystemMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const currentAddrCfg = configs["server_address"]
|
||||
const currentSiteCfg = configs["site_name"]
|
||||
const currentAddrCfg = configs['server_address'];
|
||||
const currentSiteCfg = configs['site_name'];
|
||||
await Promise.all([
|
||||
services.adminSystemConfig.updateSystemConfig("server_address", {
|
||||
services.adminSystemConfig.updateSystemConfig('server_address', {
|
||||
value: serverAddress,
|
||||
description: currentAddrCfg?.description || "服务器地址",
|
||||
description: currentAddrCfg?.description || '服务器地址',
|
||||
}),
|
||||
services.adminSystemConfig.updateSystemConfig("site_name", {
|
||||
services.adminSystemConfig.updateSystemConfig('site_name', {
|
||||
value: siteName,
|
||||
description: currentSiteCfg?.description || "系统平台的展示名称",
|
||||
description: currentSiteCfg?.description || '系统平台的展示名称',
|
||||
}),
|
||||
])
|
||||
]);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["public-config"] }),
|
||||
])
|
||||
toast.success("通用配置已成功保存")
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin', 'system-configs'],
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: ['public-config'] }),
|
||||
]);
|
||||
toast.success('通用配置已成功保存');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "保存配置失败")
|
||||
toast.error(error.message || '保存配置失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleSystemSave = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
saveSystemMutation.mutate()
|
||||
}
|
||||
e.preventDefault();
|
||||
saveSystemMutation.mutate();
|
||||
};
|
||||
|
||||
const saveSmtpMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const updates = [
|
||||
{ key: "smtp_host", value: smtpHost },
|
||||
{ key: "smtp_port", value: smtpPort },
|
||||
{ key: "smtp_username", value: smtpUsername },
|
||||
{ key: "smtp_password", value: smtpPassword },
|
||||
]
|
||||
{ key: 'smtp_host', value: smtpHost },
|
||||
{ key: 'smtp_port', value: smtpPort },
|
||||
{ key: 'smtp_username', value: smtpUsername },
|
||||
{ key: 'smtp_password', value: smtpPassword },
|
||||
];
|
||||
|
||||
for (const update of updates) {
|
||||
const currentCfg = configs[update.key]
|
||||
if (update.key === "smtp_password" && (update.value === "" || update.value === "******")) {
|
||||
const currentCfg = configs[update.key];
|
||||
if (
|
||||
update.key === 'smtp_password' &&
|
||||
(update.value === '' || update.value === '******')
|
||||
) {
|
||||
// If already configured and sent empty or mask, skip updating it (keep existing)
|
||||
if (currentCfg && currentCfg.value === "******") {
|
||||
continue
|
||||
if (currentCfg && currentCfg.value === '******') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await services.adminSystemConfig.updateSystemConfig(update.key, {
|
||||
value: update.value,
|
||||
description: currentCfg?.description || "",
|
||||
})
|
||||
description: currentCfg?.description || '',
|
||||
});
|
||||
}
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
toast.success("SMTP 邮件配置已成功保存")
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin', 'system-configs'],
|
||||
});
|
||||
toast.success('SMTP 邮件配置已成功保存');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "保存配置失败")
|
||||
toast.error(error.message || '保存配置失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleSmtpSave = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
saveSmtpMutation.mutate()
|
||||
}
|
||||
e.preventDefault();
|
||||
saveSmtpMutation.mutate();
|
||||
};
|
||||
|
||||
const testSmtpMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
setSmtpTestLog("正在发起连接测试...\n")
|
||||
setSmtpTestSuccess(null)
|
||||
setSmtpTestError("")
|
||||
setSmtpTestLog('正在发起连接测试...\n');
|
||||
setSmtpTestSuccess(null);
|
||||
setSmtpTestError('');
|
||||
|
||||
const res = await services.adminSystemConfig.testSMTP({
|
||||
smtp_host: smtpHost,
|
||||
@@ -154,151 +180,167 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
smtp_username: smtpUsername,
|
||||
smtp_password: smtpPassword,
|
||||
to: smtpTestTo,
|
||||
})
|
||||
return res
|
||||
});
|
||||
return res;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setSmtpTestLog(data.log)
|
||||
setSmtpTestLog(data.log);
|
||||
if (data.success) {
|
||||
setSmtpTestSuccess(true)
|
||||
toast.success("测试邮件发送成功")
|
||||
setSmtpTestSuccess(true);
|
||||
toast.success('测试邮件发送成功');
|
||||
} else {
|
||||
setSmtpTestSuccess(false)
|
||||
setSmtpTestError(data.error || "发送失败,请检查配置和日志。")
|
||||
toast.error("测试邮件发送失败")
|
||||
setSmtpTestSuccess(false);
|
||||
setSmtpTestError(data.error || '发送失败,请检查配置和日志。');
|
||||
toast.error('测试邮件发送失败');
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
setSmtpTestSuccess(false)
|
||||
setSmtpTestError(error.message || "请求发送失败")
|
||||
setSmtpTestLog((prev) => prev + `\n[请求错误] ${error.message}\n`)
|
||||
toast.error(error.message || "测试请求发送失败")
|
||||
setSmtpTestSuccess(false);
|
||||
setSmtpTestError(error.message || '请求发送失败');
|
||||
setSmtpTestLog((prev) => prev + `\n[请求错误] ${error.message}\n`);
|
||||
toast.error(error.message || '测试请求发送失败');
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleSmtpTestSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
if (!smtpTestTo) {
|
||||
toast.error("请输入目标邮箱地址")
|
||||
return
|
||||
toast.error('请输入目标邮箱地址');
|
||||
return;
|
||||
}
|
||||
testSmtpMutation.mutate()
|
||||
}
|
||||
testSmtpMutation.mutate();
|
||||
};
|
||||
|
||||
const indexingEnabled = configs["search_engine_indexing_enabled"]?.value === "true"
|
||||
const indexingEnabled =
|
||||
configs['search_engine_indexing_enabled']?.value === 'true';
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className='space-y-8'>
|
||||
{/* 通用设置 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="rounded-lg bg-primary/10 p-1.5 text-primary">
|
||||
<Server className="size-4" />
|
||||
<Card className='border border-dashed shadow-sm'>
|
||||
<CardHeader className='border-b border-dashed pb-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='rounded-lg bg-primary/10 p-1.5 text-primary'>
|
||||
<Server className='size-4' />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">通用设置</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
<CardTitle className='text-base font-semibold'>
|
||||
通用设置
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
配置站点标识、服务地址与搜索引擎收录策略
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={handleSystemSave} className="flex flex-col gap-6">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="site_name" className="text-xs font-semibold">
|
||||
<CardContent className='pt-6'>
|
||||
<form onSubmit={handleSystemSave} className='flex flex-col gap-6'>
|
||||
<div className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<Label htmlFor='site_name' className='text-xs font-semibold'>
|
||||
站点名称
|
||||
</Label>
|
||||
<Input
|
||||
id="site_name"
|
||||
type="text"
|
||||
id='site_name'
|
||||
type='text'
|
||||
value={siteName}
|
||||
onChange={(e) => setSiteName(e.target.value)}
|
||||
placeholder="例如: Wavelet"
|
||||
className="border-dashed bg-card text-xs"
|
||||
placeholder='例如: Wavelet'
|
||||
className='border-dashed bg-card text-xs'
|
||||
/>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
<p className='text-[11px] leading-relaxed text-muted-foreground'>
|
||||
用于网页标题、登录注册页面、管理后台与系统邮件。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Label htmlFor="server_address" className="text-xs font-semibold">
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<Label
|
||||
htmlFor='server_address'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
服务器访问地址
|
||||
</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
type='button'
|
||||
variant='link'
|
||||
size='sm'
|
||||
onClick={handleDetectAddress}
|
||||
className="h-auto px-0 text-xs"
|
||||
className='h-auto px-0 text-xs'
|
||||
>
|
||||
<Sparkles data-icon="inline-start" />
|
||||
<Sparkles data-icon='inline-start' />
|
||||
使用当前域名
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
id="server_address"
|
||||
type="url"
|
||||
id='server_address'
|
||||
type='url'
|
||||
value={serverAddress}
|
||||
onChange={(e) => setServerAddress(e.target.value)}
|
||||
placeholder="例如: https://example.com"
|
||||
className="border-dashed bg-card text-xs"
|
||||
placeholder='例如: https://example.com'
|
||||
className='border-dashed bg-card text-xs'
|
||||
/>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
<p className='text-[11px] leading-relaxed text-muted-foreground'>
|
||||
限定 API 的允许来源;留空将开放任意来源访问。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-dashed p-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<div className="rounded-md bg-muted p-1.5 text-muted-foreground">
|
||||
<Search className="size-4" />
|
||||
<div className='flex flex-col gap-3 rounded-lg border border-dashed p-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex min-w-0 items-start gap-3'>
|
||||
<div className='rounded-md bg-muted p-1.5 text-muted-foreground'>
|
||||
<Search className='size-4' />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold">搜索引擎抓取与收录</span>
|
||||
<Badge variant={indexingEnabled ? "default" : "secondary"}>
|
||||
{indexingEnabled ? "允许收录" : "禁止收录"}
|
||||
<div className='flex min-w-0 flex-col gap-1'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<span className='text-xs font-semibold'>
|
||||
搜索引擎抓取与收录
|
||||
</span>
|
||||
<Badge variant={indexingEnabled ? 'default' : 'secondary'}>
|
||||
{indexingEnabled ? '允许收录' : '禁止收录'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
<p className='text-[11px] leading-relaxed text-muted-foreground'>
|
||||
控制 Google、Baidu、Bing 等搜索引擎是否可以索引公开页面。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
aria-label="允许搜索引擎抓取与收录"
|
||||
aria-label='允许搜索引擎抓取与收录'
|
||||
checked={indexingEnabled}
|
||||
disabled={updateConfigMutation.isPending || systemConfigsQuery.isPending}
|
||||
disabled={
|
||||
updateConfigMutation.isPending || systemConfigsQuery.isPending
|
||||
}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConfigMutation.mutate({ key: "search_engine_indexing_enabled", value: checked })
|
||||
updateConfigMutation.mutate({
|
||||
key: 'search_engine_indexing_enabled',
|
||||
value: checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse gap-2 border-t border-dashed pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<Globe className="size-3.5" />
|
||||
<div className='flex flex-col-reverse gap-2 border-t border-dashed pt-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex items-center gap-2 text-[11px] text-muted-foreground'>
|
||||
<Globe className='size-3.5' />
|
||||
地址变更会影响跨域访问策略,请填写完整协议与域名。
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
type='submit'
|
||||
size='sm'
|
||||
disabled={saveSystemMutation.isPending}
|
||||
>
|
||||
{saveSystemMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 data-icon="inline-start" className="animate-spin" />
|
||||
<Loader2
|
||||
data-icon='inline-start'
|
||||
className='animate-spin'
|
||||
/>
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存配置"
|
||||
'保存配置'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -307,98 +349,120 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
</Card>
|
||||
|
||||
{/* SMTP 邮件设置 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-primary/10 text-primary">
|
||||
<Mail className="size-4" />
|
||||
<Card className='border border-dashed shadow-sm'>
|
||||
<CardHeader className='border-b border-dashed pb-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='p-1.5 rounded-lg bg-primary/10 text-primary'>
|
||||
<Mail className='size-4' />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">SMTP 邮件设置</CardTitle>
|
||||
<CardDescription className="text-xs">配置系统的邮件发送服务 (SMTP)</CardDescription>
|
||||
<CardTitle className='text-base font-semibold'>
|
||||
SMTP 邮件设置
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
配置系统的邮件发送服务 (SMTP)
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={handleSmtpSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_host" className="text-xs font-semibold">SMTP 服务器地址</Label>
|
||||
<CardContent className='pt-6'>
|
||||
<form onSubmit={handleSmtpSave} className='space-y-6'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='smtp_host' className='text-xs font-semibold'>
|
||||
SMTP 服务器地址
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp_host"
|
||||
type="text"
|
||||
id='smtp_host'
|
||||
type='text'
|
||||
value={smtpHost}
|
||||
onChange={(e) => setSmtpHost(e.target.value)}
|
||||
placeholder="例如: smtp.example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='例如: smtp.example.com'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_port" className="text-xs font-semibold">SMTP 端口</Label>
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='smtp_port' className='text-xs font-semibold'>
|
||||
SMTP 端口
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp_port"
|
||||
type="number"
|
||||
id='smtp_port'
|
||||
type='number'
|
||||
value={smtpPort}
|
||||
onChange={(e) => setSmtpPort(e.target.value)}
|
||||
placeholder="例如: 587 或 465"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='例如: 587 或 465'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_username" className="text-xs font-semibold">SMTP 账户</Label>
|
||||
<div className='space-y-1.5'>
|
||||
<Label
|
||||
htmlFor='smtp_username'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
SMTP 账户
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp_username"
|
||||
type="text"
|
||||
id='smtp_username'
|
||||
type='text'
|
||||
value={smtpUsername}
|
||||
onChange={(e) => setSmtpUsername(e.target.value)}
|
||||
placeholder="例如: sender@example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='例如: sender@example.com'
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_password" className="text-xs font-semibold">SMTP 访问凭证</Label>
|
||||
<div className='space-y-1.5'>
|
||||
<Label
|
||||
htmlFor='smtp_password'
|
||||
className='text-xs font-semibold'
|
||||
>
|
||||
SMTP 访问凭证
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp_password"
|
||||
type="password"
|
||||
id='smtp_password'
|
||||
type='password'
|
||||
value={smtpPassword}
|
||||
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||
placeholder={configs["smtp_password"]?.value === "******" ? "•••••• (已配置,留空或输入新值)" : "输入凭证密码"}
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder={
|
||||
configs['smtp_password']?.value === '******'
|
||||
? '•••••• (已配置,留空或输入新值)'
|
||||
: '输入凭证密码'
|
||||
}
|
||||
className='bg-card border-dashed text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-dashed">
|
||||
<div className='flex justify-end gap-2 pt-4 border-t border-dashed'>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
setSmtpTestOpen(true)
|
||||
setSmtpTestTo("")
|
||||
setSmtpTestLog("")
|
||||
setSmtpTestSuccess(null)
|
||||
setSmtpTestError("")
|
||||
setSmtpTestOpen(true);
|
||||
setSmtpTestTo('');
|
||||
setSmtpTestLog('');
|
||||
setSmtpTestSuccess(null);
|
||||
setSmtpTestError('');
|
||||
}}
|
||||
disabled={saveSmtpMutation.isPending}
|
||||
>
|
||||
测试发件
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
type='submit'
|
||||
size='sm'
|
||||
disabled={saveSmtpMutation.isPending}
|
||||
>
|
||||
{saveSmtpMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
<Loader2 className='mr-1.5 size-3.5 animate-spin' />
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存配置"
|
||||
'保存配置'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -407,72 +471,77 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
</Card>
|
||||
|
||||
<Dialog open={smtpTestOpen} onOpenChange={setSmtpTestOpen}>
|
||||
<DialogContent className="max-w-lg border border-dashed">
|
||||
<DialogContent className='max-w-lg border border-dashed'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-semibold">SMTP 发件测试</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
输入接收测试邮件的邮箱地址。系统将使用您在表单中当前填写的 SMTP 配置进行发件测试。
|
||||
<DialogTitle className='text-base font-semibold'>
|
||||
SMTP 发件测试
|
||||
</DialogTitle>
|
||||
<DialogDescription className='text-xs'>
|
||||
输入接收测试邮件的邮箱地址。系统将使用您在表单中当前填写的 SMTP
|
||||
配置进行发件测试。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSmtpTestSubmit} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_test_to" className="text-xs font-semibold">目标邮箱地址</Label>
|
||||
<form onSubmit={handleSmtpTestSubmit} className='space-y-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='smtp_test_to' className='text-xs font-semibold'>
|
||||
目标邮箱地址
|
||||
</Label>
|
||||
<Input
|
||||
id="smtp_test_to"
|
||||
type="email"
|
||||
id='smtp_test_to'
|
||||
type='email'
|
||||
required
|
||||
value={smtpTestTo}
|
||||
onChange={(e) => setSmtpTestTo(e.target.value)}
|
||||
placeholder="例如: receiver@example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
placeholder='例如: receiver@example.com'
|
||||
className='bg-card border-dashed text-xs'
|
||||
disabled={testSmtpMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{smtpTestLog && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">连接与传输日志</Label>
|
||||
<pre className="bg-zinc-950 text-zinc-50 font-mono p-4 rounded-lg text-[10px] h-60 overflow-y-auto whitespace-pre-wrap border border-dashed border-zinc-800 leading-relaxed">
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>连接与传输日志</Label>
|
||||
<pre className='bg-zinc-950 text-zinc-50 font-mono p-4 rounded-lg text-[10px] h-60 overflow-y-auto whitespace-pre-wrap border border-dashed border-zinc-800 leading-relaxed'>
|
||||
{smtpTestLog}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{smtpTestSuccess === true && (
|
||||
<div className="p-3 rounded-lg border border-dashed border-emerald-500/30 bg-emerald-500/5 text-emerald-500 text-xs">
|
||||
<div className='p-3 rounded-lg border border-dashed border-emerald-500/30 bg-emerald-500/5 text-emerald-500 text-xs'>
|
||||
测试成功!邮件已顺利发出。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{smtpTestSuccess === false && (
|
||||
<div className="p-3 rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 text-rose-500 text-xs break-all">
|
||||
<div className='p-3 rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 text-rose-500 text-xs break-all'>
|
||||
测试失败:{smtpTestError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0 border-t border-dashed pt-4">
|
||||
<DialogFooter className='gap-2 sm:gap-0 border-t border-dashed pt-4'>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => setSmtpTestOpen(false)}
|
||||
disabled={testSmtpMutation.isPending}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
type='submit'
|
||||
size='sm'
|
||||
disabled={testSmtpMutation.isPending}
|
||||
>
|
||||
{testSmtpMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
<Loader2 className='mr-1.5 size-3.5 animate-spin' />
|
||||
测试中...
|
||||
</>
|
||||
) : (
|
||||
"开始测试"
|
||||
'开始测试'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -480,5 +549,5 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user