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,92 +1,172 @@
|
||||
import { Truck } from 'lucide-react';
|
||||
import { SurfaceCard } from '../../../components/ui/surface';
|
||||
import { Calendar, Fuel, RefreshCw, Truck } from 'lucide-react';
|
||||
import type { CustomerType, DateQuickPick } from '../types';
|
||||
import type { HydrogenDailyVehicleScope } from '../hydrogen-daily/model';
|
||||
import { QUICK_PICK_OPTIONS, type RangeMode } from './model';
|
||||
|
||||
interface DailyRangeControlsProps {
|
||||
pick: RangeMode;
|
||||
dateRange: { start: string; end: string };
|
||||
customer: CustomerType;
|
||||
stations?: { id: number; name: string }[];
|
||||
selectedStationId?: number | null;
|
||||
vehicleScope?: HydrogenDailyVehicleScope;
|
||||
updatedAt?: string | null;
|
||||
loading?: boolean;
|
||||
onQuickPick: (pick: DateQuickPick) => void;
|
||||
onCustomPick: () => void;
|
||||
onDateRangeChange: (field: 'start' | 'end', value: string) => void;
|
||||
onCustomerChange: (customer: CustomerType) => void;
|
||||
onStationChange?: (stationId: number | null) => void;
|
||||
onVehicleScopeChange?: (scope: HydrogenDailyVehicleScope) => void;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
export default function DailyRangeControls({
|
||||
pick,
|
||||
dateRange,
|
||||
customer,
|
||||
stations = [],
|
||||
selectedStationId = null,
|
||||
vehicleScope,
|
||||
updatedAt,
|
||||
loading = false,
|
||||
onQuickPick,
|
||||
onCustomPick,
|
||||
onDateRangeChange,
|
||||
onCustomerChange,
|
||||
onStationChange,
|
||||
onVehicleScopeChange,
|
||||
onRefresh,
|
||||
}: DailyRangeControlsProps) {
|
||||
return (
|
||||
<SurfaceCard className="p-2 md:p-3">
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-1">
|
||||
{QUICK_PICK_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.id}
|
||||
onClick={() => onQuickPick(opt.id)}
|
||||
className={`min-h-9 shrink-0 rounded-xl border px-3 text-[12px] font-black transition-colors ${
|
||||
pick === opt.id
|
||||
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
||||
: 'border-slate-100 bg-white text-slate-500 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={onCustomPick}
|
||||
className={`min-h-9 shrink-0 rounded-xl border px-3 text-[12px] font-black transition-colors ${
|
||||
pick === 'custom'
|
||||
? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm'
|
||||
: 'border-slate-100 bg-white text-slate-500 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
</div>
|
||||
<section className="rounded-[14px] border border-slate-200/70 bg-white px-3 py-3 shadow-sm md:px-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<div className="flex rounded-md bg-slate-100 p-0.5">
|
||||
{QUICK_PICK_OPTIONS.map(option => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => onQuickPick(option.id)}
|
||||
className={`h-7 rounded px-3 text-[12px] font-medium transition-colors ${
|
||||
pick === option.id
|
||||
? 'bg-white font-semibold text-blue-600 shadow-sm'
|
||||
: 'text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCustomPick}
|
||||
className={`h-7 rounded px-3 text-[12px] font-medium transition-colors ${
|
||||
pick === 'custom'
|
||||
? 'bg-white font-semibold text-blue-600 shadow-sm'
|
||||
: 'text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
自定义
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-2 gap-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>
|
||||
<input
|
||||
type="date"
|
||||
value={dateRange.start}
|
||||
onChange={event => onDateRangeChange('start', event.target.value)}
|
||||
onInput={event => onDateRangeChange('start', event.currentTarget.value)}
|
||||
className="mt-1 h-6 w-full bg-transparent text-[12px] font-black text-slate-800 outline-none"
|
||||
/>
|
||||
</label>
|
||||
<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>
|
||||
<input
|
||||
type="date"
|
||||
value={dateRange.end}
|
||||
onChange={event => onDateRangeChange('end', event.target.value)}
|
||||
onInput={event => onDateRangeChange('end', event.currentTarget.value)}
|
||||
className="mt-1 h-6 w-full bg-transparent text-[12px] font-black text-slate-800 outline-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<DateField label="开始日期" value={dateRange.start} onChange={value => onDateRangeChange('start', value)} />
|
||||
<DateField label="结束日期" value={dateRange.end} onChange={value => onDateRangeChange('end', value)} />
|
||||
|
||||
<div className="mt-2 grid grid-cols-2 gap-1 rounded-xl bg-slate-100 p-1">
|
||||
{(['lingniu', 'external'] as const).map(option => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => onCustomerChange(option)}
|
||||
className={`flex min-h-9 items-center justify-center gap-1.5 rounded-lg text-[12px] font-black transition-all ${
|
||||
customer === option ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<Truck size={14} />
|
||||
{option === 'external' ? '外部车辆' : '羚牛车辆'}
|
||||
</button>
|
||||
))}
|
||||
{onStationChange ? (
|
||||
<label className="inline-flex h-8 min-w-[190px] max-w-full items-center gap-2 rounded-md border border-slate-300 bg-white px-2.5 text-slate-600 transition-colors focus-within:border-sky-600 focus-within:ring-2 focus-within:ring-sky-100">
|
||||
<Fuel size={13} className="shrink-0 text-slate-400" />
|
||||
<select
|
||||
aria-label="选择加氢站"
|
||||
value={selectedStationId ?? ''}
|
||||
onChange={event => onStationChange(event.target.value === '' ? null : Number(event.target.value))}
|
||||
className="min-w-0 flex-1 bg-transparent text-[12px] font-semibold text-slate-700 outline-none"
|
||||
>
|
||||
<option value="">请选择加氢站</option>
|
||||
{stations.map((station, index) => (
|
||||
<option key={`${station.id}-${station.name}-${index}`} value={station.id}>{station.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
{onVehicleScopeChange && vehicleScope ? (
|
||||
<div className="flex rounded-md bg-slate-100 p-0.5" aria-label="车辆归属">
|
||||
{([
|
||||
['all', '全部车辆'],
|
||||
['lingniu', '仅羚牛车辆'],
|
||||
['external', '仅外部车辆'],
|
||||
] as const).map(([id, label]) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => onVehicleScopeChange(id)}
|
||||
className={`inline-flex h-8 items-center gap-1.5 rounded px-2.5 text-[12px] font-medium transition-colors ${
|
||||
vehicleScope === id
|
||||
? 'bg-white font-semibold text-blue-600 shadow-sm'
|
||||
: 'text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
<Truck size={13} />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex rounded-md bg-slate-100 p-0.5">
|
||||
{(['lingniu', 'external'] as const).map(option => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => onCustomerChange(option)}
|
||||
className={`inline-flex h-8 items-center gap-1.5 rounded px-3 text-[12px] font-medium transition-colors ${
|
||||
customer === option
|
||||
? 'bg-white font-semibold text-blue-600 shadow-sm'
|
||||
: 'text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
<Truck size={13} />
|
||||
{option === 'external' ? '外部车辆' : '羚牛车辆'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{updatedAt ? <span className="font-mono text-[11px] text-slate-400">{updatedAt}</span> : null}
|
||||
{onRefresh ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
title="刷新数据"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-slate-200 bg-white px-3 text-[12px] font-semibold text-slate-600 transition-colors hover:border-blue-200 hover:text-blue-600 disabled:cursor-wait disabled:opacity-60"
|
||||
>
|
||||
<RefreshCw size={13} className={loading ? 'animate-spin' : ''} />
|
||||
刷新
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SurfaceCard>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function DateField({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) {
|
||||
return (
|
||||
<label className="inline-flex h-8 items-center gap-2 rounded-md border border-slate-300 bg-white px-2.5 transition-colors focus-within:border-sky-600 focus-within:ring-2 focus-within:ring-sky-100">
|
||||
<Calendar size={13} className="shrink-0 text-sky-600" />
|
||||
<span className="hidden text-[11px] font-medium text-slate-400 sm:inline">{label}</span>
|
||||
<input
|
||||
type="date"
|
||||
aria-label={label}
|
||||
value={value}
|
||||
onChange={event => onChange(event.target.value)}
|
||||
className="h-7 min-w-[112px] bg-transparent font-mono text-[11px] font-semibold text-slate-700 outline-none"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user