From 684da4132969f03a76416796e81667d8fe59e913 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期一, 31 八月 2026 09:14:25 +0800
Subject: [PATCH] fix: 数据导入页 el-tabs 触发 Element UI 布局无限循环,挂载卡死导致浏览器无响应/菜单不高亮——改用 el-radio-group 按钮切换模块 + v-show 面板

---
 traffic-audit-web/src/views/DataImport.vue |  791 ++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 729 insertions(+), 62 deletions(-)

diff --git a/traffic-audit-web/src/views/DataImport.vue b/traffic-audit-web/src/views/DataImport.vue
index 5b40688..f4434b0 100644
--- a/traffic-audit-web/src/views/DataImport.vue
+++ b/traffic-audit-web/src/views/DataImport.vue
@@ -1,6 +1,6 @@
 <template>
-  <el-container>
-    <el-header>浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 鏁版嵁瀵煎叆</el-header>
+  <el-container direction="vertical">
+    <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 鏁版嵁瀵煎叆"></app-header>
     <el-container>
       <el-aside width="200px">
         <el-menu router default-active="/import">
@@ -12,105 +12,772 @@
           <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="loadData">鍒锋柊</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>
+            <div class="module-tabs-head">
+                <el-radio-group v-model="activeModule" size="small">
+                  <el-radio-button v-for="g in groups" :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 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>
+              </div>
+            <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 v-if="dirDirsTruncated" class="tip">瀛愭枃浠跺す杩囧锛屼粎鏄剧ず鍓� 300 涓�</div>
+              <div v-if="dirFilesTruncated" class="tip">Excel 鏂囦欢杩囧锛堝叡 {{ dirExcelCount }} 涓級锛屼粎鏄剧ず鍓� 300 涓�</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="loadData">鍒锋柊</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>
   </el-container>
 </template>
 <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: [],
+      dirDirsTruncated: false,
+      dirExcelCount: 0,
+      dirFilesTruncated: false,
+      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: '杩愭斂杞﹁締妗f锛氱敤浜庤溅杈嗕俊鎭牳瀵癸紙鍚ㄤ綅/杞﹀瀷/鐕冩枡绫诲瀷锛夈��',
+        trackMileage: '杞ㄨ抗閲岀▼鏁版嵁锛氱敤浜庨噷绋嬩笌杩愯窛鏍¢獙銆�',
+        scaleSplit: '瑙勪笂瑙勪笅鎷嗗垎锛氬叏鐪�+17 甯傚窞璐ц繍閲�/鍛ㄨ浆閲忔媶鍒嗙粨鏋溿��',
+        freightTurnover: '璐ц繍閲忓懆杞噺锛氬叏鐪�+17 甯傚窞鏈堝害鍚堣锛堜粖骞� 1-N 鏈� + 鍘诲勾锛夈��',
+        passengerMonthly: '鍏矾鏃呭鏈堟姤锛氫紒涓氱骇鏄庣粏 + 杩愭斂杞﹁締鏁颁袱涓� sheet锛堣繍鏀胯溅杈嗘暟涔熷彲鍗曠嫭瀵煎叆锛夈��',
+        passengerAuth: '鏃呭杩愭斂杞﹁締鏁帮細鐙珛妯℃澘锛堜紒涓氫唬鐮�/浼佷笟鍚嶇О/杞﹁締鏁�/杞藉浣嶆暟锛夛紝鐢ㄤ簬杩愯惀杞﹁締鏍稿锛涗篃鍙殢 H203-1 鏈堟姤绗簩涓� sheet 涓�骞跺鍏ャ��',
+        passengerIndividual: '涓綋瀹㈣繍閲�/鍛ㄨ浆閲忥細浼佷笟绾夸笅姹囨姤鏁版嵁锛堟墍灞炲湴鍖�/浼佷笟浠g爜/浼佷笟鍚嶇О/涓綋瀹㈣繍閲�/涓綋鏃呭鍛ㄨ浆閲忥級锛屽鍏ュ悗鑷姩濉叆鍒嗗競宸炶〃鈥滃叾涓釜浣撯�濊銆�',
+        energyMonthly: '鑳借�楀鎶ワ細鑳借�楁槑缁� + 杩愭斂杞﹁締淇℃伅涓や釜 sheet锛堣繍鏀夸俊鎭篃鍙崟鐙鍏ワ紝瀛e害鏈湀鐢熸垚姹囨�昏〃锛夈��',
+        energyAuth: '鑳借�楄溅杈嗚繍鏀夸俊鎭細鐙珛妯℃澘锛堣溅鐗屽彿/杞﹁締绫诲瀷/鐕冩枡绫诲瀷/鏍囪鍚ㄤ綅/鍑嗙壍寮曡川閲忥級锛岀敤浜庤兘鑰楀鏍稿姣旓紱涔熷彲闅� H204 瀛f姤绗簩涓� 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: '妯℃澘_浜や紒缁烪203-2琛╛閬撹矾璐х墿杩愯緭鏈堝害鐢熶骇鎯呭喌.xlsx',
         transportAuth: '妯℃澘_杩愭斂杞﹁締.xlsx',
         trackMileage: '妯℃澘_杞ㄨ抗閲岀▼.xlsx',
         scaleSplit: '妯℃澘_瑙勪笂瑙勪笅鎷嗗垎杩愯緭閲�.xlsx',
