From 3d6c31a86e3c763aeaca57427fe8fcf044b05300 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Wed, 1 Apr 2026 21:01:58 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E9=87=8C=E7=A8=8B?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=E5=AE=9E=E6=96=BD=E8=AE=A1?= =?UTF-8?q?=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7 个 Task:后端数据库连接+API 路由、前端类型+API 客户端、 MileageModule Tab 切换、MonitoringView 1:1 复刻、 StatisticsView 1:1 复刻、集成验证。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-04-01-mileage-module.md | 815 ++++++++++++++++++ 1 file changed, 815 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-01-mileage-module.md diff --git a/docs/superpowers/plans/2026-04-01-mileage-module.md b/docs/superpowers/plans/2026-04-01-mileage-module.md new file mode 100644 index 0000000..8c5b74a --- /dev/null +++ b/docs/superpowers/plans/2026-04-01-mileage-module.md @@ -0,0 +1,815 @@ +# 里程管理模块实施计划 + +> **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:** 实现里程管理 BI 模块,1:1 复刻原型 UI,接入 hydrogen_energy + lingniu_prod 两个数据库。 + +**Architecture:** 后端新增 hydrogen_energy 数据库连接 + `/api/mileage/*` 路由(4 个端点)。前端在 `src/modules/mileage/` 下拆分为 MileageModule(Tab 切换)、MonitoringView(实时监控)、StatisticsView(统计报表)、DailyReportView(占位)。UI 1:1 复刻原型 `/Users/kkfluous/Projects/ai-coding/ln-yuanxing/lnoneos-1/src/App.tsx`。 + +**Tech Stack:** React 19, TypeScript, Tailwind CSS 4, Recharts, Motion, Lucide Icons, Hono, mysql2 + +--- + +## 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| 创建 | `src/server/mileage-db.ts` | hydrogen_energy 数据库连接池 | +| 创建 | `src/server/routes/mileage.ts` | 里程管理 API 路由(4 个端点) | +| 修改 | `src/server/index.ts` | 注册 `/api/mileage` 路由 | +| 创建 | `src/modules/mileage/types.ts` | 里程管理类型定义 | +| 创建 | `src/modules/mileage/api.ts` | API 客户端函数 | +| 重写 | `src/modules/mileage/MileageModule.tsx` | 主组件:子 Tab 切换 | +| 创建 | `src/modules/mileage/MonitoringView.tsx` | 实时监控视图 | +| 创建 | `src/modules/mileage/StatisticsView.tsx` | 统计报表视图 | +| 创建 | `src/modules/mileage/DailyReportView.tsx` | 每日汇报(占位) | + +--- + +### Task 1: 后端 — 数据库连接 + 类型定义 + +**Files:** +- Create: `src/server/mileage-db.ts` +- Create: `src/modules/mileage/types.ts` + +- [ ] **Step 1: 创建 hydrogen_energy 数据库连接池** + +创建 `src/server/mileage-db.ts`: + +```ts +import mysql from 'mysql2/promise'; + +const mileagePool = mysql.createPool({ + host: '101.133.130.65', + port: 3306, + user: 'bi_reader_02', + password: 'bi_reader_02_Pass', + database: 'hydrogen_energy', + waitForConnections: true, + connectionLimit: 5, + queueLimit: 0, +}); + +export default mileagePool; +``` + +- [ ] **Step 2: 创建前端类型定义** + +创建 `src/modules/mileage/types.ts`: + +```ts +export interface MonitoringVehicle { + plate: string; + vin: string; + dailyKm: number; + totalKm: number | null; + source: string; + isOnline: boolean; + isDataSynced: boolean; + customer: string | null; + department: string | null; + manager: string | null; +} + +export interface MonitoringData { + vehicles: MonitoringVehicle[]; + updatedAt: string; +} + +export interface TargetSummary { + id: number; + targetName: string; + vehicleCount: number; + totalMileagePerVehicle: number; + annualMileagePerVehicle: number; + assessmentYears: number; + period: string; + todayTotal: number; + cumulativeTotal: number; + avgCompletion: number; + qualifiedCount: number; + yearQualifiedCount: number; + halfQualifiedCount: number; + currentYearTarget: number; + currentYearCompleted: number; + remaining: number; + daysLeft: number; + dailyTarget: number; +} + +export interface TargetVehicle { + plateNumber: string; + todayMileage: number; + totalMileage: number; + completionRate: number; + isQualified: boolean; + currentYearIsQualified: boolean; + dailyRequiredMileage: number; +} + +export interface TrendPoint { + date: string; + mileage: number; +} +``` + +- [ ] **Step 3: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS + +- [ ] **Step 4: 提交** + +```bash +git add src/server/mileage-db.ts src/modules/mileage/types.ts +git commit -m "feat: 添加 hydrogen_energy 数据库连接和里程管理类型定义" +``` + +--- + +### Task 2: 后端 — API 路由 + +**Files:** +- Create: `src/server/routes/mileage.ts` +- Modify: `src/server/index.ts` + +- [ ] **Step 1: 创建里程管理路由** + +创建 `src/server/routes/mileage.ts`: + +```ts +import { Hono } from 'hono'; +import pool from '../db.js'; +import mileagePool from '../mileage-db.js'; + +const app = new Hono(); + +// 车辆关联信息 SQL(客户名、部门、经理) +const VEHICLE_INFO_SQL = `SELECT + truck.plate_number AS plate, + cus.customer_name AS customer, + dep.dep_name AS department, + u.user_name AS manager +FROM tab_truck truck +LEFT JOIN tab_truck_status_info si ON si.truck_id = truck.id AND si.is_deleted = 0 +LEFT JOIN tab_contract c ON c.id = si.contract_id AND c.is_deleted = 0 +LEFT JOIN tab_customer cus ON cus.id = c.customer_id AND cus.is_deleted = 0 +LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0 +LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 +WHERE truck.is_deleted = 0 AND truck.is_operation = 1`; + +// GET /monitoring — 实时监控数据 +app.get('/monitoring', async (c) => { + try { + // 1. 从 hydrogen_energy 取最新日期的里程数据 + const [dateRows] = await mileagePool.execute( + 'SELECT MAX(stat_date) as latest FROM v_vehicle_daily_stats' + ) as any; + const latestDate = dateRows[0]?.latest; + if (!latestDate) return c.json({ vehicles: [], updatedAt: new Date().toISOString() }); + + const [mileageRows] = await mileagePool.execute( + `SELECT plate, vin, daily_km, total_km, source + FROM v_vehicle_daily_stats + WHERE stat_date = ?`, + [latestDate] + ) as any; + + // 对于同一 plate 可能有多条记录(不同 source),取 daily_km 最大的 + const mileageMap = new Map(); + for (const row of mileageRows) { + const existing = mileageMap.get(row.plate); + if (!existing || Number(row.daily_km) > Number(existing.daily_km)) { + mileageMap.set(row.plate, row); + } + } + + // 2. 从 lingniu_prod 取车辆关联信息 + const [infoRows] = await pool.execute(VEHICLE_INFO_SQL) as any; + const infoMap = new Map(); + for (const row of infoRows) { + infoMap.set(row.plate, row); + } + + // 3. 合并 + const vehicles = Array.from(mileageMap.values()).map((m: any) => { + const info = infoMap.get(m.plate); + const dailyKm = Number(m.daily_km) || 0; + const source = m.source || 'NONE'; + return { + plate: m.plate, + vin: m.vin, + dailyKm, + totalKm: m.total_km !== null ? Number(m.total_km) : null, + source, + isOnline: source !== 'NONE' && dailyKm > 0, + isDataSynced: source !== 'NONE', + customer: info?.customer || null, + department: info?.department || null, + manager: info?.manager || null, + }; + }); + + return c.json({ vehicles, updatedAt: new Date().toISOString() }); + } catch (e) { + console.error('monitoring error:', e); + return c.json({ vehicles: [], updatedAt: new Date().toISOString() }, 500); + } +}); + +// GET /targets — 考核项目列表 + 汇总 +app.get('/targets', async (c) => { + try { + const [targets] = await pool.execute( + 'SELECT * FROM tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id' + ) as any; + + const [vehicleStats] = await pool.execute(` + SELECT + target_id, + COUNT(*) as total, + SUM(today_mileage) as today_total, + SUM(current_mileage) as cumulative_total, + AVG(completion_rate) as avg_completion, + SUM(CASE WHEN is_qualified = 1 THEN 1 ELSE 0 END) as qualified_count, + SUM(CASE WHEN current_year_is_qualified = 1 THEN 1 ELSE 0 END) as year_qualified_count, + SUM(CASE WHEN completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count, + SUM(current_year_mileage_task) as current_year_target, + SUM(current_year_mileage) as current_year_completed, + MAX(current_year_assessment_end_date) as year_end_date + FROM tab_mileage_assessment_vehicle + WHERE is_deleted = 0 + GROUP BY target_id + `) as any; + + const statsMap = new Map(); + for (const s of vehicleStats) { + statsMap.set(s.target_id, s); + } + + const now = new Date(); + const result = targets.map((t: any) => { + const s = statsMap.get(t.id) || {}; + const currentYearTarget = Number(s.current_year_target) || 0; + const currentYearCompleted = Number(s.current_year_completed) || 0; + const remaining = Math.max(0, currentYearTarget - currentYearCompleted); + const yearEnd = s.year_end_date ? new Date(s.year_end_date) : now; + const daysLeft = Math.max(1, Math.ceil((yearEnd.getTime() - now.getTime()) / 86400000)); + const dailyTarget = remaining / daysLeft; + + const startDate = t.default_start_date + ? new Date(t.default_start_date).toISOString().split('T')[0] + : ''; + const endDate = t.default_end_date + ? new Date(t.default_end_date).toISOString().split('T')[0] + : ''; + + return { + id: t.id, + targetName: t.target_name, + vehicleCount: Number(s.total) || t.vehicle_count, + totalMileagePerVehicle: Number(t.total_mileage_per_vehicle), + annualMileagePerVehicle: Number(t.annual_mileage_per_vehicle), + assessmentYears: t.assessment_years, + period: `${startDate} ~ ${endDate}`, + todayTotal: Number(s.today_total) || 0, + cumulativeTotal: Number(s.cumulative_total) || 0, + avgCompletion: (Number(s.avg_completion) || 0) * 100, + qualifiedCount: Number(s.qualified_count) || 0, + yearQualifiedCount: Number(s.year_qualified_count) || 0, + halfQualifiedCount: Number(s.half_qualified_count) || 0, + currentYearTarget, + currentYearCompleted, + remaining, + daysLeft, + dailyTarget: Math.round(dailyTarget * 10) / 10, + }; + }); + + return c.json(result); + } catch (e) { + console.error('targets error:', e); + return c.json([], 500); + } +}); + +// GET /target/:id/vehicles — 某项目的车辆明细 +app.get('/target/:id/vehicles', async (c) => { + const targetId = c.req.param('id'); + try { + const [rows] = await pool.execute( + `SELECT plate_number, today_mileage, vehicle_total_mileage, + completion_rate, is_qualified, current_year_is_qualified, + daily_required_mileage + FROM tab_mileage_assessment_vehicle + WHERE target_id = ? AND is_deleted = 0 + ORDER BY today_mileage DESC`, + [targetId] + ) as any; + + const result = rows.map((r: any) => ({ + plateNumber: r.plate_number, + todayMileage: Number(r.today_mileage) || 0, + totalMileage: Number(r.vehicle_total_mileage) || 0, + completionRate: Number(r.completion_rate) || 0, + isQualified: r.is_qualified === 1, + currentYearIsQualified: r.current_year_is_qualified === 1, + dailyRequiredMileage: Number(r.daily_required_mileage) || 0, + })); + + return c.json(result); + } catch (e) { + console.error('target vehicles error:', e); + return c.json([], 500); + } +}); + +// GET /trend — 7天里程趋势 +app.get('/trend', async (c) => { + const targetId = c.req.query('targetId'); + const days = Number(c.req.query('days')) || 7; + try { + let plates: string[] = []; + if (targetId) { + const [vehicleRows] = await pool.execute( + 'SELECT plate_number FROM tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0', + [targetId] + ) as any; + plates = vehicleRows.map((r: any) => r.plate_number); + if (plates.length === 0) return c.json([]); + } + + let sql = ` + SELECT DATE_FORMAT(stat_date, '%m-%d') as date, SUM(daily_km) as mileage + FROM v_vehicle_daily_stats + WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + `; + const params: any[] = [days]; + + if (plates.length > 0) { + sql += ` AND plate IN (${plates.map(() => '?').join(',')})`; + params.push(...plates); + } + + sql += ' GROUP BY stat_date ORDER BY stat_date'; + + const [rows] = await mileagePool.execute(sql, params) as any; + + const result = rows.map((r: any) => ({ + date: r.date, + mileage: Math.round(Number(r.mileage) || 0), + })); + + return c.json(result); + } catch (e) { + console.error('trend error:', e); + return c.json([], 500); + } +}); + +export default app; +``` + +- [ ] **Step 2: 注册路由到 server/index.ts** + +在 `src/server/index.ts` 中,在 `import vehiclesRouter` 之后添加: + +```ts +import mileageRouter from './routes/mileage.js'; +``` + +在 `app.route('/api/vehicles', vehiclesRouter);` 之后添加: + +```ts +app.route('/api/mileage', mileageRouter); +``` + +- [ ] **Step 3: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS + +- [ ] **Step 4: 验证 API 端点可访问** + +Run: `npm run dev:server &` 然后: +```bash +curl -s http://localhost:3001/api/mileage/targets | head -c 200 +curl -s http://localhost:3001/api/mileage/trend?days=7 | head -c 200 +``` +Expected: 返回 JSON 数据(非空数组) + +关闭服务器后继续。 + +- [ ] **Step 5: 提交** + +```bash +git add src/server/mileage-db.ts src/server/routes/mileage.ts src/server/index.ts +git commit -m "feat: 添加里程管理 API 路由(monitoring/targets/trend)" +``` + +--- + +### Task 3: 前端 — API 客户端 + +**Files:** +- Create: `src/modules/mileage/api.ts` + +- [ ] **Step 1: 创建 API 客户端** + +创建 `src/modules/mileage/api.ts`: + +```ts +import type { MonitoringData, TargetSummary, TargetVehicle, TrendPoint } from './types'; + +const BASE = '/api/mileage'; + +async function fetchJson(url: string): Promise { + const res = await fetch(url); + if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`); + return res.json(); +} + +export async function fetchMonitoring(): Promise { + return fetchJson(`${BASE}/monitoring`); +} + +export async function fetchTargets(): Promise { + return fetchJson(`${BASE}/targets`); +} + +export async function fetchTargetVehicles(targetId: number): Promise { + return fetchJson(`${BASE}/target/${targetId}/vehicles`); +} + +export async function fetchTrend(targetId?: number, days = 7): Promise { + const params = new URLSearchParams(); + if (targetId) params.set('targetId', String(targetId)); + params.set('days', String(days)); + return fetchJson(`${BASE}/trend?${params.toString()}`); +} +``` + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS + +- [ ] **Step 3: 提交** + +```bash +git add src/modules/mileage/api.ts +git commit -m "feat: 添加里程管理 API 客户端" +``` + +--- + +### Task 4: 前端 — MileageModule + DailyReportView + +**Files:** +- Rewrite: `src/modules/mileage/MileageModule.tsx` +- Create: `src/modules/mileage/DailyReportView.tsx` + +- [ ] **Step 1: 创建 DailyReportView 占位组件** + +创建 `src/modules/mileage/DailyReportView.tsx`: + +```tsx +import { FileText } from 'lucide-react'; + +export default function DailyReportView() { + return ( +
+
+ +

