Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions docs/tutorial/page-based-snapshot-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# 按页面快照管理

## 概述

新的快照管理系统支持为每个页面维护独立的快照历史,解决了之前所有页面共享快照导致的混乱问题。

## 主要特性

### 1. 页面隔离
- 每个页面拥有独立的快照历史
- 不同页面的撤销/重做操作互不影响
- 页面切换时自动切换到对应的快照上下文

### 2. 自动页面ID管理
- 编辑已保存页面时,使用页面的实际ID
- 创建新页面时,使用临时ID 'unsaved'
- 页面保存后可以迁移快照到新的页面ID

### 3. 向后兼容
- 支持旧版本的快照数据
- 数据库自动升级,添加pageId字段
- 旧快照数据不会丢失

## API 变更

### SnapshotState 新增方法

```typescript
// 设置当前页面ID
setCurrentPageId(pageId: string): void

// 获取当前页面ID
getCurrentPageId(): string

// 清空指定页面的快照
clearSnapshot(pageId?: string): Promise<void>
```

### CanvasState 新增方法

```typescript
// 设置当前页面ID(同时更新快照状态)
setCurrentPageId(pageId: string): void

// 获取当前页面ID
getCurrentPageId(): string

// 页面切换处理
switchPage(newPageId: string): void
```

## 使用方式

### 1. 在设计器中设置页面ID

```typescript
import { useCanvasState } from '@open-data-v/designer'

const canvasState = useCanvasState()

// 编辑已保存的页面
const pageId = route.params.index as string
canvasState.setCurrentPageId(pageId || 'unsaved')

// 加载页面数据
const resp = await getPageApi(pageId)
if (resp.data) {
designer.value!.setLayoutData(resp.data)
}
```

### 2. 快照操作

```typescript
import { useSnapshotState } from '@open-data-v/designer'

const snapshotState = useSnapshotState()

// 设置当前页面
snapshotState.setCurrentPageId('page-123')

// 创建快照(自动关联到当前页面)
snapshotState.saveSnapshot(canvasData, canvasStyle, dataSlotters)

// 撤销(仅影响当前页面)
const lastSnapshot = await snapshotState.lastRecord()

// 重做(仅影响当前页面)
const nextSnapshot = await snapshotState.nextRecord()

// 清空当前页面的快照
await snapshotState.clearSnapshot()

// 清空指定页面的快照
await snapshotState.clearSnapshot('page-456')

// 清空所有快照
await snapshotState.clearSnapshot('all')
```

### 3. 页面切换

```typescript
// 当用户切换到不同页面时
const newPageId = 'page-789'
canvasState.switchPage(newPageId)

// 这会自动:
// 1. 更新当前页面ID
// 2. 切换快照上下文
// 3. 重置游标位置
```

## 数据库结构

### 快照表结构

```typescript
interface StoreComponentData {
id?: number // 自增主键
pageId?: string // 页面ID(新增字段)
canvasData: any[] // 画布组件数据
canvasStyle: any // 画布样式
dataSlotters: any[] // 数据插槽
}
```

### 索引

- 主键:`id`
- 索引:`pageId` (用于快速查询特定页面的快照)

## 迁移指南

### 从旧版本升级

1. 数据库会自动升级到版本2
2. 旧的快照数据会保留,但pageId为undefined
3. 新创建的快照会自动包含pageId

### 处理旧数据

```typescript
// 查询没有pageId的旧快照
const legacySnapshots = await snapshotDb.snapshot
.where('pageId')
.equals(undefined)
.toArray()

// 可以选择清理或迁移这些数据
```

## 注意事项

1. **页面ID命名**:建议使用有意义的页面ID,避免冲突
2. **快照数量限制**:每个页面独立计算快照数量(默认10个)
3. **内存使用**:每个页面的游标信息会保存在内存中
4. **清理策略**:删除页面时建议同时清理相关快照

