Files
ln-bi/docs/superpowers/plans/2026-04-01-mileage-module.md
kkfluous 3d6c31a86e docs: 添加里程管理模块实施计划
7 个 Task:后端数据库连接+API 路由、前端类型+API 客户端、
MileageModule Tab 切换、MonitoringView 1:1 复刻、
StatisticsView 1:1 复刻、集成验证。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:01:58 +08:00

816 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 里程管理模块实施计划
> **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/` 下拆分为 MileageModuleTab 切换、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<string, any>();
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<string, any>();
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<number, any>();
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<T>(url: string): Promise<T> {
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<MonitoringData> {
return fetchJson<MonitoringData>(`${BASE}/monitoring`);
}
export async function fetchTargets(): Promise<TargetSummary[]> {
return fetchJson<TargetSummary[]>(`${BASE}/targets`);
}
export async function fetchTargetVehicles(targetId: number): Promise<TargetVehicle[]> {
return fetchJson<TargetVehicle[]>(`${BASE}/target/${targetId}/vehicles`);
}
export async function fetchTrend(targetId?: number, days = 7): Promise<TrendPoint[]> {
const params = new URLSearchParams();
if (targetId) params.set('targetId', String(targetId));
params.set('days', String(days));
return fetchJson<TrendPoint[]>(`${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 (
<div className="flex items-center justify-center py-20">
<div className="text-center">
<FileText size={48} className="mx-auto text-gray-300 mb-4" />
<h2 className="text-lg font-semibold text-gray-500"></h2>
<p className="text-sm text-gray-400 mt-2">...</p>
</div>
</div>
);
}
```
- [ ] **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 (
<div className="min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-6 relative">
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 px-1 landscape:pb-0 landscape:h-full landscape:flex-1 landscape:overflow-hidden">
{/* Sub-navigation */}
<div className="bg-white px-4 py-2 rounded-2xl border border-slate-100 shadow-sm flex items-center gap-6">
<button
onClick={() => setActiveSubTab('monitoring')}
className={`flex items-center gap-2 py-1 transition-all relative ${activeSubTab === 'monitoring' ? 'text-blue-600' : 'text-slate-400'}`}
>
<LayoutDashboard size={14} />
<span className="text-[11px] font-bold"></span>
{activeSubTab === 'monitoring' && (
<motion.div layoutId="activeSubTab" className="absolute -bottom-2 left-0 right-0 h-0.5 bg-blue-600 rounded-full" />
)}
</button>
<button
onClick={() => setActiveSubTab('statistics')}
className={`flex items-center gap-2 py-1 transition-all relative ${activeSubTab === 'statistics' ? 'text-blue-600' : 'text-slate-400'}`}
>
<BarChart3 size={14} />
<span className="text-[11px] font-bold"></span>
{activeSubTab === 'statistics' && (
<motion.div layoutId="activeSubTab" className="absolute -bottom-2 left-0 right-0 h-0.5 bg-blue-600 rounded-full" />
)}
</button>
<button
onClick={() => setActiveSubTab('report')}
className={`flex items-center gap-2 py-1 transition-all relative ${activeSubTab === 'report' ? 'text-blue-600' : 'text-slate-400'}`}
>
<FileText size={14} />
<span className="text-[11px] font-bold"></span>
{activeSubTab === 'report' && (
<motion.div layoutId="activeSubTab" className="absolute -bottom-2 left-0 right-0 h-0.5 bg-blue-600 rounded-full" />
)}
</button>
</div>
{activeSubTab === 'monitoring' ? (
<MonitoringView />
) : activeSubTab === 'statistics' ? (
<StatisticsView />
) : (
<DailyReportView />
)}
</div>
</div>
);
}
```
- [ ] **Step 3: 验证 TypeScript 编译**
注意:此时 MonitoringView 和 StatisticsView 尚未创建,可能会报错。先创建空的占位文件:
临时创建 `src/modules/mileage/MonitoringView.tsx`
```tsx
export default function MonitoringView() {
return <div>MonitoringView placeholder</div>;
}
```
临时创建 `src/modules/mileage/StatisticsView.tsx`
```tsx
export default function StatisticsView() {
return <div>StatisticsView placeholder</div>;
}
```
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<MonitoringVehicle[]>([]);`
- 使用 `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 classNameTailwind 类名 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<TargetSummary[]>([]);
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
const [targetVehicles, setTargetVehicles] = useState<Record<number, TargetVehicle[]>>({});
```
- 使用 `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: 里程管理模块集成修复"
```