From ba6a38973d6c335a6aa4a598be3ccd852e7486ad Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 16 Apr 2026 22:46:42 +0800 Subject: [PATCH] feat(scheduling): add batch filter and sort controls for candidate list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Batch dropdown filter (全部批次 / per target name) - Sort by 替换后预计 (asc/desc toggle) - Sort by 当前里程 (asc/desc toggle) - Active sort button highlighted in blue - Display count shows filtered/total (e.g. "3/12 辆") Co-Authored-By: Claude Opus 4.6 (1M context) --- src/modules/scheduling/SuggestionDetail.tsx | 78 ++++++++++++++++++--- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/src/modules/scheduling/SuggestionDetail.tsx b/src/modules/scheduling/SuggestionDetail.tsx index dc8e78d..e745ffc 100644 --- a/src/modules/scheduling/SuggestionDetail.tsx +++ b/src/modules/scheduling/SuggestionDetail.tsx @@ -1,12 +1,15 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { - X, MapPin, AlertTriangle, CheckCircle, ArrowDown, ArrowUp, ArrowRight, + X, MapPin, AlertTriangle, CheckCircle, ArrowDown, ArrowUp, ArrowRight, ArrowUpDown, } from 'lucide-react'; import { motion } from 'motion/react'; import type { SchedulingSuggestion, CandidateVehicle } from './types'; import Blur from '../../components/Blur'; import SwapPreview from './SwapPreview'; +type SortKey = 'predicted' | 'current'; +type SortDir = 'asc' | 'desc'; + interface Props { suggestion: SchedulingSuggestion; onClose: () => void; @@ -25,10 +28,36 @@ function fmtRate(rate: number): string { export default function SuggestionDetail({ suggestion: s, onClose, onNotifySuccess }: Props) { const [previewCandidate, setPreviewCandidate] = useState(null); const [sentPlates, setSentPlates] = useState>(new Set()); + const [batchFilter, setBatchFilter] = useState(''); + const [sortKey, setSortKey] = useState('predicted'); + const [sortDir, setSortDir] = useState('desc'); const v = s.currentVehicle; const isRescue = s.type === 'rescue_hopeless'; + // Batch options from candidates + const batchOptions = useMemo(() => { + const set = new Set(); + for (const c of s.candidates) if (c.targetName) set.add(c.targetName); + return [...set].sort(); + }, [s.candidates]); + + // Filtered + sorted candidates + const displayCandidates = useMemo(() => { + let list = s.candidates; + if (batchFilter) list = list.filter(c => c.targetName === batchFilter); + return [...list].sort((a, b) => { + const va = sortKey === 'predicted' ? a.predictedAfterSwap : a.totalMileage; + const vb = sortKey === 'predicted' ? b.predictedAfterSwap : b.totalMileage; + return sortDir === 'desc' ? vb - va : va - vb; + }); + }, [s.candidates, batchFilter, sortKey, sortDir]); + + const toggleSort = (key: SortKey) => { + if (sortKey === key) { setSortDir(d => d === 'desc' ? 'asc' : 'desc'); } + else { setSortKey(key); setSortDir('desc'); } + }; + return (
-
-
- 当前区域所有可替换在库车辆 - {s.candidates.length} 辆 -
+
+ 当前区域可替换在库车辆 + {displayCandidates.length}/{s.candidates.length} 辆 +
+ + {/* Filter + Sort controls */} +
+ {/* Batch filter */} + + + {/* Sort buttons */} + +
- {s.candidates.map(c => { + {displayCandidates.map(c => { const sent = sentPlates.has(c.plateNumber); return (