diff --git a/src/components/ui/surface.tsx b/src/components/ui/surface.tsx index 54ea15b..4f2e240 100644 --- a/src/components/ui/surface.tsx +++ b/src/components/ui/surface.tsx @@ -168,19 +168,19 @@ export function MetricTile({ return (
-
-
{label}
-
- {value} - {unit ? {unit} : null} -
-
+
{label}
{Icon ? ( ) : null}
+
+ + {value} + + {unit ? {unit} : null} +
{helper ?
{helper}
: null}
); diff --git a/src/index.css b/src/index.css index 187fe69..e19c174 100644 --- a/src/index.css +++ b/src/index.css @@ -65,3 +65,8 @@ body { radial-gradient(circle at 90% 12%, rgba(20, 184, 166, 0.08), transparent 26%), linear-gradient(180deg, #f8fbff 0%, var(--app-bg) 42%, #f7f9fc 100%); } + +.asset-date-input::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: 0; +} diff --git a/src/modules/assets/AssetsModule.tsx b/src/modules/assets/AssetsModule.tsx index 28f7fe7..e21f25e 100644 --- a/src/modules/assets/AssetsModule.tsx +++ b/src/modules/assets/AssetsModule.tsx @@ -950,7 +950,6 @@ export default function AssetsModule() { 交还车统计 {flowLoading && } -
默认上周六-本周五 · 提交时间口径
-
- - +
+
+ +
+ +
diff --git a/src/modules/energy/ElectricDaily.tsx b/src/modules/energy/ElectricDaily.tsx index 9dd1918..93b0725 100644 --- a/src/modules/energy/ElectricDaily.tsx +++ b/src/modules/energy/ElectricDaily.tsx @@ -1,11 +1,11 @@ import { useEffect, useMemo, useState } from 'react'; -import { BatteryCharging, CalendarDays, ChevronRight, Plug, TrendingUp, Wallet } from 'lucide-react'; +import { BatteryCharging, CalendarDays, ChevronRight, Plug, TrendingUp, Truck, Wallet } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import TrendBadge from './TrendBadge'; import { fetchElectricMonthly } from './api'; import type { CustomerType, DateQuickPick, ElectricMonthGroup } from './types'; import RotatingFooterHint from '../../components/RotatingFooterHint'; -import { EmptyState, ErrorState, LoadingState, MetricTile } from '../../components/ui/surface'; +import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface'; const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [ { id: 'thisWeek', label: '本周' }, @@ -13,26 +13,61 @@ const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [ { id: 'last15', label: '近 15 天' }, ]; +type RangeMode = DateQuickPick | 'custom'; + +function fmtYmd(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +function addDays(d: Date, days: number): Date { + const next = new Date(d); + next.setDate(next.getDate() + days); + return next; +} + +function getQuickRange(pick: DateQuickPick): { start: string; end: string } { + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (pick === 'thisWeek') { + const day = today.getDay() || 7; + return { start: fmtYmd(addDays(today, -(day - 1))), end: fmtYmd(today) }; + } + if (pick === 'thisMonth') { + return { start: fmtYmd(new Date(today.getFullYear(), today.getMonth(), 1)), end: fmtYmd(today) }; + } + return { start: fmtYmd(addDays(today, -14)), end: fmtYmd(today) }; +} + +function normalizeRange(start: string, end: string): { start: string; end: string } { + return start <= end ? { start, end } : { start: end, end: start }; +} + export default function ElectricDaily() { const [customer, setCustomer] = useState('lingniu'); - const [pick, setPick] = useState('last15'); + const [pick, setPick] = useState('last15'); + const [dateRange, setDateRange] = useState(() => getQuickRange('last15')); const [months, setMonths] = useState(null); const [openMonths, setOpenMonths] = useState>(new Set()); const [error, setError] = useState(null); + const effectiveRange = useMemo(() => normalizeRange(dateRange.start, dateRange.end), [dateRange.start, dateRange.end]); + useEffect(() => { let cancelled = false; setError(null); - fetchElectricMonthly(customer, pick) + const query = pick === 'custom' + ? { startDate: effectiveRange.start, endDate: effectiveRange.end } + : { range: pick }; + fetchElectricMonthly(customer, query) .then(m => { if (cancelled) return; setMonths(m); // 默认展开最新一个月 - if (m.length > 0) setOpenMonths(prev => prev.size > 0 ? prev : new Set([m[0].month])); + if (m.length > 0) setOpenMonths(new Set([m[0].month])); }) .catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; - }, [customer, pick]); + }, [customer, pick, effectiveRange.start, effectiveRange.end]); const toggleMonth = (m: string) => setOpenMonths(prev => { const next = new Set(prev); @@ -46,14 +81,94 @@ export default function ElectricDaily() { const abnormalDays = useMemo(() => (months ?? []).reduce((sum, m) => sum + m.rows.filter(r => Math.abs(r.chainPct) >= 0.3).length, 0), [months]); const avgKwh = activeDays > 0 ? totalKwh / activeDays : 0; const avgPrice = totalKwh > 0 ? totalFee / totalKwh : 0; - const scopeLabel = QUICK_PICK_OPTIONS.find(item => item.id === pick)?.label ?? '当前时段'; + const scopeLabel = pick === 'custom' + ? '自定义区间' + : QUICK_PICK_OPTIONS.find(item => item.id === pick)?.label ?? '当前时段'; + const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`; const hasFeeDetail = totalFee > 0; const showExternalEmpty = customer === 'external' && months !== null && totalKwh === 0; + const applyQuickPick = (nextPick: DateQuickPick) => { + setPick(nextPick); + setDateRange(getQuickRange(nextPick)); + }; + + const updateDateRange = (field: 'start' | 'end', value: string) => { + if (!value) return; + setPick('custom'); + setDateRange(prev => ({ ...prev, [field]: value })); + }; + return (
+ +
+ {QUICK_PICK_OPTIONS.map(opt => ( + + ))} + +
+ +
+ + +
+ +
+ {(['lingniu', 'external'] as const).map(c => ( + + ))} +
+
+
- + 0 ? 'rose' : 'slate'} />
- {/* 日期速选 */} -
- {QUICK_PICK_OPTIONS.map(opt => ( - - ))} -
- - {/* 客户类型 */} -
- {(['lingniu', 'external'] as const).map(c => ( - - ))} -
- {/* 外部车辆 数据未就绪 */} {showExternalEmpty && ( = [ { id: 'last15', label: '近 15 天' }, ]; +type RangeMode = DateQuickPick | 'custom'; + +function fmtYmd(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +function addDays(d: Date, days: number): Date { + const next = new Date(d); + next.setDate(next.getDate() + days); + return next; +} + +function getQuickRange(pick: DateQuickPick): { start: string; end: string } { + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (pick === 'thisWeek') { + const day = today.getDay() || 7; + return { start: fmtYmd(addDays(today, -(day - 1))), end: fmtYmd(today) }; + } + if (pick === 'thisMonth') { + return { start: fmtYmd(new Date(today.getFullYear(), today.getMonth(), 1)), end: fmtYmd(today) }; + } + return { start: fmtYmd(addDays(today, -14)), end: fmtYmd(today) }; +} + +function normalizeRange(start: string, end: string): { start: string; end: string } { + return start <= end ? { start, end } : { start: end, end: start }; +} + export default function HydrogenDaily() { - const [pick, setPick] = useState('last15'); + const [pick, setPick] = useState('last15'); + const [dateRange, setDateRange] = useState(() => getQuickRange('last15')); const [customer, setCustomer] = useState('lingniu'); const [expanded, setExpanded] = useState>(new Set()); const [rows, setRows] = useState(null); const [error, setError] = useState(null); + const effectiveRange = useMemo(() => normalizeRange(dateRange.start, dateRange.end), [dateRange.start, dateRange.end]); + useEffect(() => { let cancelled = false; setError(null); - fetchHydrogenDaily(pick, customer) + const query = pick === 'custom' + ? { startDate: effectiveRange.start, endDate: effectiveRange.end } + : { range: pick }; + fetchHydrogenDaily(query, customer) .then(r => { if (!cancelled) setRows(r); }) .catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; - }, [pick, customer]); + }, [pick, customer, effectiveRange.start, effectiveRange.end]); // 柱图:按日期升序,用于"从左到右时间流" const trendData = useMemo(() => (rows ? [...rows].sort((a, b) => a.date.localeCompare(b.date)) : []), [rows]); @@ -40,7 +75,10 @@ export default function HydrogenDaily() { return names.size; }, [rows]); const avgKg = activeDays > 0 ? totalKg / activeDays : 0; - const scopeLabel = QUICK_PICK_OPTIONS.find(item => item.id === pick)?.label ?? '当前时段'; + const scopeLabel = pick === 'custom' + ? '自定义区间' + : QUICK_PICK_OPTIONS.find(item => item.id === pick)?.label ?? '当前时段'; + const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`; const peakDay = trendData.reduce((best, item) => (!best || item.totalKg > best.totalKg ? item : best), null); const lowDay = trendData .filter(item => item.totalKg > 0) @@ -53,47 +91,92 @@ export default function HydrogenDaily() { return next; }); + const applyQuickPick = (nextPick: DateQuickPick) => { + setPick(nextPick); + setDateRange(getQuickRange(nextPick)); + }; + + const updateDateRange = (field: 'start' | 'end', value: string) => { + if (!value) return; + setPick('custom'); + setDateRange(prev => ({ ...prev, [field]: value })); + }; + return (
+ +
+ {QUICK_PICK_OPTIONS.map(opt => ( + + ))} + +
+ +
+ + +
+ +
+ {(['lingniu', 'external'] as const).map(c => ( + + ))} +
+
+
- +
- {/* 日期速选 */} -
- {QUICK_PICK_OPTIONS.map(opt => ( - - ))} -
- - {/* 客户类型 segmented */} -
- {(['lingniu', 'external'] as const).map(c => ( - - ))} -
- {/* 外部车辆:新系统数据还没准备好 */} {customer === 'external' && rows !== null && totalKg === 0 && ( {/* 顶部说明条 + 年份切换 + 刷新按钮 */}
- {lastRefreshAt ? `更新于 ${formatRelative(lastRefreshAt)}` : '数据自 2025-01-01 起'} + {lastRefreshAt ? `更新于 ${formatRefreshTime(lastRefreshAt)}` : '数据自 2025-01-01 起'}
{availableYears.map(y => { @@ -620,6 +620,18 @@ function formatRelative(ts: number): string { return new Date(ts).toLocaleString('zh-CN', { hour12: false }); } +function formatRefreshTime(ts: number): string { + const exactTime = new Date(ts).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + return `${formatRelative(ts)} · ${exactTime.replace(/\//g, '-')}`; +} + function HydrogenOverviewSkeleton() { return (
diff --git a/src/modules/energy/api.ts b/src/modules/energy/api.ts index 2558882..3885ff8 100644 --- a/src/modules/energy/api.ts +++ b/src/modules/energy/api.ts @@ -27,8 +27,17 @@ export function fetchHydrogenOverview(year?: number, force = false): Promise(`${BASE}/hydrogen/overview${q ? `?${q}` : ''}`); } -export function fetchHydrogenDaily(range: DateQuickPick, customer: CustomerType): Promise { - const q = new URLSearchParams({ range, customer }); +export interface HydrogenDailyQuery { + range?: DateQuickPick; + startDate?: string; + endDate?: string; +} + +export function fetchHydrogenDaily(query: HydrogenDailyQuery, customer: CustomerType): Promise { + const q = new URLSearchParams({ customer }); + if (query.range) q.set('range', query.range); + if (query.startDate) q.set('startDate', query.startDate); + if (query.endDate) q.set('endDate', query.endDate); return fetchJson(`${BASE}/hydrogen/daily?${q.toString()}`); } @@ -41,7 +50,10 @@ export function fetchElectricOverview(): Promise { return fetchJson(`${BASE}/electric/overview`); } -export function fetchElectricMonthly(customer: CustomerType, range: DateQuickPick = 'last15'): Promise { - const q = new URLSearchParams({ customer, range }); +export function fetchElectricMonthly(customer: CustomerType, query: HydrogenDailyQuery = { range: 'last15' }): Promise { + const q = new URLSearchParams({ customer }); + if (query.range) q.set('range', query.range); + if (query.startDate) q.set('startDate', query.startDate); + if (query.endDate) q.set('endDate', query.endDate); return fetchJson(`${BASE}/electric/monthly?${q.toString()}`); } diff --git a/src/modules/mileage/DailyReportView.tsx b/src/modules/mileage/DailyReportView.tsx index d160ba5..ea641b2 100644 --- a/src/modules/mileage/DailyReportView.tsx +++ b/src/modules/mileage/DailyReportView.tsx @@ -1,209 +1,17 @@ -import { useEffect, useMemo, useState } from 'react'; -import { AlertTriangle, ArrowDownRight, BarChart3, CheckCircle2, Database, Route, Target, Truck } from 'lucide-react'; -import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface'; -import { fetchDailyReport, type DailyReportData, type DailyReportVehicle } from './api'; - -function fmt(value: number, digits = 0) { - return value.toLocaleString('zh-CN', { maximumFractionDigits: digits }); -} - -function fmtKm(value: number, digits = 1) { - if (Math.abs(value) >= 10000) return `${(value / 10000).toFixed(digits)}万`; - return fmt(value, digits); -} +import { FileText } from 'lucide-react'; +import { SurfaceCard } from '../../components/ui/surface'; export default function DailyReportView() { - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let cancelled = false; - setLoading(true); - setError(null); - fetchDailyReport() - .then(result => { if (!cancelled) setData(result); }) - .catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }) - .finally(() => { if (!cancelled) setLoading(false); }); - return () => { cancelled = true; }; - }, []); - - const totals = useMemo(() => { - const models = data?.models ?? []; - return models.reduce( - (acc, item) => ({ - count: acc.count + item.count, - today: acc.today + item.today, - total: acc.total + item.total, - active: acc.active + item.active, - zero: acc.zero + item.zero, - dailyNeed: acc.dailyNeed + item.dailyNeed, - }), - { count: 0, today: 0, total: 0, active: 0, zero: 0, dailyNeed: 0 }, - ); - }, [data]); - - if (loading) return ; - if (error) return ; - if (!data) return ; - - const activeRate = totals.count > 0 ? (totals.active / totals.count) * 100 : 0; - const dailyGap = totals.today - totals.dailyNeed; - const maxTrend = Math.max(1, ...data.trend.map(item => item.value)); - const maxModel = Math.max(1, ...data.models.map(item => item.today)); - return ( -
- -
-
-
-
数据截止 {data.reportDate} 23:59 · 数据库口径
- - - DB LIVE - -
-

车辆里程每日汇报

-

- 基于里程考核目标、车辆日里程视图和车辆归属信息实时聚合,和统计报表/实时监控保持同一数据库口径。 -

-
-
-
-
{fmt(totals.count)}
-
车辆 台
-
-
-
{activeRate.toFixed(1)}%
-
有里程
-
-
-
{totals.zero}
-
零里程
-
-
+ +
+
+ +
+
每日汇报接入中
+
+ 日报数据口径正在整理,完成后将接入数据库统计与导出能力。
- - -
- - - - = 0 ? '+' : '-'}${fmtKm(Math.abs(dailyGap))}`} - unit="km" - helper={dailyGap >= 0 ? '今日高于日需目标' : '今日低于日需目标'} - tone={dailyGap >= 0 ? 'emerald' : 'rose'} - /> -
- -
- -
- {data.trend.map(item => ( -
-
-
-
-
{item.date}
-
- ))} -
- - - -
- {data.models.map(item => ( -
-
-
-
{item.name}
-
{item.active}/{item.count} 有里程 · 零里程 {item.zero}
-
-
{fmt(item.today, 1)} km
-
-
-
-
-
- 年度完成 {item.completion.toFixed(1)}% - 日需 {fmt(item.dailyNeed, 1)} km -
-
- ))} -
- -
- -
- - -
- -
- -
- -
{data.qualifiedCount} 台已达当前年度标准
-
{data.halfQualifiedCount} 台达到 50% 里程线
-
-
- -
- -
全量均值 {fmt(totals.count > 0 ? totals.today / totals.count : 0, 1)} km/台
-
有里程车辆均值 {fmt(totals.active > 0 ? totals.today / totals.active : 0, 1)} km/台
-
-
- -
- -
缺口 {fmtKm(Math.abs(dailyGap))} km
-
建议关注零里程及低完成率车辆
-
-
-
-
- ); -} - -function ReportList({ - title, - subtitle, - rows, - mode, -}: { - title: string; - subtitle: string; - rows: DailyReportVehicle[]; - mode: 'top' | 'risk'; -}) { - return ( - -
- {rows.length === 0 ? ( -
暂无匹配车辆
- ) : rows.map(item => ( -
-
-
- {item.plate} - {item.model} - {item.status} -
-
{item.customer}
-
-
- {mode === 'top' ? `${fmt(item.today ?? 0, 1)} km` : `${(item.completion ?? 0).toFixed(1)}%`} -
-
- ))}
); diff --git a/src/modules/mileage/MonitoringView.tsx b/src/modules/mileage/MonitoringView.tsx index 41041e8..aa1a4da 100644 --- a/src/modules/mileage/MonitoringView.tsx +++ b/src/modules/mileage/MonitoringView.tsx @@ -5,7 +5,7 @@ import { Maximize2, Minimize2, RotateCcw, ArrowUp, ArrowDown, ChevronsUp, Download, Check, CalendarDays, } from 'lucide-react'; -import { BarChart, Bar, ResponsiveContainer, Tooltip, ReferenceLine } from 'recharts'; +import { BarChart, Bar, ResponsiveContainer, Tooltip, ReferenceLine, XAxis } from 'recharts'; import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types'; import { fetchMonitoring } from './api'; import Blur from '../../components/Blur'; @@ -1140,7 +1140,6 @@ export default function MonitoringView() { 区间走势
{rangeDailyTotals.length} 天 · km
-
{rangeLabel}
{rangeDailyTotals.length === 0 ? ( @@ -1148,9 +1147,10 @@ export default function MonitoringView() { ) : ( + [`${Number(value ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })} km`, '当日里程']} - labelFormatter={(label) => `日期 ${label}`} + labelFormatter={(label) => `日期 ${String(label)}`} contentStyle={{ borderRadius: 10, borderColor: '#e2e8f0', fontSize: 11 }} cursor={{ fill: 'rgba(37, 99, 235, 0.06)' }} /> @@ -1167,15 +1167,25 @@ export default function MonitoringView() {
) : ( -
+
单日概览
{rangeLabel} · 单位 km
-
- 当前列表最高 {topLoadedVehicle ? `${topLoadedVehicle.plate} · ${Math.round(topLoadedVehicle.dailyKm).toLocaleString()} km` : '-'} +
+
当前列表最高
+ {topLoadedVehicle ? ( +
+ {topLoadedVehicle.plate} + + {topLoadedVehicle.dailyKm.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} km + +
+ ) : ( +
-
+ )}
diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts index d8115c2..976f0c4 100644 --- a/src/server/routes/energy/index.ts +++ b/src/server/routes/energy/index.ts @@ -37,6 +37,53 @@ function customerClause(customer: CustomerKind): string { } type Range = 'thisWeek' | 'thisMonth' | 'last15'; +interface DateRange { + start: string; + end: string; +} + +const YMD_RE = /^\d{4}-\d{2}-\d{2}$/; + +function fmtYmd(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +function addDays(d: Date, days: number): Date { + const next = new Date(d); + next.setDate(next.getDate() + days); + return next; +} + +function parseYmd(value: string | undefined): string | null { + if (!value || !YMD_RE.test(value)) return null; + const d = new Date(`${value}T00:00:00`); + return Number.isNaN(d.getTime()) ? null : value; +} + +function resolveDateRange(range: Range, startParam?: string, endParam?: string): DateRange { + const customStart = parseYmd(startParam); + const customEnd = parseYmd(endParam); + if (customStart && customEnd) { + return customStart <= customEnd + ? { start: customStart, end: customEnd } + : { start: customEnd, end: customStart }; + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (range === 'thisWeek') { + const day = today.getDay() || 7; + return { start: fmtYmd(addDays(today, -(day - 1))), end: fmtYmd(today) }; + } + if (range === 'thisMonth') { + return { start: fmtYmd(new Date(today.getFullYear(), today.getMonth(), 1)), end: fmtYmd(today) }; + } + return { start: fmtYmd(addDays(today, -14)), end: fmtYmd(today) }; +} + +function dateRangeClause(localExpr: string): string { + return `${localExpr} >= ? AND ${localExpr} < DATE_ADD(?, INTERVAL 1 DAY)`; +} function rangeClause(localExpr: string, range: Range): string { switch (range) { @@ -48,25 +95,16 @@ function rangeClause(localExpr: string, range: Range): string { /** 列出某 range 在当前时点下的全部日期(YYYY-MM-DD),用于补零 */ function enumerateDates(range: Range): string[] { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; - let start: Date; - if (range === 'thisWeek') { - // 周一为一周开始(与 YEARWEEK(?, 1) 一致) - const day = today.getDay() || 7; // 周日 7 - start = new Date(today); - start.setDate(today.getDate() - (day - 1)); - } else if (range === 'thisMonth') { - start = new Date(today.getFullYear(), today.getMonth(), 1); - } else { - start = new Date(today); - start.setDate(today.getDate() - 14); - } + const { start, end } = resolveDateRange(range); + return enumerateDateRange(start, end); +} + +function enumerateDateRange(startYmd: string, endYmd: string): string[] { const result: string[] = []; - const cur = new Date(start); - while (cur <= today) { - result.push(fmt(cur)); + const cur = new Date(`${startYmd}T00:00:00`); + const end = new Date(`${endYmd}T00:00:00`); + while (cur <= end) { + result.push(fmtYmd(cur)); cur.setDate(cur.getDate() + 1); } return result; @@ -331,15 +369,16 @@ app.get('/hydrogen/overview', async (c) => { // ========================================================= app.get('/hydrogen/daily', async (c) => { const range = (c.req.query('range') || 'last15') as Range; + const dateRange = resolveDateRange(range, c.req.query('startDate'), c.req.query('endDate')); const customer = (c.req.query('customer') || 'external') as CustomerKind; const force = c.req.query('force') === '1'; - const data = await cached(`hydrogen/daily?range=${range}&customer=${customer}`, async () => { + const data = await cached(`hydrogen/daily?start=${dateRange.start}&end=${dateRange.end}&customer=${customer}`, async () => { const where = [ HYDROGEN_BASE_WHERE_B, `b.${HYDROGEN_LOCAL} >= '${HYDROGEN_MIN_DATE}'`, - rangeClause(`b.${HYDROGEN_LOCAL}`, range), + dateRangeClause(`b.${HYDROGEN_LOCAL}`), customerClause(customer).replaceAll('customer_price', 'b.customer_price').replaceAll('fee_total', 'b.fee_total'), ].join(' AND '); @@ -360,6 +399,7 @@ app.get('/hydrogen/daily', async (c) => { WHERE ${where} GROUP BY d, COALESCE(b.station_id, 0) ORDER BY d DESC, kg DESC`, + [dateRange.start, dateRange.end], ); // 站点环比:同站点上一条记录的 kg @@ -409,7 +449,7 @@ app.get('/hydrogen/daily', async (c) => { } // 补零:列出 range 内全部日期,缺失日期返回 totalKg=0、stations=[] - const allDates = enumerateDates(range); + const allDates = enumerateDateRange(dateRange.start, dateRange.end); const fullDays = allDates.map(date => { const info = dayMap.get(date); return { @@ -533,9 +573,10 @@ app.get('/electric/overview', async (c) => { app.get('/electric/monthly', async (c) => { const customer = (c.req.query('customer') || 'lingniu') as CustomerKind; const range = (c.req.query('range') || 'last15') as Range; + const dateRange = resolveDateRange(range, c.req.query('startDate'), c.req.query('endDate')); const force = c.req.query('force') === '1'; - const data = await cached(`electric/monthly?customer=${customer}&range=${range}`, async () => { + const data = await cached(`electric/monthly?customer=${customer}&start=${dateRange.start}&end=${dateRange.end}`, async () => { // bi_ele_charge_record 用 vehicle_kind 区分:internal=我司,external=外部 let kindClause = '1=1'; @@ -548,8 +589,9 @@ app.get('/electric/monthly', async (c) => { SUM(fee) AS fee FROM bi_ele_charge_record WHERE ${kindClause} - AND ${rangeClause('start_time', range)} + AND ${dateRangeClause('start_time')} GROUP BY date`, + [dateRange.start, dateRange.end], ); // 实际数据 map @@ -562,7 +604,7 @@ app.get('/electric/monthly', async (c) => { } // 补零:枚举 range 全部日期 - const allDates = enumerateDates(range); + const allDates = enumerateDateRange(dateRange.start, dateRange.end); const fullDays = allDates.map(date => { const d = dataMap.get(date); return {