VTable: 不只是高性能的多維數據分析表格,更是行列間創作的方格藝術家!
在現代應用程序中,表格組件是不可或缺的一部分,它們能夠快速展示大量數據,並提供良好的可視化效果和互動體驗。VTable是一款基於可視化渲染引擎VRender的高性能表格組件庫,為用戶提供卓越的性能和強大的多維分析能力,以及靈活強大的圖形能力。
官網連結: visactor.com/vtable
github地址: github.com/VisActor/VTable
準備分享幾個簡單的實踐案例,我目前主要使用的是基礎表格ListTable在react項目中,所以案例基本也都是ListTable的,後續在項目中使用到了其他表格的過程中遇到值得分享的內容也會繼續分享的。
全部代碼示例: github.com/LLmoskk/vtable-demo
在線預覽: llmoskk.github.io/vtable-demo
先起一個react項目,經典vite起手,注: react-vtable React 19 還不被支持 我們需要使用18版本的react。
我目前使用的AI IDE是 AWS 的 krio,我們先添加context7 mcp,這個mcp的作用是讓ai獲取最新的文檔,避免使用過時的api。現在主流的IDE基本都支持添加MCP了,不在此一一舉例。
{
"mcpServers": {
"context7": {
"args": [
"-y",
"@upstash/context7-mcp@latest"
],
"command": "npx",
"disabled": false,
"autoApprove": [
"resolve-library-id"
]
},
}
}
可以看到已經成功調用mcp工具去獲取文檔信息了。我要求他實現一個樹形表格。
很快啊! ai就寫完了。效果還不錯,這裡就不貼代碼了,沒有太多參考價值,只是用來演示一下使用ai + mcp結合快速的實現vtable的功能,無需自己去翻閱文檔了。
項目場景概述:
在處理數量較大的表格時,若將所有列的寬度固定為120px,可能會導致某些數據較少的列佔據過多的空間。因此,提出了一個解決方案:動態調整列寬,根據數據值自動測量並設置每一列的最終寬度。
將編寫一個名為 use-column-width.ts 的文件,封裝一個自定義Hook。該Hook的功能是按照上述邏輯計算列寬,並與用戶拖拽的寬度進行記憶。我們將利用ahook中的 useLocalStorageState 來保持狀態信息同步存儲到 localStorage 中。
測量文本寬度 ~~參考文章~~ juejin.cn/post/7091990279565082655~~~~ 完整代碼不在這裡貼出了可訪問倉庫自取 github.com/LLmoskk/vtable-demo/blob/main/src/pages/demo1/utils/calculate-column-width.ts
(不需要自己寫了,可以用vtable寫好的measureText 😅)
如果沒有定制要求的話可以直接使用表格的 自動列寬模式(autoWidth) 已經幫忙計算過一遍了。
vtable文字測量方法: github.com/VisActor/VUtil/blob/main/packages/vutils/src/graphics/text/measure/textMeasure.ts
npm 地址: www.npmjs.com/package/@visactor/vutils
import { useLocalStorageState } from 'ahooks';
import { useCallback, useMemo } from 'react';
import { type ColumnDefine } from '@visactor/vtable';
import { calculateColumnsWidthMap } from '../utils/calculate-column-width';
import type { Sort } from '../type';
type UseColumnWidthParams<T extends ColumnDefine> = {
/** 列配置數組 */
columns?: T[];
/** localStorage的key */
storageKey: string;
/** 表格數據,用於計算列寬 */
data?: any[];
/** 默認列寬 */
defaultWidth?: number;
/** 视图ID */
viewId?: string | number;
/** 是否已排序的信息 */
sorts?: Sort[];
};
type UseColumnWidthReturn<T extends ColumnDefine> = {
/** 應用了列寬的列配置(優先級:本地存儲 > 計算值 > 默認值) */
columnsWithWidth: T[];
/** 保存列寬的函數 */
saveColumnWidths: (colWidths: number[]) => void;
/** 列寬映射對象 */
columnWidths: Record<string, number>;
};
/**
* 管理表格列寬持久化的hook
* 當columns的field順序發生變化時,會自動清理不存在的列寬設置
*
* 列寬優先級:本地存儲 > 計算值 > 默認值
*/
const useColumnWidth = <T extends ColumnDefine>({
columns,
storageKey,
data = [],
defaultWidth = 120,
sorts,
}: UseColumnWidthParams<T>): UseColumnWidthReturn<T> => {
const [storedValue, setStoredValue] = useLocalStorageState<any>(storageKey, {
defaultValue: {},
});
const columnWidths = storedValue;
const calculatedWidthMap = useMemo(
() =>
calculateColumnsWidthMap(
columns,
data,
sorts,
),
[columns, data, sorts],
);
// 保存列寬到 localStorage
const saveColumnWidths = useCallback(
(colWidths: number[]) => {
const widthMap: Record<string, number> = {};
columns?.forEach((col, index) => {
if (colWidths[index]) {
widthMap[String(col.field)] = colWidths[index];
}
});
setStoredValue(widthMap);
},
[columns, setStoredValue],
);
// 應用列寬到列配置
// 優先級:本地存儲 > 計算值 > 默認值
const columnsWithWidth = useMemo(() => {
return columns?.map((col) => {
// checkbox 固定 40 cellType 可能為函數
if (col.cellType === 'checkbox' || col.headerType === 'checkbox') {
return {
...col,
width: 40,
};
}
// 優先使用本地存儲的寬度
if (columnWidths?.[String(col.field)]) {
return {
...col,
width: columnWidths[String(col.field)],
};
}
// 其次使用計算的寬度
const calculatedWidth = calculatedWidthMap.get(String(col.field));
if (calculatedWidth) {
return {
...col,
width: calculatedWidth,
};
}
// 最後使用默認寬度
return {
...col,
width: defaultWidth,
};
});
}, [columns, columnWidths, calculatedWidthMap, defaultWidth]);
return {
columnsWithWidth: columnsWithWidth || [],
saveColumnWidths,
columnWidths: columnWidths || {},
};
};
export default useColumnWidth;
vtable提供了Arco design的主題與 暗色模式的主題,我的項目是由Arco design的table切換到vtable的,所以使用自官方提供的主題包,並且我的項目需要適配暗色模式,於是也用到了 暗色模式 來定制修改了下,基本配置信息都可以在文檔中查看 visactor.com/vtable/option/ListTable#theme
內置主題有五個
const builtinThemes = [
{ key: 'DEFAULT', name: '默認主題', theme: themes.DEFAULT },
{ key: 'DARK', name: '暗色主題', theme: themes.DARK },
{ key: 'BRIGHT', name: '明亮主題', theme: themes.BRIGHT },
{ key: 'ARCO', name: 'Arco主題', theme: themes.ARCO },
{ key: 'SIMPLIFY', name: '簡約主題', theme: themes.SIMPLIFY }
];
我們還可以extends主題進行擴展定制,我們系統中是由Arco design的Table組件遷移至vtable的,切換到ARCO主題後,發現還是和組件庫的表格有些不同的,因此我做了些樣式覆寫。
import { themes } from "@visactor/vtable";
/**
* 獲取通用的 VTable 主題配置
*/
export const getCommonVTableTheme = () => {
return themes.ARCO.extends({
frameStyle: {
borderLineWidth: 0,
},
headerStyle: {
bgColor: '#F0F1F5',
fontSize: 12,
fontWeight: 400,
autoWrapText: true,
lineClamp: 3,
},
bodyStyle: {
// 使用函數動態設置背景色,如果是 aggregation 行則使用 headerStyle 的樣式
bgColor: (args: any) => {
// 檢查是否是 aggregation 行
if (args.table && typeof args.table.isAggregation === 'function') {
const isAggregationCell = args.table.isAggregation(
args.col,
args.row,
);
if (isAggregationCell) {
return '#F0F1F5';
}
}
return '#FFFFFF';
},
},
bottomFrozenStyle: {
bgColor: '#F0F1F5',
fontWeight: 600,
fontSize: 14,
},
tooltipStyle: {
bgColor: 'black',
color: 'white',
fontSize: 12,
padding: [8, 12, 8, 12],
},
scrollStyle: {
visible: 'always', // 滾動條始終顯示
hoverOn: false, // 滾動條不懸浮在內容上,而是獨立顯示
},
selectionStyle: {
cellBgColor: 'rgba(133,165,242,0.2)',
},
});
};
對單元格的條件定制,遍歷列配置後可以單獨根據條件設置單元格樣式
const salesColumn = baseColumns.find(col => col.field === 'sales');
if (salesColumn) {
(salesColumn as any).style = {
color: (args: any) => {
const value = args.dataValue;
if (value >= 150000) return '#059669'; // 綠色:高銷售額
if (value >= 100000) return '#0891b2'; // 藍色:中等銷售額
return '#dc2626'; // 紅色:低銷售額
},
fontWeight: (args: any) => {
return args.dataValue >= 150000 ? 'bold' : 'normal';
}
};
}
凍結列功能配置: visactor.com/vtable/guide/basic_function/frozen_column_row
在後台系統看表的時候一般需要固定左側的幾列。支持頂部、底部、左側、右側的凍結設置。
我在項目中遇到的問題就是切換主題到暗色的時候,表格固定按鈕的圖標沒有跟隨主題變化,通過查詢文檔得知可以註冊 icon 自定義圖標的一些配置。然後我從源碼中獲取到原始的 svg 傳入跟隨主題變化的 frozen_color
註冊icon: visactor.com/vtable/guide/custom_define/custom_icon
export const registerVtableIcon = (isDark?: boolean) => {
const frozen_size = 22;
const frozen_size_2 = 22;
const frozen_color = isDark ? '#FFFFFF' : '#282F38';
const frozen_color_opacity = '0.35';
const freeze_color_opacity = '0.2';
register.icon('frozen', {
type: 'svg',
svg:
'<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">' +
`<path d="M8.49975 3.66663C8.32294 3.66663 8.15337 3.73686 8.02835 3.86189C7.90332 3.98691 7.83309 4.15648 7.83309 4.33329V9.63246C6.76475 10.2533 6.07942 11.1795 6.00625 12.2308C5.99892 12.2786 5.99692 12.3268 6.00009 12.3741L5.99975 12.4166C5.99975 12.5934 6.06999 12.763 6.19501 12.888C6.32004 13.0131 6.48961 13.0833 6.66642 13.0833H10.3333L10.3331 17.5L10.8611 18.292C10.8763 18.3148 10.8969 18.3335 10.9211 18.3464C10.9453 18.3594 10.9723 18.3662 10.9998 18.3662C11.0272 18.3662 11.0542 18.3594 11.0784 18.3464C11.1026 18.3335 11.1232 18.3148 11.1384 18.292L11.6664 17.5L11.6666 13.0833H15.3331C15.5099 13.0833 15.6795 13.0131 15.8045 12.888C15.9295 12.763 15.9998 12.5934 15.9998 12.4166C15.9998 12.4025 15.9998 12.3883 15.9994 12.3741C16.0028 12.3263 16.0008 12.2776 15.9933 12.2295C15.9196 11.1786 15.2343 10.2528 14.1664 9.63229V4.33329C14.1664 4.15648 14.0962 3.98691 13.9712 3.86189C13.8461 3.73686 13.6766 3.66663 13.4998 3.66663H8.49975Z" fill="${frozen_color}" fill-opacity="${frozen_color_opacity}"/>` +
'</svg>',
width: frozen_size,
height: frozen_size,
name: 'frozen',
funcType: IconFuncTypeEnum.frozen,
positionType: IconPosition.right,
marginRight: 0,
hover: {
width: frozen_size_2,
height: frozen_size_2,
bgColor: 'rgba(101, 117, 168, 0.1)',
},
cursor: 'pointer',
});
register.icon('freeze', {
type: 'svg',
svg:
'<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">' +
'<g clip-path="url(#clip0)">' +
`<path d="M17.1313 8.42047C17.1932 8.48238 17.2423 8.55587 17.2759 8.63676C17.3094 8.71764 17.3266 8.80434 17.3266 8.89189C17.3266 8.97944 17.3094 9.06613 17.2759 9.14702C17.2423 9.2279 17.1932 9.3014 17.1313 9.3633L13.3843 13.1103C13.7007 14.3048 13.5305 15.4443 12.8388 16.2395C12.8104 16.2781 12.7778 16.3136 12.7417 16.3451L12.712 16.3755C12.6501 16.4374 12.5766 16.4865 12.4957 16.52C12.4148 16.5535 12.3281 16.5707 12.2406 16.5707C12.153 16.5707 12.0663 16.5535 11.9854 16.52C11.9046 16.4865 11.8311 16.4374 11.7692 16.3755L9.17633 13.7826L6.05316 16.9058L5.11983 17.0925C5.09291 17.0979 5.06508 17.0965 5.03881 17.0886C5.01254 17.0806 4.98863 17.0663 4.96923 17.0469C4.94982 17.0275 4.9355 17.0036 4.92755 16.9773C4.9196 16.951 4.91827 16.9232 4.92366 16.8963L5.11033 15.963L8.23333 12.8396L5.64066 10.2471C5.57875 10.1852 5.52964 10.1117 5.49614 10.0309C5.46263 9.94997 5.44539 9.86327 5.44539 9.77572C5.44539 9.68817 5.46263 9.60148 5.49614 9.52059C5.52964 9.43971 5.57875 9.36621 5.64066 9.3043C5.65066 9.2943 5.66066 9.2843 5.67099 9.27464C5.70266 9.2383 5.73833 9.20547 5.77766 9.17664C6.57283 8.48564 7.71199 8.31564 8.90599 8.63197L12.6528 4.88497C12.7147 4.82306 12.7882 4.77395 12.8691 4.74045C12.95 4.70694 13.0367 4.6897 13.1242 4.6897C13.2118 4.6897 13.2985 4.70694 13.3794 4.74045C13.4603 4.77395 13.5338 4.82306 13.5957 4.88497L17.1312 8.42047H17.1313ZM15.7172 8.8918L13.1243 6.29914L9.56483 9.8588C9.47574 9.94788 9.36323 10.0099 9.24034 10.0376C9.11746 10.0654 8.98922 10.0578 8.87049 10.0156C8.22783 9.78764 7.63899 9.7553 7.17749 9.89814L12.1182 14.8388C12.261 14.3771 12.2287 13.7885 12.0007 13.146C11.9585 13.0272 11.9509 12.899 11.9787 12.7761C12.0064 12.6532 12.0684 12.5407 12.1575 12.4516L15.7172 8.89164V8.8918Z" fill="${frozen_color}" fill-opacity="${freeze_color_opacity}"/>` +
'</g>' +
'<defs>' +
'<clipPath id="clip0">' +
'<rect width="22" height="22" fill="white"/>' +
'</clipPath>' +
'</defs>' +
'</svg>',
width: frozen_size,
height: frozen_size,
name: 'freeze',
funcType: IconFuncTypeEnum.frozen,
positionType: IconPosition.right,
marginRight: 0,
hover: {
width: frozen_size_2,
height: frozen_size_2,
bgColor: 'rgba(101, 117, 168, 0.1)',
},
cursor: 'pointer',
});
};
表格轉置設置下transpose即可行轉列,轉置表格特別適合數據列很多但行數較少的場景,雖然目前我的項目中還未使用到。
行高列寬文檔: visactor.com/vtable/guide/basic_function/row_height_column_width
widthMode有三種模式 'standard' | 'adaptive' | 'autoWidth'
如果沒有業務特別定制的列寬要求的話,設置 自動列寬模式(autoWidth)最佳,可以根據列頭和 body 單元格中的內容自動計算列寬度,忽略設置的 width 屬性和 defaultColWidth。但計算會浪費一些性能,就看自己的取捨了。
還有值得注意的一點是最好設置表格 maxWidth+minWidth因為我們允許用戶自由拖拽列寬了,但又不希望無限制,所以一般我都會加上邊界限制。
傳統的dom表格可以設置表頭黏性定位,但我使用vtable的時候想要實現這個功能,只能讓整體vtable的dom黏性了,效果不是很好,最後想到的辦法是讓vtable的高度盡量保持一屏。就可以實現類似表頭黏性定位的交互了。
const [tableHeight, setTableHeight] = useState(500); // 默認撐開的一個高度
useEffect(() => {
const calculateHeight = () => {
// 計算表格高度:視口高度 - 頂部導航欄(60) - 底部間距(16) - Tabs高度(36) - 分頁器高度(32)
// 目標是讓表格高度佔滿一屏,這樣當滾動到底部時,表格正好鋪滿屏幕
const height = window.innerHeight - 60 - 16 - 36 - 32 - 80;
setTableHeight(Math.max(height, 400)); // 設置最小高度
};
calculateHeight();
window.addEventListener('resize', calculateHeight);
return () => {
window.removeEventListener('resize', calculateHeight);
};
}, []);
<ListTable
// ...
height={tableHeight}
/>
vtable支持添加 keyboardOptions copySelected 即可開啟表格ctrl + c 複製到能力。
keyboardOptions={{
copySelected: true,
}}
但是項目中的表格有些單元格我使用了 customRender 自定義渲染的能力,我將一個 json的數據轉換為icon + 文本的呈現形式,這時候複製到話就會出問題。因此需要formatCopyValue visactor.com/vtable/option/ListTable#formatCopyValue((value%3A%2520string)%2520%3D%253E%2520string))
{
"icon": "xxxx",
"text": "Demo"
}
export const formatCopyValueForVTable = (value: unknown): string => {
const toStr = (v: unknown) => (v == null ? '' : String(v));
if (typeof value !== 'string') return toStr(value);
const parseCell = (cell: string) => {
const trimmed = cell.trim();
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
try {
const obj = JSON.parse(trimmed);
if (obj && typeof obj === 'object') {
// 優先檢查 describe 字段(asset 對象)
if ('describe' in obj) {
return toStr((obj as any).describe ?? '');
}
// 其次檢查 text 字段(icon-text 對象)
if ('text' in obj) {
return toStr((obj as any).text ?? '');
}
}
} catch (_) {
// 非合法 JSON,保持原樣
}
}
return cell;
};
return value
.split('\n')
.map((line) => line.split('\t').map(parseCell).join('\t'))
.join('\n');
};
這樣複製出來的內容就正常了。
VTable 提供了高性能的表格使用體驗,幫助開發者簡化操作。其開放的可配置 API 選項豐富,基本滿足各種業務開發需求。此外,Vtable 在響應問題和產品迭代方面也展現出快速高效的特點,為用戶提供了優質的支持和服務。
感恩 Vtable 開源項目為我們帶來了如此好用的工具,它的高性能和靈活配置極大地方便了我們的開發工作。開源的精神讓更多的開發者受益,希望能夠有更多此類優秀的開源項目繼續湧現!開源萬歲!

