Files
admin-frontend-domain/src/routes/domain.tsx
T

468 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/routes/domain.tsx (最终 Bug 修复版)
import {
addDomain,
deleteDomain,
syncDomainWHOIS,
updateDomain,
useDomainList,
verifyDomain,
} from "@/api/domain"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
// 导入 shadcn/ui 组件
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Textarea } from "@/components/ui/textarea"
// 导入 API 类型和函数
import type { BillingDataMod, Domain } from "@/types/domain"
import {
CheckCircle,
Edit,
MoreVertical,
PlusCircle,
RefreshCcw,
RefreshCw,
Trash2,
} from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import useSWR from "swr"
export default function DomainPage() {
// --- React State Hooks ---
const [domains, setDomains] = useState<Domain[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [newDomainName, setNewDomainName] = useState("")
const [verificationToken, setVerificationToken] = useState("")
const [isVerificationInfoModalOpen, setIsVerificationInfoModalOpen] = useState(false)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [currentDomain, setCurrentDomain] = useState<Domain | null>(null)
const [editFormData, setEditFormData] = useState<Partial<BillingDataMod>>({})
// --- 数据获取 (使用 SWR) ---
const {
data: domainData,
error,
mutate,
} = useSWR("/api/v1/domains", useDomainList, { revalidateOnFocus: false })
useEffect(() => {
if (domainData) {
setDomains(domainData)
setIsLoading(false)
}
if (error) {
toast.error("无法加载域名列表,请检查后端服务是否正常。")
setIsLoading(false)
}
}, [domainData, error])
const handleAddDomain = async () => {
if (!newDomainName) {
toast.error("请输入域名")
return
}
try {
const response = await addDomain(newDomainName)
setVerificationToken(response.VerifyToken)
setIsAddModalOpen(false)
setIsVerificationInfoModalOpen(true)
setNewDomainName("")
mutate()
} catch (err) {
toast.error("添加失败", { description: (err as Error).message })
}
}
const handleVerify = async (domainId: number) => {
try {
const response = await verifyDomain(domainId)
if (response.success) {
toast.success("验证成功", { description: response.message })
} else {
toast.warning("验证失败", { description: response.message })
}
setTimeout(() => mutate(), 2000)
} catch (err) {
toast.error("操作失败", { description: (err as Error).message })
}
}
const handleSyncWhois = async (domainId: number) => {
const loadingToast = toast.loading("正在同步 Whois 信息...")
try {
await syncDomainWHOIS(domainId)
toast.success("同步成功", { id: loadingToast, description: "域名 Whois 信息已更新。" })
mutate()
} catch (err) {
toast.error("同步失败", { id: loadingToast, description: (err as Error).message })
}
}
const handleDelete = async (domainId: number, domainName: string) => {
if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) {
try {
await deleteDomain(domainId)
toast.success("删除成功", { description: `域名 ${domainName} 已被删除。` })
mutate()
} catch (err) {
toast.error("删除失败", { description: (err as Error).message })
}
}
}
const handlePublicToggle = async (domain: Domain) => {
try {
await updateDomain(domain.ID, {
is_public: !domain.IsPublic,
billing_data: domain.BillingData as BillingDataMod,
})
toast.success(`域名 ${domain.Domain} 的可见状态已更新`)
mutate()
} catch (err) {
toast.error("更新失败", { description: (err as Error).message })
}
}
const handleEditClick = (domain: Domain) => {
setCurrentDomain(domain)
setEditFormData(domain.BillingData || {})
setIsEditModalOpen(true)
}
const handleEditFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setEditFormData({
...editFormData,
[e.target.name]: e.target.value,
})
}
const handleUpdateDomain = async () => {
if (!currentDomain) return
try {
const dataToSend = { ...editFormData }
if (dataToSend.registeredDate) {
dataToSend.registeredDate = new Date(dataToSend.registeredDate).toISOString()
}
if (dataToSend.endDate) {
dataToSend.endDate = new Date(dataToSend.endDate).toISOString()
}
await updateDomain(currentDomain.ID, {
is_public: currentDomain.IsPublic,
billing_data: dataToSend as BillingDataMod,
})
toast.success("更新成功", {
description: `域名 ${currentDomain.Domain} 的配置已保存。`,
})
setIsEditModalOpen(false)
mutate()
} catch (err) {
toast.error("更新失败", { description: (err as Error).message })
}
}
const getStatusVariant = (
status: string,
): "default" | "secondary" | "destructive" | "outline" => {
switch (status) {
case "verified":
return "default"
case "pending":
return "secondary"
case "expired":
return "destructive"
default:
return "outline"
}
}
// --- JSX 渲染 (保持不变) ---
return (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => mutate()}
disabled={isLoading}
>
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
"example.com"
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input
value={newDomainName}
onChange={(e) => setNewDomainName(e.target.value)}
placeholder="your-domain.com"
onKeyUp={(e) => e.key === "Enter" && handleAddDomain()}
/>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => setIsAddModalOpen(false)}
>
</Button>
<Button onClick={handleAddDomain}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-10 text-muted-foreground">...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{domains.map((domain) => (
<TableRow key={domain.ID}>
<TableCell className="font-medium">
{domain.Domain}
</TableCell>
<TableCell>
<Badge variant={getStatusVariant(domain.Status)}>
{domain.Status}
</Badge>
</TableCell>
<TableCell>{domain.expires_in_days ?? "N/A"}</TableCell>
<TableCell>
<Switch
checked={domain.IsPublic}
onCheckedChange={() => handlePublicToggle(domain)}
/>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{domain.Status === "pending" && (
<DropdownMenuItem
onClick={() => handleVerify(domain.ID)}
>
<CheckCircle className="mr-2 h-4 w-4" />{" "}
</DropdownMenuItem>
)}
{domain.Status === "verified" && (
<DropdownMenuItem
onClick={() =>
handleSyncWhois(domain.ID)
}
>
<RefreshCcw className="mr-2 h-4 w-4" />{" "}
Whois
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => handleEditClick(domain)}
>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-600"
onClick={() =>
handleDelete(domain.ID, domain.Domain)
}
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* 验证信息弹窗 */}
<Dialog
open={isVerificationInfoModalOpen}
onOpenChange={setIsVerificationInfoModalOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
DNS TXT
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-2">
<p> DNS </p>
<div className="p-2 bg-muted rounded-md text-sm">
<p>
<span className="font-semibold">:</span> TXT
</p>
<p>
<span className="font-semibold">/:</span> @
</p>
<p className="font-semibold">:</p>
<p className="font-mono bg-background p-2 rounded">
{verificationToken}
</p>
</div>
<p className="text-xs text-muted-foreground">
DNS
</p>
</div>
<DialogFooter>
<Button onClick={() => setIsVerificationInfoModalOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 编辑弹窗 */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<span className="font-mono">{currentDomain?.Domain}</span>{" "}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="registrar" className="text-right">
</Label>
<Input
id="registrar"
name="registrar"
value={editFormData.registrar || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="registeredDate" className="text-right">
</Label>
<Input
id="registeredDate"
name="registeredDate"
type="date"
value={editFormData.registeredDate?.split("T")[0] || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="endDate" className="text-right">
</Label>
<Input
id="endDate"
name="endDate"
type="date"
value={editFormData.endDate?.split("T")[0] || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="renewalPrice" className="text-right">
</Label>
<Input
id="renewalPrice"
name="renewalPrice"
value={editFormData.renewalPrice || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="notes" className="text-right">
</Label>
<Textarea
id="notes"
name="notes"
value={editFormData.notes || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button variant="secondary" onClick={() => setIsEditModalOpen(false)}>
</Button>
<Button onClick={handleUpdateDomain}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}