refactor: align electric overview drilldown
This commit is contained in:
@@ -3,7 +3,7 @@ import { BatteryCharging, CalendarDays, ChevronRight, MapPin, Plug, TrendingUp,
|
|||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import TrendBadge from './TrendBadge';
|
import TrendBadge from './TrendBadge';
|
||||||
import { fetchElectricMonthly, fetchElectricOrders } from './api';
|
import { fetchElectricMonthly, fetchElectricOrders } from './api';
|
||||||
import type { CustomerType, DateQuickPick, ElectricChargeOrderResponse, ElectricMonthGroup } from './types';
|
import type { DateQuickPick, ElectricChargeOrderResponse, ElectricMonthGroup, ElectricVehicleScope } from './types';
|
||||||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||||||
import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
||||||
import {
|
import {
|
||||||
@@ -51,7 +51,7 @@ export default function ElectricDaily() {
|
|||||||
const [drillContext, setDrillContext] = useState<ElectricDrillContext>(() => (
|
const [drillContext, setDrillContext] = useState<ElectricDrillContext>(() => (
|
||||||
parseElectricDrillContext(window.location.search)
|
parseElectricDrillContext(window.location.search)
|
||||||
));
|
));
|
||||||
const [customer, setCustomer] = useState<CustomerType>(drillContext.vehicleScope);
|
const [customer, setCustomer] = useState<ElectricVehicleScope>(drillContext.vehicleScope);
|
||||||
const [pick, setPick] = useState<RangeMode>(() => (
|
const [pick, setPick] = useState<RangeMode>(() => (
|
||||||
drillContext.startDate && drillContext.endDate || drillContext.selectedDate ? 'custom' : 'last15'
|
drillContext.startDate && drillContext.endDate || drillContext.selectedDate ? 'custom' : 'last15'
|
||||||
));
|
));
|
||||||
@@ -156,7 +156,7 @@ export default function ElectricDaily() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCustomer = (next: CustomerType) => {
|
const updateCustomer = (next: ElectricVehicleScope) => {
|
||||||
setCustomer(next);
|
setCustomer(next);
|
||||||
commitDrillContext({
|
commitDrillContext({
|
||||||
...drillContext,
|
...drillContext,
|
||||||
@@ -201,6 +201,7 @@ export default function ElectricDaily() {
|
|||||||
<button
|
<button
|
||||||
key={opt.id}
|
key={opt.id}
|
||||||
onClick={() => applyQuickPick(opt.id)}
|
onClick={() => applyQuickPick(opt.id)}
|
||||||
|
aria-pressed={pick === opt.id}
|
||||||
className={`min-h-9 shrink-0 rounded-xl border px-3 text-[12px] font-black transition-colors ${
|
className={`min-h-9 shrink-0 rounded-xl border px-3 text-[12px] font-black transition-colors ${
|
||||||
pick === opt.id
|
pick === opt.id
|
||||||
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
||||||
@@ -212,6 +213,7 @@ export default function ElectricDaily() {
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={() => setPick('custom')}
|
onClick={() => setPick('custom')}
|
||||||
|
aria-pressed={pick === 'custom'}
|
||||||
className={`min-h-9 shrink-0 rounded-xl border px-3 text-[12px] font-black transition-colors ${
|
className={`min-h-9 shrink-0 rounded-xl border px-3 text-[12px] font-black transition-colors ${
|
||||||
pick === 'custom'
|
pick === 'custom'
|
||||||
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
||||||
@@ -226,6 +228,8 @@ export default function ElectricDaily() {
|
|||||||
<label className="min-w-0 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
|
<label className="min-w-0 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
|
||||||
<span className="block text-[10px] font-black text-slate-400">开始日期</span>
|
<span className="block text-[10px] font-black text-slate-400">开始日期</span>
|
||||||
<input
|
<input
|
||||||
|
id="electric-start-date"
|
||||||
|
name="electricStartDate"
|
||||||
type="date"
|
type="date"
|
||||||
value={dateRange.start}
|
value={dateRange.start}
|
||||||
onChange={e => updateDateRange('start', e.target.value)}
|
onChange={e => updateDateRange('start', e.target.value)}
|
||||||
@@ -236,6 +240,8 @@ export default function ElectricDaily() {
|
|||||||
<label className="min-w-0 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
|
<label className="min-w-0 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
|
||||||
<span className="block text-[10px] font-black text-slate-400">结束日期</span>
|
<span className="block text-[10px] font-black text-slate-400">结束日期</span>
|
||||||
<input
|
<input
|
||||||
|
id="electric-end-date"
|
||||||
|
name="electricEndDate"
|
||||||
type="date"
|
type="date"
|
||||||
value={dateRange.end}
|
value={dateRange.end}
|
||||||
onChange={e => updateDateRange('end', e.target.value)}
|
onChange={e => updateDateRange('end', e.target.value)}
|
||||||
@@ -245,17 +251,18 @@ export default function ElectricDaily() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 grid grid-cols-2 gap-1 rounded-xl bg-slate-100 p-1">
|
<div className="mt-2 grid grid-cols-3 gap-1 rounded-xl bg-slate-100 p-1">
|
||||||
{(['lingniu', 'external'] as const).map(c => (
|
{(['all', 'lingniu', 'external'] as const).map(c => (
|
||||||
<button
|
<button
|
||||||
key={c}
|
key={c}
|
||||||
onClick={() => updateCustomer(c)}
|
onClick={() => updateCustomer(c)}
|
||||||
|
aria-pressed={customer === c}
|
||||||
className={`flex min-h-9 items-center justify-center gap-1.5 rounded-lg text-[12px] font-black transition-all ${
|
className={`flex min-h-9 items-center justify-center gap-1.5 rounded-lg text-[12px] font-black transition-all ${
|
||||||
customer === c ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
customer === c ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Truck size={14} />
|
<Truck size={14} />
|
||||||
{c === 'external' ? '外部车辆' : '羚牛车辆'}
|
{c === 'all' ? '全部车辆' : c === 'external' ? '外部车辆' : '羚牛车辆'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip, Refere
|
|||||||
import { fetchElectricOverview, type ElectricOverviewResponse } from './api';
|
import { fetchElectricOverview, type ElectricOverviewResponse } from './api';
|
||||||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||||||
import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
||||||
|
import { buildElectricDrillUrl } from './electric-drill-context';
|
||||||
|
|
||||||
function fmtYuan(yuan: number) {
|
function fmtYuan(yuan: number) {
|
||||||
return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
|
return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
|
||||||
@@ -44,6 +45,19 @@ export default function ElectricOverview() {
|
|||||||
const peakDay = trendData.reduce<typeof trendData[number] | null>((best, item) => (!best || item.kwh > best.kwh ? item : best), null);
|
const peakDay = trendData.reduce<typeof trendData[number] | null>((best, item) => (!best || item.kwh > best.kwh ? item : best), null);
|
||||||
const avgPrice = k.totalKwh > 0 ? k.totalFee / k.totalKwh : 0;
|
const avgPrice = k.totalKwh > 0 ? k.totalFee / k.totalKwh : 0;
|
||||||
const monthPrice = k.monthKwh > 0 ? k.monthFee / k.monthKwh : 0;
|
const monthPrice = k.monthKwh > 0 ? k.monthFee / k.monthKwh : 0;
|
||||||
|
const openDay = (date: string) => {
|
||||||
|
const url = buildElectricDrillUrl(
|
||||||
|
{ pathname: window.location.pathname, search: window.location.search, hash: '#electric' },
|
||||||
|
{
|
||||||
|
vehicleScope: 'all',
|
||||||
|
startDate: date,
|
||||||
|
endDate: date,
|
||||||
|
selectedDate: date,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
window.history.pushState(null, '', url);
|
||||||
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -109,8 +123,22 @@ export default function ElectricOverview() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Bar dataKey="fee" radius={[4, 4, 0, 0]}>
|
<Bar dataKey="fee" radius={[4, 4, 0, 0]}>
|
||||||
{trendData.map((_, i) => (
|
{trendData.map(item => (
|
||||||
<Cell key={i} fill="url(#electricBarGrad)" />
|
<Cell
|
||||||
|
key={item.date}
|
||||||
|
fill="url(#electricBarGrad)"
|
||||||
|
className="cursor-pointer outline-none focus:stroke-blue-700 focus:stroke-2"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`${item.date},充电费用 ${fmtYuan(item.fee)},查看全部车辆订单`}
|
||||||
|
onClick={() => openDay(item.date)}
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
openDay(item.date);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Bar>
|
</Bar>
|
||||||
<defs>
|
<defs>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type {
|
|||||||
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
|
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
|
||||||
ElectricChargeOrderResponse,
|
ElectricChargeOrderResponse,
|
||||||
EtcOverviewResponse,
|
EtcOverviewResponse,
|
||||||
CustomerType, DateQuickPick,
|
CustomerType, DateQuickPick, ElectricVehicleScope,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const BASE = '/api/energy';
|
const BASE = '/api/energy';
|
||||||
@@ -56,7 +56,7 @@ export function fetchElectricOverview(): Promise<ElectricOverviewResponse> {
|
|||||||
return fetchJson<ElectricOverviewResponse>(`${BASE}/electric/overview`);
|
return fetchJson<ElectricOverviewResponse>(`${BASE}/electric/overview`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchElectricMonthly(customer: CustomerType, query: HydrogenDailyQuery = { range: 'last15' }): Promise<ElectricMonthGroup[]> {
|
export function fetchElectricMonthly(customer: ElectricVehicleScope, query: HydrogenDailyQuery = { range: 'last15' }): Promise<ElectricMonthGroup[]> {
|
||||||
const q = new URLSearchParams({ customer });
|
const q = new URLSearchParams({ customer });
|
||||||
if (query.range) q.set('range', query.range);
|
if (query.range) q.set('range', query.range);
|
||||||
if (query.startDate) q.set('startDate', query.startDate);
|
if (query.startDate) q.set('startDate', query.startDate);
|
||||||
@@ -64,7 +64,7 @@ export function fetchElectricMonthly(customer: CustomerType, query: HydrogenDail
|
|||||||
return fetchJson<ElectricMonthGroup[]>(`${BASE}/electric/monthly?${q.toString()}`);
|
return fetchJson<ElectricMonthGroup[]>(`${BASE}/electric/monthly?${q.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchElectricOrders(date: string, customer: CustomerType): Promise<ElectricChargeOrderResponse> {
|
export function fetchElectricOrders(date: string, customer: ElectricVehicleScope): Promise<ElectricChargeOrderResponse> {
|
||||||
const q = new URLSearchParams({ date, customer });
|
const q = new URLSearchParams({ date, customer });
|
||||||
return fetchJson<ElectricChargeOrderResponse>(`${BASE}/electric/orders?${q.toString()}`);
|
return fetchJson<ElectricChargeOrderResponse>(`${BASE}/electric/orders?${q.toString()}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ test('drops invalid electric dates and defaults scope', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('preserves the all-vehicle scope for overview drilldown', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
parseElectricDrillContext('?electricScope=all&electricDate=2026-07-28'),
|
||||||
|
{ vehicleScope: 'all', selectedDate: '2026-07-28' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('builds namespaced electric drill URL and preserves unrelated filters', () => {
|
test('builds namespaced electric drill URL and preserves unrelated filters', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
buildElectricDrillUrl(
|
buildElectricDrillUrl(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { CustomerType } from './types';
|
import type { ElectricVehicleScope } from './types';
|
||||||
|
|
||||||
export interface ElectricDrillContext {
|
export interface ElectricDrillContext {
|
||||||
vehicleScope: CustomerType;
|
vehicleScope: ElectricVehicleScope;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
selectedDate?: string;
|
selectedDate?: string;
|
||||||
@@ -28,8 +28,9 @@ function validDate(value: string | null): string | undefined {
|
|||||||
|
|
||||||
export function parseElectricDrillContext(search: string): ElectricDrillContext {
|
export function parseElectricDrillContext(search: string): ElectricDrillContext {
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
|
const scope = params.get('electricScope');
|
||||||
const context: ElectricDrillContext = {
|
const context: ElectricDrillContext = {
|
||||||
vehicleScope: params.get('electricScope') === 'external' ? 'external' : 'lingniu',
|
vehicleScope: scope === 'external' || scope === 'all' ? scope : 'lingniu',
|
||||||
};
|
};
|
||||||
const startDate = validDate(params.get('electricStart'));
|
const startDate = validDate(params.get('electricStart'));
|
||||||
const endDate = validDate(params.get('electricEnd'));
|
const endDate = validDate(params.get('electricEnd'));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type CustomerType = 'external' | 'lingniu';
|
export type CustomerType = 'external' | 'lingniu';
|
||||||
|
export type ElectricVehicleScope = CustomerType | 'all';
|
||||||
export type DateQuickPick = 'thisWeek' | 'thisMonth' | 'last15';
|
export type DateQuickPick = 'thisWeek' | 'thisMonth' | 'last15';
|
||||||
|
|
||||||
export interface HydrogenKpi {
|
export interface HydrogenKpi {
|
||||||
@@ -117,7 +118,7 @@ export interface ElectricChargeOrder {
|
|||||||
|
|
||||||
export interface ElectricChargeOrderResponse {
|
export interface ElectricChargeOrderResponse {
|
||||||
date: string;
|
date: string;
|
||||||
vehicleScope: CustomerType;
|
vehicleScope: ElectricVehicleScope;
|
||||||
recordCount: number;
|
recordCount: number;
|
||||||
totalKwh: number;
|
totalKwh: number;
|
||||||
totalFee: number;
|
totalFee: number;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import hydrogenPool from '../../hydrogen-db.js';
|
|||||||
import { cached } from './cache.js';
|
import { cached } from './cache.js';
|
||||||
import {
|
import {
|
||||||
parseHydrogenCustomerKind,
|
parseHydrogenCustomerKind,
|
||||||
|
parseElectricVehicleScope,
|
||||||
parseHydrogenCustomerName,
|
parseHydrogenCustomerName,
|
||||||
parseHydrogenStationId,
|
parseHydrogenStationId,
|
||||||
parseEnergyDate,
|
parseEnergyDate,
|
||||||
@@ -602,14 +603,13 @@ app.get('/electric/orders', async (c) => {
|
|||||||
if (date === null) return c.json({ error: 'date 必须是有效的 YYYY-MM-DD 日期' }, 400);
|
if (date === null) return c.json({ error: 'date 必须是有效的 YYYY-MM-DD 日期' }, 400);
|
||||||
|
|
||||||
const customerParam = c.req.query('customer');
|
const customerParam = c.req.query('customer');
|
||||||
if (customerParam !== undefined && customerParam !== 'lingniu' && customerParam !== 'external') {
|
const customer = parseElectricVehicleScope(customerParam);
|
||||||
return c.json({ error: 'customer 必须是 lingniu 或 external' }, 400);
|
if (customer === null) return c.json({ error: 'customer 必须是 all、lingniu 或 external' }, 400);
|
||||||
}
|
const vehicleKind = customer === 'all' ? null : customer === 'lingniu' ? 'internal' : 'external';
|
||||||
const customer = customerParam === 'external' ? 'external' : 'lingniu';
|
const kindClause = vehicleKind === null ? '' : 'AND vehicle_kind = ?';
|
||||||
const vehicleKind = customer === 'lingniu' ? 'internal' : 'external';
|
|
||||||
|
|
||||||
const data = await cached(`electric/orders?date=${date}&customer=${customer}`, async () => {
|
const data = await cached(`electric/orders?date=${date}&customer=${customer}`, async () => {
|
||||||
const params = [date, date, vehicleKind];
|
const params = vehicleKind === null ? [date, date] : [date, date, vehicleKind];
|
||||||
const [[summaryRows], [detailRows]] = await Promise.all([
|
const [[summaryRows], [detailRows]] = await Promise.all([
|
||||||
pool.query<RowDataPacket[]>(
|
pool.query<RowDataPacket[]>(
|
||||||
`SELECT COUNT(*) AS recordCount,
|
`SELECT COUNT(*) AS recordCount,
|
||||||
@@ -617,7 +617,7 @@ app.get('/electric/orders', async (c) => {
|
|||||||
SUM(fee) AS totalFee
|
SUM(fee) AS totalFee
|
||||||
FROM bi_ele_charge_record
|
FROM bi_ele_charge_record
|
||||||
WHERE start_time >= ? AND start_time < DATE_ADD(?, INTERVAL 1 DAY)
|
WHERE start_time >= ? AND start_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||||
AND vehicle_kind = ?`,
|
${kindClause}`,
|
||||||
params,
|
params,
|
||||||
),
|
),
|
||||||
pool.query<RowDataPacket[]>(
|
pool.query<RowDataPacket[]>(
|
||||||
@@ -634,7 +634,7 @@ app.get('/electric/orders', async (c) => {
|
|||||||
fee AS totalFee
|
fee AS totalFee
|
||||||
FROM bi_ele_charge_record
|
FROM bi_ele_charge_record
|
||||||
WHERE start_time >= ? AND start_time < DATE_ADD(?, INTERVAL 1 DAY)
|
WHERE start_time >= ? AND start_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||||
AND vehicle_kind = ?
|
${kindClause}
|
||||||
ORDER BY start_time DESC, id DESC
|
ORDER BY start_time DESC, id DESC
|
||||||
LIMIT 501`,
|
LIMIT 501`,
|
||||||
params,
|
params,
|
||||||
@@ -674,7 +674,8 @@ app.get('/electric/orders', async (c) => {
|
|||||||
// 缺失日期补零
|
// 缺失日期补零
|
||||||
// =========================================================
|
// =========================================================
|
||||||
app.get('/electric/monthly', async (c) => {
|
app.get('/electric/monthly', async (c) => {
|
||||||
const customer = parseHydrogenCustomerKind(c.req.query('customer'));
|
const customer = parseElectricVehicleScope(c.req.query('customer'));
|
||||||
|
if (customer === null) return c.json({ error: 'customer 必须是 all、lingniu 或 external' }, 400);
|
||||||
const range = (c.req.query('range') || 'last15') as Range;
|
const range = (c.req.query('range') || 'last15') as Range;
|
||||||
const dateRange = resolveDateRange(range, c.req.query('startDate'), c.req.query('endDate'));
|
const dateRange = resolveDateRange(range, c.req.query('startDate'), c.req.query('endDate'));
|
||||||
const force = c.req.query('force') === '1';
|
const force = c.req.query('force') === '1';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import test from 'node:test';
|
|||||||
import app from './index.js';
|
import app from './index.js';
|
||||||
import {
|
import {
|
||||||
parseHydrogenCustomerKind,
|
parseHydrogenCustomerKind,
|
||||||
|
parseElectricVehicleScope,
|
||||||
parseHydrogenCustomerName,
|
parseHydrogenCustomerName,
|
||||||
parseHydrogenStationId,
|
parseHydrogenStationId,
|
||||||
parseEnergyDate,
|
parseEnergyDate,
|
||||||
@@ -23,6 +24,14 @@ test('defaults unknown customer scopes to lingniu', () => {
|
|||||||
assert.equal(parseHydrogenCustomerKind(undefined), 'lingniu');
|
assert.equal(parseHydrogenCustomerKind(undefined), 'lingniu');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parses explicit electric vehicle scopes', () => {
|
||||||
|
assert.equal(parseElectricVehicleScope('all'), 'all');
|
||||||
|
assert.equal(parseElectricVehicleScope('lingniu'), 'lingniu');
|
||||||
|
assert.equal(parseElectricVehicleScope('external'), 'external');
|
||||||
|
assert.equal(parseElectricVehicleScope(undefined), 'lingniu');
|
||||||
|
assert.equal(parseElectricVehicleScope('unknown'), null);
|
||||||
|
});
|
||||||
|
|
||||||
test('normalizes bounded customer names for exact matching', () => {
|
test('normalizes bounded customer names for exact matching', () => {
|
||||||
assert.equal(parseHydrogenCustomerName(' 武汉客户 '), '武汉客户');
|
assert.equal(parseHydrogenCustomerName(' 武汉客户 '), '武汉客户');
|
||||||
assert.equal(parseHydrogenCustomerName('未指定客户'), '未指定客户');
|
assert.equal(parseHydrogenCustomerName('未指定客户'), '未指定客户');
|
||||||
@@ -48,15 +57,15 @@ test('parses only valid energy calendar dates', () => {
|
|||||||
assert.equal(parseEnergyDate(undefined), null);
|
assert.equal(parseEnergyDate(undefined), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects invalid electric order drill filters before querying data', async () => {
|
test('rejects invalid electric drill filters before querying data', async () => {
|
||||||
const invalidDate = await app.request('/electric/orders?date=2026-02-31&customer=lingniu');
|
const invalidDate = await app.request('/electric/orders?date=2026-02-31&customer=lingniu');
|
||||||
assert.equal(invalidDate.status, 400);
|
assert.equal(invalidDate.status, 400);
|
||||||
assert.deepEqual(await invalidDate.json(), { error: 'date 必须是有效的 YYYY-MM-DD 日期' });
|
assert.deepEqual(await invalidDate.json(), { error: 'date 必须是有效的 YYYY-MM-DD 日期' });
|
||||||
|
|
||||||
const invalidScope = await app.request('/electric/orders?date=2026-07-28&customer=all');
|
const invalidScope = await app.request('/electric/orders?date=2026-07-28&customer=unknown');
|
||||||
assert.equal(invalidScope.status, 400);
|
assert.equal(invalidScope.status, 400);
|
||||||
assert.deepEqual(await invalidScope.json(), { error: 'customer 必须是 lingniu 或 external' });
|
assert.deepEqual(await invalidScope.json(), { error: 'customer 必须是 all、lingniu 或 external' });
|
||||||
|
|
||||||
const unknownScope = await app.request('/electric/orders?date=2026-07-28&customer=unknown');
|
const invalidMonthlyScope = await app.request('/electric/monthly?customer=unknown');
|
||||||
assert.equal(unknownScope.status, 400);
|
assert.equal(invalidMonthlyScope.status, 400);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
export type HydrogenCustomerKind = 'external' | 'lingniu' | 'all';
|
export type HydrogenCustomerKind = 'external' | 'lingniu' | 'all';
|
||||||
|
export type ElectricVehicleScope = 'external' | 'lingniu' | 'all';
|
||||||
|
|
||||||
export function parseHydrogenCustomerKind(value: string | undefined): HydrogenCustomerKind {
|
export function parseHydrogenCustomerKind(value: string | undefined): HydrogenCustomerKind {
|
||||||
if (value === 'external' || value === 'all') return value;
|
if (value === 'external' || value === 'all') return value;
|
||||||
return 'lingniu';
|
return 'lingniu';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseElectricVehicleScope(value: string | undefined): ElectricVehicleScope | null {
|
||||||
|
if (value === undefined) return 'lingniu';
|
||||||
|
if (value === 'external' || value === 'lingniu' || value === 'all') return value;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseHydrogenStationId(value: string | undefined): number | null {
|
export function parseHydrogenStationId(value: string | undefined): number | null {
|
||||||
if (value == null || !/^\d+$/.test(value)) return null;
|
if (value == null || !/^\d+$/.test(value)) return null;
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
|
|||||||
Reference in New Issue
Block a user