467 lines
22 KiB
Markdown
467 lines
22 KiB
Markdown
# DataViz Pro — 前端开发指南
|
||
|
||
## 一、Clean Architecture 分层总览
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Frameworks & Drivers(最外层) │
|
||
│ Next.js · React · ECharts · SheetJS · jsPDF · AG Grid │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ Interface Adapters(适配层) │
|
||
│ Redux Toolkit Slices · Zustand Stores · Presenters · Gateways │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ Application(用例层) │
|
||
│ Use Cases · Input/Output Ports(接口定义) │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ Domain(领域层,最内层) │
|
||
│ Entities · Value Objects · Domain Services · Business Rules │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
依赖方向:外层 → 内层(绝对不可反向)
|
||
```
|
||
|
||
## 二、目录结构
|
||
|
||
```
|
||
src/
|
||
├── domain/ # 领域层(零依赖,纯 TypeScript)
|
||
│ ├── entities/
|
||
│ │ ├── DataSet.ts # 数据集实体
|
||
│ │ ├── Column.ts # 列实体(含类型推断规则)
|
||
│ │ ├── ChartInstance.ts # 图表实例实体
|
||
│ │ ├── FieldBinding.ts # 字段绑定实体
|
||
│ │ ├── StyleConfig.ts # 样式配置实体
|
||
│ │ ├── LayoutItem.ts # 布局项实体
|
||
│ │ └── Template.ts # 模板实体
|
||
│ │
|
||
│ ├── valueObjects/
|
||
│ │ ├── FieldType.ts # 字段类型(number|text|date|percentage|geo)
|
||
│ │ ├── ChartType.ts # 图表类型枚举 + 元数据
|
||
│ │ ├── Aggregation.ts # 聚合方式
|
||
│ │ ├── SortOrder.ts # 排序方向
|
||
│ │ └── ExportFormat.ts # 导出格式
|
||
│ │
|
||
│ ├── services/ # 领域服务(纯业务规则)
|
||
│ │ ├── FieldTypeInferenceService.ts # 字段类型推断规则
|
||
│ │ ├── DataStructureInferenceService.ts # 数据结构推断规则
|
||
│ │ ├── ChartRecommendationService.ts# 图表推荐规则
|
||
│ │ ├── DataTransformService.ts # 聚合/排序/TopN 规则
|
||
│ │ └── ValidationService.ts # 数据绑定合法性校验
|
||
│ │
|
||
│ └── rules/ # 业务规则常量
|
||
│ ├── chartBindingRules.ts # 每种图表需要什么字段绑定
|
||
│ └── chartCompatibility.ts # 字段类型与图表的兼容矩阵
|
||
│
|
||
├── application/ # 用例层(依赖 domain,不依赖外层)
|
||
│ ├── ports/ # 端口定义(接口)
|
||
│ │ ├── input/ # 输入端口(Use Case 接口)
|
||
│ │ │ ├── IImportDataUseCase.ts
|
||
│ │ │ ├── ICreateChartUseCase.ts
|
||
│ │ │ ├── IUpdateChartConfigUseCase.ts
|
||
│ │ │ ├── IExportUseCase.ts
|
||
│ │ │ ├── ITemplateUseCase.ts
|
||
│ │ │ └── ILayoutUseCase.ts
|
||
│ │ │
|
||
│ │ └── output/ # 输出端口(外部依赖的接口)
|
||
│ │ ├── IFileParser.ts # 文件解析能力
|
||
│ │ ├── IChartRenderer.ts # 图表渲染能力(生成 option)
|
||
│ │ ├── IExportGateway.ts # 导出能力
|
||
│ │ ├── IStorageGateway.ts # 持久化能力
|
||
│ │ └── IGeoDataProvider.ts # 地图数据能力
|
||
│ │
|
||
│ ├── usecases/ # 用例实现
|
||
│ │ ├── ImportDataUseCase.ts # 导入数据:解析→推断→入库
|
||
│ │ ├── CreateChartUseCase.ts # 创建图表:推荐→默认绑定→生成
|
||
│ │ ├── UpdateChartConfigUseCase.ts # 更新图表配置:校验→转换→更新
|
||
│ │ ├── ExportUseCase.ts # 导出:按格式调用 Gateway
|
||
│ │ ├── TemplateUseCase.ts # 模板:保存/加载/导入导出
|
||
│ │ └── LayoutUseCase.ts # 布局:增删改查
|
||
│ │
|
||
│ └── dto/ # 数据传输对象
|
||
│ ├── ImportResult.ts
|
||
│ ├── ChartSuggestion.ts
|
||
│ └── ExportOptions.ts
|
||
│
|
||
├── adapters/ # 适配层(实现端口,连接内外)
|
||
│ ├── gateways/ # 输出端口的具体实现
|
||
│ │ ├── SheetJSFileParser.ts # IFileParser → SheetJS 实现
|
||
│ │ ├── EChartsOptionBuilder.ts # IChartRenderer → ECharts option 构建
|
||
│ │ │ ├── barOptionBuilder.ts
|
||
│ │ │ ├── lineOptionBuilder.ts
|
||
│ │ │ ├── pieOptionBuilder.ts
|
||
│ │ │ ├── scatterOptionBuilder.ts
|
||
│ │ │ ├── radarOptionBuilder.ts
|
||
│ │ │ ├── heatmapOptionBuilder.ts
|
||
│ │ │ ├── mapOptionBuilder.ts
|
||
│ │ │ ├── wordcloudOptionBuilder.ts
|
||
│ │ │ ├── comboOptionBuilder.ts
|
||
│ │ │ └── index.ts
|
||
│ │ ├── exportGateway/ # IExportGateway 实现
|
||
│ │ │ ├── ImageExportGateway.ts # PNG/JPG/SVG
|
||
│ │ │ ├── PDFExportGateway.ts # jsPDF
|
||
│ │ │ ├── ExcelExportGateway.ts # SheetJS
|
||
│ │ │ ├── PPTExportGateway.ts # PptxGenJS
|
||
│ │ │ ├── HTMLExportGateway.ts # 独立 HTML
|
||
│ │ │ └── index.ts
|
||
│ │ ├── LocalStorageGateway.ts # IStorageGateway → localStorage
|
||
│ │ └── GeoJsonProvider.ts # IGeoDataProvider → 静态 GeoJSON
|
||
│ │
|
||
│ ├── state/ # 状态管理适配
|
||
│ │ ├── redux/ # Redux Toolkit(复杂全局状态)
|
||
│ │ │ ├── store.ts # configureStore
|
||
│ │ │ ├── dataSlice.ts # 数据集 CRUD
|
||
│ │ │ ├── chartSlice.ts # 图表实例 CRUD + 配置变更
|
||
│ │ │ ├── layoutSlice.ts # 布局状态
|
||
│ │ │ └── templateSlice.ts # 模板状态
|
||
│ │ │
|
||
│ │ └── zustand/ # Zustand(轻量 UI 状态)
|
||
│ │ ├── uiStore.ts # 面板开关/活跃选项卡/拖拽状态
|
||
│ │ ├── themeStore.ts # 主题/色板
|
||
│ │ └── interactionStore.ts # 悬停/选中/缩放等交互态
|
||
│ │
|
||
│ └── presenters/ # Presenter(Use Case 输出 → View Model)
|
||
│ ├── DataPreviewPresenter.ts # DataSet → 表格展示数据
|
||
│ ├── ChartPresenter.ts # ChartInstance → ECharts option
|
||
│ ├── FieldListPresenter.ts # Column[] → 左侧字段列表 VM
|
||
│ └── ExportPresenter.ts # 导出进度/结果 → UI 状态
|
||
│
|
||
├── frameworks/ # 框架层(Next.js + React 组件)
|
||
│ ├── app/ # Next.js App Router
|
||
│ │ ├── layout.tsx # 根布局
|
||
│ │ ├── page.tsx # 首页(仪表盘编辑器)
|
||
│ │ ├── providers.tsx # Redux Provider + 主题 Provider
|
||
│ │ └── globals.css
|
||
│ │
|
||
│ ├── components/ # React 组件(纯 UI)
|
||
│ │ ├── layout/
|
||
│ │ │ ├── TopToolbar.tsx
|
||
│ │ │ ├── LeftPanel.tsx
|
||
│ │ │ ├── CenterCanvas.tsx
|
||
│ │ │ ├── RightPanel.tsx
|
||
│ │ │ └── BottomStatusBar.tsx
|
||
│ │ │
|
||
│ │ ├── dataImport/
|
||
│ │ │ ├── DropZone.tsx
|
||
│ │ │ ├── DataPreviewTable.tsx
|
||
│ │ │ └── SheetSelector.tsx
|
||
│ │ │
|
||
│ │ ├── charts/
|
||
│ │ │ ├── ChartWrapper.tsx
|
||
│ │ │ ├── ChartRenderer.tsx
|
||
│ │ │ ├── KPICard.tsx
|
||
│ │ │ ├── EChartsBase.tsx
|
||
│ │ │ └── DataTable.tsx
|
||
│ │ │
|
||
│ │ ├── configPanel/
|
||
│ │ │ ├── DataBinding.tsx
|
||
│ │ │ ├── StyleConfig.tsx
|
||
│ │ │ ├── TitleConfig.tsx
|
||
│ │ │ ├── ColorConfig.tsx
|
||
│ │ │ ├── AxisConfig.tsx
|
||
│ │ │ ├── LegendConfig.tsx
|
||
│ │ │ ├── LabelConfig.tsx
|
||
│ │ │ ├── SizeConfig.tsx
|
||
│ │ │ └── InteractionConfig.tsx
|
||
│ │ │
|
||
│ │ ├── template/
|
||
│ │ │ ├── TemplateGallery.tsx
|
||
│ │ │ ├── TemplateCard.tsx
|
||
│ │ │ └── TemplateSaveDialog.tsx
|
||
│ │ │
|
||
│ │ └── export/
|
||
│ │ └── ExportDialog.tsx
|
||
│ │
|
||
│ ├── hooks/ # React Hooks(连接 Use Case 与组件)
|
||
│ │ ├── useImportData.ts # 调用 ImportDataUseCase
|
||
│ │ ├── useCreateChart.ts # 调用 CreateChartUseCase
|
||
│ │ ├── useChartConfig.ts # 调用 UpdateChartConfigUseCase
|
||
│ │ ├── useExport.ts # 调用 ExportUseCase
|
||
│ │ ├── useTemplate.ts # 调用 TemplateUseCase
|
||
│ │ └── useLayout.ts # 调用 LayoutUseCase
|
||
│ │
|
||
│ └── di/ # 依赖注入容器
|
||
│ └── container.ts # 组装所有端口实现
|
||
│
|
||
├── assets/
|
||
│ ├── geo/ # 中国省市 GeoJSON
|
||
│ └── sample/ # 示例数据集
|
||
│
|
||
├── next.config.ts
|
||
├── tsconfig.json
|
||
└── package.json
|
||
```
|
||
|
||
## 三、依赖方向图
|
||
|
||
```
|
||
frameworks/ adapters/ application/ domain/
|
||
(Next.js+React) (实现层) (用例层) (领域层)
|
||
|
||
components ──→ hooks ──→ usecases ──→ entities
|
||
│ │ │
|
||
│ │ valueObjects
|
||
│ │ │
|
||
│ │ services
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ports/input │
|
||
│ ports/output ←────┘
|
||
│ ▲
|
||
│ │(实现)
|
||
├──→ state/redux
|
||
├──→ state/zustand
|
||
├──→ gateways
|
||
└──→ presenters
|
||
|
||
箭头 = import 方向,全部从外向内,绝不反向
|
||
ports/output 定义接口在 application 层,实现在 adapters 层(依赖反转)
|
||
```
|
||
|
||
## 四、Zustand + Redux Toolkit 混合策略
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ Redux Toolkit(大厂模式) │
|
||
│ │
|
||
│ 管理:业务核心状态,需要以下特性的数据 │
|
||
│ · 复杂的状态转换(reducer 逻辑多) │
|
||
│ · 需要 middleware(如 thunk、日志) │
|
||
│ · 多组件深层共享 + 可预测性要求高 │
|
||
│ · DevTools 调试需求 │
|
||
│ │
|
||
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────────┐ │
|
||
│ │dataSlice │ │chartSlice│ │layoutSlice│ │templateSlice│ │
|
||
│ │数据集CRUD │ │图表实例 │ │画布布局 │ │模板管理 │ │
|
||
│ │多Sheet │ │绑定+样式 │ │网格位置 │ │保存/加载 │ │
|
||
│ └──────────┘ └──────────┘ └───────────┘ └────────────┘ │
|
||
└──────────────────────────────────────────────────────────┘
|
||
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ Zustand(轻量快速) │
|
||
│ │
|
||
│ 管理:UI 瞬态,不进 Redux 以避免频繁 dispatch 开销 │
|
||
│ · 高频变化(拖拽中坐标、hover 状态) │
|
||
│ · 局部 UI 状态(面板折叠、Tab 选中) │
|
||
│ · 无需 DevTools 追踪的状态 │
|
||
│ │
|
||
│ ┌──────────┐ ┌────────────┐ ┌──────────────────┐ │
|
||
│ │ uiStore │ │ themeStore │ │ interactionStore │ │
|
||
│ │面板开关 │ │亮/暗主题 │ │hover/选中/拖拽中 │ │
|
||
│ │活跃Tab │ │当前色板 │ │缩放级别 │ │
|
||
│ │Modal状态 │ │ │ │ │ │
|
||
│ └──────────┘ └────────────┘ └──────────────────┘ │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 划分原则
|
||
|
||
| 判断标准 | → Redux Toolkit | → Zustand |
|
||
|----------|----------------|-----------|
|
||
| 影响图表输出? | 是 | 否 |
|
||
| 需要持久化/撤销? | 是 | 否 |
|
||
| 变化频率? | 低~中 | 高 |
|
||
| 多组件共享? | 3+ 组件 | 1~2 组件 |
|
||
| 需要 DevTools? | 是 | 否 |
|
||
|
||
## 五、依赖注入(DI)
|
||
|
||
```typescript
|
||
// frameworks/di/container.ts
|
||
// 组装所有端口实现,向上提供给 hooks
|
||
|
||
import { ImportDataUseCase } from '@/application/usecases/ImportDataUseCase';
|
||
import { SheetJSFileParser } from '@/adapters/gateways/SheetJSFileParser';
|
||
import { LocalStorageGateway } from '@/adapters/gateways/LocalStorageGateway';
|
||
// ...
|
||
|
||
// 实例化 output port 实现
|
||
const fileParser = new SheetJSFileParser();
|
||
const storageGateway = new LocalStorageGateway();
|
||
const chartRenderer = new EChartsOptionBuilder();
|
||
const exportGateway = new CompositeExportGateway(/* ... */);
|
||
const geoProvider = new GeoJsonProvider();
|
||
|
||
// 注入到 use case
|
||
export const importDataUseCase = new ImportDataUseCase(fileParser);
|
||
export const createChartUseCase = new CreateChartUseCase(chartRenderer);
|
||
export const updateChartConfigUseCase = new UpdateChartConfigUseCase(chartRenderer);
|
||
export const exportUseCase = new ExportUseCase(exportGateway);
|
||
export const templateUseCase = new TemplateUseCase(storageGateway);
|
||
export const layoutUseCase = new LayoutUseCase();
|
||
```
|
||
|
||
```typescript
|
||
// frameworks/hooks/useImportData.ts
|
||
// Hook 只做:调 use case → 写 store → 返回状态
|
||
|
||
import { importDataUseCase } from '@/frameworks/di/container';
|
||
import { useAppDispatch } from '@/adapters/state/redux/store';
|
||
import { addDataSet } from '@/adapters/state/redux/dataSlice';
|
||
|
||
export function useImportData() {
|
||
const dispatch = useAppDispatch();
|
||
|
||
const handleImport = async (file: File) => {
|
||
const result = await importDataUseCase.execute(file); // 纯业务
|
||
dispatch(addDataSet(result.dataSet)); // 写状态
|
||
return result.suggestions; // 返回推荐
|
||
};
|
||
|
||
return { handleImport };
|
||
}
|
||
```
|
||
|
||
## 六、Use Case 调用链路示例
|
||
|
||
### 场景:用户拖入一个 Excel 文件
|
||
|
||
```
|
||
DropZone.tsx (UI)
|
||
│ onDrop(file)
|
||
▼
|
||
useImportData hook (frameworks/hooks)
|
||
│ 调用 importDataUseCase.execute(file)
|
||
▼
|
||
ImportDataUseCase (application/usecases)
|
||
│ ① 调用 IFileParser.parse(file) ←── 输出端口
|
||
│ ② 调用 FieldTypeInferenceService ←── 领域服务
|
||
│ ③ 调用 DataStructureInferenceService ←── 领域服务
|
||
│ ④ 调用 ChartRecommendationService ←── 领域服务
|
||
│ ⑤ 返回 { dataSet, suggestions }
|
||
▼
|
||
SheetJSFileParser (adapters/gateways) ←── 端口实现
|
||
│ SheetJS 解析 Excel → 二维数组 → DataSet Entity
|
||
▼
|
||
回到 hook
|
||
│ dispatch(addDataSet(dataSet)) ←── Redux
|
||
│ 返回 suggestions 给组件
|
||
▼
|
||
LeftPanel.tsx 更新字段列表
|
||
CenterCanvas.tsx 可选推荐图表
|
||
```
|
||
|
||
## 七、核心类型定义
|
||
|
||
```typescript
|
||
// ===== domain/entities =====
|
||
|
||
type FieldType = 'number' | 'text' | 'date' | 'percentage' | 'geo';
|
||
|
||
interface Column {
|
||
name: string; // 列名
|
||
type: FieldType; // 推断出的类型
|
||
sampleValues: any[]; // 前 10 个样本值
|
||
}
|
||
|
||
interface DataSet {
|
||
id: string;
|
||
fileName: string;
|
||
sheetName?: string;
|
||
columns: Column[];
|
||
rows: Record<string, any>[]; // 每行是 { 列名: 值 }
|
||
}
|
||
|
||
type ChartType =
|
||
| 'kpi' | 'bar' | 'grouped-bar'
|
||
| 'stacked-bar' | 'horizontal-bar'
|
||
| 'line' | 'area' | 'pie'
|
||
| 'donut' | 'scatter' | 'radar'
|
||
| 'wordcloud' | 'boston-matrix'
|
||
| 'heatmap' | 'map' | 'combo'
|
||
| 'data-table';
|
||
|
||
interface FieldBinding {
|
||
axis: 'x' | 'y' | 'series' | 'color' | 'size' | 'label' | 'value';
|
||
columnName: string;
|
||
aggregation?: 'sum' | 'count' | 'avg' | 'max' | 'min';
|
||
}
|
||
|
||
interface StyleConfig {
|
||
title: { text: string; fontSize: number; color: string; position: string };
|
||
colors: string[]; // 色板
|
||
legend: { show: boolean; position: string; orient: string };
|
||
axis: {
|
||
xLabel: string; yLabel: string;
|
||
showGrid: boolean; labelRotate: number;
|
||
};
|
||
label: { show: boolean; format: 'value' | 'percent' | 'custom'; template?: string };
|
||
background: { color: string; opacity: number };
|
||
border: { color: string; width: number; radius: number };
|
||
animation: boolean;
|
||
}
|
||
|
||
interface ChartInstance {
|
||
id: string;
|
||
type: ChartType;
|
||
dataSetId: string;
|
||
bindings: FieldBinding[];
|
||
style: StyleConfig;
|
||
filters?: FilterRule[];
|
||
sort?: { column: string; order: 'asc' | 'desc' };
|
||
topN?: number;
|
||
}
|
||
|
||
// ===== layout =====
|
||
|
||
interface LayoutItem {
|
||
chartId: string;
|
||
x: number; y: number;
|
||
w: number; h: number;
|
||
}
|
||
|
||
// ===== template =====
|
||
|
||
interface Template {
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
charts: Omit<ChartInstance, 'dataSetId'>[];
|
||
layout: LayoutItem[];
|
||
theme: string;
|
||
createdAt: string;
|
||
}
|
||
```
|
||
|
||
## 八、各 Store 职责
|
||
|
||
| Store | 状态 | 核心操作 |
|
||
|-------|------|---------|
|
||
| **dataSlice** (Redux) | dataSets[], activeDataSetId | addDataSet, removeDataSet, updateCell |
|
||
| **chartSlice** (Redux) | charts[], activeChartId | addChart, removeChart, updateBinding, updateStyle |
|
||
| **layoutSlice** (Redux) | layouts[], canvasSize, snapToGrid | updateLayout, resizeCanvas |
|
||
| **templateSlice** (Redux) | templates[] | saveTemplate, loadTemplate, deleteTemplate, exportTemplate, importTemplate |
|
||
| **uiStore** (Zustand) | panelStates, activeTab, modalVisible | togglePanel, setActiveTab, openModal, closeModal |
|
||
| **themeStore** (Zustand) | currentTheme, customPalettes | setTheme, addPalette |
|
||
| **interactionStore** (Zustand) | hoveredChartId, dragState, zoomLevel | setHover, setDrag, setZoom |
|
||
|
||
## 九、Next.js 使用策略
|
||
|
||
| 特性 | 用法 |
|
||
|------|------|
|
||
| App Router | 当前只有一个页面 `/`,但架构预留多页扩展 |
|
||
| Server Component | `layout.tsx` 作为 Server Component 提供外壳 |
|
||
| Client Component | 所有交互组件标记 `'use client'` |
|
||
| Static Export | `next.config.ts` 配置 `output: 'export'`,构建为纯静态文件 |
|
||
| 路由预留 | 未来可扩展 `/templates`、`/help` 等页面 |
|
||
|
||
## 十、技术栈总览
|
||
|
||
| 层面 | 选型 |
|
||
|------|------|
|
||
| 框架 | Next.js (App Router) + React 18 + TypeScript |
|
||
| 构建 | Next.js 内置 (Turbopack) |
|
||
| 图表引擎 | ECharts 5 |
|
||
| 词云 | echarts-wordcloud |
|
||
| 表格 | AG Grid Community |
|
||
| 文件解析 | SheetJS (xlsx/csv) + 原生 JSON.parse |
|
||
| 拖拽布局 | react-grid-layout |
|
||
| 导出 PNG/SVG | ECharts 内置 + html2canvas |
|
||
| 导出 PDF | jsPDF + html2canvas |
|
||
| 导出 PPT | PptxGenJS |
|
||
| 导出 Excel | SheetJS |
|
||
| 导出 HTML | 内联打包 ECharts + 数据 |
|
||
| 状态管理 | Redux Toolkit (业务状态) + Zustand (UI 状态) |
|
||
| UI 组件 | Ant Design 5 |
|
||
| 地图数据 | 中国省市 GeoJSON |
|
||
| 本地存储 | localStorage (模板/配置持久化) |
|