# 模块化重构实施计划 > **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: 清理残留文件和旧引用" ```