每日汇报

+

开发中...

+
+
+ ); +} +``` + +- [ ] **Step 2: 重写 MileageModule.tsx** + +这个组件负责子 Tab 切换,1:1 复刻原型中 `MileageView` 组件的导航部分(原型文件第 1585-1691 行和第 2296-2301 行)。 + +用以下内容替换 `src/modules/mileage/MileageModule.tsx`: + +```tsx +import { useState } from 'react'; +import { LayoutDashboard, BarChart3, FileText } from 'lucide-react'; +import { motion } from 'motion/react'; +import MonitoringView from './MonitoringView'; +import StatisticsView from './StatisticsView'; +import DailyReportView from './DailyReportView'; + +export default function MileageModule() { + const [activeSubTab, setActiveSubTab] = useState<'monitoring' | 'statistics' | 'report'>('monitoring'); + + return ( +
+
+ {/* Sub-navigation */} +
+ + + +
+ + {activeSubTab === 'monitoring' ? ( + + ) : activeSubTab === 'statistics' ? ( + + ) : ( + + )} +
+
+ ); +} +``` + +- [ ] **Step 3: 验证 TypeScript 编译** + +注意:此时 MonitoringView 和 StatisticsView 尚未创建,可能会报错。先创建空的占位文件: + +临时创建 `src/modules/mileage/MonitoringView.tsx`: +```tsx +export default function MonitoringView() { + return
MonitoringView placeholder
; +} +``` + +临时创建 `src/modules/mileage/StatisticsView.tsx`: +```tsx +export default function StatisticsView() { + return
StatisticsView placeholder
; +} +``` + +Run: `npx tsc --noEmit` +Expected: PASS + +- [ ] **Step 4: 提交** + +```bash +git add src/modules/mileage/MileageModule.tsx src/modules/mileage/DailyReportView.tsx src/modules/mileage/MonitoringView.tsx src/modules/mileage/StatisticsView.tsx +git commit -m "feat: MileageModule Tab 切换 + DailyReportView 占位" +``` + +--- + +### Task 5: 前端 — MonitoringView(实时监控) + +**Files:** +- Rewrite: `src/modules/mileage/MonitoringView.tsx` + +**重要:** 此组件需要 1:1 复刻原型。原型代码在 `/Users/kkfluous/Projects/ai-coding/ln-yuanxing/lnoneos-1/src/App.tsx` 的以下行范围: +- `MileageView` 组件:第 1585-2303 行(当 `activeSubTab === 'monitoring'` 时渲染的部分:第 1693-2295 行) +- `SearchableSelect` 组件:第 644-722 行 + +- [ ] **Step 1: 实现 MonitoringView** + +用完整实现替换 `src/modules/mileage/MonitoringView.tsx`。 + +**实现要求:** + +1. **读取原型文件** `/Users/kkfluous/Projects/ai-coding/ln-yuanxing/lnoneos-1/src/App.tsx`,提取以下内容: + - `SearchableSelect` 组件定义(第 644-722 行)— 在 MonitoringView 文件内部定义 + - `MileageView` 中 `activeSubTab === 'monitoring'` 分支的所有 JSX(第 1693-2295 行) + +2. **数据来源变更**(将 mock 数据替换为 API 调用): + - 删除对 `MOCK_VEHICLES` 的所有引用 + - 添加状态 `const [allVehicles, setAllVehicles] = useState([]);` + - 使用 `useEffect` 调用 `fetchMonitoring()` 加载数据,每 60 秒刷新 + - `filteredVehicles` 的 `useMemo` 改为对 `allVehicles` 进行过滤和排序 + - `departments`、`plateNumbers`、`projects` 从 `allVehicles` 提取唯一值 + - `stats` 的 `useMemo` 保持原逻辑,但基于 `filteredVehicles` 计算 + +3. **字段映射变更**: + - `v.plateNumber` → `v.plate` + - `v.customer` 保持不变 + - `v.department` 保持不变 + - `v.todayMileage` → `v.dailyKm` + - `v.totalMileage` → `v.totalKm` + - `v.isOnline` 保持不变 + - `v.isDataSynced` 保持不变 + - `v.id` → `v.plate`(作为 key) + - `v.model` → 不可用,过滤中移除 model 匹配 + - `v.assetOwner` → 不可用,移除 entity 过滤逻辑 + - `v.location` → 不可用,移除 regionCode 过滤逻辑 + +4. **保持不变的部分**: + - 所有 CSS className(Tailwind 类名 1:1 保留) + - 所有动画(motion, AnimatePresence) + - 全屏叠加层的完整 JSX + - 高级筛选面板的完整 JSX(保留所有筛选器的 UI,即使部分过滤器暂无数据源如 entity/regionCode,保留 UI 但过滤逻辑可为空操作) + - KPI 卡片布局 + - 车辆列表卡片样式 + - `toggleFullscreen` 函数 + +5. **Import 列表**(完整): +```tsx +import { useState, useEffect, useMemo } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { + Truck, Search, Filter, ChevronDown, + Maximize2, Minimize2, RotateCcw, + ArrowUp, ArrowDown, LayoutDashboard, +} from 'lucide-react'; +import type { MonitoringVehicle } from './types'; +import { fetchMonitoring } from './api'; +``` + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS + +- [ ] **Step 3: 验证 Vite 构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 4: 提交** + +```bash +git add src/modules/mileage/MonitoringView.tsx +git commit -m "feat: 实现里程管理实时监控视图(1:1 复刻原型)" +``` + +--- + +### Task 6: 前端 — StatisticsView(统计报表) + +**Files:** +- Rewrite: `src/modules/mileage/StatisticsView.tsx` + +**重要:** 此组件需要 1:1 复刻原型。原型代码在 `/Users/kkfluous/Projects/ai-coding/ln-yuanxing/lnoneos-1/src/App.tsx` 的以下行范围: +- `StatisticsView` 组件:第 843-1364 行 + +- [ ] **Step 1: 实现 StatisticsView** + +用完整实现替换 `src/modules/mileage/StatisticsView.tsx`。 + +**实现要求:** + +1. **读取原型文件** `/Users/kkfluous/Projects/ai-coding/ln-yuanxing/lnoneos-1/src/App.tsx`,提取 `StatisticsView` 组件定义(第 843-1364 行)的完整 JSX。 + +2. **数据来源变更**: + - 删除对 `MOCK_MODEL_STATS`、`MOCK_PROJECT_MILEAGE`、`MOCK_VEHICLES` 的引用 + - 添加状态: + ```tsx + const [targets, setTargets] = useState([]); + const [trendData, setTrendData] = useState([]); + const [targetVehicles, setTargetVehicles] = useState>({}); + ``` + - 使用 `useEffect` 调用 `fetchTargets()` 加载考核项目列表 + - `selectedProject` 改为 `selectedTargetId: number | null`,默认选第一个 target 的 id + - 当 `selectedTargetId` 变化时调用 `fetchTrend(selectedTargetId)` 加载趋势数据 + - 展开车辆明细时调用 `fetchTargetVehicles(targetId)` 加载 + +3. **字段映射变更**(`MOCK_MODEL_STATS` 的字段 → `TargetSummary` 的字段): + - `row.model` → `target.targetName` + - `row.count` → `target.vehicleCount` + - `row.target` → `target.totalMileagePerVehicle * target.vehicleCount` + - `row.driven` → `target.cumulativeTotal` + - `row.completion` → `target.avgCompletion` + - `row.period` → `target.period` + - `row.year1Target` → `target.annualMileagePerVehicle` + - `row.reachedCount` → `target.yearQualifiedCount` + - `row.halfReachedCount` → `target.halfQualifiedCount` + - `row.todayTotal` → `target.todayTotal` + - `row.currentYearTarget` → `target.currentYearTarget` + - `row.completedAsOf` → `target.currentYearCompleted` + - `row.remaining` → `target.remaining` + - `row.daysLeft` → `target.daysLeft` + - `row.dailyTarget` → `target.dailyTarget` + +4. **项目选择器变更**: + - `projectList` → `targets.map(t => t.targetName)` + - 选择按钮的 onClick 改为设置 `selectedTargetId` + - `currentData.trend` → `trendData` + +5. **车辆明细变更**: + - 原型中通过 `MOCK_VEHICLES.filter(v => ...)` 匹配车辆 → 改为使用 `targetVehicles[target.id]` + - 车辆明细中 `v.plateNumber` → `tv.plateNumber` + - `v.isOnline` → 不可用,暂时全部显示为在线 + - `v.todayMileage` → `tv.todayMileage` + - `v.totalMileage` → `tv.totalMileage` + +6. **查看全部侧滑面板变更**: + - `viewAllModel` 改为 `viewAllTargetId: number | null` + - 点击"查看全部"时设置 `viewAllTargetId` 并调用 `fetchTargetVehicles(id)` + - 面板内车辆数据来自 `targetVehicles[viewAllTargetId]` + +7. **保持不变**: + - 所有 CSS className + - 图表配置(Recharts BarChart/LineChart/AreaChart 的 props、样式、颜色) + - 全屏表格叠加层的布局和样式 + - 侧滑面板的动画和布局 + - 展开/折叠的 AnimatePresence 动画 + +8. **Import 列表**(完整): +```tsx +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, + ResponsiveContainer, LineChart, Line, AreaChart, Area, + Cell, LabelList, +} from 'recharts'; +import { + Truck, ChevronDown, Maximize2, Minimize2, + Search, ArrowUpDown, X, +} from 'lucide-react'; +import type { TargetSummary, TargetVehicle, TrendPoint } from './types'; +import { fetchTargets, fetchTargetVehicles, fetchTrend } from './api'; +``` + +- [ ] **Step 2: 验证 TypeScript 编译** + +Run: `npx tsc --noEmit` +Expected: PASS + +- [ ] **Step 3: 验证 Vite 构建** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 4: 提交** + +```bash +git add src/modules/mileage/StatisticsView.tsx +git commit -m "feat: 实现里程管理统计报表视图(1:1 复刻原型)" +``` + +--- + +### Task 7: 集成验证 + +**Files:** +- 无新增文件,全面验证 + +- [ ] **Step 1: 构建验证** + +Run: `npm run build` +Expected: 构建成功 + +- [ ] **Step 2: 启动开发服务器验证** + +Run: `npm run dev` + +手动检查: +1. 打开 `http://localhost:3000` → 侧边栏/底部导航有"里程管理"入口 +2. 点击进入里程管理 → 看到 3 个子 Tab(实时监控/统计报表/每日汇报) +3. **实时监控 Tab**: + - KPI 卡片显示真实数据(总里程、平均单车、监控台数) + - 车辆列表加载 1004 辆车,按今日里程降序 + - 筛选器正常工作(按部门、按客户、按车牌) + - 高级筛选面板可展开/折叠 + - 排序切换(今日/累计、升/降序)正常 + - 全屏模式正常打开/关闭 +4. **统计报表 Tab**: + - 5 个项目按钮正确显示 + - 趋势图切换(柱状/折线/面积)正常 + - 车型考核汇总卡片展开/折叠正常 + - 展开详情显示真实考核数据 + - 车辆明细(前5台)正确显示 + - "查看全部"侧滑面板正常 + - 全屏表格正常 +5. **每日汇报 Tab**:显示"开发中"占位 + +- [ ] **Step 3: 提交(如有修复)** + +```bash +git add -A +git commit -m "fix: 里程管理模块集成修复" +```