145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { CopyIcon, LogOutIcon, UserIcon } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { apiFetch } from "@/core/api/fetch-client";
|
|
import { clearStoredAuth, getAccountDisplayName, getStoredAuth } from "@/core/auth";
|
|
import { useLiveAccount } from "@/core/auth/use-live-account";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
import { OptType, writeYLog } from "@/core/log/y-log";
|
|
import { clearYLogToken } from "@/core/log/y-log-token";
|
|
|
|
import { SettingsSection } from "./settings-section";
|
|
import { PasswordSettings } from "./password-settings";
|
|
|
|
function roleLabel(role?: string) {
|
|
if (role === "admin") return "管理员";
|
|
if (role === "user") return "普通用户";
|
|
return role || "未知角色";
|
|
}
|
|
|
|
function copy(text: string, hint = "已复制") {
|
|
if (!text) return;
|
|
navigator.clipboard
|
|
.writeText(text)
|
|
.then(() => toast.success(hint))
|
|
.catch(() => toast.error("复制失败"));
|
|
}
|
|
|
|
export function AccountSettingsPage() {
|
|
// Subscribe to same-tab and cross-tab auth changes. The dialog may stay open
|
|
// while the active account is switched elsewhere.
|
|
useLiveAccount();
|
|
const auth = getStoredAuth();
|
|
|
|
if (!auth) {
|
|
return (
|
|
<SettingsSection title="个人信息" description="当前未登录或登录信息无法读取。">
|
|
<div className="text-muted-foreground rounded-md border p-6 text-center text-sm">
|
|
没有可显示的账号信息。
|
|
</div>
|
|
</SettingsSection>
|
|
);
|
|
}
|
|
|
|
const displayName = getAccountDisplayName(auth);
|
|
|
|
const handleLogout = () => {
|
|
// 操作日志(Y-Log):在清除 token1 之前上报登出(此时 token1 还在),keepalive 保证发出。
|
|
writeYLog({
|
|
optType: OptType.logout,
|
|
optDetail: "CM助手-全量-登出",
|
|
url: "/api/v1/auth/logout",
|
|
keepalive: true,
|
|
});
|
|
clearStoredAuth();
|
|
clearYLogToken();
|
|
// Match the rest of the app's logout flow: blow away the cookie session
|
|
// by hitting the gateway logout endpoint and then forcing a full reload
|
|
// so any cached query state goes too.
|
|
void apiFetch(`${getBackendBaseURL()}/api/v1/auth/logout`, { method: "POST" })
|
|
.catch(() => undefined)
|
|
.finally(() => {
|
|
window.location.assign(`${window.location.pathname}#/login`);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<SettingsSection title="个人信息" description="当前登录账号的基本信息。">
|
|
<div className="flex flex-col gap-4 rounded-xl border bg-white p-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-full">
|
|
<UserIcon className="size-5" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-sm font-medium">{displayName}</div>
|
|
<div className="text-muted-foreground truncate text-xs">
|
|
{roleLabel(auth.system_role)}
|
|
</div>
|
|
</div>
|
|
<Badge variant={auth.system_role === "admin" ? "default" : "secondary"}>
|
|
{roleLabel(auth.system_role)}
|
|
</Badge>
|
|
</div>
|
|
|
|
<InfoRow
|
|
label="用户名"
|
|
value={displayName}
|
|
onCopy={() => copy(displayName, "用户名已复制")}
|
|
/>
|
|
<InfoRow
|
|
label="用户 ID"
|
|
value={auth.user_id}
|
|
onCopy={() => copy(auth.user_id, "用户 ID 已复制")}
|
|
/>
|
|
<InfoRow label="账号角色" value={roleLabel(auth.system_role)} />
|
|
<InfoRow
|
|
label="账号状态"
|
|
value={auth.needs_setup ? "待初始化" : "已就绪"}
|
|
/>
|
|
|
|
<div className="flex justify-end pt-2">
|
|
<Button variant="outline" size="sm" onClick={handleLogout}>
|
|
<LogOutIcon className="size-4" />
|
|
退出登录
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</SettingsSection>
|
|
<PasswordSettings key={auth.user_id} userId={auth.user_id} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InfoRow({
|
|
label,
|
|
value,
|
|
onCopy,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onCopy?: () => void;
|
|
}) {
|
|
return (
|
|
<div className="flex items-center gap-3 border-t pt-3 first:border-t-0 first:pt-0">
|
|
<div className="text-muted-foreground w-20 shrink-0 text-xs">{label}</div>
|
|
<div className="min-w-0 flex-1 truncate text-sm">{value || "—"}</div>
|
|
{onCopy && value && (
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="size-7"
|
|
onClick={onCopy}
|
|
aria-label={`复制${label}`}
|
|
>
|
|
<CopyIcon className="size-3.5" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|