feat(energy): rebuild hydrogen BI board and drill-through
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-08-20 13:59:03 +08:00
parent 37a9f303ec
commit 62efef0ab9
72 changed files with 36773 additions and 965 deletions
@@ -1,53 +1,174 @@
import { RefreshCw } from 'lucide-react';
import { ChevronDown, RefreshCw, Truck } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import type { HydrogenVehicleScope } from '../../api';
import { formatRefreshTime } from '../model';
interface BiYearSelectProps {
value: number;
years: number[];
onChange: (year: number) => void;
}
function BiYearSelect({ value, years, onChange }: BiYearSelectProps) {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setIsOpen(false);
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
return (
<div className="ehb-year-select-wrapper" ref={ref}>
<button
type="button"
className={`ehb-year-select-btn ${isOpen ? 'is-active' : ''}`}
onClick={() => setIsOpen(!isOpen)}
aria-label="年份选择"
>
<span className="ehb-year-text">{value} </span>
<ChevronDown
size={13}
style={{
transition: 'transform 0.2s ease',
transform: isOpen ? 'rotate(180deg)' : 'none',
color: '#64748b',
}}
/>
</button>
{isOpen && (
<div className="ehb-year-dropdown">
<div className="ehb-year-dropdown__header"></div>
<div className="ehb-year-dropdown__list">
{years.map((y) => (
<button
key={y}
type="button"
className={`ehb-year-dropdown__item ${y === value ? 'is-selected' : ''}`}
onClick={() => {
onChange(y);
setIsOpen(false);
}}
>
<span>{y} </span>
{y === value && <span className="ehb-year-check"></span>}
</button>
))}
</div>
</div>
)}
</div>
);
}
interface OverviewHeaderProps {
activeYear: number;
availableYears: number[];
lastRefreshAt: number;
refreshing: boolean;
vehicleScope: HydrogenVehicleScope;
verifyScope?: 'all' | 'verified';
onVerifyScopeChange?: (scope: 'all' | 'verified') => void;
selectedStationId?: number | null;
selectedStationName?: string | null;
stations?: { id: number; name: string }[];
latestLedgerTime?: string | null;
lastRefreshAt?: number;
refreshing?: boolean;
onSelectYear: (year: number) => void;
onVehicleScopeChange: (scope: HydrogenVehicleScope) => void;
onStationChange?: (stationId: number | null) => void;
onRefresh: () => void;
}
export function OverviewHeader({
activeYear,
availableYears,
lastRefreshAt,
refreshing,
vehicleScope,
verifyScope = 'all',
onVerifyScopeChange,
latestLedgerTime,
lastRefreshAt = 0,
refreshing = false,
onSelectYear,
onVehicleScopeChange,
onRefresh,
}: OverviewHeaderProps) {
return (
<div className="bg-white rounded-xl border border-slate-100 px-3 py-1.5 text-[11px] text-slate-400 flex items-center justify-between gap-2">
<span className="truncate">{lastRefreshAt ? `更新于 ${formatRefreshTime(lastRefreshAt)}` : '数据自 2025-01-01 起'}</span>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center gap-1 bg-slate-50 rounded-lg p-0.5">
{availableYears.map(y => {
const active = y === activeYear;
return (
<button
key={y}
onClick={() => onSelectYear(y)}
className={`px-2 py-0.5 text-[11px] font-bold rounded-md transition-all ${
active ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400 hover:text-slate-600'
}`}
>
{y}
</button>
);
})}
<section className="ehb-daily-filter-card" style={{ marginBottom: 12 }} aria-label="总览筛选工具栏">
<div className="ehb-daily-filter-row">
<div className="ehb-daily-filter-group">
<BiYearSelect value={activeYear} years={availableYears} onChange={onSelectYear} />
<div className="ehb-pill-tabs">
<button
type="button"
className={`ehb-pill-btn ${verifyScope === 'all' ? 'is-active' : ''}`}
onClick={() => onVerifyScopeChange?.('all')}
>
</button>
<button
type="button"
className={`ehb-pill-btn ${verifyScope === 'verified' ? 'is-active' : ''}`}
onClick={() => onVerifyScopeChange?.('verified')}
title="真实账本已返回全量有效订单"
>
</button>
</div>
</div>
<div className="ehb-daily-filter-group">
<div className="ehb-fleet-segmented">
<button
type="button"
className={`ehb-fleet-btn ${vehicleScope === 'all' ? 'is-active' : ''}`}
onClick={() => onVehicleScopeChange('all')}
>
</button>
<button
type="button"
className={`ehb-fleet-btn ${vehicleScope === 'lingniu' ? 'is-active' : ''}`}
onClick={() => onVehicleScopeChange('lingniu')}
>
<Truck size={14} />
</button>
<button
type="button"
className={`ehb-fleet-btn ${vehicleScope === 'external' ? 'is-active' : ''}`}
onClick={() => onVehicleScopeChange('external')}
>
<Truck size={14} />
</button>
</div>
<span className="ehb-chrome__clock" style={{ fontSize: 12, color: '#64748b' }}>
{latestLedgerTime ? `账本 ${latestLedgerTime.slice(5, 16)}` : lastRefreshAt ? formatRefreshTime(lastRefreshAt) : '已同步'}
</span>
<button
type="button"
className="ehb-btn ehb-btn--ghost"
onClick={onRefresh}
disabled={refreshing}
title="数据刷新"
>
<RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} aria-hidden />
</button>
</div>
<button
onClick={onRefresh}
disabled={refreshing}
className="flex items-center gap-1 px-2 py-0.5 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
title="手动刷新(绕过缓存)"
>
<RefreshCw size={11} className={refreshing ? 'animate-spin' : ''} strokeWidth={2.6} />
<span className="text-[11px] font-bold"></span>
</button>
</div>
</div>
</section>
);
}