Appearance
BizVirtualTable 虚拟滚动表格
基于 el-table-v2 的虚拟滚动表格:schema 简化为 key / label / width / align / fixed / slot,单元格插槽(cell-<key> 形式的具名插槽)经 cellRenderer 桥接。支撑 10 万行级数据流畅滚动,适用于日志、流水类超大列表。
与 BizTable 的取舍:BizTable 功能全(多选 / 分页 / 工具栏 / 远程数据源),BizVirtualTable 主打超大列表渲染性能;列宽必须为数值(table-v2 固定列宽约束)。
基础用法
共 100,000 行日志数据,滚动到底部观察流畅度(虚拟渲染仅挂载可视区行)。
网关
INFO
3
2026-09-21 00:00:00
订单中心
WARN
4
2026-09-21 01:01:07
库存中心
ERROR
5
2026-09-21 02:02:14
支付中心
INFO
6
2026-09-21 03:03:21
网关
WARN
7
2026-09-21 04:04:28
订单中心
ERROR
8
2026-09-21 05:05:35
库存中心
INFO
9
2026-09-21 06:06:42
支付中心
WARN
10
2026-09-21 07:07:49
网关
ERROR
11
2026-09-21 08:08:56
订单中心
INFO
12
2026-09-21 09:09:03
库存中心
WARN
13
2026-09-21 10:10:10
服务
级别
耗时(ms)
发生时间
1
TRACE-000001
2
TRACE-000002
3
TRACE-000003
4
TRACE-000004
5
TRACE-000005
6
TRACE-000006
7
TRACE-000007
8
TRACE-000008
9
TRACE-000009
10
TRACE-000010
11
TRACE-000011
序号
链路号
10 万行日志数据 + 固定列 + 状态列插槽渲染,滚动全程仅渲染可视区行。
<script setup lang="ts">
/** 基础用法:10 万行随机数据虚拟滚动 + 状态列插槽渲染 */
import { BizStatusTag, BizVirtualTable } from 'vue3-ep-biz-lib'
import type { BizVirtualColumn } from 'vue3-ep-biz-lib'
/** 生成 10 万行 mock 数据(页面依旧流畅) */
const data = Array.from({ length: 100000 }, (_unused, i) => ({
id: i + 1,
traceNo: `TRACE-${String(i + 1).padStart(6, '0')}`,
service: ['网关', '订单中心', '库存中心', '支付中心'][i % 4],
level: (['info', 'warning', 'error'] as const)[i % 3],
cost: (i % 997) + 3,
time: `2026-09-21 ${String(i % 24).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}:${String((i * 7) % 60).padStart(2, '0')}`,
}))
const columns: BizVirtualColumn[] = [
{ key: 'id', label: '序号', width: 80, fixed: 'left' },
{ key: 'traceNo', label: '链路号', width: 160, fixed: 'left' },
{ key: 'service', label: '服务', width: 140 },
{ key: 'level', label: '级别', width: 110, slot: 'level' },
{ key: 'cost', label: '耗时(ms)', width: 120, align: 'right' },
{ key: 'time', label: '发生时间', width: 180 },
]
</script>
<template>
<div>
<p style="margin: 0 0 8px; color: var(--el-text-color-secondary); font-size: 13px">
共 {{ data.length.toLocaleString() }} 行日志数据,滚动到底部观察流畅度(虚拟渲染仅挂载可视区行)。
</p>
<BizVirtualTable :columns="columns" :data="data" :height="420">
<template #level="{ row }">
<BizStatusTag
:status="String(row.level)"
:status-map="{
info: { text: 'INFO', type: 'info' },
warning: { text: 'WARN', type: 'warning' },
error: { text: 'ERROR', type: 'danger' },
}"
/>
</template>
</BizVirtualTable>
</div>
</template>
显示代码
行着色与事件透传
紧凑行高 38px;error 行整行标红、slow 行淡黄(row-class 透传);点击任意行可定位(row-event-handlers 透传)。
/api/order/create
正常
12
/api/pay/callback
慢调用
13
/api/stock/lock
失败
14
/api/order/create
正常
15
/api/pay/callback
慢调用
16
/api/stock/lock
失败
17
/api/order/create
正常
18
/api/pay/callback
慢调用
19
/api/stock/lock
失败
20
/api/order/create
正常
21
/api/pay/callback
慢调用
22
接口
状态
耗时(ms)
1
TRACE-000001
2
TRACE-000002
3
TRACE-000003
4
TRACE-000004
5
TRACE-000005
6
TRACE-000006
7
TRACE-000007
8
TRACE-000008
9
TRACE-000009
10
TRACE-000010
11
TRACE-000011
序号
链路号
调用链监控场景:
row-class 为 error / slow 行整行着色;row-event-handlers(table-v2 行事件 prop,{ onClick } 形式)实现点击行定位;row-height 收紧至 38px 提升单屏信息密度(5 万行依旧流畅)。<script setup lang="ts">
/** 进阶:row-class 告警行整行着色、row-event-handlers 行点击定位(均经 attrs 透传 table-v2)、紧凑行高 */
import { ref } from 'vue'
import { ElTag } from 'element-plus'
import { BizVirtualTable } from 'vue3-ep-biz-lib'
import type { BizVirtualColumn, BizVirtualRow } from 'vue3-ep-biz-lib'
const data: BizVirtualRow[] = Array.from({ length: 50000 }, (_unused, i) => ({
id: i + 1,
traceNo: `TRACE-${String(i + 1).padStart(6, '0')}`,
api: ['/api/order/create', '/api/pay/callback', '/api/stock/lock'][i % 3],
status: (['ok', 'slow', 'error'] as const)[i % 3],
cost: (i % 1200) + 12,
}))
const columns: BizVirtualColumn[] = [
{ key: 'id', label: '序号', width: 70, fixed: 'left' },
{ key: 'traceNo', label: '链路号', width: 170, fixed: 'left' },
{ key: 'api', label: '接口', width: 210 },
{ key: 'status', label: '状态', width: 110, slot: 'status' },
{ key: 'cost', label: '耗时(ms)', width: 120, align: 'right' },
]
const clicked = ref<BizVirtualRow>()
const rowClass = ({ rowData }: { rowData: BizVirtualRow; rowIndex: number }): string =>
rowData.status === 'error' ? 'demo-vt-row--error' : rowData.status === 'slow' ? 'demo-vt-row--slow' : ''
const onRowClick = ({ rowData }: { rowData: BizVirtualRow; rowIndex: number; rowKey: unknown; event: Event }): void => {
clicked.value = rowData
}
</script>
<template>
<div>
<p style="margin: 0 0 8px; font-size: 13px; color: var(--el-text-color-secondary)">
紧凑行高 38px;error 行整行标红、slow 行淡黄(row-class 透传);点击任意行可定位(row-event-handlers 透传)。
</p>
<BizVirtualTable
:columns="columns"
:data="data"
:height="380"
:row-height="38"
:header-height="40"
:row-class="rowClass"
:row-event-handlers="{ onClick: onRowClick }"
>
<template #status="{ row }">
<ElTag size="small" :type="row.status === 'error' ? 'danger' : row.status === 'slow' ? 'warning' : 'success'">
{{ row.status === 'error' ? '失败' : row.status === 'slow' ? '慢调用' : '正常' }}
</ElTag>
</template>
</BizVirtualTable>
<p v-if="clicked" style="margin: 8px 0 0; font-size: 13px">
选中:{{ clicked.traceNo }}({{ clicked.api }},{{ clicked.cost }}ms)
</p>
</div>
</template>
<style>
.demo-vt-row--error {
background-color: var(--el-color-danger-light-9);
}
.demo-vt-row--slow {
background-color: var(--el-color-warning-light-9);
}
</style>
显示代码
实时日志面板
每秒追加 3 条,当前 0 行;ERROR / WARN 行整行着色(row-class 透传)
级别
服务
日志内容
时间
暂无数据
运维场景:每秒追加 3 条日志、滚动淘汰保持 5000 行上限,
row-class 让 ERROR / WARN 行整行着色,级别列插槽渲染标签——持续追加场景下滚动始终流畅。<script setup lang="ts">
/** 场景:实时日志面板——持续追加 + 告警行着色,万行级依旧流畅 */
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { ElTag } from 'element-plus'
import { BizVirtualTable } from 'vue3-ep-biz-lib'
import type { BizVirtualColumn, BizVirtualRow } from 'vue3-ep-biz-lib'
const columns: BizVirtualColumn[] = [
{ key: 'time', label: '时间', width: 110, fixed: 'left' },
{ key: 'level', label: '级别', width: 90, slot: 'level' },
{ key: 'service', label: '服务', width: 130 },
{ key: 'message', label: '日志内容', width: 460 },
]
const logs = ref<BizVirtualRow[]>([])
let seq = 0
let timer: ReturnType<typeof setInterval> | null = null
const LEVELS = ['INFO', 'INFO', 'INFO', 'WARN', 'ERROR'] as const
const MESSAGES = [
'请求处理完成,耗时 42ms',
'缓存命中率 97.3%',
'数据库连接池活跃 12/50',
'慢查询告警:order/query 耗时 1.8s',
'下游支付网关超时,已进入重试队列(第 1/3 次)',
]
/** 每秒追加一批新日志(上限 5000 行滚动淘汰旧数据) */
onMounted(() => {
timer = setInterval(() => {
const batch = Array.from({ length: 3 }, (): BizVirtualRow => {
const level = LEVELS[seq % LEVELS.length]
const date = new Date()
return {
id: ++seq,
time: `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`,
level,
service: ['gateway', 'order-center', 'pay-center'][seq % 3],
message: MESSAGES[seq % MESSAGES.length],
}
})
logs.value = [...logs.value, ...batch].slice(-5000)
}, 1000)
})
onBeforeUnmount(() => {
if (timer !== null) clearInterval(timer)
})
const rowClass = ({ rowData }: { rowData: BizVirtualRow; rowIndex: number }): string =>
rowData.level === 'ERROR' ? 'demo-live-log--error' : rowData.level === 'WARN' ? 'demo-live-log--warn' : ''
</script>
<template>
<div>
<p style="margin: 0 0 8px; font-size: 13px; color: var(--el-text-color-secondary)">
每秒追加 3 条,当前 {{ logs.length }} 行;ERROR / WARN 行整行着色(row-class 透传)
</p>
<BizVirtualTable :columns="columns" :data="logs" :height="300" :row-height="36" :header-height="40" :row-class="rowClass">
<template #level="{ row }">
<ElTag size="small" :type="row.level === 'ERROR' ? 'danger' : row.level === 'WARN' ? 'warning' : 'info'">
{{ row.level }}
</ElTag>
</template>
</BizVirtualTable>
</div>
</template>
<style>
.demo-live-log--error {
background-color: var(--el-color-danger-light-9);
}
.demo-live-log--warn {
background-color: var(--el-color-warning-light-9);
}
</style>
显示代码
财务流水 20 万行
共 200,000 条流水;支出行淡红底(row-class),金额列插槽渲染 BizAmount
6222 0212 1000
支出
1
5000000
09-01 00:00
6222 0212 1001
收入
2
4999993
09-02 01:01
6222 0212 1002
收入
3
4999986
09-03 02:02
6222 0212 1003
支出
4
4999979
09-04 03:03
6222 0212 1004
收入
5
4999972
09-05 04:04
6222 0212 1005
收入
6
4999965
09-06 05:05
6222 0212 1006
支出
7
4999958
09-07 06:06
6222 0212 1007
收入
8
4999951
09-08 07:07
6222 0212 1008
收入
9
4999944
09-09 08:08
6222 0212 1009
支出
10
4999937
09-10 09:09
6222 0212 1010
收入
11
4999930
09-11 10:10
对方账户
收支
金额(分)
余额(分)
时间
FLW100000000
FLW100000001
FLW100000002
FLW100000003
FLW100000004
FLW100000005
FLW100000006
FLW100000007
FLW100000008
FLW100000009
FLW100000010
流水号
财务场景:20 万行流水 + 支出行 row-class 着色 + 金额列插槽渲染 BizAmount + 双固定列。
<script setup lang="ts">
/** 场景:财务流水 20 万行——收支着色 + 金额右对齐 + 固定列 */
import { BizAmount, BizVirtualTable } from 'vue3-ep-biz-lib'
import type { BizVirtualColumn, BizVirtualRow } from 'vue3-ep-biz-lib'
const data: BizVirtualRow[] = Array.from({ length: 200000 }, (_unused, i) => ({
id: i + 1,
flowNo: `FLW${String(100000000 + i)}`,
account: `6222 0212 ${String(1000 + (i % 9000))}`,
direction: i % 3 === 0 ? 'expense' : 'income',
amount: (i % 99000) + 1,
balance: 5000000 - i * 7,
time: `09-${String(1 + (i % 23)).padStart(2, '0')} ${String(i % 24).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}`,
}))
const rowClass = ({ rowData }: { rowData: BizVirtualRow; rowIndex: number }): string =>
rowData.direction === 'expense' ? 'demo-flow--expense' : ''
const columns: BizVirtualColumn[] = [
{ key: 'flowNo', label: '流水号', width: 150, fixed: 'left' },
{ key: 'account', label: '对方账户', width: 170 },
{ key: 'direction', label: '收支', width: 80, slot: 'direction' },
{ key: 'amount', label: '金额(分)', width: 130, align: 'right' },
{ key: 'balance', label: '余额(分)', width: 130, align: 'right' },
{ key: 'time', label: '时间', width: 130 },
]
</script>
<template>
<div>
<p style="margin: 0 0 8px; font-size: 13px; color: var(--el-text-color-secondary)">
共 {{ data.length.toLocaleString() }} 条流水;支出行淡红底(row-class),金额列插槽渲染 BizAmount
</p>
<BizVirtualTable
:columns="columns"
:data="data"
:height="380"
:row-height="40"
:row-class="rowClass"
>
<template #direction="{ row }">
<span :style="{ color: row.direction === 'expense' ? 'var(--el-color-danger)' : 'var(--el-color-success)', fontSize: '13px' }">
{{ row.direction === 'expense' ? '支出' : '收入' }}
</span>
</template>
<template #amount="{ row }">
<BizAmount :value="Number(row.amount)" symbol="¥" :precision="2" />
</template>
</BizVirtualTable>
</div>
</template>
<style>
.demo-flow--expense {
background-color: var(--el-color-danger-light-9);
}
</style>
显示代码
集团通讯录 10 万人
总经办
13810000000
离职
研发中心
13810000001
在职
制造部
13810000002
在职
供应链
13810000003
在职
财经部
13810000004
在职
总经办
13810000005
在职
研发中心
13810000006
在职
制造部
13810000007
在职
供应链
13810000008
离职
财经部
13810000009
在职
总经办
13810000010
在职
部门
手机号
状态
EMP100000
员工000001
EMP100001
员工000002
EMP100002
员工000003
EMP100003
员工000004
EMP100004
员工000005
EMP100005
员工000006
EMP100006
员工000007
EMP100007
员工000008
EMP100008
员工000009
EMP100009
员工000010
EMP100010
员工000011
工号
姓名
HR 场景:10 万人通讯录 + 外部部门筛选(computed 切数据源)、状态列 dot 圆点、工号 row-key。
<script setup lang="ts">
/** 场景:集团通讯录 10 万人——部门/状态筛选在外部,表格零感知 */
import { computed, ref } from 'vue'
import { ElSelect, ElOption, ElTag } from 'element-plus'
import { BizStatusTag, BizVirtualTable } from 'vue3-ep-biz-lib'
import type { BizVirtualColumn, BizVirtualRow } from 'vue3-ep-biz-lib'
const all: BizVirtualRow[] = Array.from({ length: 100000 }, (_unused, i) => ({
id: i + 1,
empNo: `EMP${String(100000 + i)}`,
name: `员工${String(i + 1).padStart(6, '0')}`,
dept: ['总经办', '研发中心', '制造部', '供应链', '财经部'][i % 5],
status: i % 8 === 0 ? 'leave' : 'active',
mobile: `138${String(10000000 + i).slice(0, 8)}`,
}))
const dept = ref('全部')
const data = computed(() => (dept.value === '全部' ? all : all.filter((row) => row.dept === dept.value)))
const columns: BizVirtualColumn[] = [
{ key: 'empNo', label: '工号', width: 130, fixed: 'left' },
{ key: 'name', label: '姓名', width: 130, fixed: 'left' },
{ key: 'dept', label: '部门', width: 120 },
{ key: 'mobile', label: '手机号', width: 150 },
{ key: 'status', label: '状态', width: 100, slot: 'status' },
]
</script>
<template>
<div>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 8px">
<ElSelect v-model="dept" size="small" style="width: 140px">
<ElOption label="全部" value="全部" />
<ElOption v-for="name in ['总经办', '研发中心', '制造部', '供应链', '财经部']" :key="name" :label="name" :value="name" />
</ElSelect>
<ElTag size="small" type="info">{{ data.length.toLocaleString() }} 人</ElTag>
</div>
<BizVirtualTable :columns="columns" :data="data" :height="380" :row-height="38" row-key="empNo">
<template #status="{ row }">
<BizStatusTag
:status="String(row.status)"
:status-map="{ active: { text: '在职', type: 'success' }, leave: { text: '离职', type: 'info' } }"
dot
/>
</template>
</BizVirtualTable>
</div>
</template>
显示代码
异常调用链定位
60,000 条
/api/pay/callback
ERROR
下游超时(3000ms)
5
/api/order/create
INFO
参数校验失败:amount 必填
6
/api/stock/lock
INFO
库存不足 SKU-9981
7
/api/user/login
WARN
正常
8
/api/pay/callback
INFO
下游超时(3000ms)
9
/api/order/create
INFO
参数校验失败:amount 必填
10
/api/stock/lock
WARN
库存不足 SKU-9981
11
/api/user/login
ERROR
正常
12
/api/pay/callback
INFO
下游超时(3000ms)
13
/api/order/create
WARN
参数校验失败:amount 必填
14
/api/stock/lock
INFO
库存不足 SKU-9981
15
接口
级别
异常信息
耗时(ms)
TRC-1000000
TRC-1000001
TRC-1000002
TRC-1000003
TRC-1000004
TRC-1000005
TRC-1000006
TRC-1000007
TRC-1000008
TRC-1000009
TRC-1000010
TraceID
排障场景:ERROR / WARN 级别外部筛选、error 行整行着色、row-event-handlers 点击行定位 TraceID 与异常信息。
<script setup lang="ts">
/** 场景:异常调用链——级别列筛选 + 行点击定位错误详情 */
import { computed, ref } from 'vue'
import { ElRadioButton, ElRadioGroup } from 'element-plus'
import { BizVirtualTable } from 'vue3-ep-biz-lib'
import type { BizVirtualColumn, BizVirtualRow } from 'vue3-ep-biz-lib'
const all: BizVirtualRow[] = Array.from({ length: 60000 }, (_unused, i) => ({
id: i + 1,
trace: `TRC-${String(1000000 + i)}`,
api: ['/api/pay/callback', '/api/order/create', '/api/stock/lock', '/api/user/login'][i % 4],
level: i % 7 === 0 ? 'error' : i % 3 === 0 ? 'warn' : 'info',
message: ['下游超时(3000ms)', '参数校验失败:amount 必填', '库存不足 SKU-9981', '正常'][i % 4],
cost: (i % 3200) + 5,
}))
const level = ref('all')
const data = computed(() => (level.value === 'all' ? all : all.filter((row) => row.level === level.value)))
const columns: BizVirtualColumn[] = [
{ key: 'trace', label: 'TraceID', width: 160, fixed: 'left' },
{ key: 'api', label: '接口', width: 200 },
{ key: 'level', label: '级别', width: 90, slot: 'level' },
{ key: 'message', label: '异常信息', width: 260 },
{ key: 'cost', label: '耗时(ms)', width: 110, align: 'right' },
]
const picked = ref<BizVirtualRow>()
const rowClass = ({ rowData }: { rowData: BizVirtualRow; rowIndex: number }): string =>
rowData.level === 'error' ? 'demo-trace--error' : ''
const onRowClick = ({ rowData }: { rowData: BizVirtualRow; rowIndex: number; rowKey: unknown; event: Event }): void => {
picked.value = rowData
}
const LEVEL_COLOR: Record<string, string> = {
error: 'var(--el-color-danger)',
warn: 'var(--el-color-warning)',
info: 'var(--el-text-color-secondary)',
}
</script>
<template>
<div>
<div style="display: flex; gap: 12px; align-items: center; margin-bottom: 8px">
<ElRadioGroup v-model="level" size="small">
<ElRadioButton value="all">全部</ElRadioButton>
<ElRadioButton value="error">仅 ERROR</ElRadioButton>
<ElRadioButton value="warn">仅 WARN</ElRadioButton>
</ElRadioGroup>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">{{ data.length.toLocaleString() }} 条</span>
</div>
<BizVirtualTable
:columns="columns"
:data="data"
:height="360"
:row-height="38"
:row-class="rowClass"
:row-event-handlers="{ onClick: onRowClick }"
>
<template #level="{ row }">
<span :style="{ color: LEVEL_COLOR[String(row.level)], fontWeight: 600, fontSize: '12px' }">{{ String(row.level).toUpperCase() }}</span>
</template>
</BizVirtualTable>
<p v-if="picked" style="margin: 8px 0 0; font-size: 13px">
定位:{{ picked.trace }} · {{ picked.api }} · {{ picked.message }}({{ picked.cost }}ms)
</p>
</div>
</template>
<style>
.demo-trace--error {
background-color: var(--el-color-danger-light-9);
}
</style>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| columns | BizVirtualColumn[] | —(必填) | 列 schema:{ key, label, width(必填数值), align?, fixed?, slot? } |
| data | Record<string, unknown>[] | —(必填) | 数据(可传 10 万行级) |
| height | number | 500 | 表格高度(px) |
| row-key | string | 'id' | 行 key 字段 |
| row-height | number | 44 | 行高(px) |
| header-height | number | 44 | 列头高度(px) |
其余 attrs 透传 el-table-v2(如 width、sort-state、row-class...)。
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| — | — | 事件经 attrs 透传 el-table-v2(column-sort 等);行级事件用 row-event-handlers prop(见进阶示例) |
Slots
| 插槽名 | 说明 |
|---|---|
| cell-<key> 形式(slot 字段指定的名称) | 自定义单元格(作用域:row / index / cell) |
Expose
| 属性/方法 | 说明 |
|---|---|
| mappedColumns | 当前映射后的 table-v2 Column 配置 |