foliko 1.1.8 → 1.1.9

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 (306) hide show
  1. package/.agent/agents/code-assistant.json +17 -0
  2. package/.agent/agents/email-assistant.json +14 -0
  3. package/.agent/agents/file-assistant.json +18 -0
  4. package/.agent/agents/orchestrator-demo.md +53 -0
  5. package/.agent/agents/orchestrator.json +7 -0
  6. package/.agent/agents/poster-expert.md +228 -0
  7. package/.agent/agents/system-assistant.json +15 -0
  8. package/.agent/agents/web-assistant.json +12 -0
  9. package/.agent/data/default.json +5 -404
  10. package/.agent/data/email/processed-emails.json +1 -0
  11. package/.agent/data/plugins-state.json +173 -172
  12. package/.agent/data/scheduler/tasks.json +1 -0
  13. package/.agent/data/web/web-config.json +5 -0
  14. package/.agent/mcp_config.json +0 -14
  15. package/.agent/package.json +8 -0
  16. package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
  17. package/.agent/plugins/daytona/README.md +89 -0
  18. package/.agent/plugins/daytona/index.js +377 -0
  19. package/.agent/plugins/daytona/package.json +12 -0
  20. package/.agent/plugins/marknative/README.md +134 -0
  21. package/.agent/plugins/marknative/fonts.zip +0 -0
  22. package/.agent/plugins/marknative/index.js +256 -0
  23. package/.agent/plugins/marknative/package.json +12 -0
  24. package/.agent/plugins/system-info/index.js +387 -0
  25. package/.agent/plugins/system-info/package.json +4 -0
  26. package/.agent/plugins/system-info/test.js +40 -0
  27. package/.agent/plugins/test-plugin.py +123 -0
  28. package/.agent/plugins/test_nested_plugin.py +85 -0
  29. package/.agent/plugins.json +11 -5
  30. package/.agent/python-scripts/test_sample.py +24 -0
  31. package/.agent/sessions/cli_default.json +96 -249
  32. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +189 -0
  33. package/.agent/skills/agent-browser/SKILL.md +311 -0
  34. package/.agent/skills/agent-browser/TEST_PLAN.md +200 -0
  35. package/.agent/skills/sysinfo/SKILL.md +38 -0
  36. package/.agent/skills/sysinfo/system-info.sh +130 -0
  37. package/.agent/skills/workflow/SKILL.md +324 -0
  38. package/.agent/test-agent.js +35 -0
  39. package/.agent/weixin.json +6 -0
  40. package/.agent/workflows/email-digest.json +50 -0
  41. package/.agent/workflows/file-backup.json +21 -0
  42. package/.agent/workflows/get-ip-notify.json +32 -0
  43. package/.agent/workflows/news-aggregator.json +93 -0
  44. package/.agent/workflows/news-dashboard-v2.json +94 -0
  45. package/.agent/workflows/notification-batch.json +32 -0
  46. package/.claude/settings.local.json +9 -1
  47. package/.env.example +56 -56
  48. package/README.md +441 -441
  49. package/cli/src/ui/chat-ui.js +32 -21
  50. package/foliko_poster.png +0 -0
  51. package/output/business_poster_final.png +0 -0
  52. package/output/emoji_test_v2.png +0 -0
  53. package/output/foliko-ai-launch-poster.png +0 -0
  54. package/output/foliko-poster.png +0 -0
  55. package/output/muji_style_poster.png +0 -0
  56. package/output/new_product_launch.png +0 -0
  57. package/output/news_poster_625yi_subsidy.png +0 -0
  58. package/output/tech_future_2026.png +0 -0
  59. package/output/tech_future_poster.png +0 -0
  60. package/package.json +1 -2
  61. package/plugins/default-plugins.js +58 -6
  62. package/plugins/extension-executor-plugin.js +8 -21
  63. package/plugins/python-plugin-loader.js +461 -40
  64. package/plugins/python-plugin-loader.js.bak +856 -0
  65. package/plugins/subagent-plugin.js +0 -121
  66. package/plugins/weixin-plugin.js +38 -29
  67. package/poster.png +0 -0
  68. package/skills/find-skills/AGENTS.md +162 -162
  69. package/skills/find-skills/SKILL.md +133 -133
  70. package/skills/foliko-dev/SKILL.md +6 -0
  71. package/skills/python-plugin-dev/SKILL.md +124 -2
  72. package/src/core/agent-chat.js +220 -281
  73. package/src/core/agent.js +10 -0
  74. package/src/utils/plugin-helpers.js +22 -4
  75. package/system.md +1678 -1574
  76. package/test-scan.js +18 -0
  77. package/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
  78. package/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
  79. package/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
  80. package/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
  81. package/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
  82. package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
  83. package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
  84. package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  85. package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  86. package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
  87. package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  88. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  89. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  90. package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  91. package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
  92. package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
  93. package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  94. package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  95. package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  96. package/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
  97. package/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
  98. package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
  99. package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  100. package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
  101. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
  102. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
  103. package/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
  104. package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
  105. package/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
  106. package/.agent/ARCHITECTURE.md +0 -288
  107. package/.agent/agents/ambient-agent.md +0 -57
  108. package/.agent/agents/debugger.md +0 -55
  109. package/.agent/agents/email-assistant.md +0 -49
  110. package/.agent/agents/file-manager.md +0 -42
  111. package/.agent/agents/python-developer.md +0 -60
  112. package/.agent/agents/scheduler.md +0 -59
  113. package/.agent/agents/web-developer.md +0 -45
  114. package/.agent/data/puppeteer-sessions/undefined.json +0 -6
  115. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  116. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  117. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  118. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  119. package/.agent/memory/feedback/mnrdvj5i-ca3dkd.md +0 -9
  120. package/.agent/memory/feedback/mnre365e-7s4zax.md +0 -9
  121. package/.agent/memory/feedback/mnre36jn-nkfgmp.md +0 -9
  122. package/.agent/memory/feedback/mnre3805-kjiq6h.md +0 -9
  123. package/.agent/memory/feedback/mnsf66kp-b10rcd.md +0 -9
  124. package/.agent/memory/feedback/mnsi3sz8-p5g2cw.md +0 -9
  125. package/.agent/memory/feedback/mnsibe47-sv2ni1.md +0 -9
  126. package/.agent/memory/feedback/mnsic89w-nn228o.md +0 -9
  127. package/.agent/memory/feedback/mnsj1xe9-x83ba0.md +0 -9
  128. package/.agent/memory/feedback/mnsj21iv-wnwelx.md +0 -9
  129. package/.agent/memory/feedback/mnsj2g4a-cog7a2.md +0 -9
  130. package/.agent/memory/feedback/mnsj4js7-lktjp6.md +0 -9
  131. package/.agent/memory/feedback/mnsj5d4y-uglwvp.md +0 -9
  132. package/.agent/memory/feedback/mnslkuo9-uous66.md +0 -24
  133. package/.agent/memory/feedback/mnsm3vq0-megoil.md +0 -9
  134. package/.agent/memory/feedback/mnsnn5x2-sxcihd.md +0 -9
  135. package/.agent/memory/feedback/mnsnq17s-nabrn9.md +0 -9
  136. package/.agent/memory/feedback/mnsnybet-wz7rn3.md +0 -9
  137. package/.agent/memory/feedback/mnsrw0s7-7s9e30.md +0 -9
  138. package/.agent/memory/feedback/mnu5hpnd-tlm16q.md +0 -9
  139. package/.agent/memory/feedback/mnu60uqe-xuoxp4.md +0 -9
  140. package/.agent/memory/project/mnqx54u5-loqtoe.md +0 -9
  141. package/.agent/memory/project/mnqx84cv-mx6dmd.md +0 -9
  142. package/.agent/memory/project/mnsacuyr-hgtk5n.md +0 -20
  143. package/.agent/memory/project/mnu5hy2x-bjsg7u.md +0 -9
  144. package/.agent/memory/reference/mnre3cww-penbo1.md +0 -9
  145. package/.agent/memory/reference/mns9wn48-luerua.md +0 -14
  146. package/.agent/memory/reference/mns9yz5c-thc2s0.md +0 -16
  147. package/.agent/memory/reference/mnsfy4um-910f1o.md +0 -23
  148. package/.agent/memory/reference/mnsg37dp-lmfj18.md +0 -32
  149. package/.agent/memory/reference/mnsll60q-0j911u.md +0 -36
  150. package/.agent/memory/reference/mnsmlb5y-nej31u.md +0 -16
  151. package/.agent/memory/reference/mnssle72-yrot96.md +0 -9
  152. package/.agent/memory/user/mnsfuon6-l416q1.md +0 -21
  153. package/.agent/memory/user/mnsg9kut-95m7rf.md +0 -20
  154. package/.agent/memory/user/mnu2eo1v-yy6fhe.md +0 -9
  155. package/.agent/memory/user/mnu2etuo-8u8jk8.md +0 -9
  156. package/.agent/plugins/poster-plugin/README.md +0 -304
  157. package/.agent/plugins/poster-plugin/fonts/NotoColorEmoji-Regular.ttf +0 -0
  158. package/.agent/plugins/poster-plugin/fonts/PatuaOne-Regular.ttf +0 -0
  159. package/.agent/plugins/poster-plugin/fonts/Symbola_hint.ttf +0 -0
  160. package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221.ttf +0 -0
  161. 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
  162. package/.agent/plugins/poster-plugin/index.js +0 -13
  163. package/.agent/plugins/poster-plugin/package.json +0 -29
  164. package/.agent/plugins/poster-plugin/src/canvas.js +0 -232
  165. package/.agent/plugins/poster-plugin/src/components/arrow.js +0 -84
  166. package/.agent/plugins/poster-plugin/src/components/avatar.js +0 -71
  167. package/.agent/plugins/poster-plugin/src/components/badge.js +0 -85
  168. package/.agent/plugins/poster-plugin/src/components/barcode.js +0 -123
  169. package/.agent/plugins/poster-plugin/src/components/bubble.js +0 -154
  170. package/.agent/plugins/poster-plugin/src/components/button.js +0 -168
  171. package/.agent/plugins/poster-plugin/src/components/card.js +0 -88
  172. package/.agent/plugins/poster-plugin/src/components/chart.js +0 -127
  173. package/.agent/plugins/poster-plugin/src/components/chip.js +0 -88
  174. package/.agent/plugins/poster-plugin/src/components/columns.js +0 -121
  175. package/.agent/plugins/poster-plugin/src/components/cta.js +0 -87
  176. package/.agent/plugins/poster-plugin/src/components/divider.js +0 -55
  177. package/.agent/plugins/poster-plugin/src/components/feature.js +0 -85
  178. package/.agent/plugins/poster-plugin/src/components/featureGrid.js +0 -117
  179. package/.agent/plugins/poster-plugin/src/components/frame.js +0 -230
  180. package/.agent/plugins/poster-plugin/src/components/grid.js +0 -130
  181. package/.agent/plugins/poster-plugin/src/components/highlightText.js +0 -145
  182. package/.agent/plugins/poster-plugin/src/components/icon.js +0 -94
  183. package/.agent/plugins/poster-plugin/src/components/imageFrame.js +0 -205
  184. package/.agent/plugins/poster-plugin/src/components/index.js +0 -81
  185. package/.agent/plugins/poster-plugin/src/components/listItem.js +0 -147
  186. package/.agent/plugins/poster-plugin/src/components/notification.js +0 -123
  187. package/.agent/plugins/poster-plugin/src/components/progress.js +0 -79
  188. package/.agent/plugins/poster-plugin/src/components/progressCircle.js +0 -117
  189. package/.agent/plugins/poster-plugin/src/components/qrcode.js +0 -74
  190. package/.agent/plugins/poster-plugin/src/components/quote.js +0 -169
  191. package/.agent/plugins/poster-plugin/src/components/rating.js +0 -85
  192. package/.agent/plugins/poster-plugin/src/components/ribbon.js +0 -197
  193. package/.agent/plugins/poster-plugin/src/components/seal.js +0 -148
  194. package/.agent/plugins/poster-plugin/src/components/star.js +0 -70
  195. package/.agent/plugins/poster-plugin/src/components/statCard.js +0 -105
  196. package/.agent/plugins/poster-plugin/src/components/stepper.js +0 -118
  197. package/.agent/plugins/poster-plugin/src/components/table.js +0 -167
  198. package/.agent/plugins/poster-plugin/src/components/tagCloud.js +0 -85
  199. package/.agent/plugins/poster-plugin/src/components/timeline.js +0 -117
  200. package/.agent/plugins/poster-plugin/src/components/watermark.js +0 -54
  201. package/.agent/plugins/poster-plugin/src/composer.js +0 -2314
  202. package/.agent/plugins/poster-plugin/src/elements/artText.js +0 -69
  203. package/.agent/plugins/poster-plugin/src/elements/background.js +0 -99
  204. package/.agent/plugins/poster-plugin/src/elements/circle.js +0 -31
  205. package/.agent/plugins/poster-plugin/src/elements/image.js +0 -28
  206. package/.agent/plugins/poster-plugin/src/elements/index.js +0 -28
  207. package/.agent/plugins/poster-plugin/src/elements/line.js +0 -23
  208. package/.agent/plugins/poster-plugin/src/elements/polygon.js +0 -63
  209. package/.agent/plugins/poster-plugin/src/elements/rectangle.js +0 -32
  210. package/.agent/plugins/poster-plugin/src/elements/richText.js +0 -283
  211. package/.agent/plugins/poster-plugin/src/elements/svg.js +0 -108
  212. package/.agent/plugins/poster-plugin/src/elements/text.js +0 -112
  213. package/.agent/plugins/poster-plugin/src/fonts.js +0 -674
  214. package/.agent/plugins/poster-plugin/src/index.js +0 -2126
  215. package/.agent/plugins/poster-plugin/src/presets.js +0 -36
  216. package/.agent/plugins/poster-plugin/src/templates/business.js +0 -60
  217. package/.agent/plugins/poster-plugin/src/templates/gradient.js +0 -64
  218. package/.agent/plugins/poster-plugin/src/templates/index.js +0 -43
  219. package/.agent/plugins/poster-plugin/src/templates/modern.js +0 -69
  220. package/.agent/plugins/poster-plugin/src/templates/simple.js +0 -58
  221. package/.agent/plugins/poster-plugin/src/templates/social.js +0 -62
  222. package/.agent/plugins/poster-plugin/src/templates/tech.js +0 -84
  223. package/.agent/plugins/poster-plugin/src/utils/imageLoader.js +0 -84
  224. package/.agent/plugins/poster-plugin/yarn.lock +0 -837
  225. package/.agent/plugins/puppeteer-plugin/README.md +0 -147
  226. package/.agent/plugins/puppeteer-plugin/index.js +0 -1422
  227. package/.agent/plugins/puppeteer-plugin/package.json +0 -9
  228. package/.agent/rules/GEMINI.md +0 -273
  229. package/.agent/rules/allow-rule.md +0 -77
  230. package/.agent/rules/log-rule.md +0 -83
  231. package/.agent/rules/security-rule.md +0 -93
  232. package/.agent/scripts/auto_preview.py +0 -148
  233. package/.agent/scripts/checklist.py +0 -217
  234. package/.agent/scripts/session_manager.py +0 -120
  235. package/.agent/scripts/verify_all.py +0 -327
  236. package/.agent/skills/api-patterns/SKILL.md +0 -81
  237. package/.agent/skills/api-patterns/api-style.md +0 -42
  238. package/.agent/skills/api-patterns/auth.md +0 -24
  239. package/.agent/skills/api-patterns/documentation.md +0 -26
  240. package/.agent/skills/api-patterns/graphql.md +0 -41
  241. package/.agent/skills/api-patterns/rate-limiting.md +0 -31
  242. package/.agent/skills/api-patterns/response.md +0 -37
  243. package/.agent/skills/api-patterns/rest.md +0 -40
  244. package/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
  245. package/.agent/skills/api-patterns/security-testing.md +0 -122
  246. package/.agent/skills/api-patterns/trpc.md +0 -41
  247. package/.agent/skills/api-patterns/versioning.md +0 -22
  248. package/.agent/skills/app-builder/SKILL.md +0 -75
  249. package/.agent/skills/app-builder/agent-coordination.md +0 -71
  250. package/.agent/skills/app-builder/feature-building.md +0 -53
  251. package/.agent/skills/app-builder/project-detection.md +0 -34
  252. package/.agent/skills/app-builder/scaffolding.md +0 -118
  253. package/.agent/skills/app-builder/tech-stack.md +0 -40
  254. package/.agent/skills/app-builder/templates/SKILL.md +0 -39
  255. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
  256. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
  257. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
  258. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
  259. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
  260. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
  261. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
  262. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -122
  263. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -122
  264. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -169
  265. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -134
  266. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
  267. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -119
  268. package/.agent/skills/architecture/SKILL.md +0 -55
  269. package/.agent/skills/architecture/context-discovery.md +0 -43
  270. package/.agent/skills/architecture/examples.md +0 -94
  271. package/.agent/skills/architecture/pattern-selection.md +0 -68
  272. package/.agent/skills/architecture/patterns-reference.md +0 -50
  273. package/.agent/skills/architecture/trade-off-analysis.md +0 -77
  274. package/.agent/skills/clean-code/SKILL.md +0 -201
  275. package/.agent/skills/doc.md +0 -177
  276. package/.agent/skills/frontend-design/SKILL.md +0 -418
  277. package/.agent/skills/frontend-design/animation-guide.md +0 -331
  278. package/.agent/skills/frontend-design/color-system.md +0 -311
  279. package/.agent/skills/frontend-design/decision-trees.md +0 -418
  280. package/.agent/skills/frontend-design/motion-graphics.md +0 -306
  281. package/.agent/skills/frontend-design/scripts/accessibility_checker.py +0 -183
  282. package/.agent/skills/frontend-design/scripts/ux_audit.py +0 -722
  283. package/.agent/skills/frontend-design/typography-system.md +0 -345
  284. package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
  285. package/.agent/skills/frontend-design/visual-effects.md +0 -383
  286. package/.agent/skills/i18n-localization/SKILL.md +0 -154
  287. package/.agent/skills/i18n-localization/scripts/i18n_checker.py +0 -241
  288. package/.agent/skills/mcp-builder/SKILL.md +0 -176
  289. package/.agent/skills/poster-design/SKILL.md +0 -385
  290. package/.agent/skills/web-design-guidelines/SKILL.md +0 -57
  291. package/.agent/workflows/brainstorm.md +0 -113
  292. package/.agent/workflows/create.md +0 -59
  293. package/.agent/workflows/debug.md +0 -103
  294. package/.agent/workflows/deploy.md +0 -176
  295. package/.agent/workflows/enhance.md +0 -63
  296. package/.agent/workflows/orchestrate.md +0 -237
  297. package/.agent/workflows/plan.md +0 -89
  298. package/.agent/workflows/preview.md +0 -81
  299. package/.agent/workflows/simple-test.md +0 -42
  300. package/.agent/workflows/status.md +0 -86
  301. package/.agent/workflows/structured-orchestrate.md +0 -180
  302. package/.agent/workflows/test.md +0 -144
  303. package/.agent/workflows/ui-ux-pro-max.md +0 -296
  304. package/outputs/emoji-font-showcase.png +0 -0
  305. package/outputs/foliko-muji-style.png +0 -0
  306. /package/.agent/plugins/{poster-plugin → marknative}/fonts/SegoeUI Emoji.ttf +0 -0
