|
|
@@ -2,37 +2,23 @@
|
|
|
|
|
|
import { useState } from 'react';
|
|
|
import {
|
|
|
- RotateCcw,
|
|
|
- CheckCircle,
|
|
|
- ChevronDown,
|
|
|
- ChevronUp,
|
|
|
- Terminal,
|
|
|
- FileJson,
|
|
|
- User,
|
|
|
- History,
|
|
|
- Edit,
|
|
|
- AlertCircle
|
|
|
+ RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson,
|
|
|
+ User, History, Edit, AlertCircle, FileText
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
-// 定义任务数据结构 (导出供其他组件使用)
|
|
|
export interface VasTask {
|
|
|
id: number;
|
|
|
order_id: string;
|
|
|
routing_key: string;
|
|
|
- status: string; // pending, grabbed, running, cancelled, completed, failed
|
|
|
+ status: string;
|
|
|
priority: number;
|
|
|
script_version?: string;
|
|
|
attempt_count: number;
|
|
|
notify_count: number;
|
|
|
-
|
|
|
- // JSON 字段
|
|
|
config?: any;
|
|
|
user_inputs?: any;
|
|
|
-
|
|
|
- // 数组字段 (日志)
|
|
|
grabbed_history?: string[];
|
|
|
meta?: string[];
|
|
|
-
|
|
|
created_at: string;
|
|
|
updated_at: string;
|
|
|
expire_at: string;
|
|
|
@@ -43,253 +29,270 @@ interface TaskTableProps {
|
|
|
loading: boolean;
|
|
|
onRetry: (taskId: number) => void;
|
|
|
onManualConfirm: (taskId: number) => void;
|
|
|
- onEdit: (task: VasTask) => void; // 编辑回调
|
|
|
- onViewDetail: (task: VasTask) => void; // 详情回调 (可选,保留以备不时之需)
|
|
|
+ onEdit: (task: VasTask) => void;
|
|
|
+ onViewDetail: (task: VasTask) => void;
|
|
|
}
|
|
|
|
|
|
export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
|
|
|
- // 控制展开行的 ID 集合
|
|
|
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
|
|
|
|
|
const toggleRow = (id: number) => {
|
|
|
const newSet = new Set(expandedRows);
|
|
|
- if (newSet.has(id)) {
|
|
|
- newSet.delete(id);
|
|
|
- } else {
|
|
|
- newSet.add(id);
|
|
|
- }
|
|
|
+ if (newSet.has(id)) newSet.delete(id);
|
|
|
+ else newSet.add(id);
|
|
|
setExpandedRows(newSet);
|
|
|
};
|
|
|
|
|
|
- // 辅助函数:获取数组最后一条
|
|
|
const getLastItem = (arr: any) => {
|
|
|
if (!Array.isArray(arr) || arr.length === 0) return null;
|
|
|
const last = arr[arr.length - 1];
|
|
|
return typeof last === 'string' ? last : JSON.stringify(last);
|
|
|
};
|
|
|
|
|
|
- // 辅助函数:从 UserInputs 提取摘要
|
|
|
const getUserSummary = (inputs: any) => {
|
|
|
if (!inputs) return '-';
|
|
|
const name = inputs.name || inputs.applicant_name || inputs.first_name || inputs.full_name;
|
|
|
const passport = inputs.passport || inputs.passport_no || inputs.passport_number;
|
|
|
-
|
|
|
if (name && passport) return `${name}`;
|
|
|
if (name) return name;
|
|
|
return 'User Data';
|
|
|
};
|
|
|
|
|
|
- if (loading) {
|
|
|
- return (
|
|
|
- <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
|
|
|
- <div className="text-gray-500 text-sm">加载系统任务中...</div>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
+ // 提取状态徽章渲染逻辑
|
|
|
+ const renderStatus = (status: string) => (
|
|
|
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
|
|
|
+ ${status === 'failed' ? 'bg-red-100 text-red-700' :
|
|
|
+ status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' :
|
|
|
+ status === 'completed' ? 'bg-green-100 text-green-700' :
|
|
|
+ 'bg-gray-100 text-gray-600'}`}>
|
|
|
+ {status}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
|
|
|
- if (tasks.length === 0) {
|
|
|
- return (
|
|
|
- <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
|
|
|
- <div className="text-gray-500 text-sm">暂无待处理任务</div>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
+ if (loading) return <div className="p-12 text-center text-gray-500 bg-white rounded-lg border border-slate-200">加载系统任务中...</div>;
|
|
|
+ if (tasks.length === 0) return <div className="p-12 text-center text-gray-500 bg-white rounded-lg border border-slate-200">暂无待处理任务</div>;
|
|
|
|
|
|
return (
|
|
|
- <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
|
|
|
- <div className="overflow-x-auto">
|
|
|
- {/* 使用 table-fixed 强制列宽,防止内容挤压 */}
|
|
|
- <table className="min-w-full text-sm text-left table-fixed">
|
|
|
- <thead className="bg-slate-50 border-b border-slate-200">
|
|
|
- <tr>
|
|
|
- <th className="w-12 px-4 py-3"></th>
|
|
|
- <th className="w-[160px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
|
|
|
- <th className="w-[180px] px-4 py-3 font-medium text-slate-500">Order / User</th>
|
|
|
- <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
|
|
|
- <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
|
|
|
- <th className="w-[100px] px-4 py-3 font-medium text-slate-500">状态</th>
|
|
|
- <th className="w-[140px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
-
|
|
|
- {/*
|
|
|
- 使用多个 tbody 是合法的 HTML 结构,
|
|
|
- 可以完美解决“展开行”导致的样式错位问题。
|
|
|
- */}
|
|
|
- {tasks.map((task) => {
|
|
|
- const isExpanded = expandedRows.has(task.id);
|
|
|
- const lastHistory = getLastItem(task.grabbed_history);
|
|
|
- const lastMeta = getLastItem(task.meta);
|
|
|
-
|
|
|
- return (
|
|
|
- <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
|
|
|
-
|
|
|
- {/* === 主行 === */}
|
|
|
- <tr className="cursor-pointer" onClick={() => toggleRow(task.id)}>
|
|
|
- <td className="px-4 py-4 text-slate-400 align-top">
|
|
|
- {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
|
- </td>
|
|
|
+ <div className="space-y-4">
|
|
|
+
|
|
|
+ {/* ======================= */}
|
|
|
+ {/* 1. Desktop View (Table) */}
|
|
|
+ {/* ======================= */}
|
|
|
+ <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <table className="min-w-full text-sm text-left table-fixed">
|
|
|
+ <thead className="bg-slate-50 border-b border-slate-200">
|
|
|
+ <tr>
|
|
|
+ <th className="w-12 px-4 py-3"></th>
|
|
|
+ <th className="w-[160px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
|
|
|
+ <th className="w-[180px] px-4 py-3 font-medium text-slate-500">Order / User</th>
|
|
|
+ <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
|
|
|
+ <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
|
|
|
+ <th className="w-[100px] px-4 py-3 font-medium text-slate-500">状态</th>
|
|
|
+ <th className="w-[140px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ {tasks.map((task) => {
|
|
|
+ const isExpanded = expandedRows.has(task.id);
|
|
|
+ const lastHistory = getLastItem(task.grabbed_history);
|
|
|
+ const lastMeta = getLastItem(task.meta);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
|
|
|
+ <tr className="cursor-pointer" onClick={() => toggleRow(task.id)}>
|
|
|
+ <td className="px-4 py-4 text-slate-400 align-top">
|
|
|
+ {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-4 align-top">
|
|
|
+ <div className="font-mono text-slate-500 text-xs mb-1">#{task.id}</div>
|
|
|
+ <span className="font-mono text-[10px] text-blue-700 bg-blue-50 px-2 py-1 rounded border border-blue-100 inline-block break-all">
|
|
|
+ {task.routing_key}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-4 align-top">
|
|
|
+ <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
|
|
|
+ <div className="text-xs text-slate-500 flex items-center gap-1 truncate" title={JSON.stringify(task.user_inputs)}>
|
|
|
+ <User size={12} className="text-slate-400 flex-shrink-0" /> {getUserSummary(task.user_inputs)}
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-4 align-top">
|
|
|
+ {lastHistory ? (
|
|
|
+ <div className="text-xs text-slate-700 font-medium break-words line-clamp-3" title={lastHistory}>
|
|
|
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mr-2 mb-0.5"></span>
|
|
|
+ {lastHistory}
|
|
|
+ </div>
|
|
|
+ ) : <span className="text-xs text-gray-300 italic">No history</span>}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-4 align-top">
|
|
|
+ {lastMeta ? (
|
|
|
+ <div className="text-[10px] text-slate-600 font-mono break-all line-clamp-3 bg-slate-100 p-1.5 rounded border border-slate-200" title={lastMeta}>
|
|
|
+ {lastMeta}
|
|
|
+ </div>
|
|
|
+ ) : <span className="text-xs text-gray-300 italic">No logs</span>}
|
|
|
+ <div className="text-[10px] text-slate-300 mt-1 text-right">
|
|
|
+ {new Date(task.updated_at).toLocaleTimeString()}
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-4 align-top">
|
|
|
+ {renderStatus(task.status)}
|
|
|
+ {task.attempt_count > 0 && <div className="text-[10px] text-slate-400 mt-1 pl-1">Retry: {task.attempt_count}</div>}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
|
|
|
+ <div className="flex justify-end gap-1 flex-wrap">
|
|
|
+ <button onClick={() => onEdit(task)} className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-100"><Edit size={16} /></button>
|
|
|
+ <button onClick={() => onRetry(task.id)} className="p-1.5 rounded text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-100"><RotateCcw size={16} /></button>
|
|
|
+ <button onClick={() => onManualConfirm(task.id)} className="p-1.5 rounded text-green-600 hover:bg-green-50 border border-transparent hover:border-green-100"><CheckCircle size={16} /></button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
|
|
|
- {/* 1. ID & Route */}
|
|
|
- <td className="px-4 py-4 align-top">
|
|
|
- <div className="font-mono text-slate-500 text-xs mb-1">#{task.id}</div>
|
|
|
- <span className="font-mono text-[10px] text-blue-700 bg-blue-50 px-2 py-1 rounded border border-blue-100 inline-block break-all">
|
|
|
- {task.routing_key}
|
|
|
- </span>
|
|
|
- </td>
|
|
|
-
|
|
|
- {/* 2. Order & User */}
|
|
|
- <td className="px-4 py-4 align-top">
|
|
|
- <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
|
|
|
- <div className="text-xs text-slate-500 flex items-center gap-1 truncate" title={JSON.stringify(task.user_inputs)}>
|
|
|
- <User size={12} className="text-slate-400 flex-shrink-0" />
|
|
|
- {getUserSummary(task.user_inputs)}
|
|
|
- </div>
|
|
|
- </td>
|
|
|
+ {/* Expanded Detail (Desktop Only) */}
|
|
|
+ {isExpanded && (
|
|
|
+ <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
|
|
|
+ <td colSpan={7} className="px-4 py-4">
|
|
|
+ <TaskDetailContent task={task} />
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ )}
|
|
|
+ </tbody>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 3. 业务进度 (只显示最后一条) */}
|
|
|
- <td className="px-4 py-4 align-top">
|
|
|
- {lastHistory ? (
|
|
|
- <div className="text-xs text-slate-700 font-medium break-words line-clamp-3" title={lastHistory}>
|
|
|
- <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mr-2 mb-0.5"></span>
|
|
|
- {lastHistory}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <span className="text-xs text-gray-300 italic">No history</span>
|
|
|
- )}
|
|
|
- </td>
|
|
|
+ {/* ======================= */}
|
|
|
+ {/* 2. Mobile View (Cards) */}
|
|
|
+ {/* ======================= */}
|
|
|
+ <div className="md:hidden space-y-4">
|
|
|
+ {tasks.map((task) => {
|
|
|
+ const isExpanded = expandedRows.has(task.id);
|
|
|
+ const lastHistory = getLastItem(task.grabbed_history);
|
|
|
|
|
|
- {/* 4. 系统调试 (只显示最后一条) */}
|
|
|
- <td className="px-4 py-4 align-top">
|
|
|
- {lastMeta ? (
|
|
|
- <div className="text-[10px] text-slate-600 font-mono break-all line-clamp-3 bg-slate-100 p-1.5 rounded border border-slate-200" title={lastMeta}>
|
|
|
- {lastMeta}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <span className="text-xs text-gray-300 italic">No logs</span>
|
|
|
- )}
|
|
|
- <div className="text-[10px] text-slate-300 mt-1 text-right">
|
|
|
- {new Date(task.updated_at).toLocaleTimeString()}
|
|
|
- </div>
|
|
|
- </td>
|
|
|
+ return (
|
|
|
+ <div key={task.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
|
|
|
+ {/* Header: ID, Status, Route */}
|
|
|
+ <div className="flex justify-between items-start mb-3">
|
|
|
+ <div className="flex flex-col gap-1">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm font-bold text-slate-900">Task #{task.id}</span>
|
|
|
+ {renderStatus(task.status)}
|
|
|
+ </div>
|
|
|
+ <span className="text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded w-fit">{task.routing_key}</span>
|
|
|
+ </div>
|
|
|
+ {/* Mobile Actions */}
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <button onClick={() => onRetry(task.id)} className="p-2 bg-blue-50 text-blue-600 rounded-lg active:scale-95"><RotateCcw size={18}/></button>
|
|
|
+ <button onClick={() => onManualConfirm(task.id)} className="p-2 bg-green-50 text-green-600 rounded-lg active:scale-95"><CheckCircle size={18}/></button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 5. 状态 */}
|
|
|
- <td className="px-4 py-4 align-top">
|
|
|
- <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
|
|
|
- ${task.status === 'failed' ? 'bg-red-100 text-red-700' :
|
|
|
- task.status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' :
|
|
|
- task.status === 'completed' ? 'bg-green-100 text-green-700' :
|
|
|
- 'bg-gray-100 text-gray-600'}`}>
|
|
|
- {task.status}
|
|
|
- </span>
|
|
|
- {task.attempt_count > 0 && (
|
|
|
- <div className="text-[10px] text-slate-400 mt-1 pl-1">
|
|
|
- Retry: {task.attempt_count}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </td>
|
|
|
+ {/* Info Grid */}
|
|
|
+ <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-3">
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-xs text-slate-400">Order ID</span>
|
|
|
+ <span className="font-mono font-medium">{task.order_id}</span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between">
|
|
|
+ <span className="text-xs text-slate-400">Applicant</span>
|
|
|
+ <span className="font-medium truncate max-w-[150px]">{getUserSummary(task.user_inputs)}</span>
|
|
|
+ </div>
|
|
|
+ <div className="pt-2 border-t border-slate-200 mt-1">
|
|
|
+ <p className="text-xs text-slate-400 mb-1">Latest History:</p>
|
|
|
+ <p className="text-xs text-slate-700 line-clamp-2">{lastHistory || 'No history'}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 6. 操作按钮 */}
|
|
|
- <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
|
|
|
- <div className="flex justify-end gap-1 flex-wrap">
|
|
|
- <button
|
|
|
- onClick={() => onEdit(task)}
|
|
|
- className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 transition border border-transparent hover:border-indigo-100"
|
|
|
- title="编辑任务配置"
|
|
|
- >
|
|
|
- <Edit size={16} />
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={() => onRetry(task.id)}
|
|
|
- className="p-1.5 rounded text-blue-600 hover:bg-blue-50 transition border border-transparent hover:border-blue-100"
|
|
|
- title="重置回队列"
|
|
|
- >
|
|
|
- <RotateCcw size={16} />
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={() => onManualConfirm(task.id)}
|
|
|
- className="p-1.5 rounded text-green-600 hover:bg-green-50 transition border border-transparent hover:border-green-100"
|
|
|
- title="强制完成"
|
|
|
- >
|
|
|
- <CheckCircle size={16} />
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
+ {/* Expand Button */}
|
|
|
+ <button
|
|
|
+ onClick={() => toggleRow(task.id)}
|
|
|
+ className="w-full flex items-center justify-center py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-xs font-medium active:bg-slate-50 transition"
|
|
|
+ >
|
|
|
+ {isExpanded ? <>收起详情 <ChevronUp size={14} className="ml-1"/></> : <>查看日志 & 配置 <ChevronDown size={14} className="ml-1"/></>}
|
|
|
+ </button>
|
|
|
|
|
|
- {/* === 展开详情行 (跨越所有列) === */}
|
|
|
- {isExpanded && (
|
|
|
- <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
|
|
|
- <td colSpan={7} className="px-4 py-4">
|
|
|
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
|
|
|
-
|
|
|
- {/* 左列:配置 & 表单 */}
|
|
|
- <div className="space-y-4">
|
|
|
- <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
|
|
- <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
|
|
|
- <FileJson size={14} /> Config (配置)
|
|
|
- </div>
|
|
|
- <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px]">
|
|
|
- {JSON.stringify(task.config, null, 2)}
|
|
|
- </pre>
|
|
|
- </div>
|
|
|
- <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
|
|
- <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-blue-600 flex items-center gap-2">
|
|
|
- <User size={14} /> User Inputs (用户输入)
|
|
|
- </div>
|
|
|
- <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px]">
|
|
|
- {JSON.stringify(task.user_inputs, null, 2)}
|
|
|
- </pre>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ {/* Expanded Detail (Mobile) */}
|
|
|
+ {isExpanded && (
|
|
|
+ <div className="mt-3 pt-3 border-t border-slate-100">
|
|
|
+ <TaskDetailContent task={task} />
|
|
|
+ <div className="mt-4 pt-4 border-t border-slate-100">
|
|
|
+ <button onClick={() => onEdit(task)} className="w-full flex items-center justify-center py-2.5 bg-indigo-50 text-indigo-600 rounded-lg text-sm font-bold border border-indigo-100">
|
|
|
+ <Edit size={16} className="mr-2"/> 编辑任务配置
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
|
|
|
- {/* 中列:业务历史 (History) */}
|
|
|
- <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
|
|
|
- <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
|
|
|
- <History size={14} /> Grabbed History
|
|
|
- </div>
|
|
|
- <div className="p-3 overflow-y-auto flex-1 font-mono text-slate-700 space-y-2">
|
|
|
- {Array.isArray(task.grabbed_history) && task.grabbed_history.length > 0 ? (
|
|
|
- task.grabbed_history.map((line, i) => (
|
|
|
- <div key={i} className="border-b border-slate-50 last:border-0 pb-1 flex gap-2">
|
|
|
- <span className="text-slate-300 select-none w-6 text-right">{(i+1).toString()}</span>
|
|
|
- <span className="break-all">{line}</span>
|
|
|
- </div>
|
|
|
- ))
|
|
|
- ) : (
|
|
|
- <span className="text-slate-400 italic">暂无业务日志</span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+// === 提取详情内容组件,供 Desktop 和 Mobile 复用 ===
|
|
|
+function TaskDetailContent({ task }: { task: VasTask }) {
|
|
|
+ return (
|
|
|
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
|
|
|
+
|
|
|
+ {/* Config & Inputs */}
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
|
|
+ <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
|
|
|
+ <FileJson size={14} /> Config
|
|
|
+ </div>
|
|
|
+ <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px] whitespace-pre-wrap break-all">
|
|
|
+ {JSON.stringify(task.config, null, 2)}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
|
|
+ <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-blue-600 flex items-center gap-2">
|
|
|
+ <User size={14} /> User Inputs
|
|
|
+ </div>
|
|
|
+ <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px] whitespace-pre-wrap break-all">
|
|
|
+ {JSON.stringify(task.user_inputs, null, 2)}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 右列:调试日志 (Meta) */}
|
|
|
- <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
|
|
|
- <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
|
|
|
- <Terminal size={14} /> Meta / Debug Logs
|
|
|
- </div>
|
|
|
- <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px] space-y-1">
|
|
|
- {Array.isArray(task.meta) && task.meta.length > 0 ? (
|
|
|
- task.meta.map((line, i) => (
|
|
|
- <div key={i} className="break-all border-b border-slate-800/30 pb-1 flex gap-2">
|
|
|
- <span className="text-slate-600 mr-1">$</span>
|
|
|
- <span>{line}</span>
|
|
|
- </div>
|
|
|
- ))
|
|
|
- ) : (
|
|
|
- <span className="text-slate-600 italic">暂无调试信息</span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ {/* History */}
|
|
|
+ <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
|
|
|
+ <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
|
|
|
+ <History size={14} /> Grabbed History
|
|
|
+ </div>
|
|
|
+ <div className="p-3 overflow-y-auto flex-1 font-mono text-slate-700 space-y-2">
|
|
|
+ {Array.isArray(task.grabbed_history) && task.grabbed_history.length > 0 ? (
|
|
|
+ task.grabbed_history.map((line, i) => (
|
|
|
+ <div key={i} className="border-b border-slate-50 last:border-0 pb-1 flex gap-2">
|
|
|
+ <span className="text-slate-300 select-none w-5 text-right">{i+1}</span>
|
|
|
+ <span className="break-all">{line}</span>
|
|
|
+ </div>
|
|
|
+ ))
|
|
|
+ ) : (
|
|
|
+ <span className="text-slate-400 italic">暂无业务日志</span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- )}
|
|
|
- </tbody>
|
|
|
- );
|
|
|
- })}
|
|
|
- </table>
|
|
|
+ {/* Meta/Debug */}
|
|
|
+ <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
|
|
|
+ <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
|
|
|
+ <Terminal size={14} /> Debug Logs
|
|
|
+ </div>
|
|
|
+ <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px] space-y-1">
|
|
|
+ {Array.isArray(task.meta) && task.meta.length > 0 ? (
|
|
|
+ task.meta.map((line, i) => (
|
|
|
+ <div key={i} className="break-all border-b border-slate-800/30 pb-1 flex gap-2">
|
|
|
+ <span className="text-slate-600 mr-1">$</span>
|
|
|
+ <span>{line}</span>
|
|
|
+ </div>
|
|
|
+ ))
|
|
|
+ ) : (
|
|
|
+ <span className="text-slate-600 italic">暂无调试信息</span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+
|
|
|
</div>
|
|
|
);
|
|
|
}
|