瀏覽代碼

feat: update

jerry 3 月之前
父節點
當前提交
ea862d5583
共有 2 個文件被更改,包括 291 次插入214 次删除
  1. 88 214
      src/app/admin/slots/page.tsx
  2. 203 0
      src/components/admin/slots/DailySlotDashboard.tsx

+ 88 - 214
src/app/admin/slots/page.tsx

@@ -3,11 +3,13 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { 
-  Search, Loader2, Users, Clock, Percent, 
-  AlertTriangle, CheckCircle, ShieldAlert, X, Terminal 
+  Search, Loader2, Clock, Percent, ShieldAlert, X, Terminal, CalendarCheck
 } from 'lucide-react';
-import ProbabilityManager from '@/components/admin/slots/ProbabilityManager'; // 引入概率管理组件
+import ProbabilityManager from '@/components/admin/slots/ProbabilityManager';
+// 1. 引入新组件
+import DailySlotDashboard from '@/components/admin/slots/DailySlotDashboard';
 
+// 复用类型定义
 interface SlotItem {
   time: string;
   rate: string | number;
@@ -15,36 +17,62 @@ interface SlotItem {
 }
 
 export default function AdminSlotsPage() {
-  // === Slot List States ===
   const [loading, setLoading] = useState(false);
+  
+  // 数据源
   const [slots, setSlots] = useState<SlotItem[]>([]);
+  const [grabbedTasks, setGrabbedTasks] = useState<any[]>([]); // 存储已抢任务
+  
   const today = new Date().toISOString().split('T')[0];
   const [searchDate, setSearchDate] = useState(today);
 
-  // === User Check States ===
+  // 功能开关
   const [showChecker, setShowChecker] = useState(false);
+  const [showProbManager, setShowProbManager] = useState(false);
+  
+  // 冲突检测状态
   const [checkLoading, setCheckLoading] = useState(false);
   const [checkForm, setCheckForm] = useState({ first_name: '', last_name: '', birthday: '' });
   const [apiResult, setApiResult] = useState<any>(null);
 
-  // === Prob Manager State ===
-  const [showProbManager, setShowProbManager] = useState(false);
-
   useEffect(() => {
-    fetchSlots();
-  }, []);
+    fetchData();
+  }, []); // 初始化
 
