From 791e70f228a6878d02d1b664588bdccf983b8811 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期一, 31 八月 2026 16:20:53 +0800
Subject: [PATCH] feat: 数据导入页重构——选择数据类型/模板下载并排/失败明细表格/导入进度抽屉侧栏

---
 traffic-audit-web/src/views/DataImport.vue |  540 ++++++++++++++++-------------------------------------------
 1 files changed, 150 insertions(+), 390 deletions(-)

diff --git a/traffic-audit-web/src/views/DataImport.vue b/traffic-audit-web/src/views/DataImport.vue
index f98dd70..3d3dcdb 100644
--- a/traffic-audit-web/src/views/DataImport.vue
+++ b/traffic-audit-web/src/views/DataImport.vue
@@ -3,7 +3,7 @@
     <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 鏁版嵁瀵煎叆"></app-header>
     <el-container>
       <el-aside width="200px">
-        <el-menu router default-active="/import">
+        <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect">
           <el-menu-item index="/dashboard">宸ヤ綔鍙�</el-menu-item>
           <el-menu-item index="/import">鏁版嵁瀵煎叆</el-menu-item>
           <el-menu-item index="/data">鏁版嵁鏌ョ湅</el-menu-item>
@@ -17,188 +17,107 @@
       <el-main>
         <div class="page-scroll">
           <h3>瀵煎叆涓氬姟鏁版嵁</h3>
-          <el-card shadow="hover" class="import-card" style="margin-bottom:15px">
+          <div class="import-layout">
+            <div class="import-main">          <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 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-group>
             </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"
+            <div v-for="g in groups" :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"
                              @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 v-if="importType" class="tpl-box">
+                    <span class="tpl-name">妯℃澘锛歿{ templateName }}</span>
+                    <el-link v-if="templateUrl" type="primary" icon="el-icon-download" :underline="false"
+                             @click="downloadTemplate">涓嬭浇妯℃澘</el-link>
+                    <span v-if="importType === 'cityTaxiAuth'" class="slow-tip">璇ョ被鍨嬬害 4 涓�+ 琛岋紝棰勮鑰楁椂 5 鍒嗛挓宸﹀彸</span>
                   </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 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>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-card>
+
+
+          
+
+          
+
+                              <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>
+            <div v-if="failBatches.length" class="tip" style="margin-bottom:6px">鍏� {{ failBatches.length }} 涓壒娆″瓨鍦ㄥけ璐ユ暟鎹紝澶辫触鏄庣粏鍏� {{ failRowsFlat.length }} 鏉★細</div>
+            <el-table v-if="failBatches.length" :data="failRowsFlat" border size="small" v-loading="batchLoading" style="width:100%">
+              <el-table-column label="瀵煎叆绫诲瀷" width="140">
+                <template slot-scope="s">{{ typeLabel(s.row.importType) }}</template>
+              </el-table-column>
+              <el-table-column prop="reportPeriod" label="鎶ヨ〃鏈�" width="90"></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="230">
+                <template slot-scope="s"><span v-if="s.row.position">{{ s.row.position }}</span><span v-else>鈥�</span></template>
+              </el-table-column>
+              <el-table-column label="澶辫触鍘熷洜" min-width="300">
+                <template slot-scope="s">{{ s.row.reason }}</template>
+              </el-table-column>
+            </el-table>
+            <div v-else class="empty-tip">鏆傛棤瀵煎叆澶辫触璁板綍</div>
+          </el-card>
+            </div>
+                        <div class="import-side" :class="{ 'side-open': sideOpen }">
+              <div class="side-toggle" @click="sideOpen = !sideOpen" :title="sideOpen ? '鏀惰捣杩涘害' : '灞曞紑杩涘害'">
+                <i :class="sideOpen ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'"></i>
+                <span class="side-toggle-text">{{ sideOpen ? '鏀惰捣杩涘害' : '瀵煎叆杩涘害' }}</span>
+              </div>
+              <div v-show="sideOpen" class="side-content">
+                <div class="side-head">
+                  <span>鏈湀瀵煎叆杩涘害锛坽{ period || '鏈�夋嫨鎶ヨ〃鏈�' }}锛�</span>
+                  <el-button type="text" icon="el-icon-refresh" @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-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>
         </div>
       </el-main>
     </el-container>
@@ -235,21 +154,8 @@
     return {
       importType: 'h2032',
       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,
+      sideOpen: false,
       result: '',
       error: '',
       batches: [],
@@ -302,38 +208,18 @@
     }
   },
   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
