完善后台样式

This commit is contained in:
Azmat@qq.com
2026-08-20 11:06:22 +08:00
parent b883fed981
commit c7520e93b7
2 changed files with 537 additions and 188 deletions
+131 -88
View File
@@ -1,6 +1,8 @@
import { useEffect, useState } from "react";
import { ArrowLeft, ArrowRight, LogOut } from "lucide-react";
import { IconKitSvg } from "../../components/IconKitSvg";
import { CornerMarks, Decorations, ToastLike } from "../../components/app-shell";
import { ConfirmModal } from "../../components/overlays";
import type { Team, User } from "../../types";
import type { NavigateFn } from "../route-config";
import { AdminLedgersPage, AdminQuotaPage } from "./admin-billing";
@@ -15,8 +17,8 @@ import { AdminTeamsPage, AdminUsersPage } from "./admin-teams-users";
export type AdminNotify = (type: "success" | "error" | "info", text: string) => void;
// 平台超管后台 · 分阶段上线。section slug 与 URL /admin/<slug> 对应("" = 概览)。
// 视觉一律复用 restraint 外壳类(.app/.sidebar/.topbar/.content/.nav-section),只加极少 admin 专属样式
// 平台超管后台 · section slug 与 URL /admin/<slug> 对应("" = 概览)。
// 外壳对齐影擎侧栏 / 顶栏,页面样式收敛在 admin-page.css
export type AdminSection = {
slug: string;
label: string;
@@ -41,7 +43,29 @@ export const ADMIN_SECTIONS: AdminSection[] = [
{ slug: "governance", label: "治理", icon: "sliders", group: "系统", phase: 9 }
];
const SECTION_GROUPS = ["概览", "团队 · 用户", "内容", "生成", "财务", "系统"];
const SECTION_GROUPS = ["团队 · 用户", "内容", "生成", "财务", "系统"];
const SECTION_BLURB: Record<string, string> = {
invites: "发放开团队码,开通新团队",
teams: "团队状态、成员与定价系数",
users: "停用账号、重置登录密码",
prompts: "生图 / 视频提示词模板",
quality: "各生成阶段的画质与风格词",
reviews: "人像资产送审与结果",
tasks: "生成任务、重试与异常",
providers: "模型供应商与可用模型",
billing: "积分流水与手动调额",
quota: "团队额度与消耗策略",
governance: "数据体检与项目监控"
};
const GROUP_HINT: Record<string, string> = {
"团队 · 用户": "邀请开通、团队状态与账号",
"内容": "提示词、质量词与人像审核",
"生成": "任务队列与模型供应商",
"财务": "积分流水与额度策略",
"系统": "数据体检与项目监控"
};
type AdminAppProps = {
section: string;
@@ -55,6 +79,7 @@ type AdminAppProps = {
export function AdminApp({ section, user, team, navigateAdmin, navigate, logout }: AdminAppProps) {
const active = ADMIN_SECTIONS.find((s) => s.slug === section) || ADMIN_SECTIONS[0];
const [toast, setToast] = useState<{ type: "success" | "error" | "info"; text: string } | null>(null);
const [logoutOpen, setLogoutOpen] = useState(false);
const notify: AdminNotify = (type, text) => setToast({ type, text });
// 进后台任一页都滚到顶,行为与主壳 navigate 一致
@@ -85,86 +110,86 @@ export function AdminApp({ section, user, team, navigateAdmin, navigate, logout
<span className="brand-clip"><img className="brand-logo" src="/assets/yz/logo-horizontal.png" alt="影擎" /></span>
</a>
</div>
<div className="admin-badge mono">[ PLATFORM ADMIN ]</div>
{SECTION_GROUPS.map((group) => {
const items = ADMIN_SECTIONS.filter((s) => s.group === group);
if (items.length === 0) return null;
return (
<div key={group} className="admin-nav-group">
<div className="nav-section">{group}</div>
<nav>
{items.map((item) => (
<a
key={item.slug || "overview"}
href={item.slug ? `/admin/${item.slug}` : "/admin"}
className={active.slug === item.slug ? "active" : ""}
title={item.label}
aria-label={item.label}
onClick={(event) => {
event.preventDefault();
navigateAdmin(item.slug);
}}
>
<IconKitSvg name={item.icon} />
<span>{item.label}</span>
</a>
))}
</nav>
</div>
);
})}
<div className="aside-foot admin-foot">
{team && (
<div className="nav-panel">
<div className="admin-rail">
<p className="admin-rail-mark"></p>
<button
type="button"
className="btn btn-ghost btn-sm admin-back"
onClick={() => navigate("dashboard")}
className={`admin-link${active.slug === "" ? " active" : ""}`}
onClick={() => navigateAdmin("")}
>
<IconKitSvg name="chevronLeft" size={14} />
<IconKitSvg name="dashboard" />
<span></span>
</button>
)}
<div className="admin-user">
<div className="av">{(user.username || "A").slice(0, 1).toUpperCase()}</div>
<div className="admin-user-meta">
<span className="nm">{user.username}</span>
<span className="rl mono"></span>
{SECTION_GROUPS.map((group) => {
const items = ADMIN_SECTIONS.filter((s) => s.group === group);
if (items.length === 0) return null;
return (
<div key={group} className="admin-nav-group">
<div className="admin-group">{group}</div>
{items.map((item) => (
<button
key={item.slug}
type="button"
className={`admin-link${active.slug === item.slug ? " active" : ""}`}
onClick={() => navigateAdmin(item.slug)}
>
<IconKitSvg name={item.icon} />
<span>{item.label}</span>
</button>
))}
</div>
);
})}
</div>
<div className="admin-foot">
{team && (
<button type="button" className="admin-back" onClick={() => navigate("dashboard")}>
<ArrowLeft size={16} strokeWidth={1.8} />
</button>
)}
<div className="admin-user">
<div className="av">{(user.username || "A").slice(0, 1).toUpperCase()}</div>
<div className="admin-user-meta">
<span className="nm">{user.username}</span>
<span className="rl"></span>
</div>
<button type="button" className="admin-logout" title="退出登录" aria-label="退出登录" onClick={() => setLogoutOpen(true)}>
<LogOut size={16} strokeWidth={1.8} />
</button>
</div>
<button type="button" className="icon-btn admin-logout" title="退出登录" aria-label="退出登录" onClick={logout}>
<IconKitSvg name="logOut" size={16} />
</button>
</div>
</div>
</aside>
<main>
<Decorations />
<header className="topbar">
<div className="crumbs">
<a
href="/admin"
onClick={(event) => {
event.preventDefault();
navigateAdmin("");
}}
>
</a>
{active.slug && (
<>
<span className="sep">/</span>
<span className="here">{active.label}</span>
</>
)}
<div className="admin-crumb">
<button type="button" onClick={() => navigateAdmin("")}></button>
{active.slug ? <span>{active.label}</span> : null}
</div>
<div className="right">
<span className="pill info"><span className="dot" /></span>
<span className="admin-mode"></span>
</div>
</header>
<div className="content" id="page-content">
<CornerMarks />
{toast && <ToastLike notice={toast} />}
<AdminSectionView section={active} navigateAdmin={navigateAdmin} notify={notify} />
<div className="admin-page">
<AdminSectionView section={active} navigateAdmin={navigateAdmin} notify={notify} />
</div>
</div>
</main>
<ConfirmModal
open={logoutOpen}
title="退出登录"
icon={<LogOut size={16} />}
detail="确定退出当前超管账号?退出后需要重新登录才能进入平台后台。"
confirmText="确认退出"
onCancel={() => setLogoutOpen(false)}
onConfirm={() => { setLogoutOpen(false); logout(); }}
/>
</div>
);
}
@@ -210,34 +235,54 @@ function AdminSectionView({ section, navigateAdmin, notify }: { section: AdminSe
return <AdminPlaceholder section={section} />;
}
function AdminOverview({ navigateAdmin }: { navigateAdmin: (s: string) => void }) {
const shortcuts = ADMIN_SECTIONS.filter((s) => s.slug);
const OVERVIEW_COLS = [
["团队 · 用户", "财务"],
["内容", "系统"],
["生成"]
];
function OverviewGroup({ group, navigateAdmin }: { group: string; navigateAdmin: (s: string) => void }) {
const items = ADMIN_SECTIONS.filter((s) => s.group === group);
if (items.length === 0) return null;
return (
<>
<div className="page-head">
<div>
<h1></h1>
<div className="sub">
<span className="mono"></span> ·
</div>
</div>
<section className="admin-group-block">
<div className="admin-group-label">
<strong>{group}</strong>
<span>{GROUP_HINT[group]}</span>
</div>
<div className="tip admin-tip">
<strong></strong>
线 ,线
</div>
<div className="admin-shortcut-grid">
{shortcuts.map((s) => (
<button key={s.slug} type="button" className="shortcut" onClick={() => navigateAdmin(s.slug)}>
<span className="ic"><IconKitSvg name={s.icon} size={16} /></span>
<span className="admin-shortcut-text">
<span className="t">{s.label}</span>
<span className="d">{s.slug}</span>
<div className="admin-entry-pair">
{items.map((s) => (
<button key={s.slug} className="admin-entry" type="button" onClick={() => navigateAdmin(s.slug)}>
<IconKitSvg name={s.icon} size={22} strokeWidth={1.8} />
<span className="admin-entry-copy">
<strong>{s.label}</strong>
<small>{SECTION_BLURB[s.slug] || s.slug}</small>
</span>
<ArrowRight size={18} strokeWidth={1.8} />
</button>
))}
</div>
</>
</section>
);
}
function AdminOverview({ navigateAdmin }: { navigateAdmin: (s: string) => void }) {
return (
<div className="admin-overview">
<section className="admin-welcome">
<h1></h1>
<p></p>
</section>
<div className="admin-groups">
{OVERVIEW_COLS.map((col) => (
<div className="admin-col" key={col[0]}>
{col.map((group) => (
<OverviewGroup key={group} group={group} navigateAdmin={navigateAdmin} />
))}
</div>
))}
</div>
</div>
);
}
@@ -247,9 +292,7 @@ function AdminPlaceholder({ section }: { section: AdminSection }) {
<div className="page-head">
<div>
<h1>{section.label}</h1>
<div className="sub">
<span className="mono">admin · {section.slug}</span>
</div>
<p>admin · {section.slug}</p>
</div>
</div>
<div className="empty-state show">