完善UI
This commit is contained in:
@@ -1205,7 +1205,7 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Sidebar page={page} navigate={navigate} user={currentUser} canManageBilling={isOwner} products={products} projects={projects} productTotal={productTotal} projectTotal={projectTotal} aiUnread={aiUnread} onOpenAdmin={() => navigateAdmin("")} />
|
<Sidebar page={page} navigate={navigate} user={currentUser} team={currentTeam} canManageBilling={isOwner} products={products} projects={projects} productTotal={productTotal} projectTotal={projectTotal} aiUnread={aiUnread} onOpenAdmin={() => navigateAdmin("")} accountOpen={accountAnchor !== null} onOpenAccount={(rect) => setAccountAnchor((prev) => (prev ? null : rect))} />
|
||||||
<main>
|
<main>
|
||||||
<Decorations />
|
<Decorations />
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
@@ -1224,19 +1224,6 @@ export function App() {
|
|||||||
<IconKitSvg name="bell" />
|
<IconKitSvg name="bell" />
|
||||||
{unreadCount > 0 && <span className="count-noti">{unreadCount}</span>}
|
{unreadCount > 0 && <span className="count-noti">{unreadCount}</span>}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="topbar-avatar"
|
|
||||||
type="button"
|
|
||||||
title="账户"
|
|
||||||
aria-haspopup="menu"
|
|
||||||
aria-expanded={accountAnchor !== null}
|
|
||||||
onClick={(event) => {
|
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
|
||||||
setAccountAnchor((prev) => (prev ? null : rect));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentUser.avatar_url ? <img src={currentUser.avatar_url} alt="头像" /> : <span>{avatarChar}</span>}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="content" id="page-content" key={page}>
|
<div className="content" id="page-content" key={page}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
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 { IconKitSvg } from "./IconKitSvg";
|
||||||
import { ConfirmModal, useBodyScrollLock, useOverlayTransition } from "./overlays";
|
import { ConfirmModal, useBodyScrollLock, useOverlayTransition } from "./overlays";
|
||||||
import type { Product, Project, Team, User } from "../types";
|
import type { Product, Project, Team, User } from "../types";
|
||||||
@@ -101,9 +101,9 @@ function CommandPalette({ open, onClose, navigate, canManageBilling = true }: {
|
|||||||
|
|
||||||
type Navigate = (page: Page) => void;
|
type Navigate = (page: Page) => void;
|
||||||
|
|
||||||
// 账户下拉菜单 —— 忠实搬设计稿 shell.js 的 accountMenuHtml + toggleAccountMenu 定位逻辑。
|
// 账户下拉菜单 —— 触发器为侧栏左下角 .org-switcher;菜单 fixed 定位。
|
||||||
// 触发器(侧栏 .user / 顶栏头像)传入自己的 DOMRect 当锚点;菜单 fixed 定位、右对齐锚点、
|
// 锚点在视口左半边时左对齐并向上展开,否则右对齐向下展开;越界翻转并钳进视口。
|
||||||
// 越界自动翻到锚点上方,并钳进视口。点外部 / Esc 收起。各项 navigate,退出走 logout。
|
// 点外部 / Esc 收起。各项 navigate,退出走 logout。
|
||||||
// 认证定调:头像下方第二行展示「用户名」(user.username),本项目不用邮箱。
|
// 认证定调:头像下方第二行展示「用户名」(user.username),本项目不用邮箱。
|
||||||
const ACCOUNT_ITEMS: { act: Page; icon: string; label: string }[] = [
|
const ACCOUNT_ITEMS: { act: Page; icon: string; label: string }[] = [
|
||||||
{ act: "settings", icon: "settings", label: "个人设置" },
|
{ act: "settings", icon: "settings", label: "个人设置" },
|
||||||
@@ -137,11 +137,15 @@ export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user,
|
|||||||
const menu = menuRef.current;
|
const menu = menuRef.current;
|
||||||
const width = menu?.offsetWidth || 232;
|
const width = menu?.offsetWidth || 232;
|
||||||
const height = menu?.offsetHeight || 260;
|
const height = menu?.offsetHeight || 260;
|
||||||
let left = rect.right - width;
|
const fromLeft = rect.left < window.innerWidth / 2;
|
||||||
if (left < 12) left = rect.left;
|
let left = fromLeft ? rect.left : rect.right - width;
|
||||||
left = Math.min(Math.max(12, left), window.innerWidth - width - 12);
|
left = Math.min(Math.max(12, left), window.innerWidth - width - 12);
|
||||||
let top = rect.bottom + 8;
|
let top = fromLeft ? rect.top - height - 8 : rect.bottom + 8;
|
||||||
if (top + height > window.innerHeight - 12) top = Math.max(12, rect.top - height - 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 });
|
setPos({ left, top });
|
||||||
}, [rect]);
|
}, [rect]);
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user,
|
|||||||
const onDown = (event: MouseEvent) => {
|
const onDown = (event: MouseEvent) => {
|
||||||
const target = event.target as Node;
|
const target = event.target as Node;
|
||||||
if (menuRef.current?.contains(target)) return;
|
if (menuRef.current?.contains(target)) return;
|
||||||
if ((target as Element).closest?.(".user, .topbar-avatar")) return;
|
if ((target as Element).closest?.(".org-switcher")) return;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); };
|
const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); };
|
||||||
@@ -403,10 +407,11 @@ const PAGE_TO_NAV: Partial<Record<Page, Page>> = {
|
|||||||
settingsNotify: "settings"
|
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;
|
page: Page;
|
||||||
navigate: Navigate;
|
navigate: Navigate;
|
||||||
user: User;
|
user: User;
|
||||||
|
team?: Team | null;
|
||||||
// 主账号(owner=超管)才看得到「团队」「消费」入口;子账号隐藏(PMC#3)。默认 true 兼容旧调用。
|
// 主账号(owner=超管)才看得到「团队」「消费」入口;子账号隐藏(PMC#3)。默认 true 兼容旧调用。
|
||||||
canManageBilling?: boolean;
|
canManageBilling?: boolean;
|
||||||
products: Product[];
|
products: Product[];
|
||||||
@@ -417,6 +422,8 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
|||||||
aiUnread?: number;
|
aiUnread?: number;
|
||||||
// 平台超管入口:仅 user.is_platform_admin 时显示,点了进 /admin 后台
|
// 平台超管入口:仅 user.is_platform_admin 时显示,点了进 /admin 后台
|
||||||
onOpenAdmin?: () => void;
|
onOpenAdmin?: () => void;
|
||||||
|
onOpenAccount?: (rect: DOMRect) => void;
|
||||||
|
accountOpen?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const activeNav = PAGE_TO_NAV[page];
|
const activeNav = PAGE_TO_NAV[page];
|
||||||
// 子账号:导航里去掉「团队」「消费」两项(命令面板、账户菜单同步过滤)。
|
// 子账号:导航里去掉「团队」「消费」两项(命令面板、账户菜单同步过滤)。
|
||||||
@@ -658,6 +665,20 @@ export function Sidebar({ page, navigate, user, canManageBilling = true, product
|
|||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="org-switcher"
|
||||||
|
type="button"
|
||||||
|
aria-label="打开账户菜单"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={accountOpen}
|
||||||
|
onClick={(event) => onOpenAccount?.(event.currentTarget.getBoundingClientRect())}
|
||||||
|
>
|
||||||
|
<span className="org-badge">
|
||||||
|
{user.avatar_url ? <img src={user.avatar_url} alt="" /> : (team?.name || user.username || "A").slice(0, 1).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
<span className="org-switcher-name">{team?.name || user.username}</span>
|
||||||
|
<ChevronRight size={16} strokeWidth={1.8} />
|
||||||
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} navigate={navigate} canManageBilling={canManageBilling} />
|
<CommandPalette open={paletteOpen} onClose={() => setPaletteOpen(false)} navigate={navigate} canManageBilling={canManageBilling} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ body.sidebar-collapsed .app { grid-template-columns: 96px 1fr; }
|
|||||||
/* ─── Sidebar ─── */
|
/* ─── Sidebar ─── */
|
||||||
aside.sidebar {
|
aside.sidebar {
|
||||||
--klein: #002fa7;
|
--klein: #002fa7;
|
||||||
|
--st-black: #101012;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-right: 1px solid rgba(27, 32, 40, 0.08);
|
border-right: 1px solid rgba(27, 32, 40, 0.08);
|
||||||
background: rgba(28, 34, 43, 0.035);
|
background: rgba(28, 34, 43, 0.035);
|
||||||
@@ -414,6 +415,7 @@ aside.sidebar .nav-panel {
|
|||||||
height: calc(100vh - 116px);
|
height: calc(100vh - 116px);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
|
padding-bottom: 82px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -535,6 +537,55 @@ aside.sidebar .nav-subitem.active::before {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
box-shadow: 0 0 0 4px rgba(0, 47, 167, 0.10);
|
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 {
|
aside.sidebar .nav-subitem .pill-mini {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
min-width: 18px;
|
min-width: 18px;
|
||||||
@@ -947,28 +998,6 @@ main { position: relative; background: #fff; min-width: 0; }
|
|||||||
letter-spacing: .02em;
|
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 ─── */
|
||||||
.content {
|
.content {
|
||||||
/* 对齐 HTML 设计稿 #page-content(24px 28px 60px):原 48px 顶距使全站标题整体偏低 24px */
|
/* 对齐 HTML 设计稿 #page-content(24px 28px 60px):原 48px 顶距使全站标题整体偏低 24px */
|
||||||
|
|||||||
Reference in New Issue
Block a user