traffic-audit-web/src/views/DataImport.vue
@@ -12,41 +12,193 @@
          <el-menu-item index="/report">报表生成</el-menu-item>
          <el-menu-item index="/rules">规则管理</el-menu-item>
          <el-menu-item index="/users">用户管理</el-menu-item>
        </el-menu>
</el-menu>
      </el-aside>
      <el-main>
        <div class="page-scroll">
        <h3>导入业务数据</h3>
        <el-card style="max-width:640px">
          <el-form label-width="100px">
            <el-form-item label="导入类型">
              <el-radio-group v-model="importType">
                <el-radio label="h2032">H203-2月报</el-radio>
                <el-radio label="transportAuth">运政车辆</el-radio>
                <el-radio label="trackMileage">轨迹里程</el-radio>
                <el-radio label="scaleSplit">规上规下拆分</el-radio>
                <el-radio label="freightTurnover">货运量周转量</el-radio>
              </el-radio-group>
            </el-form-item>
            <el-form-item label="报表期">
              <el-date-picker v-model="period" type="month" placeholder="选择月份" value-format="yyyy-MM" style="width:100%"></el-date-picker>
            </el-form-item>
            <el-form-item label="选择文件">
              <el-upload ref="upload" :auto-upload="false" :limit="1" accept=".xlsx,.xls"
                         :on-change="handleFileChange" :file-list="fileList">
                <el-button slot="trigger" type="primary">选择 Excel 文件</el-button>
              </el-upload>
              <div class="tip">请使用对应模板:{{ templateName }}</div>
            </el-form-item>
            <el-form-item>
              <el-button type="success" @click="importData" :loading="loading">开始导入</el-button>
            </el-form-item>
          </el-form>
        </el-card>
        <el-alert v-if="result" type="success" style="margin-top:15px" closable @close="result=''">
          <div style="white-space: pre-line">{{ result }}</div>
        </el-alert>
        <el-alert v-if="error" :title="error" type="error" style="margin-top:15px" closable @close="error=''"></el-alert>
          <h3>导入业务数据</h3>
          <el-card shadow="hover" class="import-card" style="margin-bottom:15px">
            <div class="card-head">报表期</div>
            <el-date-picker v-model="period" type="month" placeholder="选择月份" value-format="yyyy-MM"
                            style="width:240px" @change="onPeriodChange"></el-date-picker>
            <div class="tip">选择报表期后,下方显示该月各模块的导入进度;各模块下拉互不影响,可先选好类型、挂载文件,再加入待导入队列统一执行。</div>
          </el-card>
          <el-card shadow="hover" class="import-card" style="margin-bottom:15px">
            <div class="card-head">本月导入进度({{ period || '未选择报表期' }})
              <el-button type="text" icon="el-icon-refresh" style="float:right" @click="loadOverview">刷新</el-button>
            </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-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>
                  <div class="ov-status">{{ overviewState(t.value).text }}</div>
                </div>
              </div>
            </div>
          </el-card>
          <el-card shadow="hover" class="import-card" style="margin-bottom:15px">
            <div class="card-head">选择类型并挂载文件
              <span class="tip" style="float:right;line-height:24px">各模块互不影响:先选类型、挂载文件,可「立即导入」或「加入队列」统一执行;也可在下方粘贴目录路径点「导入该目录」,文件夹里多个月的 Excel 会按文件名自动识别月份、分别导入对应报表期</span>
            </div>
            <el-tabs v-model="activeModule" class="module-tabs">
              <el-tab-pane v-for="g in groups" :key="g.key" :label="g.name" :name="g.key">
                <div class="module-sel">
                  <el-select v-model="moduleSel[g.key]" placeholder="选择导入类型" style="width:260px"
                             @change="onModuleChange(g.key)" clearable>
                    <el-option v-for="t in g.types" :key="t.value" :label="t.label" :value="t.value"></el-option>
                  </el-select>
                  <div v-if="isCityDirType(moduleSel[g.key])" class="module-file">
                    <div class="tip">无需选文件:自动扫描 docs/投资/输入/…/{{ period ? Number(period.slice(5)) : 'X' }}月 目录下全部市州报表,逐文件容错解析入库(项目主档+月度,含本月完成/建设阶段/形象进度;十五五项目自动标记)。</div>
                    <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 class="file-btns">
                      <el-upload action="#" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls"
                                 :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-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="success" plain icon="el-icon-plus" :disabled="!files[moduleSel[g.key]]"
                                 @click="addToQueue(g.key)">加入队列</el-button>
                    </div>
                    <div class="dir-import-bar">
                      <el-input v-model="dirPath" size="small" placeholder="粘贴目录路径(如 D:\数据\2026),点「导入该目录」" style="width:330px"
                                @keyup.enter.native="quickDirImport(moduleSel[g.key])" clearable></el-input>
                      <el-button size="mini" type="warning" icon="el-icon-folder" :loading="dirImporting"
                                 @click="quickDirImport(moduleSel[g.key])">导入该目录</el-button>
                      <el-button size="mini" type="text" icon="el-icon-search" @click="openDirBrowser(g.key)">浏览…</el-button>
                      <el-checkbox v-model="dirSkipImported" size="mini">跳过已导入文件</el-checkbox>
                    </div>
                    <div class="tip">目录导入:文件夹里的 Excel 会按文件名自动识别月份(支持 2026年7月 / 2026.7 / 2026-07 / 7月),分别导入对应报表期;识别不到月份的文件会报错提示。也可点「浏览…」查找目录。</div>
                    <div v-if="dirImportFiles.length" class="dir-progress">
                      <div class="card-head" style="font-size:13px">目录导入进度</div>
                      <div v-for="f in dirImportFiles" :key="f.name" class="dir-progress-row">
                        <i :class="progIcon(f.status)"></i>
                        <span class="prog-name" :title="f.name">{{ f.name }}</span>
                        <el-tag v-if="f.period" size="mini" type="info">{{ f.period }}</el-tag>
                        <el-tag size="mini" :type="progTag(f.status)">{{ progText(f.status) }}</el-tag>
                        <span v-if="f.msg" class="queue-msg" :style="f.status === 'fail' ? 'color:#F56C6C' : 'color:#67C23A'">{{ f.msg }}</span>
                      </div>
                    </div>
                  </div>
                </div>
              </el-tab-pane>
            </el-tabs>
            <div v-if="importType" class="type-info">
              <div>模板:{{ templateName }}
                <el-link v-if="templateUrl" type="primary" icon="el-icon-download" :underline="false"
                         style="margin-left:8px" @click="downloadTemplate">下载模板</el-link>
              </div>
              <div class="tip">{{ typeDesc }}</div>
              <span v-if="importType === 'cityTaxiAuth'" class="slow-tip">该类型约 4 万+ 行,预计耗时 5 分钟左右</span>
            </div>
          </el-card>
          <el-card v-if="queue.length" ref="queueCard" shadow="hover" class="import-card" style="margin-bottom:15px">
            <div class="card-head">
              待导入队列({{ queue.length }} 项)
              <el-button type="success" size="small" icon="el-icon-video-play" style="float:right"
                         :loading="queueLoading" :disabled="!pendingCount" @click="importQueue">全部导入({{ pendingCount }})</el-button>
            </div>
            <el-table :data="queue" border size="small" style="width:100%">
              <el-table-column label="模块" width="90">
                <template slot-scope="s">{{ s.row.moduleName }}</template>
              </el-table-column>
              <el-table-column label="导入类型" width="150">
                <template slot-scope="s">{{ s.row.typeLabel }}</template>
              </el-table-column>
              <el-table-column label="文件" min-width="200" show-overflow-tooltip>
                <template slot-scope="s">{{ s.row.fileName }}</template>
              </el-table-column>
              <el-table-column label="状态" width="110" align="center">
                <template slot-scope="s">
                  <el-tag v-if="s.row.status === 'pending'" type="info" size="mini">待导入</el-tag>
                  <el-tag v-else-if="s.row.status === 'uploading'" type="warning" size="mini" effect="dark">导入中…</el-tag>
                  <el-tag v-else-if="s.row.status === 'success'" type="success" size="mini">成功</el-tag>
                  <el-tag v-else type="danger" size="mini">失败</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="结果" min-width="240">
                <template slot-scope="s">
                  <span class="queue-msg">{{ s.row.message }}</span>
                </template>
              </el-table-column>
              <el-table-column label="操作" width="130" align="center">
                <template slot-scope="s">
                  <el-button v-if="s.row.status === 'pending'" size="mini" type="primary" plain @click="importOne(s.row)">导入</el-button>
                  <el-button v-if="s.row.status !== 'uploading'" size="mini" type="danger" plain icon="el-icon-delete"
                             @click="removeQueueItem(s.row)"></el-button>
                </template>
              </el-table-column>
            </el-table>
            <div class="tip" style="margin-top:8px">导入为逐个执行;同报表期重复导入会先删除旧数据再写入(幂等,不会重复存两份)。</div>
          </el-card>
          <el-dialog :title="'浏览目录 — ' + (dirTypeLabel || '')" :visible.sync="dirDialog" width="640px" top="6vh" append-to-body>
            <div class="dir-toolbar">
              <el-button size="mini" type="text" icon="el-icon-refresh" :disabled="dirLoading" @click="loadDirs(dirCurrent)">刷新</el-button>
              <el-button size="mini" type="text" icon="el-icon-folder-opened" @click="loadDirs('')">数据目录 docs</el-button>
              <el-button size="mini" type="text" icon="el-icon-back" :disabled="!dirParent" @click="goDirParent">上级目录</el-button>
            </div>
            <div class="dir-path" :title="dirCurrent">{{ dirCurrent || '(加载中…)' }}</div>
            <div class="dir-body" v-loading="dirLoading">
              <div v-if="!dirDirs.length" class="dir-empty">当前目录下没有子文件夹,可直接选择当前文件夹导入</div>
              <div v-for="d in dirDirs" :key="d.path" class="dir-row" :title="d.path + '(双击立即导入该文件夹)'" @click="loadDirs(d.path)" @dblclick="dblImportDir(d)">
                <i class="el-icon-folder"></i><span>{{ d.name }}</span>
              </div>
            </div>
            <div class="dir-files">
              <div class="tip">单击文件夹进入子文件夹;<b>双击目标文件夹立即导入</b>其中全部 Excel;也可进入后点下方按钮。导入时会按文件名自动识别月份。</div>
              <div v-if="dirExcelCount === 0" class="tip">当前文件夹内没有 Excel 文件(.xlsx/.xls)</div>
              <div v-else class="tip">当前文件夹内 Excel:{{ dirExcelCount }} 个 —— {{ dirFiles.slice(0, 6).join('、') }}{{ dirFiles.length > 6 ? ' 等' : '' }}(仅导入直接位于该文件夹的文件)</div>
            </div>
            <span slot="footer">
              <el-button size="small" @click="dirDialog = false">取消</el-button>
              <el-button size="small" type="primary" :disabled="dirExcelCount === 0 || dirImporting" :loading="dirImporting" @click="confirmDirImport">导入当前目录</el-button>
            </span>
          </el-dialog>
          <el-card shadow="never" style="margin-top:15px" class="batch-card">
            <div slot="header" class="card-head">
              导入记录({{ period || '全部报表期' }})
              <el-button type="text" icon="el-icon-refresh" style="float:right" @click="loadBatches">刷新</el-button>
            </div>
            <el-table :data="batches" border size="small" v-loading="batchLoading" style="width:100%">
              <el-table-column prop="importType" label="导入类型" width="150">
                <template slot-scope="s">{{ typeLabel(s.row.importType) }}</template>
              </el-table-column>
              <el-table-column prop="reportPeriod" label="报表期" width="100"></el-table-column>
              <el-table-column prop="fileName" label="文件名" min-width="220" show-overflow-tooltip></el-table-column>
              <el-table-column prop="totalRows" label="总行数" width="90"></el-table-column>
              <el-table-column prop="successRows" label="成功" width="80">
                <template slot-scope="s">
                  <span style="color:#67C23A" v-if="s.row.successRows > 0">{{ s.row.successRows }}</span>
                  <span v-else>0</span>
                </template>
              </el-table-column>
              <el-table-column prop="failRows" label="失败" width="70">
                <template slot-scope="s">
                  <el-tag v-if="s.row.failRows > 0" size="mini" type="danger">{{ s.row.failRows }}</el-tag>
                  <span v-else style="color:#909399">0</span>
                </template>
              </el-table-column>
              <el-table-column prop="createdAt" label="导入时间" width="165"></el-table-column>
              <el-table-column label="状态" width="90">
                <template slot-scope="s">
                  <el-tag v-if="s.row.failRows > 0" size="mini" type="warning" effect="plain">部分失败</el-tag>
                  <el-tag v-else-if="s.row.successRows > 0" size="mini" type="success" effect="plain">成功</el-tag>
                  <el-tag v-else size="mini" type="info" effect="plain">无数据</el-tag>
                </template>
              </el-table-column>
            </el-table>
          </el-card>
        </div>
      </el-main>
    </el-container>
