import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { AlertCircle, Boxes, Check, ChevronDown, ChevronRight, Info, LogOut, Settings, WalletCards } from "lucide-react"; import { IconKitSvg } from "./IconKitSvg"; import { ConfirmModal, useBodyScrollLock, useOverlayTransition } 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: "omni-create", group: "导航", label: "全能创作", sub: "预设工作流 + 对话式创作", page: "omniCreate", icon: "sparkles", key: "O" }, { id: "omni-history", group: "导航", label: "创作历史", sub: "查看与继续独立会话", page: "omniHistory", icon: "history", key: "H" }, { 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: "quick-create", group: "导航", label: "一键成片", sub: "上传商品图片,自动完成整条视频", page: "quickCreate", icon: "wand", key: "Q" }, { id: "video-remix", group: "导航", label: "提炼提示词", sub: "上传参考视频,提炼可编辑提示词", page: "videoRemix", icon: "scan", key: "R" }, { id: "video-replace", group: "导航", label: "视频复刻", sub: "保留参考视频的人物、场景与节奏,复刻出自己的带货内容", page: "videoReplace", icon: "replace", key: "E" }, { 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: "quick-create-action", group: "常用动作", label: "一键成片", sub: "输入商品名称并上传图片,自动生成视频", page: "quickCreate", icon: "wand" }, { 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(""); const { mounted, show } = useOverlayTransition(open, onClose, 220); 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 (!mounted) 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; // 账户下拉菜单 —— 触发器为侧栏左下角 .org-switcher;菜单 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({ open, anchorRect, onClose, navigate, logout, user, team, canManageBilling = true }: { open: boolean; anchorRect: DOMRect | null; onClose: () => void; navigate: Navigate; logout: () => void; user: User; team: Team | null; canManageBilling?: boolean; }) { const { mounted, show } = useOverlayTransition(open, undefined, 180); const menuRef = useRef(null); const lastRect = useRef(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; const fromLeft = rect.left < window.innerWidth / 2; let left = fromLeft ? rect.left : rect.right - width; left = Math.min(Math.max(12, left), window.innerWidth - width - 12); let top = fromLeft ? rect.top - height - 8 : rect.bottom + 8; if (top < 12 || top + height > window.innerHeight - 12) { top = fromLeft ? Math.min(rect.bottom + 8, window.innerHeight - height - 12) : Math.max(12, rect.top - height - 8); } setPos({ left, top }); }, [rect]); // 点菜单外部 / Esc → 收起(锚点本身由触发器的 toggle 处理,避免点锚点立刻又被关掉) useEffect(() => { if (!open) return; const onDown = (event: MouseEvent) => { const target = event.target as Node; if (menuRef.current?.contains(target)) return; if ((target as Element).closest?.(".org-switcher")) 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); }; }, [open, onClose]); const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase(); const go = (page: Page) => { onClose(); navigate(page); }; const [confirmLogout, setConfirmLogout] = useState(false); return ( <> {mounted && rect ? createPortal(