refactor(stage6): 去掉热力图写死的默认区间与线上文件的演示数据
热力图默认区间
- 新增 src/shared/date-range.ts(近 N 天滚动窗口,按本地自然日)。
- 前端两个热力图与后端两个 query 模型原先都把默认区间写死为
2026-01-01 ~ 2026-07-13,会随时间永久指向一段历史数据;
现改为"近 30 天"滚动,并把 now 作为可注入参数以便测试。
- 两个 model 测试改为注入固定时钟并断言滚动窗口,不再硬编码日期。
氢能看板去演示数据(board/EnergyBiBoardApp.tsx,6554 -> 3765 行)
- 取消线上路径的 mock 兜底:省份下拉、站点汇总表、客户费用汇总表、
区域排行在 overview 未就绪时显示空,而不是假的 25 个站点 / Top30 账单。
- 删除已确认"零渲染点"的死代码(用 TypeScript AST 精确定位声明范围,
避免手工推断括号边界):
KpiDrillModal / CustomerBillDrillModal / StationBillDrillModal /
DrillPeriodControls / getYearTimeRange / getDrillTimeRange /
MOCK_STATION_SUMMARY_LIST / MOCK_CUSTOMER_SUMMARY_LIST
这些组件内含大量写死的 2026-08-08 快照日期与假流水,均不可达。
线上 KPI 下钻走的是 PrototypeDrillModal(真实 API)。
- 单站日报"最后更新时间"初值不再用伪造时间戳,改为与接口空态一致的文案。
注:本次未改动任何 DOM 结构与 CSS 类名;删除的均为不可达分支。
lint / test(134) / build 全绿。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,8 @@ export const StationDailyApp: React.FC<StationDailyAppProps> = ({
|
||||
const setStartDate = onStartDateChange ?? setLocalStartDate;
|
||||
const setEndDate = onEndDateChange ?? setLocalEndDate;
|
||||
const [drillStationId, setDrillStationId] = useState<string | null>(null);
|
||||
const [updatedAt, setUpdatedAt] = useState('2026-08-12 10:00');
|
||||
// 初值不用伪造时间戳;成功回调会写入真实 latestLedgerTime,缺数据时与之一致。
|
||||
const [updatedAt, setUpdatedAt] = useState('暂无账本更新时间');
|
||||
const [tick, setTick] = useState(0);
|
||||
const [kpiDrill, setKpiDrill] = useState<KpiDrill>(null);
|
||||
const [liveBoard, setLiveBoard] = useState<HydrogenStationBoardResponse | null>(null);
|
||||
|
||||
@@ -4,10 +4,11 @@ import HydrogenAmapCanvas from './HydrogenAmapCanvas';
|
||||
import HydrogenHeatmapDetailPanel from './HydrogenHeatmapDetailPanel';
|
||||
import HydrogenHeatmapFilters from './HydrogenHeatmapFilters';
|
||||
import { fetchAmapConfig, fetchHydrogenHeatmapMeta, fetchHydrogenHeatmapPoints, fetchNearbyHydrogenStations } from './api';
|
||||
import { recentDayRange } from '../../shared/date-range';
|
||||
import type { AmapConfig, HydrogenHeatmapMeta, HydrogenHeatmapMetric, HydrogenHeatmapResponse, HydrogenNearbyResponse, HydrogenPayer } from './types';
|
||||
|
||||
const DEFAULT_START = '2026-01-01';
|
||||
const DEFAULT_END = '2026-07-13';
|
||||
// 接口返回数据水位前的兜底范围:近 30 天滚动窗口(不再写死历史区间)。
|
||||
const FALLBACK_RANGE = recentDayRange(30);
|
||||
const integerFormat = new Intl.NumberFormat('zh-CN');
|
||||
const kgFormat = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 1 });
|
||||
|
||||
@@ -37,8 +38,8 @@ export default function HydrogenHeatmapModule() {
|
||||
const [config, setConfig] = useState<AmapConfig | null>(null);
|
||||
const [data, setData] = useState<HydrogenHeatmapResponse | null>(null);
|
||||
const [nearby, setNearby] = useState<HydrogenNearbyResponse | null>(null);
|
||||
const [startDate, setStartDate] = useState(DEFAULT_START);
|
||||
const [endDate, setEndDate] = useState(DEFAULT_END);
|
||||
const [startDate, setStartDate] = useState(FALLBACK_RANGE.start);
|
||||
const [endDate, setEndDate] = useState(FALLBACK_RANGE.end);
|
||||
const [query, setQuery] = useState('');
|
||||
const [payer, setPayer] = useState<HydrogenPayer>('all');
|
||||
const [metric, setMetric] = useState<HydrogenHeatmapMetric>('kg');
|
||||
@@ -112,8 +113,8 @@ export default function HydrogenHeatmapModule() {
|
||||
}, [deferredQuery, endDate, metric, payer, startDate]);
|
||||
|
||||
const reset = () => {
|
||||
setStartDate(meta?.startDate || DEFAULT_START);
|
||||
setEndDate(meta?.endDate || DEFAULT_END);
|
||||
setStartDate(meta?.startDate || FALLBACK_RANGE.start);
|
||||
setEndDate(meta?.endDate || FALLBACK_RANGE.end);
|
||||
setQuery('');
|
||||
setPayer('all');
|
||||
setMetric('kg');
|
||||
@@ -164,7 +165,7 @@ export default function HydrogenHeatmapModule() {
|
||||
<Metric value={integerFormat.format(data?.stationCount ?? meta?.eligibleStationCount ?? 0)} label="站点" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="flex items-center gap-2 text-[12px] text-slate-500 max-xl:hidden"><Clock3 size={15} strokeWidth={1.7} />数据更新至 {meta?.endDate || DEFAULT_END}</p>
|
||||
<p className="flex items-center gap-2 text-[12px] text-slate-500 max-xl:hidden"><Clock3 size={15} strokeWidth={1.7} />数据更新至 {meta?.endDate || FALLBACK_RANGE.end}</p>
|
||||
<button type="button" onClick={() => setIsFullscreen((current) => !current)} className="grid h-9 w-9 place-items-center rounded-lg border border-slate-200 text-slate-500 transition hover:border-cyan-300 hover:bg-cyan-50 hover:text-cyan-700" title={isFullscreen ? '退出全屏' : '地图全屏'} aria-label={isFullscreen ? '退出加氢地图全屏' : '加氢地图全屏'} aria-pressed={isFullscreen}>
|
||||
{isFullscreen ? <Minimize2 size={17} /> : <Maximize2 size={17} />}
|
||||
</button>
|
||||
@@ -180,8 +181,8 @@ export default function HydrogenHeatmapModule() {
|
||||
<HydrogenHeatmapFilters
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={meta?.startDate || DEFAULT_START}
|
||||
maxDate={meta?.endDate || DEFAULT_END}
|
||||
minDate={meta?.startDate || FALLBACK_RANGE.start}
|
||||
maxDate={meta?.endDate || FALLBACK_RANGE.end}
|
||||
query={query}
|
||||
payer={payer}
|
||||
metric={metric}
|
||||
|
||||
@@ -4,10 +4,11 @@ import AmapHeatmapCanvas from './AmapHeatmapCanvas';
|
||||
import HeatmapDetailPanel from './HeatmapDetailPanel';
|
||||
import HeatmapFilters from './HeatmapFilters';
|
||||
import { fetchAmapConfig, fetchHeatmapMeta, fetchHeatmapPoints, fetchNearbyVehicles } from './api';
|
||||
import { recentDayRange } from '../../shared/date-range';
|
||||
import type { AmapConfig, HeatmapMeta, HeatmapMetric, HeatmapResponse, NearbyResponse } from './types';
|
||||
|
||||
const DEFAULT_START = '2026-01-01';
|
||||
const DEFAULT_END = '2026-07-13';
|
||||
// 接口返回数据水位前的兜底范围:近 30 天滚动窗口(不再写死历史区间)。
|
||||
const FALLBACK_RANGE = recentDayRange(30);
|
||||
const numberFormat = new Intl.NumberFormat('zh-CN');
|
||||
|
||||
function Metric({ value, label }: { value: number; label: string }) {
|
||||
@@ -24,8 +25,8 @@ export default function VehicleHeatmapModule() {
|
||||
const [config, setConfig] = useState<AmapConfig | null>(null);
|
||||
const [data, setData] = useState<HeatmapResponse | null>(null);
|
||||
const [nearby, setNearby] = useState<NearbyResponse | null>(null);
|
||||
const [startDate, setStartDate] = useState(DEFAULT_START);
|
||||
const [endDate, setEndDate] = useState(DEFAULT_END);
|
||||
const [startDate, setStartDate] = useState(FALLBACK_RANGE.start);
|
||||
const [endDate, setEndDate] = useState(FALLBACK_RANGE.end);
|
||||
const [query, setQuery] = useState('');
|
||||
const [batchModel, setBatchModel] = useState('');
|
||||
const [metric, setMetric] = useState<HeatmapMetric>('locations');
|
||||
@@ -103,8 +104,8 @@ export default function VehicleHeatmapModule() {
|
||||
}, [batchModel, deferredQuery, endDate, startDate]);
|
||||
|
||||
const reset = () => {
|
||||
setStartDate(meta?.startDate || DEFAULT_START);
|
||||
setEndDate(meta?.endDate || DEFAULT_END);
|
||||
setStartDate(meta?.startDate || FALLBACK_RANGE.start);
|
||||
setEndDate(meta?.endDate || FALLBACK_RANGE.end);
|
||||
setQuery('');
|
||||
setBatchModel('');
|
||||
setMetric('locations');
|
||||
@@ -147,7 +148,7 @@ export default function VehicleHeatmapModule() {
|
||||
<Metric value={data?.dayCount ?? meta?.dayCount ?? 0} label="天" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="flex items-center gap-2 text-[12px] text-slate-500 max-xl:hidden"><Clock3 size={15} strokeWidth={1.7} />数据更新至 {meta?.endDate || DEFAULT_END}</p>
|
||||
<p className="flex items-center gap-2 text-[12px] text-slate-500 max-xl:hidden"><Clock3 size={15} strokeWidth={1.7} />数据更新至 {meta?.endDate || FALLBACK_RANGE.end}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleFullscreen}
|
||||
@@ -179,8 +180,8 @@ export default function VehicleHeatmapModule() {
|
||||
<HeatmapFilters
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={meta?.startDate || DEFAULT_START}
|
||||
maxDate={meta?.endDate || DEFAULT_END}
|
||||
minDate={meta?.startDate || FALLBACK_RANGE.start}
|
||||
maxDate={meta?.endDate || FALLBACK_RANGE.end}
|
||||
query={query}
|
||||
batchModel={batchModel}
|
||||
batchModels={meta?.batchModels || []}
|
||||
|
||||
@@ -11,6 +11,11 @@ import {
|
||||
serializeHydrogenStation,
|
||||
type HydrogenStationRecord,
|
||||
} from './hydrogen-heatmap-model.js';
|
||||
import { recentDayRange } from '../../shared/date-range.js';
|
||||
|
||||
// 默认区间是"近 30 天"滚动窗口,注入固定时钟让断言可复现。
|
||||
const NOW = new Date('2026-07-13T12:00:00');
|
||||
const FALLBACK = recentDayRange(30, NOW);
|
||||
|
||||
function station(overrides: Partial<HydrogenStationRecord> = {}): HydrogenStationRecord {
|
||||
return {
|
||||
@@ -28,10 +33,10 @@ function station(overrides: Partial<HydrogenStationRecord> = {}): HydrogenStatio
|
||||
};
|
||||
}
|
||||
|
||||
test('加氢热力图查询继续使用固定默认日期、去除搜索空白并限制枚举值', () => {
|
||||
assert.deepEqual(parseHydrogenHeatmapQuery({}), {
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-07-13',
|
||||
test('加氢热力图查询默认使用近 30 天滚动窗口、去除搜索空白并限制枚举值', () => {
|
||||
assert.deepEqual(parseHydrogenHeatmapQuery({}, NOW), {
|
||||
startDate: FALLBACK.start,
|
||||
endDate: FALLBACK.end,
|
||||
query: '',
|
||||
payer: 'all',
|
||||
metric: 'kg',
|
||||
@@ -42,9 +47,9 @@ test('加氢热力图查询继续使用固定默认日期、去除搜索空白
|
||||
query: ' 嘉兴 ',
|
||||
payer: 'customer',
|
||||
metric: 'vehicles',
|
||||
}), {
|
||||
}, NOW), {
|
||||
startDate: '2026-06-01',
|
||||
endDate: '2026-07-13',
|
||||
endDate: FALLBACK.end,
|
||||
query: '嘉兴',
|
||||
payer: 'customer',
|
||||
metric: 'vehicles',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { recentDayRange } from '../../shared/date-range.js';
|
||||
export type HydrogenHeatmapMetric = 'kg' | 'refuels' | 'vehicles';
|
||||
export type HydrogenPayer = 'all' | 'lingniu' | 'customer';
|
||||
|
||||
@@ -28,8 +29,8 @@ type NearbyQueryInput = {
|
||||
radiusKm?: string;
|
||||
};
|
||||
|
||||
export const HYDROGEN_HEATMAP_DEFAULT_START = '2026-01-01';
|
||||
export const HYDROGEN_HEATMAP_DEFAULT_END = '2026-07-13';
|
||||
/** 未显式传日期时的兜底窗口:近 N 天滚动,不再写死历史区间。 */
|
||||
export const HEATMAP_DEFAULT_WINDOW_DAYS = 30;
|
||||
|
||||
function isDate(value: string | undefined): value is string {
|
||||
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
|
||||
@@ -45,10 +46,11 @@ function parsePayer(value: string | undefined): HydrogenPayer {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
export function parseHydrogenHeatmapQuery(input: HydrogenHeatmapQueryInput) {
|
||||
export function parseHydrogenHeatmapQuery(input: HydrogenHeatmapQueryInput, now: Date = new Date()) {
|
||||
const fallback = recentDayRange(HEATMAP_DEFAULT_WINDOW_DAYS, now);
|
||||
return {
|
||||
startDate: isDate(input.startDate) ? input.startDate : HYDROGEN_HEATMAP_DEFAULT_START,
|
||||
endDate: isDate(input.endDate) ? input.endDate : HYDROGEN_HEATMAP_DEFAULT_END,
|
||||
startDate: isDate(input.startDate) ? input.startDate : fallback.start,
|
||||
endDate: isDate(input.endDate) ? input.endDate : fallback.end,
|
||||
query: (input.query || '').trim(),
|
||||
payer: parsePayer(input.payer),
|
||||
metric: parseMetric(input.metric),
|
||||
|
||||
@@ -3,11 +3,11 @@ import type { RowDataPacket } from 'mysql2';
|
||||
import pool from '../db/mysql.js';
|
||||
import type { AuthUser } from '../auth/types.js';
|
||||
import { canAccessEnergy } from '../auth/types.js';
|
||||
import { recentDayRange } from '../../shared/date-range.js';
|
||||
import {
|
||||
buildHydrogenPoints,
|
||||
filterHydrogenStationsByRadius,
|
||||
HYDROGEN_HEATMAP_DEFAULT_END,
|
||||
HYDROGEN_HEATMAP_DEFAULT_START,
|
||||
HEATMAP_DEFAULT_WINDOW_DAYS,
|
||||
parseHydrogenHeatmapQuery,
|
||||
parseNearbyQuery,
|
||||
rankHydrogenStations,
|
||||
@@ -127,9 +127,10 @@ router.get('/meta', async (c) => {
|
||||
const profile = profileRows[0][0] || {};
|
||||
const totalRefuelCount = Number(profile.total_refuel_count) || 0;
|
||||
const eligibleRefuelCount = Number(profile.eligible_refuel_count) || 0;
|
||||
const fallback = recentDayRange(HEATMAP_DEFAULT_WINDOW_DAYS);
|
||||
return c.json({
|
||||
startDate: profile.start_date || HYDROGEN_HEATMAP_DEFAULT_START,
|
||||
endDate: profile.end_date || HYDROGEN_HEATMAP_DEFAULT_END,
|
||||
startDate: profile.start_date || fallback.start,
|
||||
endDate: profile.end_date || fallback.end,
|
||||
totalRefuelCount,
|
||||
eligibleRefuelCount,
|
||||
excludedRefuelCount: Math.max(0, totalRefuelCount - eligibleRefuelCount),
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
vehicleGridPrecision,
|
||||
type VehicleHeatmapRecord,
|
||||
} from './vehicle-heatmap-model.js';
|
||||
import { recentDayRange } from '../../shared/date-range.js';
|
||||
|
||||
// 默认区间是"近 30 天"滚动窗口,注入固定时钟让断言可复现。
|
||||
const NOW = new Date('2026-07-13T12:00:00');
|
||||
const FALLBACK = recentDayRange(30, NOW);
|
||||
|
||||
function record(overrides: Partial<VehicleHeatmapRecord> = {}): VehicleHeatmapRecord {
|
||||
return {
|
||||
@@ -27,10 +32,10 @@ function record(overrides: Partial<VehicleHeatmapRecord> = {}): VehicleHeatmapRe
|
||||
};
|
||||
}
|
||||
|
||||
test('车辆热力图查询继续使用固定默认日期和原有字符串处理规则', () => {
|
||||
assert.deepEqual(parseVehicleHeatmapQuery({}), {
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-07-13',
|
||||
test('车辆热力图查询默认使用近 30 天滚动窗口,并保留原有字符串处理规则', () => {
|
||||
assert.deepEqual(parseVehicleHeatmapQuery({}, NOW), {
|
||||
startDate: FALLBACK.start,
|
||||
endDate: FALLBACK.end,
|
||||
query: '',
|
||||
batchModel: '',
|
||||
metric: 'locations',
|
||||
@@ -41,9 +46,9 @@ test('车辆热力图查询继续使用固定默认日期和原有字符串处
|
||||
query: ' 粤A ',
|
||||
batchModel: ' 190台冷链 ',
|
||||
metric: 'vehicles',
|
||||
}), {
|
||||
}, NOW), {
|
||||
startDate: '2026-06-01',
|
||||
endDate: '2026-07-13',
|
||||
endDate: FALLBACK.end,
|
||||
query: ' 粤A ',
|
||||
batchModel: '190台冷链',
|
||||
metric: 'vehicles',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { recentDayRange } from '../../shared/date-range.js';
|
||||
export type VehicleHeatmapMetric = 'locations' | 'vehicles';
|
||||
|
||||
export type VehicleHeatmapRecord = {
|
||||
@@ -33,17 +34,22 @@ type NearbyQueryInput = {
|
||||
radiusKm?: string;
|
||||
};
|
||||
|
||||
export const VEHICLE_HEATMAP_DEFAULT_START = '2026-01-01';
|
||||
export const VEHICLE_HEATMAP_DEFAULT_END = '2026-07-13';
|
||||
/**
|
||||
* 未显式传日期时的兜底窗口。
|
||||
* 此前写死为 2026-01-01 ~ 2026-07-13,会让默认查询永久指向一段历史数据;
|
||||
* 现改为"近 N 天"滚动窗口。
|
||||
*/
|
||||
export const HEATMAP_DEFAULT_WINDOW_DAYS = 30;
|
||||
|
||||
function isDate(value: string | undefined): value is string {
|
||||
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
|
||||
}
|
||||
|
||||
export function parseVehicleHeatmapQuery(input: VehicleHeatmapQueryInput) {
|
||||
export function parseVehicleHeatmapQuery(input: VehicleHeatmapQueryInput, now: Date = new Date()) {
|
||||
const fallback = recentDayRange(HEATMAP_DEFAULT_WINDOW_DAYS, now);
|
||||
return {
|
||||
startDate: isDate(input.startDate) ? input.startDate : VEHICLE_HEATMAP_DEFAULT_START,
|
||||
endDate: isDate(input.endDate) ? input.endDate : VEHICLE_HEATMAP_DEFAULT_END,
|
||||
startDate: isDate(input.startDate) ? input.startDate : fallback.start,
|
||||
endDate: isDate(input.endDate) ? input.endDate : fallback.end,
|
||||
query: input.query || '',
|
||||
batchModel: (input.batchModel || '').trim(),
|
||||
metric: input.metric === 'vehicles' ? 'vehicles' as const : 'locations' as const,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 自然日区间工具(按本地日历日,不使用 UTC)。
|
||||
* 前后端都要按"上海/本地自然日"取数,散落各处的实现容易在跨日与月末出错,
|
||||
* 因此收敛到 shared 这一层。
|
||||
*/
|
||||
|
||||
export interface DayRange {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
function formatYmd(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 近 N 天(含今天)的区间。
|
||||
*
|
||||
* 用于接口尚未返回数据水位(meta)时的兜底范围。此前代码把兜底写死成
|
||||
* `2026-01-01 ~ 2026-07-13`,会随着时间推移永久指向一段历史数据。
|
||||
*/
|
||||
export function recentDayRange(days = 30, now: Date = new Date()): DayRange {
|
||||
const span = Math.max(1, Math.floor(days));
|
||||
const end = new Date(now);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - (span - 1));
|
||||
return { start: formatYmd(start), end: formatYmd(end) };
|
||||
}
|
||||
Reference in New Issue
Block a user