jerry 1 mese fa
parent
commit
503a9e6de5

+ 2 - 1
docker-compose.yml

@@ -5,12 +5,13 @@ services:
     container_name: next-web-ui
     build:
       context: .
-      dockerfile: Dockerfile
+      dockerfile: Dockerfile    
     restart: always
     ports:
       # 格式: "宿主机IP:宿主机端口:容器端口"
       # 将容器的 3000 映射到宿主机的 3000,且仅限本机访问
       - "127.0.0.1:3000:3000"
     environment:
+      - NEXT_DISABLE_ESLINT=1
       - NODE_ENV=production
       # 如果需要连接同在宿主机上的后端(非Docker),可以使用 host.docker.internal (需配置) 或直接填公网/局域网IP

+ 141 - 33
src/app/admin/slots/page.tsx

@@ -4,16 +4,16 @@ import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { 
   Search, Loader2, Users, Clock, Percent, 
-  AlertTriangle, CheckCircle, ShieldAlert, X, Terminal, Lock
-} from 'lucide-react'; // 新增了 Lock 图标
+  AlertTriangle, CheckCircle, ShieldAlert, X, Terminal,
+  Lock, Settings2 // 引入了新功能的图标
+} from 'lucide-react';
 
-// 引入子组件
+// === 引入子组件 ===
 import ProbabilityManager from '@/components/admin/slots/ProbabilityManager';
 import DailySlotDashboard from '@/components/admin/slots/DailySlotDashboard';
 import DashboardTaskPopup from '@/components/admin/slots/DashboardTaskPopup';
-// === 新增引入 Session 管理组件 ===
 import TroovSessionManager from '@/components/admin/slots/TroovSessionManager';
-
+import TroovBookLimitManager from '@/components/admin/slots/TroovBookLimitManager'; // 引入名额配置组件
 
 // Slot 容量数据类型
 interface SlotItem {
@@ -26,52 +26,62 @@ export default function AdminSlotsPage() {
   const [loading, setLoading] = useState(false);
   
   // === 数据源状态 ===
-  const [slots, setSlots] = useState<SlotItem[]>([]); 
-  const [grabbedTasks, setGrabbedTasks] = useState<any[]>([]); 
+  const [slots, setSlots] = useState<SlotItem[]>([]); // 容量数据
+  const [grabbedTasks, setGrabbedTasks] = useState<any[]>([]); // 当天的任务数据
   
   // 日期选择
   const today = new Date().toISOString().split('T')[0];
   const [searchDate, setSearchDate] = useState(today);
 
   // === 功能面板开关 ===
-  const [showChecker, setShowChecker] = useState(false);
-  const [showProbManager, setShowProbManager] = useState(false);
-  // === 新增:锁单池管理面板开关 ===
-  const [showSessionManager, setShowSessionManager] = useState(false); 
+  const [showSessionManager, setShowSessionManager] = useState(false); // 锁单池管理
+  const [showLimitManager, setShowLimitManager] = useState(false);     // 名额配置
+  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); 
+  const [apiResult, setApiResult] = useState<any>(null); // 存储 API 原始响应
 
   // === 任务详情弹窗状态 ===
   const [selectedTask, setSelectedTask] = useState<any>(null);
   const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
   const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
 
+  // 初始化加载 & 切换日期时加载
   useEffect(() => {
     fetchData();
-  }, [searchDate]); // 修改点:建议把 searchDate 放到依赖里,切换日期自动刷新
+  }, [searchDate]);
 
