feat: 添加用户个人信息编辑、注册功能和现代科技蓝配色方案

- 添加个人信息编辑功能(用户名、公司信息可编辑,显示名称和邮箱只读)
- 添加修改密码功能(左下角设置)
- 添加用户注册功能(登录页面)
- 右上角通知改为卡片显示联系销售信息
- 头像显示用户名称首字母
- 修复代理工厂CPU和内存显示问题
- 应用现代科技蓝配色方案(方案一)
This commit is contained in:
zhanggangyong
2026-01-11 16:22:38 +00:00
parent 5603971f58
commit 87cfbafc44
4 changed files with 728 additions and 37 deletions
+2 -2
View File
@@ -136,8 +136,8 @@ export default function AgentFactoryPage() {
if (quotaResult.status === "fulfilled" && quotaResult.value?.success) {
const quota = quotaResult.value.data
setStats({
cpu: quota?.cpuUsed || 0,
memory: quota?.memoryUsed || 0,
cpu: quota?.totalCpu || 0,
memory: quota?.totalMemory || 0,
})
}
+168 -26
View File
@@ -16,35 +16,89 @@ import { useToast } from "@/hooks/use-toast"
export default function LoginPage() {
const { language, setLanguage, t } = useLanguage()
const { toast } = useToast()
const [isRegisterMode, setIsRegisterMode] = useState(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [username, setUsername] = useState("")
const [fullName, setFullName] = useState("")
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// 注册模式验证
if (isRegisterMode) {
if (!email) {
toast({
title: t("错误", "Error"),
description: t("邮箱为必填项", "Email is required"),
variant: "destructive",
})
return
}
if (password.length < 8) {
toast({
title: t("错误", "Error"),
description: t("密码长度至少为8位", "Password must be at least 8 characters"),
variant: "destructive",
})
return
}
if (password !== confirmPassword) {
toast({
title: t("错误", "Error"),
description: t("两次输入的密码不一致", "Passwords do not match"),
variant: "destructive",
})
return
}
}
setIsLoading(true)
try {
// 默认使用 user 角色登录,可以根据需要调整
const result = await TaijiAPIClient.login(email, password, "user")
if (result && result.success) {
toast({
title: t("登录成功", "Login successful"),
description: t("欢迎回来", "Welcome back"),
})
// Redirect to dashboard on success
window.location.href = "/"
if (isRegisterMode) {
// 注册
const result = await TaijiAPIClient.register(email, password, username || undefined, fullName || undefined)
if (result && result.success) {
toast({
title: t("注册成功", "Registration successful"),
description: t("欢迎加入 Taiji AI 平台", "Welcome to Taiji AI Platform"),
})
// 注册成功后自动登录,跳转到首页
window.location.href = "/"
} else {
toast({
title: t("注册失败", "Registration failed"),
description: (result as any)?.message || t("注册失败,请重试", "Registration failed, please try again"),
variant: "destructive",
})
}
} else {
toast({
title: t("登录失败", "Login failed"),
description: (result as any)?.message || t("请检查您的邮箱和密码", "Please check your email and password"),
variant: "destructive",
})
// 登录
const result = await TaijiAPIClient.login(email, password, "user")
if (result && result.success) {
toast({
title: t("登录成功", "Login successful"),
description: t("欢迎回来", "Welcome back"),
})
// Redirect to dashboard on success
window.location.href = "/"
} else {
toast({
title: t("登录失败", "Login failed"),
description: (result as any)?.message || t("请检查您的邮箱和密码", "Please check your email and password"),
variant: "destructive",
})
}
}
} catch (error: any) {
console.error("Login error:", error)
console.error(isRegisterMode ? "Register error:" : "Login error:", error)
let errorMessage = error.message || t("网络错误,请稍后重试", "Network error, please try again")
// 检查是否是连接错误
@@ -56,7 +110,7 @@ export default function LoginPage() {
}
toast({
title: t("登录失败", "Login failed"),
title: isRegisterMode ? t("注册失败", "Registration failed") : t("登录失败", "Login failed"),
description: errorMessage,
variant: "destructive",
})
@@ -93,11 +147,14 @@ export default function LoginPage() {
<p className="text-muted-foreground">{t("企业级 AI Agent 赋能平台", "Enterprise AI Agent Empowerment")}</p>
</div>
{/* Login Card */}
{/* Login/Register Card */}
<Card className="p-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* 邮箱 - 必填 */}
<div className="space-y-2">
<Label htmlFor="email">{t("邮箱地址", "Email Address")}</Label>
<Label htmlFor="email">
{t("邮箱地址", "Email Address")} <span className="text-destructive">*</span>
</Label>
<Input
id="email"
type="email"
@@ -109,12 +166,44 @@ export default function LoginPage() {
/>
</div>
{/* 注册模式下的额外字段 */}
{isRegisterMode && (
<>
<div className="space-y-2">
<Label htmlFor="username">{t("用户名", "Username")} ({t("可选", "Optional")})</Label>
<Input
id="username"
type="text"
placeholder={t("请输入用户名(可选)", "Enter username (optional)")}
value={username}
onChange={(e) => setUsername(e.target.value)}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="fullName">{t("显示名称", "Display Name")} ({t("可选", "Optional")})</Label>
<Input
id="fullName"
type="text"
placeholder={t("请输入显示名称(可选)", "Enter display name (optional)")}
value={fullName}
onChange={(e) => setFullName(e.target.value)}
className="h-11"
/>
</div>
</>
)}
{/* 密码 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">{t("密码", "Password")}</Label>
<Button type="button" variant="link" className="px-0 text-sm text-primary hover:text-primary/80">
{t("忘记密码?", "Forgot password?")}
</Button>
{!isRegisterMode && (
<Button type="button" variant="link" className="px-0 text-sm text-primary hover:text-primary/80">
{t("忘记密码?", "Forgot password?")}
</Button>
)}
</div>
<div className="relative">
<Input
@@ -124,6 +213,7 @@ export default function LoginPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="h-11 pr-10"
/>
<Button
@@ -140,22 +230,74 @@ export default function LoginPage() {
)}
</Button>
</div>
{isRegisterMode && (
<p className="text-xs text-muted-foreground">
{t("密码长度至少为8位", "Password must be at least 8 characters")}
</p>
)}
</div>
{/* 确认密码 - 仅注册模式 */}
{isRegisterMode && (
<div className="space-y-2">
<Label htmlFor="confirmPassword">{t("确认密码", "Confirm Password")}</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder={t("请再次输入密码", "Enter password again")}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
className="h-11 pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
</div>
)}
<Button type="submit" className="w-full h-11" disabled={isLoading}>
{isLoading ? (
<span className="flex items-center gap-2">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{t("登录中...", "Signing in...")}
{isRegisterMode ? t("注册中...", "Registering...") : t("登录中...", "Signing in...")}
</span>
) : (
t("登录", "Sign In")
isRegisterMode ? t("注册", "Sign Up") : t("登录", "Sign In")
)}
</Button>
</form>
<div className="mt-6 text-center text-sm text-muted-foreground">
{t("如需账号,请联系管理员", "Please contact administrator for account access")}
{/* 切换登录/注册 */}
<div className="mt-6 text-center">
<Button
type="button"
variant="link"
className="text-sm"
onClick={() => {
setIsRegisterMode(!isRegisterMode)
setPassword("")
setConfirmPassword("")
setUsername("")
setFullName("")
}}
>
{isRegisterMode
? t("已有账号?立即登录", "Already have an account? Sign in")
: t("没有账号?立即注册", "Don't have an account? Sign up")}
</Button>
</div>
</Card>
+437 -8
View File
@@ -28,9 +28,12 @@ import {
Key,
Copy,
RefreshCw,
Lock,
Mail,
} 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,
@@ -41,6 +44,7 @@ import {
} 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 },
@@ -56,9 +60,31 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
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)
// 使用固定的初始值,避免服务器端和客户端不一致
const [apiKey, setApiKey] = useState("sk_live_1234567890abcdefghijklmnopqrstuvwxyz")
const [serviceEndpoint, setServiceEndpoint] = useState("https://api.taiji-ai.com/v1")
// 用户信息状态
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 余额状态
@@ -100,6 +126,159 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
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)
}
}
const copyToClipboard = (text: string) => {
if (typeof window !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(text)
@@ -167,7 +346,11 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
{/* Footer */}
<div className="border-t border-border p-2">
<Button variant="ghost" className={cn("w-full justify-start", collapsed && "justify-center px-0")}>
<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>
@@ -211,22 +394,83 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
{balanceLoading ? "加载中..." : euBalance !== null ? `${euBalance} EU` : "-- EU"}
</span>
</Badge>
<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>
{/* 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">AD</AvatarFallback>
<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>
<DropdownMenuItem onClick={() => setShowProfileDialog(true)}>
<User className="mr-2 h-4 w-4" />
{t("个人资料", "Profile")}
</DropdownMenuItem>
@@ -234,7 +478,7 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
<Key className="mr-2 h-4 w-4" />
{t("密钥管理", "API Keys")}
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem onClick={() => setShowSettingsDialog(true)}>
<Settings className="mr-2 h-4 w-4" />
{t("设置", "Settings")}
</DropdownMenuItem>
@@ -324,6 +568,191 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
</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="name">{t("显示名称", "Display Name")}</Label>
<Input
id="name"
value={userInfo?.name || ""}
readOnly
className="bg-muted cursor-not-allowed"
/>
<p className="text-xs text-muted-foreground">
{t("显示名称不可修改", "Display name cannot be modified")}
</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>
)
}
+121 -1
View File
@@ -184,6 +184,58 @@ export class TaijiAPIClient {
// ==================== 认证模块 API ====================
// Base URL: http://localhost:8002/api/auth
/**
* 用户注册
*/
static async register(
email: string,
password: string,
username?: string,
fullName?: string,
): Promise<APIResponse<{ token: string; refreshToken?: string; user?: any }>> {
try {
// 清除所有旧的token
clearAllTokens()
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password,
username: username || email.split("@")[0],
full_name: fullName,
}),
signal: AbortSignal.timeout(10000), // 10 second timeout
})
const data = await handleResponse<APIResponse<{ token: string; refreshToken?: string; user?: any }>>(response)
if (data.success && data.data?.token && typeof window !== "undefined") {
// 存储token
localStorage.setItem("auth_token", data.data.token)
document.cookie = `auth_token=${data.data.token}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`
if (data.data.refreshToken) {
localStorage.setItem("refresh_token", data.data.refreshToken)
}
if (data.data.user) {
localStorage.setItem("user", JSON.stringify(data.data.user))
}
}
return data
} catch (error: any) {
// 处理网络错误
if (error.name === "AbortError" || error.message?.includes("timeout")) {
throw new Error("请求超时,请检查网络连接或后端服务状态")
}
if (error.message?.includes("Failed to fetch") || error.name === "TypeError") {
throw new Error(
`无法连接到后端服务 (${API_BASE_URLS.mcpServer})。请确保 MCP Server 正在运行。`,
)
}
throw error
}
}
/**
* 用户登录
*/
@@ -300,6 +352,74 @@ export class TaijiAPIClient {
return handleResponse(response)
}
/**
* 获取当前用户信息
*/
static async getCurrentUser() {
try {
requireAuth()
} catch (error) {
throw new Error("无法获取用户信息:用户未认证。(Cannot get user info: user not authenticated.)")
}
try {
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/user/profile`, {
headers: buildHeaders(),
})
// 如果是404错误,可能是接口还未部署,直接返回特殊错误
if (response.status === 404) {
console.warn("Profile API not available (404), using local user data")
throw new Error("PROFILE_API_NOT_AVAILABLE")
}
return handleResponse(response)
} catch (error: any) {
// 如果已经是特殊错误,直接抛出
if (error.message === "PROFILE_API_NOT_AVAILABLE") {
throw error
}
// 如果是其他错误,检查错误消息中是否包含404
if (error.message?.includes("404") || error.message?.includes("Not Found") || error.message?.includes("HTTP 404")) {
console.warn("Profile API not available, using local user data")
throw new Error("PROFILE_API_NOT_AVAILABLE")
}
throw error
}
}
/**
* 更新用户信息
*/
static async updateUserProfile(data: {
username?: string
company?: string
}) {
try {
requireAuth()
} catch (error) {
throw new Error("无法更新用户信息:用户未认证。(Cannot update user profile: user not authenticated.)")
}
const response = await fetch(`${API_BASE_URLS.mcpServer}/api/user/profile`, {
method: "PUT",
headers: buildHeaders(),
body: JSON.stringify(data),
})
const result = await handleResponse(response)
// 如果更新成功,更新本地存储的用户信息
if (result.success && typeof window !== "undefined") {
const userStr = localStorage.getItem("user")
if (userStr) {
try {
const user = JSON.parse(userStr)
if (data.username) user.username = data.username
if (data.company !== undefined) user.company = data.company
localStorage.setItem("user", JSON.stringify(user))
} catch (e) {
console.error("Failed to update local user info:", e)
}
}
}
return result
}
/**
* 获取API密钥信息
*/
@@ -2284,7 +2404,7 @@ export class TaijiAPIClient {
method: "GET",
headers: buildHeaders(),
})
return handleResponse<APIResponse<{ balance: number; currency?: string }>>(response)
return handleResponse<APIResponse<{ balance: number; currency?: string; monthlySpent?: number }>>(response)
} catch (error) {
console.error("Failed to get user balance:", error)
throw error