zhizhijie
2026-09-11 09e612af50beb200027317d6d4ccf7ad7092838e
traffic-audit-web/src/views/DataImport.vue
@@ -1,4 +1,4 @@
SKIP/CHECK (0): TYPE_LABELS upper <template>
<template>
  <el-container direction="vertical">
    <app-header title="交通统计报表审核系统 - 数据导入"></app-header>
    <el-container>
@@ -54,7 +54,7 @@
                <div v-else-if="moduleSel[g.key] && !isDirOnlyType(moduleSel[g.key])" class="module-file">
                  <div class="file-name" :title="fileNamesText(moduleSel[g.key])">{{ fileNamesText(moduleSel[g.key]) || '未选择文件' }}</div>
                  <div class="file-btns">
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple accept=".xlsx,.xls,.et"
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :accept="acceptOf(moduleSel[g.key])"
                               :ref="'up' + g.key" :on-change="f => onFilePicked(g.key, f)" style="display:inline-block">
                      <el-button size="mini" type="primary" plain icon="el-icon-folder-opened" :disabled="importing">选择文件</el-button>
                    </el-upload>
@@ -105,6 +105,11 @@
                  <span>本月导入进度({{ period || '未选择报表期' }})</span>
                  <el-button type="text" icon="el-icon-refresh" @click="loadData">刷新</el-button>
                </div>
                <div v-if="period" class="side-danger">
                  <el-button size="mini" type="danger" plain icon="el-icon-delete"
                             :disabled="clearing || importing" @click="openClearDialog">清空数据…</el-button>
                  <span class="side-danger-tip">按报表期清库,用于整月重导</span>
                </div>
                <div v-if="!period" class="tip">请先在上方选择报表期</div>
                <div v-else class="overview-grid">
                  <div class="ov-row" v-for="g in visibleGroups" :key="g.key">
@@ -119,6 +124,32 @@
            </div>
          </div>
        </div>
        <el-dialog title="清空数据" :visible.sync="clearDialog" width="560px" append-to-body :close-on-click-modal="false">
          <div class="clear-dlg">
            <div class="clear-period">报表期:<b>{{ period }}</b></div>
            <el-radio-group v-model="clearScope">
              <el-radio label="all">清空该报表期全部已导入数据</el-radio>
              <el-radio label="types">只清空指定类型</el-radio>
            </el-radio-group>
            <div v-if="clearScope === 'types'" class="clear-types">
              <el-select v-model="clearTypes" multiple filterable collapse-tags placeholder="选择要清空的数据类型" style="width:100%">
                <el-option-group v-for="g in clearableGroups()" :key="g.key" :label="g.name">
                  <el-option v-for="t in g.types" :key="t.value" :value="t.value"
                             :label="t.label + '(' + overviewState(t.value).text + ')'"></el-option>
                </el-option-group>
              </el-select>
              <div class="tip" style="margin-top:6px">可多选;未导入的类型没有数据可删。</div>
            </div>
            <div class="clear-warn">
              将删除所选范围内该报表期的数据库数据,并同步删除对应审核结果、已审核标记与导入批次记录(投资项目主档保留)。此操作不可撤销,请确认已有备份或准备重新导入。
            </div>
          </div>
          <span slot="footer">
            <el-button @click="clearDialog = false">取消</el-button>
            <el-button type="danger" :loading="clearing"
                       :disabled="clearScope === 'types' && !clearTypes.length" @click="confirmClear">确认清空</el-button>
          </span>
        </el-dialog>
      </el-main>
    </el-container>
  </el-container>
