diff --git a/frontend-v2/src/api/client.ts b/frontend-v2/src/api/client.ts index 272215a..05174c2 100644 --- a/frontend-v2/src/api/client.ts +++ b/frontend-v2/src/api/client.ts @@ -265,6 +265,17 @@ export interface ApiKeyItem { expire_at: string | null; } +// 创建 API Key 时后端返回的完整 key(仅创建时返回) +export interface ApiKeyCreated { + id: string; + name: string; + api_key: string; + status: string; + created_at: string; +} + +export type ApiKeyStatus = 'enabled' | 'disabled' | 'revoked'; + export interface PurchasedAppItem { id: string; app_id: string; @@ -299,6 +310,26 @@ export const profileApi = { getApiKeys: () => data('/profile/api-keys'), getPurchasedApps: () => data('/profile/purchased-apps'), + + // ─── 写操作 ─── + + updatePassword: (payload: { old_password: string; new_password: string }) => + request<{ code: number; message: string }>('/profile/password', { + method: 'PUT', + body: JSON.stringify(payload), + }), + + createApiKey: (name: string) => + data('/profile/api-keys', { + method: 'POST', + body: JSON.stringify({ name }), + }), + + updateApiKey: (keyId: string, status: ApiKeyStatus) => + data<{ id: string; status: string }>(`/profile/api-keys/${keyId}`, { + method: 'PUT', + body: JSON.stringify({ status }), + }), }; // ─── Token 驾驶舱 ──────────────────────────────────────────────────────────────── diff --git a/frontend-v2/src/hooks/useApi.ts b/frontend-v2/src/hooks/useApi.ts index 36d7874..995be74 100644 --- a/frontend-v2/src/hooks/useApi.ts +++ b/frontend-v2/src/hooks/useApi.ts @@ -229,6 +229,32 @@ export function useProfilePurchasedApps() { }); } +// ─── Profile 写操作 ───────────────────────────────────────────────────────────── + +export function useUpdateProfilePassword() { + return useMutation({ + mutationFn: (data: { old_password: string; new_password: string }) => + profileApi.updatePassword(data), + }); +} + +export function useCreateApiKey() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => profileApi.createApiKey(name), + onSuccess: () => qc.invalidateQueries({ queryKey: ['profile', 'api-keys'] }), + }); +} + +export function useUpdateApiKey() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ keyId, status }: { keyId: string; status: 'enabled' | 'disabled' | 'revoked' }) => + profileApi.updateApiKey(keyId, status), + onSuccess: () => qc.invalidateQueries({ queryKey: ['profile', 'api-keys'] }), + }); +} + // ─── Token 驾驶舱 ───────────────────────────────────────────────────────────────── // Landing hero still shows a few headline numbers; reuse the real token overview diff --git a/frontend-v2/src/pages/front/ProfilePage/index.tsx b/frontend-v2/src/pages/front/ProfilePage/index.tsx index 56734ac..4665beb 100644 --- a/frontend-v2/src/pages/front/ProfilePage/index.tsx +++ b/frontend-v2/src/pages/front/ProfilePage/index.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { User as UserIcon, Crown, @@ -15,6 +17,10 @@ import { Mail, Phone, CalendarDays, + Pencil, + Plus, + Trash2, + CheckCircle2, type LucideIcon, } from 'lucide-react'; import { @@ -24,6 +30,10 @@ import { useProfileLoginLogs, useProfileApiKeys, useProfilePurchasedApps, + useUpdateProfile, + useUpdateProfilePassword, + useCreateApiKey, + useUpdateApiKey, } from '@/hooks/useApi'; function StatCard({ @@ -76,9 +86,146 @@ function InfoRow({ icon: Icon, label, value }: { icon: LucideIcon; label: string ); } +// 改资料:内联表单,提交后 invalidate profile 刷新只读数据 +function EditProfileSection({ profile }: { profile?: { username: string; email: string; phone: string } }) { + const updateProfile = useUpdateProfile(); + const [username, setUsername] = useState(profile?.username ?? ''); + const [email, setEmail] = useState(profile?.email ?? ''); + const [phone, setPhone] = useState(profile?.phone ?? ''); + const [editing, setEditing] = useState(false); + const [error, setError] = useState(null); + const [ok, setOk] = useState(false); + + const reset = () => { + setUsername(profile?.username ?? ''); + setEmail(profile?.email ?? ''); + setPhone(profile?.phone ?? ''); + setError(null); + setOk(false); + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setOk(false); + updateProfile.mutate( + { username, email, phone }, + { + onSuccess: () => { + setOk(true); + setEditing(false); + }, + onError: (err) => setError(err instanceof Error ? err.message : '更新失败'), + } + ); + }; + + if (!editing) { + return ( + + ); + } + + return ( +
+
+ + setUsername(e.target.value)} disabled={updateProfile.isPending} /> +
+
+ + setEmail(e.target.value)} disabled={updateProfile.isPending} /> +
+
+ + setPhone(e.target.value)} disabled={updateProfile.isPending} /> +
+ {error &&

{error}

} + {ok && ( +

+ 资料已更新 +

+ )} +
+ + +
+
+ ); +} + +// 改密:独立卡片,含原密码/新密码校验 +function ChangePasswordSection() { + const changePwd = useUpdateProfilePassword(); + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [error, setError] = useState(null); + const [ok, setOk] = useState(false); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setOk(false); + if (newPassword.length < 6) { + setError('新密码至少 6 位'); + return; + } + changePwd.mutate( + { old_password: oldPassword, new_password: newPassword }, + { + onSuccess: () => { + setOk(true); + setOldPassword(''); + setNewPassword(''); + }, + onError: (err) => setError(err instanceof Error ? err.message : '修改失败'), + } + ); + }; + + return ( + + + + 修改密码 + + 定期更新密码以保障账户安全 + + +
+
+ + setOldPassword(e.target.value)} disabled={changePwd.isPending} /> +
+
+ + setNewPassword(e.target.value)} disabled={changePwd.isPending} /> +
+ {error &&

{error}

} + {ok && ( +

+ 密码修改成功 +

+ )} + +
+
+
+ ); +} + export function ProfilePage() { const [logPage, setLogPage] = useState(1); const logPageSize = 8; + const [newKeyName, setNewKeyName] = useState(''); const { data: profile, isLoading: pLoading } = useProfile(); const { data: balance, isLoading: bLoading } = useProfileBalance(); @@ -89,6 +236,10 @@ export function ProfilePage() { ); const { data: apiKeys, isLoading: keysLoading } = useProfileApiKeys(); const { data: purchasedApps, isLoading: appsLoading } = useProfilePurchasedApps(); + const createApiKey = useCreateApiKey(); + const updateApiKey = useUpdateApiKey(); + const [keyError, setKeyError] = useState(null); + const [createdKey, setCreatedKey] = useState(null); const logTotal = loginLogs?.total ?? 0; const logItems = loginLogs?.items ?? []; @@ -118,9 +269,12 @@ export function ProfilePage() {
{/* Profile identity */} - - 账户资料 - 当前登录账户的基础信息 + +
+ 账户资料 + 当前登录账户的基础信息 +
+
@@ -222,6 +376,9 @@ export function ProfilePage() { />
+ {/* Change password */} + + {/* Login logs */} @@ -312,6 +469,48 @@ export function ProfilePage() { 用于第三方集成的访问凭证 + {/* 新建密钥 */} +
+
+ + { setNewKeyName(e.target.value); setKeyError(null); }} + disabled={createApiKey.isPending} + /> +
+ +
+ + {keyError &&

{keyError}

} + + {createdKey && ( +
+

+ 密钥已创建,请立即复制保存(仅显示一次) +

+

{createdKey}

+
+ )} + {keysLoading ? (
@@ -319,22 +518,59 @@ export function ProfilePage() { ) : !apiKeys || apiKeys.length === 0 ? (
尚未创建密钥
) : ( - apiKeys.map((k) => ( -
-
-

{k.name}

-

- {k.key_preview} -

+ apiKeys.map((k) => { + const active = k.status === 'active'; + const revoked = k.status === 'revoked'; + return ( +
+
+

{k.name}

+

+ {k.key_preview} +

+
+
+ + {active ? '启用' : revoked ? '已撤销' : k.status} + + {!revoked && ( + + )} + {!revoked && ( + + )} +
- - {k.status === 'active' ? '启用' : k.status} - -
- )) + ); + }) )}