## 故障排除

### 快照混乱
- 确保在页面加载时正确设置了页面ID
- 检查页面切换时是否调用了switchPage方法

### 撤销/重做不工作
- 验证当前页面ID是否正确设置
- 检查是否有该页面的快照数据

### 数据库升级失败
- 清空浏览器的IndexedDB数据
- 重新加载应用程序
5 changes: 5 additions & 0 deletions examples/src/pages/DesignerView/View.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const settingStore = useProjectSettingStoreWithOut()

onMounted(async () => {
const index = route.params.index as string
// 设置当前页面ID
canvasState.setCurrentPageId(index || 'unsaved')

if (index) {
await restore(index)
}
Expand All @@ -83,6 +86,8 @@ const restore = async (index: string) => {
if (!resp.data) {
return
}
// 确保页面ID已设置
canvasState.setCurrentPageId(index)
designer.value!.setLayoutData(resp.data)
}

Expand Down
4 changes: 4 additions & 0 deletions examples/src/pages/Preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const scriptState = useScriptState()
scriptState.loadPlugins([CustomScriptPlugin, SystemScriptPlugin])

onMounted(async () => {
// 预览页面使用最新的快照,不区分页面ID
// 这里可以根据需要修改为特定页面的快照
snapShotState.setCurrentPageId('unsaved') // 或者从URL参数获取页面ID

const snapshot = await snapShotState.latestRecord()
if (snapshot) {
viewer.value!.setLayoutData({
Expand Down
8 changes: 7 additions & 1 deletion packages/designer/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import type { ComponentData } from './type'

export interface StoreComponentData extends ComponentData {
id?: number
pageId?: string // 添加页面ID字段
}

export class SnapShotDexie extends Dexie {
snapshot!: Table<StoreComponentData>

constructor() {
super('snapshot')
// 版本1:原始结构
this.version(1).stores({
snapshot: '++id, canvasData, canvasStyle, dataSlotters' // Primary key and indexed props
snapshot: '++id, canvasData, canvasStyle, dataSlotters'
})
// 版本2:添加pageId索引
this.version(2).stores({
snapshot: '++id, canvasData, canvasStyle, dataSlotters, pageId'
})
}
}
Expand Down
107 changes: 107 additions & 0 deletions packages/designer/src/state/__tests__/snapshot-page-management.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'

Check failure on line 1 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Run autofix to sort these imports!
import useSnapshotState from '../snapshot'
import { snapshotDb } from '../../db'

describe('按页面快照管理', () => {
let snapshotState: ReturnType<typeof useSnapshotState>

beforeEach(async () => {
snapshotState = useSnapshotState()
// 清空数据库
await snapshotDb.snapshot.clear()
})

afterEach(async () => {
// 清空数据库
await snapshotDb.snapshot.clear()
})

it('应该能够为不同页面创建独立的快照', async () => {
// 设置页面1
snapshotState.setCurrentPageId('page1')

Check failure on line 22 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Delete `····`
// 创建页面1的快照
snapshotState.recordSnapshot(
[],
{ width: 1920, height: 1080, background: { backgroundColor: '#fff' } },
[]
)

// 等待快照保存完成
await new Promise(resolve => setTimeout(resolve, 100))

Check failure on line 31 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Replace `resolve` with `(resolve)`

// 设置页面2
snapshotState.setCurrentPageId('page2')

Check failure on line 35 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Delete `····`
// 创建页面2的快照
snapshotState.recordSnapshot(
[],
{ width: 1366, height: 768, background: { backgroundColor: '#000' } },
[]
)

// 等待快照保存完成
await new Promise(resolve => setTimeout(resolve, 100))

Check failure on line 44 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Replace `resolve` with `(resolve)`

// 验证页面1的快照
snapshotState.setCurrentPageId('page1')
const page1Snapshot = await snapshotState.latestRecord()
expect(page1Snapshot?.pageId).toBe('page1')
expect(page1Snapshot?.canvasStyle.width).toBe(1920)

// 验证页面2的快照
snapshotState.setCurrentPageId('page2')
const page2Snapshot = await snapshotState.latestRecord()
expect(page2Snapshot?.pageId).toBe('page2')
expect(page2Snapshot?.canvasStyle.width).toBe(1366)
})

it('应该能够独立清空特定页面的快照', async () => {
// 创建两个页面的快照
snapshotState.setCurrentPageId('page1')
snapshotState.recordSnapshot([], { width: 1920, height: 1080, background: {} }, [])

Check failure on line 63 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Delete `····`
snapshotState.setCurrentPageId('page2')
snapshotState.recordSnapshot([], { width: 1366, height: 768, background: {} }, [])

// 等待快照保存完成
await new Promise(resolve => setTimeout(resolve, 100))

Check failure on line 68 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Replace `resolve` with `(resolve)`

// 清空页面1的快照
await snapshotState.clearSnapshot('page1')

// 验证页面1的快照已清空
snapshotState.setCurrentPageId('page1')
const page1Snapshot = await snapshotState.latestRecord()
expect(page1Snapshot).toBeUndefined()

// 验证页面2的快照仍然存在
snapshotState.setCurrentPageId('page2')
const page2Snapshot = await snapshotState.latestRecord()
expect(page2Snapshot?.pageId).toBe('page2')
})

it('应该为每个页面维护独立的游标', async () => {
// 为页面1创建多个快照
snapshotState.setCurrentPageId('page1')
snapshotState.recordSnapshot([], { width: 1920, height: 1080, background: {} }, [])
await new Promise(resolve => setTimeout(resolve, 100))

Check failure on line 88 in packages/designer/src/state/__tests__/snapshot-page-management.test.ts

View workflow job for this annotation

GitHub Actions / preview

Replace `resolve` with `(resolve)`
snapshotState.recordSnapshot([], { width: 1920, height: 1080, background: {} }, [])
await new Promise(resolve => setTimeout(resolve, 100))

// 为页面2创建快照
snapshotState.setCurrentPageId('page2')
snapshotState.recordSnapshot([], { width: 1366, height: 768, background: {} }, [])
await new Promise(resolve => setTimeout(resolve, 100))

// 在页面1中执行撤销操作
snapshotState.setCurrentPageId('page1')
const page1LastSnapshot = await snapshotState.lastRecord()
expect(page1LastSnapshot?.pageId).toBe('page1')

// 切换到页面2,游标应该独立
snapshotState.setCurrentPageId('page2')
const page2LatestSnapshot = await snapshotState.latestRecord()
expect(page2LatestSnapshot?.pageId).toBe('page2')
})
})
30 changes: 30 additions & 0 deletions packages/designer/src/state/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ class CanvasState {
> = new Map()

private componentMap: Map<string, BaseComponent> = new Map()
private currentPageId: string = 'unsaved'

constructor(config?: CanvasStyleConfig) {
const extraStyles = config
? config
Expand Down Expand Up @@ -183,6 +185,34 @@ class CanvasState {
this.state.darkTheme = isDark
}

/**
* 设置当前页面ID
* @param pageId 页面ID
*/
setCurrentPageId(pageId: string) {
this.currentPageId = pageId || 'unsaved'
// 同时更新快照状态的页面ID
snapShotState.setCurrentPageId(this.currentPageId)
}

/**
* 获取当前页面ID
*/
getCurrentPageId(): string {
return this.currentPageId
}

/**
* 切换页面时的清理工作
* @param newPageId 新页面ID
*/
switchPage(newPageId: string) {
// 如果页面ID发生变化,更新当前页面ID
if (this.currentPageId !== newPageId) {
this.setCurrentPageId(newPageId)
}
}

get components() {
return this.state.components
}
Expand Down
Loading
Loading