| | |
| | | <template> |
| | | <el-container direction="vertical"> |
| | | <app-header title="交通统计报表审核系统 - 数据导入"></app-header> |
| | | <el-container> |
| | |
| | | <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-option v-for="t in typeList(g)" :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> |
| | |
| | | <div v-else class="overview-grid"> |
| | | <div class="ov-row" v-for="g in visibleGroups" :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-item" v-for="t in typeList(g)" :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 v-if="clearScope === 'types'" class="clear-types"> |
| | | <el-select v-model="clearTypes" multiple filterable collapse-tags placeholder="选择要清空的数据类型" style="width:100%"> |
| | | <el-option-group v-for="g in clearableGroups()" :key="g.key" :label="g.name"> |
| | | <el-option v-for="t in g.types" :key="t.value" :value="t.value" |
| | | <el-option v-for="t in typeList(g)" :key="t.value" :value="t.value" |
| | | :label="t.label + '(' + overviewState(t.value).text + ')'"></el-option> |
| | | </el-option-group> |
| | | </el-select> |
| | |
| | | <el-button @click="clearDialog = false">取消</el-button> |
| | | <el-button type="danger" :loading="clearing" |
| | | :disabled="clearScope === 'types' && !clearTypes.length" @click="confirmClear">确认清空</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | <!-- 导入结果弹窗:可拖动、内容区滚动、底部按钮固定可见(原 $alert 内容长时底部按钮会被挤出屏幕) --> |
| | | <el-dialog :title="resultDialog.title" :visible.sync="resultDialog.visible" width="860px" top="5vh" |
| | | append-to-body :close-on-click-modal="false" custom-class="import-result-dialog" v-dialog-drag> |
| | | <div v-if="resultDialog.level" class="ird-level" :class="'ird-' + resultDialog.level"> |
| | | {{ resultDialog.level === 'error' ? '导入失败' : (resultDialog.level === 'warning' ? '存在需要确认的内容' : '导入成功') }} |
| | | </div> |
| | | <div class="ird-body" v-html="resultDialog.html"></div> |
| | | <span slot="footer"> |
| | | <el-button v-if="resultDialog.rows.length" size="small" icon="el-icon-download" |
| | | @click="exportResultDialog">导出明细</el-button> |
| | | <el-button type="primary" @click="resultDialog.visible = false">确认</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | </el-main> |
| | |
| | | clearDialog: false, |
| | | clearScope: 'all', |
| | | clearTypes: [], |
| | | resultDialog: { visible: false, title: '', html: '', rows: [], columns: [], fileName: '', level: '' }, |
| | | sideOpen: false, |
| | | result: '', |
| | | error: '', |
| | |
| | | { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' }, |
| | | { value: 'cityTaxiAuth', label: '出租车运政车辆' }, |
| | | { value: 'wycTotal', label: '网约车总数' }, |
| | | { value: 'wycOrder', label: '网约车订单及全省总量' }, |
| | | { value: 'wycOrder', label: '网约车订单及全省总量', hidden: true }, |
| | | { value: 'wycOrderReport', label: '网约车订单报告' } |
| | | ] |
| | | }, |
| | |
| | | typeLabel(t) { return TYPE_LABELS[t] || t }, |
| | | downloadTemplate() { |
| | | if (!this.templateUrl) return |
| | | window.open('/api/templates/' + this.templateUrl) |
| | | const name = this.templateUrl.split('/').pop() |
| | | api.get('/templates/' + this.templateUrl, { responseType: 'blob' }) |
| | | .then(res => { |
| | | const blob = res instanceof Blob ? res : new Blob([res]) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = name |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | window.URL.revokeObjectURL(url) |
| | | document.body.removeChild(a) |
| | | }) |
| | | .catch(() => this.$message.error('模板下载失败,请稍后重试')) |
| | | }, |
| | | onModuleChange(key) { |
| | | this.importType = this.moduleSel[key] || '' |
| | |
| | | (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: '知道了' |
| | | this.showResultDialog({ |
| | | title: (label || type) + ' 批量导入完成:共 ' + d.total + ' 个文件' + |
| | | (d.fail > 0 ? ',失败 ' + d.fail + ' 条' : '') + '(成功 ' + d.success + ' 条)', |
| | | html: html, |
| | | level: errCount > 0 ? 'warning' : 'success', |
| | | columns: [{ key: 'fileName', label: '文件名' }, { key: 'city', label: '市州' }, |
| | | { key: 'success', label: '解析条数' }, { key: 'error', label: '错误' }], |
| | | rows: files.map(f => ({ fileName: f.fileName || '', city: f.city || '', success: f.success || 0, error: f.error || '' })), |
| | | fileName: (label || '批量导入') + '_' + this.period + '_导入结果' |
| | | }) |
| | | if (type === 'investment' || type === 'investmentLogistics') { |
| | | this.checkInvest(this.period) |
| | |
| | | (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.showResultDialog({ |
| | | title: '市州单表批量导入完成:共 ' + d.total + ' 条,成功 ' + d.success + ' 条' + |
| | | (d.fail > 0 ? ',失败 ' + d.fail + ' 条' : '') + |
| | | '(十五五项目匹配 ' + d.fiveYearMatched + '/' + d.fiveYearTemplateProjects + ')', |
| | | html: html, |
| | | level: errCount > 0 ? 'warning' : 'success', |
| | | columns: [{ key: 'fileName', label: '文件名' }, { key: 'city', label: '市州' }, |
| | | { key: 'success', label: '解析项目数' }, { key: 'error', label: '错误' }], |
| | | rows: files.map(f => ({ fileName: f.fileName || '', city: f.city || '', success: f.success || 0, error: f.error || '' })), |
| | | fileName: '市州单表导入结果_' + this.period |
| | | }) |
| | | this.afterImport() |
| | | }) |
| | |
| | | const text = (msg && msg.indexOf('导入') !== -1) ? msg : '数据导入成功' |
| | | this.$message({ message: text, type: 'success', duration: 5000 }) |
| | | } else { |
| | | this.$alert((msg || '数据导入失败,详见下方失败记录').replace(/\n/g, '<br>'), '数据导入失败', { |
| | | dangerouslyUseHTMLString: true, |
| | | type: 'error', |
| | | confirmButtonText: '知道了' |
| | | this.showResultDialog({ |
| | | title: '数据导入失败', |
| | | html: (msg || '数据导入失败,详见下方失败记录').replace(/\n/g, '<br>'), |
| | | level: 'error' |
| | | }) |
| | | } |
| | | }, |
| | |
| | | }, |
| | | clearableGroups() { |
| | | return this.groups.filter(g => !g.hidden) |
| | | }, |
| | | // 单个数据类型也可隐藏(hidden: true):入口、概览、清空下拉统一过滤 |
| | | typeList(g) { |
| | | return (g && g.types ? g.types : []).filter(t => !t.hidden) |
| | | }, |
| | | confirmClear() { |
| | | if (!this.period) { this.$message.warning('请先选择报表期'); return } |
| | |
| | | '<br/>实际:' + (x.actualValue || '') + ' 基准:' + (x.thresholdValue || '') + |
| | | '<br/>差异:' + (x.deviation || '') + '</div>' |
| | | }).join('') |
| | | this.$alert(html, '投资数据与投资系统不一致(' + pending.length + ' 条)', { |
| | | dangerouslyUseHTMLString: true, type: 'warning', confirmButtonText: '知道了' |
| | | this.showResultDialog({ |
| | | title: '投资数据与投资系统不一致(' + pending.length + ' 条)', |
| | | html: html, |
| | | level: 'warning', |
| | | columns: [{ key: 'rule', label: '规则' }, { key: 'name', label: '项目/企业' }, |
| | | { key: 'actual', label: '实际值' }, { key: 'base', label: '基准值' }, { key: 'dev', label: '差异' }], |
| | | rows: pending.map(x => ({ |
| | | rule: x.ruleName || '', |
| | | name: x.enterpriseName || x.actualValue || '', |
| | | actual: x.actualValue || '', |
| | | base: x.thresholdValue || '', |
| | | dev: x.deviation || '' |
| | | })), |
| | | fileName: '投资数据与投资系统不一致_' + this.period |
| | | }) |
| | | } else { |
| | | this.$message.success('已与投资系统核对,本月数据一致') |
| | | } |
| | | }) |
| | | .catch(() => {}) |
| | | }, |
| | | showResultDialog(opts) { |
| | | const o = opts || {} |
| | | this.resultDialog = { |
| | | visible: true, |
| | | title: o.title || '导入结果', |
| | | html: o.html || '', |
| | | level: o.level || '', |
| | | rows: o.rows || [], |
| | | columns: o.columns || [], |
| | | fileName: o.fileName || ('导入结果_' + (this.period || '')) |
| | | } |
| | | }, |
| | | exportResultDialog() { |
| | | const cols = this.resultDialog.columns || [] |
| | | const rows = this.resultDialog.rows || [] |
| | | if (!cols.length || !rows.length) { this.$message.warning('没有可导出的明细'); return } |
| | | const esc = v => '"' + String(v === null || v === undefined ? '' : v).replace(/"/g, '""') + '"' |
| | | const lines = [cols.map(c => esc(c.label)).join(',')] |
| | | rows.forEach(r => lines.push(cols.map(c => esc(r[c.key])).join(','))) |
| | | const blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' }) |
| | | const url = URL.createObjectURL(blob) |
| | | const a = document.createElement('a') |
| | | a.href = url |
| | | a.download = this.resultDialog.fileName + '.csv' |
| | | document.body.appendChild(a) |
| | | a.click() |
| | | document.body.removeChild(a) |
| | | URL.revokeObjectURL(url) |
| | | }, |
| | | parseFailDetails(row) { |
| | | if (!row || !row.errorDetail) return [] |
| | |
| | | .module-tabs-head { margin-bottom: 10px; } |
| | | .module-tab-pane { min-height: 120px; } |
| | | </style> |
| | | |
| | | <style> |
| | | /* 导入结果弹窗:el-dialog 默认 append-to-body,DOM 挂在 body 上,scoped 选择器匹配不到,必须写全局 */ |
| | | .import-result-dialog { border-radius: 10px; overflow: hidden; } |
| | | .import-result-dialog .el-dialog__header { cursor: move; user-select: none; -webkit-user-select: none; } |
| | | .import-result-dialog .el-dialog__headerbtn { cursor: pointer; } |
| | | .import-result-dialog .el-dialog__body { max-height: calc(100vh - 210px); overflow-y: auto; overscroll-behavior: contain; padding: 12px 20px; } |
| | | .import-result-dialog .el-dialog__footer { border-top: 1px solid #EBEEF5; padding: 10px 20px; } |
| | | .import-result-dialog .ird-body { line-height: 1.7; font-size: 13px; color: #303133; } |
| | | .import-result-dialog .ird-level { margin-bottom: 8px; font-weight: 600; } |
| | | .import-result-dialog .ird-success { color: #67C23A; } |
| | | .import-result-dialog .ird-warning { color: #E6A23C; } |
| | | .import-result-dialog .ird-error { color: #F56C6C; } |
| | | </style> |