TaskList.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <template>
  2. <div class="app-container">
  3. <el-card>
  4. <template #header>
  5. <div class="card-header">
  6. <span>参赛证生成任务</span>
  7. <el-button type="primary" size="small" @click="handleRefresh">
  8. <el-icon><Refresh /></el-icon>
  9. 刷新
  10. </el-button>
  11. </div>
  12. </template>
  13. <el-table v-loading="loading" :data="taskList" border>
  14. <el-table-column label="序号" type="index" width="60" />
  15. <el-table-column label="任务名称" prop="taskName" />
  16. <el-table-column label="赛事名称" prop="eventName" width="150">
  17. <template #default="scope">
  18. {{ scope.row.eventName || '-' }}
  19. </template>
  20. </el-table-column>
  21. <el-table-column label="状态" prop="status" width="100">
  22. <template #default="scope">
  23. <el-tag :type="getStatusType(scope.row.status)">
  24. {{ getStatusText(scope.row.status) }}
  25. </el-tag>
  26. </template>
  27. </el-table-column>
  28. <el-table-column label="创建时间" prop="createTime" width="180">
  29. <template #default="scope">
  30. {{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
  31. </template>
  32. </el-table-column>
  33. <el-table-column label="完成时间" prop="finishTime" width="180">
  34. <template #default="scope">
  35. {{ scope.row.finishTime ? parseTime(scope.row.finishTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}
  36. </template>
  37. </el-table-column>
  38. <el-table-column label="操作" width="280">
  39. <template #default="scope">
  40. <el-button
  41. v-if="scope.row.status === '0'"
  42. type="warning"
  43. size="small"
  44. @click="handleStopTask(scope.row.taskId)"
  45. >
  46. 停止
  47. </el-button>
  48. <el-button
  49. v-if="scope.row.status === '2'"
  50. type="success"
  51. size="small"
  52. :loading="downloadingTasks.has(scope.row.taskId)"
  53. @click="handleDownload(scope.row.taskId)"
  54. >
  55. {{ downloadingTasks.has(scope.row.taskId) ? '下载中...' : '下载' }}
  56. </el-button>
  57. <el-button
  58. v-if="scope.row.status === '2'"
  59. type="primary"
  60. size="small"
  61. @click="handleCopyDownloadLink(scope.row.taskId)"
  62. >
  63. 复制链接
  64. </el-button>
  65. <el-button
  66. type="danger"
  67. size="small"
  68. @click="handleDelete(scope.row.taskId)"
  69. >
  70. 删除
  71. </el-button>
  72. </template>
  73. </el-table-column>
  74. </el-table>
  75. <pagination
  76. v-show="total > 0"
  77. :total="total"
  78. v-model:page="queryParams.pageNum"
  79. v-model:limit="queryParams.pageSize"
  80. @pagination="getList"
  81. />
  82. </el-card>
  83. </div>
  84. </template>
  85. <script setup lang="ts">
  86. import { ref, onMounted, onUnmounted } from 'vue';
  87. import { useRouter } from 'vue-router';
  88. import { ElMessage, ElMessageBox } from 'element-plus';
  89. import { getTaskList, pauseTask, deleteTask, downloadTask, smartDownloadTask, getDownloadUrl } from '@/api/system/gameEvent/task';
  90. import { BASE_URL } from '@/config/api';
  91. const router = useRouter();
  92. const loading = ref(false);
  93. const taskList = ref([]);
  94. const total = ref(0);
  95. const queryParams = ref({
  96. pageNum: 1,
  97. pageSize: 10
  98. });
  99. // 下载状态管理
  100. const downloadingTasks = ref(new Set<number>());
  101. // 轮询定时器
  102. let pollingTimer: NodeJS.Timeout | null = null;
  103. // 存储上次轮询时的任务状态,用于比较状态变化
  104. const lastTaskStates = ref(new Map<number, string>());
  105. // 获取任务列表
  106. const getList = async () => {
  107. loading.value = true;
  108. try {
  109. const response = await getTaskList(queryParams.value);
  110. taskList.value = response.rows;
  111. total.value = response.total;
  112. // 记录当前任务状态,用于后续轮询比较
  113. const currentTaskStates = new Map<number, string>();
  114. response.rows.forEach(task => {
  115. currentTaskStates.set(task.taskId, task.status);
  116. });
  117. lastTaskStates.value = currentTaskStates;
  118. } finally {
  119. loading.value = false;
  120. }
  121. };
  122. // 新建任务
  123. const handleCreateTask = () => {
  124. router.push('/system/gameEvent');
  125. };
  126. // 复制下载链接
  127. const handleCopyDownloadLink = async (taskId: number) => {
  128. try {
  129. // 获取当前任务信息,检查是否有OSS文件ID
  130. const currentTask = taskList.value.find(task => task.taskId === taskId);
  131. let downloadUrl;
  132. if (currentTask && currentTask.ossId) {
  133. // 如果有OSS文件ID,获取预签名URL(最快下载方式)
  134. try {
  135. const response = await getDownloadUrl(taskId);
  136. downloadUrl = response.msg || response.data;
  137. if (!downloadUrl || downloadUrl === 'null' || downloadUrl === '') {
  138. // 如果获取预签名URL失败,回退到代理下载
  139. downloadUrl = `${BASE_URL}/system/number/public/downloadTask/${taskId}`;
  140. }
  141. } catch (error) {
  142. console.warn('获取预签名URL失败,使用代理下载:', error);
  143. downloadUrl = `${BASE_URL}/system/number/public/downloadTask/${taskId}`;
  144. }
  145. } else {
  146. // 否则使用原有的下载链接
  147. downloadUrl = `${BASE_URL}/system/number/public/downloadTask/${taskId}`;
  148. }
  149. // 复制到剪贴板
  150. await navigator.clipboard.writeText(downloadUrl);
  151. ElMessage.success('下载链接已复制到剪贴板');
  152. } catch (error) {
  153. console.error('复制失败:', error);
  154. // 降级方案:使用传统的复制方法
  155. const currentTask = taskList.value.find(task => task.taskId === taskId);
  156. let downloadUrl;
  157. if (currentTask && currentTask.ossId) {
  158. // 如果有OSS文件ID,获取预签名URL(最快下载方式)
  159. try {
  160. const response = await getDownloadUrl(taskId);
  161. downloadUrl = response.msg || response.data;
  162. if (!downloadUrl || downloadUrl === 'null' || downloadUrl === '') {
  163. // 如果获取预签名URL失败,回退到代理下载
  164. downloadUrl = `${BASE_URL}/system/number/public/downloadTask/${taskId}`;
  165. }
  166. } catch (error) {
  167. console.warn('获取预签名URL失败,使用代理下载:', error);
  168. downloadUrl = `${BASE_URL}/system/number/public/downloadTask/${taskId}`;
  169. }
  170. } else {
  171. // 否则使用原有的下载链接
  172. downloadUrl = `${BASE_URL}/system/number/public/downloadTask/${taskId}`;
  173. }
  174. const textArea = document.createElement('textarea');
  175. textArea.value = downloadUrl;
  176. document.body.appendChild(textArea);
  177. textArea.select();
  178. try {
  179. document.execCommand('copy');
  180. ElMessage.success('下载链接已复制到剪贴板');
  181. } catch (fallbackError) {
  182. ElMessage.error('复制失败,请手动复制链接');
  183. }
  184. document.body.removeChild(textArea);
  185. }
  186. };
  187. // 手动刷新
  188. const handleRefresh = () => {
  189. getList();
  190. ElMessage.success('刷新成功');
  191. };
  192. // 停止任务
  193. const handleStopTask = async (taskId: number) => {
  194. try {
  195. await ElMessageBox.confirm('确定要停止这个任务吗?', '提示', {
  196. confirmButtonText: '确定',
  197. cancelButtonText: '取消',
  198. type: 'warning'
  199. });
  200. await pauseTask(taskId);
  201. ElMessage.success('任务已停止');
  202. getList();
  203. } catch (error) {
  204. if (error !== 'cancel') {
  205. ElMessage.error('停止失败');
  206. }
  207. }
  208. };
  209. // 下载任务结果(使用智能下载)
  210. const handleDownload = async (taskId: number) => {
  211. downloadingTasks.value.add(taskId);
  212. try {
  213. await smartDownloadTask(taskId);
  214. // ElMessage.success('下载完成');
  215. } catch (error) {
  216. ElMessage.error('下载失败');
  217. } finally {
  218. downloadingTasks.value.delete(taskId);
  219. }
  220. };
  221. // 删除任务
  222. const handleDelete = async (taskId: number) => {
  223. try {
  224. await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
  225. confirmButtonText: '确定',
  226. cancelButtonText: '取消',
  227. type: 'warning'
  228. });
  229. await deleteTask(taskId);
  230. ElMessage.success('删除成功');
  231. getList();
  232. } catch (error) {
  233. if (error !== 'cancel') {
  234. ElMessage.error('删除失败');
  235. }
  236. }
  237. };
  238. // 状态相关方法
  239. const getStatusType = (status: string) => {
  240. const statusMap = {
  241. '0': 'warning', // 运行中
  242. '1': 'info', // 暂停
  243. '2': 'success', // 完成
  244. '3': 'danger' // 失败
  245. };
  246. return statusMap[status] || 'info';
  247. };
  248. const getStatusText = (status: string) => {
  249. const statusMap = {
  250. '0': '运行中',
  251. '1': '暂停',
  252. '2': '完成',
  253. '3': '失败'
  254. };
  255. return statusMap[status] || '未知';
  256. };
  257. // 开始轮询
  258. const startPolling = () => {
  259. if (pollingTimer) {
  260. clearInterval(pollingTimer);
  261. }
  262. pollingTimer = setInterval(async () => {
  263. // 只轮询有运行中任务的情况
  264. const hasRunningTasks = taskList.value.some(task => task.status === '0');
  265. if (hasRunningTasks) {
  266. await checkTaskStatusChanges();
  267. }
  268. }, 15000); // 每15秒检查一次
  269. };
  270. // 检查任务状态变化
  271. const checkTaskStatusChanges = async () => {
  272. try {
  273. const response = await getTaskList(queryParams.value);
  274. const currentTasks = response.rows;
  275. // 检查是否有任务状态发生变化
  276. let hasStatusChanged = false;
  277. const currentTaskStates = new Map<number, string>();
  278. currentTasks.forEach(task => {
  279. const taskId = task.taskId;
  280. const currentStatus = task.status;
  281. const lastStatus = lastTaskStates.value.get(taskId);
  282. currentTaskStates.set(taskId, currentStatus);
  283. // 如果状态发生变化,标记需要更新
  284. if (lastStatus && lastStatus !== currentStatus) {
  285. hasStatusChanged = true;
  286. console.log(`任务 ${taskId} 状态从 ${lastStatus} 变为 ${currentStatus}`);
  287. }
  288. });
  289. // 只有当状态发生变化时才更新UI
  290. if (hasStatusChanged) {
  291. taskList.value = currentTasks;
  292. total.value = response.total;
  293. console.log('检测到任务状态变化,更新UI');
  294. }
  295. // 更新状态记录
  296. lastTaskStates.value = currentTaskStates;
  297. } catch (error) {
  298. console.error('检查任务状态失败:', error);
  299. }
  300. };
  301. // 停止轮询
  302. const stopPolling = () => {
  303. if (pollingTimer) {
  304. clearInterval(pollingTimer);
  305. pollingTimer = null;
  306. }
  307. };
  308. onMounted(() => {
  309. getList();
  310. startPolling();
  311. });
  312. onUnmounted(() => {
  313. stopPolling();
  314. });
  315. </script>
  316. <style scoped>
  317. .card-header {
  318. display: flex;
  319. justify-content: space-between;
  320. align-items: center;
  321. }
  322. </style>