完善前端UI
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { AlertCircle, Boxes, Check, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
||||
import { AlertCircle, Boxes, Check, ChevronDown, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
||||
import { IconKitSvg } from "./IconKitSvg";
|
||||
import { useBodyScrollLock } from "./overlays";
|
||||
import { useBodyScrollLock, useOverlayTransition } from "./overlays";
|
||||
import type { Product, Project, Team, User } from "../types";
|
||||
import type { Notice, Page } from "../routes/route-config";
|
||||
|
||||
@@ -18,8 +18,8 @@ const SHELL_COMMANDS: Command[] = [
|
||||
{ id: "free-create", group: "导航", label: "自由创作", sub: "AI 视频生成 · 全能参考 / 首尾帧", page: "freeCreate", icon: "film", key: "F" },
|
||||
{ id: "library", group: "导航", label: "成品库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
|
||||
{ id: "team", group: "导航", label: "团队", sub: "成员、权限、额度、协作记录", page: "team", icon: "users" },
|
||||
{ id: "account", group: "导航", label: "消费", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
||||
{ id: "settings", group: "导航", label: "设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
||||
{ id: "account", group: "导航", label: "账单库", sub: "余额、充值、账单流水", page: "account", icon: "creditCard" },
|
||||
{ id: "settings", group: "导航", label: "系统设置", sub: "个人信息、通知、安全、偏好", page: "settings", icon: "settings" },
|
||||
{ id: "messages", group: "常用动作", label: "消息中心", sub: "任务提醒、协作评论、系统通知", page: "messages", icon: "bell", key: "M" },
|
||||
{ id: "new-product", group: "常用动作", label: "新建商品", sub: "从商品信息开始生成素材与视频", page: "productCreateUpload", icon: "productPlus" },
|
||||
{ id: "new-project", group: "常用动作", label: "新建视频项目", sub: "选择商品并进入脚本配置", page: "projectWizard", icon: "clapperboard" },
|
||||
@@ -30,6 +30,7 @@ const SHELL_COMMANDS: Command[] = [
|
||||
|
||||
function CommandPalette({ open, onClose, navigate, canManageBilling = true }: { open: boolean; onClose: () => void; navigate: Navigate; canManageBilling?: boolean }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const { mounted, show } = useOverlayTransition(open, onClose, 220);
|
||||
useBodyScrollLock(open);
|
||||
useEffect(() => { if (open) setQuery(""); }, [open]);
|
||||
const items = useMemo(() => {
|
||||
@@ -40,13 +41,13 @@ function CommandPalette({ open, onClose, navigate, canManageBilling = true }: {
|
||||
.filter((cmd) => !q || [cmd.label, cmd.sub, cmd.group, cmd.id].join(" ").toLowerCase().includes(q));
|
||||
}, [query, canManageBilling]);
|
||||
const run = (cmd: Command) => { onClose(); navigate(cmd.page); };
|
||||
if (!open) return null;
|
||||
if (!mounted) return null;
|
||||
let lastGroup = "";
|
||||
return createPortal(
|
||||
<div
|
||||
id="shell-command-bg"
|
||||
className="shell-command-bg show"
|
||||
aria-hidden="false"
|
||||
className={`shell-command-bg${show ? " show" : ""}`}
|
||||
aria-hidden={!show}
|
||||
onClick={(event) => { if (event.target === event.currentTarget) onClose(); }}
|
||||
>
|
||||
<div className="shell-command" role="dialog" aria-modal="true" aria-label="命令面板">
|
||||
@@ -108,11 +109,12 @@ const ACCOUNT_ITEMS: { act: Page; icon: string; label: string }[] = [
|
||||
{ act: "settings", icon: "settings", label: "个人设置" },
|
||||
{ act: "messages", icon: "bell", label: "消息中心" },
|
||||
{ act: "team", icon: "users", label: "团队管理" },
|
||||
{ act: "account", icon: "creditCard", label: "消费与余额" }
|
||||
{ act: "account", icon: "creditCard", label: "账单库" }
|
||||
];
|
||||
|
||||
export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: {
|
||||
anchorRect: DOMRect;
|
||||
export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: {
|
||||
open: boolean;
|
||||
anchorRect: DOMRect | null;
|
||||
onClose: () => void;
|
||||
navigate: Navigate;
|
||||
logout: () => void;
|
||||
@@ -120,24 +122,32 @@ export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team,
|
||||
team: Team | null;
|
||||
canManageBilling?: boolean;
|
||||
}) {
|
||||
const { mounted, show } = useOverlayTransition(open, undefined, 180);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>({ left: anchorRect.left, top: anchorRect.bottom + 8 });
|
||||
const lastRect = useRef<DOMRect | null>(anchorRect);
|
||||
if (anchorRect) lastRect.current = anchorRect;
|
||||
const rect = lastRect.current;
|
||||
const [pos, setPos] = useState<{ left: number; top: number }>(() => (
|
||||
rect ? { left: rect.left, top: rect.bottom + 8 } : { left: 0, top: 0 }
|
||||
));
|
||||
|
||||
// 量出真实菜单尺寸后再定位(右对齐锚点、上下翻转防溢出),与 V1 toggleAccountMenu 一致
|
||||
useLayoutEffect(() => {
|
||||
if (!rect) return;
|
||||
const menu = menuRef.current;
|
||||
const width = menu?.offsetWidth || 232;
|
||||
const height = menu?.offsetHeight || 260;
|
||||
let left = anchorRect.right - width;
|
||||
if (left < 12) left = anchorRect.left;
|
||||
let left = rect.right - width;
|
||||
if (left < 12) left = rect.left;
|
||||
left = Math.min(Math.max(12, left), window.innerWidth - width - 12);
|
||||
let top = anchorRect.bottom + 8;
|
||||
if (top + height > window.innerHeight - 12) top = Math.max(12, anchorRect.top - height - 8);
|
||||
let top = rect.bottom + 8;
|
||||
if (top + height > window.innerHeight - 12) top = Math.max(12, rect.top - height - 8);
|
||||
setPos({ left, top });
|
||||
}, [anchorRect]);
|
||||
}, [rect]);
|
||||
|
||||
// 点菜单外部 / Esc → 收起(锚点本身由触发器的 toggle 处理,避免点锚点立刻又被关掉)
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (menuRef.current?.contains(target)) return;
|
||||
@@ -151,15 +161,16 @@ export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team,
|
||||
document.removeEventListener("mousedown", onDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [onClose]);
|
||||
}, [open, onClose]);
|
||||
|
||||
const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase();
|
||||
const go = (page: Page) => { onClose(); navigate(page); };
|
||||
if (!mounted || !rect) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="shell-account-menu show"
|
||||
className={`shell-account-menu${show ? " show" : ""}`}
|
||||
id="shell-account-menu"
|
||||
role="menu"
|
||||
aria-label="账户菜单"
|
||||
@@ -199,19 +210,31 @@ const NAV: NavDef[] = [
|
||||
{ id: "free-create", page: "freeCreate", label: "自由创作", icon: "film" },
|
||||
{ id: "library", page: "library", label: "成品库", icon: "library" },
|
||||
{ id: "team", page: "team", label: "团队", icon: "users" },
|
||||
{ id: "account", page: "account", label: "消费", icon: "creditCard" },
|
||||
{ id: "account", page: "account", label: "账单库", icon: "creditCard" },
|
||||
{ id: "trash", page: "trash", label: "垃圾桶", icon: "trash" },
|
||||
{ id: "settings", page: "settings", label: "设置", icon: "settings" }
|
||||
{ id: "settings", page: "settings", label: "系统设置", icon: "settings" }
|
||||
];
|
||||
const RESOURCE_NAV: Array<{ id: string; page: Page; label: string }> = [
|
||||
{ id: "products", page: "products", label: "商品库" },
|
||||
{ id: "models", page: "models", label: "模特库" },
|
||||
{ id: "library", page: "library", label: "成品库" },
|
||||
// 成品库先隐藏,后续再加回资源中心
|
||||
{ id: "account", page: "account", label: "消费" },
|
||||
{ id: "account", page: "account", label: "账单库" },
|
||||
{ id: "trash", page: "trash", label: "垃圾桶" },
|
||||
];
|
||||
|
||||
type MajorNav = "resource" | "team" | "settings";
|
||||
const MAJOR_META: Record<MajorNav, { label: string; icon: "boxes" | "users-round" | "settings" }> = {
|
||||
resource: { label: "资源中心", icon: "boxes" },
|
||||
team: { label: "团队中心", icon: "users-round" },
|
||||
settings: { label: "系统设置", icon: "settings" },
|
||||
};
|
||||
|
||||
function LiquidIcon({ name }: { name: "boxes" | "users-round" | "settings" }) {
|
||||
if (name === "boxes") return <Boxes size={20} strokeWidth={1.9} />;
|
||||
if (name === "users-round") return <UsersRound size={20} strokeWidth={1.9} />;
|
||||
return <Settings size={20} strokeWidth={1.9} />;
|
||||
}
|
||||
|
||||
export type TopModule = "workbench" | "image" | "video";
|
||||
const OPEN_PALETTE_EVENT = "airshelf:open-palette";
|
||||
|
||||
@@ -233,6 +256,102 @@ export function topModuleForPage(page: Page): TopModule | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MODE_TABS: { id: TopModule; label: string; page: Page }[] = [
|
||||
{ id: "workbench", label: "工作台", page: "dashboard" },
|
||||
{ id: "image", label: "图片创作", page: "assetFactory" },
|
||||
{ id: "video", label: "视频创作", page: "projects" },
|
||||
];
|
||||
|
||||
// 顶栏三按钮滑动黑胶囊 · 对照影擎 .mode-tabs / moveModeIndicator 原样接入:
|
||||
// clip-path 只露出三个按钮形状,胶囊在缝里看不见;位移走 transform,不在 React state 里改尺寸。
|
||||
export function ModeTabs({ active, navigate }: { active: TopModule | null; navigate: Navigate }) {
|
||||
const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const clipRectsRef = useRef<Array<SVGRectElement | null>>([]);
|
||||
const indicatorRef = useRef<HTMLSpanElement>(null);
|
||||
const firstPaint = useRef(true);
|
||||
|
||||
const moveModeIndicator = (tab: HTMLButtonElement, animate = true) => {
|
||||
btnRefs.current.forEach((item, index) => {
|
||||
const rect = clipRectsRef.current[index];
|
||||
if (!item || !rect) return;
|
||||
rect.setAttribute("x", String(item.offsetLeft));
|
||||
rect.setAttribute("y", "0");
|
||||
rect.setAttribute("width", String(item.offsetWidth));
|
||||
rect.setAttribute("height", String(item.offsetHeight));
|
||||
});
|
||||
const modeIndicator = indicatorRef.current;
|
||||
if (!modeIndicator) return;
|
||||
if (!animate) modeIndicator.style.transition = "none";
|
||||
modeIndicator.style.width = `${tab.offsetWidth}px`;
|
||||
modeIndicator.style.transform = `translateX(${tab.offsetLeft}px)`;
|
||||
if (!animate) {
|
||||
void modeIndicator.offsetWidth;
|
||||
modeIndicator.style.transition = "";
|
||||
}
|
||||
};
|
||||
|
||||
const activeButton = () => {
|
||||
const idx = active ? MODE_TABS.findIndex((tab) => tab.id === active) : -1;
|
||||
return idx >= 0 ? btnRefs.current[idx] : null;
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const tab = activeButton();
|
||||
if (tab) moveModeIndicator(tab, !firstPaint.current);
|
||||
firstPaint.current = false;
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
const snap = () => {
|
||||
const tab = activeButton();
|
||||
if (tab) moveModeIndicator(tab, false);
|
||||
};
|
||||
window.addEventListener("resize", snap);
|
||||
void document.fonts?.ready.then(snap);
|
||||
return () => window.removeEventListener("resize", snap);
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`yz-mods${active ? "" : " no-active"}`}
|
||||
role="tablist"
|
||||
aria-label="创作模块"
|
||||
>
|
||||
<svg className="yz-mod-clip-defs" width="0" height="0" aria-hidden="true" focusable="false">
|
||||
<defs>
|
||||
<clipPath id="yzModeButtonMask" clipPathUnits="userSpaceOnUse">
|
||||
<rect ref={(el) => { clipRectsRef.current[0] = el; }} rx="22" ry="22" x="0" y="0" width="104" height="44" />
|
||||
<rect ref={(el) => { clipRectsRef.current[1] = el; }} rx="22" ry="22" x="120" y="0" width="110" height="44" />
|
||||
<rect ref={(el) => { clipRectsRef.current[2] = el; }} rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="yz-mod-indicator-mask" aria-hidden="true">
|
||||
<span
|
||||
ref={indicatorRef}
|
||||
className="yz-mod-indicator"
|
||||
style={{ width: 104, transform: "translateX(0px)" }}
|
||||
/>
|
||||
</span>
|
||||
{MODE_TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
ref={(el) => { btnRefs.current[index] = el; }}
|
||||
className={`yz-mod${active === tab.id ? " active" : ""}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active === tab.id}
|
||||
onClick={() => {
|
||||
const btn = btnRefs.current[index];
|
||||
if (btn) moveModeIndicator(btn, true);
|
||||
navigate(tab.page);
|
||||
}}
|
||||
>{tab.label}</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
|
||||
dashboard: "dashboard",
|
||||
products: "products",
|
||||
@@ -252,6 +371,7 @@ const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
|
||||
library: "library",
|
||||
team: "team",
|
||||
account: "account",
|
||||
trash: "trash",
|
||||
settings: "settings",
|
||||
settingsNotify: "settings"
|
||||
};
|
||||
@@ -276,7 +396,7 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
const navItems = NAV.filter((item) => canManageBilling || (item.page !== "team" && item.page !== "account"));
|
||||
const resourceNavItems = RESOURCE_NAV.filter((item) => navItems.some((nav) => nav.page === item.page));
|
||||
const topModule = topModuleForPage(page);
|
||||
const activeMajor: "resource" | "team" | "settings" | null =
|
||||
const activeMajor: MajorNav | null =
|
||||
activeNav === "team" ? "team"
|
||||
: activeNav === "settings" ? "settings"
|
||||
: topModule ? null
|
||||
@@ -305,12 +425,72 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [mobileNavOpen]);
|
||||
// 二级类目展开/收起:进资源中心时展开;切到顶栏模块(工作台/图片/视频)时收起,避免左边再出现选中。
|
||||
const [resourceOpen, setResourceOpen] = useState(activeMajor === "resource");
|
||||
// 二级类目:跟当前大类走,同一帧收起,避免液态胶囊先飞到「展开后的位置」再追回来。
|
||||
const [resourcePinned, setResourcePinned] = useState(true);
|
||||
useEffect(() => {
|
||||
if (activeMajor === "resource") setResourceOpen(true);
|
||||
else if (topModule) setResourceOpen(false);
|
||||
}, [activeMajor, topModule]);
|
||||
if (activeMajor === "resource") setResourcePinned(true);
|
||||
}, [activeMajor]);
|
||||
const resourceOpen = activeMajor === "resource" && resourcePinned;
|
||||
|
||||
const resourceItemRef = useRef<HTMLButtonElement>(null);
|
||||
const teamItemRef = useRef<HTMLButtonElement>(null);
|
||||
const settingsItemRef = useRef<HTMLButtonElement>(null);
|
||||
const submenuRef = useRef<HTMLDivElement>(null);
|
||||
const prevMajorRef = useRef<MajorNav | null>(activeMajor);
|
||||
const [liquid, setLiquid] = useState({ y: 0, hidden: true, pulse: false });
|
||||
const skipPulse = useRef(true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const item = activeMajor === "resource" ? resourceItemRef.current
|
||||
: activeMajor === "team" ? teamItemRef.current
|
||||
: activeMajor === "settings" ? settingsItemRef.current
|
||||
: null;
|
||||
const leavingResource = prevMajorRef.current === "resource" && activeMajor !== "resource";
|
||||
prevMajorRef.current = activeMajor;
|
||||
|
||||
const targetY = (el: HTMLElement, subtractSubmenu: boolean) => {
|
||||
let top = el.offsetTop;
|
||||
if (subtractSubmenu && submenuRef.current) {
|
||||
const style = window.getComputedStyle(submenuRef.current);
|
||||
const space = submenuRef.current.offsetHeight
|
||||
+ (Number.parseFloat(style.marginTop) || 0)
|
||||
+ (Number.parseFloat(style.marginBottom) || 0);
|
||||
if (space > 1) top -= space;
|
||||
}
|
||||
return top - Math.max(0, (76 - el.offsetHeight) / 2);
|
||||
};
|
||||
|
||||
if (!item) {
|
||||
setLiquid((prev) => ({ ...prev, hidden: true, pulse: false }));
|
||||
skipPulse.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const animate = !skipPulse.current;
|
||||
skipPulse.current = false;
|
||||
setLiquid({
|
||||
y: targetY(item, leavingResource),
|
||||
hidden: false,
|
||||
pulse: animate,
|
||||
});
|
||||
|
||||
const onResize = () => {
|
||||
const current = activeMajor === "resource" ? resourceItemRef.current
|
||||
: activeMajor === "team" ? teamItemRef.current
|
||||
: activeMajor === "settings" ? settingsItemRef.current
|
||||
: null;
|
||||
if (!current) return;
|
||||
setLiquid((prev) => ({ ...prev, y: targetY(current, false), pulse: false }));
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [activeMajor, canManageBilling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!liquid.pulse) return;
|
||||
const timer = window.setTimeout(() => setLiquid((prev) => ({ ...prev, pulse: false })), 520);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [liquid.pulse]);
|
||||
|
||||
// 命令面板:Ctrl/Cmd K,以及顶栏搜索按钮
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
@@ -354,64 +534,90 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
<span className="sidebar-toggle-icon sidebar-toggle-icon--collapse"><IconKitSvg name="chevronLeft" size={18} strokeWidth={1.8} /></span>
|
||||
<span className="sidebar-toggle-icon sidebar-toggle-icon--expand"><IconKitSvg name="chevronRight" size={18} strokeWidth={1.8} /></span>
|
||||
</button>
|
||||
<div className="yz-side-main">
|
||||
<button
|
||||
className={`yz-resource-head${activeMajor === "resource" ? " active" : ""}`}
|
||||
type="button"
|
||||
aria-expanded={resourceOpen}
|
||||
onClick={() => {
|
||||
// 在资源中心内点击=折叠/展开;在其它大类点击=切回资源中心并展开
|
||||
if (activeMajor !== "resource") {
|
||||
setResourceOpen(true);
|
||||
navigate("products");
|
||||
return;
|
||||
}
|
||||
setResourceOpen((value) => !value);
|
||||
}}
|
||||
>
|
||||
<span className="major-dot" aria-hidden="true" />
|
||||
<Boxes size={16} strokeWidth={2} />
|
||||
<span>资源中心</span>
|
||||
<span className="chev"><IconKitSvg name="chevronRight" /></span>
|
||||
</button>
|
||||
<div className={`yz-resource-sub${resourceOpen ? " open" : " collapsed"}`}>
|
||||
{resourceNavItems.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`/${item.id}`}
|
||||
className={activeNav === item.page ? "active" : ""}
|
||||
title={item.label}
|
||||
aria-label={item.label}
|
||||
onClick={(event) => { event.preventDefault(); setMobileNavOpen(false); navigate(item.page); }}
|
||||
<div className="nav-panel">
|
||||
<div className="nav-list">
|
||||
<div
|
||||
className={`liquid-indicator${liquid.hidden ? " is-hidden" : ""}${activeMajor && MAJOR_META[activeMajor].label.length >= 4 ? " wide-label" : ""}${liquid.pulse ? " pulse" : ""}`}
|
||||
aria-hidden="true"
|
||||
style={{ transform: `translateY(${liquid.y}px)` }}
|
||||
>
|
||||
<svg className="liquid-shape" viewBox="0 0 132 76" preserveAspectRatio="none">
|
||||
<path fill="#101012" d="M0 11H103C120 11 130 22 130 38C130 54 120 65 103 65H0Z" />
|
||||
</svg>
|
||||
{activeMajor && (
|
||||
<div className="indicator-content">
|
||||
<span className="indicator-icon-slot"><LiquidIcon name={MAJOR_META[activeMajor].icon} /></span>
|
||||
<span>{MAJOR_META[activeMajor].label}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
ref={resourceItemRef}
|
||||
className={`nav-item${activeMajor === "resource" ? " active" : ""}`}
|
||||
type="button"
|
||||
aria-expanded={resourceOpen}
|
||||
aria-controls="resourceSubmenu"
|
||||
onClick={() => {
|
||||
if (activeMajor !== "resource") {
|
||||
setResourcePinned(true);
|
||||
navigate("products");
|
||||
return;
|
||||
}
|
||||
setResourcePinned((value) => !value);
|
||||
}}
|
||||
>
|
||||
<span className="nav-inner">
|
||||
<Boxes size={23} strokeWidth={1.8} />
|
||||
<span className="nav-label">资源中心</span>
|
||||
<ChevronDown className="nav-chevron" size={17} strokeWidth={1.8} />
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
ref={submenuRef}
|
||||
id="resourceSubmenu"
|
||||
className={`nav-submenu${resourceOpen ? " expanded" : ""}`}
|
||||
aria-label="资源中心二级导航"
|
||||
aria-hidden={!resourceOpen}
|
||||
>
|
||||
{resourceNavItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`nav-subitem${activeNav === item.page ? " active" : ""}`}
|
||||
onClick={() => { setMobileNavOpen(false); navigate(item.page); }}
|
||||
>
|
||||
{item.label}
|
||||
{badges[item.id] !== undefined && <span className="pill-mini">{badges[item.id]}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{canManageBilling && (
|
||||
<button
|
||||
ref={teamItemRef}
|
||||
className={`nav-item${activeMajor === "team" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => { setMobileNavOpen(false); navigate("team"); }}
|
||||
>
|
||||
<span className="dot" />
|
||||
<span>{item.label}</span>
|
||||
{badges[item.id] !== undefined && <span className="pill-mini">{badges[item.id]}</span>}
|
||||
</a>
|
||||
))}
|
||||
<span className="nav-inner">
|
||||
<UsersRound size={23} strokeWidth={1.8} />
|
||||
<span className="nav-label">团队中心</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
ref={settingsItemRef}
|
||||
className={`nav-item${activeMajor === "settings" ? " active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => { setMobileNavOpen(false); navigate("settings"); }}
|
||||
>
|
||||
<span className="nav-inner">
|
||||
<Settings size={23} strokeWidth={1.8} />
|
||||
<span className="nav-label">系统设置</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
className={`yz-side-link${activeMajor === "team" ? " active" : ""}`}
|
||||
href="/team"
|
||||
onClick={(event) => { event.preventDefault(); setMobileNavOpen(false); navigate("team"); }}
|
||||
>
|
||||
<span className="major-dot" aria-hidden="true" />
|
||||
<UsersRound size={18} strokeWidth={2} />
|
||||
<span>团队中心</span>
|
||||
</a>
|
||||
<a
|
||||
className={`yz-side-link${activeMajor === "settings" ? " active" : ""}`}
|
||||
href="/settings"
|
||||
onClick={(event) => { event.preventDefault(); setMobileNavOpen(false); navigate("settings"); }}
|
||||
>
|
||||
<span className="major-dot" aria-hidden="true" />
|
||||
<Settings size={18} strokeWidth={2} />
|
||||
<span>设置</span>
|
||||
</a>
|
||||
</div>
|
||||
{user.is_platform_admin && onOpenAdmin && (
|
||||
<>
|
||||
<div className="nav-section">平台</div>
|
||||
{user.is_platform_admin && onOpenAdmin && (
|
||||
<nav>
|
||||
<a
|
||||
href="/admin"
|
||||
@@ -423,8 +629,8 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
||||
<span>平台后台</span>
|
||||
</a>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} navigate={navigate} canManageBilling={canManageBilling} />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user