refactor(admin): 管理后台 - 收敛重复边框并提升运维信息密度

This commit is contained in:
ddCat
2026-09-22 06:09:03 +08:00
parent 0b285f66e8
commit 505d6076cc
18 changed files with 489 additions and 167 deletions

View File

@@ -0,0 +1,42 @@
import { createContext, useContext, useState, type ReactNode } from "react";
export type AdminDensity = "compact" | "comfortable";
export function normalizeAdminDensity(value: string | null): AdminDensity {
return value === "comfortable" ? "comfortable" : "compact";
}
export function adminDensityStorageKey(userId: string) {
return `admin-console:${encodeURIComponent(userId)}:density`;
}
const AdminDensityContext = createContext<{ density: AdminDensity; toggleDensity: () => void }>({ density: "compact", toggleDensity: () => {} });
export function AdminDensityProvider({ userId, children }: { userId?: string; children: ReactNode }) {
const [density, setDensity] = useState<AdminDensity>(() => {
if (!userId || typeof window === "undefined") return "compact";
try {
return normalizeAdminDensity(window.localStorage.getItem(adminDensityStorageKey(userId)));
} catch (error) {
console.warn("无法读取后台密度偏好,使用紧凑布局", error);
return "compact";
}
});
const toggleDensity = () => {
const next = density === "compact" ? "comfortable" : "compact";
setDensity(next);
if (!userId) return;
try {
window.localStorage.setItem(adminDensityStorageKey(userId), next);
} catch (error) {
console.warn("后台密度已切换,但无法保存偏好", error);
}
};
return <AdminDensityContext.Provider value={{ density, toggleDensity }}>{children}</AdminDensityContext.Provider>;
}
export function useAdminDensity() {
return useContext(AdminDensityContext);
}

View File

