| | |
| | | <el-menu-item index="/explain">企业解释</el-menu-item> |
| | | <el-menu-item index="/audit">审核结果</el-menu-item> |
| | | <el-menu-item index="/report">报表生成</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">专项统计</template> |
| | | <el-menu-item index="/holiday-stat">节假日</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">规则管理</el-menu-item> |
| | | <el-menu-item index="/users">用户管理</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">系统配置</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | |
| | | <el-progress :percentage="taskInfo.percent || 0" :status="taskInfo.status === 'FAILED' ? 'exception' : (taskDone ? 'success' : null)" |
| | | :stroke-width="14"></el-progress> |
| | | <p class="task-step">{{ taskInfo.step || '排队中' }}</p> |
| | | <p class="task-meta" v-if="taskInfo.period">报表期 {{ taskInfo.period }} {{ taskInfo.fromCache ? '上次生成用时' : '已用' }} {{ (taskInfo.elapsedMs / 1000).toFixed(1) }} 秒</p> |
| | | <p class="task-meta" v-if="taskInfo.fromCache">本次直接使用 {{ taskCacheTime }} 生成的结果(10 分钟内且期间没有新导入数据)。如需按最新数据重算,请点「重新生成」。</p> |
| | | <p class="task-meta" v-if="taskInfo.period && !taskInfo.fromCache">报表期 {{ taskInfo.period }} 已用 {{ ((taskInfo.elapsedMs || 0) / 1000).toFixed(1) }} 秒</p> |
| | | <p class="task-meta" v-else-if="taskInfo.period">报表期 {{ taskInfo.period }} 上次生成于 {{ taskCacheTime }}</p> |
| | | <p class="task-meta" v-if="taskInfo.staleCache">上次生成的结果已因数据或母版变化失效,本次按最新数据重算。</p> |
| | | <p class="task-meta" v-if="taskInfo.fromCache">本次直接沿用 {{ taskCacheTime }} 生成的结果(期间没有新的数据导入)。如需按最新数据重算,请点「重新生成」。</p> |
| | | <p class="task-done" v-if="taskDownloaded"> |
| | | 已自动下载:<b>{{ taskFileName }}</b><span v-if="taskFileSize">({{ taskFileSizeText }})</span><br> |
| | | 浏览器没弹出下载时可点「再次下载」——用本地缓存保存,不会重新请求服务器。 |
| | | 已自动下载:<b>{{ taskFileName }}</b><span v-if="taskFileSize">({{ taskFileSizeText }})</span>,文件在浏览器默认下载目录。 |
| | | </p> |
| | | <p class="task-error" v-else-if="taskDone">自动下载没成功,可点「保存文件」再存一次。</p> |
| | | <p class="task-error" v-if="taskInfo.status === 'FAILED'">{{ taskInfo.error }}</p> |
| | | </div> |
| | | <span slot="footer"> |
| | | <el-button size="small" :disabled="!taskDone" @click="downloadTaskFile()">{{ taskDownloaded ? '再次下载' : (taskInfo.fromCache ? '下载' : '保存文件') }}</el-button> |
| | | <el-button size="small" v-if="taskDone && !taskDownloaded" @click="downloadTaskFile()">保存文件</el-button> |
| | | <el-button size="small" :disabled="taskRunning" @click="restartTask">重新生成</el-button> |
| | | <el-button size="small" v-if="taskRunning" @click="minimizeTask">后台运行</el-button> |
| | | <el-button size="small" v-else type="primary" @click="closeTask">关闭</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | <!-- 汇总大表导出前缺数清单弹窗(2026-10-12 用户要求:缺数摆到台面上) --> |
| | | <el-dialog title="生成前提示 - 数据缺失清单" :visible.sync="missDialogVisible" width="720px" top="6vh"> |
| | | <div class="miss-body"> |
| | | <p class="miss-lead">报表期 <b>{{ missDialogPeriod }}</b>:以下数据本次生成会缺失/异常,生成件末尾也会附《数据缺失清单》页。</p> |
| | | <template v-if="missDialogMissing.length"> |
| | | <div class="miss-sec">一、缺数清单(该有的数没有) 共 {{ missDialogMissing.length }} 条</div> |
| | | <ul class="miss-list"><li v-for="(t, i) in missDialogMissing" :key="'m' + i">{{ t }}</li></ul> |
| | | </template> |
| | | <template v-if="missDialogCheck.length"> |
| | | <div class="miss-sec">二、数据体检(数值不合理) 共 {{ missDialogCheck.length }} 条</div> |
| | | <ul class="miss-list"><li v-for="(t, i) in missDialogCheck" :key="'c' + i">{{ t }}</li></ul> |
| | | </template> |
| | | <p class="miss-tip">缺数多为源数据尚未导入,导入后重新生成即可;体检项请核对源文件。</p> |
| | | </div> |
| | | <span slot="footer"> |
| | | <el-button size="small" @click="missDialogVisible = false">取消</el-button> |
| | | <el-button size="small" type="primary" @click="confirmMissDialog">仍要生成</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | </el-main> |
| | |
| | | taskVisible: false, |
| | | taskInfo: {}, |
| | | taskTimer: null, |
| | | // 进度弹窗:完成后会自动下载一次,这里记录「已下载」与文件信息(重复点击走本地缓存,不再请求后端) |
| | | // 进度弹窗:完成后会自动下载一次,这里记录「是否已下载」与文件信息(已下载就不再显示下载按钮) |
| | | taskDownloaded: false, |
| | | taskFileName: '', |
| | | taskFileSize: 0, |
| | | taskBlob: null, |
| | | taskKey: 'summaryWorkbook', |
| | | // 导出前缺数清单弹窗(2026-10-12):缺数/体检摆到台面上,确认后再生成 |
| | | missDialogVisible: false, |
| | | missDialogPeriod: '', |
| | | missDialogMissing: [], |
| | | missDialogCheck: [], |
| | | missDialogRefresh: false, |
| | | modules: [ |
| | | { |
| | | key: 'summary', title: '汇总大表', items: [ |
| | |
| | | this.taskDownloaded = false |
| | | this.taskFileName = '' |
| | | this.taskFileSize = 0 |
| | | this.taskBlob = null |
| | | this.loading = 'gen-' + this.curModule.key |
| | | const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' }) |
| | | api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 }) |
| | |
| | | if (!id) return |
| | | const name = this.taskInfo.fileName || ('生成_道路运输量汇总表_' + this.period + '.xlsx') |
| | | this.taskFileName = name |
| | | // 已经下载过:直接用内存里的 Blob 再存一次,不再请求后端(避免同一份文件重复拉取 1MB+) |
| | | if (this.taskBlob) { |
| | | this.downloadBlob(this.taskBlob, name) |
| | | this.$message.success('已从本地缓存保存:' + name) |
| | | return |
| | | } |
| | | api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { |
| | | this.taskBlob = res |
| | | this.taskFileSize = (res && res.size) || 0 |
| | | this.taskDownloaded = true |
| | | this.downloadBlob(res, name) |
| | | this.$message.success(auto ? '生成完成,已自动下载' : '报表导出成功') |
| | | }) |
| | | .catch(err => this.failWith(err)) |
| | | }, |
| | | // 导出前缺数检查:调只读接口把缺数/体检结果摆到用户面前,确认后再走异步生成任务 |
| | | generateSummaryWorkbook(refresh) { |
| | | if (!this.period) { this.$message.warning('请先选择周期和月份'); return } |
| | | const params = { period: this.period } |
| | | Promise.all([ |
| | | api.get('/report/summaryWorkbook/missing', { params, timeout: 60000 }).catch(() => null), |
| | | api.get('/report/summaryWorkbook/freightCheck', { params, timeout: 60000 }).catch(() => null) |
| | | ]).then(res => { |
| | | const miss = (res[0] && res[0].code === 200 && res[0].data) || [] |
| | | const chk = (res[1] && res[1].code === 200 && res[1].data) || [] |
| | | if (!miss.length && !chk.length) { this.startSummaryTask(refresh); return } |
| | | this.missDialogPeriod = this.period |
| | | this.missDialogMissing = miss |
| | | this.missDialogCheck = chk |
| | | this.missDialogRefresh = refresh |
| | | this.missDialogVisible = true |
| | | }) |
| | | }, |
| | | confirmMissDialog() { |
| | | this.missDialogVisible = false |
| | | this.startSummaryTask(this.missDialogRefresh) |
| | | }, |
| | | restartTask() { this.startSummaryTask(true) }, |
| | | closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' }, |
| | |
| | | const doExport = () => { |
| | | if (items.length === 1) { |
| | | // 汇总大表生成一次约 10~20 秒,走异步任务(进度弹窗),避免点击后长时间无响应 |
| | | if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return } |
| | | if (items[0].key === 'summaryWorkbook') { this.generateSummaryWorkbook(false); return } |
| | | api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('报表导出成功'); this.loading = '' }) |
| | | .catch(err => this.failWith(err)) |
| | |
| | | .task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; } |
| | | .task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; } |
| | | .task-done { margin: 8px 0 0; font-size: 13px; color:#67C23A; line-height: 1.6; } |
| | | /* 导出前缺数清单弹窗 */ |
| | | .miss-body { max-height: 62vh; overflow-y: auto; } |
| | | .miss-lead { font-size: 13px; color: #303133; margin: 0 0 10px; line-height: 1.7; } |
| | | .miss-sec { font-size: 13px; font-weight: 600; color: #F56C6C; margin: 10px 0 4px; } |
| | | .miss-list { margin: 0; padding-left: 20px; font-size: 13px; color: #303133; line-height: 1.7; } |
| | | .miss-list li { margin: 2px 0; } |
| | | .miss-tip { font-size: 12px; color: #909399; margin: 10px 0 0; } |
| | | </style> |
| | | |
| | | <style> |