+  // === 核心数据获取逻辑 ===
   const fetchData = async () => {
     if (!searchDate) return alert("请选择日期");
     setLoading(true);
     
     try {
+      // 并行请求:容量数据 + 任务数据
       const ratePromise = api.get('/api/troov/rate', { 
         params: { date: searchDate } 
       });
       
       const taskPromise = api.get('/api/vas/task/list', { 
-        params: { page: 1, size: 200, status: 'grabbed' } 
+        params: { 
+          page: 1, 
+          size: 200, 
+          status: 'grabbed' 
+        } 
       });
 
       const [rateRes, taskRes] = await Promise.all([ratePromise, taskPromise]);
 
+      // 1. 处理 Slot 容量数据
       const list = Array.isArray(rateRes.data.data) ? rateRes.data.data : (rateRes.data.data || []);
+      // 按时间排序
       list.sort((a: SlotItem, b: SlotItem) => a.time.localeCompare(b.time));
       setSlots(list);
 
+      // 2. 处理任务数据
       const taskList = taskRes.data.data?.items || [];
       setGrabbedTasks(taskList);
 
@@ -84,30 +94,48 @@ export default function AdminSlotsPage() {
     }
   };
 
+  // === 用户冲突检测逻辑 ===
   const handleCheckUser = async (e: React.FormEvent) => {
-    // ... 原有逻辑不变 ...
     e.preventDefault();
-    if (!checkForm.first_name || !checkForm.last_name || !checkForm.birthday) return alert("请填写完整信息");
+    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) {
-      setApiResult({ status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data });
+      console.error(error);
+      setApiResult({
+        status: error.response?.status || 'Network Error',
+        statusText: error.response?.statusText || 'Failed',
+        data: error.response?.data || error.message
+      });
     } finally {
       setCheckLoading(false);
     }
   };
 
+  // === 任务操作处理 ===
   const handleTaskClick = (task: any, target: HTMLElement) => {
-    setAnchorEl(target); 
+    setAnchorEl(target); // 保存锚点
     setSelectedTask(task);
     setIsTaskModalOpen(true);
   };
 
-  const handleTaskSuccess = () => fetchData();
+  const handleTaskSuccess = () => {
+    // 任务操作成功(如标记完成)后,刷新数据
+    fetchData();
+  };
 
+  // === 统计计算 ===
   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;
@@ -122,9 +150,10 @@ export default function AdminSlotsPage() {
           <p className="text-sm text-slate-500 mt-1">综合视图:容量风险 (Rate) + 抢单结果 (Grabbed)</p>
         </div>
 
+        {/* 顶部按钮群 */}
         <div className="flex flex-wrap gap-3 w-full xl:w-auto">
           
-          {/* === 新增:锁单池管理按钮 === */}
+          {/* 锁单池管理按钮 */}
           <button 
             onClick={() => setShowSessionManager(!showSessionManager)}
             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
@@ -132,7 +161,18 @@ export default function AdminSlotsPage() {
             `}
           >
             <Lock size={16} />
-            {showSessionManager ? '关闭锁单池' : '锁单池管理'}
+            {showSessionManager ? '关闭锁单池' : '锁单池'}
+          </button>
+
+          {/* 名额限制配置按钮 */}
+          <button 
+            onClick={() => setShowLimitManager(!showLimitManager)}
+            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
+              ${showLimitManager ? 'bg-teal-50 border-teal-200 text-teal-700' : 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'}
+            `}
+          >
+            <Settings2 size={16} />
+            {showLimitManager ? '关闭名额' : '名额配置'}
           </button>
 
           {/* 用户冲突检测开关 */}
@@ -143,7 +183,7 @@ export default function AdminSlotsPage() {
             `}
           >
             <ShieldAlert size={16} />
-            冲突检测
+            {showChecker ? '关闭检测' : '冲突检测'}
           </button>
 
           {/* 概率管理开关 */}
@@ -154,7 +194,7 @@ export default function AdminSlotsPage() {
             `}
           >
             <Percent size={16} />
-            概率管理
+            {showProbManager ? '关闭概率' : '概率管理'}
           </button>
 
           {/* 日期选择与刷新 */}
@@ -179,28 +219,96 @@ export default function AdminSlotsPage() {
         </div>
       </div>
 
-      {/* === 2.1 新增:锁单池管理面板 === */}
+      {/* === 2. 动态插入的组件面板 === */}
+
+      {/* 2.1 锁单池管理面板 */}
       {showSessionManager && (
         <div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
           <TroovSessionManager />
         </div>
       )}
 
-      {/* === 2.2 概率管理面板 === */}
+      {/* 2.2 预约名额配置面板 */}
+      {showLimitManager && (
+        <div className="mb-8 animate-in slide-in-from-top-4 fade-in duration-300">
+          <TroovBookLimitManager />
+        </div>
+      )}
+
+      {/* 2.3 概率管理面板 */}
       {showProbManager && (
         <div className="mb-8 animate-in fade-in slide-in-from-top-4 duration-300">
           <ProbabilityManager />
         </div>
       )}
 
-      {/* === 3. 用户冲突检测面板 (原逻辑保持不变) === */}
+      {/* 2.4 用户冲突检测面板 */}
       {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">
-          {/* ...原有的 Check user 表单代码不变... */}
+          <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>
+          </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">
+              <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})}
+              />
+            </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>
+          </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>
+          )}
         </div>
       )}
 
