traffic-audit-web/src/views/DataView.vue
@@ -3,7 +3,7 @@
    <app-header title="交通统计报表审核系统 - 数据查看"></app-header>
    <el-container>
      <el-aside width="200px">
        <el-menu router default-active="/data">
        <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>
@@ -45,20 +45,22 @@
        <div style="color:#909399;font-size:12px;margin-bottom:6px">
          共 {{ total }} 条记录,当前第 {{ page }} 页
        </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="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 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>
          <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>
        </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>
@@ -79,12 +81,14 @@
      total: 0,
      records: [],
      columns: [],
      loading: false
      loading: false,
      loadingMore: false
    }
  },
  methods: {
    loadData() {
      this.loading = true
      this.loadingMore = false
      api.get('/data/list', {
        params: {
          type: this.type,
@@ -107,10 +111,74 @@
        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,
          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("浏览器不支持全屏")
@@ -129,14 +197,62 @@
    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-wrap {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 330px);
  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-wrap .el-pagination {
  flex: none;
  margin-top: 8px;
}
#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;
}
#app .table-wrap:fullscreen .el-pagination {
  flex: none;
}
</style>