init. project
This commit is contained in:
373
rag-web-ui/frontend/src/app/dashboard/api-keys/page.tsx
Normal file
373
rag-web-ui/frontend/src/app/dashboard/api-keys/page.tsx
Normal file
@@ -0,0 +1,373 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Copy, Check, List } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import DashboardLayout from "@/components/layout/dashboard-layout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
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 { useToast } from "@/components/ui/use-toast";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
export interface APIKey {
|
||||
id: number;
|
||||
name: string;
|
||||
key: string;
|
||||
is_active: boolean;
|
||||
last_used_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface APIKeyCreate {
|
||||
name: string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface APIKeyUpdate {
|
||||
name?: string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export default function APIKeysPage() {
|
||||
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newKeyName, setNewKeyName] = useState("");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isAPIListDialogOpen, setIsAPIListDialogOpen] = useState(false);
|
||||
const [copiedId, setCopiedId] = useState<number | null>(null);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
// 获取 API Keys 列表
|
||||
const fetchAPIKeys = async () => {
|
||||
try {
|
||||
const data = await api.get("/api/api-keys");
|
||||
setApiKeys(data);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "获取 API 密钥失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAPIKeys();
|
||||
}, []);
|
||||
|
||||
// 创建新的 API Key
|
||||
const createAPIKey = async () => {
|
||||
if (!newKeyName.trim()) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "请输入 API 密钥名称",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const data = await api.post("/api/api-keys", {
|
||||
name: newKeyName,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
setApiKeys([...apiKeys, data]);
|
||||
setNewKeyName("");
|
||||
setIsDialogOpen(false);
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥创建成功",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "创建 API 密钥失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除 API Key
|
||||
const deleteAPIKey = async (id: number) => {
|
||||
try {
|
||||
const response = await api.delete(`/api/api-keys/${id}`);
|
||||
|
||||
if (!response.ok) throw new Error("删除 API 密钥失败");
|
||||
|
||||
setApiKeys(apiKeys.filter((key) => key.id !== id));
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥删除成功",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "删除 API 密钥失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新 API Key 状态
|
||||
const toggleAPIKeyStatus = async (id: number, currentStatus: boolean) => {
|
||||
try {
|
||||
const response = await api.put(`/api/api-keys/${id}`, {
|
||||
is_active: !currentStatus,
|
||||
});
|
||||
|
||||
setApiKeys(
|
||||
apiKeys.map((key) =>
|
||||
key.id === id ? { ...key, is_active: !currentStatus } : key
|
||||
)
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥状态更新成功",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "更新 API 密钥失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 复制 API Key
|
||||
const copyAPIKey = async (id: number, key: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(key);
|
||||
setCopiedId(id);
|
||||
setTimeout(() => {
|
||||
setCopiedId(null);
|
||||
}, 3000);
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "API 密钥已复制到剪贴板",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "复制 API 密钥失败",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-2xl font-bold">API 密钥</h1>
|
||||
<div className="flex gap-4">
|
||||
<Dialog
|
||||
open={isAPIListDialogOpen}
|
||||
onOpenChange={setIsAPIListDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<List className="mr-2 h-4 w-4" />
|
||||
API 列表
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>可用 API 端点</DialogTitle>
|
||||
<DialogDescription>
|
||||
查看可用 API 端点及使用方式。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 space-y-6">
|
||||
<div className="border rounded-lg p-6 bg-slate-50">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
知识库查询
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
请求方法
|
||||
</h4>
|
||||
<code className="block p-3 bg-white border rounded-md text-sm font-mono text-blue-600">
|
||||
GET
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
接口路径
|
||||
</h4>
|
||||
<code className="block p-3 bg-white border rounded-md text-sm font-mono">
|
||||
/openapi/knowledge/{"{id}"}/query
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
查询参数
|
||||
</h4>
|
||||
<div className="bg-white border rounded-md p-3 space-y-2">
|
||||
<div className="grid grid-cols-3 text-sm">
|
||||
<div className="font-mono text-blue-600">query</div>
|
||||
<div className="col-span-2">
|
||||
查询语句
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 text-sm">
|
||||
<div className="font-mono text-blue-600">top_k</div>
|
||||
<div className="col-span-2">
|
||||
返回结果数量(可选,默认 3)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">
|
||||
请求头
|
||||
</h4>
|
||||
<div className="bg-white border rounded-md p-3 grid grid-cols-3 text-sm">
|
||||
<div className="font-mono text-blue-600">
|
||||
X-API-Key
|
||||
</div>
|
||||
<div className="col-span-2">你的 API 密钥</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
创建 API 密钥
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>创建新 API 密钥</DialogTitle>
|
||||
<DialogDescription>
|
||||
创建新的 API 密钥,用于程序化访问接口。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">名称</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
placeholder="请输入 API 密钥名称"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={createAPIKey}
|
||||
disabled={isCreating || !newKeyName.trim()}
|
||||
>
|
||||
{isCreating ? "创建中..." : "创建"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>API 密钥</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>最近使用</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((apiKey) => (
|
||||
<TableRow key={apiKey.id}>
|
||||
<TableCell>{apiKey.name}</TableCell>
|
||||
<TableCell className="flex items-center gap-2">
|
||||
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||
{apiKey.key}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => copyAPIKey(apiKey.id, apiKey.key)}
|
||||
>
|
||||
{copiedId === apiKey.id ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={apiKey.is_active}
|
||||
onCheckedChange={() =>
|
||||
toggleAPIKeyStatus(apiKey.id, apiKey.is_active)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(apiKey.created_at).toLocaleDateString("zh-CN")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{apiKey.last_used_at
|
||||
? new Date(apiKey.last_used_at).toLocaleDateString("zh-CN")
|
||||
: "从未"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteAPIKey(apiKey.id)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user