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

262 lines
8.4 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 {
cloudDocsApi,
type CloudDocLinkRead,
type CloudDocLinkCreate,
} from "@/lib/api/client";
import { FileStack, Plus, Pencil, Trash2, Loader2, ExternalLink } from "lucide-react";
import { toast } from "sonner";
export default function SettingsCloudDocsPage() {
const [links, setLinks] = useState<CloudDocLinkRead[]>([]);
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 cloudDocsApi.list();
setLinks(list);
} catch {
toast.error("加载云文档列表失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadLinks();
}, [loadLinks]);
const openAdd = () => {
setEditingId(null);
setForm({ name: "", url: "" });
setDialogOpen(true);
};
const openEdit = (item: CloudDocLinkRead) => {
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 cloudDocsApi.update(editingId, {
name: form.name.trim(),
url: form.url.trim(),
});
toast.success("已更新");
} else {
const payload: CloudDocLinkCreate = {
name: form.name.trim(),
url: form.url.trim(),
};
await cloudDocsApi.create(payload);
toast.success("已添加");
}
setDialogOpen(false);
await loadLinks();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("cloud-docs-changed"));
}
} catch (e) {
toast.error(e instanceof Error ? e.message : "保存失败");
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!confirm("确定删除该云文档入口?")) return;
try {
await cloudDocsApi.delete(id);
toast.success("已删除");
await loadLinks();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("cloud-docs-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">
<FileStack 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="cloud-doc-name"></Label>
<Input
id="cloud-doc-name"
value={form.name}
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
placeholder="如:腾讯文档、飞书、语雀"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="cloud-doc-url">/</Label>
<Input
id="cloud-doc-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>
);
}