|
- {% extends "./layout.html" %}
-
- {% block title %}下载管理{% endblock %}
-
- {% block css %}
- <style>
- .task-progress { display: flex; align-items: center; gap: 8px; }
- .task-progress .num { font-size: 12px; color: #909399; white-space: nowrap; }
- </style>
- {% endblock %}
-
- {% block content %}
- <div id="exportApp" v-cloak>
- <el-card shadow="never" class="mb-4">
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
- <div style="display:flex;align-items:center;gap:12px;">
- <el-select v-model="statusFilter" placeholder="全部状态" clearable style="width:140px;" @change="loadList(1)">
- <el-option label="待处理" :value="0"></el-option>
- <el-option label="打包中" :value="1"></el-option>
- <el-option label="已完成" :value="2"></el-option>
- <el-option label="失败" :value="3"></el-option>
- </el-select>
- <el-button type="primary" @click="loadList(1)">查询</el-button>
- </div>
- </div>
-
- <el-table :data="tableData" v-loading="loading" stripe border>
- <el-table-column prop="task_no" label="任务编号" min-width="160"></el-table-column>
- <el-table-column prop="title" label="附件类型" min-width="200"></el-table-column>
- <el-table-column label="状态" width="120" align="center">
- <template #default="{ row }">
- <el-tag v-if="row.status === 0" type="info" size="small">待处理</el-tag>
- <el-tag v-else-if="row.status === 1" type="warning" size="small">打包中</el-tag>
- <el-tag v-else-if="row.status === 2" type="success" size="small">已完成</el-tag>
- <el-tag v-else-if="row.status === 3" type="danger" size="small">失败</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="进度" min-width="180">
- <template #default="{ row }">
- <div class="task-progress" v-if="row.status === 1 && row.total_files > 0">
- <el-progress :percentage="Math.round(row.processed_files / row.total_files * 100)" :stroke-width="8" style="flex:1;"></el-progress>
- <span class="num">${ row.processed_files }/${ row.total_files }</span>
- </div>
- <span v-else-if="row.status === 2" style="color:#67C23A;">${ row.processed_files }/${ row.total_files } 个文件</span>
- <span v-else-if="row.status === 0" style="color:#909399;">等待中</span>
- <span v-else-if="row.status === 3" style="color:#F56C6C;">失败</span>
- </template>
- </el-table-column>
- <el-table-column label="文件大小" width="110" align="center">
- <template #default="{ row }">
- <span v-if="row.file_size">${ formatSize(row.file_size) }</span>
- <span v-else style="color:#C0C4CC;">—</span>
- </template>
- </el-table-column>
- <el-table-column prop="create_by_name" label="创建人" width="100" align="center"></el-table-column>
- <el-table-column prop="create_time" label="创建时间" width="170"></el-table-column>
- <el-table-column prop="finished_at" label="完成时间" width="170">
- <template #default="{ row }">
- <span v-if="row.finished_at">${ row.finished_at }</span>
- <span v-else style="color:#C0C4CC;">—</span>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200" align="center" fixed="right">
- <template #default="{ row }">
- <el-button v-if="row.status === 2 && row.file_url" type="primary" link @click="downloadFile(row)">下载</el-button>
- <el-button v-if="row.status === 3" type="warning" link @click="retryTask(row)">重试</el-button>
- <el-button v-if="row.error_log" type="info" link @click="showErrorLog(row)">日志</el-button>
- <el-button v-if="row.status !== 1" type="danger" link @click="deleteTask(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
-
- <div class="flex justify-end mt-4">
- <el-pagination
- v-model:current-page="pagination.page"
- v-model:page-size="pagination.pageSize"
- :page-sizes="[10, 20, 50]"
- :total="pagination.total"
- layout="total, sizes, prev, pager, next"
- @current-change="loadList"
- @size-change="onSizeChange"
- />
- </div>
- </el-card>
-
- <!-- 错误日志弹窗 -->
- <el-dialog v-model="logVisible" title="任务日志" width="600px" destroy-on-close>
- <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>
- </el-dialog>
- </div>
- {% endblock %}
-
- {% block js %}
- <script>
- var { createApp, ref, reactive, onMounted, onUnmounted } = Vue;
-
- var app = createApp({
- delimiters: ['${', '}'],
- setup() {
- var loading = ref(false);
- var tableData = ref([]);
- var pagination = reactive({ page: 1, pageSize: 10, total: 0 });
- var statusFilter = ref('');
- var logVisible = ref(false);
- var logContent = ref('');
- var pollTimer = null;
-
- async function loadList(page) {
- if (typeof page === 'number') pagination.page = page;
- loading.value = true;
- try {
- var params = new URLSearchParams({
- page: pagination.page,
- pageSize: pagination.pageSize
- });
- if (statusFilter.value !== '' && statusFilter.value !== null) {
- params.set('status', statusFilter.value);
- }
- var res = await fetch('/admin/export_task/list?' + params).then(function(r) { return r.json(); });
- if (res.code === 0) {
- tableData.value = res.data.data || [];
- pagination.total = res.data.count || 0;
- // 检查是否有进行中的任务,启动轮询
- checkAndPoll();
- }
- } finally {
- loading.value = false;
- }
- }
-
- function checkAndPoll() {
- var processingIds = tableData.value
- .filter(function(t) { return t.status === 0 || t.status === 1; })
- .map(function(t) { return t.id; });
-
- if (processingIds.length > 0) {
- startPoll(processingIds);
- } else {
- stopPoll();
- }
- }
-
- function startPoll(ids) {
- stopPoll();
- pollTimer = setInterval(async function() {
- try {
- var res = await fetch('/admin/export_task/batchStatus?ids=' + ids.join(',')).then(function(r) { return r.json(); });
- if (res.code === 0 && res.data) {
- var changed = false;
- res.data.forEach(function(updated) {
- var row = tableData.value.find(function(t) { return t.id === updated.id; });
- if (row) {
- if (row.status !== updated.status) changed = true;
- row.status = updated.status;
- row.processed_files = updated.processed_files;
- row.total_files = updated.total_files;
- row.file_url = updated.file_url;
- row.file_size = updated.file_size;
- }
- });
- // 如果有状态变化,重新加载完整列表获取最新数据
- if (changed) {
- loadList();
- } else {
- checkAndPoll();
- }
- }
- } catch(e) { /* ignore */ }
- }, 3000);
- }
-
- function stopPoll() {
- if (pollTimer) {
- clearInterval(pollTimer);
- pollTimer = null;
- }
- }
-
- function onSizeChange() {
- pagination.page = 1;
- loadList();
- }
-
- function formatSize(bytes) {
- if (!bytes) return '0 B';
- if (bytes < 1024) return bytes + ' B';
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
- if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
- return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
- }
-
- function downloadFile(row) {
- if (row.file_url) {
- window.open(row.file_url, '_blank');
- }
- }
-
- async function retryTask(row) {
- try {
- await ElementPlus.ElMessageBox.confirm('确定要重试该任务吗?', '确认重试', {
- confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning'
- });
- var res = await fetch('/admin/export_task/retry', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: row.id })
- }).then(function(r) { return r.json(); });
- if (res.code === 0) {
- ElementPlus.ElMessage.success('已重新提交');
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '操作失败');
- }
- } catch(e) {}
- }
-
- async function deleteTask(row) {
- try {
- await ElementPlus.ElMessageBox.confirm('确定要删除该任务吗?', '确认删除', {
- confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning'
- });
- var res = await fetch('/admin/export_task/delete', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: row.id })
- }).then(function(r) { return r.json(); });
- if (res.code === 0) {
- ElementPlus.ElMessage.success('删除成功');
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '删除失败');
- }
- } catch(e) {}
- }
-
- function showErrorLog(row) {
- logContent.value = row.error_log || '无日志';
- logVisible.value = true;
- }
-
- onMounted(function() { loadList(); });
- onUnmounted(function() { stopPoll(); });
-
- return {
- loading, tableData, pagination, statusFilter,
- logVisible, logContent,
- loadList, onSizeChange, formatSize, downloadFile, retryTask, deleteTask, showErrorLog
- };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#exportApp');
- </script>
- {% endblock %}
|