@@ -134,11 +165,11 @@
  investCityPassenger: '市州单表(客运站场)', investCityLogistics: '市州单表(物流园区)',
  'INVEST_客运站场_CITY': '市州单表(客运站场)', 'INVEST_物流园区_CITY': '市州单表(物流园区)',
  cityBus: '城市公交月报', cityTaxi: '巡游出租月报', cityTaxiAuth: '出租车运政车辆',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量', wycOrderReport: '网约车订单报告',
  H2032: 'H203-2月报', H2031: 'H203-1旅客月报', H204: 'H204能耗季报', PASSENGER_AUTH: '旅客运政车辆数',
  PASSENGER_INDIVIDUAL: '个体客运量/周转量', ENERGY_AUTH: '能耗车辆运政信息', TRANSPORT_AUTH: '运政车辆',
  TRACK_MILEAGE: '轨迹里程', SCALE_SPLIT: '规上规下拆分', FREIGHT_TURNOVER: '货运量周转量',
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量'
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量', WYC_ORDER_REPORT: '网约车订单报告'
}
const BATCH_TYPE_MAP = {
  H2032: 'h2032', TRANSPORT_AUTH: 'transportAuth', TRACK_MILEAGE: 'trackMileage',
@@ -146,7 +177,7 @@
  H2031: 'passengerMonthly', PASSENGER_AUTH: 'passengerAuth', PASSENGER_INDIVIDUAL: 'passengerIndividual',
  H204: 'energyMonthly', ENERGY_AUTH: 'energyAuth',
  CITY_BUS: 'cityBus', CITY_TAXI: 'cityTaxi', CITY_TAXI_AUTH: 'cityTaxiAuth',
  WYC_TOTAL: 'wycTotal', WYC_ORDER: 'wycOrder',
  WYC_TOTAL: 'wycTotal', WYC_ORDER: 'wycOrder', WYC_ORDER_REPORT: 'wycOrderReport',
  INVEST_SYSTEM: 'investmentSystem'
}
export default {
@@ -158,6 +189,10 @@
      period: (() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') })(),
      cityDirLoading: false,
      importing: false,
      clearing: false,
      clearDialog: false,
      clearScope: 'all',
      clearTypes: [],
      sideOpen: false,
      result: '',
      error: '',
@@ -191,7 +226,8 @@
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' },
            { value: 'wycTotal', label: '网约车总数' },
            { value: 'wycOrder', label: '网约车订单及全省总量' }
            { value: 'wycOrder', label: '网约车订单及全省总量' },
            { value: 'wycOrderReport', label: '网约车订单报告' }
          ]
        },
        {
@@ -264,7 +300,8 @@
        cityTaxi: '导入_巡游出租汽车运营服务情况月报(YYYY年M月).xls(市州级17行)',
        cityTaxiAuth: '导入模板_出租车信息(运政车辆信息).xlsx(车辆级明细,用于运营车数对比)',
        wycTotal: '模板_网约车总数.xlsx(A列=月份,B-E=全省客运量/城市内/周转量/城市内周转量)',
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)'
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)',
        wycOrderReport: '模板_网约车订单.docx(《网约车行业YYYY年M月运行监测情况分析报告》,自动解析附件表「地市名称…订单数总数」的 17 市州订单)'
      }
      return map[this.importType] || ''
    },
@@ -285,6 +322,7 @@
        cityTaxiAuth: '城市客运/导入模板_出租车信息(运政车辆信息).xlsx',
        wycTotal: '城市客运/模板_网约车总数.xlsx',
        wycOrder: '城市客运/模板_网约车订单及全省总量.xlsx',
        wycOrderReport: '城市客运/模板_网约车订单.docx',
        investment: '投资/输入/客运(客运站)/更新_7月客运站投资明细表(市州汇总大表,给领导).xlsx',
        investmentLogistics: '投资/输入/物流(物流园区)/更新_7月物流站场投资明细.xls',
        investmentSystem: '投资/模板_查询结果(投资系统-客运+物流 2026.7).xls'
@@ -324,7 +362,8 @@
        cityTaxi: '/import/cityTaxi',
        cityTaxiAuth: '/import/cityTaxiAuth',
        wycTotal: '/import/wycTotal',
        wycOrder: '/import/wycOrder'
        wycOrder: '/import/wycOrder',
        wycOrderReport: '/import/wycOrderReport'
      }
      return map[t] || ''
    },
