diff --git a/docs/superpowers/plans/2026-04-01-modular-refactor.md b/docs/superpowers/plans/2026-04-01-modular-refactor.md new file mode 100644 index 0000000..0f4c346 --- /dev/null +++ b/docs/superpowers/plans/2026-04-01-modular-refactor.md @@ -0,0 +1,509 @@ +# 模块化重构实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将单体 App.tsx 拆分为模块化架构,支持多 BI 大类(资产管理、里程管理)通过全局导航切换。 + +**Architecture:** 新增 Shell 布局组件管理全局导航(Web 侧边栏 / 移动端底部导航),每个 BI 模块作为独立目录(modules/assets、modules/mileage),通过 hash 路由切换。现有资产管理逻辑原样迁入 modules/assets/,去掉其内部底部导航。 + +**Tech Stack:** React 19, TypeScript, Tailwind CSS 4, Lucide Icons, Vite + +--- + +## 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| 创建 | `src/components/SearchSelect.tsx` | 从 App.tsx 抽取的公共搜索下拉组件 | +| 创建 | `src/components/Shell.tsx` | 全局布局壳(侧边栏 + 底部导航 + 内容区) | +| 移动 | `src/types.ts` → `src/modules/assets/types.ts` | 资产管理类型定义 | +| 移动 | `src/api.ts` → `src/modules/assets/api.ts` | 资产管理 API 客户端 | +| 创建 | `src/modules/assets/AssetsModule.tsx` | 资产管理主组件(现 App.tsx 逻辑迁入) | +| 创建 | `src/modules/mileage/MileageModule.tsx` | 里程管理占位组件 | +| 重写 | `src/App.tsx` | 顶层壳:模块注册 + Shell 渲染 | + +--- + +### Task 1: 抽取 SearchSelect 公共组件 + +**Files:** +- Create: `src/components/SearchSelect.tsx` + +- [ ] **Step 1: 创建 SearchSelect 组件文件** + +从现有 `src/App.tsx` 第 38-106 行抽取 SearchSelect 组件,加上必要的 import: + +```tsx +import { useState, useEffect, useMemo, useRef } from 'react'; +import { ChevronDown } from 'lucide-react'; + +export function SearchSelect({ value, onChange, options, placeholder, className }: { + value: string; + onChange: (v: string) => void; + options: string[]; + placeholder: string; + className?: string; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(''); + const ref = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + const filtered = useMemo(() => { + if (!query) return options; + const q = query.toLowerCase(); + return options.filter((o) => o.toLowerCase().includes(q)); + }, [options, query]); + + const displayValue = value || ''; + + return ( +
+
setOpen(!open)} + > + { setQuery(e.target.value); if (!open) setOpen(true); }} + onFocus={() => { setOpen(true); setQuery(''); }} + /> + +
+ {open && ( +
+
{ onChange(''); setQuery(''); setOpen(false); }} + > + {placeholder} +
+ {filtered.map((o) => ( +
{ onChange(o); setQuery(''); setOpen(false); }} + > + {o} +
+ ))} + {filtered.length === 0 && ( +
无匹配项
+ )} +
+ )} +
+ ); +} +``` + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: 无新增错误(SearchSelect 尚未被引用,不影响现有代码) + +- [ ] **Step 3: 提交** + +```bash +git add src/components/SearchSelect.tsx +git commit -m "refactor: 抽取 SearchSelect 为公共组件" +``` + +--- + +### Task 2: 移动 types.ts 和 api.ts 到 assets 模块 + +**Files:** +- Move: `src/types.ts` → `src/modules/assets/types.ts` +- Move: `src/api.ts` → `src/modules/assets/api.ts` +- Modify: `src/App.tsx` (更新 import 路径) +- Modify: `src/modules/assets/api.ts` (更新 import 路径) + +- [ ] **Step 1: 创建目录并移动文件** + +```bash +mkdir -p src/modules/assets +git mv src/types.ts src/modules/assets/types.ts +git mv src/api.ts src/modules/assets/api.ts +``` + +- [ ] **Step 2: 更新 api.ts 内部的 import 路径** + +`src/modules/assets/api.ts` 第 1-9 行,import 路径从 `'./types'` 保持不变(同目录),无需修改。 + +- [ ] **Step 3: 更新 App.tsx 的 import 路径** + +将 `src/App.tsx` 中的两处 import: + +```ts +import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types'; +import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './api'; +import type { WeeklyDetailItem } from './api'; +``` + +改为: + +```ts +import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './modules/assets/types'; +import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './modules/assets/api'; +import type { WeeklyDetailItem } from './modules/assets/api'; +``` + +- [ ] **Step 4: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS,无错误 + +- [ ] **Step 5: 验证开发服务器启动** + +Run: `npm run dev:client` (启动后 Ctrl+C 关闭) +Expected: Vite 正常启动,无编译错误 + +- [ ] **Step 6: 提交** + +```bash +git add -A +git commit -m "refactor: 移动 types.ts 和 api.ts 到 modules/assets/" +``` + +--- + +### Task 3: 创建 AssetsModule 组件 + +**Files:** +- Create: `src/modules/assets/AssetsModule.tsx` +- Modify: `src/App.tsx` (后续 Task 5 重写时替换) + +这一步将 `src/App.tsx` 中 `export default function App()` 及其上方的 `TABS` 常量迁移为 `AssetsModule`,并做以下调整: + +- [ ] **Step 1: 创建 AssetsModule.tsx** + +复制 `src/App.tsx` 的全部内容到 `src/modules/assets/AssetsModule.tsx`,然后做以下修改: + +**修改 1 — import 路径调整(文件顶部):** + +将: +```ts +import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './modules/assets/types'; +import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './modules/assets/api'; +import type { WeeklyDetailItem } from './modules/assets/api'; +``` +改为: +```ts +import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types'; +import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats, fetchRegionChart } from './api'; +import type { WeeklyDetailItem } from './api'; +``` + +**修改 2 — SearchSelect 改为外部导入:** + +删除文件中第 37-106 行的 SearchSelect 组件定义(`// --- SearchSelect Component ---` 到闭合的 `}`),替换为 import: + +```ts +import { SearchSelect } from '../../components/SearchSelect'; +``` + +**修改 3 — 删除 import 中不再需要的图标:** + +从 lucide-react import 中移除 `Users` 和 `Building2`(这两个只被底部导航使用,删除底部导航后不再需要)。`MapPin` 保留(在内容区域第 2106 行仍被使用)。 + +**修改 4 — 组件名改为 AssetsModule:** + +将: +```ts +export default function App() { +``` +改为: +```ts +export default function AssetsModule() { +``` + +**修改 5 — 删除底部导航栏(原第 2772-2802 行):** + +删除从 `{/* Footer / Navigation */}` 到其对应 `` 的整个代码块: + +```tsx + {/* Footer / Navigation */} +
+ ...全部删除... +
+``` + +**修改 6 — 去掉底部导航的 padding 留白:** + +将根 div 的 className: +``` +min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-6 pb-20 md:pb-6 relative +``` +改为: +``` +min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-6 relative +``` + +即去掉 `pb-20 md:pb-6`,统一使用 `p-6` 的 padding。 + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS(AssetsModule 已自包含,App.tsx 仍引用旧路径但即将被重写) + +- [ ] **Step 3: 提交** + +```bash +git add src/modules/assets/AssetsModule.tsx +git commit -m "refactor: 创建 AssetsModule,迁移资产管理逻辑" +``` + +--- + +### Task 4: 创建 Shell 布局组件 + +**Files:** +- Create: `src/components/Shell.tsx` + +- [ ] **Step 1: 创建 Shell.tsx** + +```tsx +import { useState, useEffect, type ComponentType } from 'react'; +import { Truck, Route } from 'lucide-react'; + +export interface ModuleConfig { + id: string; + label: string; + icon: ComponentType<{ size?: number; className?: string }>; + component: ComponentType; +} + +function getHashModule(modules: ModuleConfig[]): string { + const hash = window.location.hash.slice(1); + return modules.some((m) => m.id === hash) ? hash : modules[0]?.id ?? ''; +} + +export function Shell({ modules }: { modules: ModuleConfig[] }) { + const [activeModule, setActiveModule] = useState(() => getHashModule(modules)); + + useEffect(() => { + const onHashChange = () => setActiveModule(getHashModule(modules)); + window.addEventListener('hashchange', onHashChange); + return () => window.removeEventListener('hashchange', onHashChange); + }, [modules]); + + useEffect(() => { + if (!window.location.hash) { + window.location.hash = modules[0]?.id ?? ''; + } + }, [modules]); + + const switchModule = (id: string) => { + window.location.hash = id; + }; + + const ActiveComponent = modules.find((m) => m.id === activeModule)?.component ?? modules[0]?.component; + + return ( +
+ {/* Web 侧边栏 (md 及以上) */} + + + {/* 内容区 */} +
+ {ActiveComponent && } +
+ + {/* 移动端底部导航 (md 以下) */} + +
+ ); +} +``` + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS(Shell 尚未被引用) + +- [ ] **Step 3: 提交** + +```bash +git add src/components/Shell.tsx +git commit -m "feat: 创建 Shell 布局组件(侧边栏 + 底部导航)" +``` + +--- + +### Task 5: 创建里程管理占位组件 + +**Files:** +- Create: `src/modules/mileage/MileageModule.tsx` + +- [ ] **Step 1: 创建 MileageModule.tsx** + +```tsx +import { Route } from 'lucide-react'; + +export default function MileageModule() { + return ( +
+
+ +

里程管理

+

开发中...

+
+
+ ); +} +``` + +- [ ] **Step 2: 提交** + +```bash +mkdir -p src/modules/mileage +git add src/modules/mileage/MileageModule.tsx +git commit -m "feat: 创建里程管理占位组件" +``` + +--- + +### Task 6: 重写 App.tsx 为顶层壳 + +**Files:** +- Rewrite: `src/App.tsx` + +- [ ] **Step 1: 重写 App.tsx** + +用以下内容完全替换 `src/App.tsx`: + +```tsx +import { Truck, Route } from 'lucide-react'; +import { Shell, type ModuleConfig } from './components/Shell'; +import AssetsModule from './modules/assets/AssetsModule'; +import MileageModule from './modules/mileage/MileageModule'; + +const MODULES: ModuleConfig[] = [ + { id: 'assets', label: '资产管理', icon: Truck, component: AssetsModule }, + { id: 'mileage', label: '里程管理', icon: Route, component: MileageModule }, +]; + +export default function App() { + return ; +} +``` + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS,无错误 + +- [ ] **Step 3: 验证 Vite 构建** + +Run: `npm run build` +Expected: 构建成功,无错误 + +- [ ] **Step 4: 本地验证功能** + +Run: `npm run dev` + +手动检查: +1. 打开 `http://localhost:3000` — 应看到左侧侧边栏(Web)或底部导航(移动端模拟) +2. 默认进入资产管理,所有 Tab(总览/按部门/按区域/按客户)正常 +3. 数据正常加载显示 +4. 点击"里程管理"切换到占位页面 +5. 点击"资产管理"切回,数据和状态正常 +6. URL hash 随切换变化(`#assets` / `#mileage`) +7. 原有移动端底部的资产内部导航已消失 + +- [ ] **Step 5: 提交** + +```bash +git add src/App.tsx +git commit -m "refactor: 重写 App.tsx 为模块化顶层壳" +``` + +--- + +### Task 7: 清理旧文件引用 + +**Files:** +- Delete: `src/types.ts` (如果 git mv 未处理干净) +- Delete: `src/api.ts` (如果 git mv 未处理干净) +- Verify: 无残留的旧 import 路径 + +- [ ] **Step 1: 确认无残留文件** + +```bash +ls src/types.ts src/api.ts 2>/dev/null && echo "STALE FILES EXIST" || echo "CLEAN" +``` + +Expected: `CLEAN`(Task 2 已用 `git mv` 移动) + +如果有残留,删除它们: +```bash +rm -f src/types.ts src/api.ts +``` + +- [ ] **Step 2: 确认无旧 import 引用** + +```bash +grep -r "from '\./types'" src/ --include="*.tsx" --include="*.ts" | grep -v modules/assets/ | grep -v node_modules +grep -r "from '\./api'" src/ --include="*.tsx" --include="*.ts" | grep -v modules/assets/ | grep -v node_modules +``` + +Expected: 无输出(所有引用都已更新到新路径) + +- [ ] **Step 3: 最终构建验证** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 4: 提交(如有清理)** + +```bash +git add -A +git status +# 如果有变更则提交: +git commit -m "chore: 清理残留文件和旧引用" +```