foliko 1.0.87 → 1.1.0

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 (104) hide show
  1. package/.agent/data/default.json +3 -108
  2. package/.agent/data/plugins-state.json +34 -1
  3. package/.agent/mcp_config.json +0 -1
  4. package/.agent/memory/core.md +1 -0
  5. package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
  6. package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
  7. package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
  8. package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
  9. package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
  10. package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
  11. package/.agent/plugins/poster-plugin/README.md +304 -0
  12. package/.agent/plugins/poster-plugin/fonts/PatuaOne-Regular.ttf +0 -0
  13. package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221.ttf +0 -0
  14. package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221/347/262/227/344/275/223.ttf +0 -0
  15. package/.agent/plugins/poster-plugin/index.js +13 -0
  16. package/.agent/plugins/poster-plugin/package.json +28 -0
  17. package/.agent/plugins/poster-plugin/src/canvas.js +161 -0
  18. package/.agent/plugins/poster-plugin/src/components/arrow.js +84 -0
  19. package/.agent/plugins/poster-plugin/src/components/avatar.js +71 -0
  20. package/.agent/plugins/poster-plugin/src/components/badge.js +85 -0
  21. package/.agent/plugins/poster-plugin/src/components/card.js +88 -0
  22. package/.agent/plugins/poster-plugin/src/components/chart.js +127 -0
  23. package/.agent/plugins/poster-plugin/src/components/chip.js +88 -0
  24. package/.agent/plugins/poster-plugin/src/components/columns.js +107 -0
  25. package/.agent/plugins/poster-plugin/src/components/cta.js +85 -0
  26. package/.agent/plugins/poster-plugin/src/components/divider.js +55 -0
  27. package/.agent/plugins/poster-plugin/src/components/feature.js +85 -0
  28. package/.agent/plugins/poster-plugin/src/components/featureGrid.js +112 -0
  29. package/.agent/plugins/poster-plugin/src/components/grid.js +118 -0
  30. package/.agent/plugins/poster-plugin/src/components/imageFrame.js +155 -0
  31. package/.agent/plugins/poster-plugin/src/components/index.js +62 -0
  32. package/.agent/plugins/poster-plugin/src/components/listItem.js +146 -0
  33. package/.agent/plugins/poster-plugin/src/components/notification.js +123 -0
  34. package/.agent/plugins/poster-plugin/src/components/progress.js +79 -0
  35. package/.agent/plugins/poster-plugin/src/components/progressCircle.js +117 -0
  36. package/.agent/plugins/poster-plugin/src/components/quote.js +97 -0
  37. package/.agent/plugins/poster-plugin/src/components/rating.js +85 -0
  38. package/.agent/plugins/poster-plugin/src/components/star.js +70 -0
  39. package/.agent/plugins/poster-plugin/src/components/statCard.js +105 -0
  40. package/.agent/plugins/poster-plugin/src/components/stepper.js +118 -0
  41. package/.agent/plugins/poster-plugin/src/components/table.js +159 -0
  42. package/.agent/plugins/poster-plugin/src/components/tagCloud.js +78 -0
  43. package/.agent/plugins/poster-plugin/src/components/timeline.js +105 -0
  44. package/.agent/plugins/poster-plugin/src/components/watermark.js +52 -0
  45. package/.agent/plugins/poster-plugin/src/composer.js +1904 -0
  46. package/.agent/plugins/poster-plugin/src/elements/artText.js +60 -0
  47. package/.agent/plugins/poster-plugin/src/elements/background.js +52 -0
  48. package/.agent/plugins/poster-plugin/src/elements/circle.js +31 -0
  49. package/.agent/plugins/poster-plugin/src/elements/image.js +71 -0
  50. package/.agent/plugins/poster-plugin/src/elements/index.js +26 -0
  51. package/.agent/plugins/poster-plugin/src/elements/line.js +23 -0
  52. package/.agent/plugins/poster-plugin/src/elements/polygon.js +32 -0
  53. package/.agent/plugins/poster-plugin/src/elements/rectangle.js +32 -0
  54. package/.agent/plugins/poster-plugin/src/elements/svg.js +92 -0
  55. package/.agent/plugins/poster-plugin/src/elements/text.js +38 -0
  56. package/.agent/plugins/poster-plugin/src/fonts.js +118 -0
  57. package/.agent/plugins/poster-plugin/src/index.js +1659 -0
  58. package/.agent/plugins/poster-plugin/src/presets.js +36 -0
  59. package/.agent/plugins/poster-plugin/src/templates/business.js +60 -0
  60. package/.agent/plugins/poster-plugin/src/templates/gradient.js +64 -0
  61. package/.agent/plugins/poster-plugin/src/templates/index.js +43 -0
  62. package/.agent/plugins/poster-plugin/src/templates/modern.js +69 -0
  63. package/.agent/plugins/poster-plugin/src/templates/simple.js +58 -0
  64. package/.agent/plugins/poster-plugin/src/templates/social.js +62 -0
  65. package/.agent/plugins/poster-plugin/src/templates/tech.js +84 -0
  66. package/.agent/sessions/cli_default.json +24265 -0
  67. package/.agent/weixin.json +6 -0
  68. package/.claude/settings.local.json +5 -8
  69. package/CLAUDE.md +144 -108
  70. package/docs/CONTEXT_DESIGN.md +1596 -0
  71. package/examples/test-concurrent-chat.js +60 -60
  72. package/output/beef-love-poster.png +0 -0
  73. package/package.json +2 -2
  74. package/plugins/default-plugins.js +2 -1
  75. package/plugins/extension-executor-plugin.js +11 -0
  76. package/plugins/memory-plugin.js +984 -0
  77. package/plugins/session-plugin.js +57 -1
  78. package/plugins/weixin-plugin.js +24 -22
  79. package/skills/poster-guide/SKILL.md +743 -0
  80. package/skills/python-plugin-dev/SKILL.md +238 -238
  81. package/skills/skill-guide/SKILL.md +130 -108
  82. package/src/capabilities/skill-manager.js +99 -0
  83. package/src/core/agent-chat.js +538 -138
  84. package/src/core/agent-context.js +188 -0
  85. package/src/core/agent.js +6 -2
  86. package/src/core/context-manager.js +283 -0
  87. package/src/core/framework.js +264 -3
  88. package/src/core/plugin-manager.js +79 -2
  89. package/src/core/request-context.js +98 -0
  90. package/src/core/session-context.js +341 -0
  91. package/src/core/session-storage.js +274 -0
  92. package/src/executors/mcp-executor.js +2 -2
  93. package/src/utils/index.js +239 -67
  94. package/src/utils/plugin-helpers.js +17 -0
  95. package//346/265/267/346/212/245/346/217/222/344/273/266.md +621 -0
  96. package/.agent/plugins/__pycache__/test_plugin.cpython-312.pyc +0 -0
  97. package/.agent/plugins/temp-repo/LICENSE +0 -201
  98. package/.agent/plugins/temp-repo/puppeteer-plugin/README.md +0 -147
  99. package/.agent/plugins/temp-repo/puppeteer-plugin/index.js +0 -1418
  100. package/.agent/plugins/temp-repo/puppeteer-plugin/package.json +0 -9
  101. package/.agent/plugins/test_plugin.py +0 -304
  102. package/examples/test-chat-debug.js +0 -102
  103. package/examples/test-chat-result.js +0 -76
  104. package/examples/test-chat-stream-diff.js +0 -63
