add model config and update settings

This commit is contained in:
qixinbo
2026-03-14 19:56:34 +08:00
parent 98d99ded37
commit 6089192575
9 changed files with 642 additions and 202 deletions
+9
View File
@@ -6,6 +6,7 @@ import { Skills } from "./pages/Skills";
import { Settings } from "./pages/Settings";
import { Users } from "./pages/Users";
import { Login } from "./pages/Login";
import { ModelConfigs } from "./pages/ModelConfigs";
import { useAuthStore } from "./store/authStore";
// Protected Route Component
@@ -82,6 +83,14 @@ function App() {
</MainLayout>
</ProtectedRoute>
} />
<Route path="/model-configs" element={
<ProtectedRoute>
<MainLayout>
<ModelConfigs />
</MainLayout>
</ProtectedRoute>
} />
</Routes>
</BrowserRouter>
);
+27 -14
View File
@@ -1,7 +1,7 @@
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings } from "lucide-react";
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings, Brain } from "lucide-react";
import { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuthStore } from "@/store/authStore";
@@ -138,20 +138,33 @@ function SidebarBody() {
}}
>
<Settings className="h-4 w-4 text-zinc-500" />
Settings
</button>
{user?.is_admin && (
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-indigo-600 hover:bg-indigo-50 transition-colors"
onClick={() => {
navigate("/users");
setShowUserMenu(false);
}}
>
<User className="h-4 w-4" />
User Management
</button>
<>
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 transition-colors"
onClick={() => {
navigate("/model-configs");
setShowUserMenu(false);
}}
>
<Brain className="h-4 w-4 text-zinc-500" />
</button>
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-indigo-600 hover:bg-indigo-50 transition-colors"
onClick={() => {
navigate("/users");
setShowUserMenu(false);
}}
>
<User className="h-4 w-4" />
</button>
</>
)}
<div className="h-px bg-zinc-100 my-1 mx-2" />
@@ -160,7 +173,7 @@ function SidebarBody() {
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
onClick={handleLogout}
>
Log out
退
</button>
</div>
)}
+376
View File
@@ -0,0 +1,376 @@
import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { api } from "@/lib/api";
import { Loader2, Plus, RefreshCw, Search, Trash2, Pencil, Eye, EyeOff, Brain } from "lucide-react";
import { useAuthStore } from "@/store/authStore";
interface ModelConfig {
id: string;
name?: string;
provider: string;
model: string;
model_type?: string;
base_model?: string;
protocol_type?: string;
api_key?: string;
api_base?: string;
extra_headers?: Record<string, string>;
is_active: boolean;
}
const defaultForm: Omit<ModelConfig, "id"> = {
name: "",
provider: "openai",
model: "",
model_type: "LLM",
base_model: "",
protocol_type: "OpenAI",
api_key: "",
api_base: "",
extra_headers: {},
is_active: true,
};
export function ModelConfigs() {
const { user } = useAuthStore();
const isAdmin = !!user?.is_admin;
const [configs, setConfigs] = useState<ModelConfig[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [keyword, setKeyword] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [error, setError] = useState("");
const [extraConfigText, setExtraConfigText] = useState("{}");
const [form, setForm] = useState<Omit<ModelConfig, "id">>(defaultForm);
const fetchConfigs = async () => {
setIsLoading(true);
try {
const data = await api.get<ModelConfig[]>("/api/v1/llm");
setConfigs(data);
} catch (e) {
console.error(e);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchConfigs();
}, []);
const filteredConfigs = useMemo(() => {
const value = keyword.trim().toLowerCase();
if (!value) return configs;
return configs.filter((item) =>
[item.name, item.model, item.provider, item.base_model].filter(Boolean).some((v) => String(v).toLowerCase().includes(value))
);
}, [configs, keyword]);
const openCreate = () => {
setEditingId(null);
setForm(defaultForm);
setExtraConfigText("{}");
setError("");
setShowApiKey(false);
setDialogOpen(true);
};
const openEdit = (item: ModelConfig) => {
setEditingId(item.id);
setForm({
name: item.name || "",
provider: item.provider || "openai",
model: item.model || "",
model_type: item.model_type || "LLM",
base_model: item.base_model || "",
protocol_type: item.protocol_type || "OpenAI",
api_key: item.api_key || "",
api_base: item.api_base || "",
extra_headers: item.extra_headers || {},
is_active: item.is_active,
});
setExtraConfigText(JSON.stringify(item.extra_headers || {}, null, 2));
setError("");
setShowApiKey(false);
setDialogOpen(true);
};
const handleSave = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!form.model || !form.provider || !form.api_base) {
setError("请填写必填项");
return;
}
setIsSaving(true);
setError("");
try {
let extraHeaders: Record<string, string> = {};
if (extraConfigText.trim()) {
try {
const parsed = JSON.parse(extraConfigText);
if (parsed && typeof parsed === "object") extraHeaders = parsed;
} catch (err) {
setError("额外配置必须是有效的JSON");
setIsSaving(false);
return;
}
}
const payload = {
...form,
extra_headers: extraHeaders,
name: form.name || form.model,
model_type: form.model_type || "大语言模型",
base_model: form.base_model || form.model,
protocol_type: form.protocol_type || "OpenAI",
};
if (editingId) {
await api.put(`/api/v1/llm/${editingId}`, payload);
} else {
const id = `${Date.now()}`;
await api.post("/api/v1/llm", { ...payload, id });
}
setDialogOpen(false);
await fetchConfigs();
} catch (e: any) {
setError(e.message || "保存配置失败");
} finally {
setIsSaving(false);
}
};
const handleDelete = async (id: string) => {
if (!window.confirm("确认删除该模型吗?")) return;
try {
await api.delete(`/api/v1/llm/${id}`);
await fetchConfigs();
} catch (e) {
console.error(e);
}
};
const handleSetDefault = async (item: ModelConfig) => {
if (!isAdmin || item.is_active) return;
try {
await api.put(`/api/v1/llm/${item.id}`, { is_active: true });
await fetchConfigs();
} catch (e) {
console.error(e);
}
};
if (!isAdmin) {
return (
<div className="flex-1 flex flex-col h-full bg-zinc-50/30 overflow-hidden items-center justify-center">
<div className="text-zinc-500 text-lg">访使</div>
</div>
);
}
return (
<div className="flex-1 flex flex-col h-full bg-zinc-50/30 overflow-hidden">
<div className="h-14 px-6 flex items-center justify-between border-b border-zinc-100 bg-white">
<div className="flex items-center gap-2 text-zinc-700 font-medium">
<Brain className="h-5 w-5 text-indigo-500" />
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="h-4 w-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="搜索模型..." className="w-[200px] pl-9 h-8 text-sm" />
</div>
<Button variant="outline" size="icon" className="h-8 w-8 text-zinc-500" onClick={fetchConfigs}>
<RefreshCw className="h-4 w-4" />
</Button>
<Button className="h-8 px-3 bg-indigo-600 hover:bg-indigo-700 text-white text-sm" onClick={openCreate}>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
<div className="flex-1 p-6 overflow-auto">
<div className="bg-white rounded-xl border border-zinc-200 shadow-sm overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center h-40">
<Loader2 className="h-6 w-6 animate-spin text-zinc-400" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredConfigs.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center h-24 text-zinc-500">
</TableCell>
</TableRow>
) : (
filteredConfigs.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">
{item.name || item.model}
</TableCell>
<TableCell className="capitalize">{item.provider}</TableCell>
<TableCell className="text-zinc-500 font-mono text-xs">{item.model}</TableCell>
<TableCell className="text-zinc-500">{item.model_type || "大语言模型"}</TableCell>
<TableCell>
<span
onClick={() => handleSetDefault(item)}
className={`inline-flex px-2 py-1 rounded-full text-xs font-medium cursor-pointer transition-colors ${item.is_active ? 'bg-emerald-100 text-emerald-700' : 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200'}`}
title={item.is_active ? "当前默认模型" : "点击设为默认"}
>
{item.is_active ? '默认' : '设为默认'}
</span>
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-zinc-500 hover:text-indigo-600"
onClick={() => openEdit(item)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-zinc-500 hover:text-red-600"
onClick={() => handleDelete(item.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</div>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<form onSubmit={handleSave}>
<DialogHeader>
<DialogTitle>{editingId ? "编辑模型" : "添加模型"}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded-md p-2">{error}</div>}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Input value={form.name || ""} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} placeholder="如:GPT-4" />
</div>
<div className="space-y-2">
<Label> *</Label>
<Select value={form.provider} onValueChange={(v) => setForm((p) => ({ ...p, provider: v || "openai" }))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value="zhipuai">ZhipuAI</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem>
<SelectItem value="azure">Azure OpenAI</SelectItem>
<SelectItem value="local">Local</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Input value={form.model || ""} onChange={(e) => setForm((p) => ({ ...p, model: e.target.value }))} placeholder="如:gpt-4-turbo" required />
</div>
<div className="space-y-2">
<Label></Label>
<Input value={form.base_model || ""} onChange={(e) => setForm((p) => ({ ...p, base_model: e.target.value }))} placeholder="可选" />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select value={form.model_type || "大语言模型"} onValueChange={(v) => setForm((p) => ({ ...p, model_type: v || "大语言模型" }))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="大语言模型"></SelectItem>
<SelectItem value="多模态模型"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={form.protocol_type || "OpenAI"} onValueChange={(v) => setForm((p) => ({ ...p, protocol_type: v || "OpenAI" }))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="OpenAI">OpenAI</SelectItem>
<SelectItem value="Anthropic">Anthropic</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>API *</Label>
<Input value={form.api_base || ""} onChange={(e) => setForm((p) => ({ ...p, api_base: e.target.value }))} placeholder="如:https://api.openai.com/v1" required />
</div>
<div className="space-y-2">
<Label>API Key</Label>
<div className="relative">
<Input
type={showApiKey ? "text" : "password"}
value={form.api_key || ""}
onChange={(e) => setForm((p) => ({ ...p, api_key: e.target.value }))}
className="pr-10"
placeholder="不修改请留空"
/>
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600"
onClick={() => setShowApiKey((v) => !v)}
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label> (JSON)</Label>
<Textarea value={extraConfigText} onChange={(e) => setExtraConfigText(e.target.value)} className="min-h-[80px] font-mono text-xs" placeholder='{"timeout": "60"}' />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}></Button>
<Button type="submit" disabled={isSaving} className="bg-indigo-600 hover:bg-indigo-700 text-white">
{isSaving ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}
+113 -157
View File
@@ -3,188 +3,144 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Save, Loader2 } from "lucide-react";
import { api } from "@/lib/api";
interface LLMConfig {
id: string;
provider: string;
model: string;
api_key?: string;
api_base?: string;
is_active: boolean;
}
import { useAuthStore } from "@/store/authStore";
export function Settings() {
const [configId, setConfigId] = useState<string | null>(null);
const [apiKey, setApiKey] = useState('');
const [provider, setProvider] = useState('openai');
const [model, setModel] = useState('gpt-4-turbo');
const [baseUrl, setBaseUrl] = useState('https://api.openai.com/v1');
const [enableVoice, setEnableVoice] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { user, updateUser } = useAuthStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
useEffect(() => {
fetchConfig();
}, []);
const fetchConfig = async () => {
setIsLoading(true);
try {
const configs = await api.get<LLMConfig[]>('/api/v1/llm');
const activeConfig = configs.find(c => c.is_active) || configs[0];
if (activeConfig) {
setConfigId(activeConfig.id);
setProvider(activeConfig.provider);
setModel(activeConfig.model);
setApiKey(activeConfig.api_key || '');
setBaseUrl(activeConfig.api_base || '');
}
} catch (error) {
console.error("Failed to fetch LLM config", error);
} finally {
setIsLoading(false);
if (user) {
setEmail(user.email || '');
}
};
}, [user]);
const isPasswordMismatch = password !== '' && confirmPassword !== '' && password !== confirmPassword;
const handleSave = async () => {
setError('');
setSuccess('');
if (isPasswordMismatch) {
setError("两次输入的密码不一致");
return;
}
setIsSaving(true);
try {
const configData = {
provider,
model,
api_key: apiKey,
api_base: baseUrl,
is_active: true
const updateData: any = {
email: email
};
if (configId) {
await api.put(`/api/v1/llm/${configId}`, configData);
} else {
const newId = Date.now().toString();
await api.post('/api/v1/llm', { ...configData, id: newId });
setConfigId(newId);
if (password) {
updateData.password = password;
}
alert("Settings saved successfully!");
} catch (error) {
if (user && user.id) {
const response = await api.put<any>(`/api/v1/users/${user.id}`, updateData);
let successMsg = "个人设置保存成功!";
if (password) {
successMsg = "个人设置及密码修改成功!";
}
setSuccess(successMsg);
setPassword('');
setConfirmPassword('');
// Update global state with new email
updateUser({ email: response.email });
}
} catch (error: any) {
console.error("Failed to save settings", error);
alert("Failed to save settings");
setError(error.message || "保存设置失败");
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<div className="h-full flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="p-6 h-full overflow-y-auto">
<div className="mb-6">
<h1 className="text-2xl font-bold">Settings</h1>
<p className="text-muted-foreground">Configure AI model and application preferences</p>
<div className="flex-1 flex flex-col h-full bg-zinc-50/30 overflow-hidden">
<div className="h-14 px-6 flex items-center justify-between border-b border-zinc-100 bg-white">
<div className="flex items-center gap-2 text-zinc-700 font-medium">
<Save className="h-5 w-5 text-indigo-500" />
</div>
</div>
<div className="grid gap-6 max-w-2xl">
<Card>
<CardHeader>
<CardTitle>LLM Configuration</CardTitle>
<CardDescription>Manage your Large Language Model settings</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="provider">Provider</Label>
<Select value={provider} onValueChange={(val) => val && setProvider(val)}>
<SelectTrigger>
<SelectValue placeholder="Select provider" />
</SelectTrigger>
<SelectContent>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem>
<SelectItem value="azure">Azure OpenAI</SelectItem>
<SelectItem value="local">Local (Ollama/LM Studio)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="model">Model</Label>
<Select value={model} onValueChange={(val) => val && setModel(val)}>
<SelectTrigger>
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
<SelectItem value="gpt-4o">GPT-4o</SelectItem>
<SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
<SelectItem value="claude-3-opus">Claude 3 Opus</SelectItem>
<SelectItem value="claude-3-sonnet">Claude 3 Sonnet</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="api-key">API Key</Label>
<Input
id="api-key"
type="password"
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="base-url">Base URL (Optional)</Label>
<Input
id="base-url"
placeholder="https://api.openai.com/v1"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Interface Settings</CardTitle>
<CardDescription>Customize your experience</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="voice-mode">Voice Mode</Label>
<p className="text-sm text-muted-foreground">Enable voice input and output</p>
<div className="flex-1 p-6 overflow-auto">
<div className="grid gap-6 max-w-2xl mx-auto">
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded-md p-3">{error}</div>}
{success && <div className="text-sm text-emerald-600 bg-emerald-50 border border-emerald-100 rounded-md p-3">{success}</div>}
<Card className="border-zinc-200 shadow-sm">
<CardHeader>
<CardTitle className="text-xl"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username"></Label>
<Input
id="username"
value={user?.username || ''}
disabled
className="bg-zinc-50 text-zinc-500"
/>
<p className="text-xs text-zinc-400"></p>
</div>
<Switch
id="voice-mode"
checked={enableVoice}
onCheckedChange={setEnableVoice}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="dark-mode">Dark Mode</Label>
<p className="text-sm text-muted-foreground">Toggle dark/light theme</p>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<Switch id="dark-mode" defaultChecked />
</div>
</CardContent>
<CardFooter>
<Button onClick={handleSave} className="ml-auto" disabled={isSaving}>
{isSaving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
Save Changes
</Button>
</CardFooter>
</Card>
<div className="space-y-2 pt-4 border-t border-zinc-100">
<Label htmlFor="new-password"></Label>
<Input
id="new-password"
type="password"
placeholder="如不修改请留空"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
}}
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password"></Label>
<Input
id="confirm-password"
type="password"
placeholder="如不修改请留空"
value={confirmPassword}
onChange={(e) => {
setConfirmPassword(e.target.value);
setError('');
}}
/>
{isPasswordMismatch && <p className="text-sm text-red-600"></p>}
</div>
</CardContent>
<CardFooter className="bg-zinc-50/50 border-t border-zinc-100 pt-6">
<Button onClick={handleSave} className="ml-auto bg-indigo-600 hover:bg-indigo-700 text-white" disabled={isSaving || isPasswordMismatch}>
{isSaving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
</Button>
</CardFooter>
</Card>
</div>
</div>
</div>
);
+22 -22
View File
@@ -87,7 +87,7 @@ export function Users() {
} else {
// Create
if (!formData.password) {
setError("Password is required for new users");
setError("新建用户必须填写密码");
return;
}
await api.post("/api/v1/users", formData);
@@ -95,12 +95,12 @@ export function Users() {
setIsDialogOpen(false);
fetchUsers();
} catch (err: any) {
setError(err.message || "An error occurred");
setError(err.message || "发生错误");
}
};
const handleDelete = async (id: number) => {
if (window.confirm("Are you sure you want to delete this user?")) {
if (window.confirm("确认删除该用户吗?")) {
try {
await api.delete(`/api/v1/users/${id}`);
fetchUsers();
@@ -115,22 +115,22 @@ export function Users() {
<div className="h-14 px-6 flex items-center justify-between border-b border-zinc-100 bg-white">
<div className="flex items-center gap-2 text-zinc-700 font-medium">
<UsersIcon className="h-5 w-5 text-indigo-500" />
User Management
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger className="inline-flex items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-8 bg-indigo-600 hover:bg-indigo-700 text-white rounded-md px-3" onClick={() => handleOpenDialog()}>
<Plus className="h-4 w-4" />
Add User
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>{editingUser ? "Edit User" : "Add New User"}</DialogTitle>
<DialogTitle>{editingUser ? "编辑用户" : "添加新用户"}</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
{error && <div className="text-red-500 text-sm">{error}</div>}
<div className="grid gap-2">
<Label htmlFor="username">Username</Label>
<Label htmlFor="username"></Label>
<Input
id="username"
value={formData.username}
@@ -139,7 +139,7 @@ export function Users() {
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
@@ -150,7 +150,7 @@ export function Users() {
</div>
{!editingUser && (
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
@@ -161,7 +161,7 @@ export function Users() {
</div>
)}
<div className="flex items-center justify-between">
<Label htmlFor="is_active">Active</Label>
<Label htmlFor="is_active"></Label>
<Switch
id="is_active"
checked={formData.is_active}
@@ -169,7 +169,7 @@ export function Users() {
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="is_admin">Admin</Label>
<Label htmlFor="is_admin"></Label>
<Switch
id="is_admin"
checked={formData.is_admin}
@@ -179,10 +179,10 @@ export function Users() {
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white">
Save
</Button>
</DialogFooter>
</form>
@@ -201,19 +201,19 @@ export function Users() {
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Username</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead>Role</TableHead>
<TableHead>Created At</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center h-24 text-zinc-500">
No users found.
</TableCell>
</TableRow>
) : (
@@ -224,12 +224,12 @@ export function Users() {
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${user.is_active ? 'bg-emerald-100 text-emerald-700' : 'bg-zinc-100 text-zinc-600'}`}>
{user.is_active ? 'Active' : 'Inactive'}
{user.is_active ? '正常' : '禁用'}
</span>
</TableCell>
<TableCell>
<span className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${user.is_admin ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'}`}>
{user.is_admin ? 'Admin' : 'User'}
{user.is_admin ? '管理员' : '普通用户'}
</span>
</TableCell>
<TableCell className="text-zinc-500">
+8
View File
@@ -12,6 +12,7 @@ interface AuthState {
token: string | null;
isAuthenticated: boolean;
login: (user: User, token: string) => void;
updateUser: (user: Partial<User>) => void;
logout: () => void;
}
@@ -24,6 +25,13 @@ export const useAuthStore = create<AuthState>((set) => ({
localStorage.setItem('token', token);
set({ user, token, isAuthenticated: true });
},
updateUser: (updatedUser) => set((state) => {
const user = state.user ? { ...state.user, ...updatedUser } : null;
if (user) {
localStorage.setItem('user', JSON.stringify(user));
}
return { user };
}),
logout: () => {
localStorage.removeItem('user');
localStorage.removeItem('token');