feat: add n18n
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout/legacy';
|
||||
import { useDashboardStore } from '../store/dashboardStore';
|
||||
import { useProjectStore } from '../store/projectStore';
|
||||
@@ -43,6 +44,7 @@ function inferChartKeys(data: Record<string, unknown>[]) {
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
const { t } = useTranslation();
|
||||
const { charts, removeChart, updateLayout, loadCharts } = useDashboardStore();
|
||||
const { currentProject } = useProjectStore();
|
||||
|
||||
@@ -79,7 +81,7 @@ export function Dashboard() {
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<p>请选择一个项目以查看仪表板。</p>
|
||||
<p>{t('selectProjectToViewDashboard')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -87,8 +89,8 @@ export function Dashboard() {
|
||||
if (charts.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<p>当前项目暂无图表。</p>
|
||||
<p className="text-sm">前往对话页并添加可视化结果!</p>
|
||||
<p>{t('noChartsInCurrentProject')}</p>
|
||||
<p className="text-sm">{t('goToChatToAddCharts')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -119,7 +121,7 @@ export function Dashboard() {
|
||||
<CardTitle className="text-base">{chart.title}</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{chart.type === "table"
|
||||
? `TABLE · ${rows.length} 行 · ${columns.length} 列`
|
||||
? t('tableRowColDesc', { rowCount: rows.length, colCount: columns.length })
|
||||
: `${chart.type.toUpperCase()} Chart`}
|
||||
</CardDescription>
|
||||
</div>
|
||||
@@ -152,7 +154,7 @@ export function Dashboard() {
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col gap-2">
|
||||
<div className="text-[11px] text-zinc-500 px-1">
|
||||
{isTableTruncated ? `预览前 ${TABLE_PREVIEW_LIMIT} 行 / 共 ${rows.length} 行,${columns.length} 列` : `共 ${rows.length} 行,${columns.length} 列`}
|
||||
{isTableTruncated ? t('previewTableRows', { previewLimit: TABLE_PREVIEW_LIMIT, rowCount: rows.length, colCount: columns.length }) : t('totalTableRows', { rowCount: rows.length, colCount: columns.length })}
|
||||
</div>
|
||||
<ScrollArea className="flex-1 w-full border rounded-md">
|
||||
<Table>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from "@/lib/api";
|
||||
import { DataSourceForm, type DataSourceConfig } from "@/components/DataSourceForm";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Database, Pencil, Trash2, Loader2, FolderOpen, Info, ChevronLeft, FileText, Search, Network } from "lucide-react";
|
||||
import { Plus, Database, Pencil, Trash2, Loader2, Info, ChevronLeft, FileText, Search, Network } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -30,13 +30,13 @@ const SOURCE_TYPES = [
|
||||
];
|
||||
|
||||
export function DataSources() {
|
||||
const { t } = useTranslation();
|
||||
const [datasources, setDatasources] = useState<DataSourceConfig[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [view, setView] = useState<"list" | "select-type">("list");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editingDs, setEditingDs] = useState<DataSourceConfig | null>(null);
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null);
|
||||
const { user } = useAuthStore();
|
||||
const { currentProject } = useProjectStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -77,7 +77,7 @@ export function DataSources() {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!window.confirm("确定要删除这个数据源吗?")) return;
|
||||
if (!window.confirm(t('confirmDeleteDataSource'))) return;
|
||||
try {
|
||||
await api.delete(`/api/v1/datasources/${id}`);
|
||||
fetchDataSources();
|
||||
@@ -98,7 +98,7 @@ export function DataSources() {
|
||||
fetchDataSources();
|
||||
} catch (e) {
|
||||
console.error("Failed to save data source", e);
|
||||
alert("保存失败: " + (e as any).message);
|
||||
alert(t('saveFailed') + (e as any).message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,7 +121,7 @@ export function DataSources() {
|
||||
className="flex items-center text-zinc-500 hover:text-zinc-800 transition-colors mb-6 group"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1 group-hover:-translate-x-0.5 transition-transform" />
|
||||
返回列表
|
||||
{t('backToList')}
|
||||
</button>
|
||||
|
||||
<h1 className="text-2xl font-semibold text-zinc-800 mb-6">Connect an external data source</h1>
|
||||
@@ -158,7 +158,7 @@ export function DataSources() {
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingDs ? "编辑数据源" : `新建 ${SOURCE_TYPES.find(t => t.id === selectedType)?.name || ""} 数据源`}
|
||||
{editingDs ? t('editDataSource') : t('createNewDataSourceWithType', { type: SOURCE_TYPES.find(t => t.id === selectedType)?.name || "" })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
@@ -179,12 +179,12 @@ export function DataSources() {
|
||||
<div className="h-full flex flex-col bg-white">
|
||||
<div className="border-b border-zinc-100 px-8 py-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-zinc-900">数据源配置</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">管理可用于问答的数据源连接</p>
|
||||
<h1 className="text-2xl font-bold text-zinc-900">{t('dataSourceConfig')}</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">{t('manageDataSourceConnections')}</p>
|
||||
</div>
|
||||
<Button onClick={handleCreate} className="bg-indigo-600 hover:bg-indigo-700 text-white gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
新建数据源
|
||||
{t('newDataSource')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -196,8 +196,8 @@ export function DataSources() {
|
||||
) : datasources.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 border-2 border-dashed border-zinc-200 rounded-xl bg-zinc-50/50">
|
||||
<Database className="h-10 w-10 text-zinc-300 mb-3" />
|
||||
<p className="text-zinc-500 font-medium">暂无数据源</p>
|
||||
<p className="text-zinc-400 text-sm mt-1">点击右上角按钮添加第一个数据源</p>
|
||||
<p className="text-zinc-500 font-medium">{t('noDataSources')}</p>
|
||||
<p className="text-zinc-400 text-sm mt-1">{t('clickTopRightToAddFirstDataSource')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@@ -256,7 +256,7 @@ export function DataSources() {
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingDs ? `编辑 ${SOURCE_TYPES.find(t => t.id === editingDs.type)?.name || editingDs.type} 数据源` : `新建 ${SOURCE_TYPES.find(t => t.id === selectedType)?.name || ""} 数据源`}
|
||||
{editingDs ? t('editDataSourceWithType', { type: SOURCE_TYPES.find(t => t.id === editingDs.type)?.name || editingDs.type }) : t('createNewDataSourceWithType', { type: SOURCE_TYPES.find(t => t.id === selectedType)?.name || "" })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -32,6 +33,7 @@ const defaultForm: Omit<ModelConfig, "id"> = {
|
||||
};
|
||||
|
||||
export function ModelConfigs() {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuthStore();
|
||||
const isAdmin = !!user?.is_admin;
|
||||
const [configs, setConfigs] = useState<ModelConfig[]>([]);
|
||||
@@ -99,7 +101,7 @@ export function ModelConfigs() {
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!form.model || !form.provider) {
|
||||
setError("请先填写必要信息(供应商、模型ID)");
|
||||
setError(t('fillRequiredInfoFirst'));
|
||||
return;
|
||||
}
|
||||
setIsTesting(true);
|
||||
@@ -111,7 +113,7 @@ export function ModelConfigs() {
|
||||
const parsed = JSON.parse(extraConfigText);
|
||||
if (parsed && typeof parsed === "object") extraHeaders = parsed;
|
||||
} catch (err) {
|
||||
setError("额外配置必须是有效的JSON");
|
||||
setError(t('extraConfigMustBeValidJson'));
|
||||
setIsTesting(false);
|
||||
return;
|
||||
}
|
||||
@@ -126,9 +128,9 @@ export function ModelConfigs() {
|
||||
};
|
||||
|
||||
await api.post("/api/v1/llm/test", payload);
|
||||
alert("连接测试成功!");
|
||||
alert(t('connectionTestSuccessful'));
|
||||
} catch (e: any) {
|
||||
setError(e.message || "连接测试失败");
|
||||
setError(e.message || t('connectionTestFailed'));
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
@@ -137,7 +139,7 @@ export function ModelConfigs() {
|
||||
const handleSave = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!form.model || !form.provider) {
|
||||
setError("请填写必填项");
|
||||
setError(t('fillRequiredFields'));
|
||||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
@@ -149,7 +151,7 @@ export function ModelConfigs() {
|
||||
const parsed = JSON.parse(extraConfigText);
|
||||
if (parsed && typeof parsed === "object") extraHeaders = parsed;
|
||||
} catch (err) {
|
||||
setError("额外配置必须是有效的JSON");
|
||||
setError(t('extraConfigMustBeValidJson'));
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
@@ -168,14 +170,14 @@ export function ModelConfigs() {
|
||||
setDialogOpen(false);
|
||||
await fetchConfigs();
|
||||
} catch (e: any) {
|
||||
setError(e.message || "保存配置失败");
|
||||
setError(e.message || t('failedToSaveConfig'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm("确认删除该模型吗?")) return;
|
||||
if (!window.confirm(t('confirmDeleteModel'))) return;
|
||||
try {
|
||||
await api.delete(`/api/v1/llm/${id}`);
|
||||
await fetchConfigs();
|
||||
@@ -197,7 +199,7 @@ export function ModelConfigs() {
|
||||
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 className="text-zinc-500 text-lg">{t('noPermissionAdminOnly')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -206,21 +208,17 @@ export function ModelConfigs() {
|
||||
<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>
|
||||
<Brain className="h-5 w-5 text-indigo-500" />{t('modelConfig')}</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" />
|
||||
<Input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder={t('searchModel')} 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>
|
||||
<Plus className="h-4 w-4 mr-1" />{t('addModel')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -234,19 +232,17 @@ export function ModelConfigs() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>模型名称</TableHead>
|
||||
<TableHead>供应商</TableHead>
|
||||
<TableHead>模型标识</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
<TableHead>{t('modelName')}</TableHead>
|
||||
<TableHead>{t('provider')}</TableHead>
|
||||
<TableHead>{t('modelIdentifier')}</TableHead>
|
||||
<TableHead>{t('status')}</TableHead>
|
||||
<TableHead className="text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredConfigs.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center h-24 text-zinc-500">
|
||||
暂无模型数据
|
||||
</TableCell>
|
||||
<TableCell colSpan={5} className="text-center h-24 text-zinc-500">{t('noModelData')}</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredConfigs.map((item) => (
|
||||
@@ -260,9 +256,9 @@ export function ModelConfigs() {
|
||||
<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 ? "当前默认模型" : "点击设为默认"}
|
||||
title={item.is_active ? t('currentDefaultModel') : t('clickToSetDefault')}
|
||||
>
|
||||
{item.is_active ? '默认' : '设为默认'}
|
||||
{item.is_active ? t('default') : t('setDefault')}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
@@ -296,18 +292,18 @@ export function ModelConfigs() {
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<form onSubmit={handleSave}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? "编辑模型" : "添加模型"}</DialogTitle>
|
||||
<DialogTitle>{editingId ? t('editModel') : t('addModel')}</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" />
|
||||
<Label>{t('modelName')}</Label>
|
||||
<Input value={form.name || ""} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} placeholder={t('egGpt4')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>供应商 *</Label>
|
||||
<Label>{t('providerRequired')}</Label>
|
||||
<Select value={form.provider} onValueChange={(v) => setForm((p) => ({ ...p, provider: v || "openai" }))}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
@@ -318,10 +314,10 @@ export function ModelConfigs() {
|
||||
<SelectItem value="gemini">Google AI Studio (Gemini)</SelectItem>
|
||||
<SelectItem value="bedrock">AWS Bedrock</SelectItem>
|
||||
<SelectItem value="deepseek">DeepSeek</SelectItem>
|
||||
<SelectItem value="zhipuai">ZhipuAI (智谱)</SelectItem>
|
||||
<SelectItem value="zhipuai">{t('zhipuAi')}</SelectItem>
|
||||
<SelectItem value="moonshot">Moonshot (Kimi)</SelectItem>
|
||||
<SelectItem value="dashscope">DashScope (通义千问)</SelectItem>
|
||||
<SelectItem value="volcengine">Volcengine (火山引擎)</SelectItem>
|
||||
<SelectItem value="dashscope">{t('dashScope')}</SelectItem>
|
||||
<SelectItem value="volcengine">{t('volcengine')}</SelectItem>
|
||||
<SelectItem value="groq">Groq</SelectItem>
|
||||
<SelectItem value="cohere">Cohere</SelectItem>
|
||||
<SelectItem value="mistral">Mistral</SelectItem>
|
||||
@@ -336,12 +332,12 @@ export function ModelConfigs() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>模型ID *</Label>
|
||||
<Input value={form.model || ""} onChange={(e) => setForm((p) => ({ ...p, model: e.target.value }))} placeholder="如:gpt-4-turbo" required />
|
||||
<Label>{t('modelIdRequired')}</Label>
|
||||
<Input value={form.model || ""} onChange={(e) => setForm((p) => ({ ...p, model: e.target.value }))} placeholder={t('egGpt4Turbo')} required />
|
||||
</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" />
|
||||
<Label>{t('apiDomain')}</Label>
|
||||
<Input value={form.api_base || ""} onChange={(e) => setForm((p) => ({ ...p, api_base: e.target.value }))} placeholder={t('egApiDomain')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -353,7 +349,7 @@ export function ModelConfigs() {
|
||||
value={form.api_key || ""}
|
||||
onChange={(e) => setForm((p) => ({ ...p, api_key: e.target.value }))}
|
||||
className="pr-10"
|
||||
placeholder="不修改请留空"
|
||||
placeholder={t('leaveBlankIfNotModifying')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -366,7 +362,7 @@ export function ModelConfigs() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>额外配置 (JSON)</Label>
|
||||
<Label>{t('extraConfigJson')}</Label>
|
||||
<Textarea value={extraConfigText} onChange={(e) => setExtraConfigText(e.target.value)} className="min-h-[80px] font-mono text-xs" placeholder='{"timeout": "60"}' />
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,7 +372,7 @@ export function ModelConfigs() {
|
||||
测试连接
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>取消</Button>
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>{t('cancel')}</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}
|
||||
保存
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Folder, Pencil, Trash2, Loader2, Database } from 'lucide-react';
|
||||
import { useProjectStore, type Project } from '@/store/projectStore';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -11,6 +12,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function Projects() {
|
||||
const { t } = useTranslation();
|
||||
const { projects, loading, fetchProjects, addProject, updateProject, deleteProject, setCurrentProject } = useProjectStore();
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||
@@ -53,7 +55,7 @@ export function Projects() {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!window.confirm('Are you sure you want to delete this project? All associated data sources will be deleted.')) return;
|
||||
if (!window.confirm(t('confirmDeleteProject'))) return;
|
||||
try {
|
||||
await deleteProject(id);
|
||||
} catch (error) {
|
||||
@@ -76,44 +78,40 @@ export function Projects() {
|
||||
<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">
|
||||
<Folder className="h-5 w-5 text-blue-500" />
|
||||
项目管理
|
||||
</div>
|
||||
<Folder className="h-5 w-5 text-blue-500" />{t('projectManagement')}</div>
|
||||
<Button onClick={() => {
|
||||
setFormData({ name: '', description: '' });
|
||||
setIsCreateDialogOpen(true);
|
||||
}} size="sm" className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
新建项目
|
||||
</Button>
|
||||
<Plus className="h-4 w-4" />{t('newProject')}</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 overflow-auto">
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
<Card className="border-zinc-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>项目列表</CardTitle>
|
||||
<CardDescription>管理您的项目,不同项目拥有独立的数据源</CardDescription>
|
||||
<CardTitle>{t('projectList')}</CardTitle>
|
||||
<CardDescription>{t('manageProjectsDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && projects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-zinc-400">
|
||||
<Loader2 className="h-8 w-8 animate-spin mb-4" />
|
||||
<p>加载中...</p>
|
||||
<p>{t('loading')}</p>
|
||||
</div>
|
||||
) : projects.length === 0 ? (
|
||||
<div className="text-center py-12 border-2 border-dashed rounded-lg border-zinc-100">
|
||||
<Folder className="h-12 w-12 text-zinc-200 mx-auto mb-4" />
|
||||
<p className="text-zinc-500">暂无项目,请先创建一个</p>
|
||||
<p className="text-zinc-500">{t('noProjectsCreateOne')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
<TableHead>{t('name')}</TableHead>
|
||||
<TableHead>{t('description')}</TableHead>
|
||||
<TableHead>{t('createdAt')}</TableHead>
|
||||
<TableHead className="text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -132,7 +130,7 @@ export function Projects() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => goToDataSources(project)}
|
||||
title="管理数据源"
|
||||
title={t('manageDataSources')}
|
||||
>
|
||||
<Database className="h-4 w-4 text-emerald-500" />
|
||||
</Button>
|
||||
@@ -140,7 +138,7 @@ export function Projects() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => openEditDialog(project)}
|
||||
title="编辑项目"
|
||||
title={t('editProject')}
|
||||
>
|
||||
<Pencil className="h-4 w-4 text-blue-500" />
|
||||
</Button>
|
||||
@@ -148,7 +146,7 @@ export function Projects() {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(project.id)}
|
||||
title="删除项目"
|
||||
title={t('deleteProject')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
@@ -168,32 +166,32 @@ export function Projects() {
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建项目</DialogTitle>
|
||||
<DialogTitle>{t('newProject')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">项目名称</Label>
|
||||
<Label htmlFor="name">{t('projectName')}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="输入项目名称"
|
||||
placeholder={t('enterProjectName')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">描述 (可选)</Label>
|
||||
<Label htmlFor="description">{t('descriptionOptional')}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="输入项目描述"
|
||||
placeholder={t('enterProjectDescription')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>取消</Button>
|
||||
<Button variant="outline" onClick={() => setIsCreateDialogOpen(false)}>{t('cancel')}</Button>
|
||||
<Button onClick={handleCreate} disabled={isSubmitting || !formData.name.trim()}>
|
||||
{isSubmitting ? '创建中...' : '创建'}
|
||||
{isSubmitting ? t('creating') : t('create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -203,32 +201,32 @@ export function Projects() {
|
||||
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑项目</DialogTitle>
|
||||
<DialogTitle>{t('editProject')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">项目名称</Label>
|
||||
<Label htmlFor="edit-name">{t('projectName')}</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="输入项目名称"
|
||||
placeholder={t('enterProjectName')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-description">描述 (可选)</Label>
|
||||
<Label htmlFor="edit-description">{t('descriptionOptional')}</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="输入项目描述"
|
||||
placeholder={t('enterProjectDescription')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>取消</Button>
|
||||
<Button variant="outline" onClick={() => setIsEditDialogOpen(false)}>{t('cancel')}</Button>
|
||||
<Button onClick={handleUpdate} disabled={isSubmitting || !formData.name.trim()}>
|
||||
{isSubmitting ? '保存中...' : '保存'}
|
||||
{isSubmitting ? t('saving') : t('save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -8,6 +9,7 @@ import { api } from "@/lib/api";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation();
|
||||
const { user, updateUser } = useAuthStore();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -28,7 +30,7 @@ export function Settings() {
|
||||
setSuccess('');
|
||||
|
||||
if (isPasswordMismatch) {
|
||||
setError("两次输入的密码不一致");
|
||||
setError(t('passwordsDoNotMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,9 +46,9 @@ export function Settings() {
|
||||
|
||||
if (user && user.id) {
|
||||
const response = await api.put<any>(`/api/v1/users/${user.id}`, updateData);
|
||||
let successMsg = "个人设置保存成功!";
|
||||
let successMsg = t('personalSettingsSaved');
|
||||
if (password) {
|
||||
successMsg = "个人设置及密码修改成功!";
|
||||
successMsg = t('personalSettingsAndPasswordSaved');
|
||||
}
|
||||
setSuccess(successMsg);
|
||||
setPassword('');
|
||||
@@ -57,7 +59,7 @@ export function Settings() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to save settings", error);
|
||||
setError(error.message || "保存设置失败");
|
||||
setError(error.message || t('failedToSaveSettings'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -79,23 +81,23 @@ export function Settings() {
|
||||
|
||||
<Card className="border-zinc-200 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">账号信息</CardTitle>
|
||||
<CardDescription>修改您的登录邮箱和密码</CardDescription>
|
||||
<CardTitle className="text-xl">{t('accountInfo')}</CardTitle>
|
||||
<CardDescription>{t('modifyLoginEmailAndPassword')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">用户名</Label>
|
||||
<Label htmlFor="username">{t('username')}</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={user?.username || ''}
|
||||
disabled
|
||||
className="bg-zinc-50 text-zinc-500"
|
||||
/>
|
||||
<p className="text-xs text-zinc-400">用户名不可修改</p>
|
||||
<p className="text-xs text-zinc-400">{t('usernameCannotBeModified')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">邮箱地址</Label>
|
||||
<Label htmlFor="email">{t('emailAddress')}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
@@ -105,11 +107,11 @@ export function Settings() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-4 border-t border-zinc-100">
|
||||
<Label htmlFor="new-password">新密码</Label>
|
||||
<Label htmlFor="new-password">{t('newPassword')}</Label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
placeholder="如不修改请留空"
|
||||
placeholder={t('leaveBlankIfNotModifying')}
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
@@ -119,18 +121,18 @@ export function Settings() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirm-password">确认新密码</Label>
|
||||
<Label htmlFor="confirm-password">{t('confirmNewPassword')}</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
placeholder="如不修改请留空"
|
||||
placeholder={t('leaveBlankIfNotModifying')}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => {
|
||||
setConfirmPassword(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
/>
|
||||
{isPasswordMismatch && <p className="text-sm text-red-600">两次输入的密码不一致</p>}
|
||||
{isPasswordMismatch && <p className="text-sm text-red-600">{t('passwordsDoNotMatch')}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="bg-zinc-50/50 border-t border-zinc-100 pt-6">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Trash2, Edit2, Plus, Terminal, Loader2, FolderOpen, Share2, Download, Eye, ShieldCheck, AlertCircle, Wand2, Upload } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Trash2, Terminal, Loader2, FolderOpen, Eye, ShieldCheck, AlertCircle, Wand2, Upload } from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
@@ -28,11 +27,12 @@ interface Skill {
|
||||
}
|
||||
|
||||
export function Skills() {
|
||||
const { t } = useTranslation();
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingSkill, setEditingSkill] = useState<Skill | null>(null);
|
||||
const [newSkill, setNewSkill] = useState<Partial<Skill>>({ type: 'python', content: '', source: '本地导入', status: '安全' });
|
||||
const [newSkill, setNewSkill] = useState<Partial<Skill>>({ type: 'python', content: '', source: t('localImport'), status: t('safe') });
|
||||
const { currentProject } = useProjectStore();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -69,8 +69,8 @@ export function Skills() {
|
||||
await fetchSkills();
|
||||
} catch (error: any) {
|
||||
console.error("Failed to upload skill", error);
|
||||
const errorMessage = error.response?.data?.detail || error.message || "未知错误";
|
||||
alert("上传失败: " + errorMessage);
|
||||
const errorMessage = error.response?.data?.detail || error.message || t('unknownError');
|
||||
alert(t('uploadFailed') + ': ' + errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
@@ -95,7 +95,7 @@ export function Skills() {
|
||||
await api.post<Skill>('/api/v1/skills', skillToCreate);
|
||||
}
|
||||
await fetchSkills();
|
||||
setNewSkill({ type: 'python', content: '', source: '本地导入', status: '安全' });
|
||||
setNewSkill({ type: 'python', content: '', source: t('localImport'), status: t('safe') });
|
||||
setEditingSkill(null);
|
||||
setIsDialogOpen(false);
|
||||
} catch (error) {
|
||||
@@ -112,7 +112,7 @@ export function Skills() {
|
||||
|
||||
const handleDeleteSkill = async (id: string) => {
|
||||
if (!currentProject) return;
|
||||
if (!window.confirm("确定要删除这个技能吗?")) return;
|
||||
if (!window.confirm(t('confirmDeleteSkill'))) return;
|
||||
try {
|
||||
await api.delete(`/api/v1/skills/${id}?project_id=${currentProject.id}`);
|
||||
setSkills(skills.filter(s => s.id !== id));
|
||||
@@ -125,7 +125,7 @@ export function Skills() {
|
||||
return (
|
||||
<div className="h-full flex flex-col items-center justify-center text-zinc-500 gap-4">
|
||||
<FolderOpen className="h-12 w-12 text-zinc-200" />
|
||||
<p>请先在顶部选择一个项目以管理其技能</p>
|
||||
<p>{t('selectProjectToManageSkills')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -135,10 +135,8 @@ export function Skills() {
|
||||
<div className="border-b border-zinc-100 px-8 py-5 flex items-center justify-between bg-white shrink-0">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-zinc-900 flex items-center gap-2">
|
||||
< Wand2 className="h-6 w-6 text-indigo-500" />
|
||||
Skills 仓库 - {currentProject.name}
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">管理该项目的 AI 技能和工具,支持符合 agentskills.io 标准的文件上传</p>
|
||||
< Wand2 className="h-6 w-6 text-indigo-500" />{t('skillsRepository', { project: currentProject.name })}</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">{t('manageAiSkillsDesc')}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
@@ -152,9 +150,7 @@ export function Skills() {
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white gap-2"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
上传 Skill
|
||||
</Button>
|
||||
<Upload className="h-4 w-4" />{t('uploadSkill')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,11 +159,11 @@ export function Skills() {
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader className="bg-zinc-50/50">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="w-[40%] font-semibold text-zinc-700 py-3 px-4 text-sm">名称</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm">来源</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-center">安装时间</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-center">状态</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-right">操作</TableHead>
|
||||
<TableHead className="w-[40%] font-semibold text-zinc-700 py-3 px-4 text-sm">{t('name')}</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm">{t('source')}</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-center">{t('installationTime')}</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-center">{t('status')}</TableHead>
|
||||
<TableHead className="w-[15%] font-semibold text-zinc-700 py-3 px-4 text-sm text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -214,11 +210,11 @@ export function Skills() {
|
||||
</TableCell>
|
||||
<TableCell className="py-4 px-4 text-center">
|
||||
<div className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] md:text-xs font-medium whitespace-nowrap ${
|
||||
skill.status === '安全'
|
||||
skill.status === t('safe')
|
||||
? 'bg-green-50 text-green-700 border border-green-100'
|
||||
: 'bg-amber-50 text-amber-700 border border-amber-100'
|
||||
}`}>
|
||||
{skill.status === '安全' ? (
|
||||
{skill.status === t('safe') ? (
|
||||
<ShieldCheck className="h-3 w-3" />
|
||||
) : (
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
@@ -259,7 +255,7 @@ export function Skills() {
|
||||
<div className="p-4 bg-zinc-50 rounded-2xl">
|
||||
<Terminal className="h-10 w-10 opacity-20" />
|
||||
</div>
|
||||
<p className="text-sm">该项目尚无技能,点击“导入 Skill”开始</p>
|
||||
<p className="text-sm">{t('noSkillsInProjectClickImport')}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -275,20 +271,20 @@ export function Skills() {
|
||||
setIsDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingSkill(null);
|
||||
setNewSkill({ type: 'python', content: '', source: '本地导入', status: '安全' });
|
||||
setNewSkill({ type: 'python', content: '', source: t('localImport'), status: t('safe') });
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] flex flex-col rounded-2xl p-0 overflow-hidden">
|
||||
<DialogHeader className="p-6 pb-2">
|
||||
<DialogTitle className="text-xl font-bold text-zinc-900">{editingSkill ? '查看/编辑技能' : '添加新技能'}</DialogTitle>
|
||||
<DialogTitle className="text-xl font-bold text-zinc-900">{editingSkill ? t('viewOrEditSkill') : t('addNewSkill')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-2">
|
||||
<div className="grid gap-5">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="name" className="text-zinc-600 font-medium text-sm">名称</Label>
|
||||
<Label htmlFor="name" className="text-zinc-600 font-medium text-sm">{t('name')}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="技能名称"
|
||||
placeholder={t('skillName')}
|
||||
value={newSkill.name || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, name: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 h-10"
|
||||
@@ -297,14 +293,14 @@ export function Skills() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="type" className="text-zinc-600 font-medium text-sm">类型</Label>
|
||||
<Label htmlFor="type" className="text-zinc-600 font-medium text-sm">{t('type')}</Label>
|
||||
<Select
|
||||
value={newSkill.type}
|
||||
onValueChange={(val: any) => setNewSkill({...newSkill, type: val})}
|
||||
disabled={editingSkill?.is_builtin}
|
||||
>
|
||||
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
||||
<SelectValue placeholder="选择类型" />
|
||||
<SelectValue placeholder={t('selectType')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-lg">
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
@@ -314,27 +310,27 @@ export function Skills() {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="status" className="text-zinc-600 font-medium text-sm">状态</Label>
|
||||
<Label htmlFor="status" className="text-zinc-600 font-medium text-sm">{t('status')}</Label>
|
||||
<Select
|
||||
value={newSkill.status}
|
||||
onValueChange={(val: any) => setNewSkill({...newSkill, status: val})}
|
||||
disabled={editingSkill?.is_builtin}
|
||||
>
|
||||
<SelectTrigger className="rounded-lg border-zinc-200 h-10">
|
||||
<SelectValue placeholder="选择状态" />
|
||||
<SelectValue placeholder={t('selectStatus')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-lg">
|
||||
<SelectItem value="安全">安全</SelectItem>
|
||||
<SelectItem value="低风险">低风险</SelectItem>
|
||||
<SelectItem value={t('safe')}>{t('safe')}</SelectItem>
|
||||
<SelectItem value={t('lowRisk')}>{t('lowRisk')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="description" className="text-zinc-600 font-medium text-sm">描述</Label>
|
||||
<Label htmlFor="description" className="text-zinc-600 font-medium text-sm">{t('description')}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="简要描述技能的功能..."
|
||||
placeholder={t('brieflyDescribeSkillFunction')}
|
||||
value={newSkill.description || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, description: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 min-h-[80px] py-2 text-sm"
|
||||
@@ -342,13 +338,13 @@ export function Skills() {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="content" className="text-zinc-600 font-medium text-sm">内容</Label>
|
||||
<Label htmlFor="content" className="text-zinc-600 font-medium text-sm">{t('content')}</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={newSkill.content || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, content: e.target.value})}
|
||||
className="rounded-lg border-zinc-200 font-mono text-xs min-h-[160px] py-3 bg-zinc-50"
|
||||
placeholder="Python 代码、SQL 查询模板或 API 规范..."
|
||||
placeholder={t('pythonSqlApiContentPlaceholder')}
|
||||
disabled={editingSkill?.is_builtin}
|
||||
/>
|
||||
</div>
|
||||
@@ -356,9 +352,7 @@ export function Skills() {
|
||||
</div>
|
||||
<DialogFooter className="p-6 pt-2">
|
||||
{!editingSkill?.is_builtin && (
|
||||
<Button onClick={handleAddSkill} className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg px-6 h-10 w-full">
|
||||
保存技能
|
||||
</Button>
|
||||
<Button onClick={handleAddSkill} className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg px-6 h-10 w-full">{t('saveSkill')}</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -18,6 +19,7 @@ interface User {
|
||||
}
|
||||
|
||||
export function Users() {
|
||||
const { t } = useTranslation();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
@@ -87,7 +89,7 @@ export function Users() {
|
||||
} else {
|
||||
// Create
|
||||
if (!formData.password) {
|
||||
setError("新建用户必须填写密码");
|
||||
setError(t('newUserMustHavePassword'));
|
||||
return;
|
||||
}
|
||||
await api.post("/api/v1/users", formData);
|
||||
@@ -95,12 +97,12 @@ export function Users() {
|
||||
setIsDialogOpen(false);
|
||||
fetchUsers();
|
||||
} catch (err: any) {
|
||||
setError(err.message || "发生错误");
|
||||
setError(err.message || t('anErrorOccurred'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (window.confirm("确认删除该用户吗?")) {
|
||||
if (window.confirm(t('confirmDeleteUser'))) {
|
||||
try {
|
||||
await api.delete(`/api/v1/users/${id}`);
|
||||
fetchUsers();
|
||||
@@ -125,12 +127,12 @@ export function Users() {
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingUser ? "编辑用户" : "添加新用户"}</DialogTitle>
|
||||
<DialogTitle>{editingUser ? t('editUser') : t('addNewUser')}</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">用户名</Label>
|
||||
<Label htmlFor="username">{t('username')}</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username}
|
||||
@@ -139,7 +141,7 @@ export function Users() {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Label htmlFor="email">{t('email')}</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
@@ -150,7 +152,7 @@ export function Users() {
|
||||
</div>
|
||||
{!editingUser && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Label htmlFor="password">{t('password')}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
@@ -161,7 +163,7 @@ export function Users() {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="is_active">激活状态</Label>
|
||||
<Label htmlFor="is_active">{t('activeStatus')}</Label>
|
||||
<Switch
|
||||
id="is_active"
|
||||
checked={formData.is_active}
|
||||
@@ -169,7 +171,7 @@ export function Users() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="is_admin">管理员权限</Label>
|
||||
<Label htmlFor="is_admin">{t('adminPrivileges')}</Label>
|
||||
<Switch
|
||||
id="is_admin"
|
||||
checked={formData.is_admin}
|
||||
@@ -179,10 +181,10 @@ export function Users() {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="bg-indigo-600 hover:bg-indigo-700 text-white">
|
||||
保存
|
||||
{t('save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -200,13 +202,13 @@ export function Users() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>邮箱</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
<TableHead>{t('id')}</TableHead>
|
||||
<TableHead>{t('username')}</TableHead>
|
||||
<TableHead>{t('email')}</TableHead>
|
||||
<TableHead>{t('status')}</TableHead>
|
||||
<TableHead>{t('role')}</TableHead>
|
||||
<TableHead>{t('createdAt')}</TableHead>
|
||||
<TableHead className="text-right">{t('actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -224,12 +226,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 ? '正常' : '禁用'}
|
||||
{user.is_active ? t('normal') : t('disabled')}
|
||||
</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 ? '管理员' : '普通用户'}
|
||||
{user.is_admin ? t('admin') : t('regularUser')}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-zinc-500">
|
||||
|
||||
Reference in New Issue
Block a user