+    failBatches() { return this.batches.filter(b => b.failRows > 0) },
+    failRowsFlat() {
+      const out = []
+      for (const b of this.failBatches) {
+        const rows = this.parseFailDetails(b)
+        if (!rows.length) {
+          out.push({ importType: b.importType, reportPeriod: b.reportPeriod, fileName: b.fileName, position: '', reason: b.errorDetail || '璇ユ壒娆℃棤澶辫触鏄庣粏' })
+        } else {
+          rows.forEach(r => out.push({ importType: b.importType, reportPeriod: b.reportPeriod, fileName: b.fileName, position: r.position, reason: r.reason }))
+        }
       }
-      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] || ''
+      return out
     },
     templateName() {
       const map = {
@@ -388,9 +274,11 @@
     },
     onModuleChange(key) {
       this.importType = this.moduleSel[key] || ''
-      this.dirPath = (this.importType && localStorage.getItem('dirLast_' + this.importType)) || ''
     },
-    onPeriodChange() { this.loadOverview(); this.loadBatches() },
+    onModuleSwitch() {
+      this.importType = this.moduleSel[this.activeModule] || ''
+    },
+    onPeriodChange() { this.loadData() },
     endpoint(t) {
       const map = {
         h2032: '/import/h2032',
@@ -412,94 +300,6 @@
       }
       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 = '瑙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
-          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)
@@ -574,36 +374,10 @@
           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() }
@@ -620,37 +394,17 @@
         .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 } })
+    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 map = {}
           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
@@ -661,7 +415,8 @@
           })
           this.overview = map
         })
-        .catch(() => {})
+        .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]
@@ -677,15 +432,6 @@
       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()
@@ -710,25 +456,52 @@
           }
         })
         .catch(() => {})
-    }
+    },
+  parseFailDetails(row) {
+    if (!row || !row.errorDetail) return []
+    const lines = String(row.errorDetail).split(/\r?\n/)
+    const out = []
+    lines.forEach((line, i) => {
+      if (!line || !line.trim()) return
+      const m = line.match(/^绗琝s*(\d+)\s*琛孾锛�(]([^锛�)]*)[锛�)]\s*[:锛歖\s*([\s\S]*)$/)
+      if (m) out.push({ no: i + 1, position: '绗� ' + m[1] + ' 琛岋紙' + m[2] + '锛�', reason: m[3].trim() })
+      else out.push({ no: i + 1, position: '', reason: line.trim() })
+    })
+    return out
+  },
   },
   mounted() {
-    this.loadBatches()
-    this.loadOverview()
+    this.loadData()
   }
 }
 </script>
 <style scoped>
 .el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
 .import-card { margin-bottom: 0; }
+.import-layout { display: flex; align-items: flex-start; gap: 15px; }
+.import-main { flex: 1; min-width: 0; }
+.import-side { position: relative; flex-shrink: 0; width: 34px; overflow: hidden; border: 1px solid #EBEEF5; border-radius: 4px; background: #FAFAFA; transition: width .2s ease; }
+.import-side.side-open { width: 340px; }
+.side-toggle { display: flex; flex-direction: column; align-items: center; cursor: pointer; color: #409EFF; padding: 10px 0; user-select: none; white-space: nowrap; }
+.side-toggle i { font-size: 15px; }
+.side-toggle-text { writing-mode: vertical-lr; letter-spacing: 4px; margin-top: 8px; font-size: 13px; }
+.import-side.side-open .side-toggle { flex-direction: row; gap: 6px; padding: 8px 10px; border-bottom: 1px solid #EBEEF5; }
+.import-side.side-open .side-toggle-text { writing-mode: horizontal-tb; letter-spacing: normal; margin-top: 0; }
+.side-content { padding: 8px 12px 12px; max-height: calc(100vh - 220px); overflow-y: auto; overscroll-behavior: contain; }
+.side-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-weight: 600; margin-bottom: 6px; }
+.import-side .overview-grid { display: block; }
+.import-side .ov-row { display: block; margin-bottom: 6px; }
+.import-side .ov-group { width: auto; }
+.import-side .ov-item { margin-bottom: 4px; }
 .card-head { font-weight: 600; margin-bottom: 6px; }
-.module-tabs >>> .el-tabs__header { margin-bottom: 12px; }
-.module-sel { max-width: 480px; }
+.module-sel { max-width: 1100px; }
+.module-sel-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
+.tpl-box { display: flex; align-items: center; gap: 8px; min-width: 0; color: #606266; font-size: 13px; }
+.tpl-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; 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; }
@@ -742,20 +515,7 @@
 .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; }
-
+.empty-tip { padding: 24px 0; text-align: center; color: #C0C4CC; font-size: 13px; }
+.module-tabs-head { margin-bottom: 10px; }
+.module-tab-pane { min-height: 120px; }
 </style>

--
Gitblit v1.9.1