-        freightTurnover: '妯℃澘_璐ц繍閲忓懆杞噺.xlsx'
+        freightTurnover: '妯℃澘_璐ц繍閲忓懆杞噺.xlsx',
+        passengerMonthly: '鍏矾鏃呭杩愯緭鏈堝害鐢熶骇鎯呭喌.xlsx锛堝惈杩愭斂杞﹁締鏁皊heet锛�',
+        passengerAuth: '妯℃澘_鍏矾鏃呭杩愭斂杞﹁締鏁�.xlsx锛堝垪锛氫紒涓氫唬鐮�/浼佷笟鍚嶇О/杞﹁締鏁�/杞藉浣嶆暟锛�',
+        passengerIndividual: '涓綋瀹㈣繍閲忔眹鎬�.xlsx锛堝垪锛氭墍灞炲湴鍖�/浼佷笟浠g爜/浼佷笟鍚嶇О/涓綋瀹㈣繍閲�/涓綋鏃呭鍛ㄨ浆閲忥級',
+        energyMonthly: '閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐碉紙2026骞达級.xlsx锛堣兘鑰楁槑缁�+杩愭斂杞﹁締淇℃伅涓heet锛�',
+        energyAuth: '妯℃澘_鑳借�楄溅杈嗚繍鏀夸俊鎭�.xlsx锛堝垪锛氳溅鐗屽彿/杞﹁締绫诲瀷/鐕冩枡绫诲瀷/鏍囪鍚ㄤ綅/鍑嗙壍寮曡川閲忥級',
+        investment: '7鏈堝杩愮珯鎶曡祫鏄庣粏琛紙甯傚窞姹囨�诲ぇ琛紝缁欓瀵硷級.xlsx',
+        investmentLogistics: '7鏈堢墿娴佺珯鍦烘姇璧勬槑缁�.xls锛堝垎椤圭洰鎶曡祫瀹屾垚鎯呭喌sheet锛�',
+        investmentSystem: '鏌ヨ缁撴灉(鎶曡祫绯荤粺-瀹㈣繍+鐗╂祦 2026.7).xls',
+        investCityPassenger: '鈥旓紙鎵归噺鎵弿鐩綍锛屾棤闇�妯℃澘锛�',
+        investCityLogistics: '鈥旓紙鎵归噺鎵弿鐩綍锛屾棤闇�妯℃澘锛�',
+        cityBus: '瀵煎叆_鍩庡競鍏叡浜ら�氭湀搴﹁繍钀ユ儏鍐碉紙YYYY骞碝鏈堬級.xls/.xlsx锛堜紒涓氱骇鏄庣粏锛屽彇鍏叡姹界數杞﹀瓧娈碉級',
+        cityTaxi: '瀵煎叆_宸℃父鍑虹姹借溅杩愯惀鏈嶅姟鎯呭喌鏈堟姤锛圷YYY骞碝鏈堬級.xls锛堝競宸炵骇17琛岋級',
+        cityTaxiAuth: '瀵煎叆_鍑虹杞︿俊鎭�(杩愭斂杞﹁締淇℃伅).xls锛堣溅杈嗙骇鏄庣粏锛岀敤浜庤繍钀ヨ溅鏁板姣旓級'
       }
       return map[this.importType] || ''
     },
-    endpoint() {
+    templateUrl() {
+      const map = {
+        h2032: '璐ц繍/妯℃澘_浜や紒缁烪203-2琛╛閬撹矾璐х墿杩愯緭鏈堝害鐢熶骇鎯呭喌.xlsx',
+        transportAuth: '璐ц繍/妯℃澘_杩愭斂杞﹁締.xlsx',
+        trackMileage: '璐ц繍/妯℃澘_杞ㄨ抗閲岀▼.xlsx',
+        scaleSplit: '璐ц繍/妯℃澘_瑙勪笂瑙勪笅鎷嗗垎杩愯緭閲�.xlsx',
+        freightTurnover: '璐ц繍/妯℃澘_璐ц繍閲忓懆杞噺.xlsx',
+        passengerMonthly: '鍏矾鏃呭/妯℃澘_浜や紒缁烪203-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.loadData() },
+    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.dirDirsTruncated = !!d.dirsTruncated
+          this.dirExcelCount = d.excelCount || 0
+          this.dirFiles = d.files || []
+          this.dirFilesTruncated = !!d.filesTruncated
+        })
+        .catch(e => { if (e && e._toast) return; 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 }
+          if (files.length > 60) {
+            this.dirImporting = false
+            this.$message.warning('鐩綍鍐� Excel 鏂囦欢杩囧锛�' + files.length + ' 涓級锛屼负閬垮厤娴忚鍣ㄥ崱椤胯鍒嗘壒瀵煎叆锛堟瘡鎵逛笉瓒呰繃 60 涓級')
+            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 = '瑙f瀽 ' + 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
+          if (e && e._toast) return
+          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 => {
+          if (e && e._toast) return
+          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.loadData() },
+    loadData() {
+      if (this.batchLoading) return
+      this.batchLoading = true
+      const params = { limit: 200 }
+      if (this.period) params.period = this.period
+      api.get('/import/batches', { params })
+        .then(res => {
+          const records = (res.data && res.data.records) || []
+          this.batches = records.slice(0, 50)
+          const map = {}
+          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(e => { if (!e || !e._toast) this.$message.error('瀵煎叆璁板綍鍔犺浇澶辫触锛岃鍒锋柊閲嶈瘯') })
+        .finally(() => { this.batchLoading = false })
+    },
+    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.loadData()
   }
 }
 </script>
 <style scoped>
 .el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
-.tip { font-size: 12px; color: #909399; margin-top: 6px; }
+.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; }
+
+.module-tabs-head { margin-bottom: 10px; }
+.module-tab-pane { min-height: 120px; }
 </style>

--
Gitblit v1.9.1