From 378b0a350c6cd582c4e96b5e9fe28463395c6a0b Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 19 Jun 2026 05:29:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20Wave=203=20batch4=20=E2=80=94=20?= =?UTF-8?q?=E5=A4=96=E5=A3=B3=E8=B4=A6=E6=88=B7=E8=8F=9C=E5=8D=95=20+=20?= =?UTF-8?q?=E8=AE=BE=E7=BD=AEdirty=E5=BC=95=E6=93=8E=20+=20=E5=95=86?= =?UTF-8?q?=E5=93=81=E5=88=9B=E5=BB=BA=E6=8A=BD=E5=B1=89=E5=A4=9A=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 全局外壳(sub-agent):侧栏 .user 点击弹账户菜单(头像+店名+用户名[非邮箱,认证定调]+ 个人设置/消息/团队/消费/退出),复用现成 .shell-account-menu CSS,点外部/ESC 收起 设置页(sub-agent):dirty-state 引擎(baseline+draft diff 13字段,顶栏「保存所有变更」 无改动 disabled、改动激活+N项计数、统一保存、beforeunload、nav dirty-dot);修一处类型错(|| {} → 可选链) 商品创建抽屉(sub-agent):主图多图≤5+.pf-grid缩略网格+单张删、submit主图/卖点必填校验+收编bulletDraft、 上传区拖拽变橙、卖点字重对齐;内联 style 抽进 css 验证: tsc 0 · build 0 · 走查 账户菜单5项+ESC、设置保存按钮无改动disabled、抽屉multiple+pf-grid 全过 · 0 console error ⚠️ 外壳:顶栏头像菜单接线需改 App.tsx/pipeline.tsx(超出单文件范围),侧栏入口已全可用。移交监督。 Co-Authored-By: Claude Opus 4.8 --- core/frontend/src/components/app-shell.tsx | 133 +++++++- .../src/components/product-create-drawer.tsx | 149 +++++++-- core/frontend/src/product-create-page.css | 86 ++++- core/frontend/src/routes/settings.tsx | 304 ++++++++++++------ core/frontend/src/settings-page.css | 5 + core/qa/visual-parity/_wave3-batch4.mjs | 84 +++++ 6 files changed, 629 insertions(+), 132 deletions(-) create mode 100644 core/qa/visual-parity/_wave3-batch4.mjs diff --git a/core/frontend/src/components/app-shell.tsx b/core/frontend/src/components/app-shell.tsx index 429368c..4dc00b5 100644 --- a/core/frontend/src/components/app-shell.tsx +++ b/core/frontend/src/components/app-shell.tsx @@ -1,8 +1,10 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import type { MouseEvent as ReactMouseEvent } from "react"; import { createPortal } from "react-dom"; -import { AlertCircle, Check, Info } from "lucide-react"; +import { AlertCircle, Check, Info, LogOut } from "lucide-react"; import { IconKitSvg } from "./IconKitSvg"; import { useBodyScrollLock } from "./overlays"; +import { api, setToken } from "../api"; import type { Product, Project, Team, User } from "../types"; import type { Notice, Page } from "../routes/route-config"; @@ -96,6 +98,93 @@ function CommandPalette({ open, onClose, navigate }: { open: boolean; onClose: ( 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: "消费与余额" } +]; + +function AccountMenu({ anchorRect, onClose, navigate, logout, user, team }: { + anchorRect: DOMRect; + onClose: () => void; + navigate: Navigate; + logout: () => void; + user: User; + team: Team | null; +}) { + 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( +