mirror of
https://github.com/Fasthei/taiji-pda-v0.git
synced 2026-09-26 19:11:54 +00:00
feat: 添加用户个人信息编辑、注册功能和现代科技蓝配色方案
- 添加个人信息编辑功能(用户名、公司信息可编辑,显示名称和邮箱只读) - 添加修改密码功能(左下角设置) - 添加用户注册功能(登录页面) - 右上角通知改为卡片显示联系销售信息 - 头像显示用户名称首字母 - 修复代理工厂CPU和内存显示问题 - 应用现代科技蓝配色方案(方案一)
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user