feat(energy): rebuild hydrogen BI board and drill-through
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user