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/About.tsx | 19 ++++ src/App.tsx | 12 ++ src/Me.tsx | 10 ++ src/assets/hero.png | Bin 0 -> 13057 bytes src/assets/react.svg | 1 + src/assets/vite.svg | 1 + src/index.css | 14 +++ src/layouts/BasicLayout.module.css | 56 +++++++++ src/layouts/BasicLayout.tsx | 225 +++++++++++++++++++++++++++++++++++++ src/main.tsx | 17 +++ src/router.ts | 11 ++ src/routes.ts | 39 +++++++ src/theme.tsx | 5 + 13 files changed, 410 insertions(+) create mode 100644 src/About.tsx create mode 100644 src/App.tsx create mode 100644 src/Me.tsx create mode 100644 src/assets/hero.png create mode 100644 src/assets/react.svg create mode 100644 src/assets/vite.svg create mode 100644 src/index.css create mode 100644 src/layouts/BasicLayout.module.css create mode 100644 src/layouts/BasicLayout.tsx create mode 100644 src/main.tsx create mode 100644 src/router.ts create mode 100644 src/routes.ts create mode 100644 src/theme.tsx (limited to 'src') diff --git a/src/About.tsx b/src/About.tsx new file mode 100644 index 0000000..21b4c1e --- /dev/null +++ b/src/About.tsx @@ -0,0 +1,19 @@ +import { ActionIcon, Button, Group, Stack, Text, Title } from '@mantine/core' +import { SlidersHorizontalIcon } from '@phosphor-icons/react' + +function About() { + return ( + + 关于 + A React + TypeScript starter built with Vite. + + + + + + + + ) +} + +export default About diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..8b213e6 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,12 @@ +import { Stack, Text, Title } from '@mantine/core' + +function App() { + return ( + + 欢迎使用 Template React + 从左侧菜单开始,探索你的应用。 + + ) +} + +export default App diff --git a/src/Me.tsx b/src/Me.tsx new file mode 100644 index 0000000..ab1d02f --- /dev/null +++ b/src/Me.tsx @@ -0,0 +1,10 @@ +import { Stack, Text, Title } from '@mantine/core' + +export default function Me() { + return ( + + 关于我 + 欢迎来到我的个人介绍页面。 + + ) +} diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..c2482e2 --- /dev/null +++ b/src/index.css @@ -0,0 +1,14 @@ +html, +body, +#root { + min-height: 100%; +} + +body { + margin: 0; +} + +#root { + min-height: 100dvh; + width: 100%; +} 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: