174 lines
8.0 KiB
TypeScript
174 lines
8.0 KiB
TypeScript
import type { Dispatch, SetStateAction } from 'react';
|
|
import { ArrowDown, ArrowUp, Download, Filter, Maximize2, RefreshCw, RotateCcw } from 'lucide-react';
|
|
import PlateMultiSelect from '../../PlateMultiSelect';
|
|
import type { MileageSourceGroup, MonitoringFilters } from '../../types';
|
|
import { getRangePreset, type RangePreset } from '../date-range';
|
|
import BatchMultiSelect from './BatchMultiSelect';
|
|
import SearchableSelect from './SearchableSelect';
|
|
import SourcePriorityControl from './SourcePriorityControl';
|
|
|
|
type SortBy = 'today' | 'total' | 'statisticTime';
|
|
type SortOrder = 'asc' | 'desc';
|
|
|
|
export default function MonitoringToolbar({
|
|
onToggleFullscreen,
|
|
onDownload,
|
|
exporting,
|
|
lastRefreshedAt,
|
|
onManualRefresh,
|
|
manualRefreshing,
|
|
sortBy,
|
|
onSortByChange,
|
|
sortOrder,
|
|
onSortOrderChange,
|
|
sourcePriority,
|
|
onSourcePriorityChange,
|
|
filterOptions,
|
|
filterTargetNames,
|
|
onFilterTargetNamesChange,
|
|
filterRegion,
|
|
onFilterRegionChange,
|
|
plateNumbers,
|
|
filterPlates,
|
|
onFilterPlatesChange,
|
|
isFilterOpen,
|
|
onFilterOpenChange,
|
|
searchTerm,
|
|
filterDept,
|
|
filterCustomer,
|
|
filterRentStatus,
|
|
filterProject,
|
|
filterBrands,
|
|
rangeStart,
|
|
rangeEnd,
|
|
onApplyRangePreset,
|
|
rangeLabel,
|
|
}: {
|
|
onToggleFullscreen: () => void;
|
|
onDownload: () => void;
|
|
exporting: boolean;
|
|
lastRefreshedAt: Date | null;
|
|
onManualRefresh: () => void;
|
|
manualRefreshing: boolean;
|
|
sortBy: SortBy;
|
|
onSortByChange: Dispatch<SetStateAction<SortBy>>;
|
|
sortOrder: SortOrder;
|
|
onSortOrderChange: Dispatch<SetStateAction<SortOrder>>;
|
|
sourcePriority: MileageSourceGroup[];
|
|
onSourcePriorityChange: (value: MileageSourceGroup[]) => void;
|
|
filterOptions: MonitoringFilters;
|
|
filterTargetNames: string[];
|
|
onFilterTargetNamesChange: (value: string[]) => void;
|
|
filterRegion: string;
|
|
onFilterRegionChange: (value: string) => void;
|
|
plateNumbers: string[];
|
|
filterPlates: string[];
|
|
onFilterPlatesChange: (value: string[]) => void;
|
|
isFilterOpen: boolean;
|
|
onFilterOpenChange: (value: boolean) => void;
|
|
searchTerm: string;
|
|
filterDept: string;
|
|
filterCustomer: string;
|
|
filterRentStatus: string;
|
|
filterProject: string;
|
|
filterBrands: string[];
|
|
rangeStart: string;
|
|
rangeEnd: string;
|
|
onApplyRangePreset: (preset: RangePreset) => void;
|
|
rangeLabel: string;
|
|
}) {
|
|
return (
|
|
<div className="flex flex-col gap-3 rounded-2xl border border-gray-100 bg-white p-3 shadow-sm">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<div className="h-8 w-1 flex-shrink-0 rounded-full bg-blue-600" />
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<h1 className="truncate text-lg font-black leading-none text-slate-900">里程看板</h1>
|
|
<button onClick={onToggleFullscreen} className="p-1 text-slate-300 transition-colors hover:text-blue-600" title="全屏视图">
|
|
<Maximize2 size={14} />
|
|
</button>
|
|
<button onClick={onDownload} disabled={exporting} className="p-1 text-slate-300 transition-colors hover:text-blue-600 disabled:text-slate-200" title="下载当前筛选结果">
|
|
{exporting ? <RotateCcw size={14} className="animate-spin" /> : <Download size={14} />}
|
|
</button>
|
|
</div>
|
|
<div className="mt-1 flex items-center gap-1.5">
|
|
<span className="h-1.5 w-1.5 rounded-full bg-blue-500" />
|
|
<span className="text-[9px] font-bold uppercase tracking-tight text-slate-400">数据监控 • OneOS 实时查询</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-shrink-0 items-center gap-1.5">
|
|
<span className="hidden text-[9px] font-bold tabular-nums text-slate-400 sm:inline">
|
|
{lastRefreshedAt ? lastRefreshedAt.toLocaleTimeString('zh-CN', { hour12: false }) : '--:--:--'}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={onManualRefresh}
|
|
disabled={manualRefreshing}
|
|
className="flex h-7 items-center gap-1 whitespace-nowrap rounded-lg border border-blue-100 bg-blue-50 px-2 text-[9px] font-bold text-blue-600 disabled:cursor-wait disabled:opacity-60"
|
|
title="局部刷新当前筛选数据"
|
|
>
|
|
<RefreshCw size={11} className={manualRefreshing ? 'animate-spin' : ''} />
|
|
<span>{manualRefreshing ? '刷新中' : '刷新数据'}</span>
|
|
</button>
|
|
<div className="flex items-center gap-1 rounded-lg bg-slate-100 p-0.5">
|
|
<button onClick={() => onSortByChange('today')} className={`rounded-md px-2 py-1 text-[9px] font-bold transition-all ${sortBy === 'today' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>区间</button>
|
|
<button onClick={() => onSortByChange('total')} className={`rounded-md px-2 py-1 text-[9px] font-bold transition-all ${sortBy === 'total' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>累计</button>
|
|
<button onClick={() => onSortByChange('statisticTime')} className={`rounded-md px-2 py-1 text-[9px] font-bold transition-all ${sortBy === 'statisticTime' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-400'}`}>统计时间</button>
|
|
<button
|
|
onClick={() => onSortOrderChange(sortOrder === 'desc' ? 'asc' : 'desc')}
|
|
className="rounded-md p-1 text-blue-600 transition-all hover:bg-white"
|
|
title={sortOrder === 'desc' ? '当前倒序,点击切换正序' : '当前正序,点击切换倒序'}
|
|
>
|
|
{sortOrder === 'desc' ? <ArrowDown size={12} /> : <ArrowUp size={12} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<SourcePriorityControl priority={sourcePriority} onChange={onSourcePriorityChange} />
|
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="grid flex-1 grid-cols-3 gap-1.5">
|
|
<BatchMultiSelect options={filterOptions.targetNames} selected={filterTargetNames} onChange={onFilterTargetNamesChange} placeholder="批次型号" />
|
|
<SearchableSelect options={filterOptions.regions} value={filterRegion} onChange={onFilterRegionChange} placeholder="运营区域" />
|
|
<PlateMultiSelect allPlates={plateNumbers} selected={filterPlates} onChange={onFilterPlatesChange} placeholder="按车牌" />
|
|
</div>
|
|
<button
|
|
onClick={() => onFilterOpenChange(!isFilterOpen)}
|
|
className={`flex-shrink-0 rounded-lg border p-1.5 transition-all ${isFilterOpen || searchTerm || filterDept !== 'All' || filterCustomer !== 'All' || filterRentStatus !== 'All' || filterPlates.length > 0 || filterProject !== 'All' || filterTargetNames.length > 0 || filterBrands.length > 0 ? 'border-blue-100 bg-blue-50 text-blue-600' : 'border-transparent bg-slate-50 text-slate-400'}`}
|
|
title="高级筛选"
|
|
>
|
|
<Filter size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 overflow-x-auto no-scrollbar">
|
|
{([
|
|
['today', '今天'],
|
|
['yesterday', '昨天'],
|
|
['thisWeek', '本周'],
|
|
['thisMonth', '本月'],
|
|
['last15', '近15天'],
|
|
] as Array<[RangePreset, string]>).map(([preset, label]) => {
|
|
const range = getRangePreset(preset);
|
|
const active = rangeStart === range.start && rangeEnd === range.end;
|
|
return (
|
|
<button
|
|
key={preset}
|
|
type="button"
|
|
onClick={() => onApplyRangePreset(preset)}
|
|
className={`shrink-0 rounded-lg border px-2.5 py-1.5 text-[10px] font-black transition-all ${active ? 'border-blue-200 bg-blue-50 text-blue-600 shadow-sm' : 'border-slate-100 bg-slate-50 text-slate-500 hover:border-blue-100 hover:bg-blue-50 hover:text-blue-600'}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
<span className="ml-auto shrink-0 rounded-lg bg-slate-50 px-2 py-1 text-[10px] font-bold text-slate-400">{rangeLabel}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|