<template>
|
<el-container direction="vertical">
|
<app-header title="交通统计报表审核系统 - 数据查看"></app-header>
|
<el-container>
|
<el-aside width="200px">
|
<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="/explain">企业解释</el-menu-item>
|
<el-menu-item index="/audit">审核结果</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-aside>
|
<el-main>
|
<h3>导入数据查看</h3>
|
<div class="toolbar">
|
<template v-if="!isRollup">
|
<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>
|
</template>
|
<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="canRollup">
|
<span class="toolbar-label">视图</span>
|
<el-radio-group v-model="viewMode" size="small" @change="onViewChange">
|
<el-radio-button label="detail">明细</el-radio-button>
|
<el-radio-button label="rollup">按市州汇总</el-radio-button>
|
</el-radio-group>
|
</template>
|
<template v-if="isRollup">
|
<span class="toolbar-label">起止月</span>
|
<el-date-picker v-model="from" type="month" placeholder="起始月" value-format="yyyy-MM" style="width:130px"></el-date-picker>
|
<span>~</span>
|
<el-date-picker v-model="to" type="month" placeholder="结束月" value-format="yyyy-MM" style="width:130px"></el-date-picker>
|
<span class="toolbar-label">排名</span>
|
<el-select v-model="rankMode" size="small" style="width:150px">
|
<el-option label="留空" value="blank"></el-option>
|
<el-option label="按累计值重排" value="recompute"></el-option>
|
</el-select>
|
<el-button type="primary" size="small" @click="loadSummary">汇总</el-button>
|
<el-button type="success" size="small" icon="el-icon-download" @click="exportSummary">导出 Excel</el-button>
|
</template>
|
<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>
|
<template v-if="!isRollup">
|
<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>
|
</template>
|
<el-button size="small" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button>
|
</div>
|
<div class="load-hint">共 {{ total }} 条记录,已加载 {{ records.length }} 条<span v-if="isRollup && summaryHint" class="rollup-hint"> | {{ summaryHint }}</span></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-main>
|
</el-container>
|
</el-container>
|
</template>
|
<script>
|
import AppHeader from '@/components/AppHeader.vue'
|
import api from '@/api'
|
export default {
|
components: { AppHeader },
|
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: '',
|
viewMode: 'detail',
|
rollupTypes: [],
|
from: '',
|
to: '',
|
rankMode: 'blank',
|
summaryHint: '',
|
keyword: '',
|
page: 1,
|
size: 50,
|
total: 0,
|
records: [],
|
columns: [],
|
loading: false,
|
loadingMore: false
|
}
|
},
|
computed: {
|
canRollup() {
|
return (this.rollupTypes || []).some(t => t.type === this.type)
|
},
|
isRollup() {
|
return this.viewMode === 'rollup' && this.canRollup
|
},
|
// 规上规下拆分等表有"口径"列(当月/累计),把它做成上方筛选而不是表格列
|
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()
|
},
|
loadRollupTypes() {
|
api.get('/data/rollupTypes').then(res => {
|
// 拦截器已返回响应体,故 res.data 即类型数组
|
this.rollupTypes = (res && res.data) || []
|
}).catch(() => { this.rollupTypes = [] })
|
},
|
onViewChange() {
|
this.records = []
|
this.total = 0
|
if (this.isRollup) {
|
if (!this.to) this.to = this.period || this.defaultMonth()
|
if (!this.from) this.from = this.to.substring(0, 4) + '-01'
|
this.loadSummary()
|
} else {
|
this.summaryHint = ''
|
this.doQuery()
|
}
|
},
|
defaultMonth() {
|
const now = new Date()
|
return now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2)
|
},
|
loadSummary() {
|
if (!this.from || !this.to) { this.$message.warning('请先选择起止月'); return }
|
this.loading = true
|
api.get('/data/summary', { params: { type: this.type, from: this.from, to: this.to, rankMode: this.rankMode }, timeout: 120000 })
|
.then(res => {
|
const d = res.data || {}
|
this.columns = d.columns || []
|
this.records = d.records || []
|
this.total = this.records.length
|
this.summaryHint = (d.label || '') + ' ' + d.from + ' ~ ' + d.to + ' 累计;同比基数 ' + d.lastFrom + ' ~ ' + d.lastTo
|
+ ';排名口径:' + (d.rankMode === 'recompute' ? '按累计值重排' : '留空')
|
})
|
.catch(e => { if (!e || !e._toast) this.$message.error('汇总查询失败') })
|
.finally(() => { this.loading = false })
|
},
|
exportSummary() {
|
if (!this.from || !this.to) { this.$message.warning('请先选择起止月'); return }
|
api.get('/data/export', {
|
params: { type: this.type, from: this.from, to: this.to, rankMode: this.rankMode },
|
responseType: 'blob', timeout: 180000
|
}).then(res => {
|
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
const url = window.URL.createObjectURL(blob)
|
const a = document.createElement('a')
|
a.href = url
|
a.download = '按市州汇总_' + this.type + '_' + this.from + '_' + this.to + '.xlsx'
|
document.body.appendChild(a)
|
a.click()
|
document.body.removeChild(a)
|
window.URL.revokeObjectURL(url)
|
this.$message.success('汇总表已导出')
|
}).catch(() => this.$message.error('导出失败'))
|
},
|
onTypeSelect() {
|
const g = this.modules.find(x => x.types.some(t => t.value === this.type))
|
if (g) this.activeModule = g.key
|
this.periodType = ''
|
this.viewMode = 'detail'
|
this.summaryHint = ''
|
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
|
}
|
}).then(res => {
|
const d = res.data || {}
|
this.records = d.records || []
|
this.columns = d.columns || []
|
this.total = d.total || 0
|
if (this.page > 1 && this.records.length === 0) {
|
this.page = 1
|
this.loadData()
|
}
|
}).catch(e => {
|
if (e && e._toast) return
|
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.tableWrap
|
if (!document.fullscreenElement) {
|
if (el && el.requestFullscreen) el.requestFullscreen()
|
else this.$message.warning("浏览器不支持全屏")
|
} else if (document.exitFullscreen) {
|
document.exitFullscreen()
|
}
|
},
|
// 显示格式化(不改动底层数据,导出仍为全精度):
|
// - 比率类列(环比/同比/增速/占比)库内是小数(0.045 = 4.5%),显示为百分比;
|
// - 排名类列显示整数;
|
// - 其余数值千分位、保留两位小数;整数原样。
|
formatCell(v, prop) {
|
if (v === null || v === undefined || v === '') return '-'
|
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() {
|
const q = this.$route.query
|
if (q.type) this.type = q.type
|
if (q.period) this.period = q.period
|
if (q.keyword) this.keyword = q.keyword
|
this.loadRollupTypes()
|
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; }
|
.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 .rollup-hint { color:#409EFF; }
|
#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;
|
}
|
.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>
|