Appearance
Vue 流式表格
vtAIStream:一边收数据一边渲染,列自己长出来。表格初始没有任何列配置(columns: []),首批数据到达时由 inferColumns() 推断出列——类型、标题、宽度、对齐都是算出来的。
接收中往上滚会脱离底部跟随,滚回底部自动恢复。
使用的 API
ts
import { VirtTableVue, vtAIStream, type AIStreamState } from '@virt-table/vue';
const options = {
list: [],
columns: [], // 故意留空 —— 列由 vtAIStream 推断
itemKey: 'id',
estimatedSize: 40,
plugins: [
vtAIStream({
inferOptions: { minWidth: 96 },
onStateChange(state: AIStreamState) {
// state.status / received / buffered / following
},
}),
],
};
// 实例方法(ref 上调用)
tableRef.value?.startAIStream();
tableRef.value?.pushAIStreamRows(rows); // 攒到下一帧统一落地
tableRef.value?.endAIStream();
// 或者直接吃一个 AsyncIterable(SSE / ReadableStream / 异步生成器)
tableRef.value?.consumeAIStream(rowsFromSSE('/api/rows'));完整语义(帧调度与 maxRowsPerFlush、列推断规则、跟随态、中断与错误)见 Vanilla · 流式表格。
示例
微应用尚未挂载。
源码
点击查看源码
vue
<template>
<div class="demo-wrapper">
<h3 class="demo-title">Vue 流式表格</h3>
<div class="virt-table-controls">
<button type="button" class="virt-table-btn virt-table-btn-primary" @click="start">
开始接收
</button>
<button type="button" class="virt-table-btn" @click="stop">停止</button>
<button type="button" class="virt-table-btn" @click="burst">灌 2 万行</button>
<button type="button" class="virt-table-btn" @click="reset">清空重来</button>
<span class="demo-note" style="font-variant-numeric: tabular-nums">{{ stat }}</span>
</div>
<div class="demo-hint">
表格初始<strong>没有列配置</strong>,列是首批数据到达时推断出来的(金额右对齐 + 千分位、结清居中、
订单号虽是数字串但不右对齐)。接收中往上滚会脱离底部跟随,滚回底部自动恢复。
</div>
<div style="width: 100%; height: 520px" class="demo-container">
<VirtTableVue ref="tableRef" :columns="columns" :options="options" />
</div>
</div>
</template>
<script setup lang="ts">
import { onUnmounted, ref } from 'vue';
import { VirtTableVue, type VueTableColumn, type AIStreamState, vtAIStream } from '@virt-table/vue';
import type { VirtTableVueInstance } from '../../virt-table-ref';
/**
* vtAIStream:一边收数据一边渲染,列自己长出来。
*
* 这个 demo 模拟的是「agent 跑完一段任务,把结果一行一行吐回来」——
* 表格初始**没有任何列配置**(`columns: []`),首批数据到达时由
* `inferColumns()` 推断出列(类型、标题、宽度、对齐都是算出来的)。
*
* 换成真实数据源只需替换这个生成器(吃任何 `AsyncIterable`,SSE /
* `ReadableStream` / 异步生成器都行),再交给 `consumeAIStream()`。
*/
const FIRST_NAMES = ['林', '陈', '王', '李', '张', '刘', '黄', '周', '吴', '徐'];
const GIVEN = ['明', '静', '磊', '娜', '强', '敏', '杰', '芳', '涛', '燕'];
const CITIES = ['上海', '北京', '深圳', '杭州', '成都', '广州', '武汉', '西安'];
const CHANNELS = ['官网', '小程序', '门店', '电话', '第三方平台'];
const STATUS = ['已完成', '处理中', '待审核', '已取消'];
/** 一条「agent 抓回来的」记录。注意 key 是 camelCase,标题由 humanizeKey 推出来 */
function makeRow(i: number) {
const rnd = (n: number) => Math.floor(Math.random() * n);
return {
id: i,
orderNo: `SO${String(202600000 + i)}`, // 纯数字串:不该被当成数字右对齐
customerName: FIRST_NAMES[rnd(FIRST_NAMES.length)]! + GIVEN[rnd(GIVEN.length)]!,
city: CITIES[rnd(CITIES.length)]!,
channel: CHANNELS[rnd(CHANNELS.length)]!,
amount: rnd(980000) + 2000, // 数字:应当右对齐 + 千分位
settled: Math.random() > 0.4, // 布尔:应当居中
createdAt: new Date(Date.now() - rnd(86400000 * 90)).toISOString().slice(0, 10), // 日期
status: STATUS[rnd(STATUS.length)]!,
};
}
// 故意留空 —— 列由 vtAIStream 推断
const columns: VueTableColumn[] = [];
const tableRef = ref<VirtTableVueInstance | null>(null);
const stat = ref('');
const options = {
list: [] as Record<string, unknown>[],
itemKey: 'id',
estimatedSize: 40,
buffer: 6,
border: true,
emptyText: '等待数据…点上方「开始接收」',
plugins: [
vtAIStream({
inferOptions: { minWidth: 96 },
onStateChange(state: AIStreamState) {
stat.value =
state.status === 'idle'
? ''
: `${state.status} · 已落地 ${state.received.toLocaleString()} 行` +
(state.buffered > 0 ? ` · 缓冲 ${state.buffered}` : '') +
(state.following ? '' : ' · 已脱离跟随');
},
}),
],
};
let seq = 0;
let stopped = true;
let timer: ReturnType<typeof setTimeout> | null = null;
/** 模拟「模型逐条吐结果」:每 30~90ms 来 1~6 行,节奏不均匀 */
const start = () => {
if (!stopped && timer) return;
stopped = false;
tableRef.value?.startAIStream();
const step = () => {
if (stopped) return;
const n = 1 + Math.floor(Math.random() * 6);
tableRef.value?.pushAIStreamRows(Array.from({ length: n }, () => makeRow(seq++)));
timer = setTimeout(step, 30 + Math.random() * 60);
};
step();
};
const stop = () => {
stopped = true;
if (timer) clearTimeout(timer);
timer = null;
tableRef.value?.endAIStream();
};
// 一次灌很多:验证批量落地不会把一帧撑爆(maxRowsPerFlush 会分批)
const burst = () => {
tableRef.value?.pushAIStreamRows(Array.from({ length: 20000 }, () => makeRow(seq++)));
};
const reset = () => {
stop();
seq = 0;
tableRef.value?.setList([]);
tableRef.value?.setColumns([]);
tableRef.value?.startAIStream();
stat.value = '';
};
onUnmounted(() => {
stopped = true;
if (timer) clearTimeout(timer);
});
</script>