From 8319e0c1ba52b6923d06dce02e568d9063bdb055 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期一, 31 八月 2026 17:18:08 +0800
Subject: [PATCH] docs: 关机交接快照(08-31 下班前)——数据导入页重构交付(791e70f)、运行状态、待办更新
---
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