fix: 审查修补前端布局重构并归档 change

- 补实现 ProtectedRoute 空壳组件(预留接口,不启用认证逻辑)
- 修复页面组件内联 style 为 CSS 类,符合样式规范
- 补充 Sidebar 菜单项激活状态测试、404 按钮可点击测试
- 回写 admin-layout spec Header 页面标题 fallback 行为
- 同步 delta specs 至主规范(admin-layout、frontend-routing、app-constants)
- 归档 refactor-frontend-layout change
This commit is contained in:
2026-05-24 22:28:17 +08:00
parent 4caf502908
commit 1e3269380e
16 changed files with 51 additions and 210 deletions

View File

@@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-05-20

View File

@@ -1,95 +0,0 @@
## Context
当前前端为单页面应用,`app.tsx` 直接渲染 Header + Content无路由、无侧边栏。技术栈为 React 19 + TDesign React + TanStack Query + Vite。后端使用 Bun.serve已支持 SPA fallback无扩展名路径返回 index.html
目标:重构为经典企业 Admin 后台布局,引入路由,支持多页面导航。
约束:
- 前端样式开发优先使用 TDesign 组件和 CSS tokens禁止内联 style、硬编码色值
- 新增代码需编写完善测试
- 后端无需改动
## Goals / Non-Goals
**Goals:**
- 引入 React Router v7 (Declarative mode) 实现 SPA 路由
- 实现企业 Admin 后台布局Header + 侧边栏 + 内容区
- 侧边栏支持折叠/展开,状态持久化到 localStorage
- 路由定义与菜单配置统一为单一数据源
- 提供示例页面仪表盘、用户管理、系统设置、404
**Non-Goals:**
- 不实现路由守卫/权限控制(预留接口但不启用)
- 不实现懒加载(页面少,暂不需要)
- 不实现面包屑导航
- 不实现用户认证
## Decisions
### 1. 路由方案React Router v7 Declarative mode
**选择**`react-router` v7使用 Declarative mode`BrowserRouter` + `Routes` + `Route`
**理由**
- v7 统一了 `react-router``react-router-dom`,单包导入
- Declarative mode 满足当前需求,无需 framework mode 的文件系统路由
- 社区成熟,文档完善
**替代方案**
- TanStack Router类型安全强但较新、API 变动风险
- 自实现:零依赖但功能有限,扩展成本高
### 2. Routes 定义位置:独立文件
**选择**:抽离为 `src/web/routes.tsx``app.tsx` 引用
**理由**
- 路由增删时改动范围小,不污染 app.tsx
- 便于后续扩展路由守卫、懒加载
### 3. 侧边栏折叠按钮位置Header 左侧
**选择**:折叠按钮放在 Header 左侧Sidebar 不使用 Menu 的 operations prop
**理由**
- Sidebar 折叠变窄后,底部按钮变小且易被忽略
- Header 上的按钮始终可见且易触达
- 符合 Ant Design Pro 等主流企业后台习惯
### 4. 页面标题来源:复用 menu label
**选择**Header 页面标题直接使用 `menu.tsx` 中的 `label` 字段
**理由**
- 简单直接,侧边栏标签即页面标题
- 如需差异化,后续加 `title` 字段即可
### 5. Layout 嵌套:保留嵌套结构
**选择**`Layout > (Header + Layout > (Aside + Content))`
**理由**
- 为 Footer 预留位置,将来加 Footer 无需重构
- 符合 TDesign 官方组合导航布局示例
### 6. Vite code splittingreact-router 单独分组
**选择**:新增 `vendor-router` 组,包含 `react-router`
**理由**
- 路由库独立于 React 核心,更新节奏不同
- 便于缓存隔离
### 7. 菜单与路由数据源menu.tsx
**选择**`src/web/menu.tsx` 定义菜单项配置Sidebar 和 Routes 共同引用
**理由**
- 单一数据源,保证菜单项和路由同步
- 便于类型安全(`as const`
## Risks / Trade-offs
- **路由与菜单配置一致性**:需人工保证 `menu.tsx``routes.tsx` 中 Route 定义一致 → 测试覆盖路由跳转和菜单点击
- **CSS 类名迁移影响**`.dashboard-*``.app-*` 可能影响测试选择器 → 全局搜索确认影响范围,更新测试
- **React Router v7 新版本风险**v7 较新,可能存在未发现的 bug → 使用成熟 APIBrowserRouter/Routes/Route避免实验性特性

View File

@@ -1,36 +0,0 @@
## Why
当前前端为单页面应用,仅有 Header + Content 的简单布局,无路由、无侧边栏,页面结构单一,无法支撑企业级 Admin 后台的多页面导航需求。作为 Bun 全栈应用模板,需要提供更完善的前端布局范例,便于使用者在此基础上扩展业务页面。
## What Changes
- 引入 React Router v7 (Declarative mode) 实现前端 SPA 路由
- 重构 Layout 为经典企业 Admin 后台布局Header + 侧边栏 + 内容区
- 新增侧边栏菜单组件,支持折叠/展开,折叠状态持久化到 localStorage
- 新增多个示例页面仪表盘、用户管理、系统设置、404
- 将路由定义与菜单配置统一为单一数据源,保证两者同步
- 新增 `react-router` 依赖Vite code splitting 单独分组
**BREAKING**: 原 `app.tsx` 的 Content 内容迁移至 Dashboard 页面CSS 类名 `.dashboard-*` 变更为 `.app-*`
## Capabilities
### New Capabilities
- `frontend-routing`: 前端 SPA 路由能力,基于 React Router v7支持多页面导航、路由匹配、404 处理
- `admin-layout`: 企业 Admin 后台布局能力Header + 侧边栏 + 内容区,侧边栏可折叠
### Modified Capabilities
- `app-constants`: 新增 localStorage key `"sidebar.collapsed"` 用于持久化侧边栏折叠状态
## Impact
- 新增依赖:`react-router`
- 新增目录:`src/web/pages/``src/web/components/Sidebar/`
- 新增文件:`src/web/routes.tsx``src/web/menu.tsx``src/web/hooks/use-sidebar-collapsed.ts`
- 修改文件:`src/web/app.tsx`(重构 Layout`src/web/main.tsx`+ BrowserRouter`src/web/styles.css`(布局样式重写)
- 修改配置:`vite.config.ts`code splitting 新增 vendor-router 组)
- 更新文档:`DEVELOPMENT.md``README.md`
- 新增测试Sidebar 组件测试、各页面测试、test-utils 增强
- 后端无需改动SPA fallback 已支持)

