traffic-audit-web/src/views/DataImport.vue
@@ -1,4 +1,4 @@
<template>
SKIP/CHECK (0): TYPE_LABELS upper <template>
  <el-container direction="vertical">
    <app-header title="交通统计报表审核系统 - 数据导入"></app-header>
    <el-container>
@@ -29,10 +29,10 @@
            <div class="card-head">选择数据类型</div>
            <div class="module-tabs-head">
              <el-radio-group v-model="activeModule" size="small" @change="onModuleSwitch">
                <el-radio-button v-for="g in groups" :key="g.key" :label="g.key">{{ g.name }}</el-radio-button>
                <el-radio-button v-for="g in visibleGroups" :key="g.key" :label="g.key">{{ g.name }}</el-radio-button>
              </el-radio-group>
            </div>
            <div v-for="g in groups" :key="g.key" v-show="activeModule === g.key" class="module-tab-pane">
            <div v-for="g in visibleGroups" :key="g.key" v-show="activeModule === g.key" class="module-tab-pane">
              <div class="module-sel">
                                <div class="module-sel-row">
                  <el-select v-model="moduleSel[g.key]" placeholder="选择数据类型" style="width:260px"
@@ -51,15 +51,15 @@
                  <el-button size="mini" type="primary" icon="el-icon-upload2" :loading="cityDirLoading"
                             @click="importCityDir(moduleSel[g.key])">从目录批量导入</el-button>
                </div>
                <div v-else-if="moduleSel[g.key]" class="module-file">
                  <div class="file-name" :title="fileNames[moduleSel[g.key]]">{{ fileNames[moduleSel[g.key]] || '未选择文件' }}</div>
                <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" accept=".xlsx,.xls"
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple accept=".xlsx,.xls,.et"
                               :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">选择文件</el-button>
                      <el-button size="mini" type="primary" plain icon="el-icon-folder-opened" :disabled="importing">选择文件</el-button>
                    </el-upload>
                    <el-button size="mini" type="primary" icon="el-icon-upload2" :disabled="!files[moduleSel[g.key]]"
                               @click="importNow(g.key)">立即导入</el-button>
                    <el-button size="mini" type="primary" icon="el-icon-upload2" :loading="importing" :disabled="importing || !fileCount(moduleSel[g.key])"
                               @click="importNow(g.key)">{{ fileCount(moduleSel[g.key]) > 1 ? '批量导入 ' + fileCount(moduleSel[g.key]) + ' 个文件' : '立即导入' }}</el-button>
                  </div>
                </div>
              </div>
@@ -107,7 +107,7 @@
                </div>
                <div v-if="!period" class="tip">请先在上方选择报表期</div>
                <div v-else class="overview-grid">
                  <div class="ov-row" v-for="g in groups" :key="g.key">
                  <div class="ov-row" v-for="g in visibleGroups" :key="g.key">
                    <div class="ov-group">{{ g.name }}</div>
                    <div class="ov-item" v-for="t in g.types" :key="t.value" :class="overviewState(t.value).cls">
                      <div class="ov-name">{{ t.label }}</div>
