mirror of
https://github.com/Fasthei/taiji-pda-v0.git
synced 2026-09-26 21:13:25 +00:00
773 lines
30 KiB
TypeScript
773 lines
30 KiB
TypeScript
"use client"
|
|
|
|
import type React from "react"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import Link from "next/link"
|
|
import Image from "next/image"
|
|
import { usePathname } from "next/navigation"
|
|
import { TaijiAPIClient } from "@/lib/api-client"
|
|
import { cn } from "@/lib/utils"
|
|
import { Button } from "@/components/ui/button"
|
|
import { useLanguage } from "@/contexts/language-context"
|
|
import { ThemeToggle } from "@/components/theme-toggle"
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
import {
|
|
LayoutDashboard,
|
|
Database,
|
|
GitBranch,
|
|
Boxes,
|
|
Network,
|
|
CreditCard,
|
|
ChevronLeft,
|
|
Bell,
|
|
Settings,
|
|
Activity,
|
|
Zap,
|
|
Globe,
|
|
LogOut,
|
|
User,
|
|
Key,
|
|
Copy,
|
|
Lock,
|
|
Mail,
|
|
BookOpen,
|
|
} from "lucide-react"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { getUser } from "@/lib/auth"
|
|
|
|
const navigationItems = [
|
|
{ nameKey: { zh: "概览", en: "Overview" }, href: "/", icon: LayoutDashboard },
|
|
{ nameKey: { zh: "服务网关", en: "Service Gateway" }, href: "/model-gateway", icon: GitBranch },
|
|
{ nameKey: { zh: "数据与工具", en: "Data & Tools" }, href: "/data-tools", icon: Database },
|
|
{ nameKey: { zh: "代理工厂", en: "Agent Factory" }, href: "/agent-factory", icon: Boxes },
|
|
{ nameKey: { zh: "编排中心", en: "Orchestration Hub" }, href: "/orchestration", icon: Network },
|
|
{ nameKey: { zh: "计费与资源", en: "Billing & Resources" }, href: "/billing", icon: CreditCard },
|
|
{ nameKey: { zh: "文档", en: "Documentation" }, href: "http://agnetdoc.taijiaicloud.com/", icon: BookOpen, external: true },
|
|
]
|
|
|
|
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
const pathname = usePathname()
|
|
const { language, setLanguage, t } = useLanguage()
|
|
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false)
|
|
const [showProfileDialog, setShowProfileDialog] = useState(false)
|
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
|
const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false)
|
|
// LiteLLM 密钥信息(从 /api/user/resources/info 获取)
|
|
const [litellmKeys, setLitellmKeys] = useState<Array<{
|
|
modelName: string
|
|
apiKey: string
|
|
apiBase: string
|
|
rpmLimit?: number
|
|
tpmLimit?: number
|
|
status?: string
|
|
}>>([])
|
|
const [litellmApiBase, setLitellmApiBase] = useState<string>("")
|
|
const [apiKeyLoading, setApiKeyLoading] = useState(false)
|
|
// 用户信息状态
|
|
const [userInfo, setUserInfo] = useState<{
|
|
username: string
|
|
name: string
|
|
email: string
|
|
company?: string
|
|
} | null>(null)
|
|
const [profileForm, setProfileForm] = useState({
|
|
username: "",
|
|
company: "",
|
|
})
|
|
const [profileLoading, setProfileLoading] = useState(false)
|
|
// 修改密码表单
|
|
const [passwordForm, setPasswordForm] = useState({
|
|
oldPassword: "",
|
|
newPassword: "",
|
|
confirmPassword: "",
|
|
})
|
|
const [passwordLoading, setPasswordLoading] = useState(false)
|
|
// API 延迟状态 - 从系统组件 mcp_server 获取
|
|
const [apiLatency, setApiLatency] = useState<number | null>(null)
|
|
// EU 余额状态
|
|
const [euBalance, setEuBalance] = useState<number | null>(null)
|
|
const [balanceLoading, setBalanceLoading] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const loadApiLatency = async () => {
|
|
try {
|
|
const monitoringDashboard = await TaijiAPIClient.getMonitoringDashboard()
|
|
if (monitoringDashboard?.health?.services?.mcp_server?.latency !== undefined) {
|
|
setApiLatency(monitoringDashboard.health.services.mcp_server.latency)
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load API latency:", error)
|
|
}
|
|
}
|
|
loadApiLatency()
|
|
}, [])
|
|
|
|
// 加载 EU 余额
|
|
useEffect(() => {
|
|
const loadBalance = async () => {
|
|
try {
|
|
setBalanceLoading(true)
|
|
const response = await TaijiAPIClient.getUserBalance()
|
|
if (response?.success && response?.data) {
|
|
setEuBalance(response.data.balance)
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load balance:", error)
|
|
} finally {
|
|
setBalanceLoading(false)
|
|
}
|
|
}
|
|
loadBalance()
|
|
// 每30秒刷新一次余额
|
|
const balanceInterval = setInterval(loadBalance, 30000)
|
|
return () => clearInterval(balanceInterval)
|
|
}, [])
|
|
|
|
// 加载用户信息
|
|
useEffect(() => {
|
|
const loadUserInfo = () => {
|
|
const user = getUser()
|
|
if (user) {
|
|
setUserInfo({
|
|
username: user.username || user.email?.split("@")[0] || "",
|
|
name: user.name || user.full_name || user.email?.split("@")[0] || "",
|
|
email: user.email || "",
|
|
company: user.company || "",
|
|
})
|
|
setProfileForm({
|
|
username: user.username || user.email?.split("@")[0] || "",
|
|
company: user.company || "",
|
|
})
|
|
}
|
|
}
|
|
loadUserInfo()
|
|
}, [])
|
|
|
|
// 打开个人信息对话框时加载最新用户信息
|
|
useEffect(() => {
|
|
if (showProfileDialog) {
|
|
const loadUserInfo = async () => {
|
|
try {
|
|
const response = await TaijiAPIClient.getCurrentUser()
|
|
if (response?.success && response?.data) {
|
|
const user = response.data
|
|
setUserInfo({
|
|
username: user.username || user.email?.split("@")[0] || "",
|
|
name: user.name || user.full_name || user.email?.split("@")[0] || "",
|
|
email: user.email || "",
|
|
company: user.company || "",
|
|
})
|
|
setProfileForm({
|
|
username: user.username || user.email?.split("@")[0] || "",
|
|
company: user.company || "",
|
|
})
|
|
}
|
|
} catch (error: any) {
|
|
// 如果API不可用或失败,静默使用本地存储的信息
|
|
if (error.message === "PROFILE_API_NOT_AVAILABLE") {
|
|
// 接口未部署,使用本地数据
|
|
} else {
|
|
console.error("Failed to load user info:", error)
|
|
}
|
|
// 使用本地存储的信息
|
|
const user = getUser()
|
|
if (user) {
|
|
setUserInfo({
|
|
username: user.username || user.email?.split("@")[0] || "",
|
|
name: user.name || user.full_name || user.email?.split("@")[0] || "",
|
|
email: user.email || "",
|
|
company: user.company || "",
|
|
})
|
|
setProfileForm({
|
|
username: user.username || user.email?.split("@")[0] || "",
|
|
company: user.company || "",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
loadUserInfo()
|
|
}
|
|
}, [showProfileDialog])
|
|
|
|
// 保存用户信息
|
|
const handleSaveProfile = async () => {
|
|
try {
|
|
setProfileLoading(true)
|
|
const response = await TaijiAPIClient.updateUserProfile({
|
|
username: profileForm.username,
|
|
company: profileForm.company || undefined,
|
|
})
|
|
if (response?.success) {
|
|
// 更新本地用户信息
|
|
const user = getUser()
|
|
if (user) {
|
|
user.username = profileForm.username
|
|
if (profileForm.company) {
|
|
user.company = profileForm.company
|
|
} else {
|
|
delete user.company
|
|
}
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem("user", JSON.stringify(user))
|
|
}
|
|
// 更新显示的用户信息
|
|
if (userInfo) {
|
|
setUserInfo({
|
|
...userInfo,
|
|
username: profileForm.username,
|
|
company: profileForm.company || "",
|
|
})
|
|
}
|
|
}
|
|
setShowProfileDialog(false)
|
|
} else {
|
|
throw new Error(response?.message || "更新失败")
|
|
}
|
|
} catch (error: any) {
|
|
console.error("Failed to update profile:", error)
|
|
alert(error.message || t("更新失败,请重试", "Update failed, please try again"))
|
|
} finally {
|
|
setProfileLoading(false)
|
|
}
|
|
}
|
|
|
|
// 修改密码
|
|
const handleChangePassword = async () => {
|
|
// 验证表单
|
|
if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
|
alert(t("请填写所有字段", "Please fill in all fields"))
|
|
return
|
|
}
|
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
|
alert(t("新密码和确认密码不匹配", "New password and confirm password do not match"))
|
|
return
|
|
}
|
|
if (passwordForm.newPassword.length < 8) {
|
|
alert(t("密码长度至少为8位", "Password must be at least 8 characters"))
|
|
return
|
|
}
|
|
if (passwordForm.oldPassword === passwordForm.newPassword) {
|
|
alert(t("新密码不能与旧密码相同", "New password cannot be the same as old password"))
|
|
return
|
|
}
|
|
|
|
try {
|
|
setPasswordLoading(true)
|
|
const response = await TaijiAPIClient.changePassword(
|
|
passwordForm.oldPassword,
|
|
passwordForm.newPassword
|
|
)
|
|
if (response?.success) {
|
|
alert(t("密码修改成功", "Password changed successfully"))
|
|
setPasswordForm({
|
|
oldPassword: "",
|
|
newPassword: "",
|
|
confirmPassword: "",
|
|
})
|
|
setShowChangePasswordDialog(false)
|
|
} else {
|
|
throw new Error(response?.message || t("修改失败", "Change failed"))
|
|
}
|
|
} catch (error: any) {
|
|
console.error("Failed to change password:", error)
|
|
alert(error.message || t("修改失败,请重试", "Change failed, please try again"))
|
|
} finally {
|
|
setPasswordLoading(false)
|
|
}
|
|
}
|
|
|
|
// 打开密钥管理对话框时加载密钥
|
|
useEffect(() => {
|
|
if (showApiKeyDialog) {
|
|
const loadApiKeys = async () => {
|
|
try {
|
|
setApiKeyLoading(true)
|
|
const response = await TaijiAPIClient.getUserResourcesInfo()
|
|
if (response?.success && response?.data) {
|
|
setLitellmKeys(response.data.litellmKeys || [])
|
|
setLitellmApiBase(response.data.litellmApiBase || "")
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load API keys:", error)
|
|
} finally {
|
|
setApiKeyLoading(false)
|
|
}
|
|
}
|
|
loadApiKeys()
|
|
}
|
|
}, [showApiKeyDialog])
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
if (typeof window !== "undefined" && navigator.clipboard) {
|
|
navigator.clipboard.writeText(text)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-screen bg-background">
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={cn(
|
|
"flex flex-col border-r border-border bg-card transition-all duration-300",
|
|
collapsed ? "w-16" : "w-64",
|
|
)}
|
|
>
|
|
{/* Logo */}
|
|
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
|
<Link href="/" className={cn("flex items-center gap-2", collapsed && "mx-auto")}>
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg overflow-hidden shrink-0">
|
|
<Image
|
|
src="/logo.jpg"
|
|
alt="Taiji AI Platform Logo"
|
|
width={32}
|
|
height={32}
|
|
className="object-cover w-full h-full"
|
|
unoptimized
|
|
/>
|
|
</div>
|
|
{!collapsed && <span className="text-lg font-semibold">Taiji AI</span>}
|
|
</Link>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
className={cn("h-8 w-8", collapsed && "mx-auto")}
|
|
>
|
|
<ChevronLeft className={cn("h-4 w-4 transition-transform", collapsed && "rotate-180")} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 space-y-1 p-2 overflow-y-auto">
|
|
{navigationItems.map((item) => {
|
|
const isActive = pathname === item.href
|
|
const content = (
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors",
|
|
isActive
|
|
? "bg-primary text-primary-foreground"
|
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
)}
|
|
>
|
|
<item.icon className="h-5 w-5 shrink-0" />
|
|
{!collapsed && <span>{t(item.nameKey.zh, item.nameKey.en)}</span>}
|
|
</div>
|
|
)
|
|
if (item.external) {
|
|
return (
|
|
<a key={item.href} href={item.href} target="_blank" rel="noopener noreferrer">
|
|
{content}
|
|
</a>
|
|
)
|
|
}
|
|
return (
|
|
<Link key={item.href} href={item.href}>
|
|
{content}
|
|
</Link>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
<div className="border-t border-border p-2">
|
|
<Button
|
|
variant="ghost"
|
|
className={cn("w-full justify-start", collapsed && "justify-center px-0")}
|
|
onClick={() => setShowSettingsDialog(true)}
|
|
>
|
|
<Settings className="h-5 w-5 shrink-0" />
|
|
{!collapsed && <span className="ml-3">{t("设置", "Settings")}</span>}
|
|
</Button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{/* Top Bar */}
|
|
<header className="flex h-16 items-center justify-between border-b border-border bg-card px-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<Activity className="h-4 w-4 text-green-500" />
|
|
<span className="text-sm text-muted-foreground">{t("API 延迟", "API Latency")}: {apiLatency !== null ? `${apiLatency}ms` : "..."}</span>
|
|
</div>
|
|
<div className="h-4 w-px bg-border" />
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-green-500" />
|
|
<span className="text-sm text-muted-foreground">{t("所有系统正常运行", "All Systems Operational")}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Theme Toggle */}
|
|
<ThemeToggle />
|
|
|
|
{/* Language Switcher */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="gap-2">
|
|
<Globe className="h-4 w-4" />
|
|
<span className="hidden sm:inline">{language === "zh" ? "中文" : "English"}</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => setLanguage("zh")}>中文</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setLanguage("en")}>English</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<Badge variant="outline" className="gap-2">
|
|
<Zap className="h-3 w-3" />
|
|
<span className="font-mono">
|
|
{balanceLoading ? "加载中..." : euBalance !== null ? `${euBalance} EU` : "-- EU"}
|
|
</span>
|
|
</Badge>
|
|
|
|
{/* Notification Bell */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="relative">
|
|
<Bell className="h-5 w-5" />
|
|
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-destructive" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-80 p-0">
|
|
<Card className="border-0 shadow-none">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Bell className="h-4 w-4" />
|
|
{t("通知", "Notification")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("如需购买请联系销售", "For purchasing, please contact sales")}
|
|
</p>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
<a
|
|
href="mailto:november@taijiaicloud.com"
|
|
className="text-primary hover:underline"
|
|
>
|
|
november@taijiaicloud.com
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* User Menu with Logout */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="rounded-full">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback className="bg-primary text-primary-foreground text-xs">
|
|
{(() => {
|
|
// 获取用户显示名称的首字母或前两个字符
|
|
const name = userInfo?.name || getUser()?.name || getUser()?.full_name || ""
|
|
if (name) {
|
|
// 如果是中文,取前两个字符;如果是英文,取首字母大写
|
|
const isChinese = /[\u4e00-\u9fa5]/.test(name)
|
|
if (isChinese) {
|
|
return name.substring(0, 2)
|
|
} else {
|
|
// 英文取首字母,支持多个单词
|
|
const words = name.trim().split(/\s+/)
|
|
if (words.length >= 2) {
|
|
// 多个单词取首字母
|
|
return (words[0][0] + words[1][0]).toUpperCase()
|
|
} else {
|
|
// 单个单词取前两个字符
|
|
return name.substring(0, 2).toUpperCase()
|
|
}
|
|
}
|
|
}
|
|
// 如果没有名称,尝试从邮箱获取
|
|
const email = userInfo?.email || getUser()?.email || ""
|
|
if (email) {
|
|
return email.substring(0, 2).toUpperCase()
|
|
}
|
|
// 默认值
|
|
return "U"
|
|
})()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<DropdownMenuItem onClick={() => setShowProfileDialog(true)}>
|
|
<User className="mr-2 h-4 w-4" />
|
|
{t("个人资料", "Profile")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setShowApiKeyDialog(true)}>
|
|
<Key className="mr-2 h-4 w-4" />
|
|
{t("密钥管理", "API Keys")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setShowSettingsDialog(true)}>
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
{t("设置", "Settings")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/login" className="flex items-center">
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
{t("退出登录", "Sign Out")}
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page Content */}
|
|
<main className="flex-1 overflow-y-auto p-6">{children}</main>
|
|
</div>
|
|
|
|
<Dialog open={showApiKeyDialog} onOpenChange={setShowApiKeyDialog}>
|
|
<DialogContent className="sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("密钥管理", "API Key Management")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("查看和管理您的API密钥", "View and manage your API keys")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6 py-4">
|
|
{apiKeyLoading ? (
|
|
<div className="text-center py-4 text-muted-foreground">
|
|
{t("加载中...", "Loading...")}
|
|
</div>
|
|
) : litellmKeys.length === 0 ? (
|
|
<div className="text-center py-4 text-muted-foreground">
|
|
{t("暂无可用密钥", "No API keys available")}
|
|
</div>
|
|
) : (
|
|
litellmKeys.map((keyInfo, index) => (
|
|
<div key={index} className="space-y-3 p-4 rounded-lg border bg-muted/30">
|
|
{/* 模型名称 */}
|
|
{keyInfo.modelName && (
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="font-mono">
|
|
{keyInfo.modelName}
|
|
</Badge>
|
|
{keyInfo.status && (
|
|
<Badge variant={keyInfo.status === "active" ? "default" : "outline"} className="text-xs">
|
|
{keyInfo.status === "active" ? t("激活", "Active") : keyInfo.status}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)}
|
|
{/* API 密钥 */}
|
|
<div className="space-y-2">
|
|
<Label>{t("API密钥", "API Key")}</Label>
|
|
<div className="flex gap-2">
|
|
<Input value={keyInfo.apiKey} readOnly className="font-mono text-sm" type="password" />
|
|
<Button variant="outline" size="icon" onClick={() => copyToClipboard(keyInfo.apiKey)} title={t("复制", "Copy")}>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("请妥善保管您的API密钥。", "Keep your API key secure.")}
|
|
</p>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button onClick={() => setShowApiKeyDialog(false)}>{t("关闭", "Close")}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Profile Dialog */}
|
|
<Dialog open={showProfileDialog} onOpenChange={setShowProfileDialog}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("个人信息", "Profile Information")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("查看和编辑您的个人信息", "View and edit your profile information")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
{/* 用户名 - 可编辑 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="username">{t("用户名", "Username")}</Label>
|
|
<Input
|
|
id="username"
|
|
value={profileForm.username}
|
|
onChange={(e) => setProfileForm({ ...profileForm, username: e.target.value })}
|
|
placeholder={t("请输入用户名", "Enter username")}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("用户名用于登录和显示", "Username is used for login and display")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 邮箱 - 只读 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">{t("邮箱", "Email")}</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={userInfo?.email || ""}
|
|
readOnly
|
|
disabled
|
|
tabIndex={-1}
|
|
className="bg-muted cursor-not-allowed select-none"
|
|
onSelect={(e) => e.preventDefault()}
|
|
onFocus={(e) => e.target.blur()}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("邮箱不可修改", "Email cannot be modified")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 公司信息 - 可选,可编辑 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="company">{t("公司信息", "Company")} ({t("可选", "Optional")})</Label>
|
|
<Input
|
|
id="company"
|
|
value={profileForm.company}
|
|
onChange={(e) => setProfileForm({ ...profileForm, company: e.target.value })}
|
|
placeholder={t("请输入公司名称", "Enter company name")}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("公司信息为可选字段", "Company information is optional")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setShowProfileDialog(false)}>
|
|
{t("取消", "Cancel")}
|
|
</Button>
|
|
<Button onClick={handleSaveProfile} disabled={profileLoading}>
|
|
{profileLoading ? t("保存中...", "Saving...") : t("保存", "Save")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Settings Dialog */}
|
|
<Dialog open={showSettingsDialog} onOpenChange={setShowSettingsDialog}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("设置", "Settings")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("管理您的账户设置", "Manage your account settings")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start"
|
|
onClick={() => {
|
|
setShowSettingsDialog(false)
|
|
setShowChangePasswordDialog(true)
|
|
}}
|
|
>
|
|
<Lock className="mr-2 h-4 w-4" />
|
|
{t("修改密码", "Change Password")}
|
|
</Button>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button onClick={() => setShowSettingsDialog(false)}>
|
|
{t("关闭", "Close")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Change Password Dialog */}
|
|
<Dialog open={showChangePasswordDialog} onOpenChange={setShowChangePasswordDialog}>
|
|
<DialogContent className="sm:max-w-[500px]">
|
|
<DialogHeader>
|
|
<DialogTitle>{t("修改密码", "Change Password")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t("请输入您的旧密码和新密码", "Please enter your old password and new password")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
{/* 旧密码 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="oldPassword">{t("旧密码", "Old Password")}</Label>
|
|
<Input
|
|
id="oldPassword"
|
|
type="password"
|
|
value={passwordForm.oldPassword}
|
|
onChange={(e) => setPasswordForm({ ...passwordForm, oldPassword: e.target.value })}
|
|
placeholder={t("请输入旧密码", "Enter old password")}
|
|
/>
|
|
</div>
|
|
|
|
{/* 新密码 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newPassword">{t("新密码", "New Password")}</Label>
|
|
<Input
|
|
id="newPassword"
|
|
type="password"
|
|
value={passwordForm.newPassword}
|
|
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
|
|
placeholder={t("请输入新密码(至少8位)", "Enter new password (at least 8 characters)")}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("密码长度至少为8位", "Password must be at least 8 characters")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 确认密码 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirmPassword">{t("确认密码", "Confirm Password")}</Label>
|
|
<Input
|
|
id="confirmPassword"
|
|
type="password"
|
|
value={passwordForm.confirmPassword}
|
|
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
|
|
placeholder={t("请再次输入新密码", "Enter new password again")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => {
|
|
setShowChangePasswordDialog(false)
|
|
setPasswordForm({
|
|
oldPassword: "",
|
|
newPassword: "",
|
|
confirmPassword: "",
|
|
})
|
|
}}>
|
|
{t("取消", "Cancel")}
|
|
</Button>
|
|
<Button onClick={handleChangePassword} disabled={passwordLoading}>
|
|
{passwordLoading ? t("修改中...", "Changing...") : t("确认修改", "Confirm Change")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|