Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 

256 строки
9.7 KiB

  1. {% extends "./layout.html" %}
  2. {% block title %}下载管理{% endblock %}
  3. {% block css %}
  4. <style>
  5. .task-progress { display: flex; align-items: center; gap: 8px; }
  6. .task-progress .num { font-size: 12px; color: #909399; white-space: nowrap; }
  7. </style>
  8. {% endblock %}
  9. {% block content %}
  10. <div id="exportApp" v-cloak>
  11. <el-card shadow="never" class="mb-4">
  12. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
  13. <div style="display:flex;align-items:center;gap:12px;">
  14. <el-select v-model="statusFilter" placeholder="全部状态" clearable style="width:140px;" @change="loadList(1)">
  15. <el-option label="待处理" :value="0"></el-option>
  16. <el-option label="打包中" :value="1"></el-option>
  17. <el-option label="已完成" :value="2"></el-option>
  18. <el-option label="失败" :value="3"></el-option>
  19. </el-select>
  20. <el-button type="primary" @click="loadList(1)">查询</el-button>
  21. </div>
  22. </div>
  23. <el-table :data="tableData" v-loading="loading" stripe border>
  24. <el-table-column prop="task_no" label="任务编号" min-width="160"></el-table-column>
  25. <el-table-column prop="title" label="附件类型" min-width="200"></el-table-column>
  26. <el-table-column label="状态" width="120" align="center">
  27. <template #default="{ row }">
  28. <el-tag v-if="row.status === 0" type="info" size="small">待处理</el-tag>
  29. <el-tag v-else-if="row.status === 1" type="warning" size="small">打包中</el-tag>
  30. <el-tag v-else-if="row.status === 2" type="success" size="small">已完成</el-tag>
  31. <el-tag v-else-if="row.status === 3" type="danger" size="small">失败</el-tag>
  32. </template>
  33. </el-table-column>
  34. <el-table-column label="进度" min-width="180">
  35. <template #default="{ row }">
  36. <div class="task-progress" v-if="row.status === 1 && row.total_files > 0">
  37. <el-progress :percentage="Math.round(row.processed_files / row.total_files * 100)" :stroke-width="8" style="flex:1;"></el-progress>
  38. <span class="num">${ row.processed_files }/${ row.total_files }</span>
  39. </div>
  40. <span v-else-if="row.status === 2" style="color:#67C23A;">${ row.processed_files }/${ row.total_files } 个文件</span>
  41. <span v-else-if="row.status === 0" style="color:#909399;">等待中</span>
  42. <span v-else-if="row.status === 3" style="color:#F56C6C;">失败</span>
  43. </template>
  44. </el-table-column>
  45. <el-table-column label="文件大小" width="110" align="center">
  46. <template #default="{ row }">
  47. <span v-if="row.file_size">${ formatSize(row.file_size) }</span>
  48. <span v-else style="color:#C0C4CC;">—</span>
  49. </template>
  50. </el-table-column>
  51. <el-table-column prop="create_by_name" label="创建人" width="100" align="center"></el-table-column>
  52. <el-table-column prop="create_time" label="创建时间" width="170"></el-table-column>
  53. <el-table-column prop="finished_at" label="完成时间" width="170">
  54. <template #default="{ row }">
  55. <span v-if="row.finished_at">${ row.finished_at }</span>
  56. <span v-else style="color:#C0C4CC;">—</span>
  57. </template>
  58. </el-table-column>
  59. <el-table-column label="操作" width="200" align="center" fixed="right">
  60. <template #default="{ row }">
  61. <el-button v-if="row.status === 2 && row.file_url" type="primary" link @click="downloadFile(row)">下载</el-button>
  62. <el-button v-if="row.status === 3" type="warning" link @click="retryTask(row)">重试</el-button>
  63. <el-button v-if="row.error_log" type="info" link @click="showErrorLog(row)">日志</el-button>
  64. <el-button v-if="row.status !== 1" type="danger" link @click="deleteTask(row)">删除</el-button>
  65. </template>
  66. </el-table-column>
  67. </el-table>
  68. <div class="flex justify-end mt-4">
  69. <el-pagination
  70. v-model:current-page="pagination.page"
  71. v-model:page-size="pagination.pageSize"
  72. :page-sizes="[10, 20, 50]"
  73. :total="pagination.total"
  74. layout="total, sizes, prev, pager, next"
  75. @current-change="loadList"
  76. @size-change="onSizeChange"
  77. />
  78. </div>
  79. </el-card>
  80. <!-- 错误日志弹窗 -->
  81. <el-dialog v-model="logVisible" title="任务日志" width="600px" destroy-on-close>
  82. <pre style="max-height:400px;overflow:auto;background:#f5f7fa;padding:16px;border-radius:6px;font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-all;">${ logContent }</pre>
  83. </el-dialog>
  84. </div>
  85. {% endblock %}
  86. {% block js %}
  87. <script>
  88. var { createApp, ref, reactive, onMounted, onUnmounted } = Vue;
  89. var app = createApp({
  90. delimiters: ['${', '}'],
  91. setup() {
  92. var loading = ref(false);
  93. var tableData = ref([]);
  94. var pagination = reactive({ page: 1, pageSize: 10, total: 0 });
  95. var statusFilter = ref('');
  96. var logVisible = ref(false);
  97. var logContent = ref('');
  98. var pollTimer = null;
  99. async function loadList(page) {
  100. if (typeof page === 'number') pagination.page = page;
  101. loading.value = true;
  102. try {
  103. var params = new URLSearchParams({
  104. page: pagination.page,
  105. pageSize: pagination.pageSize
  106. });
  107. if (statusFilter.value !== '' && statusFilter.value !== null) {
  108. params.set('status', statusFilter.value);
  109. }
  110. var res = await fetch('/admin/export_task/list?' + params).then(function(r) { return r.json(); });
  111. if (res.code === 0) {
  112. tableData.value = res.data.data || [];
  113. pagination.total = res.data.count || 0;
  114. // 检查是否有进行中的任务,启动轮询
  115. checkAndPoll();
  116. }
  117. } finally {
  118. loading.value = false;
  119. }
  120. }
  121. function checkAndPoll() {
  122. var processingIds = tableData.value
  123. .filter(function(t) { return t.status === 0 || t.status === 1; })
  124. .map(function(t) { return t.id; });
  125. if (processingIds.length > 0) {
  126. startPoll(processingIds);
  127. } else {
  128. stopPoll();
  129. }
  130. }
  131. function startPoll(ids) {
  132. stopPoll();
  133. pollTimer = setInterval(async function() {
  134. try {
  135. var res = await fetch('/admin/export_task/batchStatus?ids=' + ids.join(',')).then(function(r) { return r.json(); });
  136. if (res.code === 0 && res.data) {
  137. var changed = false;
  138. res.data.forEach(function(updated) {
  139. var row = tableData.value.find(function(t) { return t.id === updated.id; });
  140. if (row) {
  141. if (row.status !== updated.status) changed = true;
  142. row.status = updated.status;
  143. row.processed_files = updated.processed_files;
  144. row.total_files = updated.total_files;
  145. row.file_url = updated.file_url;
  146. row.file_size = updated.file_size;
  147. }
  148. });
  149. // 如果有状态变化,重新加载完整列表获取最新数据
  150. if (changed) {
  151. loadList();
  152. } else {
  153. checkAndPoll();
  154. }
  155. }
  156. } catch(e) { /* ignore */ }
  157. }, 3000);
  158. }
  159. function stopPoll() {
  160. if (pollTimer) {
  161. clearInterval(pollTimer);
  162. pollTimer = null;
  163. }
  164. }
  165. function onSizeChange() {
  166. pagination.page = 1;
  167. loadList();
  168. }
  169. function formatSize(bytes) {
  170. if (!bytes) return '0 B';
  171. if (bytes < 1024) return bytes + ' B';
  172. if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
  173. if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
  174. return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
  175. }
  176. function downloadFile(row) {
  177. if (row.file_url) {
  178. window.open(row.file_url, '_blank');
  179. }
  180. }
  181. async function retryTask(row) {
  182. try {
  183. await ElementPlus.ElMessageBox.confirm('确定要重试该任务吗?', '确认重试', {
  184. confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning'
  185. });
  186. var res = await fetch('/admin/export_task/retry', {
  187. method: 'POST',
  188. headers: { 'Content-Type': 'application/json' },
  189. body: JSON.stringify({ id: row.id })
  190. }).then(function(r) { return r.json(); });
  191. if (res.code === 0) {
  192. ElementPlus.ElMessage.success('已重新提交');
  193. loadList();
  194. } else {
  195. ElementPlus.ElMessage.error(res.msg || '操作失败');
  196. }
  197. } catch(e) {}
  198. }
  199. async function deleteTask(row) {
  200. try {
  201. await ElementPlus.ElMessageBox.confirm('确定要删除该任务吗?', '确认删除', {
  202. confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning'
  203. });
  204. var res = await fetch('/admin/export_task/delete', {
  205. method: 'POST',
  206. headers: { 'Content-Type': 'application/json' },
  207. body: JSON.stringify({ id: row.id })
  208. }).then(function(r) { return r.json(); });
  209. if (res.code === 0) {
  210. ElementPlus.ElMessage.success('删除成功');
  211. loadList();
  212. } else {
  213. ElementPlus.ElMessage.error(res.msg || '删除失败');
  214. }
  215. } catch(e) {}
  216. }
  217. function showErrorLog(row) {
  218. logContent.value = row.error_log || '无日志';
  219. logVisible.value = true;
  220. }
  221. onMounted(function() { loadList(); });
  222. onUnmounted(function() { stopPoll(); });
  223. return {
  224. loading, tableData, pagination, statusFilter,
  225. logVisible, logContent,
  226. loadList, onSizeChange, formatSize, downloadFile, retryTask, deleteTask, showErrorLog
  227. };
  228. }
  229. });
  230. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  231. app.mount('#exportApp');
  232. </script>
  233. {% endblock %}