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

27 KiB
Raw Blame History

里程管理模块实施计划

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

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

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: 提交
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

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 之后添加:

import mileageRouter from './routes/mileage.js';

app.route('/api/vehicles', vehiclesRouter); 之后添加:

app.route('/api/mileage', mileageRouter);
  • Step 3: 验证 TypeScript 编译

Run: npx tsc --noEmit Expected: PASS

  • Step 4: 验证 API 端点可访问

Run: npm run dev:server & 然后:

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: 提交
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

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: 提交
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

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

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

export default function MonitoringView() {
  return <div>MonitoringView placeholder</div>;
}

临时创建 src/modules/mileage/StatisticsView.tsx

export default function StatisticsView() {
  return <div>StatisticsView placeholder</div>;
}

Run: npx tsc --noEmit Expected: PASS

  • Step 4: 提交
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 文件内部定义
    • MileageViewactiveSubTab === 'monitoring' 分支的所有 JSX第 1693-2295 行)
  2. 数据来源变更(将 mock 数据替换为 API 调用):

    • 删除对 MOCK_VEHICLES 的所有引用
    • 添加状态 const [allVehicles, setAllVehicles] = useState<MonitoringVehicle[]>([]);
    • 使用 useEffect 调用 fetchMonitoring() 加载数据,每 60 秒刷新
    • filteredVehiclesuseMemo 改为对 allVehicles 进行过滤和排序
    • departmentsplateNumbersprojectsallVehicles 提取唯一值
    • statsuseMemo 保持原逻辑,但基于 filteredVehicles 计算
  3. 字段映射变更

    • v.plateNumberv.plate
    • v.customer 保持不变
    • v.department 保持不变
    • v.todayMileagev.dailyKm
    • v.totalMileagev.totalKm
    • v.isOnline 保持不变
    • v.isDataSynced 保持不变
    • v.idv.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 列表(完整):

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: 提交
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_STATSMOCK_PROJECT_MILEAGEMOCK_VEHICLES 的引用
    • 添加状态:
      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.modeltarget.targetName
    • row.counttarget.vehicleCount
    • row.targettarget.totalMileagePerVehicle * target.vehicleCount
    • row.driventarget.cumulativeTotal
    • row.completiontarget.avgCompletion
    • row.periodtarget.period
    • row.year1Targettarget.annualMileagePerVehicle
    • row.reachedCounttarget.yearQualifiedCount
    • row.halfReachedCounttarget.halfQualifiedCount
    • row.todayTotaltarget.todayTotal
    • row.currentYearTargettarget.currentYearTarget
    • row.completedAsOftarget.currentYearCompleted
    • row.remainingtarget.remaining
    • row.daysLefttarget.daysLeft
    • row.dailyTargettarget.dailyTarget
  4. 项目选择器变更

    • projectListtargets.map(t => t.targetName)
    • 选择按钮的 onClick 改为设置 selectedTargetId
    • currentData.trendtrendData
  5. 车辆明细变更

    • 原型中通过 MOCK_VEHICLES.filter(v => ...) 匹配车辆 → 改为使用 targetVehicles[target.id]
    • 车辆明细中 v.plateNumbertv.plateNumber
    • v.isOnline → 不可用,暂时全部显示为在线
    • v.todayMileagetv.todayMileage
    • v.totalMileagetv.totalMileage
  6. 查看全部侧滑面板变更

    • viewAllModel 改为 viewAllTargetId: number | null
    • 点击"查看全部"时设置 viewAllTargetId 并调用 fetchTargetVehicles(id)
    • 面板内车辆数据来自 targetVehicles[viewAllTargetId]
  7. 保持不变

    • 所有 CSS className
    • 图表配置Recharts BarChart/LineChart/AreaChart 的 props、样式、颜色
    • 全屏表格叠加层的布局和样式
    • 侧滑面板的动画和布局
    • 展开/折叠的 AnimatePresence 动画
  8. Import 列表(完整):

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: 提交
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: 提交(如有修复)
git add -A
git commit -m "fix: 里程管理模块集成修复"