@@ -429,6 +468,7 @@
          this.$message.error((e.response && e.response.data && e.response.data.message) || '批量导入失败')
        })
    },
    acceptOf(type) { return type === 'wycOrderReport' ? '.docx,.doc' : '.xlsx,.xls,.et' },
    isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    isDirOnlyType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    importCityDir(type) {
@@ -515,10 +555,49 @@
              map[key] = { totalRows: r.totalRows, successRows: r.successRows, failRows: r.failRows, latestId: r.id }
            }
          })
          if (map.wycOrderReport && !map.wycOrder) map.wycOrder = map.wycOrderReport
          this.overview = map
        })
        .catch(e => { if (!e || !e._toast) this.$message.error('导入记录加载失败,请刷新重试') })
        .finally(() => { this.batchLoading = false })
    },
    openClearDialog() {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.importing) { this.$message.warning('正在导入数据,请等待导入完成后再清空'); return }
      this.clearScope = 'all'
      this.clearTypes = []
      this.clearDialog = true
    },
    clearableGroups() {
      return this.groups.filter(g => !g.hidden)
    },
    confirmClear() {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.clearing) return
      if (this.clearScope === 'types' && !this.clearTypes.length) { this.$message.warning('请选择要清空的数据类型'); return }
      const scopeText = this.clearScope === 'all'
        ? '该报表期全部已导入数据'
        : '以下 ' + this.clearTypes.length + ' 个类型:' + this.clearTypes.map(t => this.typeLabel(t)).join('、')
      this.$confirm('将清空 ' + this.period + ' 的' + scopeText + ',并同步删除对应审核结果、已审核标记与导入批次记录。此操作不可撤销,是否继续?',
        '清空确认', { type: 'warning', confirmButtonText: '确认清空', cancelButtonText: '取消' })
        .then(() => {
          this.clearing = true
          const params = { period: this.period }
          if (this.clearScope === 'types') params.types = this.clearTypes.join(',')
          return api.post('/import/clear', null, { params, timeout: 600000 })
            .then(res => {
              const d = (res && res.data) || {}
              this.$message.success('已清空 ' + this.period + ' 数据:业务数据 ' + (d.dataRows || 0) +
                ' 行、审核结果 ' + (d.auditRows || 0) + ' 行、导入批次 ' + (d.batchRows || 0) + ' 条,可以重新导入')
              this.clearDialog = false
              this.loadData()
            })
            .catch(e => {
              if (e && e._toast) return
              this.$message.error((e.response && e.response.data && e.response.data.message) || '清空失败,请稍后重试')
            })
            .finally(() => { this.clearing = false })
        }).catch(() => {})
    },
    batchKeyToType(k) {
      if (BATCH_TYPE_MAP[k]) return BATCH_TYPE_MAP[k]
@@ -591,6 +670,12 @@
.import-side.side-open .side-toggle-text { writing-mode: horizontal-tb; letter-spacing: normal; margin-top: 0; }
.side-content { padding: 8px 12px 12px; max-height: calc(100vh - 220px); overflow-y: auto; overscroll-behavior: contain; }
.side-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-weight: 600; margin-bottom: 6px; }
.side-danger { display: flex; align-items: center; gap: 8px; margin: 4px 0 10px; padding: 6px 8px; background: #FEF0F0; border: 1px solid #FDE2E2; border-radius: 4px; }
.side-danger-tip { font-size: 12px; color: #F56C6C; line-height: 1.3; }
.clear-dlg { line-height: 1.7; }
.clear-period { margin-bottom: 6px; }
.clear-types { margin-top: 10px; }
.clear-warn { margin-top: 12px; padding: 8px 10px; background: #FEF0F0; border: 1px solid #FDE2E2; border-radius: 4px; color: #F56C6C; font-size: 12px; line-height: 1.6; }
.import-side .overview-grid { display: block; }
.import-side .ov-row { display: block; margin-bottom: 6px; }
.import-side .ov-group { width: auto; }