xyc
3 天以前 465879cec6931baa8dacc94c7107ebe591f76b2b
traffic-audit-web/src/views/ReportExport.vue
@@ -10,8 +10,13 @@
          <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>
@@ -162,21 +167,46 @@
        </el-dialog>
        <!-- 汇总大表异步生成进度(生成一次约 10~20 秒,避免点击后长时间无响应) -->
        <el-dialog title="正在生成汇总大表" :visible.sync="taskVisible" width="460px"
        <el-dialog :title="taskDialogTitle" :visible.sync="taskVisible" width="460px"
                   :close-on-click-modal="false" :show-close="taskFinished">
          <div class="task-body">
            <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>,文件在浏览器默认下载目录。
            </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" type="primary" :disabled="!taskDone" @click="downloadTaskFile">{{ 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 @click="closeTask">关闭</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>
@@ -212,7 +242,17 @@
      taskVisible: false,
      taskInfo: {},
      taskTimer: null,
      // 进度弹窗:完成后会自动下载一次,这里记录「是否已下载」与文件信息(已下载就不再显示下载按钮)
      taskDownloaded: false,
      taskFileName: '',
      taskFileSize: 0,
      taskKey: 'summaryWorkbook',
      // 导出前缺数清单弹窗(2026-10-12):缺数/体检摆到台面上,确认后再生成
      missDialogVisible: false,
      missDialogPeriod: '',
      missDialogMissing: [],
      missDialogCheck: [],
      missDialogRefresh: false,
      modules: [
        {
          key: 'summary', title: '汇总大表', items: [
@@ -320,6 +360,18 @@
      const d = new Date(t)
      const p = (n) => String(n).padStart(2, '0')
      return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
    },
    // 弹窗标题随任务状态变化,避免「已完成」还写着「正在生成」
    taskDialogTitle() {
      const s = this.taskInfo && this.taskInfo.status
      if (s === 'FAILED') return '汇总大表生成失败'
      if (s === 'DONE') return '汇总大表已生成'
      return '正在生成汇总大表'
    },
    taskFileSizeText() {
      const n = this.taskFileSize || 0
      if (!n) return ''
      return n >= 1048576 ? (n / 1048576).toFixed(2) + ' MB' : Math.round(n / 1024) + ' KB'
    }
  },
  watch: {
@@ -524,6 +576,9 @@
      this.stopTaskPolling()
      this.taskInfo = { status: 'PENDING', percent: 0, step: '提交任务', period: this.period, elapsedMs: 0 }
      this.taskVisible = true
      this.taskDownloaded = false
      this.taskFileName = ''
      this.taskFileSize = 0
      this.loading = 'gen-' + this.curModule.key
      const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' })
      api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 })
@@ -536,7 +591,7 @@
            return
          }
          this.taskInfo = res.data
          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(); return }
          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(true); return }
          if (this.taskInfo.status === 'FAILED') { this.loading = ''; return }
          this.pollTask()
        })
@@ -568,7 +623,7 @@
            if (this.taskInfo.status === 'DONE') {
              this.stopTaskPolling()
              this.loading = ''
              this.downloadTaskFile()
              this.downloadTaskFile(true)
            } else if (this.taskInfo.status === 'FAILED') {
              this.stopTaskPolling()
              this.loading = ''
@@ -581,13 +636,42 @@
    stopTaskPolling() {
      if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
    },
    downloadTaskFile() {
    // auto=true 表示「任务完成后的自动下载」;用户点按钮时为 false
    downloadTaskFile(auto) {
      const id = this.taskInfo && this.taskInfo.taskId
      if (!id) return
      const name = this.taskInfo.fileName || ('生成_道路运输量汇总表_' + this.period + '.xlsx')
      this.taskFileName = name
      api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 })
        .then(res => { this.downloadBlob(res, name); this.$message.success('报表导出成功') })
        .then(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 = '' },
@@ -606,7 +690,7 @@
      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))
@@ -756,6 +840,14 @@
.task-step { margin: 12px 0 4px; font-size: 14px; color:#303133; }
.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>