import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Filter, RotateCcw, X, Search, ChevronDown } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { fetchSuggestions } from './api'; import type { SchedulingResponse, SchedulingSuggestion } from './types'; import SuggestionList from './SuggestionList'; import SuggestionDetail from './SuggestionDetail'; type TypeFilter = 'all' | 'qualified' | 'hopeless'; interface AdvancedFilters { plateSearch: string; region: string; vehicleType: string; customer: string; department: string; manager: string; } const EMPTY_FILTERS: AdvancedFilters = { plateSearch: '', region: '', vehicleType: '', customer: '', department: '', manager: '' }; function shortTargetName(name: string): string { const match = name.match(/(\d+)[辆台](.+)/); if (!match) return name; const count = match[1]; let desc = match[2]; desc = desc.replace('4.5T普货', '普货'); desc = desc.replace('4.5T冷链车', '冷藏车'); desc = desc.replace('4.5T冷链', '冷藏车'); return `${count}台${desc}`; } function hasActiveFilters(f: AdvancedFilters): boolean { return f.plateSearch !== '' || f.region !== '' || f.vehicleType !== '' || f.customer !== ''; } function FilterSelect({ label, options, value, onChange, placeholder }: { label: string; options: string[]; value: string; onChange: (v: string) => void; placeholder: string; }) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); const ref = useRef(null); const filtered = options.filter(o => o.toLowerCase().includes(search.toLowerCase())); useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); return (
{open && (
{options.length > 5 && (
setSearch(e.target.value)} placeholder="搜索..." autoFocus className="w-full pl-7 pr-2 py-1.5 text-xs bg-slate-50 rounded border-none outline-none" />
)}
{filtered.map(opt => ( ))}
)}
); } /** Skeleton pulse block */ function Sk({ className }: { className?: string }) { return
; } function SkeletonPage() { return (
{/* Cards skeleton */}
{[0, 1, 2].map(i => (
))}
{/* List card skeleton */}
{/* Header */}
{[0, 1, 2, 3].map(i => )}
{/* Rows */}
{Array.from({ length: 8 }).map((_, i) => (
))}
); } export default function SchedulingModule() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [selectedTargetId, setSelectedTargetId] = useState(undefined); const [selectedSuggestion, setSelectedSuggestion] = useState(null); const [typeFilter, setTypeFilter] = useState('all'); const [showFilter, setShowFilter] = useState(false); const [filters, setFilters] = useState(EMPTY_FILTERS); const [tempFilters, setTempFilters] = useState(EMPTY_FILTERS); const loadData = useCallback(async () => { setLoading(true); try { setData(await fetchSuggestions(selectedTargetId)); } finally { setLoading(false); } }, [selectedTargetId]); useEffect(() => { loadData(); }, [loadData]); const handleNotifySuccess = useCallback(() => { loadData(); }, [loadData]); const filterOptions = useMemo(() => { if (!data) return { regions: [], vehicleTypes: [], customers: [], departments: [], managers: [] }; const r = new Set(), t = new Set(), c = new Set(), d = new Set(), m = new Set(); for (const s of data.suggestions) { const v = s.currentVehicle; if (v.region) r.add(v.region); if (v.vehicleType) t.add(v.vehicleType); if (v.customer) c.add(v.customer); if (v.department) d.add(v.department); if (v.manager) m.add(v.manager); } return { regions: [...r].sort(), vehicleTypes: [...t].sort(), customers: [...c].sort(), departments: [...d].sort(), managers: [...m].sort() }; }, [data]); const filteredSuggestions = useMemo(() => { if (!data) return []; let list = data.suggestions; if (typeFilter === 'qualified') list = list.filter(s => s.type === 'replace_qualified'); if (typeFilter === 'hopeless') list = list.filter(s => s.type === 'rescue_hopeless'); if (filters.plateSearch) { const q = filters.plateSearch.toLowerCase(); list = list.filter(s => s.currentVehicle.plateNumber.toLowerCase().includes(q)); } if (filters.region) list = list.filter(s => s.currentVehicle.region === filters.region); if (filters.vehicleType) list = list.filter(s => s.currentVehicle.vehicleType === filters.vehicleType); if (filters.customer) list = list.filter(s => s.currentVehicle.customer === filters.customer); if (filters.department) list = list.filter(s => s.currentVehicle.department === filters.department); if (filters.manager) list = list.filter(s => s.currentVehicle.manager === filters.manager); return list; }, [data, typeFilter, filters]); const summary = data?.summary; const activeFilterCount = [filters.plateSearch, filters.region, filters.vehicleType, filters.customer, filters.department, filters.manager].filter(Boolean).length; // Initial load — full page skeleton if (loading && !data) return ; return (
{/* ===== Summary Cards ===== */}
{/* 里程高·换下 — warm orange */} {/* 里程低·换走 — cool blue */} {/* 替换建议 — neutral dark */}
{/* ===== List Card ===== */}
{/* Header */}

智能调度干预清单

{data?.targets.map(t => ( ))}
{/* Filter Panel */} {showFilter && (
高级筛选 {hasActiveFilters(tempFilters) && ( )}
setTempFilters(prev => ({ ...prev, plateSearch: e.target.value }))} placeholder="搜索车牌号..." className="w-full pl-8 pr-3 py-2 bg-white rounded-lg text-xs border border-slate-200 outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all" />
setTempFilters(prev => ({ ...prev, region: v }))} placeholder="全部区域" /> setTempFilters(prev => ({ ...prev, vehicleType: v }))} placeholder="全部类型" />
setTempFilters(prev => ({ ...prev, department: v }))} placeholder="全部部门" /> setTempFilters(prev => ({ ...prev, manager: v }))} placeholder="全部负责人" />
setTempFilters(prev => ({ ...prev, customer: v }))} placeholder="全部客户" />
)}
{/* Active filter tags */} {activeFilterCount > 0 && !showFilter && (
筛选: {filters.plateSearch && 车牌 "{filters.plateSearch}" setFilters(prev => ({ ...prev, plateSearch: '' }))} />} {filters.region && {filters.region} setFilters(prev => ({ ...prev, region: '' }))} />} {filters.vehicleType && {filters.vehicleType} setFilters(prev => ({ ...prev, vehicleType: '' }))} />} {filters.department && {filters.department} setFilters(prev => ({ ...prev, department: '' }))} />} {filters.manager && {filters.manager} setFilters(prev => ({ ...prev, manager: '' }))} />} {filters.customer && {filters.customer} setFilters(prev => ({ ...prev, customer: '' }))} />}
)} {(activeFilterCount > 0 || typeFilter !== 'all') && (
共 {filteredSuggestions.length} 条结果
)} {loading ? ( /* List skeleton while refreshing */
{Array.from({ length: 6 }).map((_, i) => (
))}
) : ( )}
{selectedSuggestion && ( setSelectedSuggestion(null)} onNotifySuccess={handleNotifySuccess} /> )}
); }