@@ -0,0 +1,1904 @@
1
+ /**
2
+ * 海报组件化生成器
3
+ *
4
+ * 支持 JSON 配置驱动,一次调用生成完整海报
5
+ */
6
+
7
+ const paper = require('paper')
8
+
9
+ /**
10
+ * 辅助函数:将元素添加到活跃层
11
+ */
12
+ function addToLayer(project, element) {
13
+ if (project && project.activeLayer && element) {
14
+ if (element.insert) {
15
+ project.activeLayer.addChild(element)
16
+ }
17
+ }
18
+ }
19
+
20
+ /**
21
+ * 辅助函数:批量添加元素到活跃层
22
+ */
23
+ function addAllToLayer(project, elements) {
24
+ if (project && project.activeLayer && elements) {
25
+ elements.forEach(el => {
26
+ if (el.id && el.type) {
27
+ const item = project.getItem({ id: el.id })
28
+ if (item) project.activeLayer.addChild(item)
29
+ }
30
+ })
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 组件类型注册表
36
+ */
37
+ const COMPONENT_TYPES = {
38
+ // 基础元素
39
+ background: 'background',
40
+ rectangle: 'rectangle',
41
+ circle: 'circle',
42
+ line: 'line',
43
+ polygon: 'polygon',
44
+ text: 'text',
45
+ artText: 'artText',
46
+ image: 'image',
47
+ svg: 'svg',
48
+ imageFrame: 'imageFrame',
49
+ // 布局组件
50
+ columns: 'columns',
51
+ grid: 'grid',
52
+ // 装饰组件
53
+ star: 'star',
54
+ arrow: 'arrow',
55
+ progressCircle: 'progressCircle',
56
+ chip: 'chip',
57
+ chart: 'chart',
58
+ watermark: 'watermark',
59
+ table: 'table',
60
+ // 高级组件
61
+ card: 'card',
62
+ badge: 'badge',
63
+ cta: 'cta',
64
+ feature: 'feature',
65
+ featureGrid: 'featureGrid',
66
+ divider: 'divider',
67
+ avatar: 'avatar',
68
+ progress: 'progress',
69
+ rating: 'rating',
70
+ quote: 'quote',
71
+ statCard: 'statCard',
72
+ tagCloud: 'tagCloud',
73
+ stepper: 'stepper',
74
+ timeline: 'timeline',
75
+ listItem: 'listItem',
76
+ notification: 'notification',
77
+ }
78
+
79
+ /**
80
+ * 从配置创建海报
81
+ *
82
+ * @param {Object} project - Paper.js 项目
83
+ * @param {Object} canvas - 画布对象
84
+ * @param {Object} config - 海报配置
85
+ * @returns {Object} 创建结果
86
+ */
87
+ async function createFromConfig(project, canvas, config) {
88
+ const { components = [] } = config
89
+ const results = []
90
+
91
+ for (const component of components) {
92
+ const result = await createComponent(project, canvas, component)
93
+ results.push(result)
94
+ }
95
+
96
+ // 将所有创建的元素添加到活动层
97
+ if (project && project.activeLayer) {
98
+ project.activeLayer.addChildren(project.layers.flatMap(l => l.children))
99
+ }
100
+
101
+ return {
102
+ success: true,
103
+ componentCount: results.length,
104
+ results,
105
+ }
106
+ }
107
+
108
+ /**
109
+ * 根据配置创建单个组件
110
+ */
111
+ function createComponent(project, canvas, config) {
112
+ const { type, ...args } = config
113
+
114
+ switch (type) {
115
+ // 基础元素
116
+ case 'background':
117
+ return createBackgroundElement(project, canvas, args)
118
+ case 'rectangle':
119
+ return createRectangleElement(project, args)
120
+ case 'circle':
121
+ return createCircleElement(project, args)
122
+ case 'line':
123
+ return createLineElement(project, args)
124
+ case 'polygon':
125
+ return createPolygonElement(project, args)
126
+ case 'text':
127
+ return createTextElement(project, args)
128
+ case 'artText':
129
+ return createArtTextElement(project, args)
130
+ case 'image':
131
+ return createImageElement(project, args)
132
+ case 'svg':
133
+ return createSVGElement(project, args)
134
+ case 'imageFrame':
135
+ return createImageFrameComponent(project, canvas, args)
136
+ case 'columns':
137
+ return createColumnsComponent(project, canvas, args)
138
+ case 'grid':
139
+ return createGridComponent(project, canvas, args)
140
+
141
+ // 装饰组件
142
+ case 'star':
143
+ return createStarComponent(project, canvas, args)
144
+ case 'arrow':
145
+ return createArrowComponent(project, canvas, args)
146
+ case 'progressCircle':
147
+ return createProgressCircleComponent(project, canvas, args)
148
+ case 'chip':
149
+ return createChipComponent(project, canvas, args)
150
+ case 'chart':
151
+ return createChartComponent(project, canvas, args)
152
+ case 'watermark':
153
+ return createWatermarkComponent(project, canvas, args)
154
+ case 'table':
155
+ return createTableComponent(project, canvas, args)
156
+
157
+ // 高级组件
158
+ case 'card':
159
+ return createCardComponent(project, canvas, args)
160
+ case 'badge':
161
+ return createBadgeComponent(project, canvas, args)
162
+ case 'cta':
163
+ return createCTAComponent(project, canvas, args)
164
+ case 'feature':
165
+ return createFeatureComponent(project, canvas, args)
166
+ case 'featureGrid':
167
+ return createFeatureGridComponent(project, canvas, args)
168
+ case 'divider':
169
+ return createDividerComponent(project, canvas, args)
170
+ case 'avatar':
171
+ return createAvatarComponent(project, canvas, args)
172
+ case 'progress':
173
+ return createProgressComponent(project, canvas, args)
174
+ case 'rating':
175
+ return createRatingComponent(project, canvas, args)
176
+ case 'quote':
177
+ return createQuoteComponent(project, canvas, args)
178
+ case 'statCard':
179
+ return createStatCardComponent(project, canvas, args)
180
+ case 'tagCloud':
181
+ return createTagCloudComponent(project, canvas, args)
182
+ case 'stepper':
183
+ return createStepperComponent(project, canvas, args)
184
+ case 'timeline':
185
+ return createTimelineComponent(project, canvas, args)
186
+ case 'listItem':
187
+ return createListItemComponent(project, canvas, args)
188
+ case 'notification':
189
+ return createNotificationComponent(project, canvas, args)
190
+
191
+ default:
192
+ return { success: false, error: `Unknown component type: ${type}` }
193
+ }
194
+ }
195
+
196
+ // ============= 基础元素创建函数 =============
197
+
198
+ function createBackgroundElement(project, canvas, { color, gradient }) {
199
+ if (gradient) {
200
+ const paperColors = gradient.colors.map(c => new paper.Color(c))
201
+ const { type, direction } = gradient
202
+
203
+ if (type === 'linear') {
204
+ const angle = (direction || 45) * Math.PI / 180
205
+ const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2)
206
+ const centerX = canvas.width / 2
207
+ const centerY = canvas.height / 2
208
+ const start = new paper.Point(
209
+ centerX - Math.cos(angle) * diagonal / 2,
210
+ centerY - Math.sin(angle) * diagonal / 2
211
+ )
212
+ const stop = new paper.Point(
213
+ centerX + Math.cos(angle) * diagonal / 2,
214
+ centerY + Math.sin(angle) * diagonal / 2
215
+ )
216
+ project.activeLayer.fillColor = new paper.Color({
217
+ gradient: { stops: paperColors },
218
+ origin: start,
219
+ destination: stop,
220
+ })
221
+ } else {
222
+ const center = new paper.Point(canvas.width / 2, canvas.height / 2)
223
+ const radius = Math.max(canvas.width, canvas.height) / 2
224
+ project.activeLayer.fillColor = new paper.Color({
225
+ gradient: { stops: paperColors },
226
+ origin: center,
227
+ destination: center.add(new paper.Point(radius, 0)),
228
+ })
229
+ }
230
+ } else if (color) {
231
+ project.activeLayer.fillColor = new paper.Color(color)
232
+ }
233
+
234
+ return { success: true, type: 'background' }
235
+ }
236
+
237
+ function createRectangleElement(project, { x, y, width, height, fill, stroke, strokeWidth, radius, opacity }) {
238
+ const rect = new paper.Path.Rectangle({
239
+ point: [x, y],
240
+ size: [width, height],
241
+ radius: radius || 0,
242
+ })
243
+
244
+ if (fill) rect.fillColor = new paper.Color(fill)
245
+ if (stroke) {
246
+ rect.strokeColor = new paper.Color(stroke)
247
+ rect.strokeWidth = strokeWidth || 1
248
+ }
249
+ if (opacity !== undefined) rect.opacity = opacity
250
+
251
+ return { success: true, id: rect.id, type: 'rectangle' }
252
+ }
253
+
254
+ function createCircleElement(project, { cx, cy, rx, ry, fill, stroke, strokeWidth, opacity }) {
255
+ const circle = new paper.Path.Ellipse({
256
+ center: [cx, cy],
257
+ radius: [rx, ry || rx],
258
+ })
259
+
260
+ if (fill) circle.fillColor = new paper.Color(fill)
261
+ if (stroke) {
262
+ circle.strokeColor = new paper.Color(stroke)
263
+ circle.strokeWidth = strokeWidth || 1
264
+ }
265
+ if (opacity !== undefined) circle.opacity = opacity
266
+
267
+ return { success: true, id: circle.id, type: 'circle' }
268
+ }
269
+
270
+ function createLineElement(project, { x1, y1, x2, y2, stroke, strokeWidth }) {
271
+ const line = new paper.Path.Line({
272
+ from: [x1, y1],
273
+ to: [x2, y2],
274
+ strokeColor: new paper.Color(stroke || '#ffffff'),
275
+ strokeWidth: strokeWidth || 2,
276
+ })
277
+
278
+ return { success: true, id: line.id, type: 'line' }
279
+ }
280
+
281
+ function createPolygonElement(project, { cx, cy, radius, sides, fill, stroke, strokeWidth, opacity }) {
282
+ const polygon = new paper.Path.RegularPolygon({
283
+ center: [cx, cy],
284
+ radius: radius,
285
+ sides: sides,
286
+ })
287
+
288
+ if (fill) polygon.fillColor = new paper.Color(fill)
289
+ if (stroke) {
290
+ polygon.strokeColor = new paper.Color(stroke)
291
+ polygon.strokeWidth = strokeWidth || 1
292
+ }
293
+ if (opacity !== undefined) polygon.opacity = opacity
294
+
295
+ return { success: true, id: polygon.id, type: 'polygon' }
296
+ }
297
+
298
+ function createTextElement(project, { text, x, y, fontSize, fontFamily, color, align, shadow }) {
299
+ const { validateFont, getDefaultFont } = require('./fonts')
300
+
301
+ const textItem = new paper.PointText({
302
+ point: [x, y],
303
+ content: text,
304
+ fontSize: fontSize || 48,
305
+ fontFamily: validateFont(fontFamily) || getDefaultFont(),
306
+ fillColor: new paper.Color(color || '#ffffff'),
307
+ justification: align || 'left',
308
+ })
309
+
310
+ if (shadow) {
311
+ textItem.shadowColor = new paper.Color(shadow.color)
312
+ textItem.shadowBlur = shadow.blur || 5
313
+ textItem.shadowOffset = new paper.Point(shadow.offsetX || 2, shadow.offsetY || 2)
314
+ }
315
+
316
+ return { success: true, id: textItem.id, type: 'text' }
317
+ }
318
+
319
+ function createArtTextElement(project, { text, x, y, fontSize, fontFamily, gradient, strokeColor, strokeWidth, shadow }) {
320
+ const { validateFont, getDefaultFont } = require('./fonts')
321
+
322
+ const textItem = new paper.PointText({
323
+ point: [x, y],
324
+ content: text,
325
+ fontSize: fontSize || 120,
326
+ fontFamily: validateFont(fontFamily) || getDefaultFont(),
327
+ fillColor: gradient ? new paper.Color(gradient.colors[0]) : new paper.Color('#ffffff'),
328
+ justification: 'center',
329
+ })
330
+
331
+ if (gradient && gradient.colors.length > 0) {
332
+ const colors = gradient.colors.map(c => new paper.Color(c))
333
+ textItem.fillColor = new paper.Color({
334
+ gradient: { stops: colors },
335
+ origin: textItem.bounds.topLeft,
336
+ destination: textItem.bounds.topRight,
337
+ })
338
+ }
339
+
340
+ if (strokeColor) {
341
+ textItem.strokeColor = new paper.Color(strokeColor)
342
+ textItem.strokeWidth = strokeWidth || 2
343
+ }
344
+
345
+ if (shadow) {
346
+ textItem.shadowColor = new paper.Color(shadow.color)
347
+ textItem.shadowBlur = shadow.blur || 10
348
+ textItem.shadowOffset = new paper.Point(shadow.offsetX || 3, shadow.offsetY || 3)
349
+ }
350
+
351
+ return { success: true, id: textItem.id, type: 'artText' }
352
+ }
353
+
354
+ function createImageElement(project, { src, x, y, width, height, opacity }) {
355
+ const fs = require('fs')
356
+ const path = require('path')
357
+
358
+ // 将图片转换为 Base64 data URL
359
+ let imageUrl = src
360
+
361
+ // 如果是本地文件,转换为 Base64
362
+ if (!src.startsWith('data:') && !src.startsWith('http')) {
363
+ let absolutePath = src
364
+ if (!path.isAbsolute(absolutePath)) {
365
+ absolutePath = path.join(process.cwd(), absolutePath)
366
+ }
367
+
368
+ if (!fs.existsSync(absolutePath)) {
369
+ return { success: false, error: `文件不存在: ${absolutePath}` }
370
+ }
371
+
372
+ const buffer = fs.readFileSync(absolutePath)
373
+ const ext = path.extname(absolutePath).toLowerCase()
374
+ const mimeTypes = {
375
+ '.png': 'image/png',
376
+ '.jpg': 'image/jpeg',
377
+ '.jpeg': 'image/jpeg',
378
+ '.gif': 'image/gif',
379
+ '.webp': 'image/webp',
380
+ '.bmp': 'image/bmp'
381
+ }
382
+ const mimeType = mimeTypes[ext] || 'image/png'
383
+ imageUrl = `data:${mimeType};base64,${buffer.toString('base64')}`
384
+ }
385
+
386
+ const raster = new paper.Raster(imageUrl)
387
+
388
+ raster.onLoad = () => {
389
+ if (width && height) {
390
+ raster.bounds = new paper.Rectangle(x, y, width, height)
391
+ } else if (width) {
392
+ const scale = width / raster.width
393
+ raster.bounds = new paper.Rectangle(x, y, width, raster.height * scale)
394
+ } else if (height) {
395
+ const scale = height / raster.height
396
+ raster.bounds = new paper.Rectangle(x, y, raster.width * scale, height)
397
+ } else {
398
+ raster.position = new paper.Point(x, y)
399
+ }
400
+
401
+ if (opacity !== undefined) raster.opacity = opacity
402
+ }
403
+
404
+ return {
405
+ success: true,
406
+ id: raster.id,
407
+ type: 'image',
408
+ width: raster.width,
409
+ height: raster.height,
410
+ }
411
+ }
412
+
413
+ const fs = require('fs')
414
+
415
+ function createSVGElement(project, { src, x, y, width, height, opacity }) {
416
+ let svgContent = src
417
+
418
+ // 如果是文件路径,读取文件内容
419
+ if (!src.startsWith('<') && !src.startsWith('<?xml')) {
420
+ try {
421
+ svgContent = fs.readFileSync(src, 'utf8')
422
+ } catch (e) {
423
+ return { success: false, error: `Failed to read SVG file: ${e.message}` }
424
+ }
425
+ }
426
+
427
+ // 导入 SVG
428
+ const svg = project.importSVG(svgContent)
429
+
430
+ if (!svg) {
431
+ return { success: false, error: 'Failed to import SVG' }
432
+ }
433
+
434
+ // 设置位置
435
+ svg.position = new paper.Point(x, y)
436
+
437
+ // 设置尺寸
438
+ if (width && height) {
439
+ svg.bounds.width = width
440
+ svg.bounds.height = height
441
+ } else if (width) {
442
+ svg.scale(width / svg.bounds.width)
443
+ } else if (height) {
444
+ svg.scale(height / svg.bounds.height)
445
+ }
446
+
447
+ // 设置透明度
448
+ if (opacity !== undefined) {
449
+ svg.opacity = opacity
450
+ }
451
+
452
+ return {
453
+ success: true,
454
+ id: svg.id,
455
+ type: 'svg',
456
+ width: svg.bounds.width,
457
+ height: svg.bounds.height,
458
+ }
459
+ }
460
+
461
+ // ============= 高级组件创建函数 =============
462
+
463
+ function createCardComponent(project, canvas, {
464
+ x, y, width, height,
465
+ background, border, borderWidth, radius,
466
+ title, titleSize, titleColor,
467
+ subtitle, subtitleSize, subtitleColor,
468
+ padding = 20,
469
+ }) {
470
+ const elements = []
471
+
472
+ // 卡片背景
473
+ const card = new paper.Path.Rectangle({
474
+ point: [x, y],
475
+ size: [width, height],
476
+ radius: radius || 0,
477
+ })
478
+ card.fillColor = new paper.Color(background || '#ffffff')
479
+ if (border) {
480
+ card.strokeColor = new paper.Color(border)
481
+ card.strokeWidth = borderWidth || 1
482
+ }
483
+ elements.push({ type: 'rectangle', id: card.id })
484
+
485
+ // 标题
486
+ if (title) {
487
+ const titleText = new paper.PointText({
488
+ point: [x + padding, y + padding + (titleSize || 24)],
489
+ content: title,
490
+ fontSize: titleSize || 24,
491
+ fillColor: new paper.Color(titleColor || '#000000'),
492
+ justification: 'left',
493
+ })
494
+ elements.push({ type: 'text', id: titleText.id })
495
+ }
496
+
497
+ // 副标题
498
+ if (subtitle) {
499
+ const subY = title ? y + padding + (titleSize || 24) + (subtitleSize || 16) + 10 : y + padding
500
+ const subtitleText = new paper.PointText({
501
+ point: [x + padding, subY],
502
+ content: subtitle,
503
+ fontSize: subtitleSize || 16,
504
+ fillColor: new paper.Color(subtitleColor || '#666666'),
505
+ justification: 'left',
506
+ })
507
+ elements.push({ type: 'text', id: subtitleText.id })
508
+ }
509
+
510
+ return { success: true, elements, type: 'card' }
511
+ }
512
+
513
+ function createBadgeComponent(project, canvas, {
514
+ x, y, text,
515
+ background = '#007bff', color = '#ffffff',
516
+ border, fontSize = 18, padding = 15, radius = 4,
517
+ }) {
518
+ const textWidth = text.length * fontSize * 0.6
519
+ const badgeWidth = textWidth + padding * 2
520
+ const badgeHeight = fontSize + padding * 2
521
+ const badgeX = x - badgeWidth / 2
522
+
523
+ const badge = new paper.Path.Rectangle({
524
+ point: [badgeX, y],
525
+ size: [badgeWidth, badgeHeight],
526
+ radius: radius,
527
+ })
528
+ badge.fillColor = new paper.Color(background)
529
+ if (border) badge.strokeColor = new paper.Color(border)
530
+
531
+ const badgeText = new paper.PointText({
532
+ point: [x, y + badgeHeight / 2 + fontSize / 3],
533
+ content: text,
534
+ fontSize: fontSize,
535
+ fillColor: new paper.Color(color),
536
+ justification: 'center',
537
+ })
538
+
539
+ return { success: true, elements: [{ type: 'rectangle', id: badge.id }, { type: 'text', id: badgeText.id }], type: 'badge' }
540
+ }
541
+
542
+ function createCTAComponent(project, canvas, {
543
+ x, y, text,
544
+ background = '#007bff', color = '#ffffff',
545
+ border, fontSize = 20, padding = 25, radius = 8, shadow,
546
+ }) {
547
+ const textWidth = text.length * fontSize * 0.7
548
+ const btnWidth = textWidth + padding * 2
549
+ const btnHeight = fontSize + padding * 2
550
+ const btnX = x - btnWidth / 2
551
+
552
+ const button = new paper.Path.Rectangle({
553
+ point: [btnX, y],
554
+ size: [btnWidth, btnHeight],
555
+ radius: radius,
556
+ })
557
+ button.fillColor = new paper.Color(background)
558
+ if (border) button.strokeColor = new paper.Color(border)
559
+
560
+ if (shadow) {
561
+ button.shadowColor = new paper.Color(shadow.color || 'rgba(0,0,0,0.3)')
562
+ button.shadowBlur = shadow.blur || 10
563
+ button.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
564
+ }
565
+
566
+ const buttonText = new paper.PointText({
567
+ point: [x, y + btnHeight / 2 + fontSize / 3],
568
+ content: text,
569
+ fontSize: fontSize,
570
+ fillColor: new paper.Color(color),
571
+ justification: 'center',
572
+ })
573
+
574
+ return { success: true, elements: [{ type: 'rectangle', id: button.id }, { type: 'text', id: buttonText.id }], type: 'cta' }
575
+ }
576
+
577
+ function createFeatureComponent(project, canvas, {
578
+ x, y, width,
579
+ icon, title, description,
580
+ iconColor = '#007bff', titleColor = '#ffffff', descColor = '#aaaaaa',
581
+ iconSize = 32, titleSize = 20, descSize = 14,
582
+ }) {
583
+ const elements = []
584
+ const padding = 15
585
+ let currentY = y
586
+
587
+ if (icon) {
588
+ elements.push(new paper.PointText({
589
+ point: [x + padding, currentY + iconSize],
590
+ content: icon,
591
+ fontSize: iconSize,
592
+ fillColor: new paper.Color(iconColor),
593
+ justification: 'left',
594
+ }))
595
+ currentY += iconSize + 5
596
+ }
597
+
598
+ if (title) {
599
+ elements.push(new paper.PointText({
600
+ point: [x + padding, currentY + titleSize],
601
+ content: title,
602
+ fontSize: titleSize,
603
+ fillColor: new paper.Color(titleColor),
604
+ justification: 'left',
605
+ }))
606
+ currentY += titleSize + 5
607
+ }
608
+
609
+ if (description) {
610
+ elements.push(new paper.PointText({
611
+ point: [x + padding, currentY + descSize],
612
+ content: description,
613
+ fontSize: descSize,
614
+ fillColor: new paper.Color(descColor),
615
+ justification: 'left',
616
+ }))
617
+ }
618
+
619
+ return { success: true, elements, type: 'feature' }
620
+ }
621
+
622
+ function createFeatureGridComponent(project, canvas, {
623
+ x, y,
624
+ columns = 3, itemWidth = 200, itemHeight = 120, gap = 20,
625
+ items = [],
626
+ background = '#1a1a2e', borderColor = '#00d9ff', radius = 8,
627
+ }) {
628
+ const elements = []
629
+
630
+ for (let i = 0; i < items.length; i++) {
631
+ const item = items[i]
632
+ const col = i % columns
633
+ const row = Math.floor(i / columns)
634
+ const itemX = x + col * (itemWidth + gap)
635
+ const itemY = y + row * (itemHeight + gap)
636
+
637
+ const bg = new paper.Path.Rectangle({
638
+ point: [itemX, itemY],
639
+ size: [itemWidth, itemHeight],
640
+ radius: radius,
641
+ })
642
+ bg.fillColor = new paper.Color(background)
643
+ bg.strokeColor = new paper.Color(borderColor)
644
+ bg.strokeWidth = 0.5
645
+ bg.opacity = 0.8
646
+ elements.push(bg)
647
+
648
+ const padding = 15
649
+ let offsetY = itemY + padding
650
+
651
+ if (item.icon) {
652
+ elements.push(new paper.PointText({
653
+ point: [itemX + padding, offsetY + 24],
654
+ content: item.icon,
655
+ fontSize: 28,
656
+ fillColor: new paper.Color(item.iconColor || '#00ff88'),
657
+ justification: 'left',
658
+ }))
659
+ offsetY += 35
660
+ }
661
+
662
+ if (item.title) {
663
+ elements.push(new paper.PointText({
664
+ point: [itemX + padding, offsetY + 18],
665
+ content: item.title,
666
+ fontSize: 16,
667
+ fillColor: new paper.Color(item.titleColor || '#ffffff'),
668
+ justification: 'left',
669
+ }))
670
+ offsetY += 22
671
+ }
672
+
673
+ if (item.description) {
674
+ elements.push(new paper.PointText({
675
+ point: [itemX + padding, offsetY + 14],
676
+ content: item.description,
677
+ fontSize: 12,
678
+ fillColor: new paper.Color(item.descColor || '#888888'),
679
+ justification: 'left',
680
+ }))
681
+ }
682
+ }
683
+
684
+ return {
685
+ success: true,
686
+ elements,
687
+ type: 'featureGrid',
688
+ rows: Math.ceil(items.length / columns),
689
+ cols: columns,
690
+ }
691
+ }
692
+
693
+ function createDividerComponent(project, canvas, {
694
+ x, y, width, color = '#00d9ff', thickness = 1, style = 'solid', align = 'center',
695
+ }) {
696
+ let startX = x
697
+ let endX = x + width
698
+
699
+ if (align === 'center') {
700
+ startX = x - width / 2
701
+ endX = x + width / 2
702
+ } else if (align === 'right') {
703
+ startX = x - width
704
+ endX = x
705
+ }
706
+
707
+ const line = new paper.Path.Line({
708
+ from: [startX, y],
709
+ to: [endX, y],
710
+ strokeColor: new paper.Color(color),
711
+ strokeWidth: thickness,
712
+ })
713
+
714
+ if (style === 'dashed') line.dashArray = [10, 5]
715
+
716
+ return { success: true, id: line.id, type: 'divider' }
717
+ }
718
+
719
+ module.exports = {
720
+ createFromConfig,
721
+ createComponent,
722
+ COMPONENT_TYPES,
723
+ }
724
+
725
+
726
+ // ============= 新增组件创建函数 =============
727
+
728
+ function createAvatarComponent(project, canvas, { x, y, size = 80, initials, background = '#6366f1', border, borderWidth = 0, color = '#ffffff' }) {
729
+ const elements = []
730
+ const radius = size / 2
731
+
732
+ const circle = new paper.Path.Circle({
733
+ center: [x, y],
734
+ radius: radius,
735
+ })
736
+ circle.fillColor = new paper.Color(background)
737
+ if (border) {
738
+ circle.strokeColor = new paper.Color(border)
739
+ circle.strokeWidth = borderWidth
740
+ }
741
+ elements.push({ type: 'circle', id: circle.id })
742
+
743
+ if (initials) {
744
+ const text = new paper.PointText({
745
+ point: [x, y + size / 6],
746
+ content: initials.charAt(0).toUpperCase(),
747
+ fontSize: size * 0.4,
748
+ fillColor: new paper.Color(color),
749
+ justification: 'center',
750
+ })
751
+ elements.push({ type: 'text', id: text.id })
752
+ }
753
+
754
+ return { success: true, elements, type: 'avatar', size }
755
+ }
756
+
757
+ function createProgressComponent(project, canvas, { x, y, width = 300, height = 20, value = 50, trackColor = '#e0e0e0', fillColor = '#6366f1', radius = 10, showLabel = false, label }) {
758
+ const elements = []
759
+
760
+ const track = new paper.Path.Rectangle({
761
+ point: [x, y],
762
+ size: [width, height],
763
+ radius: radius,
764
+ })
765
+ track.fillColor = new paper.Color(trackColor)
766
+ elements.push({ type: 'rectangle', id: track.id })
767
+
768
+ const progressWidth = (value / 100) * width
769
+ if (progressWidth > 0) {
770
+ const fill = new paper.Path.Rectangle({
771
+ point: [x, y],
772
+ size: [progressWidth, height],
773
+ radius: radius,
774
+ })
775
+ fill.fillColor = new paper.Color(fillColor)
776
+ elements.push({ type: 'rectangle', id: fill.id })
777
+ }
778
+
779
+ if (showLabel && label) {
780
+ const labelText = new paper.PointText({
781
+ point: [x + width / 2, y - 8],
782
+ content: label,
783
+ fontSize: 14,
784
+ fillColor: new paper.Color('#666666'),
785
+ justification: 'center',
786
+ })
787
+ elements.push({ type: 'text', id: labelText.id })
788
+ }
789
+
790
+ return { success: true, elements, value }
791
+ }
792
+
793
+ function createRatingComponent(project, canvas, { x, y, value = 4, max = 5, size = 24, filledColor = '#fbbf24', emptyColor = '#e5e7eb', gap = 4 }) {
794
+ const elements = []
795
+
796
+ for (let i = 0; i < max; i++) {
797
+ const starX = x + i * (size + gap)
798
+ const filled = i < Math.floor(value)
799
+
800
+ const star = new paper.Path.Star({
801
+ center: [starX + size / 2, y + size / 2],
802
+ points: 5,
803
+ radius1: size / 4,
804
+ radius2: size / 2,
805
+ })
806
+ star.fillColor = new paper.Color(filled ? filledColor : emptyColor)
807
+ elements.push({ type: 'polygon', id: star.id })
808
+ }
809
+
810
+ return { success: true, elements, value }
811
+ }
812
+
813
+ function createQuoteComponent(project, canvas, { x, y, width = 400, text, author, background = '#f8fafc', borderColor = '#6366f1', borderWidth = 4, padding = 20, radius = 8, textColor = '#1e293b', authorColor = '#64748b', fontSize = 18 }) {
814
+ const elements = []
815
+ const lineHeight = 22
816
+
817
+ const bg = new paper.Path.Rectangle({
818
+ point: [x, y],
819
+ size: [width, author ? 80 + fontSize * 2 : 40 + fontSize * 1.5],
820
+ radius: radius,
821
+ })
822
+ bg.fillColor = new paper.Color(background)
823
+ elements.push({ type: 'rectangle', id: bg.id })
824
+
825
+ const border = new paper.Path.Rectangle({
826
+ point: [x, y],
827
+ size: [borderWidth, author ? 80 + fontSize * 2 : 40 + fontSize * 1.5],
828
+ })
829
+ border.fillColor = new paper.Color(borderColor)
830
+ elements.push({ type: 'rectangle', id: border.id })
831
+
832
+ const quoteMark = new paper.PointText({
833
+ point: [x + padding + 10, y + padding + fontSize],
834
+ content: '"',
835
+ fontSize: fontSize * 2,
836
+ fillColor: new paper.Color(borderColor),
837
+ justification: 'left',
838
+ })
839
+ elements.push({ type: 'text', id: quoteMark.id })
840
+
841
+ const quoteText = new paper.PointText({
842
+ point: [x + padding + 30, y + padding + fontSize * 1.5],
843
+ content: text,
844
+ fontSize: fontSize,
845
+ fillColor: new paper.Color(textColor),
846
+ justification: 'left',
847
+ })
848
+ elements.push({ type: 'text', id: quoteText.id })
849
+
850
+ if (author) {
851
+ const authorText = new paper.PointText({
852
+ point: [x + padding, y + padding + fontSize * 2.5 + 10],
853
+ content: `— ${author}`,
854
+ fontSize: fontSize * 0.8,
855
+ fillColor: new paper.Color(authorColor),
856
+ justification: 'left',
857
+ })
858
+ elements.push({ type: 'text', id: authorText.id })
859
+ }
860
+
861
+ return { success: true, elements, type: 'quote' }
862
+ }
863
+
864
+ function createStatCardComponent(project, canvas, { x, y, width = 200, height = 120, label = 'Total', value = '0', change, positive = true, icon, iconColor = '#6366f1', background = '#ffffff', border = '#e5e7eb', radius = 12 }) {
865
+ const elements = []
866
+
867
+ const bg = new paper.Path.Rectangle({
868
+ point: [x, y],
869
+ size: [width, height],
870
+ radius: radius,
871
+ })
872
+ bg.fillColor = new paper.Color(background)
873
+ bg.strokeColor = new paper.Color(border)
874
+ bg.strokeWidth = 1
875
+ elements.push({ type: 'rectangle', id: bg.id })
876
+
877
+ if (icon) {
878
+ elements.push(new paper.PointText({
879
+ point: [x + 20, y + 35],
880
+ content: icon,
881
+ fontSize: 24,
882
+ fillColor: new paper.Color(iconColor),
883
+ justification: 'left',
884
+ }))
885
+ }
886
+
887
+ elements.push(new paper.PointText({
888
+ point: [x + 20, y + 50 + (icon ? 10 : 0)],
889
+ content: label,
890
+ fontSize: 14,
891
+ fillColor: new paper.Color('#64748b'),
892
+ justification: 'left',
893
+ }))
894
+
895
+ elements.push(new paper.PointText({
896
+ point: [x + 20, y + 75 + (icon ? 10 : 0)],
897
+ content: value,
898
+ fontSize: 28,
899
+ fillColor: new paper.Color('#1e293b'),
900
+ justification: 'left',
901
+ }))
902
+
903
+ if (change) {
904
+ const changeColor = positive ? '#22c55e' : '#ef4444'
905
+ const changeIcon = positive ? '↑' : '↓'
906
+ elements.push(new paper.PointText({
907
+ point: [x + 20, y + 95 + (icon ? 10 : 0)],
908
+ content: `${changeIcon} ${change}`,
909
+ fontSize: 14,
910
+ fillColor: new paper.Color(changeColor),
911
+ justification: 'left',
912
+ }))
913
+ }
914
+
915
+ return { success: true, elements, type: 'statCard' }
916
+ }
917
+
918
+ function createTagCloudComponent(project, canvas, { x, y, tags = [], fontSize = 14, padding = 12, gap = 10, maxWidth = 400 }) {
919
+ const elements = []
920
+ let currentX = x
921
+ let currentY = y
922
+ let rowHeight = 0
923
+
924
+ for (const tag of tags) {
925
+ const textWidth = tag.text.length * fontSize * 0.6
926
+ const tagWidth = textWidth + padding * 2
927
+ const tagHeight = fontSize + padding * 2
928
+
929
+ if (currentX + tagWidth > x + maxWidth && currentX > x) {
930
+ currentX = x
931
+ currentY += rowHeight + gap
932
+ rowHeight = 0
933
+ }
934
+
935
+ const tagBg = new paper.Path.Rectangle({
936
+ point: [currentX, currentY],
937
+ size: [tagWidth, tagHeight],
938
+ radius: tagHeight / 2,
939
+ })
940
+ tagBg.fillColor = new paper.Color(tag.bgColor || '#e0e7ff')
941
+ elements.push({ type: 'rectangle', id: tagBg.id })
942
+
943
+ elements.push(new paper.PointText({
944
+ point: [currentX + tagWidth / 2, currentY + tagHeight / 2 + fontSize / 3],
945
+ content: tag.text,
946
+ fontSize: fontSize,
947
+ fillColor: new paper.Color(tag.color || '#4338ca'),
948
+ justification: 'center',
949
+ }))
950
+
951
+ currentX += tagWidth + gap
952
+ rowHeight = Math.max(rowHeight, tagHeight)
953
+ }
954
+
955
+ return { success: true, elements, type: 'tagCloud', height: rowHeight }
956
+ }
957
+
958
+ function createStepperComponent(project, canvas, { x, y, width = 600, steps = [], currentStep = 0, activeColor = '#6366f1', inactiveColor = '#e5e7eb', completedColor = '#22c55e', circleSize = 40 }) {
959
+ const elements = []
960
+ const stepWidth = steps.length > 1 ? width / (steps.length - 1) : width
961
+ const lineY = y + circleSize / 2
962
+
963
+ if (steps.length > 1) {
964
+ elements.push({
965
+ type: 'line',
966
+ id: new paper.Path.Line({
967
+ from: [x + circleSize / 2, lineY],
968
+ to: [x + width - circleSize / 2, lineY],
969
+ strokeColor: new paper.Color(inactiveColor),
970
+ strokeWidth: 2,
971
+ }).id
972
+ })
973
+ }
974
+
975
+ for (let i = 0; i < steps.length; i++) {
976
+ const stepX = steps.length > 1 ? x + i * stepWidth : x
977
+ let color = inactiveColor
978
+ if (i < currentStep) color = completedColor
979
+ else if (i === currentStep) color = activeColor
980
+
981
+ const circle = new paper.Path.Circle({
982
+ center: [stepX + circleSize / 2, lineY],
983
+ radius: circleSize / 2,
984
+ })
985
+ circle.fillColor = new paper.Color(color)
986
+ elements.push({ type: 'circle', id: circle.id })
987
+
988
+ const icon = i < currentStep ? '✓' : String(i + 1)
989
+ elements.push(new paper.PointText({
990
+ point: [stepX + circleSize / 2, lineY + circleSize / 6],
991
+ content: icon,
992
+ fontSize: 16,
993
+ fillColor: new paper.Color('#ffffff'),
994
+ justification: 'center',
995
+ }))
996
+
997
+ elements.push(new paper.PointText({
998
+ point: [stepX + circleSize / 2, y + circleSize + 20],
999
+ content: steps[i].title || `Step ${i + 1}`,
1000
+ fontSize: 14,
1001
+ fillColor: new paper.Color(i <= currentStep ? '#1e293b' : '#94a3b8'),
1002
+ justification: 'center',
1003
+ }))
1004
+
1005
+ if (steps[i].description) {
1006
+ elements.push(new paper.PointText({
1007
+ point: [stepX + circleSize / 2, y + circleSize + 38],
1008
+ content: steps[i].description,
1009
+ fontSize: 11,
1010
+ fillColor: new paper.Color('#94a3b8'),
1011
+ justification: 'center',
1012
+ }))
1013
+ }
1014
+ }
1015
+
1016
+ return { success: true, elements, type: 'stepper' }
1017
+ }
1018
+
1019
+ function createTimelineComponent(project, canvas, { x, y, width = 500, items = [], lineColor = '#e2e8f0', dotColor = '#6366f1', dotSize = 16, gap = 60 }) {
1020
+ const elements = []
1021
+ const centerX = x + 80
1022
+ const contentX = x + 120
1023
+
1024
+ if (items.length > 1) {
1025
+ elements.push({
1026
+ type: 'line',
1027
+ id: new paper.Path.Line({
1028
+ from: [centerX, y + dotSize / 2],
1029
+ to: [centerX, y + (items.length - 1) * gap + dotSize / 2],
1030
+ strokeColor: new paper.Color(lineColor),
1031
+ strokeWidth: 2,
1032
+ }).id
1033
+ })
1034
+ }
1035
+
1036
+ for (let i = 0; i < items.length; i++) {
1037
+ const itemY = y + i * gap
1038
+ const isActive = items[i].active !== false
1039
+
1040
+ const dot = new paper.Path.Circle({
1041
+ center: [centerX, itemY + dotSize / 2],
1042
+ radius: dotSize / 2,
1043
+ })
1044
+ dot.fillColor = new paper.Color(isActive ? dotColor : lineColor)
1045
+ elements.push({ type: 'circle', id: dot.id })
1046
+
1047
+ if (items[i].date) {
1048
+ elements.push(new paper.PointText({
1049
+ point: [x + 10, itemY + dotSize / 2 + 5],
1050
+ content: items[i].date,
1051
+ fontSize: 12,
1052
+ fillColor: new paper.Color('#94a3b8'),
1053
+ justification: 'left',
1054
+ }))
1055
+ }
1056
+
1057
+ elements.push(new paper.PointText({
1058
+ point: [contentX, itemY + dotSize / 2 + 5],
1059
+ content: items[i].title || `Event ${i + 1}`,
1060
+ fontSize: 16,
1061
+ fillColor: new paper.Color(isActive ? '#1e293b' : '#94a3b8'),
1062
+ justification: 'left',
1063
+ }))
1064
+
1065
+ if (items[i].description) {
1066
+ elements.push(new paper.PointText({
1067
+ point: [contentX, itemY + dotSize / 2 + 28],
1068
+ content: items[i].description,
1069
+ fontSize: 13,
1070
+ fillColor: new paper.Color('#64748b'),
1071
+ justification: 'left',
1072
+ }))
1073
+ }
1074
+ }
1075
+
1076
+ return { success: true, elements, type: 'timeline', height: items.length * gap }
1077
+ }
1078
+
1079
+ function createListItemComponent(project, canvas, { x, y, width = 400, icon = '→', title, description, badge, badgeColor = '#6366f1', iconColor = '#6366f1', background = '#ffffff', borderColor = '#e5e7eb', height = 60, radius = 8 }) {
1080
+ const elements = []
1081
+
1082
+ const bg = new paper.Path.Rectangle({
1083
+ point: [x, y],
1084
+ size: [width, height],
1085
+ radius: radius,
1086
+ })
1087
+ bg.fillColor = new paper.Color(background)
1088
+ bg.strokeColor = new paper.Color(borderColor)
1089
+ bg.strokeWidth = 1
1090
+ elements.push({ type: 'rectangle', id: bg.id })
1091
+
1092
+ elements.push(new paper.PointText({
1093
+ point: [x + 15, y + height / 2 + 6],
1094
+ content: icon,
1095
+ fontSize: 20,
1096
+ fillColor: new paper.Color(iconColor),
1097
+ justification: 'center',
1098
+ }))
1099
+
1100
+ elements.push(new paper.PointText({
1101
+ point: [x + 50, y + height / 2 - 5],
1102
+ content: title || 'List Item',
1103
+ fontSize: 16,
1104
+ fillColor: new paper.Color('#1e293b'),
1105
+ justification: 'left',
1106
+ }))
1107
+
1108
+ if (description) {
1109
+ elements.push(new paper.PointText({
1110
+ point: [x + 50, y + height / 2 + 15],
1111
+ content: description,
1112
+ fontSize: 12,
1113
+ fillColor: new paper.Color('#64748b'),
1114
+ justification: 'left',
1115
+ }))
1116
+ }
1117
+
1118
+ if (badge) {
1119
+ const badgeWidth = badge.length * 10 + 20
1120
+ const badgeX = x + width - badgeWidth - 15
1121
+ const badgeY = y + (height - 24) / 2
1122
+
1123
+ elements.push({
1124
+ type: 'rectangle',
1125
+ id: new paper.Path.Rectangle({
1126
+ point: [badgeX, badgeY],
1127
+ size: [badgeWidth, 24],
1128
+ radius: 12,
1129
+ }).id
1130
+ })
1131
+
1132
+ elements.push(new paper.PointText({
1133
+ point: [badgeX + badgeWidth / 2, badgeY + 16],
1134
+ content: badge,
1135
+ fontSize: 12,
1136
+ fillColor: new paper.Color('#ffffff'),
1137
+ justification: 'center',
1138
+ }))
1139
+ }
1140
+
1141
+ return { success: true, elements, type: 'listItem' }
1142
+ }
1143
+
1144
+ function createNotificationComponent(project, canvas, { x, y, width = 360, type = 'info', title, message, showIcon = true, radius = 12 }) {
1145
+ const config = {
1146
+ success: { icon: '✓', bgColor: '#dcfce7', iconColor: '#22c55e', borderColor: '#22c55e' },
1147
+ warning: { icon: '⚠', bgColor: '#fef9c3', iconColor: '#eab308', borderColor: '#eab308' },
1148
+ error: { icon: '✕', bgColor: '#fee2e2', iconColor: '#ef4444', borderColor: '#ef4444' },
1149
+ info: { icon: 'ℹ', bgColor: '#dbeafe', iconColor: '#3b82f6', borderColor: '#3b82f6' },
1150
+ }
1151
+
1152
+ const c = config[type] || config.info
1153
+ const padding = 16
1154
+ const lineHeight = 22
1155
+ const iconSize = 24
1156
+ const height = padding * 2 + (title ? lineHeight + 8 : 0) + (message ? lineHeight : 0)
1157
+ const elements = []
1158
+
1159
+ const bg = new paper.Path.Rectangle({
1160
+ point: [x, y],
1161
+ size: [width, height],
1162
+ radius: radius,
1163
+ })
1164
+ bg.fillColor = new paper.Color(c.bgColor)
1165
+ bg.strokeColor = new paper.Color(c.borderColor)
1166
+ bg.strokeWidth = 1
1167
+ elements.push({ type: 'rectangle', id: bg.id })
1168
+
1169
+ if (showIcon) {
1170
+ elements.push(new paper.PointText({
1171
+ point: [x + padding + iconSize / 2, y + padding + iconSize / 2 + 6],
1172
+ content: c.icon,
1173
+ fontSize: iconSize,
1174
+ fillColor: new paper.Color(c.iconColor),
1175
+ justification: 'center',
1176
+ }))
1177
+ }
1178
+
1179
+ const textX = showIcon ? x + padding + iconSize + 12 : x + padding
1180
+ let currentY = y + padding
1181
+
1182
+ if (title) {
1183
+ elements.push(new paper.PointText({
1184
+ point: [textX, currentY + 18],
1185
+ content: title,
1186
+ fontSize: 16,
1187
+ fillColor: new paper.Color('#1e293b'),
1188
+ justification: 'left',
1189
+ }))
1190
+ currentY += lineHeight + 8
1191
+ }
1192
+
1193
+ if (message) {
1194
+ elements.push(new paper.PointText({
1195
+ point: [textX, currentY + 16],
1196
+ content: message,
1197
+ fontSize: 14,
1198
+ fillColor: new paper.Color('#475569'),
1199
+ justification: 'left',
1200
+ }))
1201
+ }
1202
+
1203
+ return { success: true, elements, type: 'notification' }
1204
+ }
1205
+
1206
+ /**
1207
+ * 创建图片框组件
1208
+ */
1209
+ async function createImageFrameComponent(project, canvas, {
1210
+ src,
1211
+ x,
1212
+ y,
1213
+ width,
1214
+ height,
1215
+ borderColor = '#ffffff',
1216
+ borderWidth = 3,
1217
+ outerColor = '#1a1a2e',
1218
+ outerWidth = 6,
1219
+ shadowBlur = 0,
1220
+ shadowOffsetX = 0,
1221
+ shadowOffsetY = 0,
1222
+ shadowColor = 'rgba(0,0,0,0.3)',
1223
+ radius = 0,
1224
+ overlayColor,
1225
+ overlayOpacity = 0,
1226
+ fit = 'cover'
1227
+ }) {
1228
+ const elements = []
1229
+
1230
+ // 绘制外边框(装饰层)
1231
+ if (outerWidth > 0) {
1232
+ const outerBg = new paper.Path.Rectangle({
1233
+ point: [x - outerWidth, y - outerWidth],
1234
+ size: [width + outerWidth * 2, height + outerWidth * 2],
1235
+ radius: radius + outerWidth
1236
+ })
1237
+ outerBg.fillColor = new paper.Color(outerColor)
1238
+ elements.push({ type: 'rectangle', id: outerBg.id })
1239
+ }
1240
+
1241
+ // 绘制内边框
1242
+ if (borderWidth > 0) {
1243
+ const innerBg = new paper.Path.Rectangle({
1244
+ point: [x - borderWidth, y - borderWidth],
1245
+ size: [width + borderWidth * 2, height + borderWidth * 2],
1246
+ radius: radius + borderWidth
1247
+ })
1248
+ innerBg.fillColor = new paper.Color(borderColor)
1249
+ elements.push({ type: 'rectangle', id: innerBg.id })
1250
+ }
1251
+
1252
+ // 加载并绘制图片
1253
+ const image = await _loadImage(src)
1254
+ const imgWidth = image.width
1255
+ const imgHeight = image.height
1256
+ const imgRatio = imgWidth / imgHeight
1257
+ const boxRatio = width / height
1258
+
1259
+ let drawX = x, drawY = y, drawW = width, drawH = height
1260
+
1261
+ if (fit === 'cover') {
1262
+ if (imgRatio > boxRatio) {
1263
+ drawH = height
1264
+ drawW = height * imgRatio
1265
+ drawX = x - (drawW - width) / 2
1266
+ } else {
1267
+ drawW = width
1268
+ drawH = width / imgRatio
1269
+ drawY = y - (drawH - height) / 2
1270
+ }
1271
+ } else if (fit === 'contain') {
1272
+ if (imgRatio > boxRatio) {
1273
+ drawW = width
1274
+ drawH = width / imgRatio
1275
+ drawY = y + (height - drawH) / 2
1276
+ } else {
1277
+ drawH = height
1278
+ drawW = height * imgRatio
1279
+ drawX = x + (width - drawW) / 2
1280
+ }
1281
+ }
1282
+
1283
+ // 创建裁剪区域
1284
+ const clipPath = new paper.Path.Rectangle({
1285
+ point: [x, y],
1286
+ size: [width, height],
1287
+ radius: radius
1288
+ })
1289
+
1290
+ // 添加阴影
1291
+ if (shadowBlur > 0) {
1292
+ const shadowRect = new paper.Path.Rectangle({
1293
+ point: [x + shadowOffsetX, y + shadowOffsetY],
1294
+ size: [width, height],
1295
+ radius: radius
1296
+ })
1297
+ shadowRect.fillColor = new paper.Color(shadowColor)
1298
+ shadowRect.opacity = shadowBlur / 50
1299
+ shadowRect.shadowColor = new paper.Color(shadowColor)
1300
+ shadowRect.shadowBlur = shadowBlur
1301
+ elements.push({ type: 'rectangle', id: shadowRect.id })
1302
+ }
1303
+
1304
+ // 绘制图片
1305
+ const raster = new paper.Raster({
1306
+ source: src,
1307
+ position: [drawX + drawW / 2, drawY + drawH / 2]
1308
+ })
1309
+
1310
+ await new Promise((resolve) => {
1311
+ raster.onLoad = resolve
1312
+ })
1313
+
1314
+ raster.size = new paper.Size(drawW, drawH)
1315
+ raster.position = new paper.Point(drawX + drawW / 2, drawY + drawH / 2)
1316
+
1317
+ // 应用裁剪
1318
+ clipPath.clipMask = true
1319
+
1320
+ elements.push({ type: 'raster', id: raster.id })
1321
+
1322
+ // 叠加颜色
1323
+ if (overlayColor && overlayOpacity > 0) {
1324
+ const overlay = new paper.Path.Rectangle({
1325
+ point: [x, y],
1326
+ size: [width, height],
1327
+ radius: radius
1328
+ })
1329
+ overlay.fillColor = new paper.Color(overlayColor)
1330
+ overlay.opacity = overlayOpacity
1331
+ elements.push({ type: 'rectangle', id: overlay.id })
1332
+ }
1333
+
1334
+ return { success: true, elements, type: 'imageFrame' }
1335
+ }
1336
+
1337
+ // 辅助函数:加载图片
1338
+ async function _loadImage(src) {
1339
+ return new Promise((resolve, reject) => {
1340
+ const img = new Image()
1341
+ img.crossOrigin = 'anonymous'
1342
+ img.onload = () => resolve(img)
1343
+ img.onerror = () => {
1344
+ if (src.startsWith('data:')) {
1345
+ img.src = src
1346
+ } else {
1347
+ reject(new Error(`Failed to load image: ${src}`))
1348
+ }
1349
+ }
1350
+ img.src = src
1351
+ })
1352
+ }
1353
+
1354
+ /**
1355
+ * 创建分栏布局组件
1356
+ */
1357
+ function createColumnsComponent(project, canvas, {
1358
+ x,
1359
+ y,
1360
+ width,
1361
+ height,
1362
+ columns = 2,
1363
+ gap = 20,
1364
+ background,
1365
+ borderColor,
1366
+ borderWidth = 1,
1367
+ radius = 0,
1368
+ direction = 'horizontal',
1369
+ align = 'top'
1370
+ }) {
1371
+ const elements = []
1372
+
1373
+ // 计算每列宽度
1374
+ const totalGap = gap * (columns - 1)
1375
+ const columnWidth = (width - totalGap) / columns
1376
+
1377
+ // 绘制背景
1378
+ if (background) {
1379
+ const bg = new paper.Path.Rectangle({
1380
+ point: [x, y],
1381
+ size: [width, height],
1382
+ radius: radius
1383
+ })
1384
+ bg.fillColor = new paper.Color(background)
1385
+ elements.push({ type: 'rectangle', id: bg.id })
1386
+ }
1387
+
1388
+ // 绘制边框
1389
+ if (borderColor && borderWidth > 0) {
1390
+ const border = new paper.Path.Rectangle({
1391
+ point: [x, y],
1392
+ size: [width, height],
1393
+ radius: radius
1394
+ })
1395
+ border.fillColor = new paper.Color('transparent')
1396
+ border.strokeColor = new paper.Color(borderColor)
1397
+ border.strokeWidth = borderWidth
1398
+ elements.push({ type: 'rectangle', id: border.id })
1399
+ }
1400
+
1401
+ // 生成分割线
1402
+ for (let i = 1; i < columns; i++) {
1403
+ const lineX = x + columnWidth * i + gap * (i - 1) + gap / 2
1404
+ const line = new paper.Path.Line({
1405
+ from: [lineX, y + 20],
1406
+ to: [lineX, y + height - 20]
1407
+ })
1408
+ line.strokeColor = new paper.Color('#e0e0e0')
1409
+ line.strokeWidth = 1
1410
+ elements.push({ type: 'line', id: line.id })
1411
+ }
1412
+
1413
+ // 返回列位置信息
1414
+ const columnPositions = []
1415
+ for (let i = 0; i < columns; i++) {
1416
+ const colX = x + (columnWidth + gap) * i
1417
+ const colY = align === 'center' ? y + (height - height) / 2 : align === 'bottom' ? y + height - height : y
1418
+
1419
+ columnPositions.push({
1420
+ index: i,
1421
+ x: colX,
1422
+ y: colY,
1423
+ width: columnWidth,
1424
+ height: height,
1425
+ centerX: colX + columnWidth / 2,
1426
+ centerY: colY + height / 2
1427
+ })
1428
+ }
1429
+
1430
+ return {
1431
+ success: true,
1432
+ elements,
1433
+ columnPositions,
1434
+ columnWidth,
1435
+ totalWidth: width,
1436
+ totalHeight: height,
1437
+ type: 'columns'
1438
+ }
1439
+ }
1440
+
1441
+ /**
1442
+ * 创建网格布局组件
1443
+ */
1444
+ function createGridComponent(project, canvas, {
1445
+ x,
1446
+ y,
1447
+ width,
1448
+ height,
1449
+ columns = 3,
1450
+ rows = 2,
1451
+ gapX = 20,
1452
+ gapY = 20,
1453
+ background,
1454
+ borderColor,
1455
+ borderWidth = 1,
1456
+ radius = 0,
1457
+ direction = 'row'
1458
+ }) {
1459
+ const elements = []
1460
+
1461
+ // 计算单元格尺寸
1462
+ const totalGapX = gapX * (columns - 1)
1463
+ const totalGapY = gapY * (rows - 1)
1464
+ const cellWidth = (width - totalGapX) / columns
1465
+ const cellHeight = (height - totalGapY) / rows
1466
+
1467
+ // 绘制背景
1468
+ if (background) {
1469
+ const bg = new paper.Path.Rectangle({
1470
+ point: [x, y],
1471
+ size: [width, height],
1472
+ radius: radius
1473
+ })
1474
+ bg.fillColor = new paper.Color(background)
1475
+ elements.push({ type: 'rectangle', id: bg.id })
1476
+ }
1477
+
1478
+ // 绘制边框
1479
+ if (borderColor && borderWidth > 0) {
1480
+ const border = new paper.Path.Rectangle({
1481
+ point: [x, y],
1482
+ size: [width, height],
1483
+ radius: radius
1484
+ })
1485
+ border.fillColor = new paper.Color('transparent')
1486
+ border.strokeColor = new paper.Color(borderColor)
1487
+ border.strokeWidth = borderWidth
1488
+ elements.push({ type: 'rectangle', id: border.id })
1489
+ }
1490
+
1491
+ // 生成网格线(可选,这里不绘制让用户自己控制)
1492
+
1493
+ // 生成网格位置信息
1494
+ const cellPositions = []
1495
+ const totalCells = columns * rows
1496
+
1497
+ for (let i = 0; i < totalCells; i++) {
1498
+ let col, row
1499
+
1500
+ if (direction === 'row') {
1501
+ col = i % columns
1502
+ row = Math.floor(i / columns)
1503
+ } else {
1504
+ row = i % rows
1505
+ col = Math.floor(i / rows)
1506
+ }
1507
+
1508
+ const cellX = x + col * (cellWidth + gapX)
1509
+ const cellY = y + row * (cellHeight + gapY)
1510
+
1511
+ cellPositions.push({
1512
+ index: i,
1513
+ column: col,
1514
+ row: row,
1515
+ x: cellX,
1516
+ y: cellY,
1517
+ width: cellWidth,
1518
+ height: cellHeight,
1519
+ centerX: cellX + cellWidth / 2,
1520
+ centerY: cellY + cellHeight / 2
1521
+ })
1522
+ }
1523
+
1524
+ // 返回网格布局信息
1525
+ return {
1526
+ success: true,
1527
+ elements,
1528
+ cellPositions,
1529
+ cellWidth,
1530
+ cellHeight,
1531
+ columns,
1532
+ rows,
1533
+ totalCells,
1534
+ totalWidth: width,
1535
+ totalHeight: height,
1536
+ type: 'grid'
1537
+ }
1538
+ }
1539
+
1540
+
1541
+ /**
1542
+ * 创建星形组件
1543
+ */
1544
+ function createStarComponent(project, canvas, {
1545
+ cx, cy, points = 5, innerRadius: providedInnerRadius, outerRadius,
1546
+ fill, stroke, strokeWidth = 1, opacity = 1, rotation = 0
1547
+ }) {
1548
+ const innerRadius = providedInnerRadius || outerRadius * 0.4
1549
+ const path = new paper.Path()
1550
+ const angleStep = Math.PI / points
1551
+
1552
+ for (let i = 0; i < points * 2; i++) {
1553
+ const radius = i % 2 === 0 ? outerRadius : innerRadius
1554
+ const angle = i * angleStep - Math.PI / 2 + (rotation * Math.PI / 180)
1555
+ const x = cx + radius * Math.cos(angle)
1556
+ const y = cy + radius * Math.sin(angle)
1557
+ if (i === 0) path.moveTo(x, y)
1558
+ else path.lineTo(x, y)
1559
+ }
1560
+ path.closePath()
1561
+
1562
+ if (fill) path.fillColor = new paper.Color(fill)
1563
+ if (stroke) { path.strokeColor = new paper.Color(stroke); path.strokeWidth = strokeWidth }
1564
+ path.opacity = opacity
1565
+ if (project && project.activeLayer) project.activeLayer.addChild(path)
1566
+
1567
+ return { success: true, elements: [{ type: 'path', id: path.id }], type: 'star' }
1568
+ }
1569
+
1570
+ /**
1571
+ * 创建箭头组件
1572
+ */
1573
+ function createArrowComponent(project, canvas, {
1574
+ x1, y1, x2, y2, color = '#333333', strokeWidth = 2, headSize = 12, style = 'solid', direction = 'end'
1575
+ }) {
1576
+ const elements = []
1577
+ const angle = Math.atan2(y2 - y1, x2 - x1)
1578
+
1579
+ const line = new paper.Path.Line({ from: [x1, y1], to: [x2, y2] })
1580
+ line.strokeColor = new paper.Color(color)
1581
+ line.strokeWidth = strokeWidth
1582
+ if (style === 'dashed') line.dashArray = [10, 5]
1583
+ elements.push({ type: 'line', id: line.id })
1584
+
1585
+ if (direction === 'end' || direction === 'both') {
1586
+ const arrowHead = new paper.Path()
1587
+ arrowHead.moveTo(x2, y2)
1588
+ arrowHead.lineTo(x2 + headSize * Math.cos(angle + Math.PI * 0.8), y2 + headSize * Math.sin(angle + Math.PI * 0.8))
1589
+ arrowHead.moveTo(x2, y2)
1590
+ arrowHead.lineTo(x2 + headSize * Math.cos(angle - Math.PI * 0.8), y2 + headSize * Math.sin(angle - Math.PI * 0.8))
1591
+ arrowHead.strokeColor = new paper.Color(color)
1592
+ arrowHead.strokeWidth = strokeWidth
1593
+ arrowHead.strokeCap = 'round'
1594
+ elements.push({ type: 'path', id: arrowHead.id })
1595
+ }
1596
+
1597
+ if (direction === 'start' || direction === 'both') {
1598
+ const startAngle = angle + Math.PI
1599
+ const arrowHead = new paper.Path()
1600
+ arrowHead.moveTo(x1, y1)
1601
+ arrowHead.lineTo(x1 + headSize * Math.cos(startAngle + Math.PI * 0.8), y1 + headSize * Math.sin(startAngle + Math.PI * 0.8))
1602
+ arrowHead.moveTo(x1, y1)
1603
+ arrowHead.lineTo(x1 + headSize * Math.cos(startAngle - Math.PI * 0.8), y1 + headSize * Math.sin(startAngle - Math.PI * 0.8))
1604
+ arrowHead.strokeColor = new paper.Color(color)
1605
+ arrowHead.strokeWidth = strokeWidth
1606
+ arrowHead.strokeCap = 'round'
1607
+ elements.push({ type: 'path', id: arrowHead.id })
1608
+ }
1609
+
1610
+ return { success: true, elements, type: 'arrow' }
1611
+ }
1612
+
1613
+ /**
1614
+ * 创建环形进度条组件
1615
+ */
1616
+ function createProgressCircleComponent(project, canvas, {
1617
+ cx, cy, radius, value, strokeWidth = 10, trackColor = '#e0e0e0',
1618
+ fillColor = '#3b82f6', backgroundColor, showLabel = true, labelColor, startAngle = -90
1619
+ }) {
1620
+ const elements = []
1621
+
1622
+ if (backgroundColor) {
1623
+ const bgCircle = new paper.Path.Circle({ center: [cx, cy], radius: radius })
1624
+ bgCircle.fillColor = new paper.Color(backgroundColor)
1625
+ elements.push({ type: 'path', id: bgCircle.id })
1626
+ }
1627
+
1628
+ const trackCircle = new paper.Path.Circle({ center: [cx, cy], radius: radius })
1629
+ trackCircle.fillColor = new paper.Color('transparent')
1630
+ trackCircle.strokeColor = new paper.Color(trackColor)
1631
+ trackCircle.strokeWidth = strokeWidth
1632
+ elements.push({ type: 'path', id: trackCircle.id })
1633
+
1634
+ if (value > 0) {
1635
+ const endAngle = startAngle + (value / 100) * 360
1636
+ const startRad = startAngle * Math.PI / 180
1637
+ const endRad = endAngle * Math.PI / 180
1638
+
1639
+ const arc = new paper.Path()
1640
+ arc.moveTo(cx + radius * Math.cos(startRad), cy + radius * Math.sin(startRad))
1641
+ arc.arcTo([cx, cy], radius, endRad - startRad)
1642
+ arc.strokeColor = new paper.Color(fillColor)
1643
+ arc.strokeWidth = strokeWidth
1644
+ arc.strokeCap = 'round'
1645
+ elements.push({ type: 'path', id: arc.id })
1646
+ }
1647
+
1648
+ if (showLabel) {
1649
+ const textColor = labelColor || fillColor
1650
+ const label = new paper.PointText({
1651
+ point: [cx, cy + 6],
1652
+ content: `${Math.round(value)}%`,
1653
+ fontSize: radius * 0.4,
1654
+ fillColor: new paper.Color(textColor),
1655
+ justification: 'center',
1656
+ fontWeight: 'bold'
1657
+ })
1658
+ elements.push({ type: 'text', id: label.id })
1659
+ }
1660
+
1661
+ return { success: true, elements, type: 'progressCircle' }
1662
+ }
1663
+
1664
+ /**
1665
+ * 创建 Chip 标签组件
1666
+ */
1667
+ function createChipComponent(project, canvas, {
1668
+ x, y, text, background = '#e0e0e0', color = '#333333', borderColor,
1669
+ fontSize = 12, padding = 12, radius = 16, icon
1670
+ }) {
1671
+ const elements = []
1672
+ const textWidth = text.length * fontSize * 0.6
1673
+ const iconWidth = icon ? fontSize : 0
1674
+ const totalWidth = padding * 2 + textWidth + iconWidth + 4
1675
+ const height = fontSize + padding * 2
1676
+ const rectX = x - totalWidth / 2
1677
+ const rectY = y - height / 2
1678
+
1679
+ const bg = new paper.Path.Rectangle({ point: [rectX, rectY], size: [totalWidth, height], radius: radius })
1680
+ bg.fillColor = new paper.Color(background)
1681
+ if (borderColor) { bg.strokeColor = new paper.Color(borderColor); bg.strokeWidth = 1 }
1682
+ elements.push({ type: 'path', id: bg.id })
1683
+
1684
+ if (icon) {
1685
+ const iconText = new paper.PointText({
1686
+ point: [rectX + padding + iconWidth / 2, y + fontSize / 3],
1687
+ content: icon,
1688
+ fontSize: fontSize + 2,
1689
+ fillColor: new paper.Color(color),
1690
+ justification: 'center'
1691
+ })
1692
+ elements.push({ type: 'text', id: iconText.id })
1693
+ }
1694
+
1695
+ const textX = icon ? rectX + padding + iconWidth + 4 + textWidth / 2 : x
1696
+ const label = new paper.PointText({
1697
+ point: [textX, y + fontSize / 3],
1698
+ content: text,
1699
+ fontSize: fontSize,
1700
+ fillColor: new paper.Color(color),
1701
+ justification: 'center'
1702
+ })
1703
+ elements.push({ type: 'text', id: label.id })
1704
+
1705
+ return { success: true, elements, width: totalWidth, height, type: 'chip' }
1706
+ }
1707
+
1708
+ /**
1709
+ * 创建图表组件
1710
+ */
1711
+ function createChartComponent(project, canvas, {
1712
+ type = 'bar', x, y, width, height, data = [], barColor = '#3b82f6',
1713
+ showLabels = true, showValues = true, barGap = 4
1714
+ }) {
1715
+ const elements = []
1716
+
1717
+ if (type === 'bar' && data.length > 0) {
1718
+ const maxValue = Math.max(...data.map(d => d.value))
1719
+ const barCount = data.length
1720
+ const totalGap = barGap * (barCount - 1)
1721
+ const barWidth = (width - totalGap) / barCount
1722
+ const labelHeight = showLabels ? 24 : 0
1723
+ const valueHeight = showValues ? 20 : 0
1724
+ const chartHeight = height - labelHeight - valueHeight - 10
1725
+
1726
+ data.forEach((item, index) => {
1727
+ const barHeight = (item.value / maxValue) * chartHeight
1728
+ const barX = x + index * (barWidth + barGap)
1729
+ const barY = y + height - labelHeight - valueHeight - barHeight - 5
1730
+ const color = item.color || barColor
1731
+
1732
+ const bar = new paper.Path.Rectangle({
1733
+ point: [barX, barY],
1734
+ size: [barWidth, barHeight],
1735
+ radius: [4, 4, 0, 0]
1736
+ })
1737
+ bar.fillColor = new paper.Color(color)
1738
+ elements.push({ type: 'path', id: bar.id })
1739
+
1740
+ if (showValues) {
1741
+ const valueText = new paper.PointText({
1742
+ point: [barX + barWidth / 2, barY - 8],
1743
+ content: String(item.value),
1744
+ fontSize: 12,
1745
+ fillColor: new paper.Color('#666666'),
1746
+ justification: 'center'
1747
+ })
1748
+ elements.push({ type: 'text', id: valueText.id })
1749
+ }
1750
+
1751
+ if (showLabels) {
1752
+ const labelText = new paper.PointText({
1753
+ point: [barX + barWidth / 2, y + height - 8],
1754
+ content: item.label || '',
1755
+ fontSize: 11,
1756
+ fillColor: new paper.Color('#333333'),
1757
+ justification: 'center'
1758
+ })
1759
+ elements.push({ type: 'text', id: labelText.id })
1760
+ }
1761
+ })
1762
+ } else if (type === 'pie' && data.length > 0) {
1763
+ const cx = x + width / 2
1764
+ const cy = y + height / 2
1765
+ const radius = Math.min(width, height) / 2 - 10
1766
+ const total = data.reduce((sum, d) => sum + d.value, 0)
1767
+ let currentAngle = -90
1768
+ const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']
1769
+
1770
+ data.forEach((item, index) => {
1771
+ const percentage = item.value / total
1772
+ const endAngle = currentAngle + percentage * 360
1773
+ const path = new paper.Path()
1774
+ path.moveTo(cx, cy)
1775
+ path.arc([cx, cy], radius, currentAngle * Math.PI / 180, endAngle * Math.PI / 180)
1776
+ path.closePath()
1777
+ path.fillColor = new paper.Color(item.color || colors[index % colors.length])
1778
+ elements.push({ type: 'path', id: path.id })
1779
+
1780
+ if (showLabels && percentage > 0.05) {
1781
+ const midAngle = (currentAngle + endAngle) / 2
1782
+ const midRad = midAngle * Math.PI / 180
1783
+ const labelX = cx + radius * 0.7 * Math.cos(midRad)
1784
+ const labelY = cy + radius * 0.7 * Math.sin(midRad)
1785
+ const labelText = new paper.PointText({
1786
+ point: [labelX, labelY + 4],
1787
+ content: `${Math.round(percentage * 100)}%`,
1788
+ fontSize: 11,
1789
+ fillColor: new paper.Color('#ffffff'),
1790
+ justification: 'center',
1791
+ fontWeight: 'bold'
1792
+ })
1793
+ elements.push({ type: 'text', id: labelText.id })
1794
+ }
1795
+
1796
+ currentAngle = endAngle
1797
+ })
1798
+ }
1799
+
1800
+ return { success: true, elements, type: 'chart' }
1801
+ }
1802
+
1803
+ /**
1804
+ * 创建水印组件
1805
+ */
1806
+ function createWatermarkComponent(project, canvas, {
1807
+ text, cx, cy, color = 'rgba(0,0,0,0.1)', fontSize = 48,
1808
+ fontFamily = 'sans-serif', opacity = 0.1, rotation = 0, align = 'center'
1809
+ }) {
1810
+ const label = new paper.PointText({
1811
+ point: [cx, cy],
1812
+ content: text,
1813
+ fontSize: fontSize,
1814
+ fontFamily: fontFamily,
1815
+ fillColor: new paper.Color(color),
1816
+ justification: align,
1817
+ opacity: opacity
1818
+ })
1819
+
1820
+ if (rotation !== 0) {
1821
+ label.rotate(rotation, new paper.Point(cx, cy))
1822
+ }
1823
+
1824
+ return { success: true, elements: [{ type: 'text', id: label.id }], type: 'watermark' }
1825
+ }
1826
+
1827
+ /**
1828
+ * 创建表格组件
1829
+ */
1830
+ function createTableComponent(project, canvas, {
1831
+ x, y, width, columns = [], rows = [], rowHeight = 36,
1832
+ headerBg = '#f0f0f0', headerColor = '#333333', borderColor = '#e0e0e0',
1833
+ cellColor = '#333333', fontSize = 12, headerFontSize = 13, striped = true, stripeColor = '#fafafa'
1834
+ }) {
1835
+ const elements = []
1836
+ if (columns.length === 0) return { success: true, elements, type: 'table' }
1837
+
1838
+ const totalHeight = rowHeight * (rows.length + 1)
1839
+
1840
+ const outerBorder = new paper.Path.Rectangle({ point: [x, y], size: [width, totalHeight] })
1841
+ outerBorder.fillColor = new paper.Color('transparent')
1842
+ outerBorder.strokeColor = new paper.Color(borderColor)
1843
+ outerBorder.strokeWidth = 1
1844
+ elements.push({ type: 'path', id: outerBorder.id })
1845
+
1846
+ const headerBgRect = new paper.Path.Rectangle({ point: [x, y], size: [width, rowHeight] })
1847
+ headerBgRect.fillColor = new paper.Color(headerBg)
1848
+ headerBgRect.strokeColor = new paper.Color(borderColor)
1849
+ headerBgRect.strokeWidth = 0.5
1850
+ elements.push({ type: 'path', id: headerBgRect.id })
1851
+
1852
+ let currentX = x
1853
+ columns.forEach((col, index) => {
1854
+ const colWidth = col.width || (width / columns.length)
1855
+ if (index > 0) {
1856
+ const line = new paper.Path.Line({ from: [currentX, y], to: [currentX, y + totalHeight] })
1857
+ line.strokeColor = new paper.Color(borderColor)
1858
+ line.strokeWidth = 0.5
1859
+ elements.push({ type: 'line', id: line.id })
1860
+ }
1861
+ const headerText = new paper.PointText({
1862
+ point: [currentX + colWidth / 2, y + rowHeight / 2 + fontSize / 3],
1863
+ content: col.title || '',
1864
+ fontSize: headerFontSize,
1865
+ fillColor: new paper.Color(headerColor),
1866
+ justification: col.align || 'center',
1867
+ fontWeight: 'bold'
1868
+ })
1869
+ elements.push({ type: 'text', id: headerText.id })
1870
+ currentX += colWidth
1871
+ })
1872
+
1873
+ rows.forEach((row, rowIndex) => {
1874
+ const rowY = y + rowHeight * (rowIndex + 1)
1875
+ if (striped && rowIndex % 2 === 1) {
1876
+ const stripeBg = new paper.Path.Rectangle({ point: [x, rowY], size: [width, rowHeight] })
1877
+ stripeBg.fillColor = new paper.Color(stripeColor)
1878
+ stripeBg.strokeColor = new paper.Color(borderColor)
1879
+ stripeBg.strokeWidth = 0.5
1880
+ elements.push({ type: 'path', id: stripeBg.id })
1881
+ }
1882
+ const rowLine = new paper.Path.Line({ from: [x, rowY], to: [x + width, rowY] })
1883
+ rowLine.strokeColor = new paper.Color(borderColor)
1884
+ rowLine.strokeWidth = 0.5
1885
+ elements.push({ type: 'line', id: rowLine.id })
1886
+
1887
+ let cellX = x
1888
+ columns.forEach((col, colIndex) => {
1889
+ const colWidth = col.width || (width / columns.length)
1890
+ const cellValue = row[colIndex] || ''
1891
+ const cellText = new paper.PointText({
1892
+ point: [cellX + colWidth / 2, rowY + rowHeight / 2 + fontSize / 3],
1893
+ content: String(cellValue),
1894
+ fontSize: fontSize,
1895
+ fillColor: new paper.Color(cellColor),
1896
+ justification: col.align || 'center'
1897
+ })
1898
+ elements.push({ type: 'text', id: cellText.id })
1899
+ cellX += colWidth
1900
+ })
1901
+ })
1902
+
1903
+ return { success: true, elements, width, height: totalHeight, type: 'table' }
1904
+ }