@@ -134,10 +134,11 @@
  investCityPassenger: '市州单表(客运站场)', investCityLogistics: '市州单表(物流园区)',
  'INVEST_客运站场_CITY': '市州单表(客运站场)', 'INVEST_物流园区_CITY': '市州单表(物流园区)',
  cityBus: '城市公交月报', cityTaxi: '巡游出租月报', cityTaxiAuth: '出租车运政车辆',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量',
  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: '出租车运政车辆'
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量'
}
const BATCH_TYPE_MAP = {
  H2032: 'h2032', TRANSPORT_AUTH: 'transportAuth', TRACK_MILEAGE: 'trackMileage',
@@ -145,6 +146,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',
  INVEST_SYSTEM: 'investmentSystem'
}
export default {
@@ -155,6 +157,7 @@
      importType: 'h2032',
      period: (() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') })(),
      cityDirLoading: false,
      importing: false,
      sideOpen: false,
      result: '',
      error: '',
@@ -167,8 +170,8 @@
      overview: {},
      groups: [
        {
          key: 'freight', name: '货运', types: [
            { value: 'h2032', label: 'H203-2月报' },
          key: 'freight', name: '道路货运', types: [
            { value: 'h2032', label: '道路货物运输月度生产情况' },
            { value: 'transportAuth', label: '运政车辆' },
            { value: 'trackMileage', label: '轨迹里程' },
            { value: 'scaleSplit', label: '规上规下拆分' },
@@ -177,20 +180,22 @@
        },
        {
          key: 'passenger', name: '公路旅客', types: [
            { value: 'passengerMonthly', label: 'H203-1旅客月报' },
            { value: 'passengerMonthly', label: '公路旅客运输月度生产情况' },
            { value: 'passengerAuth', label: '运政车辆数' },
            { value: 'passengerIndividual', label: '个体客运量/周转量' }
          ]
        },
        {
          key: 'city', name: '城市客运', types: [
            { value: 'cityBus', label: '城市公交月报' },
            { value: 'cityTaxi', label: '巡游出租月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' }
            { value: 'cityBus', label: '城市公共交通月度运营情况' },
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' },
            { value: 'wycTotal', label: '网约车总数' },
            { value: 'wycOrder', label: '网约车订单及全省总量' }
          ]
        },
        {
          key: 'invest', name: '投资', types: [
          key: 'invest', name: '站场投资', types: [
            { value: 'investment', label: '投资月报(客运站场)' },
            { value: 'investmentLogistics', label: '投资月报(物流园区)' },
            { value: 'investmentSystem', label: '投资系统导出' },
@@ -199,15 +204,32 @@
          ]
        },
        {
          key: 'energy', name: '能耗', types: [
            { value: 'energyMonthly', label: 'H204能耗季报' },
          key: 'energy', name: '货运能耗季报', types: [
            { value: 'energyMonthly', label: '道路货运车辆能源消耗情况' },
            { value: 'energyAuth', label: '能耗车辆运政信息' }
          ]
        },
        {
          key: 'annual', hidden: true, name: '年报', types: [
            { value: 'annualCompanyBasic', label: '单位基本情况' },
            { value: 'annualEnterpriseFinance', label: '法人企业财务状况' },
            { value: 'annualPassengerEnergy', label: '公路旅客运输能源消耗情况' },
            { value: 'annualBusOperation', label: '公共汽电车运营情况' },
            { value: 'annualMetroVehicles', label: '城市轨道交通车辆及线路情况' },
            { value: 'annualMetroStaff', label: '城市轨道交通人员及运营情况' },
            { value: 'annualFerry', label: '城市客运轮渡运营情况' },
            { value: 'annualPublicEnergy', label: '公共交通能源消耗情况' },
            { value: 'annualCityInfrastructureOperators', label: '城市客运基础设施情况、城市客运经营业户情况' },
            { value: 'annualTaxiOwnership', label: '巡游出租汽车营运车辆拥有情况' },
            { value: 'annualTaxiProduction', label: '巡游出租汽车运输生产情况' }
          ]
        }
      ]
    }
  },
  computed: {
    // 年报暂不开放:隐藏入口(数据保留,后端实现后可移除 hidden)
    visibleGroups() { return this.groups.filter(g => !g.hidden) },
    failBatches() { return this.batches.filter(b => b.failRows > 0) },
    failRowsFlat() {
      const out = []
@@ -229,7 +251,7 @@
        scaleSplit: '模板_规上规下拆分运输量.xlsx',
        freightTurnover: '模板_货运量周转量.xlsx',
        passengerMonthly: '公路旅客运输月度生产情况.xlsx(含运政车辆数sheet)',
        passengerAuth: '模板_公路旅客运政车辆数.xlsx(列:企业代码/企业名称/车辆数/载客位数)',
        passengerAuth: '模板_公路旅客运政车辆数.xls(支持 企业代码/企业名称/车辆数/载客位数 与 运政库导出 OWNERNAME/总车辆数/客位数)',
        passengerIndividual: '个体客运量汇总.xlsx(列:所属地区/企业代码/企业名称/个体客运量/个体旅客周转量)',
        energyMonthly: '道路货运车辆能源消耗情况(2026年).xlsx(能耗明细+运政车辆信息两sheet)',
        energyAuth: '模板_能耗车辆运政信息.xlsx(列:车牌号/车辆类型/燃料类型/标记吨位/准牵引质量)',
@@ -240,7 +262,9 @@
        investCityLogistics: '—(批量扫描目录,无需模板)',
        cityBus: '导入_城市公共交通月度运营情况(YYYY年M月).xls/.xlsx(企业级明细,取公共汽电车字段)',
        cityTaxi: '导入_巡游出租汽车运营服务情况月报(YYYY年M月).xls(市州级17行)',
        cityTaxiAuth: '导入模板_出租车信息(运政车辆信息).xlsx(车辆级明细,用于运营车数对比)'
        cityTaxiAuth: '导入模板_出租车信息(运政车辆信息).xlsx(车辆级明细,用于运营车数对比)',
        wycTotal: '模板_网约车总数.xlsx(A列=月份,B-E=全省客运量/城市内/周转量/城市内周转量)',
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)'
      }
      return map[this.importType] || ''
    },
@@ -252,13 +276,15 @@
        scaleSplit: '货运/模板_规上规下拆分运输量.xlsx',
        freightTurnover: '货运/模板_货运量周转量.xlsx',
        passengerMonthly: '公路旅客/模板_交企统H203-1表_公路旅客运输月度生产情况.xlsx',
        passengerAuth: '公路旅客/模板_公路旅客运政车辆数.xlsx',
        passengerAuth: '公路旅客/模板_公路旅客运政车辆数.xls',
        passengerIndividual: '公路旅客/个体客运量汇总.xlsx',
        energyMonthly: '能耗/模板_道路货运车辆能源消耗情况(2026).xlsx',
        energyAuth: '能耗/模板_能耗车辆运政信息.xlsx',
        cityBus: '城市客运/导入模板_城市公交.xlsx',
        cityTaxi: '城市客运/导入模板_巡游出租.xlsx',
        cityBus: '城市客运/生成_城市公交客运量分市州明细.xlsx',
        cityTaxi: '城市客运/生成_巡游出租客运量分市州明细.xlsx',
        cityTaxiAuth: '城市客运/导入模板_出租车信息(运政车辆信息).xlsx',
        wycTotal: '城市客运/模板_网约车总数.xlsx',
        wycOrder: '城市客运/模板_网约车订单及全省总量.xlsx',
        investment: '投资/输入/客运(客运站)/更新_7月客运站投资明细表(市州汇总大表,给领导).xlsx',
        investmentLogistics: '投资/输入/物流(物流园区)/更新_7月物流站场投资明细.xls',
        investmentSystem: '投资/模板_查询结果(投资系统-客运+物流 2026.7).xls'
@@ -296,28 +322,42 @@
        investmentSystem: '/import/investmentSystem',
        cityBus: '/import/cityBus',
        cityTaxi: '/import/cityTaxi',
        cityTaxiAuth: '/import/cityTaxiAuth'
        cityTaxiAuth: '/import/cityTaxiAuth',
        wycTotal: '/import/wycTotal',
        wycOrder: '/import/wycOrder'
      }
      return map[t] || ''
    },
    onFilePicked(key, file) {
      this.$set(this.files, this.moduleSel[key], file.raw)
      this.$set(this.fileNames, this.moduleSel[key], file.name)
      const type = this.moduleSel[key]
      if (!type) return
      if (this.importing) { this.$message.warning('正在导入中,请稍候再选择文件'); return }
      const list = (this.files[type] || []).filter(f => f.name !== file.name)
      list.push(file.raw)
      this.$set(this.files, type, list)
      this.$set(this.fileNames, type, list.map(f => f.name))
      const up = this.$refs['up' + key]
      if (up && up.clearFiles) up.clearFiles()
    },
    importNow(key) {
      const type = this.moduleSel[key]
      if (!type) return
      if (this.isCityDirType(type)) {
      if (this.isDirOnlyType(type)) {
        this.importCityDir(type)
        this.moduleSel[key] = ''
        this.importType = ''
        return
      }
      const file = this.files[type]
      if (!file) { this.$message.warning('请先为该类型选择文件'); return }
      const fileList = this.files[type] || []
      if (!fileList.length) { this.$message.warning('请先为该类型选择文件'); return }
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      if (this.importing) { this.$message.warning('正在导入中,请稍候…'); return }
      this.importing = true
      if (fileList.length > 1 || type === 'investment' || type === 'investmentLogistics') {
        this.batchImport(type, key, fileList).finally(() => { this.importing = false })
        return
      }
      const file = fileList[0]
      const g = this.groups.find(x => x.key === key)
      const t = g.types.find(x => x.value === type)
      const row = {
@@ -340,12 +380,59 @@
        this.importType = ''
        this.files[type] = null
        this.$delete(this.fileNames, type)
      })
      }).finally(() => { this.importing = false })
    },
    fileCount(type) { return (this.files[type] || []).length },
    fileNamesText(type) {
      const list = this.fileNames[type] || []
      return list.length ? '已选 ' + list.length + ' 个文件:' + list.join('、') : ''
    },
    batchImport(type, key, fileList) {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      const fd = new FormData()
      fd.append('type', type)
      fd.append('period', this.period)
      fileList.forEach(f => fd.append('files', f))
      const g = this.groups.find(x => x.key === key)
      const label = g ? g.name : type
      this.$message({ message: '正在批量导入 ' + fileList.length + ' 个文件…', duration: 3000 })
      return api.post('/import/batch', fd, { timeout: 600000 })
        .then(res => {
          const d = (res && res.data) || {}
          if (!d.files) {
            this.$message.error((res && res.message) || '批量导入失败')
            return
          }
          const files = d.files || []
          const errCount = files.filter(f => f.error || f.success === 0).length
          const html = files.map(f =>
            '<div style="margin:6px 0;line-height:1.6"><b>' + (f.fileName || '') + '</b>' +
            (f.city ? '(' + f.city + ')' : '') + ':解析 ' + (f.success || 0) + ' 条' +
            (f.error ? ' <span style="color:#F56C6C">' + f.error + '</span>' : '') + '</div>'
          ).join('')
          this.$alert(html, (label || type) + ' 批量导入完成:共 ' + d.total + ' 个文件' +
            (d.fail > 0 ? ',失败 ' + d.fail + ' 条' : '') + '(成功 ' + d.success + ' 条)', {
            dangerouslyUseHTMLString: true,
            type: errCount > 0 ? 'warning' : 'success',
            confirmButtonText: '知道了'
          })
          if (type === 'investment' || type === 'investmentLogistics') {
            this.checkInvest(this.period)
          }
          this.afterImport()
          this.files[type] = null
          this.$delete(this.fileNames, type)
        })
        .catch(e => {
          if (e && e._toast) return
          this.$message.error((e.response && e.response.data && e.response.data.message) || '批量导入失败')
        })
    },
    isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    isDirOnlyType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    importCityDir(type) {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      const category = type === 'investCityPassenger' ? '客运站场' : '物流园区'
      const category = (type === 'investCityPassenger' || type === 'investment') ? '客运站场' : '物流园区'
      const fd = new FormData()
      fd.append('period', this.period)
      fd.append('category', category)