Skip to content

Pxcharts Plus 技术文档

体验地址:pro版多维表格编辑器

项目概述

Pxcharts Plus 是一个基于 React 18+ 构建的现代化数据表格管理系统,提供了多种视图模式、一键生成可视化图表、自定义表单、数据导入导出等核心功能。项目采用 TypeScript 开发,使用 Zustand 进行状态管理,支持多种数据展示方式和交互操作。

核心特性

  • 🗂️ 多视图支持:表格视图、看板视图、虚拟化表格、图表仪表板
  • 📊 图表可视化:支持柱状图、折线图、饼图等多种图表类型
  • 🔄 数据操作:拖拽排序、批量操作、实时编辑、数据验证
  • 📥 数据导入导出:支持 CSV、Excel 格式
  • 🎨 自定义表格搭建能力,一键导出可二次开发的表单html代码
  • 🎨 主题定制:支持深色/浅色主题切换

技术架构

技术栈

  • 前端框架: Next.js 14 (App Router)
  • UI 库: React 18 + TypeScript
  • 状态管理: Zustand + Immer
  • 样式系统: Tailwind CSS + CSS Modules
  • 组件库: Radix UI + Lucide React Icons
  • 拖拽功能: @hello-pangea/dnd
  • 表格组件: react-window (虚拟化)
  • 图表库: Recharts
  • 表单处理: React Hook Form + Zod
  • 数据持久化: Zustand Persist

项目结构

mute-table-plus/
├── app/                    # Next.js App Router 页面
├── components/            # React 组件
│   ├── ui/               # 基础 UI 组件
│   ├── table-*.tsx       # 表格相关组件
│   ├── kanban-*.tsx      # 看板相关组件
│   └── form-generator.tsx # 表单生成器
├── lib/                   # 核心逻辑和工具
│   ├── store.ts          # Zustand 状态管理
│   ├── types.ts          # TypeScript 类型定义
│   ├── ai-service.ts     # AI 服务
│   └── utils/            # 工具函数
├── styles/                # 全局样式
└── public/                # 静态资源

架构设计原则

  1. 组件化设计: 每个功能模块独立封装,支持复用和扩展
  2. 状态集中管理: 使用 Zustand 统一管理应用状态
  3. 类型安全: 完整的 TypeScript 类型定义
  4. 性能优化: 虚拟化渲染、懒加载、React.memo 优化
  5. 可扩展性: 插件化架构,支持功能模块动态加载

核心数据模型

基础类型定义

typescript
// 字段类型
export type FieldType = "text" | "textarea" | "number" | "select" | "checkbox" | "date"

// 字段定义
export interface Field {
  id: string
  name: string
  type: FieldType
  required?: boolean
  options?: { value: string; label: string }[]
}

// 数据记录
export interface Record {
  id: string
  tableId: string
  data: { [fieldId: string]: any }
  createdAt: string
  updatedAt: string
}

// 表格定义
export interface Table {
  id: string
  name: string
  fields: Field[]
  records?: Record[]
  createdAt: string
  updatedAt: string
}

// 视图类型
export type ViewType = "table" | "kanban" | "virtual" | "lazy" | "optimized" | "chart"

状态管理结构

typescript
interface TableStore {
  // 数据状态
  tables: Table[]
  currentTableId: string | null
  currentView: ViewType
  
  // 交互状态
  searchTerm: string
  filters: Filter[]
  selectedRecords: Set<string>
  
  // 业务状态
  dashboardLayouts: DashboardLayout[]
  templates: Template[]
  
  // 操作方法
  createTable: (name: string) => void
  addRecord: (record: Record) => void
  updateRecord: (id: string, data: any) => void
  // ... 其他方法
}

表格渲染引擎

1. 虚拟化表格实现

核心原理

虚拟化表格通过 react-window 库实现,只渲染可视区域内的行,大幅提升大数据量下的渲染性能。

typescript
// 虚拟化表格核心实现
import { FixedSizeList as List } from 'react-window'

