traffic-audit-web/src/views/DataView.vue
@@ -22,11 +22,19 @@
          <span class="toolbar-sep"></span>
          <span class="toolbar-label">数据类型</span>
          <el-select v-model="type" size="small" style="width:300px" placeholder="选择数据类型" filterable
                     :title="currentTypeLabel" @change="onTypeSelect">
                     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>
@@ -37,7 +45,7 @@
            <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]) }}</span>
                  <span>{{ formatCell(scope.row[c.prop], c.prop) }}</span>
                </template>
              </el-table-column>
            </el-table>
@@ -99,6 +107,7 @@
      activeModule: 'freight',
      type: 'h2032',
      period: '',
      periodType: '',
      keyword: '',
      page: 1,
      size: 50,
@@ -110,6 +119,10 @@
    }
  },
  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)
@@ -122,33 +135,31 @@
      return g
    },
    visibleColumns() {
      const hiddenProps = ['enterpriseCode', 'unifiedCreditCode', 'reportUnit']
      const hiddenLabels = ['企业代码', '统一社会信用代码', '填报单位']
      return (this.columns || []).filter(c =>
        hiddenProps.indexOf(c.prop) < 0 && hiddenLabels.indexOf(c.label) < 0)
      return (this.columns || []).filter(c => this.keepColumn(c))
    },
    // 左侧冻结"这是谁的数据"所需列:横向滚动时企业名称/市州不会跑出视野。
    // 只冻结从第 1 列开始的一段连续列,避免冻结列之间被普通列打断。
    // 左侧冻结"这是谁的数据"的列:市州/地区 + 企业(项目)名称/车牌号,最多 3 列。
    // 报表期与所属地区不参与冻结(报表期是筛选条件,所属地区是区划代码,已由市州列替代)。
    renderColumns() {
      const stickyProps = ['reportPeriod', 'regionCode', 'city', 'regionName', 'periodType',
        'enterpriseName', 'projectName', 'plateNo']
      const nameProps = ['enterpriseName', 'projectName', 'plateNo']
      const stickyProps = ['city', 'regionName', 'region', 'periodType', 'enterpriseName', 'projectName', 'plateNo']
      const cols = this.visibleColumns
      let limit = -1
      for (let i = 0; i < cols.length && i < 6; i++) {
        if (nameProps.indexOf(cols[i].prop) >= 0) { limit = Math.min(i, 3); break }
      let count = 0
      while (count < cols.length && count < 3 && stickyProps.indexOf(cols[count].prop) >= 0) {
        count++
      }
      if (limit < 0) {
        // 表里没有名称列(如货运量周转量)时,冻结开头的连续标识列
        limit = 0
        while (limit + 1 < cols.length && limit < 2 && stickyProps.indexOf(cols[limit + 1].prop) >= 0) {
          limit++
        }
      }
      return cols.map((c, i) => Object.assign({}, c, { fixed: i <= limit && stickyProps.indexOf(c.prop) >= 0 }))
      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
@@ -160,6 +171,7 @@
    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() {
@@ -175,13 +187,14 @@
          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 || []).filter(c => ['enterpriseCode', 'unifiedCreditCode', 'reportUnit'].indexOf(c.prop) < 0 && ['企业代码', '统一社会信用代码', '填报单位'].indexOf(c.label) < 0)
        this.columns = d.columns || []
        this.total = d.total || 0
        if (this.page > 1 && this.records.length === 0) {
          this.page = 1
@@ -217,6 +230,7 @@
          type: this.type,
          period: this.period || undefined,
          keyword: this.keyword || undefined,
          periodType: this.periodType || undefined,
          page: nextPage,
          size: this.size
        }
@@ -230,7 +244,7 @@
        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.filter(c => ['enterpriseCode', 'unifiedCreditCode', 'reportUnit'].indexOf(c.prop) < 0 && ['企业代码', '统一社会信用代码', '填报单位'].indexOf(c.label) < 0)
        if (d.columns && d.columns.length) this.columns = d.columns
      }).catch(e => {
        if (e && e._toast) return
        this.$message.error('加载更多失败')
@@ -267,9 +281,21 @@
        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() {
@@ -310,4 +336,20 @@
#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>
<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>