From 2f1057a9d9d51d0e657c1203d3781173e8efc611 Mon Sep 17 00:00:00 2001 From: shu Date: Sun, 6 Sep 2026 17:04:49 +0800 Subject: 初始化 React TypeScript 模板并实现 Mantine 响应式基础布局 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 Vite、React 和 TypeScript 初始化项目,配置 pnpm、Oxlint 与 Mantine PostCSS - 接入 React Router Data Mode,将页面路由、菜单标题和图标统一维护在路由配置中 - 添加首页、关于概览与关于我页面,使用 Nested NavLinks 根据路由生成多级菜单 - 实现桌面侧栏展开和图标模式、移动端全宽抽屉以及与品牌区对齐的顶栏 - 将布局自定义样式拆分为 CSS Modules,统一 xs 间距并固定菜单图标列位置 - 添加文档流页脚,展示自动更新年份的示例版权说明 - 配置 Mantine 默认圆角、项目说明及中文提交约定 验证:pnpm build、pnpm lint、git diff --cached --check 均通过。 --- src/layouts/BasicLayout.module.css | 56 +++++++++ src/layouts/BasicLayout.tsx | 225 +++++++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 src/layouts/BasicLayout.module.css create mode 100644 src/layouts/BasicLayout.tsx (limited to 'src/layouts') diff --git a/src/layouts/BasicLayout.module.css b/src/layouts/BasicLayout.module.css new file mode 100644 index 0000000..a010ca5 --- /dev/null +++ b/src/layouts/BasicLayout.module.css @@ -0,0 +1,56 @@ +.main { + display: flex; + flex-direction: column; +} + +.content { + flex: 1; + min-width: 0; +} + +.footer { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border-top: 1px solid var(--app-shell-border-color); +} + +.menuItem { + justify-content: flex-start; +} + +.menuItem .icon[data-position='left'] { + width: calc(var(--collapsed-navigation-width) - 4 * var(--mantine-spacing-xs)); + flex-shrink: 0; + justify-content: center; + margin: 0; +} + +.label { + padding-inline-start: var(--mantine-spacing-xs); +} + +.menuItem[data-collapsed] .label { + visibility: hidden; + flex: 0 0 auto; + width: 0; + padding-inline-start: 0; +} + +.brand { + height: var(--app-shell-header-height); + flex-shrink: 0; + display: flex; + align-items: center; + border-bottom: 1px solid var(--app-shell-border-color); + white-space: nowrap; +} + +.brand[data-collapsed] { + justify-content: center; +} + +.menuItem[data-collapsed] .icon[data-position='right'] { + display: none; +} diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx new file mode 100644 index 0000000..6af8564 --- /dev/null +++ b/src/layouts/BasicLayout.tsx @@ -0,0 +1,225 @@ +import { useEffect, useState } from 'react' +import { + AppShell, + ActionIcon, + Box, + Burger, + Drawer, + Group, + NavLink, + Stack, + Text, + Tooltip, + rem, + useMantineTheme, +} from '@mantine/core' +import { useDisclosure, useMediaQuery } from '@mantine/hooks' +import { CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react' +import { NavLink as RouterNavLink, Outlet, useLocation, useMatches } from 'react-router' +import { pageRoutes } from '../routes.ts' +import type { PageRoute } from '../routes.ts' +import classes from './BasicLayout.module.css' + +const collapsedNavbarWidth = rem(64) + +type NavigationProps = { + onNavigate?: () => void + collapsed?: boolean + routes?: PageRoute[] + parentPath?: string +} + +function NavigationItem({ + route, + parentPath, + collapsed, + onNavigate, +}: { + route: PageRoute + parentPath: string + collapsed: boolean + onNavigate?: () => void +}) { + const matches = useMatches() + const active = matches.some((match) => match.id === route.id) + const selected = matches.at(-1)?.id === route.id + const [opened, setOpened] = useState(active) + + if (!route.handle) return null + + const { title: label, icon: Icon } = route.handle + const to = !route.path + ? parentPath + : route.path.startsWith('/') + ? route.path + : `${parentPath.replace(/\/$/, '')}/${route.path}` + const hasChildren = route.children?.some((child) => child.handle) + const sharedProps = { + label, + noWrap: true, + p: 'xs', + 'aria-label': label, + leftSection: