dv/docs/frontend-architecture-guide.md

467 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/ # PresenterUse 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 (模板/配置持久化) |