-      {/* === 4. 统计看板 === */}
+      {/* === 3. 统计看板 === */}
       <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>
@@ -225,21 +333,21 @@ export default function AdminSlotsPage() {
         </div>
       </div>
 
-      {/* === 5. Daily Slot Dashboard (主视图) === */}
+      {/* === 4. Daily Slot Dashboard (主视图) === */}
       <DailySlotDashboard 
         date={searchDate}
         capacityData={slots}
         grabbedTasks={grabbedTasks}
         loading={loading}
-        onTaskClick={handleTaskClick} 
+        onTaskClick={handleTaskClick} // 绑定点击事件,弹出操作框
       />
 
-      {/* === 任务详情气泡组件 === */}
+      {/* === 5. 任务操作气泡组件 === */}
       <DashboardTaskPopup 
           isOpen={isTaskModalOpen}
           onClose={() => setIsTaskModalOpen(false)}
           task={selectedTask}
-          anchorEl={anchorEl} 
+          anchorEl={anchorEl} // 传入锚点
           onSuccess={handleTaskSuccess}
       />
 

+ 207 - 0
src/components/admin/slots/TroovBookLimitManager.tsx

@@ -0,0 +1,207 @@
+'use client';
+
+import { useState, useEffect, useMemo } from 'react';
+import api from '@/lib/api';
+import { 
+  Loader2, Save, RefreshCw, Settings2, Plus, Trash2, AlertCircle
+} from 'lucide-react';
+import { toast } from 'react-hot-toast'; // 假设你项目中使用了 react-hot-toast,如果没有可以用 alert 替代
+
+export default function TroovBookLimitManager() {
+  const [limits, setLimits] = useState<Record<string, number>>({});
+  const [loading, setLoading] = useState(false);
+  const [saving, setSaving] = useState(false);
+  
+  // 新增时间点的表单状态
+  const [newTime, setNewTime] = useState('');
+  const [newValue, setNewValue] = useState(1);
+
+  useEffect(() => {
+    fetchConfig();
+  }, []);
+
+  // 获取配置
+  const fetchConfig = async () => {
+    setLoading(true);
+    try {
+      const res = await api.get('/api/dynamic-configurations/key/troov.book.limit');
+      let val = res.data?.data?.config_value;
+      
+      // 兼容后端可能返回 JSON 字符串或直接返回对象的情况
+      if (typeof val === 'string') {
+        try { val = JSON.parse(val); } catch (e) { val = {}; }
+      }
+      setLimits(val || {});
+    } catch (error) {
+      console.error('获取配置失败:', error);
+      toast.error('加载名额配置失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 保存配置
+  const handleSave = async () => {
+    setSaving(true);
+    try {
+      // 按照你提供的 curl 示例,PUT 接口需要将 config_value 作为序列化后的 JSON 字符串传入
+      await api.put('/api/dynamic-configurations/key/troov.book.limit', {
+        config_value: JSON.stringify(limits)
+      });
+      toast.success('配置保存成功!');
+      fetchConfig();
+    } catch (error) {
+      console.error('保存配置失败:', error);
+      toast.error('保存配置失败,请重试');
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  // 更新某个时间点的值
+  const handleUpdateLimit = (time: string, value: string) => {
+    const num = parseInt(value, 10);
+    setLimits(prev => ({
+      ...prev,
+      [time]: isNaN(num) ? 0 : Math.max(0, num) // 保证不小于0
+    }));
+  };
+
+  // 删除某个时间点
+  const handleDeleteTime = (time: string) => {
+    if (!confirm(`确定要删除 ${time} 的配置吗?`)) return;
+    const newLimits = { ...limits };
+    delete newLimits[time];
+    setLimits(newLimits);
+  };
+
+  // 新增时间点
+  const handleAddTime = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!newTime) return;
+    
+    // 简单校验格式 HH:mm
+    if (!/^\d{2}:\d{2}$/.test(newTime)) {
+      return toast.error('时间格式需为 HH:mm,如 09:30');
+    }
+    if (limits[newTime] !== undefined) {
+      return toast.error('该时间点已存在!');
+    }
+
+    setLimits(prev => ({ ...prev, [newTime]: newValue }));
+    setNewTime('');
+    setNewValue(1);
+  };
+
+  // 对时间键进行排序 (09:00, 09:15, 10:00...) 方便展示
+  const sortedTimes = useMemo(() => {
+    return Object.keys(limits).sort((a, b) => {
+      const [hA, mA] = a.split(':').map(Number);
+      const [hB, mB] = b.split(':').map(Number);
+      return (hA * 60 + mA) - (hB * 60 + mB);
+    });
+  }, [limits]);
+
+  return (
+    <div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex flex-col">
+      {/* 头部 */}
+      <div className="p-4 border-b border-slate-200 bg-slate-50 flex flex-wrap gap-4 items-center justify-between">
+        <h2 className="font-bold text-slate-800 flex items-center gap-2">
+          <Settings2 size={18} className="text-teal-600" />
+          Troov 预约名额限制配置
+        </h2>
+        
+        <div className="flex items-center gap-3">
+          <button 
+            onClick={fetchConfig} 
+            disabled={loading}
+            className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-slate-600 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition disabled:opacity-50"
+          >
+            <RefreshCw size={14} className={loading ? 'animate-spin' : ''} /> 刷新
+          </button>
+          
+          <button 
+            onClick={handleSave} 
+            disabled={saving}
+            className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-bold text-white bg-teal-600 rounded-lg shadow-sm hover:bg-teal-700 transition disabled:opacity-50"
+          >
+            {saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
+            保存配置
+          </button>
+        </div>
+      </div>
+
+      {/* 主体内容 */}
+      <div className="p-4 md:p-6 bg-slate-50/30">
+        
+        {loading && Object.keys(limits).length === 0 ? (
+          <div className="py-12 flex justify-center text-slate-400">
+            <Loader2 className="animate-spin" size={24} />
+          </div>
+        ) : (
+          <div className="space-y-6">
+            
+            {/* 警告提示 */}
+            <div className="flex items-start gap-2 bg-amber-50 text-amber-800 p-3 rounded-lg border border-amber-200 text-sm">
+              <AlertCircle size={16} className="mt-0.5 shrink-0" />
+              <p>修改此配置将实时影响 Troov 法签机器人对应时间点的<strong>单日最大预约数</strong>。点击右上角“保存配置”后生效。</p>
+            </div>
+
+            {/* 配置网格 */}
+            <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
+              {sortedTimes.map((time) => (
+                <div key={time} className="flex items-center justify-between bg-white border border-slate-200 p-2 rounded-lg shadow-sm group">
+                  <span className="font-mono font-bold text-slate-700 text-sm pl-1">{time}</span>
+                  <div className="flex items-center gap-1">
+                    <input 
+                      type="number" 
+                      min="0"
+                      className="w-14 text-center text-sm font-bold border border-slate-200 rounded p-1 outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-500"
+                      value={limits[time]}
+                      onChange={(e) => handleUpdateLimit(time, e.target.value)}
+                    />
+                    <button 
+                      onClick={() => handleDeleteTime(time)}
+                      className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded transition opacity-0 group-hover:opacity-100"
+                      title="删除时间点"
+                    >
+                      <Trash2 size={14} />
+                    </button>
+                  </div>
+                </div>
+              ))}
+            </div>
+
+            {/* 快速添加时间点 */}
+            <form onSubmit={handleAddTime} className="flex items-center gap-2 pt-4 border-t border-slate-100">
+              <span className="text-sm font-bold text-slate-500">新增时段:</span>
+              <input 
+                type="time" 
+                required
+                className="border border-slate-300 rounded-md px-2 py-1.5 text-sm font-mono outline-none focus:border-teal-500"
+                value={newTime}
+                onChange={(e) => setNewTime(e.target.value)}
+              />
+              <span className="text-sm font-bold text-slate-500">数量:</span>
+              <input 
+                type="number" 
+                min="1"
+                required
+                className="w-16 border border-slate-300 rounded-md px-2 py-1.5 text-sm outline-none focus:border-teal-500 text-center"
+                value={newValue}
+                onChange={(e) => setNewValue(parseInt(e.target.value) || 1)}
+              />
+              <button 
+                type="submit"
+                className="flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm font-medium rounded-md transition"
+              >
+                <Plus size={14} /> 添加
+              </button>
+            </form>
+            
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}

+ 59 - 105
src/components/admin/slots/TroovSessionManager.tsx

@@ -2,46 +2,48 @@
 
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
-import { 
-  Loader2, RefreshCw, Play, Lock, CheckCircle2, XCircle, Search 
-} from 'lucide-react';
+import { Loader2, RefreshCw, Lock, Search } from 'lucide-react';
 
 interface TroovSession {
   session_id: string;
   slot_date: string;
   slot_time: string;
   source: string;
-  status: string;
   created_at: string;
 }
 
 export default function TroovSessionManager() {
   const [sessions, setSessions] = useState<TroovSession[]>([]);
   const [loading, setLoading] = useState(false);
-  const [actionLoading, setActionLoading] = useState<string | null>(null);
 
-  // 筛选与分页状态
-  const [status, setStatus] = useState<string>('pending');
+  // 分页与搜索状态
   const [keyword, setKeyword] = useState<string>('');
   const [page, setPage] = useState(1);
   const [total, setTotal] = useState(0);
-  const size = 10;
+  const size = 100; // 每页 100 条
 
   useEffect(() => {
     fetchSessions();
-  }, [page, status]); // 当页码或状态改变时重新获取
+    // 监听 page 变化,自动触发刷新
+  }, [page]); 
 
   const fetchSessions = async () => {
     setLoading(true);
     try {
-      // 这里的 API 路由请替换为你实际后端的路由前缀
+      // API 请求,传入 keyword(如果为空则传 undefined)
       const res = await api.get('/api/troov-session/list', {
-        params: { page, size, status, keyword: keyword || undefined }
+        params: { 
+          page, 
+          size, 
+          keyword: keyword || undefined 
+        }
       });
       
-      // 适配你的 paginate 返回格式 (假设包含 items 和 total)
       const data = res.data?.data || res.data; 
-      setSessions(data.items || []);
+      const items: TroovSession[] = data.items || [];
+
+      // 直接按数据库返回顺序设置
+      setSessions(items);
       setTotal(data.total || 0);
     } catch (error) {
       console.error('Failed to fetch sessions:', error);
@@ -53,45 +55,10 @@ export default function TroovSessionManager() {
   // 搜索处理
   const handleSearch = (e: React.FormEvent) => {
     e.preventDefault();
-    setPage(1);
-    fetchSessions();
-  };
-
-  // 下发预订指令操作
-  const handleExecuteBooking = async (session: TroovSession) => {
-    if (!confirm(`确定要使用锁定时间 [${session.slot_date} ${session.slot_time}] 进行预订吗?`)) return;
-
-    setActionLoading(session.session_id);
-    try {
-      // 1. 这里调用更新 API 将状态改为 booking 
-      // 或者直接调用你的核心预订业务逻辑 API
-      await api.put(`/api/troov-session/${session.session_id}`, {
-        status: 'booking'
-      });
-      
-      // 2. 刷新列表
+    if (page === 1) {
       fetchSessions();
-      alert('指令下发成功,开始执行预订!');
-    } catch (error) {
-      console.error('Action failed:', error);
-      alert('操作失败');
-    } finally {
-      setActionLoading(null);
-    }
-  };
-
-  // 状态 Badge 颜色渲染
-  const renderStatusBadge = (status: string) => {
-    switch (status) {
-      case 'pending':
-        return <span className="px-2 py-1 text-xs font-bold rounded-md bg-yellow-100 text-yellow-700 flex items-center gap-1 w-max"><Lock size={12}/> 已锁定 (Pending)</span>;
-      case 'booking':
-        return <span className="px-2 py-1 text-xs font-bold rounded-md bg-blue-100 text-blue-700 flex items-center gap-1 w-max"><Loader2 size={12} className="animate-spin"/> 预订中 (Booking)</span>;
-      case 'success':
-        return <span className="px-2 py-1 text-xs font-bold rounded-md bg-green-100 text-green-700 flex items-center gap-1 w-max"><CheckCircle2 size={12}/> 成功</span>;
-      case 'expired':
-      default:
-        return <span className="px-2 py-1 text-xs font-bold rounded-md bg-slate-100 text-slate-500 flex items-center gap-1 w-max"><XCircle size={12}/> 失效</span>;
+    } else {
+      setPage(1); // 切换回第一页会触发 useEffect
     }
   };
 
@@ -104,37 +71,28 @@ export default function TroovSessionManager() {
           锁单池管理 (Session Pool)
         </h2>
         
-        <div className="flex items-center gap-3">
-          <select 
-            className="text-sm border-slate-300 rounded-md shadow-sm outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"
-            value={status}
-            onChange={(e) => { setStatus(e.target.value); setPage(1); }}
-          >
-            <option value="">全部状态</option>
-            <option value="pending">可使用 (Pending)</option>
-            <option value="booking">预订中 (Booking)</option>
-            <option value="expired">已过期 (Expired)</option>
-          </select>
-
-          <form onSubmit={handleSearch} className="flex relative">
+        <div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
+          {/* 关键词搜索框 */}
+          <form onSubmit={handleSearch} className="flex relative flex-1 md:flex-none">
             <input 
               type="text" 
-              placeholder="搜索 Session ID / Source"
-              className="text-sm border border-slate-300 rounded-l-md px-3 py-1.5 outline-none focus:border-indigo-500 focus:z-10"
+              placeholder="搜索 Session / Source"
+              className="w-full md:w-64 text-sm border border-slate-300 rounded-l-md px-3 py-1.5 outline-none focus:border-indigo-500 focus:z-10"
               value={keyword}
               onChange={(e) => setKeyword(e.target.value)}
             />
-            <button type="submit" className="bg-slate-100 border border-l-0 border-slate-300 rounded-r-md px-3 hover:bg-slate-200 text-slate-600">
+            <button type="submit" className="bg-slate-100 border border-l-0 border-slate-300 rounded-r-md px-3 hover:bg-slate-200 text-slate-600 transition">
               <Search size={14} />
             </button>
           </form>
 
+          {/* 刷新按钮 */}
           <button 
             onClick={fetchSessions} 
-            className="p-1.5 text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 rounded transition"
+            className="p-1.5 text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 rounded transition border border-transparent hover:border-indigo-100"
             title="刷新"
           >
-            <RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
+            <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
           </button>
         </div>
       </div>
@@ -146,44 +104,30 @@ export default function TroovSessionManager() {
             <tr>
               <th className="px-4 py-3 whitespace-nowrap">Slot 时间</th>
               <th className="px-4 py-3">Session ID / Source</th>
-              <th className="px-4 py-3">状态</th>
               <th className="px-4 py-3">创建时间</th>
-              <th className="px-4 py-3 text-right">操作</th>
             </tr>
           </thead>
           <tbody className="divide-y divide-slate-100">
             {loading && sessions.length === 0 ? (
-              <tr><td colSpan={5} className="py-8 text-center text-slate-400"><Loader2 className="animate-spin inline mr-2"/> 加载中...</td></tr>
+              <tr><td colSpan={3} className="py-8 text-center text-slate-400"><Loader2 className="animate-spin inline mr-2"/> 加载中...</td></tr>
             ) : sessions.length === 0 ? (
-              <tr><td colSpan={5} className="py-8 text-center text-slate-400">暂无锁单数据</td></tr>
+              <tr><td colSpan={3} className="py-8 text-center text-slate-400">暂无锁单数据</td></tr>
             ) : (
-              sessions.map((item) => (
-                <tr key={item.session_id} className="hover:bg-slate-50 transition-colors">
-                  <td className="px-4 py-3 font-bold text-slate-800">
+              sessions.map((item, index) => (
+                <tr key={`${item.session_id}-${index}`} className="hover:bg-slate-50 transition-colors">
+                  <td className="px-4 py-3 font-bold text-slate-800 whitespace-nowrap">
                     {item.slot_date} <span className="text-indigo-600">{item.slot_time}</span>
                   </td>
-                  <td className="px-4 py-3 font-mono text-xs">
-                    <div className="truncate w-48" title={item.session_id}>{item.session_id.substring(0, 16)}...</div>
-                    <div className="text-slate-400 mt-0.5">{item.source}</div>
+                  <td className="px-4 py-3 font-mono text-xs max-w-lg">
+                    {/* 使用 break-all 使太长的 session_id 完整换行显示 */}
+                    <div className="break-all text-slate-800 leading-relaxed" title={item.session_id}>
+                      {item.session_id}
+                    </div>
+                    <div className="text-slate-400 mt-1">{item.source}</div>
                   </td>
-                  <td className="px-4 py-3">
-                    {renderStatusBadge(item.status)}
-                  </td>
-                  <td className="px-4 py-3 text-xs text-slate-500">
+                  <td className="px-4 py-3 text-xs text-slate-500 whitespace-nowrap">
                     {new Date(item.created_at).toLocaleString()}
                   </td>
-                  <td className="px-4 py-3 text-right">
-                    {item.status === 'pending' && (
-                      <button
-                        onClick={() => handleExecuteBooking(item)}
-                        disabled={actionLoading === item.session_id}
-                        className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 text-white rounded shadow-sm text-xs font-bold hover:bg-indigo-700 disabled:opacity-50 transition"
-                      >
-                        {actionLoading === item.session_id ? <Loader2 size={12} className="animate-spin"/> : <Play size={12}/>}
-                        执行预订
-                      </button>
-                    )}
-                  </td>
                 </tr>
               ))
             )}
@@ -191,17 +135,27 @@ export default function TroovSessionManager() {
         </table>
       </div>
 
-      {/* Pagination Simple View */}
-      {total > size && (
-        <div className="p-3 border-t border-slate-200 bg-slate-50 flex justify-between items-center text-xs text-slate-500">
-          <span>共 {total} 条</span>
-          <div className="flex gap-2">
-            <button disabled={page <= 1} onClick={() => setPage(p => p - 1)} className="px-2 py-1 border rounded bg-white hover:bg-slate-50 disabled:opacity-50">上一页</button>
-            <span className="px-2 py-1">第 {page} 页</span>
-            <button disabled={page * size >= total} onClick={() => setPage(p => p + 1)} className="px-2 py-1 border rounded bg-white hover:bg-slate-50 disabled:opacity-50">下一页</button>
-          </div>
+      {/* Pagination View */}
+      <div className="p-3 border-t border-slate-200 bg-slate-50 flex justify-between items-center text-xs text-slate-500">
+        <span>共 {total} 条</span>
+        <div className="flex items-center gap-2">
+          <button 
+            disabled={page <= 1} 
+            onClick={() => setPage(p => p - 1)} 
+            className="px-2 py-1 border border-slate-300 rounded bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition"
+          >
+            上一页
+          </button>
+          <span className="px-2 py-1 font-medium">第 {page} 页</span>
+          <button 
+            disabled={page * size >= total} 
+            onClick={() => setPage(p => p + 1)} 
+            className="px-2 py-1 border border-slate-300 rounded bg-white hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition"
+          >
+            下一页
+          </button>
         </div>
-      )}
+      </div>
     </div>
   );
 }