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 { IconKitSvg } from "./IconKitSvg"; import { useBodyScrollLock } from "./overlays"; import type { Product, Project, Team, User } from "../types"; import type { Notice, Page } from "../routes/route-config"; const SIDEBAR_COLLAPSED_KEY = "airshelf:sidebar-collapsed"; // 全局命令面板(Ctrl K / 点搜索框)—— 忠实搬设计稿 SHELL_COMMANDS,href 改成真路由导航 type Command = { id: string; group: string; label: string; sub: string; page: Page; icon: string; key?: string }; const SHELL_COMMANDS: Command[] = [ { id: "dashboard", group: "导航", label: "工作台", sub: "任务队列、今日消耗、项目进度", page: "dashboard", icon: "dashboard", key: "D" }, { id: "products", group: "导航", label: "商品库", sub: "管理 SKU、商品图册、卖点信息", page: "products", icon: "package", key: "P" }, { id: "projects", group: "导航", label: "视频创作", sub: "从商品或参考视频出发,选择生产方式", page: "projects", icon: "clapperboard", key: "V" }, { id: "asset-factory", group: "导航", label: "图片生成", sub: "模特上身图、平台套图、图片创作", page: "assetFactory", icon: "sparkles", key: "I" }, { 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: "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" }, { id: "model-photo", group: "常用动作", label: "生成模特上身图", sub: "快速生成 3:4 商品展示素材", page: "modelPhoto", icon: "users" }, { id: "platform-cover", group: "常用动作", label: "生成平台套图", sub: "适配电商平台封面与详情图", page: "platformCover", icon: "images" }, { id: "image-optimize", group: "常用动作", label: "图片创作", sub: "对话式生成、编辑", page: "imageOptimize", icon: "images" } ]; function CommandPalette({ open, onClose, navigate, canManageBilling = true }: { open: boolean; onClose: () => void; navigate: Navigate; canManageBilling?: boolean }) { const [query, setQuery] = useState(""); useBodyScrollLock(open); useEffect(() => { if (open) setQuery(""); }, [open]); const items = useMemo(() => { const q = query.trim().toLowerCase(); // 非主账号:命令面板里也不暴露「团队」「消费」(与侧栏一致,PMC#3) return SHELL_COMMANDS .filter((cmd) => canManageBilling || (cmd.page !== "team" && cmd.page !== "account")) .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; let lastGroup = ""; return createPortal(
{ if (event.target === event.currentTarget) onClose(); }} >
setQuery(event.target.value)} onKeyDown={(event) => { if (event.key === "Escape") { event.preventDefault(); onClose(); } else if (event.key === "Enter" && items[0]) { event.preventDefault(); run(items[0]); } }} /> {items.length} 项
{items.length === 0 && (
没有匹配的入口 换个关键词试试
)} {items.map((cmd, i) => { const section = cmd.group !== lastGroup ?
{cmd.group}
: null; lastGroup = cmd.group; return (
{section}
); })}
, document.body ); } type Navigate = (page: Page) => void; // 账户下拉菜单 —— 忠实搬设计稿 shell.js 的 accountMenuHtml + toggleAccountMenu 定位逻辑。 // 触发器(侧栏 .user / 顶栏头像)传入自己的 DOMRect 当锚点;菜单 fixed 定位、右对齐锚点、 // 越界自动翻到锚点上方,并钳进视口。点外部 / Esc 收起。各项 navigate,退出走 logout。 // 认证定调:头像下方第二行展示「用户名」(user.username),本项目不用邮箱。 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: "消费与余额" } ]; export function AccountMenu({ anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: { anchorRect: DOMRect; onClose: () => void; navigate: Navigate; logout: () => void; user: User; team: Team | null; canManageBilling?: boolean; }) { const menuRef = useRef(null); const [pos, setPos] = useState<{ left: number; top: number }>({ left: anchorRect.left, top: anchorRect.bottom + 8 }); // 量出真实菜单尺寸后再定位(右对齐锚点、上下翻转防溢出),与 V1 toggleAccountMenu 一致 useLayoutEffect(() => { 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; 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); setPos({ left, top }); }, [anchorRect]); // 点菜单外部 / Esc → 收起(锚点本身由触发器的 toggle 处理,避免点锚点立刻又被关掉) useEffect(() => { const onDown = (event: MouseEvent) => { const target = event.target as Node; if (menuRef.current?.contains(target)) return; if ((target as Element).closest?.(".user, .topbar-avatar")) return; onClose(); }; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); }; }, [onClose]); const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase(); const go = (page: Page) => { onClose(); navigate(page); }; return createPortal(