diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 5697828..2754c65 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -1205,7 +1205,7 @@ export function App() { return (
- navigateAdmin("")} /> + navigateAdmin("")} accountOpen={accountAnchor !== null} onOpenAccount={(rect) => setAccountAnchor((prev) => (prev ? null : rect))} />
@@ -1224,19 +1224,6 @@ export function App() { {unreadCount > 0 && {unreadCount}} -
diff --git a/core/frontend/src/components/app-shell.tsx b/core/frontend/src/components/app-shell.tsx index cd4cef4..103e917 100644 --- a/core/frontend/src/components/app-shell.tsx +++ b/core/frontend/src/components/app-shell.tsx @@ -1,6 +1,6 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { AlertCircle, Boxes, Check, ChevronDown, Info, LogOut, Settings, UsersRound } from "lucide-react"; +import { AlertCircle, Boxes, Check, ChevronDown, ChevronRight, Info, LogOut, Settings, UsersRound } from "lucide-react"; import { IconKitSvg } from "./IconKitSvg"; import { ConfirmModal, useBodyScrollLock, useOverlayTransition } from "./overlays"; import type { Product, Project, Team, User } from "../types"; @@ -101,9 +101,9 @@ function CommandPalette({ open, onClose, navigate, canManageBilling = true }: { type Navigate = (page: Page) => void; -// 账户下拉菜单 —— 忠实搬设计稿 shell.js 的 accountMenuHtml + toggleAccountMenu 定位逻辑。 -// 触发器(侧栏 .user / 顶栏头像)传入自己的 DOMRect 当锚点;菜单 fixed 定位、右对齐锚点、 -// 越界自动翻到锚点上方,并钳进视口。点外部 / Esc 收起。各项 navigate,退出走 logout。 +// 账户下拉菜单 —— 触发器为侧栏左下角 .org-switcher;菜单 fixed 定位。 +// 锚点在视口左半边时左对齐并向上展开,否则右对齐向下展开;越界翻转并钳进视口。 +// 点外部 / Esc 收起。各项 navigate,退出走 logout。 // 认证定调:头像下方第二行展示「用户名」(user.username),本项目不用邮箱。 const ACCOUNT_ITEMS: { act: Page; icon: string; label: string }[] = [ { act: "settings", icon: "settings", label: "个人设置" }, @@ -137,11 +137,15 @@ export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, const menu = menuRef.current; const width = menu?.offsetWidth || 232; const height = menu?.offsetHeight || 260; - let left = rect.right - width; - if (left < 12) left = rect.left; + 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 = rect.bottom + 8; - if (top + height > window.innerHeight - 12) top = Math.max(12, rect.top - height - 8); + 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]); @@ -151,7 +155,7 @@ export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, 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; + if ((target as Element).closest?.(".org-switcher")) return; onClose(); }; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; @@ -403,10 +407,11 @@ const PAGE_TO_NAV: Partial> = { settingsNotify: "settings" }; -export function Sidebar({ page, navigate, user, canManageBilling = true, products, projects, productTotal, projectTotal, aiUnread, onOpenAdmin }: { +export function Sidebar({ page, navigate, user, team, canManageBilling = true, products, projects, productTotal, projectTotal, aiUnread, onOpenAdmin, onOpenAccount, accountOpen = false }: { page: Page; navigate: Navigate; user: User; + team?: Team | null; // 主账号(owner=超管)才看得到「团队」「消费」入口;子账号隐藏(PMC#3)。默认 true 兼容旧调用。 canManageBilling?: boolean; products: Product[]; @@ -417,6 +422,8 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product aiUnread?: number; // 平台超管入口:仅 user.is_platform_admin 时显示,点了进 /admin 后台 onOpenAdmin?: () => void; + onOpenAccount?: (rect: DOMRect) => void; + accountOpen?: boolean; }) { const activeNav = PAGE_TO_NAV[page]; // 子账号:导航里去掉「团队」「消费」两项(命令面板、账户菜单同步过滤)。 @@ -658,6 +665,20 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product )}
+ setPaletteOpen(false)} navigate={navigate} canManageBilling={canManageBilling} /> diff --git a/core/frontend/src/design-restraint.css b/core/frontend/src/design-restraint.css index 5864285..4e194b4 100644 --- a/core/frontend/src/design-restraint.css +++ b/core/frontend/src/design-restraint.css @@ -229,6 +229,7 @@ body.sidebar-collapsed .app { grid-template-columns: 96px 1fr; } /* ─── Sidebar ─── */ aside.sidebar { --klein: #002fa7; + --st-black: #101012; padding: 0; border-right: 1px solid rgba(27, 32, 40, 0.08); background: rgba(28, 34, 43, 0.035); @@ -414,6 +415,7 @@ aside.sidebar .nav-panel { height: calc(100vh - 116px); min-height: 0; padding-top: 14px; + padding-bottom: 82px; overflow-x: hidden; overflow-y: auto; } @@ -535,6 +537,55 @@ aside.sidebar .nav-subitem.active::before { opacity: 1; box-shadow: 0 0 0 4px rgba(0, 47, 167, 0.10); } + +aside.sidebar .org-switcher { + position: absolute; + z-index: 6; + left: 12px; + right: 6px; + bottom: 24px; + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 0; + border: 0; + background: transparent; + color: #31343a; + font: inherit; + font-size: 14px; + text-align: left; + cursor: pointer; +} +aside.sidebar .org-switcher-name { + min-width: 0; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +aside.sidebar .org-switcher svg { width: 16px; height: 16px; flex-shrink: 0; color: #575f6f; } +aside.sidebar .org-badge { + width: 34px; + height: 34px; + flex-shrink: 0; + display: grid; + place-items: center; + overflow: hidden; + border-radius: 50%; + color: #fff; + background: var(--st-black); + font-weight: 600; + font-size: 13px; +} +aside.sidebar .org-badge img { width: 100%; height: 100%; object-fit: cover; display: block; } +body.sidebar-collapsed aside.sidebar .org-switcher { + left: 0; + right: 0; + justify-content: center; +} +body.sidebar-collapsed aside.sidebar .org-switcher-name, +body.sidebar-collapsed aside.sidebar .org-switcher svg { display: none; } aside.sidebar .nav-subitem .pill-mini { margin-left: auto; min-width: 18px; @@ -947,28 +998,6 @@ main { position: relative; background: #fff; min-width: 0; } letter-spacing: .02em; } -/* ─── Topbar · 头像 ─── */ -.topbar-avatar { - width: 40px; height: 40px; - border-radius: var(--r-pill); - background: var(--accent-black); - color: var(--accent-white); - display: inline-flex; align-items: center; justify-content: center; - font-size: 13px; font-weight: 600; - cursor: pointer; - border: 1px solid var(--border-faint); - transition: transform var(--t-fast), box-shadow var(--t-base); - flex-shrink: 0; - overflow: hidden; -} -.topbar-avatar:hover { - transform: scale(1.04); - box-shadow: 0 0 0 3px var(--heat-12); -} -.topbar-avatar img { - width: 100%; height: 100%; object-fit: cover; -} - /* ─── Content ─── */ .content { /* 对齐 HTML 设计稿 #page-content(24px 28px 60px):原 48px 顶距使全站标题整体偏低 24px */