gd-web-core 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +132 -131
  2. package/src/components/DrawBlocks/DrawBlocks.vue +413 -0
  3. package/src/components/DrawBlocks/core/BlockManager.ts +184 -0
  4. package/src/components/DrawBlocks/core/Camera.ts +289 -0
  5. package/src/components/DrawBlocks/core/DrawBlocks.ts +782 -0
  6. package/src/components/DrawBlocks/core/ImageManager.ts +202 -0
  7. package/src/components/DrawBlocks/core/events/EventEmitter.ts +37 -0
  8. package/src/components/DrawBlocks/core/events/PointerHandler.ts +592 -0
  9. package/src/components/DrawBlocks/core/index.ts +26 -0
  10. package/src/components/DrawBlocks/core/renderer/BackgroundLayer.ts +325 -0
  11. package/src/components/DrawBlocks/core/renderer/CanvasRenderer.ts +155 -0
  12. package/src/components/DrawBlocks/core/renderer/index.ts +4 -0
  13. package/src/components/DrawBlocks/core/types.ts +77 -0
  14. package/src/components/DrawBlocks/core/utils/LayoutUtils.ts +10 -0
  15. package/src/components/DrawBlocks/core/utils/math.ts +41 -0
  16. package/src/components/DrawBlocks/index.ts +5 -0
  17. package/src/components/ScrollContainer/ScrollContainer.vue +1 -0
  18. package/src/components/SliderCaptcha/SliderCaptcha.css +602 -475
  19. package/src/components/SliderCaptcha/SliderCaptcha.vue +579 -319
  20. package/src/components/SliderCaptcha/SliderCaptchaContent.vue +182 -107
  21. package/src/components/SliderCaptcha/SliderCaptchaFooter.vue +257 -241
  22. package/src/components/SliderCaptcha/SliderCaptchaHeader.vue +112 -115
  23. package/src/components/SliderCaptcha/apis.ts +78 -8
  24. package/src/components/SliderCaptcha/demo.html +220 -0
  25. package/src/components/SliderCaptcha/slider-captcha.umd.ts +189 -0
  26. package/src/components/SliderCaptcha/useSliderCaptcha.ts +363 -271
  27. package/src/components/SliderCaptcha/vite.config.ts +42 -0
  28. package/src/components/index.ts +6 -5
  29. package/src/utils/request.ts +9 -3