View File

@@ -1,15 +0,0 @@
## ADDED Requirements
### Requirement: 侧边栏折叠状态 localStorage key
侧边栏折叠状态存储 key SHALL 为 `"sidebar.collapsed"`,不包含应用名前缀。
#### Scenario: 折叠状态持久化
- **WHEN** 用户切换侧边栏折叠状态
- **THEN** 系统 SHALL 将状态存储到 localStorage key `"sidebar.collapsed"`
#### Scenario: 折叠状态读取
- **WHEN** 应用初始化时读取侧边栏折叠状态
- **THEN** 系统 SHALL 从 localStorage key `"sidebar.collapsed"` 读取

View File

@@ -1,56 +0,0 @@
## 1. 依赖与配置
- [x] 1.1 安装 react-router 依赖
- [x] 1.2 更新 vite.config.ts code splitting 配置,新增 vendor-router 组
## 2. 工具与 Hook
- [x] 2.1 创建 src/web/menu.tsx定义菜单项配置
- [x] 2.2 创建 src/web/hooks/use-sidebar-collapsed.ts实现侧边栏折叠状态 hook
## 3. 页面组件
- [x] 3.1 创建 src/web/pages/dashboard/index.tsx迁移原 app.tsx 内容区逻辑
- [x] 3.2 创建 src/web/pages/users/index.tsx实现用户管理占位页面
- [x] 3.3 创建 src/web/pages/settings/index.tsx实现系统设置占位页面
- [x] 3.4 创建 src/web/pages/404/index.tsx实现 404 页面
## 4. 侧边栏组件
- [x] 4.1 创建 src/web/components/Sidebar/index.tsx实现侧边栏菜单组件
## 5. 路由配置
- [x] 5.1 创建 src/web/routes.tsx定义所有路由
## 6. 根组件重构
- [x] 6.1 重构 src/web/app.tsx实现 Header + Aside + Content 布局
- [x] 6.2 修改 src/web/main.tsx添加 BrowserRouter 包裹
## 7. 样式更新
- [x] 7.1 更新 src/web/styles.css迁移 .dashboard-* 为 .app-*,新增布局样式
## 8. 测试工具增强
- [x] 8.1 增强 tests/web/test-utils.tsx提供 renderWithProviders 函数
## 9. 组件测试
- [x] 9.1 创建 tests/web/components/Sidebar/index.test.tsx
- [x] 9.2 创建 tests/web/routes/dashboard.test.tsx
- [x] 9.3 创建 tests/web/routes/users.test.tsx
- [x] 9.4 创建 tests/web/routes/settings.test.tsx
- [x] 9.5 创建 tests/web/routes/404.test.tsx
- [x] 9.6 更新 tests/web/App.test.tsx 适配 BrowserRouter
## 10. 文档更新
- [x] 10.1 更新 DEVELOPMENT.md更新技术栈表格、组件树、目录结构说明
- [x] 10.2 更新 README.md更新技术栈表格、项目结构说明
## 11. 质量保障
- [x] 11.1 运行 bun run check 确保类型检查、lint、测试通过
- [x] 11.2 运行 bun run build 验证构建成功

