From fbc1c1590e7e81a550beb50c7cf65c663c7c5b41 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 01 九月 2026 16:42:06 +0800
Subject: [PATCH] 数据导入与报表生成多项改进
---
traffic-audit-web/src/views/DataImport.vue | 593 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 534 insertions(+), 59 deletions(-)
diff --git a/traffic-audit-web/src/views/DataImport.vue b/traffic-audit-web/src/views/DataImport.vue
index 2ba149a..8e277e2 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>
@@ -12,41 +12,112 @@
<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>
+ <h3>瀵煎叆涓氬姟鏁版嵁</h3>
+ <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>
+ </el-card>
+
+
+ <el-card shadow="hover" class="import-card" style="margin-bottom:15px">
+ <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>
- </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>
+ </div>
+ <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="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>
+ <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] && !isDirOnlyType(moduleSel[g.key])" class="module-file">
+ <div class="file-name" :title="fileNamesText(moduleSel[g.key])">{{ fileNamesText(moduleSel[g.key]) || '鏈�夋嫨鏂囦欢' }}</div>
+ <div class="file-btns">
+ <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple accept=".xlsx,.xls,.et"
+ :ref="'up' + g.key" :on-change="f => onFilePicked(g.key, f)" style="display:inline-block">
+ <el-button size="mini" type="primary" plain icon="el-icon-folder-opened">閫夋嫨鏂囦欢</el-button>
+ </el-upload>
+ <el-button size="mini" type="primary" icon="el-icon-upload2" :disabled="!fileCount(moduleSel[g.key])"
+ @click="importNow(g.key)">{{ fileCount(moduleSel[g.key]) > 1 ? '鎵归噺瀵煎叆 ' + fileCount(moduleSel[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>
+ </div>
+ </div>
+ </div>
</div>
</el-main>
</el-container>
@@ -55,63 +126,467 @@
<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') })(),
+ cityDirLoading: false,
+ sideOpen: false,
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: '閬撹矾璐х墿杩愯緭鏈堝害鐢熶骇鎯呭喌' },
+ { value: 'transportAuth', label: '杩愭斂杞﹁締' },
+ { value: 'trackMileage', label: '杞ㄨ抗閲岀▼' },
+ { value: 'scaleSplit', label: '瑙勪笂瑙勪笅鎷嗗垎' },
+ { value: 'freightTurnover', label: '璐ц繍閲忓懆杞噺' }
+ ]
+ },
+ {
+ key: 'passenger', name: '鍏矾鏃呭', types: [
+ { value: 'passengerMonthly', label: '鍏矾鏃呭杩愯緭鏈堝害鐢熶骇鎯呭喌' },
+ { 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: '閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐�' },
+ { value: 'energyAuth', label: '鑳借�楄溅杈嗚繍鏀夸俊鎭�' }
+ ]
+ },
+ {
+ key: 'annual', name: '骞存姤', types: [
+ { value: 'annualCompanyBasic', label: '鍗曚綅鍩烘湰鎯呭喌' },
+ { value: 'annualEnterpriseFinance', label: '娉曚汉浼佷笟璐㈠姟鐘跺喌' },
+ { value: 'annualPassengerEnergy', label: '鍏矾鏃呭杩愯緭鑳芥簮娑堣�楁儏鍐�' },
+ { value: 'annualBusOperation', label: '鍏叡姹界數杞﹁繍钀ユ儏鍐�' },
+ { value: 'annualMetroVehicles', label: '鍩庡競杞ㄩ亾浜ら�氳溅杈嗗強绾胯矾鎯呭喌' },
+ { value: 'annualMetroStaff', label: '鍩庡競杞ㄩ亾浜ら�氫汉鍛樺強杩愯惀鎯呭喌' },
+ { value: 'annualFerry', label: '鍩庡競瀹㈣繍杞浮杩愯惀鎯呭喌' },
+ { value: 'annualPublicEnergy', label: '鍏叡浜ら�氳兘婧愭秷鑰楁儏鍐�' },
+ { value: 'annualCityInfrastructureOperators', label: '鍩庡競瀹㈣繍鍩虹璁炬柦鎯呭喌銆佸煄甯傚杩愮粡钀ヤ笟鎴锋儏鍐�' },
+ { value: 'annualTaxiOwnership', label: '宸℃父鍑虹姹借溅钀ヨ繍杞﹁締鎷ユ湁鎯呭喌' },
+ { value: 'annualTaxiProduction', label: '宸℃父鍑虹姹借溅杩愯緭鐢熶骇鎯呭喌' }
+ ]
+ }
+ ]
}
},
computed: {
+ 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 out
+ },
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: '瀵煎叆妯℃澘_鍑虹杞︿俊鎭�(杩愭斂杞﹁締淇℃伅).xlsx锛堣溅杈嗙骇鏄庣粏锛岀敤浜庤繍钀ヨ溅鏁板姣旓級'
}
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: '鍏矾鏃呭/涓綋瀹㈣繍閲忔眹鎬�.xlsx',
+ energyMonthly: '鑳借��/妯℃澘_閬撹矾璐ц繍杞﹁締鑳芥簮娑堣�楁儏鍐碉紙2026锛�.xlsx',
+ energyAuth: '鑳借��/妯℃澘_鑳借�楄溅杈嗚繍鏀夸俊鎭�.xlsx',
+ cityBus: '鍩庡競瀹㈣繍/瀵煎叆妯℃澘_鍩庡競鍏氦.xlsx',
+ cityTaxi: '鍩庡競瀹㈣繍/瀵煎叆妯℃澘_宸℃父鍑虹.xlsx',
+ cityTaxiAuth: '鍩庡競瀹㈣繍/瀵煎叆妯℃澘_鍑虹杞︿俊鎭�(杩愭斂杞﹁締淇℃伅).xlsx',
+ 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] || ''
+ },
+ onModuleSwitch() {
+ this.importType = this.moduleSel[this.activeModule] || ''
+ },
+ 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]
- }
- },
- 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)
+ return map[t] || ''
+ },
+ onFilePicked(key, file) {
+ const type = this.moduleSel[key]
+ if (!type) return
+ const list = (this.files[type] || []).filter(f => f.name !== file.name)
+ list.push(file.raw)
+ this.$set(this.files, type, list)
+ this.$set(this.fileNames, type, list.map(f => f.name))
+ const up = this.$refs['up' + key]
+ if (up && up.clearFiles) up.clearFiles()
+ },
+ importNow(key) {
+ const type = this.moduleSel[key]
+ if (!type) return
+ if (this.isDirOnlyType(type)) {
+ this.importCityDir(type)
+ this.moduleSel[key] = ''
+ this.importType = ''
+ return
+ }
+ const fileList = this.files[type] || []
+ if (!fileList.length) { this.$message.warning('璇峰厛涓鸿绫诲瀷閫夋嫨鏂囦欢'); return }
+ if (fileList.length > 1 || type === 'investment' || type === 'investmentLogistics') {
+ this.batchImport(type, key, fileList)
+ return
+ }
+ const file = fileList[0]
+ 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)
+ })
+ },
+ fileCount(type) { return (this.files[type] || []).length },
+ fileNamesText(type) {
+ const list = this.fileNames[type] || []
+ return list.length ? '宸查�� ' + list.length + ' 涓枃浠讹細' + list.join('銆�') : ''
+ },
+ batchImport(type, key, fileList) {
+ if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鎶ヨ〃鏈�'); return }
+ const fd = new FormData()
+ fd.append('type', type)
fd.append('period', this.period)
- api.post(this.endpoint, fd, { headers: { 'Content-Type': 'multipart/form-data' } })
+ fileList.forEach(f => fd.append('files', f))
+ const g = this.groups.find(x => x.key === key)
+ const label = g ? g.name : type
+ this.$message({ message: '姝e湪鎵归噺瀵煎叆 ' + fileList.length + ' 涓枃浠垛��', duration: 3000 })
+ api.post('/import/batch', fd, { timeout: 600000 })
.then(res => {
- this.result = res.data || '瀵煎叆鎴愬姛'
- this.fileList = []
- this.file = null
+ const d = (res && res.data) || {}
+ if (!d.files) {
+ this.$message.error((res && res.message) || '鎵归噺瀵煎叆澶辫触')
+ return
+ }
+ const files = d.files || []
+ const errCount = files.filter(f => f.error || f.success === 0).length
+ const html = files.map(f =>
+ '<div style="margin:6px 0;line-height:1.6"><b>' + (f.fileName || '') + '</b>' +
+ (f.city ? '锛�' + f.city + '锛�' : '') + '锛氳В鏋� ' + (f.success || 0) + ' 鏉�' +
+ (f.error ? ' <span style="color:#F56C6C">' + f.error + '</span>' : '') + '</div>'
+ ).join('')
+ this.$alert(html, (label || type) + ' 鎵归噺瀵煎叆瀹屾垚锛氬叡 ' + d.total + ' 涓枃浠�' +
+ (d.fail > 0 ? '锛屽け璐� ' + d.fail + ' 鏉�' : '') + '锛堟垚鍔� ' + d.success + ' 鏉★級', {
+ dangerouslyUseHTMLString: true,
+ type: errCount > 0 ? 'warning' : 'success',
+ confirmButtonText: '鐭ラ亾浜�'
+ })
+ if (type === 'investment' || type === 'investmentLogistics') {
+ this.checkInvest(this.period)
+ }
+ this.afterImport()
+ this.files[type] = null
+ this.$delete(this.fileNames, type)
})
- .catch(e => { this.error = (e.response && e.response.data && e.response.data.message) || '瀵煎叆澶辫触' })
- .finally(() => { this.loading = false })
- }
+ .catch(e => {
+ if (e && e._toast) return
+ this.$message.error((e.response && e.response.data && e.response.data.message) || '鎵归噺瀵煎叆澶辫触')
+ })
+ },
+ isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
+ isDirOnlyType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
+ importCityDir(type) {
+ if (!this.period) { this.$message.warning('璇峰厛閫夋嫨鎶ヨ〃鏈�'); return }
+ const category = (type === 'investCityPassenger' || type === 'investment') ? '瀹㈣繍绔欏満' : '鐗╂祦鍥尯'
+ 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 })
+ },
+ 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(() => {})
+ },
+ 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: '鏃犳暟鎹�' }
+ },
+ 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(() => {})
+ },
+ 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.loadData()
}
}
</script>
<style scoped>
-.tip { font-size: 12px; color: #909399; margin-top: 6px; }
+.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
+.import-card { margin-bottom: 0; }
+.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-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; }
+.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; }
+.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