| | |
| | | <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> |
| | |
| | | <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" :disabled="importing">选择文件</el-button> |
| | | </el-upload> |
| | | <el-button size="mini" type="primary" icon="el-icon-upload2" :loading="importing" :disabled="importing || !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> |
| | |
| | | <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, |
| | | importing: 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: '模板_交企统H203-2表_道路货物运输月度生产情况.xlsx', |
| | | transportAuth: '模板_运政车辆.xlsx', |
| | | trackMileage: '模板_轨迹里程.xlsx', |
| | | scaleSplit: '模板_规上规下拆分运输量.xlsx', |
| | | freightTurnover: '模板_货运量周转量.xlsx' |
| | | freightTurnover: '模板_货运量周转量.xlsx', |
| | | passengerMonthly: '公路旅客运输月度生产情况.xlsx(含运政车辆数sheet)', |
| | | passengerAuth: '模板_公路旅客运政车辆数.xlsx(列:企业代码/企业名称/车辆数/载客位数)', |
| | | passengerIndividual: '个体客运量汇总.xlsx(列:所属地区/企业代码/企业名称/个体客运量/个体旅客周转量)', |
| | | energyMonthly: '道路货运车辆能源消耗情况(2026年).xlsx(能耗明细+运政车辆信息两sheet)', |
| | | energyAuth: '模板_能耗车辆运政信息.xlsx(列:车牌号/车辆类型/燃料类型/标记吨位/准牵引质量)', |
| | | investment: '7月客运站投资明细表(市州汇总大表,给领导).xlsx', |
| | | investmentLogistics: '7月物流站场投资明细.xls(分项目投资完成情况sheet)', |
| | | investmentSystem: '查询结果(投资系统-客运+物流 2026.7).xls', |
| | | investCityPassenger: '—(批量扫描目录,无需模板)', |
| | | investCityLogistics: '—(批量扫描目录,无需模板)', |
| | | cityBus: '导入_城市公共交通月度运营情况(YYYY年M月).xls/.xlsx(企业级明细,取公共汽电车字段)', |
| | | cityTaxi: '导入_巡游出租汽车运营服务情况月报(YYYY年M月).xls(市州级17行)', |
| | | cityTaxiAuth: '导入模板_出租车信息(运政车辆信息).xlsx(车辆级明细,用于运营车数对比)' |
| | | } |
| | | return map[this.importType] || '' |
| | | }, |
| | | endpoint() { |
| | | templateUrl() { |
| | | const map = { |
| | | h2032: '货运/模板_交企统H203-2表_道路货物运输月度生产情况.xlsx', |
| | | transportAuth: '货运/模板_运政车辆.xlsx', |
| | | trackMileage: '货运/模板_轨迹里程.xlsx', |
| | | scaleSplit: '货运/模板_规上规下拆分运输量.xlsx', |
| | | freightTurnover: '货运/模板_货运量周转量.xlsx', |
| | | passengerMonthly: '公路旅客/模板_交企统H203-1表_公路旅客运输月度生产情况.xlsx', |
| | | passengerAuth: '公路旅客/模板_公路旅客运政车辆数.xlsx', |
| | | passengerIndividual: '公路旅客/个体客运量汇总.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 |
| | | if (this.importing) { this.$message.warning('正在导入中,请稍候再选择文件'); 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 (!this.period) { this.$message.warning('请先选择报表期'); return } |
| | | if (this.importing) { this.$message.warning('正在导入中,请稍候…'); return } |
| | | this.importing = true |
| | | if (fileList.length > 1 || type === 'investment' || type === 'investmentLogistics') { |
| | | this.batchImport(type, key, fileList).finally(() => { this.importing = false }) |
| | | return |
| | | } |
| | | const file = fileList[0] |
| | | 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) |
| | | }).finally(() => { this.importing = false }) |
| | | }, |
| | | 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: '正在批量导入 ' + fileList.length + ' 个文件…', duration: 3000 }) |
| | | return 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> |