View File

@@ -1,4 +1,4 @@
## ADDED Requirements
## Requirements
### Requirement: 企业 Admin 后台布局
@@ -27,6 +27,7 @@ Header SHALL 包含折叠按钮、页面标题、主题切换控件。
- **WHEN** Header 渲染
- **THEN** 系统 SHALL 显示当前路由对应的页面标题(从 menu 获取)
- **AND** 若当前路由无匹配菜单项SHALL 显示 `APP.title` 作为 fallback
#### Scenario: Header 右侧主题切换

View File

@@ -59,3 +59,17 @@
- **WHEN** 应用初始化时读取用户主题偏好
- **THEN** 系统 SHALL 从 localStorage key `"theme.preference"` 读取
### Requirement: 侧边栏折叠状态 localStorage key
侧边栏折叠状态存储 key SHALL 为 `"sidebar.collapsed"`,不包含应用名前缀。
#### Scenario: 折叠状态持久化
- **WHEN** 用户切换侧边栏折叠状态
- **THEN** 系统 SHALL 将状态存储到 localStorage key `"sidebar.collapsed"`
#### Scenario: 折叠状态读取
- **WHEN** 应用初始化时读取侧边栏折叠状态
- **THEN** 系统 SHALL 从 localStorage key `"sidebar.collapsed"` 读取

View File

@@ -1,4 +1,4 @@
## ADDED Requirements
## Requirements
### Requirement: 前端 SPA 路由

View File

@@ -11,7 +11,7 @@ export function NotFoundPage() {
return (
<Space align="center" className="not-found-page" direction="vertical" size="large">
<ErrorCircleIcon size="64px" style={{ color: "var(--td-warning-color)" }} />
<ErrorCircleIcon className="not-found-icon" size="64px" />
<h1>404</h1>
<p>访</p>
<Button onClick={handleGoHome} theme="primary">

View File

@@ -14,7 +14,7 @@ export function DashboardPage() {
});
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Space className="full-width-space" direction="vertical" size="large">
<h2>使 {APP.title}</h2>
<p> /health API </p>
{health && <pre className="health-response">{JSON.stringify(health, null, 2)}</pre>}

View File

@@ -2,7 +2,7 @@ import { Card, Space } from "tdesign-react";
export function SettingsPage() {
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Space className="full-width-space" direction="vertical" size="large">
<h2></h2>
<Card>
<p>...</p>

View File

@@ -2,7 +2,7 @@ import { Card, Space } from "tdesign-react";
export function UsersPage() {
return (
<Space direction="vertical" size="large" style={{ width: "100%" }}>
<Space className="full-width-space" direction="vertical" size="large">
<h2></h2>
<Card>
<p>...</p>

View File

@@ -1,3 +1,5 @@
import type { ReactNode } from "react";
import { Route, Routes } from "react-router";
import { NotFoundPage } from "./pages/404";
@@ -15,3 +17,7 @@ export function AppRoutes() {
</Routes>
);
}
export function ProtectedRoute({ children }: { children: ReactNode }) {
return children;
}

View File

@@ -86,6 +86,14 @@
color: var(--td-text-color-disabled);
}
.full-width-space {
width: 100%;
}
.not-found-icon {
color: var(--td-warning-color);
}
.tabular-nums {
font-variant-numeric: tabular-nums;
}

View File

@@ -21,4 +21,12 @@ describe("Sidebar", () => {
expect(screen.getByText("用户管理")).not.toBeNull();
expect(screen.getByText("系统设置")).not.toBeNull();
});
test("高亮当前路由对应的菜单项", () => {
renderWithProviders(createElement(Sidebar, { collapsed: false }), { initialRoute: "/users" });
const activeItem = document.querySelector(".t-is-active");
expect(activeItem).not.toBeNull();
expect(activeItem?.textContent).toContain("用户管理");
});
});

View File

@@ -13,4 +13,12 @@ describe("NotFoundPage", () => {
expect(screen.getByText("您访问的页面不存在")).not.toBeNull();
expect(screen.getByText("返回首页")).not.toBeNull();
});
test("返回首页按钮存在且可点击", () => {
renderWithProviders(createElement(NotFoundPage));
const button = screen.getByText("返回首页");
expect(button).not.toBeNull();
expect(button.closest("button")).not.toBeNull();
});
});