| | |
| | | </el-aside> |
| | | <el-main> |
| | | <h3>导入数据查看</h3> |
| | | <el-form :inline="true"> |
| | | <el-form-item label="数据类型"> |
| | | <el-radio-group v-model="type" @change="loadData"> |
| | | <el-radio-button label="h2032">H203-2月报</el-radio-button> |
| | | <el-radio-button label="transportAuth">运政车辆</el-radio-button> |
| | | <el-radio-button label="trackMileage">轨迹里程</el-radio-button> |
| | | <el-radio-button label="scaleSplit">规上规下拆分</el-radio-button> |
| | | <el-radio-button label="freightTurnover">货运量周转量</el-radio-button> |
| | | <el-radio-button label="h2031">H203-1旅客月报</el-radio-button> |
| | | <el-radio-button label="passengerAuth">旅客运政车辆</el-radio-button> |
| | | <el-radio-button label="cityBus">城市公交月报</el-radio-button> |
| | | <el-radio-button label="cityTaxi">巡游出租月报</el-radio-button> |
| | | <el-radio-button label="cityTaxiAuth">出租车运政车辆</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" clearable style="width:140px" @change="loadData"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item label="关键字"> |
| | | <el-input v-model="keyword" placeholder="企业/市州名称" clearable style="width:200px" @keyup.enter.native="loadData"></el-input> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="loadData">查询</el-button> |
| | | <el-button type="success" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div style="color:#909399;font-size:12px;margin-bottom:6px"> |
| | | 共 {{ total }} 条记录,当前第 {{ page }} 页 |
| | | <div class="toolbar"> |
| | | <span class="toolbar-label">报表期</span> |
| | | <el-date-picker v-model="period" type="month" placeholder="报表期(全部)" value-format="yyyy-MM" clearable style="width:150px" @change="doQuery"></el-date-picker> |
| | | <span class="toolbar-sep"></span> |
| | | <span class="toolbar-label">数据类型</span> |
| | | <el-select v-model="type" size="small" style="width:300px" placeholder="选择数据类型" filterable |
| | | popper-class="data-type-select" :title="currentTypeLabel" @change="onTypeSelect"> |
| | | <el-option-group v-for="g in modules" :key="g.key" :label="g.name"> |
| | | <el-option v-for="t in g.types" :key="t.value" :label="t.label" :value="t.value"></el-option> |
| | | </el-option-group> |
| | | </el-select> |
| | | <template v-if="hasPeriodType"> |
| | | <span class="toolbar-label">口径</span> |
| | | <el-select v-model="periodType" size="small" style="width:110px" @change="doQuery"> |
| | | <el-option label="全部" value=""></el-option> |
| | | <el-option label="当月" value="MONTH"></el-option> |
| | | <el-option label="累计" value="CUMULATIVE"></el-option> |
| | | </el-select> |
| | | </template> |
| | | <el-input v-model="keyword" placeholder="企业/市州名称" clearable style="width:180px" @keyup.enter.native="doQuery"></el-input> |
| | | <el-button type="primary" size="small" @click="doQuery">查询</el-button> |
| | | <el-button size="small" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button> |
| | | </div> |
| | | <div class="table-area" ref="tableArea"> |
| | | <el-table :data="records" border v-loading="loading" size="small" style="width:100%" height="100%"> |
| | | <el-table-column v-for="c in columns" :key="c.prop" :prop="c.prop" :label="c.label" :min-width="c.width" show-overflow-tooltip> |
| | | <template slot-scope="scope"> |
| | | <span>{{ formatCell(scope.row[c.prop]) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="load-hint">共 {{ total }} 条记录,已加载 {{ records.length }} 条</div> |
| | | <div class="table-wrap" ref="tableWrap"> |
| | | <div class="table-area"> |
| | | <el-table :data="records" border v-loading="loading" size="small" style="width:100%" height="100%"> |
| | | <el-table-column v-for="c in renderColumns" :key="c.prop" :prop="c.prop" :label="c.label" :min-width="c.width" :fixed="c.fixed ? 'left' : false" show-overflow-tooltip> |
| | | <template slot-scope="scope"> |
| | | <span>{{ formatCell(scope.row[c.prop], c.prop) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <el-pagination style="margin-top:15px;text-align:right" |
| | | background layout="total, prev, pager, next, sizes" |
| | | :total="total" :current-page.sync="page" :page-size.sync="size" |
| | | :page-sizes="[20, 50, 100]" @current-change="loadData" @size-change="loadData"> |
| | | </el-pagination> |
| | | |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | |
| | | name: 'DataView', |
| | | data() { |
| | | return { |
| | | modules: [ |
| | | { |
| | | key: 'freight', name: '道路货运', types: [ |
| | | { value: 'h2032', label: '道路货物运输月度生产情况' }, |
| | | { value: 'transportAuth', label: '运政车辆' }, |
| | | { value: 'trackMileage', label: '轨迹里程' }, |
| | | { value: 'scaleSplit', label: '规上规下拆分' }, |
| | | { value: 'freightTurnover', label: '货运量周转量' } |
| | | ] |
| | | }, |
| | | { |
| | | key: 'passenger', name: '公路旅客', types: [ |
| | | { value: 'h2031', label: '公路旅客运输月度生产情况' }, |
| | | { value: 'passengerAuth', label: '运政车辆数' }, |
| | | { value: 'passengerIndividual', label: '个体客运量/周转量' } |
| | | ] |
| | | }, |
| | | { |
| | | key: 'city', name: '城市客运', types: [ |
| | | { value: 'cityBus', label: '城市公共交通月度运营情况' }, |
| | | { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' }, |
| | | { value: 'cityTaxiAuth', label: '出租车运政车辆' }, |
| | | { value: 'wycTotal', label: '网约车总数' }, |
| | | { value: 'wycOrder', label: '网约车订单及全省总量' } |
| | | ] |
| | | }, |
| | | { |
| | | key: 'invest', name: '投资', types: [ |
| | | { value: 'investmentProject', label: '投资项目基础表' }, |
| | | { value: 'investmentMonthly', label: '市州月报项目进度' }, |
| | | { value: 'investmentSystem', label: '投资系统每月导入项目表' } |
| | | ] |
| | | }, |
| | | { |
| | | key: 'energy', name: '能耗', types: [ |
| | | { value: 'energyVehicle', label: '车辆能耗季度数据' }, |
| | | { value: 'energyAuth', label: '能耗运政车辆数' } |
| | | ] |
| | | } |
| | | ], |
| | | activeModule: 'freight', |
| | | type: 'h2032', |
| | | period: '', |
| | | periodType: '', |
| | | keyword: '', |
| | | page: 1, |
| | | size: 20, |
| | | size: 50, |
| | | total: 0, |
| | | records: [], |
| | | columns: [], |
| | | loading: false |
| | | loading: false, |
| | | loadingMore: false |
| | | } |
| | | }, |
| | | computed: { |
| | | // 规上规下拆分等表有"口径"列(当月/累计),把它做成上方筛选而不是表格列 |
| | | hasPeriodType() { |
| | | return (this.columns || []).some(c => c.prop === 'periodType') |
| | | }, |
| | | currentTypeLabel() { |
| | | for (const g of this.modules) { |
| | | const t = g.types.find(x => x.value === this.type) |
| | | if (t) return g.name + ' · ' + t.label |
| | | } |
| | | return this.type |
| | | }, |
| | | activeTypes() { |
| | | const g = this.modules.find(x => x.key === this.activeModule) || this.modules[0] |
| | | return g |
| | | }, |
| | | visibleColumns() { |
| | | return (this.columns || []).filter(c => this.keepColumn(c)) |
| | | }, |
| | | // 左侧冻结"这是谁的数据"的列:市州/地区 + 企业(项目)名称/车牌号,最多 3 列。 |
| | | // 报表期与所属地区不参与冻结(报表期是筛选条件,所属地区是区划代码,已由市州列替代)。 |
| | | renderColumns() { |
| | | const stickyProps = ['city', 'regionName', 'region', 'periodType', 'enterpriseName', 'projectName', 'plateNo'] |
| | | const cols = this.visibleColumns |
| | | let count = 0 |
| | | while (count < cols.length && count < 3 && stickyProps.indexOf(cols[count].prop) >= 0) { |
| | | count++ |
| | | } |
| | | return cols.map((c, i) => Object.assign({}, c, { fixed: i < count })) |
| | | } |
| | | }, |
| | | methods: { |
| | | // 数据查看统一列过滤:内部标识列不展示;报表期(由筛选条件体现)与所属地区(区划代码,已被市州列替代)不展示 |
| | | keepColumn(c) { |
| | | if (!c) return false |
| | | // 口径列上移为筛选;拆分表货运量三列源文件未提供(全空)故不展示 |
| | | const hiddenProps = ['enterpriseCode', 'unifiedCreditCode', 'reportUnit', 'projectId', 'regionCode', 'reportPeriod', |
| | | 'periodType', 'aboveScaleFreight', 'belowScaleFreight', 'totalFreight'] |
| | | const hiddenLabels = ['企业代码', '统一社会信用代码', '填报单位', '项目ID', '所属地区', '报表期', '口径'] |
| | | if (hiddenProps.indexOf(c.prop) >= 0 || hiddenLabels.indexOf(c.label) >= 0) return false |
| | | return true |
| | | }, |
| | | onModuleChange(key) { |
| | | const g = this.modules.find(x => x.key === key) |
| | | if (!g || !g.types.length) return |
| | | if (!g.types.some(t => t.value === this.type)) { |
| | | this.type = g.types[0].value |
| | | } |
| | | this.doQuery() |
| | | }, |
| | | onTypeSelect() { |
| | | const g = this.modules.find(x => x.types.some(t => t.value === this.type)) |
| | | if (g) this.activeModule = g.key |
| | | this.periodType = '' |
| | | this.doQuery() |
| | | }, |
| | | doQuery() { |
| | | this.page = 1 |
| | | this.records = [] |
| | | this.loadData() |
| | | }, |
| | | loadData() { |
| | | this.loading = true |
| | | this.loadingMore = false |
| | | api.get('/data/list', { |
| | | params: { |
| | | type: this.type, |
| | | period: this.period || undefined, |
| | | keyword: this.keyword || undefined, |
| | | periodType: this.periodType || undefined, |
| | | page: this.page, |
| | | size: this.size |
| | | } |
| | |
| | | this.$message.error('数据查询失败') |
| | | }).finally(() => { |
| | | this.loading = false |
| | | this.$nextTick(() => this.attachBodyScroll()) |
| | | }) |
| | | }, |
| | | // el-table 固定高度后,真正的竖向滚动容器是内部的 body-wrapper(页面本身不滚动) |
| | | attachBodyScroll() { |
| | | const wrap = this.$refs.tableWrap |
| | | if (!wrap) return |
| | | const bw = wrap.querySelector('.el-table__body-wrapper') |
| | | if (bw && bw._vwScrollHandler !== this.onScroll) { |
| | | if (bw._vwScrollHandler) bw.removeEventListener('scroll', bw._vwScrollHandler) |
| | | bw.addEventListener('scroll', this.onScroll, { passive: true }) |
| | | bw._vwScrollHandler = this.onScroll |
| | | } |
| | | }, |
| | | // 滚动到底部自动追加下一页(保留手动分页跳页能力) |
| | | loadMore() { |
| | | if (this.loading || this.loadingMore) return |
| | | if (this.records.length >= this.total || this.total <= 0) return |
| | | const nextPage = this.page + 1 |
| | | this.loadingMore = true |
| | | api.get('/data/list', { |
| | | params: { |
| | | type: this.type, |
| | | period: this.period || undefined, |
| | | keyword: this.keyword || undefined, |
| | | periodType: this.periodType || undefined, |
| | | page: nextPage, |
| | | size: this.size |
| | | } |
| | | }).then(res => { |
| | | const d = res.data || {} |
| | | const addRows = d.records || [] |
| | | if (!addRows.length) { |
| | | this.page = nextPage - 1 |
| | | return |
| | | } |
| | | this.records = this.records.concat(addRows) |
| | | this.page = nextPage |
| | | if (d.total != null) this.total = d.total |
| | | if (d.columns && d.columns.length) this.columns = d.columns |
| | | }).catch(e => { |
| | | if (e && e._toast) return |
| | | this.$message.error('加载更多失败') |
| | | }).finally(() => { |
| | | this.loadingMore = false |
| | | this.$nextTick(() => this.attachBodyScroll()) |
| | | }) |
| | | }, |
| | | onScroll(e) { |
| | | // 普通/全屏下真正的滚动容器都是 el-table 内部 body-wrapper(捕获阶段可收到); |
| | | // 页面整体滚动时以 window/document 为准。 |
| | | const t = e && e.target |
| | | let scrollTop, clientHeight, scrollHeight |
| | | if (t && t.nodeType === 1 && t !== document && typeof t.scrollTop === 'number' && t.scrollHeight > t.clientHeight) { |
| | | scrollTop = t.scrollTop |
| | | clientHeight = t.clientHeight |
| | | scrollHeight = t.scrollHeight |
| | | } else { |
| | | const se = document.scrollingElement || document.documentElement |
| | | scrollTop = se.scrollTop |
| | | clientHeight = window.innerHeight |
| | | scrollHeight = se.scrollHeight |
| | | } |
| | | if (scrollHeight > 0 && scrollTop + clientHeight >= scrollHeight - 80) { |
| | | this.loadMore() |
| | | } |
| | | }, |
| | | toggleFullscreen() { |
| | | const el = this.$refs.tableArea |
| | | const el = this.$refs.tableWrap |
| | | if (!document.fullscreenElement) { |
| | | if (el && el.requestFullscreen) el.requestFullscreen() |
| | | else this.$message.warning("浏览器不支持全屏") |
| | |
| | | document.exitFullscreen() |
| | | } |
| | | }, |
| | | formatCell(v) { |
| | | // 显示格式化(不改动底层数据,导出仍为全精度): |
| | | // - 比率类列(环比/同比/增速/占比)库内是小数(0.045 = 4.5%),显示为百分比; |
| | | // - 排名类列显示整数; |
| | | // - 其余数值千分位、保留两位小数;整数原样。 |
| | | formatCell(v, prop) { |
| | | if (v === null || v === undefined || v === '') return '-' |
| | | return v |
| | | const s = typeof v === 'string' ? v.trim() : v |
| | | if (typeof v !== 'number' && !/^-?\d+(\.\d+)?$/.test(String(s))) return v |
| | | const n = Number(s) |
| | | if (!isFinite(n)) return v |
| | | const key = String(prop || '').toLowerCase() |
| | | if (/rank/.test(key)) return Math.round(n).toLocaleString('zh-CN') |
| | | if (/(rate|yoy|mom|growth|ratio|percent|share)/.test(key)) return (n * 100).toFixed(2) + '%' |
| | | if (Number.isInteger(n)) return n.toLocaleString('zh-CN') |
| | | return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) |
| | | } |
| | | }, |
| | | mounted() { |
| | |
| | | if (q.period) this.period = q.period |
| | | if (q.keyword) this.keyword = q.keyword |
| | | this.loadData() |
| | | window.addEventListener('scroll', this.onScroll, { passive: true }) |
| | | if (this.$refs.tableWrap) { |
| | | // capture=true:el-table 内部 body-wrapper 的滚动在捕获阶段也能收到 |
| | | this.$refs.tableWrap.addEventListener('scroll', this.onScroll, { passive: true, capture: true }) |
| | | } |
| | | this.$nextTick(() => this.attachBodyScroll()) |
| | | }, |
| | | beforeDestroy() { |
| | | window.removeEventListener('scroll', this.onScroll) |
| | | if (this.$refs.tableWrap) { |
| | | this.$refs.tableWrap.removeEventListener('scroll', this.onScroll, { capture: true }) |
| | | const bw = this.$refs.tableWrap.querySelector('.el-table__body-wrapper') |
| | | if (bw && bw._vwScrollHandler) bw.removeEventListener('scroll', bw._vwScrollHandler) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | <style scoped> |
| | | .el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; } |
| | | #app .table-area:fullscreen { |
| | | padding: 20px; |
| | | background: #fff; |
| | | overflow: auto; |
| | | .type-row { margin-bottom: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } |
| | | .type-label { color: #606266; font-weight: 600; } |
| | | .filter-row { margin-bottom: 4px; } |
| | | #app .toolbar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:8px 12px; background:#FAFAFA; border:1px solid #EBEEF5; border-radius:8px; margin-bottom:8px; } |
| | | #app .toolbar-label { color:#606266; font-weight:600; white-space:nowrap; } |
| | | #app .toolbar-sep { width:1px; height:22px; background:#DCDFE6; margin:0 2px; } |
| | | #app .load-hint { color:#909399; font-size:12px; margin-bottom:6px; } |
| | | #app .table-wrap { display:flex; flex-direction:column; height:calc(100vh - 250px); min-height:300px; } |
| | | #app .table-area { flex:1 1 auto; min-height:0; overflow:hidden; } |
| | | #app .table-area .el-table { width:100%; height:100%; } |
| | | #app .table-area:fullscreen { padding:20px; background:#fff; overflow:auto; } |
| | | #app .table-wrap:fullscreen { padding:20px; background:#fff; overflow:auto; display:flex; flex-direction:column; height:100vh; } |
| | | #app .table-wrap:fullscreen .table-area { flex:1 1 auto; min-height:0; } |
| | | </style> |
| | | <style> |
| | | /* 数据类型下拉:Element UI 默认把分组标题做成 12px 灰色小字,看起来"比选项更小更淡"。 |
| | | 这里把它改成明确的组标题样式(加深、加粗、略大、带底色),与选项形成有意的层级。 */ |
| | | .data-type-select .el-select-group__title { |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: #303133; |
| | | line-height: 28px; |
| | | background: #F5F7FA; |
| | | padding-left: 12px; |
| | | letter-spacing: .5px; |
| | | } |
| | | </style> |
| | | .data-type-select .el-select-group__title::after { display: none; } |
| | | .data-type-select .el-select-group { padding-bottom: 6px; } |
| | | .data-type-select .el-select-group .el-select-dropdown__item { padding-left: 20px; } |
| | | </style> |