const VirtualTableView = ({ table }: { table: Table }) => {
  const records = useMemo(() => getFilteredRecords(table.id), [table.id])
  
  const Row = ({ index, style }: { index: number; style: CSSProperties }) => (
    <div style={style}>
      <TableRow 
        record={records[index]} 
        index={index} 
        table={table}
        // ... 其他属性
      />
    </div>
  )

  return (
    <List
      height={600}
      itemCount={records.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </List>
  )
}

性能优化策略

  • 行高固定: 使用 FixedSizeList 避免动态计算行高
  • 组件记忆化: 使用 React.memo 避免不必要的重渲染
  • 事件委托: 将事件处理函数提升到父组件
  • 懒加载: 按需加载数据和组件

2. 拖拽排序系统

实现架构

基于 @hello-pangea/dnd 库实现,支持表格行拖拽和看板卡片拖拽。

typescript
// 表格拖拽实现
const handleDragEnd = (result: DropResult) => {
  if (!result.destination) return
  
  const { source, destination } = result
  const sourceIndex = source.index
  const destinationIndex = destination.index
  
  // 更新记录顺序
  reorderRecords(table.id, groupBy, sourceIndex, destinationIndex)
}

// 看板拖拽实现
const handleKanbanDragEnd = (result: DropResult) => {
  const { source, destination, draggableId } = result
  
  reorderKanbanCards(
    table.id,
    groupFieldId,
    source.droppableId,
    source.index,
    destination.index
  )
}

拖拽状态管理

  • 拖拽中状态: 实时更新拖拽项的视觉反馈
  • 位置计算: 精确计算拖拽目标位置
  • 数据同步: 拖拽完成后立即更新数据状态

3. 实时编辑系统

单元格编辑

每个单元格都支持实时编辑,通过 EditableCell 组件实现:

typescript
const EditableCell = ({ field, value, onChange }: EditableCellProps) => {
  const [isEditing, setIsEditing] = useState(false)
  const [editValue, setEditValue] = useState(value)

  const handleSave = () => {
    onChange(editValue)
    setIsEditing(false)
  }

  const renderEditor = () => {
    switch (field.type) {
      case 'text':
        return <Input value={editValue} onChange={(e) => setEditValue(e.target.value)} />
      case 'select':
        return <Select value={editValue} onValueChange={setEditValue}>
          {field.options?.map(option => (
            <SelectItem key={option.value} value={option.value}>
              {option.label}
            </SelectItem>
          ))}
        </Select>
      // ... 其他字段类型
    }
  }

  return isEditing ? (
    <div className="flex gap-2">
      {renderEditor()}
      <Button size="sm" onClick={handleSave}>保存</Button>
    </div>
  ) : (
    <div onClick={() => setIsEditing(true)}>
      {renderDisplayValue()}
    </div>
  )
}

数据验证

  • 类型验证: 根据字段类型进行数据格式验证
  • 必填验证: 检查必填字段是否为空
  • 格式验证: 支持正则表达式和自定义验证规则

状态管理系统

1. Zustand Store 架构

Store 设计模式

采用单一 Store 模式,通过切片(slice)组织不同功能模块:

typescript
export const useTableStore = create<TableStore>()(
  persist(
    (set, get) => ({
      // 状态定义
      tables: [],
      currentTableId: null,
      
      // 操作方法
      createTable: (name: string) => {
        const newTable: Table = {
          id: `table_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
          name,
          fields: [/* 默认字段 */],
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString()
        }
        
        set((state) => ({
          tables: [...state.tables, newTable],
          currentTableId: newTable.id
        }))
      },
      
      // 其他方法...
    }),
    {
      name: 'table-store',
      partialize: (state) => ({
        tables: state.tables,
        templates: state.templates,
        dashboardLayouts: state.dashboardLayouts
      })
    }
  )
)

状态持久化

  • 选择性持久化: 只持久化重要数据,避免存储临时状态
  • 版本控制: 支持数据结构版本升级
  • 错误恢复: 数据损坏时自动回退到默认状态

2. 数据流管理

单向数据流

用户操作 → Action → Store 更新 → 组件重渲染 → UI 更新

异步操作处理

typescript
// 异步数据操作示例
const addRecordAsync = async (recordData: any) => {
  try {
    // 1. 乐观更新
    const tempRecord = { ...recordData, id: `temp_${Date.now()}` }
    set((state) => ({
      tables: state.tables.map(table => 
        table.id === currentTableId 
          ? { ...table, records: [...(table.records || []), tempRecord] }
          : table
      )
    }))
    
    // 2. 服务端同步
    const savedRecord = await api.createRecord(recordData)
    
    // 3. 更新真实数据
    set((state) => ({
      tables: state.tables.map(table => 
        table.id === currentTableId 
          ? { 
              ...table, 
              records: table.records?.map(r => 
                r.id === tempRecord.id ? savedRecord : r
              ) || [savedRecord]
            }
          : table
      )
    }))
  } catch (error) {
    // 4. 错误回滚
    set((state) => ({
      tables: state.tables.map(table => 
        table.id === currentTableId 
          ? { 
              ...table, 
              records: table.records?.filter(r => r.id !== tempRecord.id) || []
            }
          : table
      )
    }))
  }
}

性能优化策略

1. 渲染优化

React.memo 使用

typescript
const TableRow = React.memo(({ record, index, table, ...props }) => {
  // 组件实现
}, (prevProps, nextProps) => {
  // 自定义比较函数
  return (
    prevProps.record.id === nextProps.record.id &&
    prevProps.record.data === nextProps.record.data &&
    prevProps.index === nextProps.index
  )
})

useMemo 和 useCallback

typescript
const filteredRecords = useMemo(() => {
  return records.filter(record => {
    // 复杂的过滤逻辑
    return filters.every(filter => {
      const value = record.data[filter.fieldId]
      return matchFilterCondition(value, filter.condition, filter.value)
    })
  })
}, [records, filters])

const handleCellUpdate = useCallback((recordId: string, fieldId: string, value: any) => {
  updateRecord(recordId, { [fieldId]: value })
}, [updateRecord])

2. 内存管理

虚拟化渲染

  • 固定高度: 避免动态计算行高
  • 缓冲区: 预渲染可视区域外的少量行
  • 懒加载: 按需加载数据

事件监听器优化

typescript
useEffect(() => {
  const handleResize = debounce(() => {
    // 处理窗口大小变化
  }, 100)
  
  window.addEventListener('resize', handleResize)
  
  return () => {
    window.removeEventListener('resize', handleResize)
    handleResize.cancel() // 取消未执行的防抖函数
  }
}, [])

错误处理和边界

1. 错误边界

typescript
class TableErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  componentDidCatch(error, errorInfo) {
    console.error('Table Error:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h3>表格加载失败</h3>
          <button onClick={() => this.setState({ hasError: false })}>
            重试
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

2. 数据验证

typescript
const validateRecord = (record: Record, table: Table): ValidationResult => {
  const errors: string[] = []
  
  table.fields.forEach(field => {
    const value = record.data[field.id]
    
    // 必填验证
    if (field.required && (value === undefined || value === null || value === '')) {
      errors.push(`${field.name} 是必填字段`)
    }
    
    // 类型验证
    if (value !== undefined && value !== null) {
      switch (field.type) {
        case 'number':
          if (isNaN(Number(value))) {
            errors.push(`${field.name} 必须是数字`)
          }
          break
        case 'date':
          if (isNaN(Date.parse(value))) {
            errors.push(`${field.name} 必须是有效日期`)
          }
          break
      }
    }
  })
  
  return {
    isValid: errors.length === 0,
    errors
  }
}

二次开发指南

1. 代码结构规范

组件命名规范

typescript
// 组件文件命名:kebab-case
// 组件类名:PascalCase
export function TableManager() { ... }

// 组件目录结构
components/
├── table-manager/
│   ├── index.tsx          # 主组件
│   ├── table-manager.tsx  # 组件实现
│   ├── types.ts           # 类型定义
│   ├── hooks.ts           # 自定义 Hooks
│   └── utils.ts           # 工具函数

类型定义规范

typescript
// 在 lib/types.ts 中定义全局类型
export interface TableManagerProps {
  table: Table
  onTableUpdate?: (table: Table) => void
  className?: string
}

// 在组件文件中定义局部类型
interface LocalState {
  isEditing: boolean
  editValue: string
}

2. 状态管理规范

Store 切片模式

typescript
// 创建新的 Store 切片
interface TableManagerSlice {
  // 状态
  tableManagerState: {
    isVisible: boolean
    currentMode: 'view' | 'edit' | 'create'
  }
  
  // 操作方法
  setTableManagerVisible: (visible: boolean) => void
  setTableManagerMode: (mode: 'view' | 'edit' | 'create') => void
}

// 在 store.ts 中扩展
export const useTableStore = create<TableStore & TableManagerSlice>()(
  persist(
    (set, get) => ({
      // 原有状态...
      
      // 新增状态
      tableManagerState: {
        isVisible: false,
        currentMode: 'view'
      },
      
      // 新增方法
      setTableManagerVisible: (visible: boolean) => 
        set((state) => ({
          tableManagerState: { ...state.tableManagerState, isVisible: visible }
        })),
      
      setTableManagerMode: (mode: 'view' | 'edit' | 'create') =>
        set((state) => ({
          tableManagerState: { ...state.tableManagerState, currentMode: mode }
        }))
    })
  )
)

3. 组件开发规范

组件模板

typescript
"use client"

import React from "react"
import { useTableStore } from "@/lib/store"
import type { ComponentProps } from "./types"

interface ComponentNameProps extends ComponentProps {
  // 扩展属性
}

export function ComponentName({ 
  // 属性解构
  ...props 
}: ComponentNameProps) {
  // 状态管理
  const { /* store 状态 */ } = useTableStore()
  
  // 本地状态
  const [localState, setLocalState] = useState(initialState)
  
  // 事件处理
  const handleEvent = useCallback((event: EventType) => {
    // 事件处理逻辑
  }, [/* 依赖项 */])
  
  // 副作用
  useEffect(() => {
    // 副作用逻辑
    return () => {
      // 清理逻辑
    }
  }, [/* 依赖项 */])
  
  // 计算值
  const computedValue = useMemo(() => {
    // 计算逻辑
  }, [/* 依赖项 */])
  
  // 渲染
  return (
    <div className="component-name">
      {/* 组件内容 */}
    </div>
  )
}

功能扩展指南

1. 添加新的字段类型

步骤 1: 扩展类型定义

typescript
// 在 lib/types.ts 中
export type FieldType = 
  | "text" 
  | "textarea" 
  | "number" 
  | "select" 
  | "checkbox" 
  | "date"
  | "email"        // 新增
  | "phone"        // 新增
  | "url"          // 新增
  | "color"        // 新增

步骤 2: 创建字段组件

typescript
// components/fields/email-field.tsx
export function EmailField({ 
  value, 
  onChange, 
  required 
}: FieldComponentProps) {
  const [isValid, setIsValid] = useState(true)
  
  const validateEmail = (email: string) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return emailRegex.test(email)
  }
  
  const handleChange = (newValue: string) => {
    const valid = validateEmail(newValue)
    setIsValid(valid)
    onChange(newValue)
  }
  
  return (
    <div className="email-field">
      <Input
        type="email"
        value={value || ''}
        onChange={(e) => handleChange(e.target.value)}
        className={!isValid ? 'border-red-500' : ''}
        placeholder="请输入邮箱地址"
      />
      {!isValid && <span className="text-red-500 text-sm">邮箱格式不正确</span>}
    </div>
  )
}

步骤 3: 在 EditableCell 中集成

typescript
// components/editable-cell.tsx
const renderEditor = () => {
  switch (field.type) {
    // 原有类型...
    case 'email':
      return <EmailField value={editValue} onChange={setEditValue} required={field.required} />
    case 'phone':
      return <PhoneField value={editValue} onChange={setEditValue} required={field.required} />
    case 'url':
      return <UrlField value={editValue} onChange={setEditValue} required={field.required} />
    case 'color':
      return <ColorField value={editValue} onChange={setEditValue} required={field.required} />
  }
}

2. 添加新的视图类型

步骤 1: 扩展视图类型

typescript
// 在 lib/types.ts 中
export type ViewType = 
  | "table" 
  | "kanban" 
  | "virtual" 
  | "lazy" 
  | "optimized" 
  | "chart"
  | "timeline"     // 新增
  | "calendar"     // 新增
  | "gantt"        // 新增

步骤 2: 创建视图组件

typescript
// components/timeline-view.tsx
export function TimelineView({ table }: { table: Table }) {
  const { getFilteredRecords } = useTableStore()
  const records = getFilteredRecords(table.id)
  
  // 找到日期字段
  const dateField = table.fields.find(f => f.type === 'date')
  
  // 按日期分组记录
  const groupedRecords = useMemo(() => {
    if (!dateField) return {}
    
    return records.reduce((groups, record) => {
      const date = record.data[dateField.id]
      if (date) {
        const dateKey = new Date(date).toDateString()
        if (!groups[dateKey]) groups[dateKey] = []
        groups[dateKey].push(record)
      }
      return groups
    }, {} as Record<string, Record[]>)
  }, [records, dateField])
  
  return (
    <div className="timeline-view">
      {Object.entries(groupedRecords).map(([date, dayRecords]) => (
        <div key={date} className="timeline-day">
          <div className="timeline-date">{date}</div>
          <div className="timeline-events">
            {dayRecords.map(record => (
              <div key={record.id} className="timeline-event">
                {/* 事件内容 */}
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  )
}

步骤 3: 在视图切换中集成

typescript
// components/table-manager.tsx
const renderView = () => {
  switch (currentView) {
    // 原有视图...
    case 'timeline':
      return <TimelineView table={currentTable} />
    case 'calendar':
      return <CalendarView table={currentTable} />
    case 'gantt':
      return <GanttView table={currentTable} />
  }
}

3. 添加新的数据源

步骤 1: 创建数据源接口

typescript
// lib/data-sources/base.ts
export interface DataSource {
  id: string
  name: string
  type: 'database' | 'api' | 'file' | 'custom'
  
  // 连接方法
  connect(): Promise<boolean>
  disconnect(): Promise<void>
  
  // 数据操作方法
  query(query: string): Promise<any[]>
  insert(data: any): Promise<string>
  update(id: string, data: any): Promise<boolean>
  delete(id: string): Promise<boolean>
  
  // 元数据方法
  getTables(): Promise<string[]>
  getFields(tableName: string): Promise<Field[]>
}

// lib/data-sources/mysql.ts
export class MySQLDataSource implements DataSource {
  private connection: any
  
  async connect(): Promise<boolean> {
    try {
      // MySQL 连接逻辑
      return true
    } catch (error) {
      console.error('MySQL 连接失败:', error)
      return false
    }
  }
  
  // 实现其他方法...
}

步骤 2: 集成到 Store

typescript
// 在 store.ts 中扩展
interface TableStore {
  // 原有状态...
  dataSources: DataSource[]
  currentDataSource: DataSource | null
  
  // 新增方法
  addDataSource: (source: DataSource) => void
  setCurrentDataSource: (source: DataSource) => void
  loadDataFromSource: (tableId: string) => Promise<void>
}

4. 添加新的图表类型

步骤 1: 扩展图表配置

typescript
// 在 lib/types.ts 中
export interface ChartConfig {
  id: string
  title: string
  type: "bar" | "line" | "pie" | "area" | "scatter" | "heatmap" | "tree" | "sankey"
  fieldId: string
  aggregation: "count" | "sum" | "avg" | "min" | "max" | "median" | "mode"
  color: string
  options?: {
    // 图表特定选项
    [key: string]: any
  }
}

步骤 2: 创建图表组件

typescript
// components/charts/scatter-chart.tsx
export function ScatterChart({ config, data }: ChartProps) {
  const chartData = useMemo(() => {
    // 数据处理逻辑
    return data.map(record => ({
      x: record.data[config.fieldId],
      y: record.data[config.options?.yField],
      size: record.data[config.options?.sizeField] || 1
    }))
  }, [data, config])
  
  return (
    <ResponsiveContainer width="100%" height={300}>
      <ScatterChart data={chartData}>
        <CartesianGrid />
        <XAxis dataKey="x" />
        <YAxis dataKey="y" />
        <Scatter dataKey="y" fill={config.color} />
      </ScatterChart>
    </ResponsiveContainer>
  )
}

测试指南

1. 单元测试

typescript
// __tests__/components/table-row.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { TableRow } from '@/components/table-row'

describe('TableRow', () => {
  const mockRecord = {
    id: '1',
    data: { field_1: 'Test Value' }
  }
  
  const mockTable = {
    id: 'table_1',
    fields: [{ id: 'field_1', name: 'Test Field', type: 'text' }]
  }
  
  it('renders record data correctly', () => {
    render(<TableRow record={mockRecord} table={mockTable} />)
    expect(screen.getByText('Test Value')).toBeInTheDocument()
  })
  
  it('handles cell updates', () => {
    const mockOnUpdate = jest.fn()
    render(
      <TableRow 
        record={mockRecord} 
        table={mockTable} 
        onCellUpdate={mockOnUpdate}
      />
    )
    
    fireEvent.click(screen.getByText('Test Value'))
    // 测试编辑功能
  })
})

2. 集成测试

typescript
// __tests__/integration/table-operations.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { TableManager } from '@/components/table-manager'

describe('Table Operations Integration', () => {
  it('creates new table and adds records', async () => {
    render(<TableManager />)
    
    // 创建表格
    fireEvent.click(screen.getByText('新建表格'))
    fireEvent.change(screen.getByPlaceholderText('表格名称'), {
      target: { value: 'Test Table' }
    })
    fireEvent.click(screen.getByText('创建'))
    
    // 添加记录
    await waitFor(() => {
      expect(screen.getByText('Test Table')).toBeInTheDocument()
    })
  })
})

部署指南

1. 构建配置

bash
# 生产构建
pnpm build

# 静态导出(可选)
pnpm export

# 启动生产服务器
pnpm start

2. 环境变量配置

bash
# 生产环境变量
NODE_ENV=production
NEXT_PUBLIC_APP_URL=https://your-domain.com
NEXT_PUBLIC_AI_API_KEY=your_production_api_key

3. 性能优化

typescript
// next.config.mjs
const nextConfig = {
  // 启用压缩
  compress: true,
  
  // 图片优化
  images: {
    domains: ['your-domain.com'],
    formats: ['image/webp', 'image/avif']
  },
  
  // 实验性功能
  experimental: {
    optimizeCss: true,
    optimizePackageImports: ['@radix-ui/react-icons']
  }
}

常见问题解决

1. 性能问题

  • 表格渲染慢: 启用虚拟化渲染,使用 react-window
  • 状态更新频繁: 使用 useMemouseCallback 优化
  • 内存泄漏: 检查 useEffect 的清理函数

2. 类型错误

  • 类型不匹配: 检查 lib/types.ts 中的类型定义
  • 泛型约束: 确保泛型参数满足约束条件
  • 联合类型: 使用类型守卫确保类型安全

3. 样式问题

  • Tailwind 类不生效: 检查 tailwind.config.ts 配置
  • 响应式问题: 使用正确的断点前缀
  • 主题切换: 检查 theme-provider.tsx 配置

让技术更平权,致力于高性价办公协同解决方案