-  const fetchSlots = async () => {
+  // === 核心修改:同时获取容量和任务数据 ===
+  const fetchData = async () => {
     if (!searchDate) return alert("请选择日期");
     setLoading(true);
     try {
-      const res = await api.get('/api/troov/rate', { params: { date: searchDate } });
-      const list = Array.isArray(res.data.data) ? res.data.data : (res.data.data || []);
-      list.sort((a: SlotItem, b: SlotItem) => a.time.localeCompare(b.time));
-      setSlots(list);
+      // 1. 获取容量 (Rate)
+      const ratePromise = api.get('/api/troov/rate', { params: { date: searchDate } });
+      
+      // 2. 获取已抢任务 (Grabbed Tasks)
+      // 注意:这里我们获取 status=grabbed 的任务。
+      // 由于后端暂时不支持按日期筛选任务,我们先获取最近的 200 条,然后在前端 (DailySlotDashboard) 进行日期匹配。
+      // *理想情况是后端 task/list 接口支持 date 参数过滤*
+      const taskPromise = api.get('/api/vas/task/list', { 
+        params: { 
+          page: 1, 
+          size: 200, // 尽量多取一点以覆盖当天
+          status: 'grabbed',
+          // 如果需要特定 routing_key,可以在这里加,或者在组件内过滤
+        } 
+      });
+
+      const [rateRes, taskRes] = await Promise.all([ratePromise, taskPromise]);
+
+      // 处理 Capacity 数据
+      const slotList = Array.isArray(rateRes.data.data) ? rateRes.data.data : (rateRes.data.data || []);
+      setSlots(slotList);
+
+      // 处理 Task 数据
+      const taskList = taskRes.data.data?.items || [];
+      setGrabbedTasks(taskList);
+
     } catch (e) {
-      console.warn("API Error, using mock data");
-      setSlots([]);
+      console.warn("API Error", e);
+      // Mock data logic can be added here if needed
     } finally {
       setLoading(false);
     }
@@ -52,25 +80,15 @@ export default function AdminSlotsPage() {
 
   const handleCheckUser = async (e: React.FormEvent) => {
     e.preventDefault();
-    if (!checkForm.first_name || !checkForm.last_name || !checkForm.birthday) {
-      return alert("请填写完整信息");
-    }
-
     setCheckLoading(true);
     setApiResult(null);
-
     try {
       const res = await api.post('/api/troov/book', checkForm);
-      setApiResult({
-        status: res.status,
-        statusText: res.statusText,
-        data: res.data
-      });
+      setApiResult({ status: res.status, statusText: res.statusText, data: res.data });
     } catch (error: any) {
-      console.error(error);
       setApiResult({
-        status: error.response?.status || 'Network Error',
-        statusText: error.response?.statusText || 'Failed',
+        status: error.response?.status || 'Error',
+        statusText: 'Failed',
         data: error.response?.data || error.message
       });
     } finally {
@@ -78,239 +96,95 @@ export default function AdminSlotsPage() {
     }
   };
 
-  const totalCapacity = slots.reduce((acc, curr) => acc + curr.capacity, 0);
-  const validSlots = slots.filter(s => s.capacity > 0 && Number(s.rate) <= 100).length;
-  const riskSlots = slots.filter(s => Number(s.rate) > 100).length;
-
   return (
     <div className="p-4 md:p-6">
       
-      {/* === 1. 顶部 Header & 操作栏 === */}
-      <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
+      {/* Header & Controls */}
+      <div className="flex flex-col xl:flex-row justify-between items-start xl:items-center mb-6 gap-4">
         <div>
           <h1 className="text-2xl font-bold text-slate-800">Slot 监控与风险预警</h1>
-          <p className="text-sm text-slate-500 mt-1">查询预约名额容量(Capacity)及使用率(Rate)</p>
+          <p className="text-sm text-slate-500 mt-1">综合视图:容量风险 (Rate) + 抢单结果 (Grabbed)</p>
         </div>
 
-        <div className="flex flex-wrap gap-3 w-full md:w-auto">
-          {/* 用户检测按钮 */}
+        <div className="flex flex-wrap gap-3 w-full xl:w-auto">
+          
           <button 
             onClick={() => setShowChecker(!showChecker)}
-            className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition whitespace-nowrap
+            className={`flex items-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition
               ${showChecker ? 'bg-orange-50 border-orange-200 text-orange-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
             `}
           >
-            <ShieldAlert size={16} />
-            {showChecker ? '关闭检测' : '用户冲突检测'}
+            <ShieldAlert size={16} /> {showChecker ? '关闭检测' : '冲突检测'}
           </button>
 
-          {/* 概率管理按钮 */}
           <button 
             onClick={() => setShowProbManager(!showProbManager)}
-            className={`flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition whitespace-nowrap
+            className={`flex items-center gap-2 px-4 py-2 border rounded-lg text-sm font-bold transition
               ${showProbManager ? 'bg-purple-50 border-purple-200 text-purple-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
             `}
           >
-            <Percent size={16} />
-            {showProbManager ? '关闭概率管理' : '概率管理'}
+            <Percent size={16} /> {showProbManager ? '关闭概率' : '概率管理'}
           </button>
 
-          {/* 日期查询 */}
-          <div className="flex items-center gap-2 bg-white p-1 rounded-lg border border-slate-200 shadow-sm w-full md:w-auto">
-            <div className="relative flex-1 md:flex-none">
+          <div className="flex items-center gap-2 bg-white p-1 rounded-lg border border-slate-200 shadow-sm">
+            <div className="relative">
               <input 
                 type="date" 
-                className="w-full pl-8 pr-2 py-1.5 text-sm border-none outline-none bg-transparent text-slate-700 font-medium"
+                className="pl-8 pr-2 py-1.5 text-sm border-none outline-none bg-transparent text-slate-700 font-medium"
                 value={searchDate}
                 onChange={(e) => setSearchDate(e.target.value)}
               />
               <Clock size={14} className="absolute left-2.5 top-2.5 text-slate-400" />
             </div>
-            <button 
-              onClick={fetchSlots}
-              disabled={loading}
-              className="p-1.5 bg-slate-900 text-white rounded-md hover:bg-slate-800 transition disabled:opacity-70"
-            >
+            <button onClick={fetchData} disabled={loading} className="p-1.5 bg-slate-900 text-white rounded-md hover:bg-slate-800 transition disabled:opacity-70">
               {loading ? <Loader2 size={16} className="animate-spin" /> : <Search size={16} />}
             </button>
           </div>
         </div>
       </div>
 
-      {/* === 2. 概率管理面板 (Probability Manager) === */}
-      {showProbManager && (
-        <div className="mb-8 animate-in fade-in slide-in-from-top-4 duration-300">
-          <ProbabilityManager />
-        </div>
-      )}
-
-      {/* === 3. 用户冲突检测面板 (User Check) === */}
+      {/* Panels */}
+      {showProbManager && <div className="mb-8 animate-in fade-in slide-in-from-top-4"><ProbabilityManager /></div>}
+      
       {showChecker && (
-        <div className="mb-8 bg-orange-50/50 border border-orange-200 rounded-xl p-6 animate-in slide-in-from-top-2 fade-in duration-300">
+        <div className="mb-8 bg-orange-50/50 border border-orange-200 rounded-xl p-6 animate-in slide-in-from-top-2">
           <div className="flex justify-between items-start mb-4">
-            <h3 className="font-bold text-orange-900 flex items-center gap-2">
-              <ShieldAlert size={20} /> 预订资格预检 API (Raw Response)
-            </h3>
-            <button onClick={() => setShowChecker(false)} className="text-orange-400 hover:text-orange-600"><X size={20}/></button>
+            <h3 className="font-bold text-orange-900 flex items-center gap-2"><ShieldAlert size={20} /> 预订资格预检 (Raw)</h3>
+            <button onClick={() => setShowChecker(false)}><X size={20} className="text-orange-400"/></button>
           </div>
-
           <form onSubmit={handleCheckUser} className="flex flex-col md:flex-row gap-4 items-end mb-4">
-            <div className="flex-1 w-full md:w-auto">
-              <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">First Name</label>
-              <input 
-                required type="text" placeholder="e.g. Hongping"
-                className="w-full border border-orange-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-orange-400 outline-none"
-                value={checkForm.first_name} onChange={e => setCheckForm({...checkForm, first_name: e.target.value})}
-              />
-            </div>
-            <div className="flex-1 w-full md:w-auto">
-              <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">Last Name</label>
-              <input 
-                required type="text" placeholder="e.g. Liu"
-                className="w-full border border-orange-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-orange-400 outline-none"
-                value={checkForm.last_name} onChange={e => setCheckForm({...checkForm, last_name: e.target.value})}
-              />
-            </div>
-            <div className="flex-1 w-full md:w-auto">
+            {['first_name', 'last_name'].map(field => (
+              <div key={field} className="flex-1 w-full">
+                <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">{field.replace('_', ' ')}</label>
+                <input required type="text" className="w-full border border-orange-200 rounded-lg p-2.5 text-sm outline-none focus:ring-2 focus:ring-orange-400"
+                  value={(checkForm as any)[field]} onChange={e => setCheckForm({...checkForm, [field]: e.target.value})} />
+              </div>
+            ))}
+            <div className="flex-1 w-full">
               <label className="block text-xs font-bold text-orange-800 mb-1 uppercase">Birthday</label>
-              <input 
-                required type="date"
-                className="w-full border border-orange-200 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-orange-400 outline-none"
-                value={checkForm.birthday} onChange={e => setCheckForm({...checkForm, birthday: e.target.value})}
-              />
+              <input required type="date" className="w-full border border-orange-200 rounded-lg p-2.5 text-sm outline-none focus:ring-2 focus:ring-orange-400"
+                value={checkForm.birthday} onChange={e => setCheckForm({...checkForm, birthday: e.target.value})} />
             </div>
-            <button 
-              type="submit" 
-              disabled={checkLoading}
-              className="w-full md:w-auto px-6 py-2.5 bg-orange-600 text-white rounded-lg font-bold hover:bg-orange-700 transition disabled:opacity-70 shadow-sm flex items-center justify-center gap-2"
-            >
-              {checkLoading ? <Loader2 size={16} className="animate-spin" /> : <Terminal size={16} />}
-              调用 API
+            <button type="submit" disabled={checkLoading} className="px-6 py-2.5 bg-orange-600 text-white rounded-lg font-bold hover:bg-orange-700 transition disabled:opacity-70 flex items-center gap-2">
+              {checkLoading ? <Loader2 size={16} className="animate-spin"/> : <Terminal size={16}/>} 调用 API
             </button>
           </form>
-
-          {/* Raw Response Viewer */}
           {apiResult && (
-            <div className="bg-slate-900 rounded-lg overflow-hidden border border-slate-700 shadow-inner">
-              <div className="bg-slate-800 px-4 py-2 flex justify-between items-center text-xs text-slate-300 border-b border-slate-700">
-                <span className="font-mono">Response</span>
-                <span className={`font-bold px-2 py-0.5 rounded ${apiResult.status >= 200 && apiResult.status < 300 ? 'bg-green-900 text-green-300' : 'bg-red-900 text-red-300'}`}>
-                  HTTP {apiResult.status}
-                </span>
-              </div>
-              <div className="p-4 overflow-x-auto">
-                <pre className="text-xs font-mono text-green-400 whitespace-pre-wrap break-all">
-                  {JSON.stringify(apiResult.data, null, 2)}
-                </pre>
-              </div>
+            <div className="bg-slate-900 rounded-lg overflow-hidden border border-slate-700 p-4 overflow-x-auto">
+              <pre className="text-xs font-mono text-green-400 whitespace-pre-wrap break-all">{JSON.stringify(apiResult, null, 2)}</pre>
             </div>
           )}
         </div>
       )}
 
-      {/* === 4. 统计看板 === */}
-      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
-        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
-          <div>
-            <p className="text-xs text-slate-400 font-bold uppercase mb-1">总释放名额</p>
-            <p className="text-2xl font-bold text-slate-800">{totalCapacity}</p>
-          </div>
-          <div className="p-3 bg-blue-50 text-blue-600 rounded-full"><Users size={20} /></div>
-        </div>
-        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
-          <div>
-            <p className="text-xs text-slate-400 font-bold uppercase mb-1">有效时间点 (Safe)</p>
-            <p className="text-2xl font-bold text-green-600">{validSlots}</p>
-          </div>
-          <div className="p-3 bg-green-50 text-green-600 rounded-full"><CheckCircle size={20} /></div>
-        </div>
-        <div className="bg-white px-5 py-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
-          <div>
-            <p className="text-xs text-slate-400 font-bold uppercase mb-1">高风险 (Rate &gt; 100)</p>
-            <p className="text-2xl font-bold text-red-600">{riskSlots}</p>
-          </div>
-          <div className="p-3 bg-red-50 text-red-600 rounded-full"><AlertTriangle size={20} /></div>
-        </div>
-      </div>
-
-      {/* === 5. Slot 列表 === */}
-      {loading ? (
-        <div className="py-20 text-center text-slate-400 flex flex-col items-center">
-          <Loader2 size={32} className="animate-spin mb-3" />
-          正在同步 TROOV 数据...
-        </div>
-      ) : slots.length === 0 ? (
-        <div className="py-20 text-center border-2 border-dashed border-slate-200 rounded-xl bg-slate-50 text-slate-400">
-          该日期暂无记录
-        </div>
-      ) : (
-        <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
-          {slots.map((slot, index) => {
-            const rateVal = Number(slot.rate);
-            const isRisk = rateVal > 100;
-            const hasCapacity = slot.capacity > 0;
-
-            let cardStyle = "bg-white border-slate-200 opacity-60"; 
-            let textStyle = "text-slate-400";
-            
-            if (isRisk) {
-              cardStyle = "bg-red-50 border-red-300 shadow-sm ring-1 ring-red-200";
-              textStyle = "text-red-700";
-            } else if (hasCapacity) {
-              cardStyle = "bg-emerald-50 border-emerald-300 shadow-sm hover:shadow-md transition-all";
-              textStyle = "text-emerald-800";
-            }
+      {/* === New: Unified Dashboard View === */}
+      <DailySlotDashboard 
+        date={searchDate}
+        capacityData={slots}
+        grabbedTasks={grabbedTasks}
+        loading={loading}
+      />
 
-            return (
-              <div 
-                key={index}
-                className={`relative p-4 rounded-xl border flex flex-col justify-between h-36 ${cardStyle}`}
-              >
-                <div className="flex justify-between items-start">
-                  <span className={`text-xl font-mono font-bold ${textStyle}`}>
-                    {slot.time}
-                  </span>
-                  {isRisk && (
-                    <AlertTriangle size={18} className="text-red-500 animate-pulse" />
-                  )}
-                </div>
-
-                <div className="space-y-2">
-                  <div className="flex items-center justify-between text-xs">
-                    <div className="flex items-center gap-1 opacity-80">
-                      <Percent size={12} />
-                      <span>使用率</span>
-                    </div>
-                    <span className={`font-bold ${isRisk ? 'text-red-600' : 'text-slate-600'}`}>
-                      {rateVal}%
-                    </span>
-                  </div>
-                  
-                  <div className="w-full bg-black/5 rounded-full h-1.5 overflow-hidden">
-                    <div 
-                      className={`h-full rounded-full ${isRisk ? 'bg-red-500' : 'bg-emerald-500'}`} 
-                      style={{ width: `${Math.min(rateVal, 100)}%` }}
-                    ></div>
-                  </div>
-
-                  <div className={`flex items-center justify-between text-sm font-bold pt-1 border-t border-black/5 ${textStyle}`}>
-                    <span className="text-xs opacity-80 font-normal">余号</span>
-                    <span className="flex items-center gap-1">
-                      <Users size={14} /> {slot.capacity}
-                    </span>
-                  </div>
-                </div>
-
-                {isRisk && (
-                  <div className="absolute -top-2 -right-2 bg-red-600 text-white text-[10px] px-2 py-0.5 rounded-full shadow-sm font-bold">
-                    易取消
-                  </div>
-                )}
-              </div>
-            );
-          })}
-        </div>
-      )}
     </div>
   );
 }

+ 203 - 0
src/components/admin/slots/DailySlotDashboard.tsx

@@ -0,0 +1,203 @@
+'use client';
+
+import { useState, useMemo } from 'react';
+import { 
+  Users, AlertTriangle, CheckCircle, Clock, 
+  ChevronDown, ChevronUp, User, Mail, Hash 
+} from 'lucide-react';
+import LocalTime from '@/components/common/LocalTime';
+
+// 定义 props 类型
+interface SlotCapacity {
+  time: string; // "09:00"
+  rate: string | number;
+  capacity: number;
+}
+
+interface GrabbedTask {
+  id: number;
+  order_id: string;
+  grabbed_history?: {
+    book_date?: string; // ISO String
+  };
+  user_inputs?: {
+    first_name?: string;
+    last_name?: string;
+    email?: string;
+  };
+}
+
+interface DailySlotDashboardProps {
+  date: string; // 当前选中的日期 YYYY-MM-DD
+  capacityData: SlotCapacity[];
+  grabbedTasks: GrabbedTask[];
+  loading: boolean;
+}
+
+export default function DailySlotDashboard({ date, capacityData, grabbedTasks, loading }: DailySlotDashboardProps) {
+  const [expandedTime, setExpandedTime] = useState<string | null>(null);
+
+  // === 核心逻辑:数据合并 ===
+  const mergedSlots = useMemo(() => {
+    // 1. 创建基础的时间轴 Map
+    const slotMap = new Map<string, { 
+      capacityInfo: SlotCapacity | null; 
+      tasks: GrabbedTask[] 
+    }>();
+
+    // 2. 填充容量数据
+    capacityData.forEach(item => {
+      slotMap.set(item.time, { capacityInfo: item, tasks: [] });
+    });
+
+    // 3. 填充已抢任务数据
+    grabbedTasks.forEach(task => {
+      const bookDateStr = task.grabbed_history?.book_date;
+      if (!bookDateStr) return;
+
+      // 解析时间,假设格式为 ISO,提取 HH:mm
+      // 注意:这里需要确保时区处理正确,或者后端返回的就是当地时间字符串
+      const bookDate = new Date(bookDateStr);
+      // 简单比对日期字符串是否匹配当前选中的 date
+      const taskDateStr = bookDate.toLocaleDateString('en-CA'); // YYYY-MM-DD
+      
+      if (taskDateStr === date) {
+         const timeKey = bookDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
+         
+         if (!slotMap.has(timeKey)) {
+           // 如果该时间点没有容量数据(可能是隐藏Slot),也初始化它
+           slotMap.set(timeKey, { capacityInfo: null, tasks: [] });
+         }
+         slotMap.get(timeKey)!.tasks.push(task);
+      }
+    });
+
+    // 4. 转数组并排序
+    return Array.from(slotMap.entries())
+      .map(([time, data]) => ({ time, ...data }))
+      .sort((a, b) => a.time.localeCompare(b.time));
+
+  }, [date, capacityData, grabbedTasks]);
+
+  // 渲染加载状态
+  if (loading) {
+    return (
+      <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4 animate-pulse">
+        {[1, 2, 3, 4, 5, 6, 7, 8].map(i => (
+          <div key={i} className="h-40 bg-slate-200 rounded-xl"></div>
+        ))}
+      </div>
+    );
+  }
+
+  if (mergedSlots.length === 0) {
+    return (
+      <div className="py-20 text-center border-2 border-dashed border-slate-200 rounded-xl bg-slate-50 text-slate-400">
+        该日期 ({date}) 暂无 Slot 容量数据或抓取记录
+      </div>
+    );
+  }
+
+  return (
+    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
+      {mergedSlots.map(({ time, capacityInfo, tasks }) => {
+        const rateVal = capacityInfo ? Number(capacityInfo.rate) : 0;
+        const isHighRisk = rateVal > 100; // 风险阈值
+        const hasCapacity = capacityInfo && capacityInfo.capacity > 0;
+        const grabCount = tasks.length;
+        
+        // 样式判定
+        let borderColor = "border-slate-200";
+        let bgColor = "bg-white";
+        let headerColor = "bg-slate-50 text-slate-700";
+
+        if (isHighRisk) {
+          borderColor = "border-red-200 ring-1 ring-red-100";
+          bgColor = "bg-red-50/30";
+          headerColor = "bg-red-100 text-red-800";
+        } else if (hasCapacity) {
+          borderColor = "border-emerald-200";
+          bgColor = "bg-emerald-50/30";
+          headerColor = "bg-emerald-100 text-emerald-800";
+        }
+
+        return (
+          <div key={time} className={`rounded-xl border ${borderColor} ${bgColor} overflow-hidden transition-all hover:shadow-md flex flex-col`}>
+            
+            {/* --- 卡片头部:时间 & 宏观指标 --- */}
+            <div className={`px-4 py-3 flex justify-between items-center ${headerColor}`}>
+              <div className="flex items-center gap-2">
+                <Clock size={16} />
+                <span className="text-lg font-mono font-bold">{time}</span>
+              </div>
+              
+              {/* 容量/风险指示器 */}
+              {capacityInfo ? (
+                <div className="flex items-center gap-3 text-xs">
+                  <div className="flex flex-col items-end">
+                    <span className="font-bold">{capacityInfo.capacity} Slots</span>
+                    <span className={`${isHighRisk ? 'text-red-600 font-bold' : 'opacity-70'}`}>
+                      {rateVal}% Rate
+                    </span>
+                  </div>
+                  {isHighRisk && <AlertTriangle size={18} className="text-red-600 animate-pulse" />}
+                </div>
+              ) : (
+                <span className="text-xs opacity-50">Unknown Cap</span>
+              )}
+            </div>
+
+            {/* --- 卡片主体:抢单结果 --- */}
+            <div className="p-4 flex-1 flex flex-col">
+              
+              {/* 抢单统计摘要 */}
+              <div className="flex justify-between items-center mb-3">
+                <span className="text-xs font-bold text-slate-500 uppercase tracking-wide">Grabbed Result</span>
+                <span className={`text-xs font-bold px-2 py-0.5 rounded-full ${grabCount > 0 ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-400'}`}>
+                  {grabCount} Users
+                </span>
+              </div>
+
+              {/* 用户列表 (限制高度,可滚动) */}
+              <div className="space-y-2 max-h-[200px] overflow-y-auto pr-1 custom-scrollbar">
+                {grabCount === 0 ? (
+                  <div className="text-center py-4 text-xs text-slate-400 italic">
+                    无人抢到该时段
+                  </div>
+                ) : (
+                  tasks.map(task => (
+                    <div key={task.id} className="bg-white border border-slate-100 p-2.5 rounded-lg shadow-sm">
+                      <div className="flex justify-between items-start">
+                        <div className="flex items-center gap-1.5 font-bold text-slate-700 text-sm">
+                          <User size={12} className="text-blue-500" />
+                          <span className="truncate max-w-[100px]" title={`${task.user_inputs?.first_name} ${task.user_inputs?.last_name}`}>
+                            {task.user_inputs?.first_name} {task.user_inputs?.last_name}
+                          </span>
+                        </div>
+                        <span className="text-[10px] font-mono text-slate-400">#{task.id}</span>
+                      </div>
+                      
+                      <div className="flex items-center gap-1.5 mt-1 text-xs text-slate-500">
+                        <Mail size={10} />
+                        <span className="truncate max-w-[140px]">{task.user_inputs?.email}</span>
+                      </div>
+
+                      {/* 如果是高风险时段,给具体用户打标记 */}
+                      {isHighRisk && (
+                        <div className="mt-1.5 pt-1.5 border-t border-slate-50 flex items-center gap-1 text-[10px] text-red-600 font-medium">
+                          <AlertTriangle size={10} />
+                          <span>位于高风险区间,留意取消</span>
+                        </div>
+                      )}
+                    </div>
+                  ))
+                )}
+              </div>
+            </div>
+
+          </div>
+        );
+      })}
+    </div>
+  );
+}