From 6f7555a407eae560fd7782ca96f3f0676711ce1e Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 16 Apr 2026 21:12:02 +0800 Subject: [PATCH] feat(scheduling): make summary cards clickable filters + refine color scheme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cards filter suggestions by type (已达标/无望达标/全部) - Toggle: click active card again to reset to all - Default: white bg + gray border; active: colored bg + ring - Batch selector: dark pills instead of blue - Refresh button moved into list header - Reset type filter when switching batch Co-Authored-By: Claude Opus 4.6 (1M context) --- src/modules/scheduling/SchedulingModule.tsx | 127 ++++++++++++++------ src/modules/scheduling/SuggestionList.tsx | 26 ++-- 2 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/modules/scheduling/SchedulingModule.tsx b/src/modules/scheduling/SchedulingModule.tsx index 0ac4516..7f8d302 100644 --- a/src/modules/scheduling/SchedulingModule.tsx +++ b/src/modules/scheduling/SchedulingModule.tsx @@ -1,11 +1,12 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Activity, AlertTriangle, CheckCircle, TrendingUp, RotateCcw } from 'lucide-react'; -import { motion } from 'motion/react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { RotateCcw } from 'lucide-react'; import { fetchSuggestions } from './api'; import type { SchedulingResponse, SchedulingSuggestion } from './types'; import SuggestionList from './SuggestionList'; import SuggestionDetail from './SuggestionDetail'; +type TypeFilter = 'all' | 'qualified' | 'hopeless'; + function shortTargetName(name: string): string { const match = name.match(/(\d+)[辆台](.+)/); if (!match) return name; @@ -22,6 +23,7 @@ export default function SchedulingModule() { const [loading, setLoading] = useState(false); const [selectedTargetId, setSelectedTargetId] = useState(undefined); const [selectedSuggestion, setSelectedSuggestion] = useState(null); + const [typeFilter, setTypeFilter] = useState('all'); const loadData = useCallback(async () => { setLoading(true); @@ -33,13 +35,61 @@ export default function SchedulingModule() { } }, [selectedTargetId]); - useEffect(() => { - loadData(); - }, [loadData]); + useEffect(() => { loadData(); }, [loadData]); - const handleNotifySuccess = useCallback(() => { - loadData(); - }, [loadData]); + const handleNotifySuccess = useCallback(() => { loadData(); }, [loadData]); + + const filteredSuggestions = useMemo(() => { + if (!data) return []; + if (typeFilter === 'qualified') return data.suggestions.filter(s => s.type === 'replace_qualified'); + if (typeFilter === 'hopeless') return data.suggestions.filter(s => s.type === 'rescue_hopeless'); + return data.suggestions; + }, [data, typeFilter]); + + const cardConfigs = [ + { + key: 'qualified' as TypeFilter, + label: '已达标', + count: data?.summary.qualifiedCount ?? 0, + unit: '台', + sub: null, + // Muted teal/green + idle: 'border-slate-200 bg-white', + active: 'border-emerald-500 bg-emerald-50 ring-1 ring-emerald-500/20', + labelColor: 'text-slate-500', + labelActive: 'text-emerald-600', + numColor: 'text-slate-800', + numActive: 'text-emerald-700', + }, + { + key: 'hopeless' as TypeFilter, + label: '无望达标', + count: data?.summary.hopelessCount ?? 0, + unit: '台', + sub: null, + // Warm red + idle: 'border-slate-200 bg-white', + active: 'border-rose-500 bg-rose-50 ring-1 ring-rose-500/20', + labelColor: 'text-slate-500', + labelActive: 'text-rose-600', + numColor: 'text-slate-800', + numActive: 'text-rose-700', + }, + { + key: 'all' as TypeFilter, + label: '可干预', + count: data?.summary.suggestionCount ?? 0, + unit: '条', + sub: data ? `+${data.summary.estimatedGain} 台可达标` : null, + // Blue accent + idle: 'border-slate-200 bg-white', + active: 'border-blue-500 bg-blue-50 ring-1 ring-blue-500/20', + labelColor: 'text-slate-500', + labelActive: 'text-blue-600', + numColor: 'text-slate-800', + numActive: 'text-blue-700', + }, + ]; return (
@@ -48,11 +98,11 @@ export default function SchedulingModule() { {/* Batch Selector */}
{loading && !data ? ( - /* Loading State */
) : data ? ( <> - {/* Summary Cards */} + {/* Summary Cards — clickable filter */}
-
-
已达标
-
{data.summary.qualifiedCount}
-
-
-
无望达标
-
{data.summary.hopelessCount}
-
-
-
可干预
-
{data.summary.suggestionCount}
-
+{data.summary.estimatedGain} 台可达标
-
+ {cardConfigs.map(cfg => { + const isActive = typeFilter === cfg.key; + return ( + + ); + })}
{/* Suggestion List */} ) : null} - {/* Detail Modal */} {selectedSuggestion && ( )} -
); diff --git a/src/modules/scheduling/SuggestionList.tsx b/src/modules/scheduling/SuggestionList.tsx index 8fb72b7..7fc55dd 100644 --- a/src/modules/scheduling/SuggestionList.tsx +++ b/src/modules/scheduling/SuggestionList.tsx @@ -1,4 +1,4 @@ -import { ArrowRightLeft, ChevronRight } from 'lucide-react'; +import { ArrowRightLeft, ChevronRight, RotateCcw } from 'lucide-react'; import { motion } from 'motion/react'; import type { SchedulingSuggestion } from './types'; import Blur from '../../components/Blur'; @@ -6,18 +6,15 @@ import Blur from '../../components/Blur'; interface Props { suggestions: SchedulingSuggestion[]; onSelect: (s: SchedulingSuggestion) => void; -} - -function fmtKm(value: number): string { - if (value >= 10000) return (value / 10000).toFixed(1) + '万'; - return value.toLocaleString(); + loading?: boolean; + onRefresh?: () => void; } function fmtRate(rate: number): string { return (rate * 100).toFixed(1) + '%'; } -export default function SuggestionList({ suggestions, onSelect }: Props) { +export default function SuggestionList({ suggestions, onSelect, loading, onRefresh }: Props) { if (suggestions.length === 0) { return (
@@ -32,6 +29,15 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
调度干预清单 + {onRefresh && ( + + )} {suggestions.length} @@ -51,10 +57,10 @@ export default function SuggestionList({ suggestions, onSelect }: Props) { className="px-4 py-3 hover:bg-slate-50/60 cursor-pointer transition-colors active:bg-slate-100 flex items-center gap-3" onClick={() => onSelect(s)} > - {/* Left: color bar */} + {/* Color bar */}
- {/* Center: info */} + {/* Info */}
@@ -76,7 +82,7 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
- {/* Right: candidate count + arrow */} + {/* Right */}
{s.candidates.length}