This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { Filter, RotateCcw, X, Search, ChevronDown, CheckSquare, Send, Clock, Download } from 'lucide-react';
|
||||
import { Activity, AlertTriangle, CheckCircle2, Filter, RotateCcw, X, Search, ChevronDown, CheckSquare, Send, Clock, Download, SendHorizonal } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { fetchSuggestions, sendNotifyBatch } from './api';
|
||||
import type { SchedulingResponse, SchedulingSuggestion, CandidateVehicle } from './types';
|
||||
@@ -9,6 +9,7 @@ import NotificationHistory from './NotificationHistory';
|
||||
import { exportSuggestionsCsv } from './csv-export';
|
||||
import Blur from '../../components/Blur';
|
||||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||||
import { MetricTile, PageFrame, SkeletonBlock, SurfaceCard } from '../../components/ui/surface';
|
||||
|
||||
type TypeFilter = 'all' | 'qualified' | 'hopeless';
|
||||
|
||||
@@ -87,63 +88,43 @@ function FilterSelect({ label, options, value, onChange, placeholder }: {
|
||||
);
|
||||
}
|
||||
|
||||
/** Skeleton pulse block */
|
||||
function Sk({ className }: { className?: string }) {
|
||||
return <div className={`animate-pulse bg-slate-200/70 rounded ${className ?? ''}`} />;
|
||||
}
|
||||
|
||||
function SkeletonPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F0F4F8] font-sans p-3 md:p-6">
|
||||
<div className="max-w-6xl mx-auto flex flex-col gap-3 pb-16 md:pb-0">
|
||||
{/* Cards skeleton */}
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{[0, 1, 2].map(i => (
|
||||
<div key={i} className="p-4 rounded-2xl bg-white border border-slate-100 space-y-2.5">
|
||||
<Sk className="h-3 w-16" />
|
||||
<Sk className="h-7 w-12" />
|
||||
<Sk className="h-2.5 w-24" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* List card skeleton */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200/60 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b border-slate-100 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Sk className="h-4 w-28" />
|
||||
<div className="flex gap-2"><Sk className="h-6 w-6 rounded-lg" /><Sk className="h-6 w-6 rounded-lg" /></div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{[0, 1, 2, 3].map(i => <Sk key={i} className="h-7 w-20 rounded-full" />)}
|
||||
</div>
|
||||
<PageFrame
|
||||
title="智能调度工作台"
|
||||
subtitle="自动识别高里程可释放车辆与低里程待救援车辆,形成可登记、可追踪的运营干预建议。"
|
||||
icon={Activity}
|
||||
eyebrow="SCHEDULING OPS"
|
||||
meta="建议生成中 · 正在计算候选车辆"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => <SkeletonBlock key={i} className="h-28" />)}
|
||||
</div>
|
||||
<SurfaceCard>
|
||||
<div className="space-y-3 p-4">
|
||||
<SkeletonBlock className="h-5 w-40" />
|
||||
<div className="flex gap-2">
|
||||
{[0, 1, 2, 3].map(i => <SkeletonBlock key={i} className="h-8 w-24 rounded-full" />)}
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
<div className="divide-y divide-slate-50">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="px-4 py-3 flex items-center gap-3">
|
||||
<Sk className="w-1 h-10 rounded-full" />
|
||||
<div key={i} className="flex items-center gap-3 py-3">
|
||||
<SkeletonBlock className="h-10 w-1 rounded-full" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sk className="h-3.5 w-20" />
|
||||
<Sk className="h-3 w-10 rounded-full" />
|
||||
<Sk className="h-3 w-14" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Sk className="h-2.5 w-28" />
|
||||
<Sk className="h-2.5 w-16" />
|
||||
<Sk className="h-2.5 w-14" />
|
||||
</div>
|
||||
<SkeletonBlock className="h-3.5 w-48" />
|
||||
<SkeletonBlock className="h-2.5 w-72 max-w-full" />
|
||||
</div>
|
||||
<Sk className="h-4 w-8" />
|
||||
<SkeletonBlock className="h-6 w-16" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SurfaceCard>
|
||||
</PageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,89 +256,33 @@ export default function SchedulingModule() {
|
||||
if (loading && !data) return <SkeletonPage />;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F0F4F8] text-slate-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">
|
||||
<PageFrame
|
||||
title="智能调度工作台"
|
||||
subtitle="自动识别高里程可释放车辆与低里程待救援车辆,形成可登记、可追踪的运营干预建议。"
|
||||
icon={Activity}
|
||||
eyebrow="SCHEDULING OPS"
|
||||
meta={`当前 ${filteredSuggestions.length} 条建议 · ${activeFilterCount} 个筛选条件`}
|
||||
actions={(
|
||||
<button onClick={loadData} disabled={loading} className="inline-flex items-center gap-1.5 rounded-xl bg-slate-900 px-3 py-2 text-xs font-black text-white shadow-sm transition-colors hover:bg-slate-800 disabled:opacity-60">
|
||||
<RotateCcw size={14} className={loading ? 'animate-spin' : ''} />
|
||||
刷新建议
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
|
||||
{/* ===== Summary Cards ===== */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2.5">
|
||||
{/* 里程高·换下 — warm orange */}
|
||||
<button
|
||||
onClick={() => setTypeFilter(typeFilter === 'qualified' ? 'all' : 'qualified')}
|
||||
className={`p-3.5 rounded-2xl text-left transition-all cursor-pointer ${
|
||||
typeFilter === 'qualified'
|
||||
? 'bg-orange-500 text-white shadow-lg shadow-orange-500/25'
|
||||
: 'bg-gradient-to-br from-orange-50 to-amber-50 border border-orange-200/60'
|
||||
}`}
|
||||
>
|
||||
<div className={`text-[10px] font-bold mb-1 ${typeFilter === 'qualified' ? 'text-orange-100' : 'text-orange-600'}`}>
|
||||
已完成考核目标
|
||||
</div>
|
||||
<div className={`text-2xl font-black ${typeFilter === 'qualified' ? 'text-white' : 'text-orange-700'}`}>
|
||||
{loading && !data ? '-' : summary?.qualifiedCount ?? 0}
|
||||
<span className={`text-[10px] font-normal ml-1 ${typeFilter === 'qualified' ? 'text-orange-200' : 'text-orange-400'}`}>台</span>
|
||||
</div>
|
||||
<div className={`text-[9px] mt-0.5 ${typeFilter === 'qualified' ? 'text-orange-200' : 'text-orange-400'}`}>
|
||||
换下,腾位给待达标车
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<button type="button" onClick={() => setTypeFilter(typeFilter === 'qualified' ? 'all' : 'qualified')} className={typeFilter === 'qualified' ? 'rounded-2xl ring-2 ring-orange-400 ring-offset-2 ring-offset-[var(--app-bg)]' : 'rounded-2xl'}>
|
||||
<MetricTile icon={CheckCircle2} label="已完成考核目标" value={summary?.qualifiedCount ?? 0} unit="台" helper="换下,腾位给待达标车" tone="amber" />
|
||||
</button>
|
||||
|
||||
{/* 里程低·换走 — cool blue */}
|
||||
<button
|
||||
onClick={() => setTypeFilter(typeFilter === 'hopeless' ? 'all' : 'hopeless')}
|
||||
className={`p-3.5 rounded-2xl text-left transition-all cursor-pointer ${
|
||||
typeFilter === 'hopeless'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/25'
|
||||
: 'bg-gradient-to-br from-blue-50 to-indigo-50 border border-blue-200/60'
|
||||
}`}
|
||||
>
|
||||
<div className={`text-[10px] font-bold mb-1 ${typeFilter === 'hopeless' ? 'text-blue-100' : 'text-blue-600'}`}>
|
||||
预估无法达标
|
||||
</div>
|
||||
<div className={`text-2xl font-black ${typeFilter === 'hopeless' ? 'text-white' : 'text-blue-700'}`}>
|
||||
{loading && !data ? '-' : summary?.hopelessCount ?? 0}
|
||||
<span className={`text-[10px] font-normal ml-1 ${typeFilter === 'hopeless' ? 'text-blue-200' : 'text-blue-400'}`}>台</span>
|
||||
</div>
|
||||
<div className={`text-[9px] mt-0.5 ${typeFilter === 'hopeless' ? 'text-blue-200' : 'text-blue-400'}`}>
|
||||
换走,换上快达标的车
|
||||
</div>
|
||||
<button type="button" onClick={() => setTypeFilter(typeFilter === 'hopeless' ? 'all' : 'hopeless')} className={typeFilter === 'hopeless' ? 'rounded-2xl ring-2 ring-blue-500 ring-offset-2 ring-offset-[var(--app-bg)]' : 'rounded-2xl'}>
|
||||
<MetricTile icon={AlertTriangle} label="预估无法达标" value={summary?.hopelessCount ?? 0} unit="台" helper="换走,换上快达标的车" tone="blue" />
|
||||
</button>
|
||||
|
||||
{/* 替换建议 — neutral dark */}
|
||||
<button
|
||||
onClick={() => setTypeFilter('all')}
|
||||
className={`p-3.5 rounded-2xl text-left transition-all cursor-pointer ${
|
||||
typeFilter === 'all'
|
||||
? 'bg-slate-800 text-white shadow-lg shadow-slate-800/25'
|
||||
: 'bg-gradient-to-br from-slate-50 to-slate-100 border border-slate-200/60'
|
||||
}`}
|
||||
>
|
||||
<div className={`text-[10px] font-bold mb-1 ${typeFilter === 'all' ? 'text-slate-300' : 'text-slate-500'}`}>
|
||||
替换建议
|
||||
</div>
|
||||
<div className={`text-2xl font-black ${typeFilter === 'all' ? 'text-white' : 'text-slate-800'}`}>
|
||||
{loading && !data ? '-' : summary?.suggestionCount ?? 0}
|
||||
<span className={`text-[10px] font-normal ml-1 ${typeFilter === 'all' ? 'text-slate-400' : 'text-slate-400'}`}>条</span>
|
||||
</div>
|
||||
<div className={`text-[9px] mt-0.5 ${typeFilter === 'all' ? 'text-slate-400' : 'text-slate-400'}`}>
|
||||
执行后预计 +{summary?.estimatedGain ?? 0} 台达标
|
||||
</div>
|
||||
<button type="button" onClick={() => setTypeFilter('all')} className={typeFilter === 'all' ? 'rounded-2xl ring-2 ring-slate-700 ring-offset-2 ring-offset-[var(--app-bg)]' : 'rounded-2xl'}>
|
||||
<MetricTile icon={Activity} label="替换建议" value={summary?.suggestionCount ?? 0} unit="条" helper={`执行后预计 +${summary?.estimatedGain ?? 0} 台达标`} tone="slate" />
|
||||
</button>
|
||||
|
||||
{/* 近期已干预 — emerald */}
|
||||
<button
|
||||
onClick={() => { setShowHistory(true); setHistoryRecentOnly(true); }}
|
||||
className="p-3.5 rounded-2xl text-left transition-all cursor-pointer bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-200/60"
|
||||
>
|
||||
<div className="text-[10px] font-bold mb-1 text-emerald-600">
|
||||
近期已干预
|
||||
</div>
|
||||
<div className="text-2xl font-black text-emerald-700">
|
||||
{loading && !data ? '-' : summary?.recentInterventionCount ?? 0}
|
||||
<span className="text-[10px] font-normal ml-1 text-emerald-400">条</span>
|
||||
</div>
|
||||
<div className="text-[9px] mt-0.5 text-emerald-400">
|
||||
最近 7 天 · 点击查看
|
||||
</div>
|
||||
<button type="button" onClick={() => { setShowHistory(true); setHistoryRecentOnly(true); }} className="rounded-2xl">
|
||||
<MetricTile icon={SendHorizonal} label="近期已干预" value={summary?.recentInterventionCount ?? 0} unit="条" helper="最近 7 天 · 点击查看" tone="emerald" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -632,8 +557,7 @@ export default function SchedulingModule() {
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RotatingFooterHint className="pb-4" />
|
||||
</div>
|
||||
</PageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user