Files
AiTool/frontend/app/(main)/settings/portal-links/page.tsx
2026-03-15 16:38:59 +08:00

257 lines
8.2 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.
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
portalLinksApi,
type PortalLinkRead,
type PortalLinkCreate,
} from "@/lib/api/client";
import { Globe, Plus, Pencil, Trash2, Loader2, ExternalLink } from "lucide-react";
import { toast } from "sonner";
export default function SettingsPortalLinksPage() {
const [links, setLinks] = useState<PortalLinkRead[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({ name: "", url: "" });
const loadLinks = useCallback(async () => {
try {
const list = await portalLinksApi.list();
setLinks(list);
} catch {
toast.error("加载快捷门户列表失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadLinks();
}, [loadLinks]);
const openAdd = () => {
setEditingId(null);
setForm({ name: "", url: "" });
setDialogOpen(true);
};
const openEdit = (item: PortalLinkRead) => {
setEditingId(item.id);
setForm({ name: item.name, url: item.url });
setDialogOpen(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.name.trim() || !form.url.trim()) {
toast.error("请填写名称和链接");
return;
}
setSaving(true);
try {
if (editingId) {
await portalLinksApi.update(editingId, {
name: form.name.trim(),
url: form.url.trim(),
});
toast.success("已更新");
} else {
const payload: PortalLinkCreate = {
name: form.name.trim(),
url: form.url.trim(),
};
await portalLinksApi.create(payload);
toast.success("已添加");
}
setDialogOpen(false);
await loadLinks();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("portal-links-changed"));
}
} catch (e) {
toast.error(e instanceof Error ? e.message : "保存失败");
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("确定删除该快捷门户入口?")) return;
try {
await portalLinksApi.delete(id);
toast.success("已删除");
await loadLinks();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("portal-links-changed"));
}
} catch (e) {
toast.error(e instanceof Error ? e.message : "删除失败");
}
};
return (
<div className="p-6 max-w-4xl">
<div className="mb-4">
<Link
href="/settings"
className="text-sm text-muted-foreground hover:text-foreground"
>
</Link>
</div>
<Card>
<CardHeader>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<CardTitle className="flex items-center gap-2">
<Globe className="h-5 w-5" />
</CardTitle>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
<Button onClick={openAdd}>
<Plus className="h-4 w-4" />
<span className="ml-2"></span>
</Button>
</div>
</CardHeader>
<CardContent>
{loading ? (
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Loader2 className="h-4 w-4 animate-spin" />
</p>
) : links.length === 0 ? (
<p className="text-sm text-muted-foreground py-4">
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[120px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{links.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline truncate max-w-[280px] inline-block"
>
{item.url}
</a>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => openEdit(item)}
title="编辑"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(item.id)}
title="删除"
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
<Button variant="ghost" size="sm" asChild title="打开">
<a href={item.url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingId ? "编辑快捷门户入口" : "添加快捷门户入口"}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="portal-name"></Label>
<Input
id="portal-name"
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
placeholder="如:电子税务局、公积金"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="portal-url"></Label>
<Input
id="portal-url"
type="url"
value={form.url}
onChange={(e) => setForm((f) => ({ ...f, url: e.target.value }))}
placeholder="https://..."
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setDialogOpen(false)}
>
</Button>
<Button type="submit" disabled={saving}>
{saving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
{editingId ? "保存" : "添加"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}