package/package.json CHANGED
@@ -1,131 +1,132 @@
1
- {
2
- "name": "gd-web-core",
3
- "version": "0.0.13",
4
- "description": "Vue 3 基座能力封装库 - 组件与工具函数",
5
- "type": "module",
6
- "files": [
7
- "src/assets",
8
- "src/components",
9
- "src/utils",
10
- "src/index.ts",
11
- "docs/components",
12
- "docs/utils",
13
- "docs/INDEX.md"
14
- ],
15
- "main": "./src/index.ts",
16
- "module": "./src/index.ts",
17
- "types": "./src/index.ts",
18
- "exports": {
19
- ".": {
20
- "types": "./src/index.ts",
21
- "import": "./src/index.ts",
22
- "require": "./dist/index.umd.cjs"
23
- },
24
- "./components": {
25
- "types": "./src/components/index.ts",
26
- "import": "./src/components/index.ts"
27
- },
28
- "./utils": {
29
- "types": "./src/utils/index.ts",
30
- "import": "./src/utils/index.ts"
31
- },
32
- "./style.css": "./dist/index.css",
33
- "./package.json": "./package.json"
34
- },
35
- "sideEffects": [
36
- "**/*.css"
37
- ],
38
- "scripts": {
39
- "dev": "vite",
40
- "build": "vite build",
41
- "build:check": "vue-tsc --noEmit && vite build",
42
- "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist",
43
- "preview": "vite preview",
44
- "lint": "eslint . --fix",
45
- "format": "prettier --write .",
46
- "test": "vitest",
47
- "test:coverage": "vitest run --coverage",
48
- "release:patch": "npm version patch && npm publish",
49
- "release:minor": "npm version minor && npm publish",
50
- "release:major": "npm version major && npm publish",
51
- "publish:dry": "npm publish --dry-run",
52
- "check:package": "npm pack --dry-run"
53
- },
54
- "peerDependencies": {
55
- "vue": "^3.4.0",
56
- "vue-router": "^4.0.0"
57
- },
58
- "peerDependenciesMeta": {
59
- "vue-router": {
60
- "optional": true
61
- }
62
- },
63
- "optionalDependencies": {
64
- "ant-design-vue": "^4.0.0",
65
- "echarts": "^5.0.0"
66
- },
67
- "keywords": [
68
- "vue",
69
- "vue3",
70
- "components",
71
- "utils",
72
- "library"
73
- ],
74
- "author": "Your Name <your.email@example.com>",
75
- "license": "MIT",
76
- "repository": {
77
- "type": "git",
78
- "url": "https://github.com/your-org/gd-web-core.git"
79
- },
80
- "homepage": "https://github.com/your-org/gd-web-core#readme",
81
- "bugs": {
82
- "url": "https://github.com/your-org/gd-web-core/issues"
83
- },
84
- "publishConfig": {
85
- "access": "public"
86
- },
87
- "dependencies": {
88
- "@surely-vue/table": "4.1.9",
89
- "axios": "^1.13.6",
90
- "dayjs": "^1.11.19",
91
- "echarts": "^6.0.0",
92
- "nprogress": "^0.2.0",
93
- "pinia": "^3.0.4",
94
- "prismjs": "^1.30.0"
95
- },
96
- "devDependencies": {
97
- "@ant-design/icons-vue": "^7.0.1",
98
- "@eslint/js": "^9.39.2",
99
- "@types/node": "^22.10.6",
100
- "@types/nprogress": "^0.2.3",
101
- "@typescript-eslint/eslint-plugin": "^8.53.0",
102
- "@typescript-eslint/parser": "^8.53.0",
103
- "@vitejs/plugin-vue": "^6.0.1",
104
- "@vitest/ui": "^4.0.18",
105
- "ant-design-vue": "^4.2.6",
106
- "echarts": "^5.5.0",
107
- "eslint": "^9.39.2",
108
- "eslint-config-prettier": "^10.1.8",
109
- "eslint-plugin-prettier": "^5.5.4",
110
- "eslint-plugin-vue": "^10.6.2",
111
- "gd-vite-core": "^1.0.8",
112
- "globals": "^17.0.0",
113
- "happy-dom": "^20.5.0",
114
- "highlight.js": "^11.11.1",
115
- "less": "^4.5.1",
116
- "marked": "^17.0.1",
117
- "prettier": "^3.7.4",
118
- "typescript": "~5.6.0",
119
- "unplugin-auto-import": "^21.0.0",
120
- "unplugin-vue-components": "^30.0.0",
121
- "vite": "^7.2.4",
122
- "vite-plugin-css-injected-by-js": "^3.5.2",
123
- "vite-plugin-dts": "^4.2.0",
124
- "vite-plugin-require-transform": "^1.0.21",
125
- "vitest": "^4.0.18",
126
- "vue": "^3.5.24",
127
- "vue-eslint-parser": "^10.2.0",
128
- "vue-router": "^4.6.4",
129
- "vue-tsc": "^2.2.4"
130
- }
131
- }
1
+ {
2
+ "name": "gd-web-core",
3
+ "version": "0.0.15",
4
+ "description": "Vue 3 基座能力封装库 - 组件与工具函数",
5
+ "type": "module",
6
+ "files": [
7
+ "src/assets",
8
+ "src/components",
9
+ "src/utils",
10
+ "src/index.ts",
11
+ "docs/components",
12
+ "docs/utils",
13
+ "docs/INDEX.md"
14
+ ],
15
+ "main": "./src/index.ts",
16
+ "module": "./src/index.ts",
17
+ "types": "./src/index.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.ts",
21
+ "import": "./src/index.ts",
22
+ "require": "./dist/index.umd.cjs"
23
+ },
24
+ "./components": {
25
+ "types": "./src/components/index.ts",
26
+ "import": "./src/components/index.ts"
27
+ },
28
+ "./utils": {
29
+ "types": "./src/utils/index.ts",
30
+ "import": "./src/utils/index.ts"
31
+ },
32
+ "./style.css": "./dist/index.css",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "sideEffects": [
36
+ "**/*.css"
37
+ ],
38
+ "scripts": {
39
+ "dev": "vite",
40
+ "build": "eslint . --max-warnings=0 && vue-tsc --noEmit && vite build",
41
+ "build:check": "vue-tsc --noEmit && vite build",
42
+ "build:slider-captcha": "vite build --config src/components/SliderCaptcha/vite.config.ts && node -e \"require('fs').copyFileSync('src/components/SliderCaptcha/demo.html','dist/slider-captcha/demo.html')\"",
43
+ "build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist",
44
+ "preview": "vite preview",
45
+ "lint": "eslint . --fix",
46
+ "format": "prettier --write .",
47
+ "test": "vitest",
48
+ "test:coverage": "vitest run --coverage",
49
+ "release:patch": "npm version patch && npm publish",
50
+ "release:minor": "npm version minor && npm publish",
51
+ "release:major": "npm version major && npm publish",
52
+ "publish:dry": "npm publish --dry-run",
53
+ "check:package": "npm pack --dry-run"
54
+ },
55
+ "peerDependencies": {
56
+ "vue": "^3.4.0",
57
+ "vue-router": "^4.0.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "vue-router": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "optionalDependencies": {
65
+ "ant-design-vue": "^4.0.0",
66
+ "echarts": "^5.0.0"
67
+ },
68
+ "keywords": [
69
+ "vue",
70
+ "vue3",
71
+ "components",
72
+ "utils",
73
+ "library"
74
+ ],
75
+ "author": "Your Name <your.email@example.com>",
76
+ "license": "MIT",
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "https://github.com/your-org/gd-web-core.git"
80
+ },
81
+ "homepage": "https://github.com/your-org/gd-web-core#readme",
82
+ "bugs": {
83
+ "url": "https://github.com/your-org/gd-web-core/issues"
84
+ },
85
+ "publishConfig": {
86
+ "access": "public"
87
+ },
88
+ "dependencies": {
89
+ "@surely-vue/table": "4.1.9",
90
+ "axios": "^1.13.6",
91
+ "dayjs": "^1.11.19",
92
+ "echarts": "^6.0.0",
93
+ "nprogress": "^0.2.0",
94
+ "pinia": "^3.0.4",
95
+ "prismjs": "^1.30.0"
96
+ },
97
+ "devDependencies": {
98
+ "@ant-design/icons-vue": "^7.0.1",
99
+ "@eslint/js": "^9.39.2",
100
+ "@types/node": "^22.10.6",
101
+ "@types/nprogress": "^0.2.3",
102
+ "@typescript-eslint/eslint-plugin": "^8.53.0",
103
+ "@typescript-eslint/parser": "^8.53.0",
104
+ "@vitejs/plugin-vue": "^6.0.1",
105
+ "@vitest/ui": "^4.0.18",
106
+ "ant-design-vue": "^4.2.6",
107
+ "echarts": "^5.5.0",
108
+ "eslint": "^9.39.2",
109
+ "eslint-config-prettier": "^10.1.8",
110
+ "eslint-plugin-prettier": "^5.5.4",
111
+ "eslint-plugin-vue": "^10.6.2",
112
+ "gd-vite-core": "^1.0.8",
113
+ "globals": "^17.0.0",
114
+ "happy-dom": "^20.5.0",
115
+ "highlight.js": "^11.11.1",
116
+ "less": "^4.5.1",
117
+ "marked": "^17.0.1",
118
+ "prettier": "^3.7.4",
119
+ "typescript": "~5.6.0",
120
+ "unplugin-auto-import": "^21.0.0",
121
+ "unplugin-vue-components": "^30.0.0",
122
+ "vite": "^7.2.4",
123
+ "vite-plugin-css-injected-by-js": "^3.5.2",
124
+ "vite-plugin-dts": "^4.2.0",
125
+ "vite-plugin-require-transform": "^1.0.21",
126
+ "vitest": "^4.0.18",
127
+ "vue": "^3.5.24",
128
+ "vue-eslint-parser": "^10.2.0",
129
+ "vue-router": "^4.6.4",
130
+ "vue-tsc": "^2.2.4"
131
+ }
132
+ }
@@ -0,0 +1,413 @@
1
+ <template>
2
+ <div ref="containerRef" class="draw-blocks-container" :style="containerStyle">
3
+ <!-- Canvas 将由 DrawBlocks 实例动态创建 -->
4
+ <!-- 8 个控制点 -->
5
+ <template v-if="activeBlock && camera">
6
+ <div
7
+ v-for="point in controlPoints"
8
+ :key="point.type"
9
+ class="control-point"
10
+ :class="point.type"
11
+ :style="getControlPointStyle(point)"
12
+ @pointerdown.stop="handleControlPointDown($event, point.type)"
13
+ ></div>
14
+ </template>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
20
+ import { DrawBlocks, type DrawBlocksOptions, type Block, type Image, type CameraState } from './core'
21
+
22
+ /** 控制点类型 */
23
+ type ControlPointType = 'nw' | 'ne' | 'sw' | 'se' | 'tc' | 'bc' | 'lc' | 'rc'
24
+
25
+ /** 控制点信息 */
26
+ interface ControlPoint {
27
+ type: ControlPointType
28
+ x: number
29
+ y: number
30
+ }
31
+
32
+ /** Props */
33
+ interface Props {
34
+ defaultScale?: number
35
+ minScale?: number
36
+ maxScale?: number
37
+ bounceFactor?: number
38
+ boundPadding?: number
39
+ renderOnChange?: boolean
40
+ height?: string
41
+ initialData?: {
42
+ images?: Image[]
43
+ blocks?: Block[]
44
+ camera?: Partial<CameraState>
45
+ }
46
+ }
47
+
48
+ const props = withDefaults(defineProps<Props>(), {
49
+ defaultScale: 1,
50
+ minScale: 0.1,
51
+ maxScale: 5,
52
+ bounceFactor: 0.2,
53
+ boundPadding: 50,
54
+ renderOnChange: true,
55
+ height: '100%'
56
+ })
57
+
58
+ const emit = defineEmits<{
59
+ mounted: [instance: DrawBlocks]
60
+ ready: []
61
+ }>()
62
+
63
+ const containerRef = ref<HTMLElement | null>(null)
64
+ let drawBlocksInstance: DrawBlocks | null = null
65
+
66
+ /** 当前选中块 */
67
+ const activeBlock = ref<Block | null>(null)
68
+
69
+ /** 相机状态 */
70
+ const camera = ref<CameraState | null>(null)
71
+
72
+ /** 容器尺寸 */
73
+ const containerSize = reactive({ width: 0, height: 0, dpr: 1 })
74
+
75
+ /** 容器样式 */
76
+ const containerStyle = computed(() => ({
77
+ height: props.height
78
+ }))
79
+
80
+ /** 控制点列表 */
81
+ const controlPoints = computed<ControlPoint[]>(() => {
82
+ if (!activeBlock.value) return []
83
+
84
+ const block = activeBlock.value
85
+ const x1 = Math.min(block.x1, block.x2)
86
+ const x2 = Math.max(block.x1, block.x2)
87
+ const y1 = Math.min(block.y1, block.y2)
88
+ const y2 = Math.max(block.y1, block.y2)
89
+ const centerX = (x1 + x2) / 2
90
+ const centerY = (y1 + y2) / 2
91
+
92
+ return [
93
+ { type: 'nw', x: x1, y: y1 },
94
+ { type: 'ne', x: x2, y: y1 },
95
+ { type: 'sw', x: x1, y: y2 },
96
+ { type: 'se', x: x2, y: y2 },
97
+ { type: 'tc', x: centerX, y: y1 },
98
+ { type: 'bc', x: centerX, y: y2 },
99
+ { type: 'lc', x: x1, y: centerY },
100
+ { type: 'rc', x: x2, y: centerY }
101
+ ]
102
+ })
103
+
104
+ /** 获取控制点的屏幕样式 */
105
+ function getControlPointStyle(point: ControlPoint): Record<string, string> {
106
+ // 默认隐藏
107
+ const hidden = { left: '-100px', top: '-100px', width: '8px', height: '8px' }
108
+ if (!camera.value) return hidden
109
+
110
+ // 世界坐标转 canvas CSS 像素坐标
111
+ // 公式: (world + camera) * scale
112
+ const scale = camera.value.scale
113
+ const camX = camera.value.x
114
+ const camY = camera.value.y
115
+ const screenX = (point.x + camX) * scale
116
+ const screenY = (point.y + camY) * scale
117
+
118
+ const size = 20 // 移动端触摸友好的尺寸
119
+ return {
120
+ left: `${screenX}px`,
121
+ top: `${screenY}px`,
122
+ width: `${size}px`,
123
+ height: `${size}px`,
124
+ transform: 'translate(-50%, -50%)'
125
+ }
126
+ }
127
+
128
+ /** 处理控制点按下 */
129
+ function handleControlPointDown(e: PointerEvent, type: ControlPointType) {
130
+ // 阻止冒泡,让 canvas 不处理这个事件
131
+ e.preventDefault()
132
+ e.stopPropagation()
133
+
134
+ if (!activeBlock.value || !drawBlocksInstance) return
135
+
136
+ // 获取相机状态
137
+ const cam = drawBlocksInstance.getCamera()
138
+ const block = activeBlock.value
139
+
140
+ // 计算点击位置的世界坐标
141
+ const rect = containerRef.value!.getBoundingClientRect()
142
+ const screenX = e.clientX - rect.left
143
+ const screenY = e.clientY - rect.top
144
+
145
+ // 屏幕坐标转世界坐标:screenX / scale - cameraX
146
+ const worldX = screenX / cam.scale - cam.x
147
+ const worldY = screenY / cam.scale - cam.y
148
+
149
+ // 通知 DrawBlocks 进入 resize 模式
150
+ // 这里需要通过事件或其他方式通知
151
+ // 由于是 HTML overlay,我们需要手动处理 resize 逻辑
152
+ handleResizeStart(e, type, worldX, worldY, block)
153
+ }
154
+
155
+ /** resize 起始状态 */
156
+ let resizeStartState: {
157
+ type: ControlPointType
158
+ startWorldX: number
159
+ startWorldY: number
160
+ blockX1: number
161
+ blockY1: number
162
+ blockX2: number
163
+ blockY2: number
164
+ } | null = null
165
+
166
+ /** 处理 resize 开始 */
167
+ function handleResizeStart(e: PointerEvent, type: ControlPointType, worldX: number, worldY: number, block: Block) {
168
+ resizeStartState = {
169
+ type,
170
+ startWorldX: worldX,
171
+ startWorldY: worldY,
172
+ blockX1: block.x1,
173
+ blockY1: block.y1,
174
+ blockX2: block.x2,
175
+ blockY2: block.y2
176
+ }
177
+
178
+ // 监听 pointermove 和 pointerup
179
+ document.addEventListener('pointermove', handleResizeMove)
180
+ document.addEventListener('pointerup', handleResizeEnd)
181
+ }
182
+
183
+ /** 处理 resize 移动 */
184
+ function handleResizeMove(e: PointerEvent) {
185
+ if (!resizeStartState || !activeBlock.value || !drawBlocksInstance) return
186
+
187
+ const rect = containerRef.value!.getBoundingClientRect()
188
+ const screenX = e.clientX - rect.left
189
+ const screenY = e.clientY - rect.top
190
+ const cam = drawBlocksInstance.getCamera()
191
+
192
+ // 屏幕坐标转世界坐标:screenX / scale - cameraX
193
+ const worldX = screenX / cam.scale - cam.x
194
+ const worldY = screenY / cam.scale - cam.y
195
+
196
+ const deltaX = worldX - resizeStartState.startWorldX
197
+ const deltaY = worldY - resizeStartState.startWorldY
198
+
199
+ const { type, blockX1, blockY1, blockX2, blockY2 } = resizeStartState
200
+ let updates: Partial<Block> = {}
201
+
202
+ switch (type) {
203
+ case 'nw':
204
+ updates = { x1: blockX1 + deltaX, y1: blockY1 + deltaY }
205
+ break
206
+ case 'ne':
207
+ updates = { x2: blockX2 + deltaX, y1: blockY1 + deltaY }
208
+ break
209
+ case 'sw':
210
+ updates = { x1: blockX1 + deltaX, y2: blockY2 + deltaY }
211
+ break
212
+ case 'se':
213
+ updates = { x2: blockX2 + deltaX, y2: blockY2 + deltaY }
214
+ break
215
+ case 'tc':
216
+ updates = { y1: blockY1 + deltaY }
217
+ break
218
+ case 'bc':
219
+ updates = { y2: blockY2 + deltaY }
220
+ break
221
+ case 'lc':
222
+ updates = { x1: blockX1 + deltaX }
223
+ break
224
+ case 'rc':
225
+ updates = { x2: blockX2 + deltaX }
226
+ break
227
+ }
228
+
229
+ // 限制最小尺寸
230
+ const newX1 = updates.x1 ?? blockX1
231
+ const newX2 = updates.x2 ?? blockX2
232
+ const newY1 = updates.y1 ?? blockY1
233
+ const newY2 = updates.y2 ?? blockY2
234
+ if (Math.abs(newX2 - newX1) < 10 || Math.abs(newY2 - newY1) < 10) return
235
+
236
+ drawBlocksInstance.updateBlock(activeBlock.value.id, updates)
237
+ }
238
+
239
+ /** 处理 resize 结束 */
240
+ function handleResizeEnd() {
241
+ resizeStartState = null
242
+ document.removeEventListener('pointermove', handleResizeMove)
243
+ document.removeEventListener('pointerup', handleResizeEnd)
244
+ }
245
+
246
+ /** 更新控制点和相机状态 */
247
+ function updateState() {
248
+ if (!drawBlocksInstance) return
249
+
250
+ // 更新相机状态
251
+ const cam = drawBlocksInstance.getCamera()
252
+ camera.value = cam.getState()
253
+
254
+ // 更新选中块
255
+ const blockManager = drawBlocksInstance.getBlockManager()
256
+ const allBlocks = blockManager.getAllBlocks()
257
+ const active = allBlocks.find(b => b.active)
258
+ activeBlock.value = active || null
259
+
260
+ // 更新容器尺寸
261
+ if (containerRef.value) {
262
+ const rect = containerRef.value.getBoundingClientRect()
263
+ containerSize.width = rect.width
264
+ containerSize.height = rect.height
265
+ }
266
+ }
267
+
268
+ let animationFrameId: number | null = null
269
+ function startUpdateLoop() {
270
+ const loop = () => {
271
+ updateState()
272
+ animationFrameId = requestAnimationFrame(loop)
273
+ }
274
+ loop()
275
+ }
276
+
277
+ function stopUpdateLoop() {
278
+ if (animationFrameId !== null) {
279
+ cancelAnimationFrame(animationFrameId)
280
+ animationFrameId = null
281
+ }
282
+ }
283
+
284
+ onMounted(() => {
285
+ if (!containerRef.value) return
286
+
287
+ const options: DrawBlocksOptions = {
288
+ defaultScale: props.defaultScale,
289
+ minScale: props.minScale,
290
+ maxScale: props.maxScale,
291
+ bounceFactor: props.bounceFactor,
292
+ boundPadding: props.boundPadding,
293
+ renderOnChange: props.renderOnChange,
294
+ initialData: props.initialData
295
+ }
296
+
297
+ drawBlocksInstance = new DrawBlocks(containerRef.value, options)
298
+
299
+ emit('mounted', drawBlocksInstance)
300
+ emit('ready')
301
+
302
+ // 监听 DrawBlocks 内部事件
303
+ drawBlocksInstance.on('blockUpdate', () => updateState())
304
+ drawBlocksInstance.on('blockDelete', () => updateState())
305
+ drawBlocksInstance.on('blockClick', () => updateState())
306
+
307
+ // 监听窗口尺寸变化
308
+ window.addEventListener('resize', updateState)
309
+
310
+ // 启动更新循环
311
+ startUpdateLoop()
312
+ })
313
+
314
+ onUnmounted(() => {
315
+ stopUpdateLoop()
316
+ handleResizeEnd()
317
+ window.removeEventListener('resize', updateState)
318
+ drawBlocksInstance?.destroy()
319
+ drawBlocksInstance = null
320
+ })
321
+
322
+ /** 暴露方法给父组件 */
323
+ defineExpose({
324
+ /** 获取 DrawBlocks 实例 */
325
+ getInstance: () => drawBlocksInstance,
326
+ /** 获取数据 */
327
+ getData: () => drawBlocksInstance?.getData(),
328
+ /** 设置相机 */
329
+ setCamera: (x: number, y: number, scale: number) => drawBlocksInstance?.setCamera(x, y, scale),
330
+ /** 渲染 */
331
+ render: () => drawBlocksInstance?.render(),
332
+ /** 强制渲染 */
333
+ forceRender: () => drawBlocksInstance?.forceRender(),
334
+ /** 添加图片 */
335
+ addImage: (image: Image) => drawBlocksInstance?.addImage(image),
336
+ /** 删除图片 */
337
+ removeImage: (imageId: string) => drawBlocksInstance?.removeImage(imageId),
338
+ /** 添加画块 */
339
+ addBlock: (block: Block) => drawBlocksInstance?.addBlock(block),
340
+ /** 删除画块 */
341
+ removeBlock: (blockId: string) => drawBlocksInstance?.removeBlock(blockId),
342
+ /** 更新画块 */
343
+ updateBlock: (blockId: string, updates: Partial<Block>) => drawBlocksInstance?.updateBlock(blockId, updates),
344
+ /** 获取画块 */
345
+ getBlock: (blockId: string) => drawBlocksInstance?.getBlock(blockId),
346
+ /** 获取所有画块 */
347
+ getAllBlocks: () => drawBlocksInstance?.getAllBlocks(),
348
+ /** 设置模式 */
349
+ setMode: (mode: 'pan' | 'draw' | 'block') => drawBlocksInstance?.setMode(mode),
350
+ /** 设置数据 */
351
+ setData: (data: { images?: Image[]; blocks?: Block[] }) => drawBlocksInstance?.setData(data),
352
+ /** 事件绑定 */
353
+ on: (event: string, handler: (...args: any[]) => void) => drawBlocksInstance?.on(event, handler),
354
+ /** 事件解绑 */
355
+ off: (event: string, handler: (...args: any[]) => void) => drawBlocksInstance?.off(event, handler),
356
+ /** 获取相机实例 */
357
+ getCamera: () => drawBlocksInstance?.getCamera(),
358
+ /** 获取渲染器实例 */
359
+ getRenderer: () => drawBlocksInstance?.getRenderer(),
360
+ /** 获取图片管理器实例 */
361
+ getImageManager: () => drawBlocksInstance?.getImageManager(),
362
+ /** 获取画块管理器实例 */
363
+ getBlockManager: () => drawBlocksInstance?.getBlockManager()
364
+ })
365
+ </script>
366
+
367
+ <style scoped>
368
+ .draw-blocks-container {
369
+ width: 100%;
370
+ position: relative;
371
+ overflow: hidden;
372
+ touch-action: none;
373
+ }
374
+
375
+ .draw-blocks-container :deep(canvas) {
376
+ display: block;
377
+ touch-action: none;
378
+ }
379
+
380
+ .control-point {
381
+ position: absolute;
382
+ background: #fff;
383
+ border: 1px solid #333;
384
+ box-sizing: border-box;
385
+ pointer-events: auto;
386
+ z-index: 10;
387
+ }
388
+
389
+ .control-point.nw,
390
+ .control-point.se {
391
+ cursor: nwse-resize;
392
+ }
393
+
394
+ .control-point.ne,
395
+ .control-point.sw {
396
+ cursor: nesw-resize;
397
+ }
398
+
399
+ .control-point.tc,
400
+ .control-point.bc {
401
+ cursor: ns-resize;
402
+ }
403
+
404
+ .control-point.lc,
405
+ .control-point.rc {
406
+ cursor: ew-resize;
407
+ }
408
+
409
+ .control-point:hover {
410
+ background: #4ecdc4;
411
+ border-color: #333;
412
+ }
413
+ </style>