功能修改
This commit is contained in:
@@ -9,13 +9,15 @@ import {
|
||||
ShieldCheck,
|
||||
UserRoundPen,
|
||||
WandSparkles,
|
||||
Wrench
|
||||
Wrench,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { type ComponentType, type ReactNode, useEffect, useState } from "react";
|
||||
import { type ComponentType, type ReactNode, type TouchEvent, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { PageId, PageParams, PageRoute } from "../contracts/page";
|
||||
import type { CurrentUserView } from "../contracts/workspace";
|
||||
import type { CurrentUserView, OperationRecord } from "../contracts/workspace";
|
||||
import { MagicalParticleLayer } from "./MagicalParticleLayer";
|
||||
import { OperationsTray } from "./OperationsTray";
|
||||
import {
|
||||
applyBackgroundImage,
|
||||
applyThemeBackgroundPreset,
|
||||
@@ -33,6 +35,7 @@ interface AppShellProps {
|
||||
routes: PageRoute[];
|
||||
currentPage: PageId;
|
||||
session: CurrentUserView;
|
||||
operations: OperationRecord[];
|
||||
onNavigate: (pageId: PageId, params?: PageParams) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -59,9 +62,11 @@ const roleLabels: Record<CurrentUserView["roles"][number], string> = {
|
||||
serverAdmin: "服务器管理员"
|
||||
};
|
||||
|
||||
export function AppShell({ routes, currentPage, session, onNavigate, children }: AppShellProps) {
|
||||
export function AppShell({ routes, currentPage, session, operations, onNavigate, children }: AppShellProps) {
|
||||
const [themeState, setThemeState] = useState<WorkspaceThemeState>(() => loadThemeState());
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
const touchStart = useRef<{ x: number; y: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleThemePaletteChange(event: Event) {
|
||||
@@ -81,6 +86,29 @@ export function AppShell({ routes, currentPage, session, onNavigate, children }:
|
||||
return () => window.removeEventListener(themePaletteChangeEvent, handleThemePaletteChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobileSidebarOpen) {
|
||||
return;
|
||||
}
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
setIsMobileSidebarOpen(false);
|
||||
}
|
||||
}
|
||||
const isNarrow = window.matchMedia("(max-width: 760px)").matches;
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
if (isNarrow) {
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
if (isNarrow) {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
}
|
||||
};
|
||||
}, [isMobileSidebarOpen]);
|
||||
|
||||
const activePalette = themePalettes.find((palette) => palette.id === themeState.paletteId) ?? themePalettes[0];
|
||||
const routesById = new Map(routes.map((route) => [route.id, route]));
|
||||
const visibleGroups = menuGroups
|
||||
@@ -91,15 +119,66 @@ export function AppShell({ routes, currentPage, session, onNavigate, children }:
|
||||
.filter((group) => group.routes.length > 0);
|
||||
|
||||
function activateGroup(group: (typeof visibleGroups)[number]) {
|
||||
setIsMobileSidebarOpen(false);
|
||||
if (group.routes[0].id !== currentPage) {
|
||||
onNavigate(group.routes[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart(event: TouchEvent<HTMLDivElement>) {
|
||||
const touch = event.touches[0];
|
||||
touchStart.current = touch ? { x: touch.clientX, y: touch.clientY } : null;
|
||||
}
|
||||
|
||||
function handleTouchEnd(event: TouchEvent<HTMLDivElement>) {
|
||||
const start = touchStart.current;
|
||||
const touch = event.changedTouches[0];
|
||||
touchStart.current = null;
|
||||
if (!start || !touch || !window.matchMedia("(max-width: 760px)").matches) {
|
||||
return;
|
||||
}
|
||||
const deltaX = touch.clientX - start.x;
|
||||
const deltaY = touch.clientY - start.y;
|
||||
if (Math.abs(deltaY) >= Math.abs(deltaX)) {
|
||||
return;
|
||||
}
|
||||
if (!isMobileSidebarOpen && start.x <= 28 && deltaX >= 56) {
|
||||
setIsMobileSidebarOpen(true);
|
||||
} else if (isMobileSidebarOpen && deltaX <= -56) {
|
||||
setIsMobileSidebarOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx("app-shell", isSidebarCollapsed && "app-shell-sidebar-collapsed")}>
|
||||
<div
|
||||
className={cx("app-shell", isSidebarCollapsed && "app-shell-sidebar-collapsed", isMobileSidebarOpen && "app-shell-mobile-sidebar-open")}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<MagicalParticleLayer />
|
||||
<aside className="app-sidebar">
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-sidebar-handle"
|
||||
aria-label="打开导航菜单"
|
||||
aria-controls="primary-sidebar"
|
||||
aria-expanded={isMobileSidebarOpen}
|
||||
onClick={() => setIsMobileSidebarOpen(true)}
|
||||
>
|
||||
<WandSparkles size={17} aria-hidden="true" />
|
||||
<span>菜单</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-sidebar-backdrop"
|
||||
aria-label="关闭导航菜单"
|
||||
aria-hidden={!isMobileSidebarOpen}
|
||||
tabIndex={isMobileSidebarOpen ? 0 : -1}
|
||||
onClick={() => setIsMobileSidebarOpen(false)}
|
||||
/>
|
||||
<aside id="primary-sidebar" className={cx("app-sidebar", isMobileSidebarOpen && "app-sidebar-mobile-open")}>
|
||||
<button type="button" className="mobile-sidebar-close" aria-label="关闭导航菜单" onClick={() => setIsMobileSidebarOpen(false)}>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
<div className="app-brand" aria-label={themeTokens.appName}>
|
||||
<span className="app-brand-mark">
|
||||
<WandSparkles size={17} />
|
||||
@@ -140,8 +219,17 @@ export function AppShell({ routes, currentPage, session, onNavigate, children }:
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<OperationsTray operations={operations} />
|
||||
<div className="app-session">
|
||||
<button type="button" className="app-account-button" aria-current={currentPage === "profileSettings" ? "page" : undefined} onClick={() => onNavigate("profileSettings")}>
|
||||
<button
|
||||
type="button"
|
||||
className="app-account-button"
|
||||
aria-current={currentPage === "profileSettings" ? "page" : undefined}
|
||||
onClick={() => {
|
||||
setIsMobileSidebarOpen(false);
|
||||
onNavigate("profileSettings");
|
||||
}}
|
||||
>
|
||||
<span className="account-avatar" aria-hidden="true">
|
||||
{session.profile.avatarUrl ? <img src={session.profile.avatarUrl} alt="" /> : <Heart size={17} />}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user