@@ -27,6 +27,7 @@ import {
PlugZap,
RadioTower,
RefreshCw,
Rows3,
Sparkles,
Settings2,
ShieldAlert,
@@ -48,6 +49,7 @@ import { useUserStore } from "@/stores/use-user-store";
import { useAppearanceStore } from "@/stores/use-appearance-store";
import { getIsolatedAdminAntTheme } from "../theme/admin-ant-theme";
import { AdminTooltip } from "../ui/controls";
import { AdminDensityProvider, useAdminDensity } from "./admin-density";
import "@/styles/admin-ui.css";
import "../theme/admin-tokens.css";
import "../theme/admin-chrome.css";
@@ -62,40 +64,42 @@ type AdminNavigationItem = {
const adminNavigation: Array<{ label: string; items: AdminNavigationItem[] }> = [
{
label: "概览",
items: [{ path: "/admin", label: "数据概览", description: "活跃、调用与成本趋势", icon: <BarChart3 className="size-4" /> }],
label: "监控与排查",
items: [
{ path: "/admin", label: "运行总览", description: "请求质量、队列与成本趋势", icon: <BarChart3 className="size-4" /> },
{ path: "/admin/logs", label: "请求明细", description: "上游调用与费用", icon: <FileClock className="size-4" /> },
{ path: "/admin/settings/system-performance", label: "系统性能", description: "主机、数据库与缓存状态", icon: <Activity className="size-4" /> },
],
},
{
label: "平台资源",
label: "模型与资源",
items: [
{ path: "/admin/users", label: "用户管理", description: "账号、角色与状态", icon: <UsersRound className="size-4" /> },
{ path: "/admin/channels", label: "系统渠道", description: "渠道、模型与售价", icon: <RadioTower className="size-4" /> },
{ path: "/admin/models", label: "前台模型", description: "展示、线路与用户价格", icon: <Layers3 className="size-4" />, requireFeature: "frontendModelsEnabled" },
{ path: "/admin/plugins", label: "插件管理", description: "平台可用性、上传与卸载", icon: <PlugZap className="size-4" /> },
{ path: "/admin/prompt-templates", label: "提示词模板", description: "平台创作策略版本", icon: <MessageSquareText className="size-4" /> },
{ path: "/admin/resources", label: "存储资源", description: "资源列表、容量与预览", icon: <Database className="size-4" /> },
],
},
{
label: "运营",
label: "用户与运营",
items: [
{ path: "/admin/users", label: "用户管理", description: "账号、角色与状态", icon: <UsersRound className="size-4" /> },
{ path: "/admin/announcements", label: "系统公告", description: "发布、关闭与历史公告", icon: <BellRing className="size-4" /> },
{ path: "/admin/banner-announcements", label: "常驻通知", description: "首页顶部常驻滚动通知", icon: <Megaphone className="size-4" /> },
{ path: "/admin/agent-lessons", label: "Agent 记忆", description: "按用户查看个人记忆", icon: <Sparkles className="size-4" /> },
{ path: "/admin/payments", label: "支付充值", description: "支付渠道、订单与对账", icon: <CreditCard className="size-4" /> },
{ path: "/admin/credit-operations", label: "积分运营", description: "人工调账与异常计费", icon: <Coins className="size-4" /> },
{ path: "/admin/redemption-codes", label: "兑换码", description: "生成与查看兑换码批次", icon: <TicketCheck className="size-4" /> },
{ path: "/admin/logs", label: "请求明细", description: "上游调用与费用", icon: <FileClock className="size-4" /> },
],
},
{
label: "系统配置",
label: "平台配置",
items: [
{ path: "/admin/plugins", label: "插件管理", description: "平台可用性、上传与卸载", icon: <PlugZap className="size-4" /> },
{ path: "/admin/settings/appearance", label: "站点及外观", description: "品牌、SEO、备案与皮肤", icon: <Palette className="size-4" /> },
{ path: "/admin/settings/features", label: "功能开放", description: "工作台、插件与模型能力", icon: <ToggleLeft className="size-4" /> },
{ path: "/admin/settings/drawing-engine", label: "绘图工具", description: "画布绘图节点默认引擎", icon: <Paintbrush className="size-4" /> },
{ path: "/admin/settings/runtime-policy", label: "资源与策略", description: "配额、并发、频控与超时", icon: <Settings2 className="size-4" /> },
{ path: "/admin/settings/system-performance", label: "系统性能", description: "主机、数据库与缓存状态", icon: <Activity className="size-4" /> },
{ path: "/admin/settings/access", label: "登录与注册", description: "账号创建与第三方登录", icon: <ShieldCheck className="size-4" /> },
{ path: "/admin/settings/email", label: "邮件服务", description: "注册验证码与 SMTP", icon: <Mail className="size-4" /> },
{ path: "/admin/settings/storage", label: "存储服务", description: "对象存储与资源存储", icon: <HardDrive className="size-4" /> },
@@ -119,6 +123,12 @@ function adminPopupContainer(node?: HTMLElement) {
}
export function AdminShell() {
const userId = useUserStore((state) => state.user?.id);
return <AdminDensityProvider key={userId || "anonymous"} userId={userId}><AdminShellLayout /></AdminDensityProvider>;
}
function AdminShellLayout() {
const { density } = useAdminDensity();
const appearance = useAppearanceStore((state) => state.appearance);
const [collapsed, setCollapsed] = useState(readWorkspaceSidebarCollapsed);
const dark = useThemeStore((state) => state.theme === "dark");
@@ -141,7 +151,7 @@ export function AdminShell() {
return (
<ConfigProvider theme={getIsolatedAdminAntTheme(dark, appearance.activeSkin)} getPopupContainer={(node) => adminPopupContainer(node)}>
<App>
<main id="admin-root" data-admin-root className="admin-shell flex h-full min-h-0 overflow-hidden">
<main id="admin-root" data-admin-root data-admin-density={density} className="admin-shell flex h-full min-h-0 overflow-hidden">
<aside className={cn("admin-sidebar hidden shrink-0 flex-col overflow-hidden lg:flex", collapsed && "is-collapsed")}>
<div className="admin-sidebar-identity shrink-0">
<AdminTooltip title={collapsed ? "查看更新日志" : undefined} placement="right">
@@ -223,6 +233,7 @@ export function AdminPageFrame({ title, description, actions, back, scroll = fal
</div>
<div className="admin-page-actions flex shrink-0 flex-wrap items-center">
{actions}
<AdminDensityButton />
<AdminThemeButton />
</div>
</header>
@@ -232,6 +243,18 @@ export function AdminPageFrame({ title, description, actions, back, scroll = fal
);
}
function AdminDensityButton() {
const { density, toggleDensity } = useAdminDensity();
return (
<AdminTooltip title={density === "compact" ? "切换为舒适行距" : "切换为紧凑行距"}>
<button type="button" className="admin-density-toggle" onClick={toggleDensity} aria-label="紧凑表格行距" aria-pressed={density === "compact"}>
<Rows3 className="size-4" aria-hidden="true" />
{density === "compact" ? "紧凑" : "舒适"}
</button>
</AdminTooltip>
);
}
function AdminThemeButton() {
const theme = useThemeStore((state) => state.theme);
const setTheme = useThemeStore((state) => state.setTheme);

View File

@@ -1,7 +1,6 @@
import { Alert, App, Button, DatePicker, Select, Tabs, Tag } from "antd";
import { Tooltip } from "@/pages/admin/ui/controls";
import { useCountUp } from "@/hooks/use-count-up";
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import type { ColumnsType } from "antd/es/table";
import dayjs, { type Dayjs } from "dayjs";
@@ -40,6 +39,8 @@ export default function AnalyticsPanel({ users, channels }: Props) {
const [capability, setCapability] = useState(searchParams.get("capability") || undefined);
const [data, setData] = useState<AdminAnalytics | null>(null);
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState("");
const requestSequence = useRef(0);
const [userOptions, setUserOptions] = useState(users);
const [searchingUsers, setSearchingUsers] = useState(false);
const [modelPage, setModelPage] = useState(1);
@@ -62,14 +63,21 @@ export default function AnalyticsPanel({ users, channels }: Props) {
);
const reload = useCallback(async () => {
const sequence = ++requestSequence.current;
setLoading(true);
setLoadError("");
setData(null);
try {
const analytics = await getAdminAnalytics(filters);
if (sequence !== requestSequence.current) return;
setData(analytics);
} catch (error) {
message.error(error instanceof Error ? error.message : "读取统计数据失败");
if (sequence !== requestSequence.current) return;
const text = error instanceof Error ? error.message : "读取统计数据失败";
setLoadError(text);
message.error(text);
} finally {
setLoading(false);
if (sequence === requestSequence.current) setLoading(false);
}
}, [filters, message]);
@@ -83,6 +91,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
else next.delete("rangePreset");
setSearchParams(next, { replace: true });
void reload();
return () => { requestSequence.current += 1; };
}, [filters, rangePreset]);
useEffect(() => {
@@ -194,7 +203,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
};
return (
<div className="admin-analytics-panel space-y-5">
<div className="admin-analytics-panel" aria-busy={loading}>
<ListToolbar
className="admin-analytics-toolbar"
active={Boolean(userId || model || channelId || capability)}
@@ -266,29 +275,43 @@ export default function AnalyticsPanel({ users, channels }: Props) {
</ListToolbar>
{financeUnavailable && <Alert type="warning" showIcon title="后端未返回完整财务统计,缺失金额显示为 --。请确认后端已更新并重启后刷新。" />}
{loadError && <Alert type="error" showIcon title="统计数据读取失败" description={loadError} action={<Button size="small" onClick={() => void reload()}>重试</Button>} />}
<section className="admin-analytics-health-grid" aria-label="运营健康指标">
<AnalyticsHealthCard
icon={<UsersRound className="size-4" />}
label="活跃用户"
value={data ? <AnimatedHealthValue target={data.kpi.activeUsers} format={formatNumber} /> : "--"}
value={data ? formatNumber(data.kpi.activeUsers) : "--"}
trend={formatCountDelta(currentTrend?.activeUsers, previousTrend?.activeUsers)}
detail={data ? `DAU ${formatNumber(data.kpi.dau)} · WAU ${formatNumber(data.kpi.wau)} · MAU ${formatNumber(data.kpi.mau)}` : undefined}
detail={data ? `日 ${formatNumber(data.kpi.dau)} · 周 ${formatNumber(data.kpi.wau)} · 月 ${formatNumber(data.kpi.mau)}` : undefined}
/>
<AnalyticsHealthCard
icon={<Workflow className="size-4" />}
label="生成任务"
value={data ? <AnimatedHealthValue target={data.kpi.generationTasks} format={formatNumber} /> : "--"}
value={data ? formatNumber(data.kpi.generationTasks) : "--"}
trend={formatCountDelta(currentTrend?.tasks, previousTrend?.tasks)}
detail={data ? `上游请求 ${formatNumber(data.kpi.upstreamRequests)} · 队列 ${formatNumber(data.kpi.currentQueuedTasks)}` : undefined}
detail={data ? `上游请求 ${formatNumber(data.kpi.upstreamRequests)}` : undefined}
/>
<AnalyticsHealthCard
icon={<Gauge className="size-4" />}
label="服务质量"
value={data ? <AnimatedHealthValue target={data.kpi.successRate} format={percent} /> : "--"}
value={data && data.kpi.upstreamRequests > 0 ? percent(data.kpi.successRate) : "--"}
trend={formatRateDelta(currentTrend?.requestSuccessRate, previousTrend?.requestSuccessRate)}
detail={data ? `P95 ${formatDuration(data.kpi.p95DurationMs)}` : undefined}
tone={data && data.kpi.successRate < 90 ? "warning" : "success"}
detail={data ? (data.kpi.upstreamRequests ? "真实上游请求成功率" : "当前范围暂无请求") : undefined}
tone={!data || !data.kpi.upstreamRequests ? "neutral" : data.kpi.successRate < 90 ? "warning" : "success"}
/>
<AnalyticsHealthCard
icon={<Clock3 className="size-4" />}
label="P95 请求耗时"
value={data && data.kpi.upstreamRequests > 0 ? formatDuration(data.kpi.p95DurationMs) : "--"}
detail="筛选范围内的上游请求"
/>
<AnalyticsHealthCard
icon={<Workflow className="size-4" />}
label="当前排队"
value={data ? formatNumber(data.kpi.currentQueuedTasks) : "--"}
detail="实时快照 · 非历史累计"
tone={data?.kpi.currentQueuedTasks ? "warning" : "neutral"}
/>
<AnalyticsHealthCard
icon={<CircleDollarSign className="size-4" />}
@@ -364,8 +387,8 @@ export default function AnalyticsPanel({ users, channels }: Props) {
<span>
<BarChart3 className="size-5" />
</span>
<div className="font-medium">当前范围暂无{trendMetric === "quality" ? "质量" : trendMetric === "activity" ? "活跃" : "使用"}数据</div>
<p>可以调整时间范围或筛选条件后重新查看。</p>
<div className="font-medium">{!data ? (loading ? "正在读取趋势…" : "趋势数据不可用") : `当前范围暂无${trendMetric === "quality" ? "质量" : trendMetric === "activity" ? "活跃" : "使用"}数据`}</div>
<p>{loadError ? "请重试,无法依据缺失数据判断运行状态。" : "按自然日统计,可调整时间范围或筛选条件。"}</p>
</div>
)}
</section>
@@ -376,23 +399,23 @@ export default function AnalyticsPanel({ users, channels }: Props) {
<h2 id="admin-analytics-attention-title">需要关注</h2>
<p>优先展示可能需要处理的运行状态。</p>
</div>
<AdminStatusBadge label={failureTotal > 0 ? `${formatNumber(failureTotal)} 次异常` : "运行平稳"} tone={failureTotal > 0 ? "warning" : "success"} />
<AdminStatusBadge label={!data ? "状态未知" : failureTotal > 0 ? `${formatNumber(failureTotal)} 次异常` : "未记录异常"} tone={!data ? "neutral" : failureTotal > 0 ? "warning" : "success"} />
</div>
<div className="admin-analytics-attention-list">
<AnalyticsAttentionItem
icon={<AlertTriangle className="size-4" />}
label="异常请求"
value={data ? formatNumber(failureTotal) : "--"}
description={topFailure ? `${topFailure.type} · ${topFailure.model}` : "当前范围未记录异常"}
tone={failureTotal > 0 ? "warning" : "success"}
description={!data ? "数据尚未就绪" : topFailure ? `${topFailure.type} · ${topFailure.model}` : "当前范围未记录异常"}
tone={!data ? "neutral" : failureTotal > 0 ? "warning" : "success"}
onClick={failureTotal > 0 ? () => openAnalysis("failures") : undefined}
/>
<AnalyticsAttentionItem
icon={<Clock3 className="size-4" />}
label="当前队列"
value={data ? formatNumber(data.kpi.currentQueuedTasks) : "--"}
description={data?.kpi.currentQueuedTasks ? "存在等待执行的生成任务" : "没有排队中的生成任务"}
tone={data?.kpi.currentQueuedTasks ? "warning" : "success"}
description={!data ? "数据尚未就绪" : data.kpi.currentQueuedTasks ? "存在等待执行的生成任务" : "没有排队中的生成任务"}
tone={!data ? "neutral" : data.kpi.currentQueuedTasks ? "warning" : "success"}
/>
<AnalyticsAttentionItem
icon={<CircleDollarSign className="size-4" />}
@@ -469,11 +492,6 @@ export default function AnalyticsPanel({ users, channels }: Props) {
);
}
function AnimatedHealthValue({ target, format }: { target: number; format: (value: number) => string }) {
const display = useCountUp(target, 420, 0);
return <>{format(display)}</>;
}
function AnalyticsHealthCard({ icon, label, value, trend, detail, tone = "neutral" }: { icon: ReactNode; label: string; value: ReactNode; trend?: { value: string; tone?: AdminStatusTone }; detail?: string; tone?: AdminStatusTone }) {
return (
<article className="admin-analytics-health-card" data-tone={tone}>