foliko 1.1.1 → 1.1.2

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 (57) hide show
  1. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  2. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  3. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  4. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  5. package/.agent/plugins/poster-plugin/package.json +2 -1
  6. package/.agent/plugins/poster-plugin/src/canvas.js +70 -7
  7. package/.agent/plugins/poster-plugin/src/components/barcode.js +120 -0
  8. package/.agent/plugins/poster-plugin/src/components/bubble.js +153 -0
  9. package/.agent/plugins/poster-plugin/src/components/button.js +124 -0
  10. package/.agent/plugins/poster-plugin/src/components/cta.js +26 -24
  11. package/.agent/plugins/poster-plugin/src/components/featureGrid.js +22 -17
  12. package/.agent/plugins/poster-plugin/src/components/frame.js +230 -0
  13. package/.agent/plugins/poster-plugin/src/components/highlightText.js +144 -0
  14. package/.agent/plugins/poster-plugin/src/components/icon.js +94 -0
  15. package/.agent/plugins/poster-plugin/src/components/index.js +19 -0
  16. package/.agent/plugins/poster-plugin/src/components/listItem.js +6 -5
  17. package/.agent/plugins/poster-plugin/src/components/qrcode.js +74 -0
  18. package/.agent/plugins/poster-plugin/src/components/ribbon.js +193 -0
  19. package/.agent/plugins/poster-plugin/src/components/seal.js +146 -0
  20. package/.agent/plugins/poster-plugin/src/components/table.js +17 -9
  21. package/.agent/plugins/poster-plugin/src/components/tagCloud.js +24 -17
  22. package/.agent/plugins/poster-plugin/src/components/timeline.js +24 -12
  23. package/.agent/plugins/poster-plugin/src/composer.js +392 -150
  24. package/.agent/plugins/poster-plugin/src/elements/background.js +36 -4
  25. package/.agent/plugins/poster-plugin/src/elements/image.js +4 -47
  26. package/.agent/plugins/poster-plugin/src/elements/index.js +2 -0
  27. package/.agent/plugins/poster-plugin/src/elements/richText.js +230 -0
  28. package/.agent/plugins/poster-plugin/src/elements/svg.js +35 -19
  29. package/.agent/plugins/poster-plugin/src/index.js +430 -7
  30. package/.agent/plugins/poster-plugin/src/utils/imageLoader.js +84 -0
  31. package/.agent/plugins/poster-plugin/test-background.svg +1 -0
  32. package/.agent/plugins/poster-plugin/test-full-poster.svg +2 -0
  33. package/.agent/plugins/poster-plugin/test-image.png +0 -0
  34. package/.agent/sessions/cli_default.json +1089 -145
  35. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +8902 -0
  36. package/.claude/settings.local.json +6 -1
  37. package/output/beef-love-poster.png +0 -0
  38. package/output/international-news-daily.png +0 -0
  39. package/package.json +2 -1
  40. package/plugins/extension-executor-plugin.js +33 -32
  41. package/plugins/file-system-plugin.js +4 -19
  42. package/plugins/subagent-plugin.js +37 -14
  43. package/plugins/weixin-plugin.js +167 -47
  44. package/poster-test-2.png +0 -0
  45. package/skills/poster-guide/SKILL.md +497 -5
  46. package/src/core/agent-chat.js +141 -8
  47. package/src/core/agent.js +6 -3
  48. package/calc_tokens_weixin.js +0 -81
  49. package/foliko-creative-3.png +0 -0
  50. package/foliko-creative-4.png +0 -0
  51. package/foliko-creative-5.png +0 -0
  52. package/story-cover-book-v2.png +0 -0
  53. package/story-cover-japanese-1.png +0 -0
  54. package/story-cover-japanese-2.png +0 -0
  55. package/story-cover-japanese-3.png +0 -0
  56. package/story-cover-moran.png +0 -0
  57. package/undefined.png +0 -0
@@ -7,7 +7,8 @@
7
7
  "canvas": "^3.2.0",
8
8
  "fontkit": "^2.0.4",
9
9
  "jsdom": "^16.2.2",
10
- "paper": "^0.12.18"
10
+ "paper": "^0.12.18",
11
+ "qrcode": "^1.5.4"
11
12
  },