@@ -55,63 +207,555 @@
<script>
import AppHeader from '@/components/AppHeader.vue'
import api from '@/api'
const TYPE_LABELS = {
  h2032: 'H203-2月报', transportAuth: '运政车辆', trackMileage: '轨迹里程', scaleSplit: '规上规下拆分',
  freightTurnover: '货运量周转量', passengerMonthly: 'H203-1旅客月报', passengerIndividual: '个体客运量/周转量',
  passengerAuth: '旅客运政车辆数', energyMonthly: 'H204能耗季报', energyAuth: '能耗车辆运政信息',
  investment: '投资月报(客运站场)', investmentLogistics: '投资月报(物流园区)', investmentSystem: '投资系统导出',
  investCityPassenger: '市州单表(客运站场)', investCityLogistics: '市州单表(物流园区)',
  'INVEST_客运站场_CITY': '市州单表(客运站场)', 'INVEST_物流园区_CITY': '市州单表(物流园区)',
  cityBus: '城市公交月报', cityTaxi: '巡游出租月报', cityTaxiAuth: '出租车运政车辆',
  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: '出租车运政车辆'
}
const BATCH_TYPE_MAP = {
  H2032: 'h2032', TRANSPORT_AUTH: 'transportAuth', TRACK_MILEAGE: 'trackMileage',
  SCALE_SPLIT: 'scaleSplit', FREIGHT_TURNOVER: 'freightTurnover',
  H2031: 'passengerMonthly', PASSENGER_AUTH: 'passengerAuth', PASSENGER_INDIVIDUAL: 'passengerIndividual',
  H204: 'energyMonthly', ENERGY_AUTH: 'energyAuth',
  CITY_BUS: 'cityBus', CITY_TAXI: 'cityTaxi', CITY_TAXI_AUTH: 'cityTaxiAuth',
  INVEST_SYSTEM: 'investmentSystem'
}
export default {
  components: { AppHeader },
  name: 'DataImport',
  data() {
    return {
      importType: 'h2032',
      period: '',
      file: null,
      fileList: [],
      loading: false,
      period: (() => { const d = new Date(); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') })(),
      queue: [],
      queueLoading: false,
      cityDirLoading: false,
      dirDialog: false,
      dirKey: '',
      dirCurrent: '',
      dirParent: '',
      dirDirs: [],
      dirExcelCount: 0,
      dirFiles: [],
      dirLoading: false,
      dirImporting: false,
      dirPath: '',
      dirImportFiles: [],
      dirSkipImported: true,
      result: '',
      error: ''
      error: '',
      batches: [],
      batchLoading: false,
      moduleSel: { freight: 'h2032', passenger: '', city: '', energy: '', invest: '' },
      activeModule: 'freight',
      files: {},
      fileNames: {},
      overview: {},
      groups: [
        {
          key: 'freight', name: '货运', types: [
            { value: 'h2032', label: 'H203-2月报' },
            { value: 'transportAuth', label: '运政车辆' },
            { value: 'trackMileage', label: '轨迹里程' },
            { value: 'scaleSplit', label: '规上规下拆分' },
            { value: 'freightTurnover', label: '货运量周转量' }
          ]
        },
        {
          key: 'passenger', name: '公路旅客', types: [
            { value: 'passengerMonthly', label: 'H203-1旅客月报' },
            { value: 'passengerAuth', label: '运政车辆数' },
            { value: 'passengerIndividual', label: '个体客运量/周转量' }
          ]
        },
        {
          key: 'city', name: '城市客运', types: [
            { value: 'cityBus', label: '城市公交月报' },
            { value: 'cityTaxi', label: '巡游出租月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' }
          ]
        },
        {
          key: 'invest', name: '投资', types: [
            { value: 'investment', label: '投资月报(客运站场)' },
            { value: 'investmentLogistics', label: '投资月报(物流园区)' },
            { value: 'investmentSystem', label: '投资系统导出' },
            { value: 'investCityPassenger', label: '市州单表批量导入(客运站场)' },
            { value: 'investCityLogistics', label: '市州单表批量导入(物流园区)' }
          ]
        },
        {
          key: 'energy', name: '能耗', types: [
            { value: 'energyMonthly', label: 'H204能耗季报' },
            { value: 'energyAuth', label: '能耗车辆运政信息' }
          ]
        }
      ]
    }
  },
  computed: {
    pendingCount() { return this.queue.filter(x => x.status === 'pending').length },
    dirTypeLabel() {
      const t = this.moduleSel[this.dirKey]
      if (!t) return ''
      for (const g of this.groups) {
        const found = g.types.find(x => x.value === t)
        if (found) return g.name + ' / ' + found.label
      }
      return t
    },
    typeDesc() {
      const map = {
        h2032: '企业级货运月报:用于规上货运量审核与分市州汇总(17 市州企业明细)。',
        transportAuth: '运政车辆档案:用于车辆信息核对(吨位/车型/燃料类型)。',
        trackMileage: '轨迹里程数据:用于里程与运距校验。',
        scaleSplit: '规上规下拆分:全省+17 市州货运量/周转量拆分结果。',
        freightTurnover: '货运量周转量:全省+17 市州月度合计(今年 1-N 月 + 去年)。',
        passengerMonthly: '公路旅客月报:企业级明细 + 运政车辆数两个 sheet(运政车辆数也可单独导入)。',
        passengerAuth: '旅客运政车辆数:独立模板(企业代码/企业名称/车辆数/载客位数),用于运营车辆核对;也可随 H203-1 月报第二个 sheet 一并导入。',
        passengerIndividual: '个体客运量/周转量:企业线下汇报数据(所属地区/企业代码/企业名称/个体客运量/个体旅客周转量),导入后自动填入分市州表“其中个体”行。',
        energyMonthly: '能耗季报:能耗明细 + 运政车辆信息两个 sheet(运政信息也可单独导入,季度末月生成汇总表)。',
        energyAuth: '能耗车辆运政信息:独立模板(车牌号/车辆类型/燃料类型/标记吨位/准牵引质量),用于能耗审核对比;也可随 H204 季报第二个 sheet 一并导入。',
        investment: '投资月报(客运站场):市州汇总大表,导入后生成 4 张领导报表。',
        investmentLogistics: '投资月报(物流园区):分项目投资完成情况 sheet。',
        investmentSystem: '投资系统导出:用于与月报数据跨表校验。',
        investCityPassenger: '无需选文件:自动扫描 docs/投资/输入/客运(客运站)/X月 目录全部市州报表,容错解析入库(含本月完成/建设阶段/形象进度,十五五项目自动标记)。',
        investCityLogistics: '无需选文件:自动扫描 docs/投资/输入/物流(物流园区)/X月 目录全部市州报表,容错解析入库(含本月完成/建设阶段/形象进度,十五五项目自动标记)。',
        cityBus: '城市公交月报:企业级运营数据,含轨道/轮渡字段。',
        cityTaxi: '巡游出租月报:市州级 17 行运营数据。',
        cityTaxiAuth: '出租车运政车辆:车辆级明细(约 4 万+ 行),用于运营车数对比。'
      }
      return map[this.importType] || ''
    },
    templateName() {
      const map = {
        h2032: '模板_交企统H203-2表_道路货物运输月度生产情况.xlsx',
        transportAuth: '模板_运政车辆.xlsx',
        trackMileage: '模板_轨迹里程.xlsx',
        scaleSplit: '模板_规上规下拆分运输量.xlsx',
        freightTurnover: '模板_货运量周转量.xlsx'
        freightTurnover: '模板_货运量周转量.xlsx',
        passengerMonthly: '公路旅客运输月度生产情况.xlsx(含运政车辆数sheet)',
        passengerAuth: '模板_公路旅客运政车辆数.xlsx(列:企业代码/企业名称/车辆数/载客位数)',
        passengerIndividual: '个体客运量汇总.xlsx(列:所属地区/企业代码/企业名称/个体客运量/个体旅客周转量)',
        energyMonthly: '道路货运车辆能源消耗情况(2026年).xlsx(能耗明细+运政车辆信息两sheet)',
        energyAuth: '模板_能耗车辆运政信息.xlsx(列:车牌号/车辆类型/燃料类型/标记吨位/准牵引质量)',
        investment: '7月客运站投资明细表(市州汇总大表,给领导).xlsx',
        investmentLogistics: '7月物流站场投资明细.xls(分项目投资完成情况sheet)',
        investmentSystem: '查询结果(投资系统-客运+物流 2026.7).xls',
        investCityPassenger: '—(批量扫描目录,无需模板)',
        investCityLogistics: '—(批量扫描目录,无需模板)',
        cityBus: '导入_城市公共交通月度运营情况(YYYY年M月).xls/.xlsx(企业级明细,取公共汽电车字段)',
        cityTaxi: '导入_巡游出租汽车运营服务情况月报(YYYY年M月).xls(市州级17行)',
        cityTaxiAuth: '导入_出租车信息(运政车辆信息).xls(车辆级明细,用于运营车数对比)'
      }
      return map[this.importType] || ''
    },
    endpoint() {
    templateUrl() {
      const map = {
        h2032: '货运/模板_交企统H203-2表_道路货物运输月度生产情况.xlsx',
        transportAuth: '货运/模板_运政车辆.xlsx',
        trackMileage: '货运/模板_轨迹里程.xlsx',
        scaleSplit: '货运/模板_规上规下拆分运输量.xlsx',
        freightTurnover: '货运/模板_货运量周转量.xlsx',
        passengerMonthly: '公路旅客/模板_交企统H203-1表_公路旅客运输月度生产情况.xlsx',
        passengerAuth: '公路旅客/模板_公路旅客运政车辆数.xlsx',
        passengerIndividual: '',
        energyMonthly: '能耗/模板_道路货运车辆能源消耗情况(2026).xlsx',
        energyAuth: '能耗/模板_能耗车辆运政信息.xlsx',
        cityBus: '城市客运/导入模板_城市公交.xlsx',
        cityTaxi: '城市客运/导入模板_巡游出租.xlsx',
        cityTaxiAuth: '城市客运/导入模板_出租车信息(运政车辆信息).xls',
        investment: '投资/输入/客运(客运站)/更新_7月客运站投资明细表(市州汇总大表,给领导).xlsx',
        investmentLogistics: '投资/输入/物流(物流园区)/更新_7月物流站场投资明细.xls',
        investmentSystem: '投资/模板_查询结果(投资系统-客运+物流 2026.7).xls'
      }
      return map[this.importType] || ''
    }
  },
  methods: {
    typeLabel(t) { return TYPE_LABELS[t] || t },
    downloadTemplate() {
      if (!this.templateUrl) return
      window.open('/api/templates/' + this.templateUrl)
    },
    onModuleChange(key) {
      this.importType = this.moduleSel[key] || ''
      this.dirPath = (this.importType && localStorage.getItem('dirLast_' + this.importType)) || ''
    },
    onPeriodChange() { this.loadOverview(); this.loadBatches() },
    endpoint(t) {
      const map = {
        h2032: '/import/h2032',
        transportAuth: '/import/transportAuth',
        trackMileage: '/import/trackMileage',
        scaleSplit: '/import/scaleSplit',
        freightTurnover: '/import/freightTurnover'
        freightTurnover: '/import/freightTurnover',
        passengerMonthly: '/import/passengerMonthly',
        passengerAuth: '/import/passengerAuth',
        passengerIndividual: '/import/passengerIndividual',
        energyMonthly: '/import/energyMonthly',
        energyAuth: '/import/energyAuth',
        investment: '/import/investment',
        investmentLogistics: '/import/investment',
        investmentSystem: '/import/investmentSystem',
        cityBus: '/import/cityBus',
        cityTaxi: '/import/cityTaxi',
        cityTaxiAuth: '/import/cityTaxiAuth'
      }
      return map[this.importType]
      return map[t] || ''
    },
    openDirBrowser(key) {
      const type = this.moduleSel[key]
      if (!type) { this.$message.warning('请先选择导入类型'); return }
      this.dirKey = key
      this.dirDialog = true
      const last = localStorage.getItem('dirLast_' + type)
      this.loadDirs(last || '')
    },
    loadDirs(path) {
      this.dirLoading = true
      api.get('/import/dirs', { params: { path: path || '' } })
        .then(res => {
          const d = (res && res.data) || {}
          this.dirCurrent = d.current || ''
          this.dirParent = d.parent || ''
          this.dirDirs = d.dirs || []
          this.dirExcelCount = d.excelCount || 0
          this.dirFiles = d.files || []
        })
        .catch(e => { this.$message.error((e.response && e.response.data && e.response.data.message) || '目录加载失败') })
        .finally(() => { this.dirLoading = false })
    },
    goDirParent() { if (this.dirParent) this.loadDirs(this.dirParent) },
    dblImportDir(d) {
      this.dirDialog = false
      this.runDirImport(this.moduleSel[this.dirKey], d.path)
    },
    confirmDirImport() {
      if (!this.dirCurrent) return
      this.runDirImport(this.moduleSel[this.dirKey], this.dirCurrent)
    },
    quickDirImport(type) {
      if (!type) { this.$message.warning('请先选择导入类型'); return }
      this.runDirImport(type, this.dirPath)
    },
    runDirImport(type, dir) {
      if (!type) { this.$message.warning('请先选择导入类型'); return }
      if (!dir || !dir.trim()) { this.$message.warning('请先填写目录路径(或点「浏览…」选择)'); return }
      dir = dir.trim()
      this.dirImporting = true
      this.dirImportFiles = []
      api.get('/import/dirs', { params: { path: dir } })
        .then(res => {
          const d = (res && res.data) || {}
          const files = (d.files || []).filter(n => /\.(xlsx|xls)$/i.test(n))
          if (!files.length) { this.$message.warning('该目录没有 Excel 文件(.xlsx/.xls):' + dir); this.dirImporting = false; return }
          localStorage.setItem('dirLast_' + type, dir)
          this.dirPath = dir
          this.dirImportFiles = files.map(n => ({ name: n, status: 'pending', period: '', msg: '' }))
          const run = (i) => {
            if (i >= files.length) {
              this.dirImporting = false
              const ok = this.dirImportFiles.filter(x => x.status === 'success').length
              const fail = this.dirImportFiles.filter(x => x.status === 'fail').length
              const skip = this.dirImportFiles.filter(x => x.status === 'skipped').length
              if (fail > 0) this.$message.warning('目录导入完成:成功 ' + ok + ',失败 ' + fail + ',跳过 ' + skip + '(失败原因见列表)')
              else this.$message.success('目录导入完成:成功 ' + ok + ',跳过 ' + skip)
              this.afterImport()
              return
            }
            const row = this.dirImportFiles[i]
            row.status = 'importing'
            const fd = new FormData()
            fd.append('type', type)
            fd.append('period', this.period || '')
            fd.append('skipImported', this.dirSkipImported ? 'true' : 'false')
            fd.append('path', dir.replace(/[\\/]+$/, '') + '\\' + files[i])
            api.post('/import/fromDirFile', fd, { timeout: 600000 })
              .then(res => {
                const r = (res && res.data) || {}
                row.period = r.period || ''
                if (r.skipped) { row.status = 'skipped'; row.msg = r.error || '已跳过' }
                else if (r.fail > 0 || r.error) { row.status = 'fail'; row.msg = r.error || ('失败 ' + r.fail + ' 条') }
                else { row.status = 'success'; row.msg = '解析 ' + r.success + ' 条' }
              })
              .catch(e => { row.status = 'fail'; row.msg = (e.response && e.response.data && e.response.data.message) || '导入失败' })
              .finally(() => run(i + 1))
          }
          run(0)
        })
        .catch(e => {
          this.dirImporting = false
          this.$message.error((e.response && e.response.data && e.response.data.message) || '目录不存在或无法访问:' + dir)
        })
    },
    progText(s) { return s === 'pending' ? '待导入' : s === 'importing' ? '导入中…' : s === 'success' ? '成功' : s === 'skipped' ? '已跳过' : '失败' },
    progTag(s) { return s === 'pending' ? 'info' : s === 'importing' ? 'warning' : s === 'success' ? 'success' : s === 'skipped' ? 'info' : 'danger' },
    progIcon(s) { return s === 'pending' ? 'el-icon-time' : s === 'importing' ? 'el-icon-loading' : s === 'success' ? 'el-icon-success' : s === 'skipped' ? 'el-icon-minus' : 'el-icon-error' },
    onFilePicked(key, file) {
      this.$set(this.files, this.moduleSel[key], file.raw)
      this.$set(this.fileNames, this.moduleSel[key], file.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)) {
        this.importCityDir(type)
        this.moduleSel[key] = ''
        this.importType = ''
        return
      }
      const file = this.files[type]
      if (!file) { this.$message.warning('请先为该类型选择文件'); return }
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      const g = this.groups.find(x => x.key === key)
      const t = g.types.find(x => x.value === type)
      const row = {
        id: Date.now() + Math.random(),
        moduleKey: key,
        moduleName: g.name,
        type,
        typeLabel: t.label,
        file,
        fileName: this.fileNames[type] || file.name,
        status: 'pending',
        message: ''
      }
      this.importOne(row).then(() => {
        if ((row.type === 'investment' || row.type === 'investmentLogistics') && row.status === 'success') {
          this.checkInvest(this.period)
        }
        this.afterImport()
        this.moduleSel[key] = ''
        this.importType = ''
        this.files[type] = null
        this.$delete(this.fileNames, type)
      })
    },
    isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    importCityDir(type) {
      if (!this.period) { this.$message.warning('请先选择报表期'); return }
      const category = type === 'investCityPassenger' ? '客运站场' : '物流园区'
      const fd = new FormData()
      fd.append('period', this.period)
      fd.append('category', category)
      this.cityDirLoading = true
      api.post('/import/investCityBatch', fd, { timeout: 300000 })
        .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, '市州单表批量导入完成:共 ' + d.total + ' 条,成功 ' + d.success + ' 条' +
            (d.fail > 0 ? ',失败 ' + d.fail + ' 条' : '') +
            '(十五五项目匹配 ' + d.fiveYearMatched + '/' + d.fiveYearTemplateProjects + ')', {
            dangerouslyUseHTMLString: true,
            type: errCount > 0 ? 'warning' : 'success',
            confirmButtonText: '知道了'
          })
          this.afterImport()
        })
        .catch(e => {
          this.$message.error((e.response && e.response.data && e.response.data.message) || '批量导入失败')
        })
        .finally(() => { this.cityDirLoading = false })
    },
    addToQueue(key) {
      const type = this.moduleSel[key]
      if (!type) return
      const file = this.files[type]
      if (!file) { this.$message.warning('请先为该类型选择文件'); return }
      const g = this.groups.find(x => x.key === key)
      const t = g.types.find(x => x.value === type)
      this.queue.push({
        id: Date.now() + Math.random(),
        moduleKey: key,
        moduleName: g.name,
        type,
        typeLabel: t.label,
        file,
        fileName: this.fileNames[type] || file.name,
        status: 'pending',
        message: ''
      })
      this.moduleSel[key] = ''
      this.importType = ''
      this.files[type] = null
      this.$delete(this.fileNames, type)
    },
    removeQueueItem(row) {
      const i = this.queue.indexOf(row)
      if (i >= 0) this.queue.splice(i, 1)
    },
    importOne(row) {
      if (!this.period) { this.$message.warning('请先选择报表期'); return Promise.resolve() }
      row.status = 'uploading'
      row.message = ''
      const fd = new FormData()
      fd.append('file', row.file)
      fd.append('period', this.period)
      if (row.type === 'investment' || row.type === 'investmentLogistics') {
        fd.append('category', row.type === 'investment' ? '客运站场' : '物流园区')
      }
      return api.post(this.endpoint(row.type), fd, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 600000 })
        .then(res => { row.status = 'success'; row.message = res.data || '导入成功' })
        .catch(e => { row.status = 'fail'; row.message = (e.response && e.response.data && e.response.data.message) || '导入失败' })
        .finally(() => {})
    },
    importQueue() {
      if (!this.period) { this.$message.warning('请先选择报表期'); return Promise.resolve(false) }
      const pending = this.queue.filter(x => x.status === 'pending')
      if (pending.length === 0) { this.$message.warning('队列中没有待导入项'); return Promise.resolve(false) }
      this.queueLoading = true
      return new Promise(resolve => {
        const run = (i) => {
          if (i >= pending.length) {
            this.queueLoading = false
            this.afterImport()
            resolve(true)
            return
          }
          this.importOne(pending[i]).then(() => {
            const t = pending[i].type
            if ((t === 'investment' || t === 'investmentLogistics') && pending[i].status === 'success') {
              this.checkInvest(this.period)
            }
            run(i + 1)
          })
        }
        run(0)
      })
    },
    afterImport() { this.loadOverview(); this.loadBatches() },
    loadOverview() {
      if (!this.period) return
      api.get('/import/batches', { params: { period: this.period, limit: 200 } })
        .then(res => {
          const map = {}
          const records = (res.data && res.data.records) || []
          records.forEach(r => {
            const key = this.batchKeyToType(r.importType)
            if (!key) return
            const cur = map[key]
            if (!cur || (cur.latestId || 0) < (r.id || 0)) {
              map[key] = { totalRows: r.totalRows, successRows: r.successRows, failRows: r.failRows, latestId: r.id }
            }
          })
          this.overview = map
        })
        .catch(() => {})
    },
    batchKeyToType(k) {
      if (BATCH_TYPE_MAP[k]) return BATCH_TYPE_MAP[k]
      if (k && k.indexOf('INVEST_') === 0) {
        if (k.indexOf('_CITY') >= 0) return k === 'INVEST_物流园区_CITY' ? 'investCityLogistics' : 'investCityPassenger'
        return k === 'INVEST_物流园区' ? 'investmentLogistics' : 'investment'
      }
      return ''
    },
    overviewState(type) {
      const o = this.overview[type]
      if (!o) return { cls: 'ov-empty', text: '未导入' }
      if (o.failRows > 0) return { cls: 'ov-fail', text: '部分失败(失败 ' + o.failRows + ' 行)' }
      if (o.successRows > 0) return { cls: 'ov-ok', text: '已导入 ' + o.successRows + ' 行' }
      return { cls: 'ov-empty', text: '无数据' }
    },
    loadBatches() {
      this.batchLoading = true
      const params = { limit: 50 }
      if (this.period) params.period = this.period
      api.get('/import/batches', { params })
        .then(res => { this.batches = (res.data && res.data.records) || [] })
        .catch(() => {})
        .finally(() => { this.batchLoading = false })
    },
    checkInvest(period) {
      const fd = new FormData()
      fd.append('period', period)
      fd.append('reportType', 'INVEST')
      api.post('/audit/execute', fd, { timeout: 120000 })
        .then(res => {
          const list = res.data || []
          const pending = list.filter(x => x.status === 'PENDING')
          if (pending.length > 0) {
            const html = pending.map(x => {
              const name = x.enterpriseName || x.actualValue || ''
              return '<div style="margin:8px 0;line-height:1.6"><b>' + (x.ruleName || '') + '</b>:' + name +
                '<br/>实际:' + (x.actualValue || '') + ' 基准:' + (x.thresholdValue || '') +
                '<br/>差异:' + (x.deviation || '') + '</div>'
            }).join('')
            this.$alert(html, '投资数据与投资系统不一致(' + pending.length + ' 条)', {
              dangerouslyUseHTMLString: true, type: 'warning', confirmButtonText: '知道了'
            })
          } else {
            this.$message.success('已与投资系统核对,本月数据一致')
          }
        })
        .catch(() => {})
    }
  },
  methods: {
    handleFileChange(file) { this.file = file.raw },
    importData() {
      if (!this.file) { this.error = '请选择文件'; return }
      if (!this.period) { this.error = '请选择报表期'; return }
      this.loading = true; this.error = ''; this.result = ''
      let fd = new FormData()
      fd.append('file', this.file)
      fd.append('period', this.period)
      api.post(this.endpoint, fd, { headers: { 'Content-Type': 'multipart/form-data' } })
        .then(res => {
          this.result = res.data || '导入成功'
          this.fileList = []
          this.file = null
        })
        .catch(e => { this.error = (e.response && e.response.data && e.response.data.message) || '导入失败' })
        .finally(() => { this.loading = false })
    }
  mounted() {
    this.loadBatches()
    this.loadOverview()
  }
}
</script>
<style scoped>
.tip { font-size: 12px; color: #909399; margin-top: 6px; }
.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
.import-card { margin-bottom: 0; }
.card-head { font-weight: 600; margin-bottom: 6px; }
.module-tabs >>> .el-tabs__header { margin-bottom: 12px; }
.module-sel { max-width: 480px; }
.group-label { font-size: 13px; color: #606266; font-weight: 600; margin-bottom: 6px; padding-left: 6px; border-left: 3px solid #409EFF; }
.module-file { margin-top: 8px; }
.file-name { font-size: 12px; color: #606266; background: #F5F7FA; border: 1px solid #EBEEF5; border-radius: 4px; padding: 4px 8px; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-btns { display: flex; gap: 6px; }
.type-info { margin-top: 12px; color: #606266; font-size: 13px; background: #F5F7FA; border-radius: 4px; padding: 8px 10px; }
.tip { font-size: 12px; color: #909399; margin-top: 6px; line-height: 1.5; }
.slow-tip { font-size: 12px; color: #E6A23C; margin-left: 8px; }
.overview-grid { display: flex; flex-direction: column; gap: 8px; }
.ov-row { display: flex; align-items: stretch; gap: 8px; }
.ov-group { flex: none; width: 90px; display: flex; align-items: center; font-weight: 600; color: #606266; font-size: 13px; }
.ov-item { flex: 1; min-width: 0; border: 1px solid #EBEEF5; border-radius: 6px; padding: 6px 8px; background: #FAFAFA; }
.ov-item.ov-ok { background: #F0F9EB; border-color: #E1F3D8; }
.ov-item.ov-fail { background: #FDF6EC; border-color: #FAECD8; }
.ov-name { font-size: 12px; color: #303133; }
.ov-status { font-size: 12px; margin-top: 2px; }
.ov-ok .ov-status { color: #67C23A; }
.ov-fail .ov-status { color: #E6A23C; }
.ov-empty .ov-status { color: #C0C4CC; }
.queue-msg { font-size: 12px; color: #606266; word-break: break-all; white-space: pre-line; }
.dir-import-bar { display: flex; align-items: center; gap: 8px; margin: 6px 0 4px; flex-wrap: wrap; }
.dir-progress { margin-top: 8px; border: 1px solid #EBEEF5; border-radius: 4px; padding: 8px; background: #FAFAFA; }
.dir-progress-row { display: flex; align-items: center; gap: 8px; padding: 4px 2px; font-size: 12px; color: #303133; flex-wrap: wrap; }
.dir-progress-row i { color: #409EFF; }
.dir-progress-row .prog-name { min-width: 0; max-width: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dir-progress-row .queue-msg { width: 100%; padding-left: 22px; }
.dir-toolbar { margin-bottom: 6px; }
.dir-path { font-size: 12px; color: #606266; background: #F5F7FA; border: 1px solid #EBEEF5; border-radius: 4px; padding: 6px 8px; margin-bottom: 8px; word-break: break-all; }
.dir-body { max-height: 320px; overflow-y: auto; border: 1px solid #EBEEF5; border-radius: 4px; }
.dir-row { display: flex; align-items: center; gap: 6px; padding: 7px 10px; cursor: pointer; font-size: 13px; color: #303133; }
.dir-row:hover { background: #ECF5FF; color: #409EFF; }
.dir-row i { color: #E6A23C; }
.dir-empty { padding: 14px; font-size: 12px; color: #909399; text-align: center; }
.dir-files { margin-top: 8px; }
</style>