Files
ln-bi/src/modules/mileage/monitoring/components/MonitoringToolbar.tsx
T

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>
);
}