@@ -1,2314 +0,0 @@
1
- /**
2
- * 海报组件化生成器
3
- *
4
- * 支持 JSON 配置驱动,一次调用生成完整海报
5
- */
6
-
7
- const paper = require('paper')
8
-
9
- // 导入新组件
10
- const createButton = require('./components/button')
11
- const createIcon = require('./components/icon')
12
- const createQRCode = require('./components/qrcode')
13
- const createFrame = require('./components/frame')
14
- const createBubble = require('./components/bubble')
15
- const createRibbon = require('./components/ribbon')
16
- const createSeal = require('./components/seal')
17
- const createHighlightText = require('./components/highlightText')
18
- const createBarcode = require('./components/barcode')
19
- const createQuote = require('./components/quote')
20
- const { loadImageAsRaster } = require('./utils/imageLoader')
21
-
22
- // 组件包装函数
23
- async function createButtonComponent(project, canvas, args) {
24
- return await createButton(project, args)
25
- }
26
- async function createIconComponent(project, canvas, args) {
27
- return await createIcon(project, args)
28
- }
29
- async function createQRCodeComponent(project, canvas, args) {
30
- return await createQRCode(project, args)
31
- }
32
- async function createFrameComponent(project, canvas, args) {
33
- return await createFrame(project, args)
34
- }
35
- async function createBubbleComponent(project, canvas, args) {
36
- return await createBubble(project, args)
37
- }
38
- async function createRibbonComponent(project, canvas, args) {
39
- return await createRibbon(project, args)
40
- }
41
- async function createSealComponent(project, canvas, args) {
42
- return await createSeal(project, args)
43
- }
44
- async function createHighlightTextComponent(project, canvas, args) {
45
- return await createHighlightText(project, args)
46
- }
47
- async function createBarcodeComponent(project, canvas, args) {
48
- return await createBarcode(project, args)
49
- }
50
- async function createQuoteComponentWrapper(project, canvas, args) {
51
- return await createQuote(project, canvas, args)
52
- }
53
-
54
- /**
55
- * 辅助函数:将元素添加到活跃层
56
- */
57
- function addToLayer(project, element) {
58
- if (project && project.activeLayer && element) {
59
- if (element.insert) {
60
- project.activeLayer.addChild(element)
61
- }
62
- }
63
- }
64
-
65
- /**
66
- * 辅助函数:批量添加元素到活跃层
67
- */
68
- function addAllToLayer(project, elements) {
69
- if (project && project.activeLayer && elements) {
70
- elements.forEach(el => {
71
- if (el.id && el.type) {
72
- const item = project.getItem({ id: el.id })
73
- if (item) project.activeLayer.addChild(item)
74
- }
75
- })
76
- }
77
- }
78
-
79
- /**
80
- * 组件类型注册表
81
- */
82
- const COMPONENT_TYPES = {
83
- // 基础元素
84
- background: 'background',
85
- rectangle: 'rectangle',
86
- circle: 'circle',
87
- line: 'line',
88
- polygon: 'polygon',
89
- text: 'text',
90
- artText: 'artText',
91
- image: 'image',
92
- svg: 'svg',
93
- imageFrame: 'imageFrame',
94
- // 布局组件
95
- columns: 'columns',
96
- grid: 'grid',
97
- // 装饰组件
98
- star: 'star',
99
- arrow: 'arrow',
100
- progressCircle: 'progressCircle',
101
- chip: 'chip',
102
- chart: 'chart',
103
- watermark: 'watermark',
104
- table: 'table',
105
- // 高级组件
106
- card: 'card',
107
- badge: 'badge',
108
- cta: 'cta',
109
- feature: 'feature',
110
- featureGrid: 'featureGrid',
111
- divider: 'divider',
112
- avatar: 'avatar',
113
- progress: 'progress',
114
- rating: 'rating',
115
- quote: 'quote',
116
- statCard: 'statCard',
117
- tagCloud: 'tagCloud',
118
- stepper: 'stepper',
119
- timeline: 'timeline',
120
- listItem: 'listItem',
121
- notification: 'notification',
122
- // 设计组件
123
- button: 'button',
124
- icon: 'icon',
125
- qrcode: 'qrcode',
126
- frame: 'frame',
127
- bubble: 'bubble',
128
- ribbon: 'ribbon',
129
- seal: 'seal',
130
- highlightText: 'highlightText',
131
- barcode: 'barcode',
132
- }
133
-
134
- /**
135
- * 从配置创建海报
136
- *
137
- * @param {Object} project - Paper.js 项目
138
- * @param {Object} canvas - 画布对象
139
- * @param {Object} config - 海报配置
140
- * @returns {Object} 创建结果
141
- */
142
- async function createFromConfig(project, canvas, config) {
143
- const { components = [] } = config
144
- const results = []
145
-
146
- for (const component of components) {
147
- const result = await createComponent(project, canvas, component)
148
- results.push(result)
149
- }
150
-
151
- // 将所有创建的元素添加到活动层
152
- if (project && project.activeLayer) {
153
- project.activeLayer.addChildren(project.layers.flatMap(l => l.children))
154
- }
155
-
156
- return {
157
- success: true,
158
- componentCount: results.length,
159
- results,
160
- }
161
- }
162
-
163
- /**
164
- * 根据配置创建单个组件
165
- */
166
- async function createComponent(project, canvas, config) {
167
- const { type, ...args } = config
168
-
169
- switch (type) {
170
- // 基础元素
171
- case 'background':
172
- return await createBackgroundElement(project, canvas, args)
173
- case 'rectangle':
174
- return createRectangleElement(project, args)
175
- case 'circle':
176
- return createCircleElement(project, args)
177
- case 'line':
178
- return createLineElement(project, args)
179
- case 'polygon':
180
- return createPolygonElement(project, args)
181
- case 'text':
182
- return createTextElement(project, args)
183
- case 'artText':
184
- return await createArtTextElement(project, args)
185
- case 'richText':
186
- return await createRichTextElement(project, args)
187
- case 'image':
188
- return await createImageElement(project, args)
189
- case 'svg':
190
- return await createSVGElement(project, args)
191
- case 'imageFrame':
192
- return await createImageFrameComponent(project, canvas, args)
193
- case 'columns':
194
- return await createColumnsComponent(project, canvas, args)
195
- case 'grid':
196
- return await createGridComponent(project, canvas, args)
197
-
198
- // 装饰组件
199
- case 'star':
200
- return await createStarComponent(project, canvas, args)
201
- case 'arrow':
202
- return await createArrowComponent(project, canvas, args)
203
- case 'progressCircle':
204
- return await createProgressCircleComponent(project, canvas, args)
205
- case 'chip':
206
- return await createChipComponent(project, canvas, args)
207
- case 'chart':
208
- return await createChartComponent(project, canvas, args)
209
- case 'watermark':
210
- return await createWatermarkComponent(project, canvas, args)
211
- case 'table':
212
- return await createTableComponent(project, canvas, args)
213
-
214
- // 高级组件
215
- case 'card':
216
- return await createCardComponent(project, canvas, args)
217
- case 'badge':
218
- return await createBadgeComponent(project, canvas, args)
219
- case 'cta':
220
- return await createCTAComponent(project, canvas, args)
221
- case 'feature':
222
- return await createFeatureComponent(project, canvas, args)
223
- case 'featureGrid':
224
- return await createFeatureGridComponent(project, canvas, args)
225
- case 'divider':
226
- return await createDividerComponent(project, canvas, args)
227
- case 'avatar':
228
- return await createAvatarComponent(project, canvas, args)
229
- case 'progress':
230
- return await createProgressComponent(project, canvas, args)
231
- case 'rating':
232
- return await createRatingComponent(project, canvas, args)
233
- case 'quote':
234
- return await createQuoteComponentWrapper(project, canvas, args)
235
- case 'statCard':
236
- return await createStatCardComponent(project, canvas, args)
237
- case 'tagCloud':
238
- return await createTagCloudComponent(project, canvas, args)
239
- case 'stepper':
240
- return await createStepperComponent(project, canvas, args)
241
- case 'timeline':
242
- return await createTimelineComponent(project, canvas, args)
243
- case 'listItem':
244
- return await createListItemComponent(project, canvas, args)
245
- case 'notification':
246
- return await createNotificationComponent(project, canvas, args)
247
-
248
- // 设计组件
249
- case 'button':
250
- return await createButtonComponent(project, canvas, args)
251
- case 'icon':
252
- return await createIconComponent(project, canvas, args)
253
- case 'qrcode':
254
- return await createQRCodeComponent(project, canvas, args)
255
- case 'frame':
256
- return await createFrameComponent(project, canvas, args)
257
- case 'bubble':
258
- return await createBubbleComponent(project, canvas, args)
259
- case 'ribbon':
260
- return await createRibbonComponent(project, canvas, args)
261
- case 'seal':
262
- return await createSealComponent(project, canvas, args)
263
- case 'highlightText':
264
- return await createHighlightTextComponent(project, canvas, args)
265
- case 'barcode':
266
- return createBarcodeComponent(project, canvas, args)
267
-
268
- default:
269
- return { success: false, error: `Unknown component type: ${type}` }
270
- }
271
- }
272
-
273
- // ============= 基础元素创建函数 =============
274
-
275
- function createBackgroundElement(project, canvas, { color, gradient, image }) {
276
- if (image) {
277
- const fs = require('fs')
278
- const path = require('path')
279
-
280
- // 确保 image 是字符串
281
- if (typeof image !== 'string') {
282
- return { success: false, error: 'Background image must be a string' }
283
- }
284
-
285
- // 本地文件路径
286
- let absolutePath = image
287
- if (!path.isAbsolute(absolutePath)) {
288
- absolutePath = path.join(process.cwd(), absolutePath)
289
- }
290
-
291
- if (!fs.existsSync(absolutePath)) {
292
- throw new Error(`背景图片文件不存在: ${absolutePath}`)
293
- }
294
-
295
- const buffer = fs.readFileSync(absolutePath)
296
- const ext = path.extname(absolutePath).toLowerCase()
297
- const mimeTypes = {
298
- '.png': 'image/png',
299
- '.jpg': 'image/jpeg',
300
- '.jpeg': 'image/jpeg',
301
- '.gif': 'image/gif',
302
- '.webp': 'image/webp',
303
- '.bmp': 'image/bmp'
304
- }
305
- const mimeType = mimeTypes[ext] || 'image/png'
306
- const imageUrl = `data:${mimeType};base64,${buffer.toString('base64')}`
307
-
308
- const raster = new paper.Raster(imageUrl)
309
-
310
- // 添加到项目活动层
311
- if (project && project.activeLayer) {
312
- project.activeLayer.addChild(raster)
313
- }
314
-
315
- raster.onLoad = () => {
316
- // 计算缩放比例,使图片覆盖整个画布(cover 模式)
317
- const canvasRatio = canvas.width / canvas.height
318
- const imageRatio = raster.width / raster.height
319
-
320
- let scaledWidth, scaledHeight, offsetX, offsetY
321
-
322
- if (imageRatio > canvasRatio) {
323
- // 图片更宽,以高度为基准缩放
324
- scaledHeight = canvas.height
325
- scaledWidth = raster.width * (canvas.height / raster.height)
326
- offsetX = (canvas.width - scaledWidth) / 2
327
- offsetY = 0
328
- } else {
329
- // 图片更高,以宽度为基准缩放
330
- scaledWidth = canvas.width
331
- scaledHeight = raster.height * (canvas.width / raster.width)
332
- offsetX = 0
333
- offsetY = (canvas.height - scaledHeight) / 2
334
- }
335
-
336
- raster.bounds = new paper.Rectangle(offsetX, offsetY, scaledWidth, scaledHeight)
337
- raster.sendToBack()
338
- }
339
- } else if (gradient) {
340
- const paperColors = gradient.colors.map(c => new paper.Color(c))
341
- const { type, direction } = gradient
342
-
343
- if (type === 'linear') {
344
- const angle = (direction || 45) * Math.PI / 180
345
- const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2)
346
- const centerX = canvas.width / 2
347
- const centerY = canvas.height / 2
348
- const start = new paper.Point(
349
- centerX - Math.cos(angle) * diagonal / 2,
350
- centerY - Math.sin(angle) * diagonal / 2
351
- )
352
- const stop = new paper.Point(
353
- centerX + Math.cos(angle) * diagonal / 2,
354
- centerY + Math.sin(angle) * diagonal / 2
355
- )
356
- project.activeLayer.fillColor = new paper.Color({
357
- gradient: { stops: paperColors },
358
- origin: start,
359
- destination: stop,
360
- })
361
- } else {
362
- const center = new paper.Point(canvas.width / 2, canvas.height / 2)
363
- const radius = Math.max(canvas.width, canvas.height) / 2
364
- project.activeLayer.fillColor = new paper.Color({
365
- gradient: { stops: paperColors },
366
- origin: center,
367
- destination: center.add(new paper.Point(radius, 0)),
368
- })
369
- }
370
- } else if (color) {
371
- project.activeLayer.fillColor = new paper.Color(color)
372
- }
373
-
374
- return { success: true, type: 'background' }
375
- }
376
-
377
- function createRectangleElement(project, { x, y, width, height, fill, stroke, strokeWidth, radius, opacity }) {
378
- const rect = new paper.Path.Rectangle({
379
- point: [x, y],
380
- size: [width, height],
381
- radius: radius || 0,
382
- })
383
-
384
- if (fill) rect.fillColor = new paper.Color(fill)
385
- if (stroke) {
386
- rect.strokeColor = new paper.Color(stroke)
387
- rect.strokeWidth = strokeWidth || 1
388
- }
389
- if (opacity !== undefined) rect.opacity = opacity
390
-
391
- if (project && project.activeLayer) {
392
- project.activeLayer.addChild(rect)
393
- }
394
-
395
- return { success: true, id: rect.id, type: 'rectangle' }
396
- }
397
-
398
- function createCircleElement(project, { x, y, cx, cy, radius, rx, ry, fill, stroke, strokeWidth, opacity }) {
399
- // 兼容 x, y, radius 或 cx, cy, rx, ry 格式
400
- const centerX = cx || x
401
- const centerY = cy || y
402
- const radiusX = rx || radius || 30
403
- const radiusY = ry || radius || 30
404
-
405
- const circle = new paper.Path.Ellipse({
406
- center: [centerX, centerY],
407
- radius: [radiusX, radiusY],
408
- })
409
-
410
- if (fill) circle.fillColor = new paper.Color(fill)
411
- if (stroke) {
412
- circle.strokeColor = new paper.Color(stroke)
413
- circle.strokeWidth = strokeWidth || 1
414
- }
415
- if (opacity !== undefined) circle.opacity = opacity
416
-
417
- if (project && project.activeLayer) {
418
- project.activeLayer.addChild(circle)
419
- }
420
-
421
- return { success: true, id: circle.id, type: 'circle' }
422
- }
423
-
424
- function createLineElement(project, { x1, y1, x2, y2, stroke, strokeWidth }) {
425
- const line = new paper.Path.Line({
426
- from: [x1, y1],
427
- to: [x2, y2],
428
- strokeColor: new paper.Color(stroke || '#ffffff'),
429
- strokeWidth: strokeWidth || 2,
430
- })
431
-
432
- if (project && project.activeLayer) {
433
- project.activeLayer.addChild(line)
434
- }
435
-
436
- return { success: true, id: line.id, type: 'line' }
437
- }
438
-
439
- function createPolygonElement(project, { cx, cy, radius, sides, fill, stroke, strokeWidth, opacity }) {
440
- const polygon = new paper.Path.RegularPolygon({
441
- center: [cx, cy],
442
- radius: radius,
443
- sides: sides,
444
- })
445
-
446
- if (fill) polygon.fillColor = new paper.Color(fill)
447
- if (stroke) {
448
- polygon.strokeColor = new paper.Color(stroke)
449
- polygon.strokeWidth = strokeWidth || 1
450
- }
451
- if (opacity !== undefined) polygon.opacity = opacity
452
-
453
- if (project && project.activeLayer) {
454
- project.activeLayer.addChild(polygon)
455
- }
456
-
457
- return { success: true, id: polygon.id, type: 'polygon' }
458
- }
459
-
460
- function createTextElement(project, { text, x, y, fontSize, fontFamily, color, align, shadow }) {
461
- const { validateFont, getDefaultFontFamily, getFontFallbackChain } = require('./fonts')
462
-
463
- const fontSizeVal = fontSize || 48
464
- const textColor = color || '#ffffff'
465
- const alignment = align || 'left'
466
-
467
- // 计算文字宽度用于居中/右对齐
468
- let offsetX = 0
469
- if (alignment === 'center' || alignment === 'right') {
470
- // 估算文字宽度:中文约 1.0 倍字体大小,英文约 0.5 倍
471
- const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length
472
- const otherChars = text.length - chineseChars
473
- const textWidth = chineseChars * fontSizeVal * 1.0 + otherChars * fontSizeVal * 0.5
474
-
475
- if (alignment === 'center') {
476
- offsetX = -textWidth / 2
477
- } else if (alignment === 'right') {
478
- offsetX = -textWidth
479
- }
480
- }
481
-
482
- // 获取字体链(支持 @napi-rs/canvas 字体回退)
483
- const chain = getFontFallbackChain(fontFamily, text)
484
- const finalFontFamily = chain.length === 1 ? chain[0] : chain.join(', ')
485
-
486
- const textItem = new paper.PointText({
487
- point: [x + offsetX, y],
488
- content: text,
489
- fontSize: fontSizeVal,
490
- fontFamily: finalFontFamily,
491
- fillColor: new paper.Color(textColor),
492
- justification: alignment,
493
- })
494
-
495
- if (shadow) {
496
- textItem.shadowColor = new paper.Color(shadow.color)
497
- textItem.shadowBlur = shadow.blur || 5
498
- textItem.shadowOffset = new paper.Point(shadow.offsetX || 2, shadow.offsetY || 2)
499
- }
500
-
501
- if (project && project.activeLayer) {
502
- project.activeLayer.addChild(textItem)
503
- }
504
-
505
- return { success: true, id: textItem.id, type: 'text' }
506
- }
507
-
508
- function createArtTextElement(project, { text, x, y, fontSize, fontFamily, gradient, strokeColor, strokeWidth, shadow }) {
509
- const { getFontFallbackChain } = require('./fonts')
510
-
511
- // 获取字体链
512
- const chain = getFontFallbackChain(fontFamily, text)
513
- const finalFont = chain.length === 1 ? chain[0] : chain.join(', ')
514
-
515
- const textItem = new paper.PointText({
516
- point: [x, y],
517
- content: text,
518
- fontSize: fontSize || 120,
519
- fontFamily: finalFont,
520
- fillColor: gradient ? new paper.Color(gradient.colors[0]) : new paper.Color('#ffffff'),
521
- justification: 'center',
522
- })
523
-
524
- if (gradient && gradient.colors.length > 0) {
525
- const colors = gradient.colors.map(c => new paper.Color(c))
526
- textItem.fillColor = new paper.Color({
527
- gradient: { stops: colors },
528
- origin: textItem.bounds.topLeft,
529
- destination: textItem.bounds.topRight,
530
- })
531
- }
532
-
533
- if (strokeColor) {
534
- textItem.strokeColor = new paper.Color(strokeColor)
535
- textItem.strokeWidth = strokeWidth || 2
536
- }
537
-
538
- if (shadow) {
539
- textItem.shadowColor = new paper.Color(shadow.color)
540
- textItem.shadowBlur = shadow.blur || 10
541
- textItem.shadowOffset = new paper.Point(shadow.offsetX || 3, shadow.offsetY || 3)
542
- }
543
-
544
- if (project && project.activeLayer) {
545
- project.activeLayer.addChild(textItem)
546
- }
547
-
548
- return { success: true, id: textItem.id, type: 'artText' }
549
- }
550
-
551
- async function createImageElement(project, { src, x = 0, y = 0, width, height, opacity = 1 }) {
552
- try {
553
- const { raster } = await loadImageAsRaster(project, src, { x, y, width, height }, opacity)
554
-
555
- return {
556
- success: true,
557
- id: raster.id,
558
- type: 'image',
559
- }
560
- } catch (err) {
561
- return { success: false, error: `Failed to load image: ${err.message}` }
562
- }
563
- }
564
-
565
- /**
566
- * 创建富文本元素(支持多行文本和自动换行)
567
- */
568
- function createRichTextElement(project, {
569
- x = 0,
570
- y = 0,
571
- width,
572
- text = '',
573
- fontSize = 48,
574
- fontFamily = 'sans-serif',
575
- color = '#ffffff',
576
- align = 'left',
577
- lineHeight = fontSize * 1.4,
578
- letterSpacing = 0,
579
- opacity = 1,
580
- rotation = 0,
581
- shadow = null,
582
- }) {
583
- const { getFontFallbackChain } = require('./fonts')
584
-
585
- // 获取字体链
586
- const chain = getFontFallbackChain(fontFamily, text)
587
- const finalFontFamily = chain.length === 1 ? chain[0] : chain.join(', ')
588
-
589
- // 估算字符宽度(中文约等于 fontSize,英文约等于 fontSize * 0.5)
590
- const getCharWidth = (char) => {
591
- return /[\u4e00-\u9fa5]/.test(char) ? fontSize : fontSize * 0.5
592
- }
593
-
594
- // 计算文本宽度
595
- const calcTextWidth = (str) => {
596
- let width = 0
597
- for (const char of str) {
598
- width += getCharWidth(char)
599
- }
600
- return width + (str.length - 1) * letterSpacing
601
- }
602
-
603
- // 处理自动换行
604
- const lines = []
605
- if (width) {
606
- const paragraphs = text.split('\n')
607
- for (const paragraph of paragraphs) {
608
- if (!paragraph) {
609
- lines.push('')
610
- continue
611
- }
612
- let currentLine = ''
613
- let currentWidth = 0
614
- for (const char of paragraph) {
615
- const charWidth = getCharWidth(char) + letterSpacing
616
- if (currentWidth + charWidth > width && currentLine) {
617
- lines.push(currentLine)
618
- currentLine = char
619
- currentWidth = charWidth
620
- } else {
621
- currentLine += char
622
- currentWidth += charWidth
623
- }
624
- }
625
- if (currentLine) {
626
- lines.push(currentLine)
627
- }
628
- }
629
- } else {
630
- lines.push(text)
631
- }
632
-
633
- // 创建文本元素组
634
- const group = new paper.Group()
635
- const textItems = []
636
-
637
- for (let i = 0; i < lines.length; i++) {
638
- const line = lines[i]
639
- const lineY = y + i * lineHeight
640
-
641
- // 计算对齐偏移
642
- let offsetX = 0
643
- if (align === 'center' && width) {
644
- offsetX = (width - calcTextWidth(line)) / 2
645
- } else if (align === 'right' && width) {
646
- offsetX = width - calcTextWidth(line)
647
- }
648
-
649
- const textItem = new paper.PointText({
650
- point: [x + offsetX, lineY + fontSize],
651
- content: line,
652
- fontSize: fontSize,
653
- fontFamily: finalFontFamily,
654
- fillColor: new paper.Color(color),
655
- justification: align,
656
- })
657
-
658
- if (letterSpacing !== 0) {
659
- textItem.letterSpacing = letterSpacing
660
- }
661
-
662
- if (opacity !== 1) {
663
- textItem.opacity = opacity
664
- }
665
-
666
- if (rotation !== 0) {
667
- textItem.rotate(rotation, new paper.Point(x, y))
668
- }
669
-
670
- if (shadow) {
671
- textItem.shadowColor = new paper.Color(shadow.color || 'rgba(0,0,0,0.5)')
672
- textItem.shadowBlur = shadow.blur || 5
673
- textItem.shadowOffset = new paper.Point(shadow.offsetX || 2, shadow.offsetY || 2)
674
- }
675
-
676
- textItems.push(textItem)
677
- group.addChild(textItem)
678
- }
679
-
680
- // 添加到项目
681
- if (project && project.activeLayer) {
682
- project.activeLayer.addChild(group)
683
- }
684
-
685
- return {
686
- success: true,
687
- id: group.id,
688
- type: 'richText',
689
- lines: lines.length,
690
- height: lines.length * lineHeight,
691
- }
692
- }
693
-
694
- const fs = require('fs')
695
-
696
- async function createSVGElement(project, { src, x = 0, y = 0, width, height, opacity = 1 }) {
697
- const fs = require('fs')
698
- const path = require('path')
699
-
700
- // 确保 src 是字符串
701
- if (typeof src !== 'string') {
702
- return { success: false, error: 'SVG source must be a string' }
703
- }
704
-
705
- let svgContent = src
706
-
707
- // 如果是文件路径,读取文件内容
708
- if (!src.startsWith('<') && !src.startsWith('<?xml')) {
709
- try {
710
- let filePath = src
711
- if (!path.isAbsolute(filePath)) {
712
- filePath = path.join(process.cwd(), filePath)
713
- }
714
- svgContent = fs.readFileSync(filePath, 'utf8')
715
- } catch (e) {
716
- return { success: false, error: `Failed to read SVG file: ${e.message}` }
717
- }
718
- }
719
-
720
- // 导入 SVG 到指定项目
721
- let svg
722
- try {
723
- svg = project.importSVG(svgContent)
724
- } catch (e) {
725
- return { success: false, error: `Failed to import SVG: ${e.message}` }
726
- }
727
-
728
- if (!svg) {
729
- return { success: false, error: 'Failed to import SVG' }
730
- }
731
-
732
- // 确保 SVG 添加到活动层
733
- if (project && project.activeLayer && svg.parent !== project.activeLayer) {
734
- project.activeLayer.addChild(svg)
735
- }
736
-
737
- // 设置位置
738
- svg.position = new paper.Point(x, y)
739
-
740
- // 设置尺寸
741
- if (width && height) {
742
- const scaleX = width / svg.bounds.width
743
- const scaleY = height / svg.bounds.height
744
- svg.scale(Math.min(scaleX, scaleY), svg.bounds.center)
745
- } else if (width) {
746
- svg.scale(width / svg.bounds.width, svg.bounds.center)
747
- } else if (height) {
748
- svg.scale(height / svg.bounds.height, svg.bounds.center)
749
- }
750
-
751
- // 设置透明度
752
- if (opacity !== undefined) {
753
- svg.opacity = opacity
754
- }
755
-
756
- return {
757
- success: true,
758
- id: svg.id,
759
- type: 'svg',
760
- width: svg.bounds.width,
761
- height: svg.bounds.height,
762
- }
763
- }
764
-
765
- // ============= 高级组件创建函数 =============
766
-
767
- function createCardComponent(project, canvas, {
768
- x, y, width, height,
769
- background, border, borderWidth, radius,
770
- title, titleSize, titleColor,
771
- subtitle, subtitleSize, subtitleColor,
772
- padding = 20,
773
- }) {
774
- const elements = []
775
-
776
- // 卡片背景
777
- const card = new paper.Path.Rectangle({
778
- point: [x, y],
779
- size: [width, height],
780
- radius: radius || 0,
781
- })
782
- card.fillColor = new paper.Color(background || '#ffffff')
783
- if (border) {
784
- card.strokeColor = new paper.Color(border)
785
- card.strokeWidth = borderWidth || 1
786
- }
787
- elements.push({ type: 'rectangle', id: card.id })
788
-
789
- // 标题
790
- if (title) {
791
- const titleText = new paper.PointText({
792
- point: [x + padding, y + padding + (titleSize || 24)],
793
- content: title,
794
- fontSize: titleSize || 24,
795
- fillColor: new paper.Color(titleColor || '#000000'),
796
- justification: 'left',
797
- })
798
- elements.push({ type: 'text', id: titleText.id })
799
- }
800
-
801
- // 副标题
802
- if (subtitle) {
803
- const subY = title ? y + padding + (titleSize || 24) + (subtitleSize || 16) + 10 : y + padding
804
- const subtitleText = new paper.PointText({
805
- point: [x + padding, subY],
806
- content: subtitle,
807
- fontSize: subtitleSize || 16,
808
- fillColor: new paper.Color(subtitleColor || '#666666'),
809
- justification: 'left',
810
- })
811
- elements.push({ type: 'text', id: subtitleText.id })
812
- }
813
-
814
- return { success: true, elements, type: 'card' }
815
- }
816
-
817
- function createBadgeComponent(project, canvas, {
818
- x, y, text,
819
- background = '#007bff', color = '#ffffff',
820
- border, fontSize = 18, padding = 15, radius = 4,
821
- }) {
822
- const textWidth = text.length * fontSize * 0.6
823
- const badgeWidth = textWidth + padding * 2
824
- const badgeHeight = fontSize + padding * 2
825
- const badgeX = x - badgeWidth / 2
826
-
827
- const badge = new paper.Path.Rectangle({
828
- point: [badgeX, y],
829
- size: [badgeWidth, badgeHeight],
830
- radius: radius,
831
- })
832
- badge.fillColor = new paper.Color(background)
833
- if (border) badge.strokeColor = new paper.Color(border)
834
-
835
- const badgeText = new paper.PointText({
836
- point: [x, y + badgeHeight / 2 + fontSize / 3],
837
- content: text,
838
- fontSize: fontSize,
839
- fillColor: new paper.Color(color),
840
- justification: 'center',
841
- })
842
-
843
- return { success: true, elements: [{ type: 'rectangle', id: badge.id }, { type: 'text', id: badgeText.id }], type: 'badge' }
844
- }
845
-
846
- function createCTAComponent(project, canvas, {
847
- x, y, text,
848
- background = '#007bff', color = '#ffffff',
849
- border, fontSize = 20, padding = 25, radius = 8, shadow,
850
- width: customWidth,
851
- }) {
852
- // 确保 text 是字符串
853
- const textStr = String(text || '')
854
- // 使用更准确的字符宽度估算:中文约1.0,英文约0.5
855
- const chineseChars = (textStr.match(/[\u4e00-\u9fa5]/g) || []).length
856
- const otherChars = textStr.length - chineseChars
857
- const textWidth = chineseChars * fontSize * 1.0 + otherChars * fontSize * 0.5
858
- const btnWidth = customWidth || (textWidth + padding * 2)
859
- const btnHeight = fontSize + padding * 2
860
- const btnX = x - btnWidth / 2
861
-
862
- const button = new paper.Path.Rectangle({
863
- point: [btnX, y],
864
- size: [btnWidth, btnHeight],
865
- radius: radius,
866
- })
867
- button.fillColor = new paper.Color(background)
868
- if (border) button.strokeColor = new paper.Color(border)
869
-
870
- if (shadow) {
871
- button.shadowColor = new paper.Color(shadow.color || 'rgba(0,0,0,0.3)')
872
- button.shadowBlur = shadow.blur || 10
873
- button.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
874
- }
875
-
876
- const buttonText = new paper.PointText({
877
- point: [x, y + btnHeight / 2 + fontSize / 3],
878
- content: textStr,
879
- fontSize: fontSize,
880
- fillColor: new paper.Color(color),
881
- justification: 'center',
882
- })
883
-
884
- // 添加到项目
885
- if (project && project.activeLayer) {
886
- project.activeLayer.addChild(button)
887
- project.activeLayer.addChild(buttonText)
888
- }
889
-
890
- return { success: true, elements: [{ type: 'rectangle', id: button.id }, { type: 'text', id: buttonText.id }], type: 'cta' }
891
- }
892
-
893
- function createFeatureComponent(project, canvas, {
894
- x, y, width,
895
- icon, title, description,
896
- iconColor = '#007bff', titleColor = '#ffffff', descColor = '#aaaaaa',
897
- iconSize = 32, titleSize = 20, descSize = 14,
898
- }) {
899
- const elements = []
900
- const padding = 15
901
- let currentY = y
902
-
903
- if (icon) {
904
- elements.push(new paper.PointText({
905
- point: [x + padding, currentY + iconSize],
906
- content: icon,
907
- fontSize: iconSize,
908
- fillColor: new paper.Color(iconColor),
909
- justification: 'left',
910
- }))
911
- currentY += iconSize + 5
912
- }
913
-
914
- if (title) {
915
- elements.push(new paper.PointText({
916
- point: [x + padding, currentY + titleSize],
917
- content: title,
918
- fontSize: titleSize,
919
- fillColor: new paper.Color(titleColor),
920
- justification: 'left',
921
- }))
922
- currentY += titleSize + 5
923
- }
924
-
925
- if (description) {
926
- elements.push(new paper.PointText({
927
- point: [x + padding, currentY + descSize],
928
- content: description,
929
- fontSize: descSize,
930
- fillColor: new paper.Color(descColor),
931
- justification: 'left',
932
- }))
933
- }
934
-
935
- return { success: true, elements, type: 'feature' }
936
- }
937
-
938
- function createFeatureGridComponent(project, canvas, {
939
- x, y,
940
- columns = 3, itemWidth = 200, itemHeight = 120, gap = 20,
941
- items = [],
942
- background = '#1a1a2e', borderColor = '#00d9ff', radius = 8,
943
- }) {
944
- const elements = []
945
-
946
- // 确保 items 是数组
947
- if (!Array.isArray(items)) {
948
- items = []
949
- }
950
-
951
- const rows = items.length > 0 ? Math.ceil(items.length / columns) : 0
952
-
953
- for (let i = 0; i < items.length; i++) {
954
- const item = items[i]
955
- const col = i % columns
956
- const row = Math.floor(i / columns)
957
- const itemX = x + col * (itemWidth + gap)
958
- const itemY = y + row * (itemHeight + gap)
959
-
960
- const bg = new paper.Path.Rectangle({
961
- point: [itemX, itemY],
962
- size: [itemWidth, itemHeight],
963
- radius: radius,
964
- })
965
- bg.fillColor = new paper.Color(background)
966
- bg.strokeColor = new paper.Color(borderColor)
967
- bg.strokeWidth = 0.5
968
- bg.opacity = 0.8
969
- elements.push(bg)
970
-
971
- // 添加到项目
972
- if (project && project.activeLayer) {
973
- project.activeLayer.addChild(bg)
974
- }
975
-
976
- const padding = 15
977
- let offsetY = itemY + padding
978
-
979
- if (item.icon) {
980
- const iconText = new paper.PointText({
981
- point: [itemX + padding, offsetY + 24],
982
- content: item.icon,
983
- fontSize: 28,
984
- fillColor: new paper.Color(item.iconColor || '#00ff88'),
985
- justification: 'left',
986
- })
987
- elements.push(iconText)
988
- if (project && project.activeLayer) {
989
- project.activeLayer.addChild(iconText)
990
- }
991
- offsetY += 35
992
- }
993
-
994
- if (item.title) {
995
- const titleText = new paper.PointText({
996
- point: [itemX + padding, offsetY + 18],
997
- content: item.title,
998
- fontSize: 16,
999
- fillColor: new paper.Color(item.titleColor || '#ffffff'),
1000
- justification: 'left',
1001
- })
1002
- elements.push(titleText)
1003
- if (project && project.activeLayer) {
1004
- project.activeLayer.addChild(titleText)
1005
- }
1006
- offsetY += 22
1007
- }
1008
-
1009
- if (item.description) {
1010
- const descText = new paper.PointText({
1011
- point: [itemX + padding, offsetY + 14],
1012
- content: item.description,
1013
- fontSize: 12,
1014
- fillColor: new paper.Color(item.descColor || '#888888'),
1015
- justification: 'left',
1016
- })
1017
- elements.push(descText)
1018
- if (project && project.activeLayer) {
1019
- project.activeLayer.addChild(descText)
1020
- }
1021
- }
1022
- }
1023
-
1024
- return {
1025
- success: true,
1026
- elements,
1027
- type: 'featureGrid',
1028
- width: columns * itemWidth + (columns - 1) * gap,
1029
- height: rows * itemHeight + Math.max(0, rows - 1) * gap,
1030
- rows,
1031
- cols: columns,
1032
- }
1033
- }
1034
-
1035
- function createDividerComponent(project, canvas, {
1036
- x, y, width, color = '#00d9ff', thickness = 1, style = 'solid', align = 'center',
1037
- }) {
1038
- let startX = x
1039
- let endX = x + width
1040
-
1041
- if (align === 'center') {
1042
- startX = x - width / 2
1043
- endX = x + width / 2
1044
- } else if (align === 'right') {
1045
- startX = x - width
1046
- endX = x
1047
- }
1048
-
1049
- const line = new paper.Path.Line({
1050
- from: [startX, y],
1051
- to: [endX, y],
1052
- strokeColor: new paper.Color(color),
1053
- strokeWidth: thickness,
1054
- })
1055
-
1056
- if (style === 'dashed') line.dashArray = [10, 5]
1057
-
1058
- return { success: true, id: line.id, type: 'divider' }
1059
- }
1060
-
1061
- module.exports = {
1062
- createFromConfig,
1063
- createComponent,
1064
- COMPONENT_TYPES,
1065
- }
1066
-
1067
-
1068
- // ============= 新增组件创建函数 =============
1069
-
1070
- function createAvatarComponent(project, canvas, { x, y, size = 80, initials, background = '#6366f1', border, borderWidth = 0, color = '#ffffff' }) {
1071
- const elements = []
1072
- const radius = size / 2
1073
-
1074
- const circle = new paper.Path.Circle({
1075
- center: [x, y],
1076
- radius: radius,
1077
- })
1078
- circle.fillColor = new paper.Color(background)
1079
- if (border) {
1080
- circle.strokeColor = new paper.Color(border)
1081
- circle.strokeWidth = borderWidth
1082
- }
1083
- elements.push({ type: 'circle', id: circle.id })
1084
-
1085
- if (initials) {
1086
- const text = new paper.PointText({
1087
- point: [x, y + size / 6],
1088
- content: initials.charAt(0).toUpperCase(),
1089
- fontSize: size * 0.4,
1090
- fillColor: new paper.Color(color),
1091
- justification: 'center',
1092
- })
1093
- elements.push({ type: 'text', id: text.id })
1094
- }
1095
-
1096
- return { success: true, elements, type: 'avatar', size }
1097
- }
1098
-
1099
- function createProgressComponent(project, canvas, { x, y, width = 300, height = 20, value = 50, trackColor = '#e0e0e0', fillColor = '#6366f1', radius = 10, showLabel = false, label }) {
1100
- const elements = []
1101
-
1102
- const track = new paper.Path.Rectangle({
1103
- point: [x, y],
1104
- size: [width, height],
1105
- radius: radius,
1106
- })
1107
- track.fillColor = new paper.Color(trackColor)
1108
- elements.push({ type: 'rectangle', id: track.id })
1109
-
1110
- const progressWidth = (value / 100) * width
1111
- if (progressWidth > 0) {
1112
- const fill = new paper.Path.Rectangle({
1113
- point: [x, y],
1114
- size: [progressWidth, height],
1115
- radius: radius,
1116
- })
1117
- fill.fillColor = new paper.Color(fillColor)
1118
- elements.push({ type: 'rectangle', id: fill.id })
1119
- }
1120
-
1121
- if (showLabel && label) {
1122
- const labelText = new paper.PointText({
1123
- point: [x + width / 2, y - 8],
1124
- content: label,
1125
- fontSize: 14,
1126
- fillColor: new paper.Color('#666666'),
1127
- justification: 'center',
1128
- })
1129
- elements.push({ type: 'text', id: labelText.id })
1130
- }
1131
-
1132
- return { success: true, elements, value }
1133
- }
1134
-
1135
- function createRatingComponent(project, canvas, { x, y, value = 4, max = 5, size = 24, filledColor = '#fbbf24', emptyColor = '#e5e7eb', gap = 4 }) {
1136
- const elements = []
1137
-
1138
- for (let i = 0; i < max; i++) {
1139
- const starX = x + i * (size + gap)
1140
- const filled = i < Math.floor(value)
1141
-
1142
- const star = new paper.Path.Star({
1143
- center: [starX + size / 2, y + size / 2],
1144
- points: 5,
1145
- radius1: size / 4,
1146
- radius2: size / 2,
1147
- })
1148
- star.fillColor = new paper.Color(filled ? filledColor : emptyColor)
1149
- elements.push({ type: 'polygon', id: star.id })
1150
- }
1151
-
1152
- return { success: true, elements, value }
1153
- }
1154
-
1155
- 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 }) {
1156
- const elements = []
1157
- const lineHeight = 22
1158
-
1159
- const bg = new paper.Path.Rectangle({
1160
- point: [x, y],
1161
- size: [width, author ? 80 + fontSize * 2 : 40 + fontSize * 1.5],
1162
- radius: radius,
1163
- })
1164
- bg.fillColor = new paper.Color(background)
1165
- elements.push({ type: 'rectangle', id: bg.id })
1166
-
1167
- const border = new paper.Path.Rectangle({
1168
- point: [x, y],
1169
- size: [borderWidth, author ? 80 + fontSize * 2 : 40 + fontSize * 1.5],
1170
- })
1171
- border.fillColor = new paper.Color(borderColor)
1172
- elements.push({ type: 'rectangle', id: border.id })
1173
-
1174
- const quoteMark = new paper.PointText({
1175
- point: [x + padding + 10, y + padding + fontSize],
1176
- content: '"',
1177
- fontSize: fontSize * 2,
1178
- fillColor: new paper.Color(borderColor),
1179
- justification: 'left',
1180
- })
1181
- elements.push({ type: 'text', id: quoteMark.id })
1182
-
1183
- const quoteText = new paper.PointText({
1184
- point: [x + padding + 30, y + padding + fontSize * 1.5],
1185
- content: text,
1186
- fontSize: fontSize,
1187
- fillColor: new paper.Color(textColor),
1188
- justification: 'left',
1189
- })
1190
- elements.push({ type: 'text', id: quoteText.id })
1191
-
1192
- if (author) {
1193
- const authorText = new paper.PointText({
1194
- point: [x + padding, y + padding + fontSize * 2.5 + 10],
1195
- content: `— ${author}`,
1196
- fontSize: fontSize * 0.8,
1197
- fillColor: new paper.Color(authorColor),
1198
- justification: 'left',
1199
- })
1200
- elements.push({ type: 'text', id: authorText.id })
1201
- }
1202
-
1203
- return { success: true, elements, type: 'quote' }
1204
- }
1205
-
1206
- 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 }) {
1207
- const elements = []
1208
-
1209
- const bg = new paper.Path.Rectangle({
1210
- point: [x, y],
1211
- size: [width, height],
1212
- radius: radius,
1213
- })
1214
- bg.fillColor = new paper.Color(background)
1215
- bg.strokeColor = new paper.Color(border)
1216
- bg.strokeWidth = 1
1217
- elements.push({ type: 'rectangle', id: bg.id })
1218
-
1219
- if (icon) {
1220
- elements.push(new paper.PointText({
1221
- point: [x + 20, y + 35],
1222
- content: icon,
1223
- fontSize: 24,
1224
- fillColor: new paper.Color(iconColor),
1225
- justification: 'left',
1226
- }))
1227
- }
1228
-
1229
- elements.push(new paper.PointText({
1230
- point: [x + 20, y + 50 + (icon ? 10 : 0)],
1231
- content: label,
1232
- fontSize: 14,
1233
- fillColor: new paper.Color('#64748b'),
1234
- justification: 'left',
1235
- }))
1236
-
1237
- elements.push(new paper.PointText({
1238
- point: [x + 20, y + 75 + (icon ? 10 : 0)],
1239
- content: value,
1240
- fontSize: 28,
1241
- fillColor: new paper.Color('#1e293b'),
1242
- justification: 'left',
1243
- }))
1244
-
1245
- if (change) {
1246
- const changeColor = positive ? '#22c55e' : '#ef4444'
1247
- const changeIcon = positive ? '↑' : '↓'
1248
- elements.push(new paper.PointText({
1249
- point: [x + 20, y + 95 + (icon ? 10 : 0)],
1250
- content: `${changeIcon} ${change}`,
1251
- fontSize: 14,
1252
- fillColor: new paper.Color(changeColor),
1253
- justification: 'left',
1254
- }))
1255
- }
1256
-
1257
- return { success: true, elements, type: 'statCard' }
1258
- }
1259
-
1260
- function createTagCloudComponent(project, canvas, { x, y, tags = [], fontSize = 14, padding = 12, gap = 10, maxWidth = 400 }) {
1261
- const elements = []
1262
-
1263
- // 确保 tags 是数组
1264
- if (!Array.isArray(tags) || tags.length === 0) {
1265
- return { success: true, elements: [], type: 'tagCloud', height: 0 }
1266
- }
1267
-
1268
- let currentX = x
1269
- let currentY = y
1270
- let rowHeight = 0
1271
-
1272
- for (const tag of tags) {
1273
- // 确保 tag.text 是字符串
1274
- const tagText = String(tag.text || '')
1275
- if (!tagText) continue
1276
-
1277
- const textWidth = tagText.length * fontSize * 0.6
1278
- const tagWidth = textWidth + padding * 2
1279
- const tagHeight = fontSize + padding * 2
1280
-
1281
- if (currentX + tagWidth > x + maxWidth && currentX > x) {
1282
- currentX = x
1283
- currentY += rowHeight + gap
1284
- rowHeight = 0
1285
- }
1286
-
1287
- const tagBg = new paper.Path.Rectangle({
1288
- point: [currentX, currentY],
1289
- size: [tagWidth, tagHeight],
1290
- radius: tagHeight / 2,
1291
- })
1292
- tagBg.fillColor = new paper.Color(tag.bgColor || '#e0e7ff')
1293
- elements.push({ type: 'rectangle', id: tagBg.id })
1294
-
1295
- // 添加到项目
1296
- if (project && project.activeLayer) {
1297
- project.activeLayer.addChild(tagBg)
1298
- }
1299
-
1300
- const tagTextEl = new paper.PointText({
1301
- point: [currentX + tagWidth / 2, currentY + tagHeight / 2 + fontSize / 3],
1302
- content: tagText,
1303
- fontSize: fontSize,
1304
- fillColor: new paper.Color(tag.color || '#4338ca'),
1305
- justification: 'center',
1306
- })
1307
- elements.push(tagTextEl)
1308
- if (project && project.activeLayer) {
1309
- project.activeLayer.addChild(tagTextEl)
1310
- }
1311
-
1312
- currentX += tagWidth + gap
1313
- rowHeight = Math.max(rowHeight, tagHeight)
1314
- }
1315
-
1316
- return { success: true, elements, type: 'tagCloud', height: rowHeight }
1317
- }
1318
-
1319
- function createStepperComponent(project, canvas, { x, y, width = 600, steps = [], currentStep = 0, activeColor = '#6366f1', inactiveColor = '#e5e7eb', completedColor = '#22c55e', circleSize = 40 }) {
1320
- const elements = []
1321
- const stepWidth = steps.length > 1 ? width / (steps.length - 1) : width
1322
- const lineY = y + circleSize / 2
1323
-
1324
- if (steps.length > 1) {
1325
- elements.push({
1326
- type: 'line',
1327
- id: new paper.Path.Line({
1328
- from: [x + circleSize / 2, lineY],
1329
- to: [x + width - circleSize / 2, lineY],
1330
- strokeColor: new paper.Color(inactiveColor),
1331
- strokeWidth: 2,
1332
- }).id
1333
- })
1334
- }
1335
-
1336
- for (let i = 0; i < steps.length; i++) {
1337
- const stepX = steps.length > 1 ? x + i * stepWidth : x
1338
- let color = inactiveColor
1339
- if (i < currentStep) color = completedColor
1340
- else if (i === currentStep) color = activeColor
1341
-
1342
- const circle = new paper.Path.Circle({
1343
- center: [stepX + circleSize / 2, lineY],
1344
- radius: circleSize / 2,
1345
- })
1346
- circle.fillColor = new paper.Color(color)
1347
- elements.push({ type: 'circle', id: circle.id })
1348
-
1349
- const icon = i < currentStep ? '✓' : String(i + 1)
1350
- elements.push(new paper.PointText({
1351
- point: [stepX + circleSize / 2, lineY + circleSize / 6],
1352
- content: icon,
1353
- fontSize: 16,
1354
- fillColor: new paper.Color('#ffffff'),
1355
- justification: 'center',
1356
- }))
1357
-
1358
- elements.push(new paper.PointText({
1359
- point: [stepX + circleSize / 2, y + circleSize + 20],
1360
- content: steps[i].title || `Step ${i + 1}`,
1361
- fontSize: 14,
1362
- fillColor: new paper.Color(i <= currentStep ? '#1e293b' : '#94a3b8'),
1363
- justification: 'center',
1364
- }))
1365
-
1366
- if (steps[i].description) {
1367
- elements.push(new paper.PointText({
1368
- point: [stepX + circleSize / 2, y + circleSize + 38],
1369
- content: steps[i].description,
1370
- fontSize: 11,
1371
- fillColor: new paper.Color('#94a3b8'),
1372
- justification: 'center',
1373
- }))
1374
- }
1375
- }
1376
-
1377
- return { success: true, elements, type: 'stepper' }
1378
- }
1379
-
1380
- function createTimelineComponent(project, canvas, { x, y, width = 500, items = [], lineColor = '#e2e8f0', dotColor = '#6366f1', dotSize = 16, gap = 60 }) {
1381
- const elements = []
1382
-
1383
- // 确保 items 是数组
1384
- if (!Array.isArray(items) || items.length === 0) {
1385
- return { success: true, elements: [], type: 'timeline', height: 0 }
1386
- }
1387
-
1388
- const centerX = x + 80
1389
- const contentX = x + 120
1390
-
1391
- if (items.length > 1) {
1392
- const mainLine = new paper.Path.Line({
1393
- from: [centerX, y + dotSize / 2],
1394
- to: [centerX, y + (items.length - 1) * gap + dotSize / 2],
1395
- strokeColor: new paper.Color(lineColor),
1396
- strokeWidth: 2,
1397
- })
1398
- elements.push({ type: 'line', id: mainLine.id })
1399
- if (project && project.activeLayer) {
1400
- project.activeLayer.addChild(mainLine)
1401
- }
1402
- }
1403
-
1404
- for (let i = 0; i < items.length; i++) {
1405
- const item = items[i]
1406
- if (!item) continue
1407
-
1408
- const itemY = y + i * gap
1409
- const isActive = item.active !== false
1410
-
1411
- const dot = new paper.Path.Circle({
1412
- center: [centerX, itemY + dotSize / 2],
1413
- radius: dotSize / 2,
1414
- })
1415
- dot.fillColor = new paper.Color(isActive ? dotColor : lineColor)
1416
- elements.push({ type: 'circle', id: dot.id })
1417
- if (project && project.activeLayer) {
1418
- project.activeLayer.addChild(dot)
1419
- }
1420
-
1421
- if (item.date) {
1422
- const dateText = new paper.PointText({
1423
- point: [x + 10, itemY + dotSize / 2 + 5],
1424
- content: item.date,
1425
- fontSize: 12,
1426
- fillColor: new paper.Color('#94a3b8'),
1427
- justification: 'left',
1428
- })
1429
- elements.push(dateText)
1430
- if (project && project.activeLayer) {
1431
- project.activeLayer.addChild(dateText)
1432
- }
1433
- }
1434
-
1435
- const titleText = new paper.PointText({
1436
- point: [contentX, itemY + dotSize / 2 + 5],
1437
- content: item.title || `Event ${i + 1}`,
1438
- fontSize: 16,
1439
- fillColor: new paper.Color(isActive ? '#1e293b' : '#94a3b8'),
1440
- justification: 'left',
1441
- })
1442
- elements.push(titleText)
1443
- if (project && project.activeLayer) {
1444
- project.activeLayer.addChild(titleText)
1445
- }
1446
-
1447
- if (item.description) {
1448
- const descText = new paper.PointText({
1449
- point: [contentX, itemY + dotSize / 2 + 28],
1450
- content: item.description,
1451
- fontSize: 13,
1452
- fillColor: new paper.Color('#64748b'),
1453
- justification: 'left',
1454
- })
1455
- elements.push(descText)
1456
- if (project && project.activeLayer) {
1457
- project.activeLayer.addChild(descText)
1458
- }
1459
- }
1460
- }
1461
-
1462
- return { success: true, elements, type: 'timeline', height: items.length * gap }
1463
- }
1464
-
1465
- function createListItemComponent(project, canvas, { x = 0, y = 0, width = 400, icon = '→', title, description, badge, badgeColor = '#6366f1', iconColor = '#6366f1', background = '#ffffff', borderColor = '#e5e7eb', height = 60, radius = 8 }) {
1466
- const elements = []
1467
-
1468
- // 添加到项目活动层
1469
- const addToProject = (item) => {
1470
- if (project && project.activeLayer) {
1471
- project.activeLayer.addChild(item)
1472
- }
1473
- elements.push(item)
1474
- }
1475
-
1476
- const bg = new paper.Path.Rectangle({
1477
- point: [x, y],
1478
- size: [width, height],
1479
- radius: radius,
1480
- })
1481
- bg.fillColor = new paper.Color(background)
1482
- bg.strokeColor = new paper.Color(borderColor)
1483
- bg.strokeWidth = 1
1484
- addToProject(bg)
1485
-
1486
- const iconText = new paper.PointText({
1487
- point: [x + 15, y + height / 2 + 6],
1488
- content: icon,
1489
- fontSize: 20,
1490
- fillColor: new paper.Color(iconColor),
1491
- justification: 'center',
1492
- })
1493
- addToProject(iconText)
1494
-
1495
- const titleText = new paper.PointText({
1496
- point: [x + 50, y + height / 2 - 5],
1497
- content: title || 'List Item',
1498
- fontSize: 16,
1499
- fillColor: new paper.Color('#1e293b'),
1500
- justification: 'left',
1501
- })
1502
- addToProject(titleText)
1503
-
1504
- if (description) {
1505
- const descText = new paper.PointText({
1506
- point: [x + 50, y + height / 2 + 15],
1507
- content: description,
1508
- fontSize: 12,
1509
- fillColor: new paper.Color('#64748b'),
1510
- justification: 'left',
1511
- })
1512
- addToProject(descText)
1513
- }
1514
-
1515
- if (badge) {
1516
- const badgeWidth = badge.length * 10 + 20
1517
- const badgeX = x + width - badgeWidth - 15
1518
- const badgeY = y + (height - 24) / 2
1519
-
1520
- const badgeRect = new paper.Path.Rectangle({
1521
- point: [badgeX, badgeY],
1522
- size: [badgeWidth, 24],
1523
- radius: 12,
1524
- })
1525
- badgeRect.fillColor = new paper.Color(badgeColor)
1526
- addToProject(badgeRect)
1527
-
1528
- const badgeText = new paper.PointText({
1529
- point: [badgeX + badgeWidth / 2, badgeY + 16],
1530
- content: badge,
1531
- fontSize: 12,
1532
- fillColor: new paper.Color('#ffffff'),
1533
- justification: 'center',
1534
- })
1535
- addToProject(badgeText)
1536
- }
1537
-
1538
- return { success: true, elements, type: 'listItem' }
1539
- }
1540
-
1541
- function createNotificationComponent(project, canvas, { x, y, width = 360, type = 'info', title, message, showIcon = true, radius = 12 }) {
1542
- const config = {
1543
- success: { icon: '✓', bgColor: '#dcfce7', iconColor: '#22c55e', borderColor: '#22c55e' },
1544
- warning: { icon: '⚠', bgColor: '#fef9c3', iconColor: '#eab308', borderColor: '#eab308' },
1545
- error: { icon: '✕', bgColor: '#fee2e2', iconColor: '#ef4444', borderColor: '#ef4444' },
1546
- info: { icon: 'ℹ', bgColor: '#dbeafe', iconColor: '#3b82f6', borderColor: '#3b82f6' },
1547
- }
1548
-
1549
- const c = config[type] || config.info
1550
- const padding = 16
1551
- const lineHeight = 22
1552
- const iconSize = 24
1553
- const height = padding * 2 + (title ? lineHeight + 8 : 0) + (message ? lineHeight : 0)
1554
- const elements = []
1555
-
1556
- const bg = new paper.Path.Rectangle({
1557
- point: [x, y],
1558
- size: [width, height],
1559
- radius: radius,
1560
- })
1561
- bg.fillColor = new paper.Color(c.bgColor)
1562
- bg.strokeColor = new paper.Color(c.borderColor)
1563
- bg.strokeWidth = 1
1564
- elements.push({ type: 'rectangle', id: bg.id })
1565
-
1566
- if (showIcon) {
1567
- elements.push(new paper.PointText({
1568
- point: [x + padding + iconSize / 2, y + padding + iconSize / 2 + 6],
1569
- content: c.icon,
1570
- fontSize: iconSize,
1571
- fillColor: new paper.Color(c.iconColor),
1572
- justification: 'center',
1573
- }))
1574
- }
1575
-
1576
- const textX = showIcon ? x + padding + iconSize + 12 : x + padding
1577
- let currentY = y + padding
1578
-
1579
- if (title) {
1580
- elements.push(new paper.PointText({
1581
- point: [textX, currentY + 18],
1582
- content: title,
1583
- fontSize: 16,
1584
- fillColor: new paper.Color('#1e293b'),
1585
- justification: 'left',
1586
- }))
1587
- currentY += lineHeight + 8
1588
- }
1589
-
1590
- if (message) {
1591
- elements.push(new paper.PointText({
1592
- point: [textX, currentY + 16],
1593
- content: message,
1594
- fontSize: 14,
1595
- fillColor: new paper.Color('#475569'),
1596
- justification: 'left',
1597
- }))
1598
- }
1599
-
1600
- return { success: true, elements, type: 'notification' }
1601
- }
1602
-
1603
- /**
1604
- * 创建图片框组件
1605
- */
1606
- async function createImageFrameComponent(project, canvas, {
1607
- src,
1608
- x,
1609
- y,
1610
- width,
1611
- height,
1612
- borderColor = '#ffffff',
1613
- borderWidth = 3,
1614
- outerColor = '#1a1a2e',
1615
- outerWidth = 6,
1616
- shadowBlur = 0,
1617
- shadowOffsetX = 0,
1618
- shadowOffsetY = 0,
1619
- shadowColor = 'rgba(0,0,0,0.3)',
1620
- radius = 0,
1621
- overlayColor,
1622
- overlayOpacity = 0,
1623
- fit = 'cover'
1624
- }) {
1625
- const elements = []
1626
-
1627
- // 绘制外边框(装饰层)
1628
- if (outerWidth > 0) {
1629
- const outerBg = new paper.Path.Rectangle({
1630
- point: [x - outerWidth, y - outerWidth],
1631
- size: [width + outerWidth * 2, height + outerWidth * 2],
1632
- radius: radius + outerWidth
1633
- })
1634
- outerBg.fillColor = new paper.Color(outerColor)
1635
- elements.push({ type: 'rectangle', id: outerBg.id })
1636
- }
1637
-
1638
- // 绘制内边框
1639
- if (borderWidth > 0) {
1640
- const innerBg = new paper.Path.Rectangle({
1641
- point: [x - borderWidth, y - borderWidth],
1642
- size: [width + borderWidth * 2, height + borderWidth * 2],
1643
- radius: radius + borderWidth
1644
- })
1645
- innerBg.fillColor = new paper.Color(borderColor)
1646
- elements.push({ type: 'rectangle', id: innerBg.id })
1647
- }
1648
-
1649
- // 加载并绘制图片
1650
- const image = await _loadImage(src)
1651
- const imgWidth = image.width
1652
- const imgHeight = image.height
1653
- const imgRatio = imgWidth / imgHeight
1654
- const boxRatio = width / height
1655
-
1656
- let drawX = x, drawY = y, drawW = width, drawH = height
1657
-
1658
- if (fit === 'cover') {
1659
- if (imgRatio > boxRatio) {
1660
- drawH = height
1661
- drawW = height * imgRatio
1662
- drawX = x - (drawW - width) / 2
1663
- } else {
1664
- drawW = width
1665
- drawH = width / imgRatio
1666
- drawY = y - (drawH - height) / 2
1667
- }
1668
- } else if (fit === 'contain') {
1669
- if (imgRatio > boxRatio) {
1670
- drawW = width
1671
- drawH = width / imgRatio
1672
- drawY = y + (height - drawH) / 2
1673
- } else {
1674
- drawH = height
1675
- drawW = height * imgRatio
1676
- drawX = x + (width - drawW) / 2
1677
- }
1678
- }
1679
-
1680
- // 创建裁剪区域
1681
- const clipPath = new paper.Path.Rectangle({
1682
- point: [x, y],
1683
- size: [width, height],
1684
- radius: radius
1685
- })
1686
-
1687
- // 添加阴影
1688
- if (shadowBlur > 0) {
1689
- const shadowRect = new paper.Path.Rectangle({
1690
- point: [x + shadowOffsetX, y + shadowOffsetY],
1691
- size: [width, height],
1692
- radius: radius
1693
- })
1694
- shadowRect.fillColor = new paper.Color(shadowColor)
1695
- shadowRect.opacity = shadowBlur / 50
1696
- shadowRect.shadowColor = new paper.Color(shadowColor)
1697
- shadowRect.shadowBlur = shadowBlur
1698
- elements.push({ type: 'rectangle', id: shadowRect.id })
1699
- }
1700
-
1701
- // 绘制图片
1702
- const raster = new paper.Raster({
1703
- source: src,
1704
- position: [drawX + drawW / 2, drawY + drawH / 2]
1705
- })
1706
-
1707
- await new Promise((resolve) => {
1708
- raster.onLoad = resolve
1709
- })
1710
-
1711
- raster.size = new paper.Size(drawW, drawH)
1712
- raster.position = new paper.Point(drawX + drawW / 2, drawY + drawH / 2)
1713
-
1714
- // 应用裁剪
1715
- clipPath.clipMask = true
1716
-
1717
- elements.push({ type: 'raster', id: raster.id })
1718
-
1719
- // 叠加颜色
1720
- if (overlayColor && overlayOpacity > 0) {
1721
- const overlay = new paper.Path.Rectangle({
1722
- point: [x, y],
1723
- size: [width, height],
1724
- radius: radius
1725
- })
1726
- overlay.fillColor = new paper.Color(overlayColor)
1727
- overlay.opacity = overlayOpacity
1728
- elements.push({ type: 'rectangle', id: overlay.id })
1729
- }
1730
-
1731
- return { success: true, elements, type: 'imageFrame' }
1732
- }
1733
-
1734
- // 辅助函数:加载图片
1735
- async function _loadImage(src) {
1736
- return new Promise((resolve, reject) => {
1737
- const img = new Image()
1738
- img.crossOrigin = 'anonymous'
1739
- img.onload = () => resolve(img)
1740
- img.onerror = () => {
1741
- if (src.startsWith('data:')) {
1742
- img.src = src
1743
- } else {
1744
- reject(new Error(`Failed to load image: ${src}`))
1745
- }
1746
- }
1747
- img.src = src
1748
- })
1749
- }
1750
-
1751
- /**
1752
- * 创建分栏布局组件
1753
- */
1754
- function createColumnsComponent(project, canvas, {
1755
- x,
1756
- y,
1757
- width,
1758
- height,
1759
- columns = 2,
1760
- gap = 20,
1761
- background,
1762
- borderColor,
1763
- borderWidth = 1,
1764
- radius = 0,
1765
- direction = 'horizontal',
1766
- align = 'top'
1767
- }) {
1768
- const elements = []
1769
-
1770
- // 计算每列宽度
1771
- const totalGap = gap * (columns - 1)
1772
- const columnWidth = (width - totalGap) / columns
1773
-
1774
- // 绘制背景
1775
- if (background) {
1776
- const bg = new paper.Path.Rectangle({
1777
- point: [x, y],
1778
- size: [width, height],
1779
- radius: radius
1780
- })
1781
- bg.fillColor = new paper.Color(background)
1782
- elements.push({ type: 'rectangle', id: bg.id })
1783
- }
1784
-
1785
- // 绘制边框
1786
- if (borderColor && borderWidth > 0) {
1787
- const border = new paper.Path.Rectangle({
1788
- point: [x, y],
1789
- size: [width, height],
1790
- radius: radius
1791
- })
1792
- border.fillColor = new paper.Color('transparent')
1793
- border.strokeColor = new paper.Color(borderColor)
1794
- border.strokeWidth = borderWidth
1795
- elements.push({ type: 'rectangle', id: border.id })
1796
- }
1797
-
1798
- // 生成分割线
1799
- for (let i = 1; i < columns; i++) {
1800
- const lineX = x + columnWidth * i + gap * (i - 1) + gap / 2
1801
- const line = new paper.Path.Line({
1802
- from: [lineX, y + 20],
1803
- to: [lineX, y + height - 20]
1804
- })
1805
- line.strokeColor = new paper.Color('#e0e0e0')
1806
- line.strokeWidth = 1
1807
- elements.push({ type: 'line', id: line.id })
1808
- }
1809
-
1810
- // 返回列位置信息
1811
- const columnPositions = []
1812
- for (let i = 0; i < columns; i++) {
1813
- const colX = x + (columnWidth + gap) * i
1814
- const colY = align === 'center' ? y + (height - height) / 2 : align === 'bottom' ? y + height - height : y
1815
-
1816
- columnPositions.push({
1817
- index: i,
1818
- x: colX,
1819
- y: colY,
1820
- width: columnWidth,
1821
- height: height,
1822
- centerX: colX + columnWidth / 2,
1823
- centerY: colY + height / 2
1824
- })
1825
- }
1826
-
1827
- return {
1828
- success: true,
1829
- elements,
1830
- columnPositions,
1831
- columnWidth,
1832
- totalWidth: width,
1833
- totalHeight: height,
1834
- type: 'columns'
1835
- }
1836
- }
1837
-
1838
- /**
1839
- * 创建网格布局组件
1840
- */
1841
- function createGridComponent(project, canvas, {
1842
- x,
1843
- y,
1844
- width,
1845
- height,
1846
- columns = 3,
1847
- rows = 2,
1848
- gapX = 20,
1849
- gapY = 20,
1850
- background,
1851
- borderColor,
1852
- borderWidth = 1,
1853
- radius = 0,
1854
- direction = 'row'
1855
- }) {
1856
- const elements = []
1857
-
1858
- // 计算单元格尺寸
1859
- const totalGapX = gapX * (columns - 1)
1860
- const totalGapY = gapY * (rows - 1)
1861
- const cellWidth = (width - totalGapX) / columns
1862
- const cellHeight = (height - totalGapY) / rows
1863
-
1864
- // 绘制背景
1865
- if (background) {
1866
- const bg = new paper.Path.Rectangle({
1867
- point: [x, y],
1868
- size: [width, height],
1869
- radius: radius
1870
- })
1871
- bg.fillColor = new paper.Color(background)
1872
- elements.push({ type: 'rectangle', id: bg.id })
1873
- }
1874
-
1875
- // 绘制边框
1876
- if (borderColor && borderWidth > 0) {
1877
- const border = new paper.Path.Rectangle({
1878
- point: [x, y],
1879
- size: [width, height],
1880
- radius: radius
1881
- })
1882
- border.fillColor = new paper.Color('transparent')
1883
- border.strokeColor = new paper.Color(borderColor)
1884
- border.strokeWidth = borderWidth
1885
- elements.push({ type: 'rectangle', id: border.id })
1886
- }
1887
-
1888
- // 生成网格线(可选,这里不绘制让用户自己控制)
1889
-
1890
- // 生成网格位置信息
1891
- const cellPositions = []
1892
- const totalCells = columns * rows
1893
-
1894
- for (let i = 0; i < totalCells; i++) {
1895
- let col, row
1896
-
1897
- if (direction === 'row') {
1898
- col = i % columns
1899
- row = Math.floor(i / columns)
1900
- } else {
1901
- row = i % rows
1902
- col = Math.floor(i / rows)
1903
- }
1904
-
1905
- const cellX = x + col * (cellWidth + gapX)
1906
- const cellY = y + row * (cellHeight + gapY)
1907
-
1908
- cellPositions.push({
1909
- index: i,
1910
- column: col,
1911
- row: row,
1912
- x: cellX,
1913
- y: cellY,
1914
- width: cellWidth,
1915
- height: cellHeight,
1916
- centerX: cellX + cellWidth / 2,
1917
- centerY: cellY + cellHeight / 2
1918
- })
1919
- }
1920
-
1921
- // 返回网格布局信息
1922
- return {
1923
- success: true,
1924
- elements,
1925
- cellPositions,
1926
- cellWidth,
1927
- cellHeight,
1928
- columns,
1929
- rows,
1930
- totalCells,
1931
- totalWidth: width,
1932
- totalHeight: height,
1933
- type: 'grid'
1934
- }
1935
- }
1936
-
1937
-
1938
- /**
1939
- * 创建星形组件
1940
- */
1941
- function createStarComponent(project, canvas, {
1942
- cx, cy, points = 5, innerRadius, outerRadius,
1943
- fill, stroke, strokeWidth = 1, opacity = 1, rotation = 0
1944
- }) {
1945
- const actualInnerRadius = innerRadius || outerRadius * 0.4
1946
- const path = new paper.Path()
1947
- const angleStep = Math.PI / points
1948
-
1949
- for (let i = 0; i < points * 2; i++) {
1950
- const radius = i % 2 === 0 ? outerRadius : actualInnerRadius
1951
- const angle = i * angleStep - Math.PI / 2 + (rotation * Math.PI / 180)
1952
- const x = cx + radius * Math.cos(angle)
1953
- const y = cy + radius * Math.sin(angle)
1954
- if (i === 0) path.moveTo(x, y)
1955
- else path.lineTo(x, y)
1956
- }
1957
- path.closePath()
1958
-
1959
- if (fill) path.fillColor = new paper.Color(fill)
1960
- if (stroke) { path.strokeColor = new paper.Color(stroke); path.strokeWidth = strokeWidth }
1961
- path.opacity = opacity
1962
- if (project && project.activeLayer) project.activeLayer.addChild(path)
1963
-
1964
- return { success: true, elements: [{ type: 'path', id: path.id }], type: 'star' }
1965
- }
1966
-
1967
- /**
1968
- * 创建箭头组件
1969
- */
1970
- function createArrowComponent(project, canvas, {
1971
- x1, y1, x2, y2, color = '#333333', strokeWidth = 2, headSize = 12, style = 'solid', direction = 'end'
1972
- }) {
1973
- const elements = []
1974
- const angle = Math.atan2(y2 - y1, x2 - x1)
1975
-
1976
- const line = new paper.Path.Line({ from: [x1, y1], to: [x2, y2] })
1977
- line.strokeColor = new paper.Color(color)
1978
- line.strokeWidth = strokeWidth
1979
- if (style === 'dashed') line.dashArray = [10, 5]
1980
- elements.push({ type: 'line', id: line.id })
1981
-
1982
- if (direction === 'end' || direction === 'both') {
1983
- const arrowHead = new paper.Path()
1984
- arrowHead.moveTo(x2, y2)
1985
- arrowHead.lineTo(x2 + headSize * Math.cos(angle + Math.PI * 0.8), y2 + headSize * Math.sin(angle + Math.PI * 0.8))
1986
- arrowHead.moveTo(x2, y2)
1987
- arrowHead.lineTo(x2 + headSize * Math.cos(angle - Math.PI * 0.8), y2 + headSize * Math.sin(angle - Math.PI * 0.8))
1988
- arrowHead.strokeColor = new paper.Color(color)
1989
- arrowHead.strokeWidth = strokeWidth
1990
- arrowHead.strokeCap = 'round'
1991
- elements.push({ type: 'path', id: arrowHead.id })
1992
- }
1993
-
1994
- if (direction === 'start' || direction === 'both') {
1995
- const startAngle = angle + Math.PI
1996
- const arrowHead = new paper.Path()
1997
- arrowHead.moveTo(x1, y1)
1998
- arrowHead.lineTo(x1 + headSize * Math.cos(startAngle + Math.PI * 0.8), y1 + headSize * Math.sin(startAngle + Math.PI * 0.8))
1999
- arrowHead.moveTo(x1, y1)
2000
- arrowHead.lineTo(x1 + headSize * Math.cos(startAngle - Math.PI * 0.8), y1 + headSize * Math.sin(startAngle - Math.PI * 0.8))
2001
- arrowHead.strokeColor = new paper.Color(color)
2002
- arrowHead.strokeWidth = strokeWidth
2003
- arrowHead.strokeCap = 'round'
2004
- elements.push({ type: 'path', id: arrowHead.id })
2005
- }
2006
-
2007
- return { success: true, elements, type: 'arrow' }
2008
- }
2009
-
2010
- /**
2011
- * 创建环形进度条组件
2012
- */
2013
- function createProgressCircleComponent(project, canvas, {
2014
- cx, cy, radius, value, strokeWidth = 10, trackColor = '#e0e0e0',
2015
- fillColor = '#3b82f6', backgroundColor, showLabel = true, labelColor, startAngle = -90
2016
- }) {
2017
- const elements = []
2018
-
2019
- if (backgroundColor) {
2020
- const bgCircle = new paper.Path.Circle({ center: [cx, cy], radius: radius })
2021
- bgCircle.fillColor = new paper.Color(backgroundColor)
2022
- elements.push({ type: 'path', id: bgCircle.id })
2023
- }
2024
-
2025
- const trackCircle = new paper.Path.Circle({ center: [cx, cy], radius: radius })
2026
- trackCircle.fillColor = new paper.Color('transparent')
2027
- trackCircle.strokeColor = new paper.Color(trackColor)
2028
- trackCircle.strokeWidth = strokeWidth
2029
- elements.push({ type: 'path', id: trackCircle.id })
2030
-
2031
- if (value > 0) {
2032
- const endAngle = startAngle + (value / 100) * 360
2033
- const startRad = startAngle * Math.PI / 180
2034
- const endRad = endAngle * Math.PI / 180
2035
-
2036
- const arc = new paper.Path()
2037
- arc.moveTo(cx + radius * Math.cos(startRad), cy + radius * Math.sin(startRad))
2038
- arc.arcTo([cx, cy], radius, endRad - startRad)
2039
- arc.strokeColor = new paper.Color(fillColor)
2040
- arc.strokeWidth = strokeWidth
2041
- arc.strokeCap = 'round'
2042
- elements.push({ type: 'path', id: arc.id })
2043
- }
2044
-
2045
- if (showLabel) {
2046
- const textColor = labelColor || fillColor
2047
- const label = new paper.PointText({
2048
- point: [cx, cy + 6],
2049
- content: `${Math.round(value)}%`,
2050
- fontSize: radius * 0.4,
2051
- fillColor: new paper.Color(textColor),
2052
- justification: 'center',
2053
- fontWeight: 'bold'
2054
- })
2055
- elements.push({ type: 'text', id: label.id })
2056
- }
2057
-
2058
- return { success: true, elements, type: 'progressCircle' }
2059
- }
2060
-
2061
- /**
2062
- * 创建 Chip 标签组件
2063
- */
2064
- function createChipComponent(project, canvas, {
2065
- x, y, text, background = '#e0e0e0', color = '#333333', borderColor,
2066
- fontSize = 12, padding = 12, radius = 16, icon
2067
- }) {
2068
- const elements = []
2069
- const textWidth = text.length * fontSize * 0.6
2070
- const iconWidth = icon ? fontSize : 0
2071
- const totalWidth = padding * 2 + textWidth + iconWidth + 4
2072
- const height = fontSize + padding * 2
2073
- const rectX = x - totalWidth / 2
2074
- const rectY = y - height / 2
2075
-
2076
- const bg = new paper.Path.Rectangle({ point: [rectX, rectY], size: [totalWidth, height], radius: radius })
2077
- bg.fillColor = new paper.Color(background)
2078
- if (borderColor) { bg.strokeColor = new paper.Color(borderColor); bg.strokeWidth = 1 }
2079
- elements.push({ type: 'path', id: bg.id })
2080
-
2081
- if (icon) {
2082
- const iconText = new paper.PointText({
2083
- point: [rectX + padding + iconWidth / 2, y + fontSize / 3],
2084
- content: icon,
2085
- fontSize: fontSize + 2,
2086
- fillColor: new paper.Color(color),
2087
- justification: 'center'
2088
- })
2089
- elements.push({ type: 'text', id: iconText.id })
2090
- }
2091
-
2092
- const textX = icon ? rectX + padding + iconWidth + 4 + textWidth / 2 : x
2093
- const label = new paper.PointText({
2094
- point: [textX, y + fontSize / 3],
2095
- content: text,
2096
- fontSize: fontSize,
2097
- fillColor: new paper.Color(color),
2098
- justification: 'center'
2099
- })
2100
- elements.push({ type: 'text', id: label.id })
2101
-
2102
- return { success: true, elements, width: totalWidth, height, type: 'chip' }
2103
- }
2104
-
2105
- /**
2106
- * 创建图表组件
2107
- */
2108
- function createChartComponent(project, canvas, {
2109
- type = 'bar', x, y, width, height, data = [], barColor = '#3b82f6',
2110
- showLabels = true, showValues = true, barGap = 4
2111
- }) {
2112
- const elements = []
2113
-
2114
- if (type === 'bar' && data.length > 0) {
2115
- const maxValue = Math.max(...data.map(d => d.value))
2116
- const barCount = data.length
2117
- const totalGap = barGap * (barCount - 1)
2118
- const barWidth = (width - totalGap) / barCount
2119
- const labelHeight = showLabels ? 24 : 0
2120
- const valueHeight = showValues ? 20 : 0
2121
- const chartHeight = height - labelHeight - valueHeight - 10
2122
-
2123
- data.forEach((item, index) => {
2124
- const barHeight = (item.value / maxValue) * chartHeight
2125
- const barX = x + index * (barWidth + barGap)
2126
- const barY = y + height - labelHeight - valueHeight - barHeight - 5
2127
- const color = item.color || barColor
2128
-
2129
- const bar = new paper.Path.Rectangle({
2130
- point: [barX, barY],
2131
- size: [barWidth, barHeight],
2132
- radius: [4, 4, 0, 0]
2133
- })
2134
- bar.fillColor = new paper.Color(color)
2135
- elements.push({ type: 'path', id: bar.id })
2136
-
2137
- if (showValues) {
2138
- const valueText = new paper.PointText({
2139
- point: [barX + barWidth / 2, barY - 8],
2140
- content: String(item.value),
2141
- fontSize: 12,
2142
- fillColor: new paper.Color('#666666'),
2143
- justification: 'center'
2144
- })
2145
- elements.push({ type: 'text', id: valueText.id })
2146
- }
2147
-
2148
- if (showLabels) {
2149
- const labelText = new paper.PointText({
2150
- point: [barX + barWidth / 2, y + height - 8],
2151
- content: item.label || '',
2152
- fontSize: 11,
2153
- fillColor: new paper.Color('#333333'),
2154
- justification: 'center'
2155
- })
2156
- elements.push({ type: 'text', id: labelText.id })
2157
- }
2158
- })
2159
- } else if (type === 'pie' && data.length > 0) {
2160
- const cx = x + width / 2
2161
- const cy = y + height / 2
2162
- const radius = Math.min(width, height) / 2 - 10
2163
- const total = data.reduce((sum, d) => sum + d.value, 0)
2164
- let currentAngle = -90
2165
- const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']
2166
-
2167
- data.forEach((item, index) => {
2168
- const percentage = item.value / total
2169
- const endAngle = currentAngle + percentage * 360
2170
- const path = new paper.Path()
2171
- path.moveTo(cx, cy)
2172
- path.arc([cx, cy], radius, currentAngle * Math.PI / 180, endAngle * Math.PI / 180)
2173
- path.closePath()
2174
- path.fillColor = new paper.Color(item.color || colors[index % colors.length])
2175
- elements.push({ type: 'path', id: path.id })
2176
-
2177
- if (showLabels && percentage > 0.05) {
2178
- const midAngle = (currentAngle + endAngle) / 2
2179
- const midRad = midAngle * Math.PI / 180
2180
- const labelX = cx + radius * 0.7 * Math.cos(midRad)
2181
- const labelY = cy + radius * 0.7 * Math.sin(midRad)
2182
- const labelText = new paper.PointText({
2183
- point: [labelX, labelY + 4],
2184
- content: `${Math.round(percentage * 100)}%`,
2185
- fontSize: 11,
2186
- fillColor: new paper.Color('#ffffff'),
2187
- justification: 'center',
2188
- fontWeight: 'bold'
2189
- })
2190
- elements.push({ type: 'text', id: labelText.id })
2191
- }
2192
-
2193
- currentAngle = endAngle
2194
- })
2195
- }
2196
-
2197
- return { success: true, elements, type: 'chart' }
2198
- }
2199
-
2200
- /**
2201
- * 创建水印组件
2202
- */
2203
- function createWatermarkComponent(project, canvas, {
2204
- text, cx, cy, color = 'rgba(0,0,0,0.1)', fontSize = 48,
2205
- fontFamily = 'sans-serif', opacity = 0.1, rotation = 0, align = 'center'
2206
- }) {
2207
- const { getFontFallbackChain } = require('./fonts')
2208
-
2209
- // 获取字体链
2210
- const chain = getFontFallbackChain(fontFamily, text)
2211
- const finalFont = chain.length === 1 ? chain[0] : chain.join(', ')
2212
-
2213
- const label = new paper.PointText({
2214
- point: [cx, cy],
2215
- content: text,
2216
- fontSize: fontSize,
2217
- fontFamily: finalFont,
2218
- fillColor: new paper.Color(color),
2219
- justification: align,
2220
- opacity: opacity
2221
- })
2222
-
2223
- if (rotation !== 0) {
2224
- label.rotate(rotation, new paper.Point(cx, cy))
2225
- }
2226
-
2227
- return { success: true, elements: [{ type: 'text', id: label.id }], type: 'watermark' }
2228
- }
2229
-
2230
- /**
2231
- * 创建表格组件
2232
- */
2233
- function createTableComponent(project, canvas, {
2234
- x, y, width, columns = [], rows = [], rowHeight = 36,
2235
- headerBg = '#f0f0f0', headerColor = '#333333', borderColor = '#e0e0e0',
2236
- cellColor = '#333333', fontSize = 12, headerFontSize = 13, striped = true, stripeColor = '#fafafa'
2237
- }) {
2238
- const elements = []
2239
- // 确保 columns 是数组
2240
- if (!Array.isArray(columns) || columns.length === 0) {
2241
- return { success: true, elements, type: 'table' }
2242
- }
2243
- // 确保 rows 是数组
2244
- if (!Array.isArray(rows)) {
2245
- rows = []
2246
- }
2247
-
2248
- const totalHeight = rowHeight * (rows.length + 1)
2249
-
2250
- const outerBorder = new paper.Path.Rectangle({ point: [x, y], size: [width, totalHeight] })
2251
- outerBorder.fillColor = new paper.Color('transparent')
2252
- outerBorder.strokeColor = new paper.Color(borderColor)
2253
- outerBorder.strokeWidth = 1
2254
- elements.push({ type: 'path', id: outerBorder.id })
2255
-
2256
- const headerBgRect = new paper.Path.Rectangle({ point: [x, y], size: [width, rowHeight] })
2257
- headerBgRect.fillColor = new paper.Color(headerBg)
2258
- headerBgRect.strokeColor = new paper.Color(borderColor)
2259
- headerBgRect.strokeWidth = 0.5
2260
- elements.push({ type: 'path', id: headerBgRect.id })
2261
-
2262
- let currentX = x
2263
- columns.forEach((col, index) => {
2264
- const colWidth = col.width || (width / columns.length)
2265
- if (index > 0) {
2266
- const line = new paper.Path.Line({ from: [currentX, y], to: [currentX, y + totalHeight] })
2267
- line.strokeColor = new paper.Color(borderColor)
2268
- line.strokeWidth = 0.5
2269
- elements.push({ type: 'line', id: line.id })
2270
- }
2271
- const headerText = new paper.PointText({
2272
- point: [currentX + colWidth / 2, y + rowHeight / 2 + fontSize / 3],
2273
- content: col.title || '',
2274
- fontSize: headerFontSize,
2275
- fillColor: new paper.Color(headerColor),
2276
- justification: col.align || 'center',
2277
- fontWeight: 'bold'
2278
- })
2279
- elements.push({ type: 'text', id: headerText.id })
2280
- currentX += colWidth
2281
- })
2282
-
2283
- rows.forEach((row, rowIndex) => {
2284
- const rowY = y + rowHeight * (rowIndex + 1)
2285
- if (striped && rowIndex % 2 === 1) {
2286
- const stripeBg = new paper.Path.Rectangle({ point: [x, rowY], size: [width, rowHeight] })
2287
- stripeBg.fillColor = new paper.Color(stripeColor)
2288
- stripeBg.strokeColor = new paper.Color(borderColor)
2289
- stripeBg.strokeWidth = 0.5
2290
- elements.push({ type: 'path', id: stripeBg.id })
2291
- }
2292
- const rowLine = new paper.Path.Line({ from: [x, rowY], to: [x + width, rowY] })
2293
- rowLine.strokeColor = new paper.Color(borderColor)
2294
- rowLine.strokeWidth = 0.5
2295
- elements.push({ type: 'line', id: rowLine.id })
2296
-
2297
- let cellX = x
2298
- columns.forEach((col, colIndex) => {
2299
- const colWidth = col.width || (width / columns.length)
2300
- const cellValue = row[colIndex] || ''
2301
- const cellText = new paper.PointText({
2302
- point: [cellX + colWidth / 2, rowY + rowHeight / 2 + fontSize / 3],
2303
- content: String(cellValue),
2304
- fontSize: fontSize,
2305
- fillColor: new paper.Color(cellColor),
2306
- justification: col.align || 'center'
2307
- })
2308
- elements.push({ type: 'text', id: cellText.id })
2309
- cellX += colWidth
2310
- })
2311
- })
2312
-
2313
- return { success: true, elements, width, height: totalHeight, type: 'table' }
2314
- }