上个月在 趣谈AI 发布了我实现的多维表格1.0版本,没有用到任何第三方组件,完全组件化设计。最近对多维表格进行了进一步的升级优化,满打满算花了接近3个月时间,累计代码接近1w行。

接下来就和大家聊聊我做的 flowmix/mute多维表格 的核心功能和技术实现。
核心功能介绍
1. 多视图模式

目前多维表格支持多种视图模式:表格视图,看板视图,人员分配视图。用户可以轻松在不同视图下切换并进行可视化操作数据。
2. 多条件筛选功能

我们可以基于不同维度进行筛选和排序,并支持组合筛选。
3. 多维度分组功能

表格视图中,我们可以基于用户,优先级,状态,对数据进行分组管理,提高表格数据的查看效率。
4. 表格字段管理功能

多维表格中不仅支持字段的管理控制,同时还支持添加自定义字段:

5. 表格行列支持自定义拖拽排序功能

表格我们不仅仅支持列的宽度拖拽,还支持拖拽调整列的排序,同时表格的行也支持拖拽,可以跨分组进行拖拽,也支持在组内进行拖拽排序,极大的提高了数据管理的效率。
6. 表格支持一键编辑

我们可以在菜单按钮中开启编辑模式,也可以双击编辑单元格一键编辑表格内容,同时大家还可以进行扩展。
7. 表格支持一键转换为可视化分析视图表

我们可以将表格数据转换为可视化分析图表,帮助管理者更好地掌握数据动向。
8. 表格支持一键导入任务数据

目前多维表格支持导出和导入json数据,并一键渲染为多维表格。技术实现多维表格的设计我采用了组件化的实现的方式, 并支持数据持久化,具体使用如下:
html
<div className="flex-1 bg-gray-50">
{currentView === "tasks" && <TaskManagementTable sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />}
{currentView === "statistics" && <StatisticsView />}
{currentView === "documentation" && <DocumentationView />}
{currentView === "assignment" && <AssignmentView />}
{currentView === "deployment" && <DeploymentView />}
</div>
在开发多维表格的过程中其实需要考虑很多复杂逻辑,比如表格用什么方式渲染,如何优化表格性能,如何实现表格的列排序,行排序,表格编辑等。传统表格组件大多基于div
模拟行列,虽然灵活但渲染性能差。所以可以做如下优化:
- 虚拟滚动当数据量超过 500 行时,启用虚拟滚动机制,仅渲染可见区域的 DOM 节点,内存占用降低 70%;
- 行列冻结 通过固定定位
position: sticky
实现表头和固定列冻结,解决大数据表格的滚动迷失问题; - 异步加载 采用
Intersection Observer
监听表格滚动事件,动态加载可视区域外的数据,避免一次性请求全量数据。
接下来分享一下简版的虚拟滚动的实现方案:
js
// 虚拟滚动核心代码(简化版)
function renderVirtualTable(data, visibleHeight) {
const totalRows = data.length;
const rowHeight = 40; // 行高固定
const visibleRows = Math.ceil(visibleHeight / rowHeight);
const startIndex = scrollTop / rowHeight | 0;
const endIndex = startIndex + visibleRows;
// 渲染可见区域数据
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const row = document.createElement('tr');
row.innerHTML = data[i].cells.map(cell => `<td>${cell.value}</td>`).join('');
fragment.appendChild(row);
}
// 更新滚动条高度和偏移量
table.scrollHeight = totalRows * rowHeight;
table.innerHTML = `<thead>${header}</thead><tbody>${fragment}</tbody>`;
}
对于大表格数据量需要在本地缓存,所以需要设计表格数据的缓存处理逻辑,目前我采用的是hooks的实现方案,具体实现如下:
js
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 初始化状态
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// 获取本地存储中的值
if (typeof window === "undefined") {
return initialValue
}
const item = window.localStorage.getItem(key)
// 解析存储的JSON或返回初始值
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 如果出错,返回初始值
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
// 返回一个包装版本的 useState setter 函数
// 将新值同步到 localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// 允许值是一个函数
const valueToStore = value instanceof Function ? value(storedValue) : value
// 保存到 state
setStoredValue(valueToStore)
// 保存到 localStorage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage item "${key}":`, error)
}
}
}
// 添加事件监听器
if (typeof window !== "undefined") {
window.addEventListener("storage", handleStorageChange)
}
// 清理事件监听器
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("storage", handleStorageChange)
}
}
}, [key])
return [storedValue, setValue]
}
其实在实现多维表格的过程中,我也调研了很多开源的方案,但是对于扩展性,灵活度和功能复杂度上,都略显简单,所以我才考虑花时间来实现这款多维表格方案。另一个比较复杂的逻辑是表格的列拖拽和排序,我们需要对可展开折叠的表格支持排序和拖拽,并保持优秀的用户体验:

技术实现如下:
js
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 初始化状态
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// 获取本地存储中的值
if (typeof window === "undefined") {
return initialValue
}
const item = window.localStorage.getItem(key)
// 解析存储的JSON或返回初始值
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 如果出错,返回初始值
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
// 返回一个包装版本的 useState setter 函数
// 将新值同步到 localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// 允许值是一个函数
const valueToStore = value instanceof Function ? value(storedValue) : value
// 保存到 state
setStoredValue(valueToStore)
// 保存到 localStorage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage item "${key}":`, error)
}
}
}
// 添加事件监听器
if (typeof window !== "undefined") {
window.addEventListener("storage", handleStorageChange)
}
// 清理事件监听器
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("storage", handleStorageChange)
}
}
}, [key])
return [storedValue, setValue]
}
多维表格还支持多种视图的转换,比如可以将表格视图一键转换为可视化分析图表:

对用户和团队进行多维度的数据分析。技术实现如下:
js
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from "recharts"
import type { Task } from "@/lib/types"
interface PriorityDistributionChartProps {
tasks: Task[]
}
export function PriorityDistributionChart({ tasks }: PriorityDistributionChartProps) {
// 计算每个优先级的任务数量
const priorityCounts: Record<string, number> = {}
tasks.forEach((task) => {
const priority = task.priority || "未设置"
priorityCounts[priority] = (priorityCounts[priority] || 0) + 1
})
// 转换为图表数据格式
const chartData = Object.entries(priorityCounts).map(([priority, count]) => ({
priority,
count,
}))
// 为不同优先级设置不同颜色
const COLORS = ["#FF8042", "#FFBB28", "#00C49F", "#0088FE"]
return (
<div className="w-full h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={true}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="priority"
label={({ priority, percent }) => `${priority}: ${(percent * 100).toFixed(0)}%`}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value, name, props) => [`${value} 个任务`, props.payload.priority]} />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
)
}
项目的体验地址:mute.turntip.cn
如果大家有好的想法,欢迎评论区留言反馈~