page.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. // src/app/admin/tasks/page.tsx
  2. 'use client';
  3. import { useState, useEffect } from 'react';
  4. import api from '@/lib/api';
  5. import { RefreshCw, Search } from 'lucide-react';
  6. // 组件导入
  7. import TaskTable, { VasTask } from '@/components/admin/tasks/TaskTable';
  8. import TaskDetailModal from '@/components/admin/tasks/TaskDetailModal';
  9. import TaskEditModal from '@/components/admin/tasks/TaskEditModal';
  10. import Pagination from '@/components/common/Pagination';
  11. import ExpiringTaskAlert from '@/components/admin/tasks/ExpiringTaskAlert'; // [新增] 紧急任务组件
  12. export default function AdminTasksPage() {
  13. // === 状态定义 ===
  14. const [tasks, setTasks] = useState<VasTask[]>([]);
  15. const [loading, setLoading] = useState(true);
  16. // 分页、搜索、筛选状态
  17. const [page, setPage] = useState(1);
  18. const [pageSize] = useState(10);
  19. const [total, setTotal] = useState(0);
  20. const [keyword, setKeyword] = useState('');
  21. const [statusFilter, setStatusFilter] = useState('all');
  22. // 弹窗状态
  23. const [selectedTask, setSelectedTask] = useState<VasTask | null>(null);
  24. const [isDetailOpen, setIsDetailOpen] = useState(false);
  25. const [isEditOpen, setIsEditOpen] = useState(false);
  26. // === 数据获取逻辑 ===
  27. const fetchTasks = async (targetPage: number = page) => {
  28. setLoading(true);
  29. try {
  30. const params: any = {
  31. keyword,
  32. page: targetPage,
  33. size: pageSize,
  34. };
  35. if (statusFilter !== 'all') {
  36. params.status = statusFilter;
  37. }
  38. const res = await api.get('/api/vas/task/list', { params });
  39. const data = res.data.data || {};
  40. // 兼容两种常见的分页返回格式
  41. if (Array.isArray(data)) {
  42. setTasks(data);
  43. setTotal(data.length);
  44. } else {
  45. setTasks(data.items || []);
  46. setTotal(data.total || 0);
  47. }
  48. setPage(targetPage);
  49. } catch (e) {
  50. console.warn("API Error, maybe using mock data or network failed");
  51. setTasks([]);
  52. setTotal(0);
  53. } finally {
  54. setLoading(false);
  55. }
  56. };
  57. // 监听筛选条件变化,自动刷新
  58. useEffect(() => {
  59. fetchTasks(1);
  60. }, [statusFilter]);
  61. // === 事件处理 ===
  62. const handleSearch = () => {
  63. fetchTasks(1);
  64. };
  65. const handleKeyDown = (e: React.KeyboardEvent) => {
  66. if (e.key === 'Enter') handleSearch();
  67. };
  68. // 重置任务回队列
  69. const handleRetry = async (taskId: number) => {
  70. if(!confirm("确定要重置该任务回队列吗?状态将变为 pending。")) return;
  71. try {
  72. await api.post('/api/vas/task/return_to_queue', null, { params: { task_id: taskId } });
  73. alert("操作成功");
  74. fetchTasks(page);
  75. } catch (e) {
  76. alert("操作失败");
  77. }
  78. };
  79. // 强制标记完成
  80. const handleConfirm = async (taskId: number) => {
  81. if(!confirm("确定要强制标记完成吗?这将跳过后续脚本执行。")) return;
  82. try {
  83. await api.post('/api/vas/task/manual_confirm', null, { params: { task_id: taskId } });
  84. alert("操作成功");
  85. fetchTasks(page);
  86. } catch (e) {
  87. alert("操作失败");
  88. }
  89. };
  90. // 打开详情弹窗
  91. const handleViewDetail = (task: VasTask) => {
  92. setSelectedTask(task);
  93. setIsDetailOpen(true);
  94. };
  95. // 打开编辑弹窗
  96. const handleEdit = (task: VasTask) => {
  97. setSelectedTask(task);
  98. setIsEditOpen(true);
  99. };
  100. // 提交编辑
  101. const handleSubmitEdit = async (taskId: number, data: any) => {
  102. try {
  103. await api.post('/api/vas/task/update', data, {params: {"id": taskId}});
  104. alert("任务更新成功");
  105. setIsEditOpen(false);
  106. fetchTasks(page); // 刷新列表
  107. } catch (e: any) {
  108. alert("更新失败: " + (e.response?.data?.message || "未知错误"));
  109. }
  110. };
  111. return (
  112. <div className="p-4 md:p-6">
  113. {/* === 1. 头部区域 === */}
  114. <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
  115. {/* 标题 */}
  116. <div>
  117. <h1 className="text-2xl font-bold text-slate-800">系统任务队列</h1>
  118. <p className="text-sm text-slate-500 mt-1">监控机器人执行状态及调试日志</p>
  119. </div>
  120. {/* 操作区:筛选、搜索、刷新 */}
  121. <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
  122. {/* 状态筛选下拉框 */}
  123. <select
  124. className="w-full sm:w-auto border border-slate-300 rounded-lg text-sm px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500 bg-white appearance-none cursor-pointer"
  125. value={statusFilter}
  126. onChange={(e) => setStatusFilter(e.target.value)}
  127. >
  128. <option value="all">所有状态 (All)</option>
  129. <option value="pending">等待中 (Pending)</option>
  130. <option value="running">运行中 (Running)</option>
  131. <option value="grabbed">待确认 (Grabbed)</option>
  132. <option value="completed">已完成 (Completed)</option>
  133. <option value="cancelled">已取消 (Cancelled)</option>
  134. </select>
  135. {/* 搜索框组 */}
  136. <div className="flex gap-2 w-full sm:w-auto">
  137. <div className="relative flex-1 sm:w-64">
  138. <input
  139. type="text"
  140. placeholder="Order / Route / User Inputs"
  141. className="w-full pl-9 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
  142. value={keyword}
  143. onChange={e => setKeyword(e.target.value)}
  144. onKeyDown={handleKeyDown}
  145. />
  146. <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
  147. </div>
  148. <button
  149. onClick={handleSearch}
  150. className="flex-shrink-0 p-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-600 transition"
  151. title="刷新列表"
  152. >
  153. <RefreshCw size={18} />
  154. </button>
  155. </div>
  156. </div>
  157. </div>
  158. {/* === [新增] 2. 紧急任务预警区域 === */}
  159. {/* 如果有即将过期的任务,这个组件会自动显示;否则不渲染 */}
  160. <ExpiringTaskAlert
  161. onViewDetail={handleViewDetail}
  162. onEdit={handleEdit}
  163. />
  164. {/* === 3. 主表格区域 === */}
  165. <TaskTable
  166. tasks={tasks}
  167. loading={loading}
  168. onRetry={handleRetry}
  169. onManualConfirm={handleConfirm}
  170. onViewDetail={handleViewDetail}
  171. onEdit={handleEdit}
  172. />
  173. {/* === 4. 分页区域 === */}
  174. <div className="mt-4">
  175. <Pagination
  176. currentPage={page}
  177. total={total}
  178. pageSize={pageSize}
  179. onPageChange={(p) => fetchTasks(p)}
  180. />
  181. </div>
  182. {/* === 5. 弹窗组件 === */}
  183. <TaskDetailModal
  184. isOpen={isDetailOpen}
  185. onClose={() => setIsDetailOpen(false)}
  186. task={selectedTask}
  187. />
  188. <TaskEditModal
  189. isOpen={isEditOpen}
  190. onClose={() => setIsEditOpen(false)}
  191. task={selectedTask}
  192. onSubmit={handleSubmitEdit}
  193. />
  194. </div>
  195. );
  196. }