feat(scheduling): add SchedulingModule main entry component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
149
src/modules/scheduling/SchedulingModule.tsx
Normal file
149
src/modules/scheduling/SchedulingModule.tsx
Normal 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">达标概率 < 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user