12
13
  "overrides": {
13
14
  "canvas": "^3.2.0"
@@ -4,6 +4,8 @@
4
4
 
5
5
  const paper = require('paper')
6
6
  const PRESETS = require('./presets')
7
+ const fs = require('fs')
8
+ const path = require('path')
7
9
 
8
10
  /**
9
11
  * 画布管理器类
@@ -20,7 +22,7 @@ class CanvasManager {
20
22
  /**
21
23
  * 创建画布
22
24
  */
23
- create({ preset, width, height, background }) {
25
+ async create({ preset, width, height, background }) {
24
26
  let w, h
25
27
 
26
28
  if (preset) {
@@ -49,12 +51,21 @@ class CanvasManager {
49
51
 
50
52
  // 添加背景
51
53
  if (background) {
52
- const bg = new paper.Path.Rectangle({
53
- point: [0, 0],
54
- size: [w, h],
55
- fillColor: background,
56
- })
57
- bg.sendToBack()
54
+ if (typeof background === 'string' && path.isAbsolute(background)) {
55
+ // 背景是绝对路径图片
56
+ await this._addBackgroundImage(background, w, h)
57
+ } else if (typeof background === 'object' && background.image) {
58
+ // 背景是对象形式 { image: 'path' }
59
+ await this._addBackgroundImage(background.image, w, h)
60
+ } else {
61
+ // 背景是颜色
62
+ const bg = new paper.Path.Rectangle({
63
+ point: [0, 0],
64
+ size: [w, h],
65
+ fillColor: background,
66
+ })
67
+ bg.sendToBack()
68
+ }
58
69
  }
59
70
 
60
71
  return {
@@ -65,6 +76,58 @@ class CanvasManager {
65
76
  }
66
77
  }
67
78
 
79
+ /**
80
+ * 添加背景图片
81
+ */
82
+ async _addBackgroundImage(imageSrc, w, h) {
83
+ // 本地文件路径
84
+ let absolutePath = imageSrc
85
+ if (!path.isAbsolute(absolutePath)) {
86
+ absolutePath = path.join(process.cwd(), absolutePath)
87
+ }
88
+
89
+ if (!fs.existsSync(absolutePath)) {
90
+ throw new Error(`背景图片文件不存在: ${absolutePath}`)
91
+ }
92
+
93
+ // 使用 loadImage 获取图片数据
94
+ const { loadImage } = require('canvas')
95
+ const imageData = await loadImage(absolutePath)
96
+
97
+ // 创建 Paper.js Raster
98
+ const raster = new paper.Raster(imageData)
99
+
100
+ // 等待 raster 加载完成
101
+ await new Promise((resolve) => {
102
+ if (raster.loaded) {
103
+ resolve()
104
+ } else {
105
+ raster.onLoad = resolve
106
+ }
107
+ })
108
+
109
+ // 计算 cover 模式缩放
110
+ const canvasRatio = w / h
111
+ const imageRatio = raster.width / raster.height
112
+
113
+ let scaledWidth, scaledHeight, offsetX, offsetY
114
+
115
+ if (imageRatio > canvasRatio) {
116
+ scaledHeight = h
117
+ scaledWidth = raster.width * (h / raster.height)
118
+ offsetX = (w - scaledWidth) / 2
119
+ offsetY = 0
120
+ } else {
121
+ scaledWidth = w
122
+ scaledHeight = raster.height * (w / raster.width)
123
+ offsetX = 0
124
+ offsetY = (h - scaledHeight) / 2
125
+ }
126
+
127
+ raster.bounds = new paper.Rectangle(offsetX, offsetY, scaledWidth, scaledHeight)
128
+ raster.sendToBack()
129
+ }
130
+
68
131
  /**
69
132
  * 获取画布
70
133
  */
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 条形码组件 - 静态图片
3
+ */
4
+
5
+ const paper = require('paper')
6
+ const { loadImageAsRaster, downloadImage } = require('../utils/imageLoader')
7
+
8
+ /**
9
+ * 创建条形码
10
+ */
11
+ async function createBarcode(project, args) {
12
+ const {
13
+ x = 0,
14
+ y = 0,
15
+ width = 300,
16
+ height = 100,
17
+ content = '1234567890',
18
+ showText = true,
19
+ textColor = '#000000',
20
+ fontSize = 16,
21
+ opacity = 1,
22
+ } = args
23
+
24
+ // 内容太短或像URL,回退到简单条形码
25
+ if (content.length < 8 || content.startsWith('http')) {
26
+ return createSimpleBarcode(project, { x, y, width, height, content, showText, textColor, fontSize, opacity })
27
+ }
28
+
29
+ try {
30
+ const barcodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${width}x${height}&data=${encodeURIComponent(content)}&format=png`
31
+ const { raster } = await loadImageAsRaster(project, barcodeUrl, { x, y, width, height }, opacity)
32
+ const items = [raster]
33
+
34
+ // 文字
35
+ if (showText) {
36
+ const textItem = new paper.PointText({
37
+ point: [x + width / 2, y + height + fontSize + 5],
38
+ content: content,
39
+ fontSize,
40
+ fontFamily: 'monospace',
41
+ fillColor: new paper.Color(textColor),
42
+ justification: 'center',
43
+ })
44
+ if (opacity !== 1) textItem.opacity = opacity
45
+ items.push(textItem)
46
+ }
47
+
48
+ return {
49
+ success: true,
50
+ type: 'barcode',
51
+ items: items.map(i => i.id),
52
+ }
53
+ } catch (err) {
54
+ // 回退到简单条形码
55
+ return createSimpleBarcode(project, { x, y, width, height, content, showText, textColor, fontSize, opacity })
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 创建简单的条形码(使用线条绘制)
61
+ */
62
+ function createSimpleBarcode(project, args) {
63
+ const {
64
+ x = 0,
65
+ y = 0,
66
+ width = 300,
67
+ height = 80,
68
+ content = '123456',
69
+ color = '#000000',
70
+ showText = true,
71
+ textColor = '#000000',
72
+ fontSize = 14,
73
+ opacity = 1,
74
+ } = args
75
+
76
+ const items = []
77
+ const barHeight = showText ? height - 25 : height
78
+ const barWidth = width / (content.length * 15)
79
+
80
+ // 生成简单的条形图案
81
+ for (let i = 0; i < content.length; i++) {
82
+ const charCode = content.charCodeAt(i)
83
+ const density = (charCode % 3) + 1
84
+
85
+ for (let d = 0; d < density; d++) {
86
+ const isWide = (charCode + d) % 2 === 0
87
+ const barW = isWide ? barWidth * 2 : barWidth
88
+
89
+ const bar = new paper.Path.Rectangle({
90
+ point: [x + (i * density + d) * barWidth, y],
91
+ size: [barW * 0.8, barHeight],
92
+ fillColor: new paper.Color(color),
93
+ })
94
+ if (opacity !== 1) bar.opacity = opacity
95
+ items.push(bar)
96
+ }
97
+ }
98
+
99
+ // 文字
100
+ if (showText) {
101
+ const textItem = new paper.PointText({
102
+ point: [x + width / 2, y + barHeight + fontSize + 3],
103
+ content: content,
104
+ fontSize,
105
+ fontFamily: 'monospace',
106
+ fillColor: new paper.Color(textColor),
107
+ justification: 'center',
108
+ })
109
+ if (opacity !== 1) textItem.opacity = opacity
110
+ items.push(textItem)
111
+ }
112
+
113
+ return {
114
+ success: true,
115
+ type: 'barcode',
116
+ items: items.map(i => i.id),
117
+ }
118
+ }
119
+
120
+ module.exports = createBarcode
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 对话气泡组件
3
+ */
4
+
5
+ const paper = require('paper')
6
+
7
+ /**
8
+ * 创建对话气泡
9
+ */
10
+ function createBubble(project, args) {
11
+ const {
12
+ x = 0,
13
+ y = 0,
14
+ width = 300,
15
+ height = 100,
16
+ text = '',
17
+ fontSize = 24,
18
+ fontFamily,
19
+ color = '#000000',
20
+ backgroundColor = '#ffffff',
21
+ borderColor,
22
+ borderWidth = 1,
23
+ radius = 20,
24
+ tailDirection = 'bottom', // bottom, top, left, right
25
+ tailPosition = 'left', // left, center, right
26
+ shadow,
27
+ opacity = 1,
28
+ } = args
29
+
30
+ const items = []
31
+
32
+ // 气泡主体
33
+ const bubbleX = tailDirection === 'left' ? x + 15 : x
34
+ const bubbleY = tailDirection === 'top' ? y + 15 : y
35
+ const bubbleWidth = tailDirection === 'left' ? width - 15 : width
36
+ const bubbleHeight = tailDirection === 'top' ? height - 15 : height
37
+
38
+ const bgOpts = {
39
+ point: [bubbleX, bubbleY],
40
+ size: [bubbleWidth, bubbleHeight],
41
+ radius: radius,
42
+ }
43
+
44
+ if (borderColor) {
45
+ bgOpts.strokeColor = new paper.Color(borderColor)
46
+ bgOpts.strokeWidth = borderWidth
47
+ }
48
+
49
+ const bg = new paper.Path.Rectangle(bgOpts)
50
+ bg.fillColor = new paper.Color(backgroundColor)
51
+
52
+ if (shadow) {
53
+ bg.shadowColor = new paper.Color(shadow.color || '#000000')
54
+ bg.shadowBlur = shadow.blur || 10
55
+ bg.shadowOffset = new paper.Point(shadow.offsetX || 3, shadow.offsetY || 3)
56
+ }
57
+
58
+ if (opacity !== 1) bg.opacity = opacity
59
+ items.push(bg)
60
+
61
+ // 气泡尾巴
62
+ const tailSize = 20
63
+ let tailX, tailY, tailRotation
64
+
65
+ // 计算尾巴位置
66
+ switch (tailDirection) {
67
+ case 'bottom':
68
+ tailY = bubbleY + bubbleHeight - 5
69
+ if (tailPosition === 'left') tailX = bubbleX + 30
70
+ else if (tailPosition === 'right') tailX = bubbleX + bubbleWidth - 30
71
+ else tailX = bubbleX + bubbleWidth / 2
72
+ tailRotation = 0
73
+ break
74
+ case 'top':
75
+ tailY = bubbleY + 5
76
+ if (tailPosition === 'left') tailX = bubbleX + 30
77
+ else if (tailPosition === 'right') tailX = bubbleX + bubbleWidth - 30
78
+ else tailX = bubbleX + bubbleWidth / 2
79
+ tailRotation = 180
80
+ break
81
+ case 'left':
82
+ tailX = bubbleX + 5
83
+ if (tailPosition === 'left') tailY = bubbleY + 30
84
+ else if (tailPosition === 'right') tailY = bubbleY + bubbleHeight - 30
85
+ else tailY = bubbleY + bubbleHeight / 2
86
+ tailRotation = 90
87
+ break
88
+ case 'right':
89
+ tailX = bubbleX + bubbleWidth - 5
90
+ if (tailPosition === 'left') tailY = bubbleY + 30
91
+ else if (tailPosition === 'right') tailY = bubbleY + bubbleHeight - 30
92
+ else tailY = bubbleY + bubbleHeight / 2
93
+ tailRotation = 270
94
+ break
95
+ }
96
+
97
+ const tail = new paper.Path()
98
+ tail.add(new paper.Point(tailX - tailSize, tailY))
99
+ tail.add(new paper.Point(tailX, tailY + tailSize * 0.7))
100
+ tail.add(new paper.Point(tailX, tailY - tailSize * 0.7))
101
+ tail.closed = true
102
+ tail.fillColor = new paper.Color(backgroundColor)
103
+ if (borderColor) {
104
+ tail.strokeColor = new paper.Color(borderColor)
105
+ tail.strokeWidth = borderWidth
106
+ }
107
+ tail.rotate(tailRotation, new paper.Point(tailX, tailY))
108
+
109
+ if (opacity !== 1) tail.opacity = opacity
110
+ items.push(tail)
111
+
112
+ // 文字
113
+ const padding = 25
114
+ const textItem = new paper.PointText({
115
+ point: [bubbleX + padding, bubbleY + bubbleHeight / 2 + fontSize / 3],
116
+ content: text,
117
+ fontSize,
118
+ fontFamily: fontFamily || 'sans-serif',
119
+ fillColor: new paper.Color(color),
120
+ justification: tailPosition === 'left' ? 'left' : tailPosition === 'right' ? 'right' : 'center',
121
+ })
122
+
123
+ // 限制文字宽度
124
+ const maxTextWidth = bubbleWidth - padding * 2
125
+ if (textItem.bounds.width > maxTextWidth) {
126
+ textItem.justification = 'left'
127
+ const charsPerLine = Math.floor((text.length * maxTextWidth) / textItem.bounds.width)
128
+ // 简单换行处理
129
+ let lines = []
130
+ let currentLine = ''
131
+ for (const char of text) {
132
+ currentLine += char
133
+ const testText = new paper.PointText({ content: currentLine, fontSize, fontFamily: fontFamily || 'sans-serif' })
134
+ if (testText.bounds.width > maxTextWidth) {
135
+ lines.push(currentLine.slice(0, -1))
136
+ currentLine = char
137
+ }
138
+ }
139
+ if (currentLine) lines.push(currentLine)
140
+ textItem.content = lines.join('\n')
141
+ }
142
+
143
+ if (opacity !== 1) textItem.opacity = opacity
144
+ items.push(textItem)
145
+
146
+ return {
147
+ success: true,
148
+ type: 'bubble',
149
+ items: items.map(i => i.id),
150
+ }
151
+ }
152
+
153
+ module.exports = createBubble
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 按钮组件
3
+ */
4
+
5
+ const paper = require('paper')
6
+ const { loadImageAsRaster } = require('../utils/imageLoader')
7
+
8
+ /**
9
+ * 创建按钮
10
+ */
11
+ async function createButton(project, args) {
12
+ const {
13
+ x = 0,
14
+ y = 0,
15
+ width = 200,
16
+ height = 60,
17
+ text = '按钮',
18
+ fontSize = 24,
19
+ fontFamily,
20
+ color = '#ffffff',
21
+ backgroundColor = '#3b82f6',
22
+ borderColor,
23
+ borderWidth = 0,
24
+ radius = 8,
25
+ shadow,
26
+ gradient,
27
+ icon,
28
+ iconPosition = 'left',
29
+ opacity = 1,
30
+ } = args
31
+
32
+ // 创建按钮背景
33
+ const bgOptions = {
34
+ point: [x, y],
35
+ size: [width, height],
36
+ radius: radius,
37
+ }
38
+
39
+ if (borderColor) {
40
+ bgOptions.strokeColor = new paper.Color(borderColor)
41
+ bgOptions.strokeWidth = borderWidth
42
+ }
43
+
44
+ const bg = new paper.Path.Rectangle(bgOptions)
45
+
46
+ // 渐变或纯色填充
47
+ if (gradient && gradient.colors && gradient.colors.length > 0) {
48
+ const colors = gradient.colors.map(c => new paper.Color(c))
49
+ bg.fillColor = new paper.Color({
50
+ gradient: { stops: colors },
51
+ origin: bg.bounds.topLeft,
52
+ destination: bg.bounds.bottomLeft,
53
+ })
54
+ } else {
55
+ bg.fillColor = new paper.Color(backgroundColor)
56
+ }
57
+
58
+ // 阴影
59
+ if (shadow) {
60
+ bg.shadowColor = new paper.Color(shadow.color || '#000000')
61
+ bg.shadowBlur = shadow.blur || 10
62
+ bg.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
63
+ }
64
+
65
+ if (opacity !== 1) {
66
+ bg.opacity = opacity
67
+ }
68
+
69
+ const items = [bg]
70
+ let textX = x + width / 2
71
+
72
+ // 添加图标
73
+ if (icon) {
74
+ const iconSize = Math.min(height * 0.5, 40)
75
+ const iconX = iconPosition === 'left' ? x + 20 : x + width - 40 - iconSize
76
+
77
+ if (icon.startsWith('http') || icon.startsWith('data:')) {
78
+ // URL图片图标
79
+ const { raster } = await loadImageAsRaster(project, icon, {
80
+ x: iconX,
81
+ y: y + (height - iconSize) / 2,
82
+ width: iconSize,
83
+ height: iconSize
84
+ }, opacity)
85
+ items.push(raster)
86
+ } else {
87
+ // Emoji 或文本图标
88
+ const iconText = new paper.PointText({
89
+ point: [iconX + iconSize / 2, y + height / 2 + fontSize / 3],
90
+ content: icon,
91
+ fontSize: iconSize,
92
+ justification: 'center',
93
+ })
94
+ if (opacity !== 1) iconText.opacity = opacity
95
+ items.push(iconText)
96
+ }
97
+
98
+ textX = iconPosition === 'left' ? x + width / 2 + 25 : x + width / 2 - 25
99
+ }
100
+
101
+ // 添加文字
102
+ const textItem = new paper.PointText({
103
+ point: [textX, y + height / 2 + fontSize / 3],
104
+ content: text,
105
+ fontSize,
106
+ fontFamily: fontFamily || 'sans-serif',
107
+ fillColor: new paper.Color(color),
108
+ justification: 'center',
109
+ })
110
+
111
+ if (opacity !== 1) {
112
+ textItem.opacity = opacity
113
+ }
114
+
115
+ items.push(textItem)
116
+
117
+ return {
118
+ success: true,
119
+ type: 'button',
120
+ items: items.map(i => i.id),
121
+ }
122
+ }
123
+
124
+ module.exports = createButton
@@ -6,25 +6,11 @@ const paper = require('paper')
6
6
 
7
7
  /**
8
8
  * 创建 CTA 按钮
9
- *
10
- * @param {Object} project - Paper.js 项目
11
- * @param {Object} canvas - 画布对象
12
- * @param {Object} args - 组件参数
13
- * @param {number} args.x - X坐标(居中)
14
- * @param {number} args.y - Y坐标
15
- * @param {string} args.text - 按钮文字
16
- * @param {string} args.background - 背景色
17
- * @param {string} args.color - 文字颜色
18
- * @param {string} args.border - 边框颜色
19
- * @param {number} args.fontSize - 字体大小
20
- * @param {number} args.padding - 内边距
21
- * @param {number} args.radius - 圆角半径
22
- * @param {Object} args.shadow - 阴影设置
23
9
  */
24
10
  function createCTA(project, canvas, args) {
25
11
  const {
26
- x, y,
27
- text,
12
+ x = 0, y = 0,
13
+ text = '',
28
14
  background = '#007bff',
29
15
  color = '#ffffff',
30
16
  border,
@@ -32,13 +18,18 @@ function createCTA(project, canvas, args) {
32
18
  padding = 25,
33
19
  radius = 8,
34
20
  shadow,
21
+ width: customWidth,
35
22
  } = args
36
23
 
37
24
  const elements = []
38
25
 
39
- // 计算按钮尺寸
40
- const textWidth = text.length * fontSize * 0.7
41
- const btnWidth = textWidth + padding * 2
26
+ // 确保 text 是字符串
27
+ const textStr = String(text || '')
28
+ // 使用更准确的字符宽度估算:中文约1.0,英文约0.5
29
+ const chineseChars = (textStr.match(/[\u4e00-\u9fa5]/g) || []).length
30
+ const otherChars = textStr.length - chineseChars
31
+ const textWidth = chineseChars * fontSize * 1.0 + otherChars * fontSize * 0.5
32
+ const btnWidth = customWidth || (textWidth + padding * 2)
42
33
  const btnHeight = fontSize + padding * 2
43
34
 
44
35
  const btnX = x - btnWidth / 2
@@ -56,22 +47,32 @@ function createCTA(project, canvas, args) {
56
47
  button.strokeWidth = 1
57
48
  }
58
49
 
59
- if (shadow) {
60
- button.shadowColor = new paper.Color(shadow.color || 'rgba(0,0,0,0.3)')
61
- button.shadowBlur = shadow.blur || 10
62
- button.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
50
+ if (shadow && typeof shadow === 'object') {
51
+ try {
52
+ if (shadow.color) button.shadowColor = new paper.Color(shadow.color)
53
+ button.shadowBlur = shadow.blur || 10
54
+ button.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
55
+ } catch (e) {
56
+ // 忽略阴影错误
57
+ }
63
58
  }
64
59
 
60
+ if (project && project.activeLayer) {
61
+ project.activeLayer.addChild(button)
62
+ }
65
63
  elements.push({ type: 'rectangle', id: button.id })
66
64
 
67
65
  // 绘制文字
68
66
  const buttonText = new paper.PointText({
69
67
  point: [x, y + btnHeight / 2 + fontSize / 3],
70
- content: text,
68
+ content: textStr,
71
69
  fontSize: fontSize,
72
70
  fillColor: new paper.Color(color),
73
71
  justification: 'center',
74
72
  })
73
+ if (project && project.activeLayer) {
74
+ project.activeLayer.addChild(buttonText)
75
+ }
75
76
  elements.push({ type: 'text', id: buttonText.id })
76
77
 
77
78
  return {
@@ -79,6 +80,7 @@ function createCTA(project, canvas, args) {
79
80
  elements,
80
81
  width: btnWidth,
81
82
  height: btnHeight,
83
+ type: 'cta',
82
84
  }
83
85
  }
84
86
 
@@ -6,20 +6,6 @@ const paper = require('paper')
6
6
 
7
7
  /**
8
8
  * 创建特性网格
9
- *
10
- * @param {Object} project - Paper.js 项目
11
- * @param {Object} canvas - 画布对象
12
- * @param {Object} args - 组件参数
13
- * @param {number} args.x - X坐标
14
- * @param {number} args.y - Y坐标
15
- * @param {number} args.columns - 列数
16
- * @param {number} args.itemWidth - 每个特性宽度
17
- * @param {number} args.itemHeight - 每个特性高度
18
- * @param {number} args.gap - 间距
19
- * @param {Array} args.items - 特性数组 [{icon, title, description}]
20
- * @param {string} args.background - 背景色
21
- * @param {string} args.borderColor - 边框颜色
22
- * @param {number} args.radius - 圆角半径
23
9
  */
24
10
  function createFeatureGrid(project, canvas, args) {
25
11
  const {
@@ -35,7 +21,13 @@ function createFeatureGrid(project, canvas, args) {
35
21
  } = args
36
22
 
37
23
  const elements = []
38
- const rows = Math.ceil(items.length / columns)
24
+
25
+ // 确保 items 是数组
26
+ if (!Array.isArray(items)) {
27
+ items = []
28
+ }
29
+
30
+ const rows = items.length > 0 ? Math.ceil(items.length / columns) : 0
39
31
 
40
32
  for (let i = 0; i < items.length; i++) {
41
33
  const item = items[i]
@@ -55,6 +47,9 @@ function createFeatureGrid(project, canvas, args) {
55
47
  bg.strokeColor = new paper.Color(borderColor)
56
48
  bg.strokeWidth = 0.5
57
49
  bg.opacity = 0.8
50
+ if (project && project.activeLayer) {
51
+ project.activeLayer.addChild(bg)
52
+ }
58
53
  elements.push({ type: 'rectangle', id: bg.id })
59
54
 
60
55
  const padding = 15
@@ -69,6 +64,9 @@ function createFeatureGrid(project, canvas, args) {
69
64
  fillColor: new paper.Color(item.iconColor || '#00ff88'),
70
65
  justification: 'left',
71
66
  })
67
+ if (project && project.activeLayer) {
68
+ project.activeLayer.addChild(iconText)
69
+ }
72
70
  elements.push({ type: 'text', id: iconText.id })
73
71
  itemYOffset += 35
74
72
  }
@@ -82,6 +80,9 @@ function createFeatureGrid(project, canvas, args) {
82
80
  fillColor: new paper.Color(item.titleColor || '#ffffff'),
83
81
  justification: 'left',
84
82
  })
83
+ if (project && project.activeLayer) {
84
+ project.activeLayer.addChild(titleText)
85
+ }
85
86
  elements.push({ type: 'text', id: titleText.id })
86
87
  itemYOffset += 22
87
88
  }
@@ -95,6 +96,9 @@ function createFeatureGrid(project, canvas, args) {
95
96
  fillColor: new paper.Color(item.descColor || '#888888'),
96
97
  justification: 'left',
97
98
  })
99
+ if (project && project.activeLayer) {
100
+ project.activeLayer.addChild(descText)
101
+ }
98
102
  elements.push({ type: 'text', id: descText.id })
99
103
  }
100
104
  }
@@ -102,10 +106,11 @@ function createFeatureGrid(project, canvas, args) {
102
106
  return {
103
107
  success: true,
104
108
  elements,
105
- width: columns * itemWidth + (columns - 1) * gap,
106
- height: rows * itemHeight + (rows - 1) * gap,
109
+ width: columns * itemWidth + Math.max(0, columns - 1) * gap,
110
+ height: rows * itemHeight + Math.max(0, rows - 1) * gap,
107
111
  rows,
108
112
  cols: columns,
113
+ type: 'featureGrid',
109
114
  }
110
115
  }
111
116