feat(scheduling): add SchedulingModule main entry component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-16 20:25:13 +08:00
parent 4169e04a9c
commit 82ee7f5480

View File

@@ -0,0 +1,149 @@
import { useState, useEffect, useCallback } from 'react';
import { Activity, AlertTriangle, CheckCircle, TrendingUp, RotateCcw } from 'lucide-react';
import { motion } from 'motion/react';
import { fetchSuggestions } from './api';
import type { SchedulingResponse, SchedulingSuggestion } from './types';
import SuggestionList from './SuggestionList';
import SuggestionDetail from './SuggestionDetail';
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}`;
}
export default function SchedulingModule() {
const [data, setData] = useState<SchedulingResponse | null>(null);
const [loading, setLoading] = useState(false);
const [selectedTargetId, setSelectedTargetId] = useState<number | undefined>(undefined);
const [selectedSuggestion, setSelectedSuggestion] = useState<SchedulingSuggestion | null>(null);
const loadData = useCallback(async () => {
setLoading(true);
try {
const result = await fetchSuggestions(selectedTargetId);
setData(result);
} finally {
setLoading(false);
}
}, [selectedTargetId]);
useEffect(() => {
loadData();
}, [loadData]);
const handleNotifySuccess = useCallback(() => {
loadData();
}, [loadData]);
return (
<div className="min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-3 md:p-6" style={{ overflowX: 'clip' }}>
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 md:pb-0">
{/* Batch Selector */}
<div className="bg-white p-2 rounded-2xl shadow-sm border border-slate-100 flex gap-1 overflow-x-auto no-scrollbar">
<button
onClick={() => setSelectedTargetId(undefined)}
className={`flex-shrink-0 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all ${
selectedTargetId === undefined
? 'bg-blue-600 text-white shadow-md shadow-blue-200'
: 'bg-slate-50 text-slate-500'
}`}
>
</button>
{data?.targets.map((target) => (
<button
key={target.id}
onClick={() => setSelectedTargetId(target.id)}
className={`flex-shrink-0 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all ${
selectedTargetId === target.id
? 'bg-blue-600 text-white shadow-md shadow-blue-200'
: 'bg-slate-50 text-slate-500'
}`}
>
{shortTargetName(target.name)}
</button>
))}
</div>
{loading && !data ? (
/* Loading State */
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
) : data ? (
<>
{/* Summary Cards */}
<div className="grid grid-cols-3 gap-2">
{/* Card 1: 已达标车辆 */}
<div className="bg-emerald-50 border border-emerald-100 p-4 rounded-2xl">
<div className="flex items-center gap-1.5 mb-2">
<CheckCircle size={14} className="text-emerald-600" />
<span className="text-[11px] font-semibold text-emerald-700"></span>
</div>
<div className="text-2xl font-black text-emerald-700">{data.summary.qualifiedCount}</div>
<div className="text-[10px] text-emerald-500 mt-1"> 120%</div>
</div>
{/* Card 2: 无望达标 */}
<div className="bg-rose-50 border border-rose-100 p-4 rounded-2xl">
<div className="flex items-center gap-1.5 mb-2">
<AlertTriangle size={14} className="text-rose-600" />
<span className="text-[11px] font-semibold text-rose-700"></span>
</div>
<div className="text-2xl font-black text-rose-700">{data.summary.hopelessCount}</div>
<div className="text-[10px] text-rose-500 mt-1"> &lt; 60%</div>
</div>
{/* Card 3: 可干预 */}
<div className="bg-blue-50 border border-blue-100 p-4 rounded-2xl">
<div className="flex items-center gap-1.5 mb-2">
<TrendingUp size={14} className="text-blue-600" />
<span className="text-[11px] font-semibold text-blue-700"></span>
</div>
<div className="text-2xl font-black text-blue-700">{data.summary.suggestionCount}</div>
<div className="text-[10px] text-blue-500 mt-1">
+{data.summary.estimatedGain}
</div>
</div>
</div>
{/* Refresh Button */}
<div className="flex justify-end">
<button
onClick={loadData}
disabled={loading}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-600 transition-colors px-3 py-1.5 rounded-xl hover:bg-slate-100"
>
<RotateCcw size={13} className={loading ? 'animate-spin' : ''} />
</button>
</div>
{/* Suggestion List */}
<SuggestionList
suggestions={data.suggestions}
onSelect={setSelectedSuggestion}
/>
</>
) : null}
{/* Detail Modal */}
{selectedSuggestion && (
<SuggestionDetail
suggestion={selectedSuggestion}
onClose={() => setSelectedSuggestion(null)}
onNotifySuccess={handleNotifySuccess}
/>
)}
</div>
</div>
);
}