@anker-in/campaign-ui 0.3.2 → 0.3.4

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 (229) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  3. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  4. package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +23 -2
  5. package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
  6. package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
  7. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js +1 -1
  8. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  9. package/dist/cjs/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  10. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js +1 -1
  11. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  12. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
  13. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  14. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  15. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
  16. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  17. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  18. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  19. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  20. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  21. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  22. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  23. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  24. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  25. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  26. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  27. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  28. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  29. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  30. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  31. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  32. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  33. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  34. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  35. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  36. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  37. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  38. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  39. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  40. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  41. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  42. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  43. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js +1 -1
  44. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  45. package/dist/cjs/components/LiveChatWidget/components/MessageList.js +2 -2
  46. package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +2 -2
  47. package/dist/cjs/components/LiveChatWidget/constants.d.ts +5 -0
  48. package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
  49. package/dist/cjs/components/LiveChatWidget/constants.js.map +3 -3
  50. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  51. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  52. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  53. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +35 -2
  54. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  55. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  56. package/dist/cjs/components/LiveChatWidget/index.d.ts +1 -1
  57. package/dist/cjs/components/LiveChatWidget/index.js +1 -1
  58. package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
  59. package/dist/cjs/components/LiveChatWidget/types.d.ts +212 -3
  60. package/dist/cjs/components/LiveChatWidget/types.js +1 -1
  61. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  62. package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  63. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
  64. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  65. package/dist/cjs/components/chat/markdown.js +1 -1
  66. package/dist/cjs/components/chat/markdown.js.map +2 -2
  67. package/dist/cjs/components/credits/creditsBanner/index.js +2 -2
  68. package/dist/cjs/components/credits/creditsBanner/index.js.map +2 -2
  69. package/dist/cjs/components/index.d.ts +2 -0
  70. package/dist/cjs/components/index.js +1 -1
  71. package/dist/cjs/components/index.js.map +3 -3
  72. package/dist/cjs/stories/LiveChatWidget.stories.d.ts +1 -79
  73. package/dist/cjs/stories/LiveChatWidget.stories.js +8 -47
  74. package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
  75. package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  76. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  77. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  78. package/dist/esm/components/LiveChatWidget/api/chat.d.ts +23 -2
  79. package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
  80. package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
  81. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js +1 -1
  82. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  83. package/dist/esm/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  84. package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
  85. package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  86. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
  87. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  88. package/dist/esm/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  89. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
  90. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  91. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  92. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  93. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  94. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  95. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  96. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  97. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  98. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  99. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  100. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  101. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  102. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  103. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  104. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  105. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  106. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  107. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  108. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  109. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  110. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  111. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  112. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  113. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  114. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  115. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  116. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  117. package/dist/esm/components/LiveChatWidget/components/MessageContent.js +1 -1
  118. package/dist/esm/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  119. package/dist/esm/components/LiveChatWidget/components/MessageList.js +2 -2
  120. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +2 -2
  121. package/dist/esm/components/LiveChatWidget/constants.d.ts +5 -0
  122. package/dist/esm/components/LiveChatWidget/constants.js +1 -1
  123. package/dist/esm/components/LiveChatWidget/constants.js.map +3 -3
  124. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  125. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  126. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  127. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +35 -2
  128. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  129. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  130. package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
  131. package/dist/esm/components/LiveChatWidget/index.js +1 -1
  132. package/dist/esm/components/LiveChatWidget/index.js.map +2 -2
  133. package/dist/esm/components/LiveChatWidget/types.d.ts +212 -3
  134. package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  135. package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
  136. package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  137. package/dist/esm/components/chat/markdown.js +1 -1
  138. package/dist/esm/components/chat/markdown.js.map +2 -2
  139. package/dist/esm/components/credits/creditsBanner/index.js +2 -2
  140. package/dist/esm/components/credits/creditsBanner/index.js.map +2 -2
  141. package/dist/esm/components/index.d.ts +2 -0
  142. package/dist/esm/components/index.js +1 -1
  143. package/dist/esm/components/index.js.map +3 -3
  144. package/dist/esm/stories/LiveChatWidget.stories.d.ts +1 -79
  145. package/dist/esm/stories/LiveChatWidget.stories.js +8 -47
  146. package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
  147. package/dist/index.d.mts +1305 -0
  148. package/dist/index.d.ts +1305 -0
  149. package/dist/index.js +26656 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/index.mjs +26641 -0
  152. package/dist/index.mjs.map +1 -0
  153. package/package.json +8 -1
  154. package/src/components/LiveChatWidget/LiveChatWidget.tsx +887 -0
  155. package/src/components/LiveChatWidget/api/chat.ts +175 -0
  156. package/src/components/LiveChatWidget/components/ChatBubble.tsx +152 -0
  157. package/src/components/LiveChatWidget/components/ChatHeader.tsx +150 -0
  158. package/src/components/LiveChatWidget/components/ChatInput.tsx +253 -0
  159. package/src/components/LiveChatWidget/components/ChatMessage.tsx +190 -0
  160. package/src/components/LiveChatWidget/components/ChatWindow.tsx +363 -0
  161. package/src/components/LiveChatWidget/components/ComplianceDialog.tsx +216 -0
  162. package/src/components/LiveChatWidget/components/MessageContent/CartCard.tsx +202 -0
  163. package/src/components/LiveChatWidget/components/MessageContent/ErrorBlock.tsx +75 -0
  164. package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +128 -0
  165. package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +152 -0
  166. package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +227 -0
  167. package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +377 -0
  168. package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +293 -0
  169. package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +170 -0
  170. package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +91 -0
  171. package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +110 -0
  172. package/src/components/LiveChatWidget/components/MessageContent/ThinkingBlock.tsx +53 -0
  173. package/src/components/LiveChatWidget/components/MessageContent/index.ts +16 -0
  174. package/src/components/LiveChatWidget/components/MessageContent.tsx +113 -0
  175. package/src/components/LiveChatWidget/components/MessageList.tsx +261 -0
  176. package/src/components/LiveChatWidget/components/ScrollAnchor.tsx +75 -0
  177. package/src/components/LiveChatWidget/constants.ts +36 -0
  178. package/src/components/LiveChatWidget/hooks/useChatAPI.ts +146 -0
  179. package/src/components/LiveChatWidget/hooks/useChatState.ts +1090 -0
  180. package/src/components/LiveChatWidget/hooks/useSession.ts +123 -0
  181. package/src/components/LiveChatWidget/index.tsx +63 -0
  182. package/src/components/LiveChatWidget/types.ts +1011 -0
  183. package/src/components/LiveChatWidget/utils/cartTransformers.ts +72 -0
  184. package/src/components/LiveChatWidget/utils/fetcher.ts +131 -0
  185. package/src/components/LiveChatWidget/utils/messageRenderers.ts +120 -0
  186. package/src/components/LiveChatWidget/utils/productTransformers.ts +149 -0
  187. package/src/components/LiveChatWidget/utils/userId.ts +140 -0
  188. package/src/components/LiveChatWidget/utils/validation.ts +99 -0
  189. package/src/components/chat/markdown.tsx +1 -1
  190. package/src/components/credits/creditsBanner/index.tsx +5 -5
  191. package/src/components/index.ts +23 -0
  192. package/src/stories/LiveChatWidget.stories.tsx +322 -0
  193. package/src/styles/livechat.css +317 -0
  194. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  195. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  196. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  197. package/dist/cjs/components/credits/context/utils/atobID.d.ts +0 -1
  198. package/dist/cjs/components/credits/context/utils/atobID.js +0 -2
  199. package/dist/cjs/components/credits/context/utils/atobID.js.map +0 -7
  200. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  201. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  202. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  203. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  204. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  205. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  206. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  207. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  208. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  209. package/dist/cjs/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  210. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js +0 -2
  211. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js.map +0 -7
  212. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  213. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  214. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  215. package/dist/esm/components/credits/context/utils/atobID.d.ts +0 -1
  216. package/dist/esm/components/credits/context/utils/atobID.js +0 -2
  217. package/dist/esm/components/credits/context/utils/atobID.js.map +0 -7
  218. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  219. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  220. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  221. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  222. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  223. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  224. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  225. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  226. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  227. package/dist/esm/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  228. package/dist/esm/components/credits/context/utils/variantGetCoupon.js +0 -2
  229. package/dist/esm/components/credits/context/utils/variantGetCoupon.js.map +0 -7
@@ -0,0 +1,227 @@
1
+ /**
2
+ * 商品卡片渲染器 - 紧凑型
3
+ * 显示单个商品的详细信息(横向布局)
4
+ * 基于 specs/livechat-widget/data-model.md 的商品数据模型
5
+ */
6
+
7
+ import React from 'react'
8
+ import type { MessageRenderer, ProductCardContent, Product, CommonText } from '../../types'
9
+ import { CURRENCY_SYMBOLS, DEFAULT_COMMON_TEXT } from '../../constants.js'
10
+
11
+ /**
12
+ * 格式化价格
13
+ */
14
+ function formatPrice(price: Product['price']): string {
15
+ const { amount, currency } = price
16
+
17
+ const symbol = CURRENCY_SYMBOLS[currency] || currency
18
+ return `${symbol}${amount.toFixed(2)}`
19
+ }
20
+
21
+ /**
22
+ * 格式化折扣标签文本
23
+ * @param discount 折扣对象
24
+ * @param currency 货币代码
25
+ * @param offText "OFF" 文案
26
+ * @returns 格式化后的折扣文本(如 "$10 OFF" 或 "20% OFF")
27
+ */
28
+ function formatDiscountLabel(
29
+ discount: { discount_type?: string; discount_value?: string | number },
30
+ currency: string,
31
+ offText: string = DEFAULT_COMMON_TEXT.off
32
+ ): string {
33
+ if (!discount.discount_type || discount.discount_value === undefined) {
34
+ return ''
35
+ }
36
+
37
+ // 将 discount_value 转换为数字
38
+ const value =
39
+ typeof discount.discount_value === 'string' ? parseFloat(discount.discount_value) : discount.discount_value
40
+
41
+ if (isNaN(value)) {
42
+ return ''
43
+ }
44
+
45
+ if (discount.discount_type === 'percentage') {
46
+ return `${Math.round(value)}% ${offText}`
47
+ }
48
+
49
+ if (discount.discount_type === 'fixed_amount') {
50
+ const symbol = CURRENCY_SYMBOLS[currency] || currency
51
+ return `${symbol}${Math.round(value)} ${offText}`
52
+ }
53
+
54
+ return ''
55
+ }
56
+
57
+ /**
58
+ * 紧凑型商品卡片组件(横向布局)
59
+ */
60
+ const CompactProductCard: React.FC<{
61
+ product: Product
62
+ onAddToCart?: (product: Product) => void
63
+ addToCartText?: string
64
+ offText?: string
65
+ }> = ({ product, onAddToCart, addToCartText = DEFAULT_COMMON_TEXT.addToCart, offText = DEFAULT_COMMON_TEXT.off }) => {
66
+ const { title, description, price, imageUrl, stockStatus, averageRating, variants } = product
67
+
68
+ const isOutOfStock = stockStatus === 'out_of_stock'
69
+
70
+ // 获取第一个变体的折扣信息
71
+ const firstVariant = variants?.[0]
72
+ const hasDiscount = firstVariant?.discount?.has_discount
73
+ const discountPrice = hasDiscount ? firstVariant?.discount?.discount_price : null
74
+ const discount = firstVariant?.discount
75
+
76
+ // 当前显示价格:有折扣时显示折扣价,否则显示原价
77
+ const currentPrice = discountPrice ? { amount: discountPrice, currency: price.currency } : price
78
+
79
+ // 格式化折扣标签
80
+ const discountLabel = discount && hasDiscount ? formatDiscountLabel(discount, price.currency, offText) : ''
81
+
82
+ const handleAddToCart = (e: React.MouseEvent) => {
83
+ e.preventDefault()
84
+ e.stopPropagation()
85
+ if (onAddToCart) {
86
+ onAddToCart(product)
87
+ }
88
+ }
89
+
90
+ return (
91
+ <div className="block w-full overflow-hidden rounded-2xl bg-[#F5F6F7] transition-shadow mb-[32px]">
92
+ <div className="block">
93
+ <div className="flex gap-2 p-4 bg-white">
94
+ {/* 商品图片 */}
95
+ <div className=" flex shrink-0 items-center overflow-hidden rounded-md " style={{ width: '40%' }}>
96
+ <img
97
+ src={imageUrl}
98
+ alt={title}
99
+ className={`h-auto w-full object-cover ${isOutOfStock ? 'opacity-50' : ''}`}
100
+ loading="lazy"
101
+ />
102
+ </div>
103
+
104
+ {/* 商品信息 */}
105
+ <div className="flex flex-1 flex-col justify-center">
106
+ {/* 折扣标签 */}
107
+ {discountLabel && (
108
+ <div
109
+ className="mb-1 w-fit rounded-full px-2 text-sm font-bold leading-none tracking-[-0.04em] text-white"
110
+ style={{ backgroundColor: '#005D8E', paddingTop: '6px', paddingBottom: '4px' }}
111
+ >
112
+ {discountLabel}
113
+ </div>
114
+ )}
115
+
116
+ {/* 标题 */}
117
+ <h4 className="line-clamp-2 text-base font-bold leading-[1.4] tracking-[-0.02em] text-[#080A0F]">
118
+ {title}
119
+ </h4>
120
+
121
+ {/* 描述(可选) */}
122
+ {/* {description && (
123
+ <p className="line-clamp-2 text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">
124
+ {description}
125
+ </p>
126
+ )} */}
127
+
128
+ {/* 价格和评分 */}
129
+ <div className="mt-4 flex items-center gap-2">
130
+ <div className="flex items-center gap-1">
131
+ {/* 当前价格(有折扣时显示折扣价,否则显示原价) */}
132
+ <span className="text-base font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">
133
+ {formatPrice(currentPrice)}
134
+ </span>
135
+ {/* 原价(划线价)- 仅在有折扣时显示 */}
136
+ {hasDiscount && (
137
+ <span className="text-base font-bold leading-[1.4] tracking-[-0.02em] text-[#6D6D6F] line-through">
138
+ {formatPrice(price)}
139
+ </span>
140
+ )}
141
+ </div>
142
+ {/* 评分(可选) */}
143
+ {averageRating !== undefined && (
144
+ <div className="flex items-center gap-0.5 text-xs text-gray-600">
145
+ <span className="text-yellow-500">⭐</span>
146
+ <span>{averageRating.toFixed(1)}</span>
147
+ </div>
148
+ )}
149
+ </div>
150
+
151
+ {/* Add to Cart 按钮 - 在价格下方 */}
152
+ <button
153
+ type="button"
154
+ onClick={handleAddToCart}
155
+ className="mt-2 w-fit rounded-full px-[20px] py-[10px] text-center text-sm font-bold leading-[1.2] tracking-[-0.04em] text-white"
156
+ style={{ backgroundColor: '#1D1D1F' }}
157
+ >
158
+ {addToCartText}
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ )
165
+ }
166
+
167
+ /**
168
+ * 商品卡片渲染器
169
+ *
170
+ * 功能:
171
+ * - 紧凑型横向布局(图片在左,信息在右)
172
+ * - 显示折扣标签、价格、评分
173
+ * - 支持 Add to Cart 按钮
174
+ * - 独立成段展示
175
+ * - 支持自定义渲染函数
176
+ *
177
+ * 布局:
178
+ * ```
179
+ * ┌─────────────────────────────┐
180
+ * │ [图片] 折扣标签 │
181
+ * │ 商品标题 │
182
+ * │ $29.99 (原价) │
183
+ * │ ⭐ 4.5 │
184
+ * │ [Add to Cart] │
185
+ * └─────────────────────────────┘
186
+ * ```
187
+ *
188
+ * @example
189
+ * ```tsx
190
+ * const content: ProductCardContent = {
191
+ * type: 'product_card',
192
+ * data: {
193
+ * product: { ... },
194
+ * onAddToCart: (product) => { ... },
195
+ * productCardRender: (product, productHandle) => <CustomCard product={product} handle={productHandle} />
196
+ * }
197
+ * }
198
+ * <ProductCard.render(content, false, false) />
199
+ * ```
200
+ */
201
+ export const ProductCard: MessageRenderer = {
202
+ render: (content, isUser, isSystem) => {
203
+ const productContent = content as ProductCardContent
204
+ const { product, rawProduct, productHandle, onAddToCart, productCardRender } = productContent.data
205
+
206
+ console.log('[ProductCard] 渲染产品卡片:', {
207
+ productHandle,
208
+ hasProduct: !!product,
209
+ hasRawProduct: !!rawProduct,
210
+ hasCustomRender: !!productCardRender,
211
+ })
212
+
213
+ // 如果提供了自定义渲染函数,使用自定义渲染(传入原始后端数据和 productHandle)
214
+ // 即使 product 为空,也调用自定义渲染函数,让应用层可以用 productHandle 查询产品
215
+ if (productCardRender) {
216
+ console.log('[ProductCard] 使用自定义渲染, productHandle:', productHandle)
217
+ return <>{productCardRender(rawProduct || product, productHandle)}</>
218
+ }
219
+
220
+ // 默认渲染:如果没有产品数据则不渲染
221
+ if (!product) {
222
+ return null
223
+ }
224
+
225
+ return <CompactProductCard product={product} onAddToCart={onAddToCart} />
226
+ },
227
+ }
@@ -0,0 +1,377 @@
1
+ /**
2
+ * 产品对比组件
3
+ * 显示多个产品的对比信息,采用表格布局展示各维度差异
4
+ * 基于参考设计:顶部产品展示 + 底部对比表格
5
+ */
6
+
7
+ import React, { useState } from 'react'
8
+ import type { Product, MessageRenderer, CommonText } from '../../types'
9
+ import { DEFAULT_COMMON_TEXT, CURRENCY_SYMBOLS } from '../../constants'
10
+
11
+ /**
12
+ * 对比维度数据结构
13
+ */
14
+ interface ComparisonDimension {
15
+ label: string
16
+ values: Array<{
17
+ product_id: string
18
+ [key: string]: any
19
+ }>
20
+ }
21
+
22
+ /**
23
+ * 产品对比数据结构
24
+ */
25
+ export interface ProductComparisonData {
26
+ products: Product[]
27
+ dimensions: {
28
+ price?: ComparisonDimension
29
+ variants?: ComparisonDimension
30
+ member_price?: ComparisonDimension
31
+ discount?: ComparisonDimension
32
+ reviews?: ComparisonDimension
33
+ [key: string]: ComparisonDimension | undefined
34
+ }
35
+ commonText?: CommonText
36
+ }
37
+
38
+ export interface ProductComparisonProps {
39
+ /**
40
+ * 产品对比数据
41
+ */
42
+ data: ProductComparisonData
43
+
44
+ /**
45
+ * 是否为用户消息
46
+ */
47
+ isUser?: boolean
48
+
49
+ /**
50
+ * 是否为系统消息
51
+ */
52
+ isSystem?: boolean
53
+
54
+ /**
55
+ * 添加到购物车回调
56
+ */
57
+ onAddToCart?: (product: Product) => void
58
+
59
+ /**
60
+ * 通用文案配置
61
+ */
62
+ commonText?: CommonText
63
+ }
64
+
65
+ /**
66
+ * 格式化价格显示
67
+ */
68
+ const formatPrice = (amount: number, currency: string = 'USD'): string => {
69
+ const symbol = CURRENCY_SYMBOLS[currency] || currency
70
+ return `${symbol}${amount}`
71
+ }
72
+
73
+ /**
74
+ * 产品对比组件
75
+ *
76
+ * 布局:
77
+ * ```
78
+ * ┌─────────────────────────────────────┐
79
+ * │ [产品1图片] [产品2图片] │
80
+ * │ 价格1 价格2 │
81
+ * │ 颜色选项 颜色选项 │
82
+ * ├─────────────────────────────────────┤
83
+ * │ 维度1 │ 值1-1 │ 值1-2 │
84
+ * │ 维度2 │ 值2-1 │ 值2-2 │
85
+ * │ 维度3 │ 值3-1 │ 值3-2 │
86
+ * └─────────────────────────────────────┘
87
+ * ```
88
+ */
89
+ export const ProductComparison: React.FC<ProductComparisonProps> = ({ data, onAddToCart, commonText }) => {
90
+ const { products: rawProducts, dimensions } = data
91
+
92
+ // 合并默认文案和自定义文案
93
+ const mergedText = { ...DEFAULT_COMMON_TEXT, ...commonText }
94
+
95
+ // 过滤掉 null 或无效的产品
96
+ const allProducts = rawProducts?.filter(p => p && p.shopifyId) || []
97
+
98
+ // 对比列数(固定为2列)
99
+ const COMPARISON_COLUMNS = 2
100
+
101
+ // 初始化每个对比位置的选中产品(默认取前两个产品)
102
+ const initialSelectedProducts = allProducts.slice(0, COMPARISON_COLUMNS)
103
+ const [selectedProducts, setSelectedProducts] = useState<Product[]>(initialSelectedProducts)
104
+
105
+ // Early return 必须在所有 hooks 之后
106
+ if (allProducts.length === 0) {
107
+ return null
108
+ }
109
+
110
+ // 处理产品选择变更
111
+ const handleProductChange = (index: number, productId: string) => {
112
+ const newProduct = allProducts.find(p => p.shopifyId === productId)
113
+ if (newProduct) {
114
+ const newSelectedProducts = [...selectedProducts]
115
+ newSelectedProducts[index] = newProduct
116
+ setSelectedProducts(newSelectedProducts)
117
+ }
118
+ }
119
+
120
+ // 当前显示的产品(确保只显示指定列数)
121
+ const products = selectedProducts.slice(0, COMPARISON_COLUMNS)
122
+
123
+ /**
124
+ * 获取指定产品在某个维度的值
125
+ */
126
+ const getDimensionValue = (dimension: ComparisonDimension, productId: string): any => {
127
+ if (!dimension || !dimension.values || !Array.isArray(dimension.values)) {
128
+ return null
129
+ }
130
+ return dimension.values.find(v => v && v.product_id === productId)
131
+ }
132
+
133
+ /**
134
+ * 渲染通用对比行
135
+ */
136
+ const renderComparisonRow = (label: string, dimension: ComparisonDimension | undefined) => {
137
+ if (!dimension) return null
138
+
139
+ return (
140
+ <div className="border-b border-[#DADCE0] pb-2">
141
+ {/* 维度标签(标题) */}
142
+ <div className="mb-1">
143
+ <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#86868C]">{label==='price'?'has member price':label}</span>
144
+ </div>
145
+
146
+ {/* 产品值列表(横向排列) */}
147
+ <div className="flex gap-4" style={{ gap: '36px' }}>
148
+ {products.map((product, index) => {
149
+ if (!product || !product.shopifyId) return null
150
+ const value = getDimensionValue(dimension, product.shopifyId)
151
+
152
+ return (
153
+ <div key={`comparison-${index}`} className="flex-1">
154
+ {renderDimensionValue(value, dimension.label)}
155
+ </div>
156
+ )
157
+ })}
158
+ </div>
159
+ </div>
160
+ )
161
+ }
162
+
163
+ /**
164
+ * 渲染维度值
165
+ */
166
+ const renderDimensionValue = (value: any, dimensionLabel: string) => {
167
+ if (!value) {
168
+ return <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">-</span>
169
+ }
170
+
171
+ // 根据不同维度类型渲染不同格式
172
+ if (dimensionLabel.toLowerCase().includes('price')) {
173
+ const hasMemberPrice = value?.has_member_price
174
+ // 价格维度
175
+ const priceDisplay = value?.available?
176
+ value.min === value.max
177
+ ? formatPrice(value.min, value.currency)
178
+ : `${formatPrice(value.min, value.currency)} - ${formatPrice(value.max, value.currency)}`:'-'
179
+ return <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">{hasMemberPrice?'Yes':'No'}</span>
180
+ }
181
+
182
+ if (dimensionLabel.toLowerCase().includes('variant')) {
183
+ // 变体数量
184
+ return (
185
+ <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">
186
+ {value.count || 0} {value.count === 1 ? 'variant' : 'variants'}
187
+ </span>
188
+ )
189
+ }
190
+
191
+ if (dimensionLabel.toLowerCase().includes('review')) {
192
+ // 评论维度:显示评分和数量
193
+ const rating = value.rating || 0
194
+ const count = value.count || 0
195
+ return (
196
+ <div className="flex items-center gap-1">
197
+ <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">
198
+ ⭐ {rating.toFixed(1)}
199
+ </span>
200
+ <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#86868C]">
201
+ ({count})
202
+ </span>
203
+ </div>
204
+ )
205
+ }
206
+
207
+ // 默认显示
208
+ return (
209
+ <span className="text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]">
210
+ {value.display || value.value || '-'}
211
+ </span>
212
+ )
213
+ }
214
+
215
+ return (
216
+ <div className="w-full overflow-hidden rounded-2xl bg-[#F5F6F7]">
217
+ {/* 顶部产品展示区域 */}
218
+ <div className="flex p-4" style={{ gap: '36px', paddingBottom: '0px' }}>
219
+ {products.map((product, index) => {
220
+ if (!product || !product.shopifyId) return null
221
+
222
+ // 获取价格信息
223
+ const priceInfo = dimensions.price ? getDimensionValue(dimensions.price, product.shopifyId) : null
224
+
225
+ // 获取折扣信息
226
+ const firstVariant = product.variants?.[0]
227
+ const hasDiscount = firstVariant?.discount?.has_discount
228
+ const discountPrice = hasDiscount ? firstVariant?.discount?.discount_price : null
229
+
230
+ // 当前显示价格:有折扣时显示折扣价,否则显示原价
231
+ const currentPrice = discountPrice || priceInfo?.min || product.price.amount
232
+ const originalPrice = product.price.amount
233
+
234
+ // Add to Cart 按钮点击处理
235
+ const handleAddToCart = (e: React.MouseEvent) => {
236
+ e.preventDefault()
237
+ e.stopPropagation()
238
+ if (onAddToCart) {
239
+ onAddToCart(product)
240
+ }
241
+ }
242
+
243
+ return (
244
+ <div key={`product-column-${index}`} className="flex flex-1 flex-col items-center">
245
+ {/* 产品选择下拉框 */}
246
+ <div className="mb-4 w-full">
247
+ <select
248
+ value={product.shopifyId}
249
+ onChange={e => handleProductChange(index, e.target.value)}
250
+ className="w-full rounded-lg border border-[#DADCE0] bg-[#F5F6F7] px-3 py-2 text-sm font-bold leading-[1.4] tracking-[-0.02em] text-[#1D1D1F]"
251
+ style={{
252
+ appearance: 'none',
253
+ backgroundImage:
254
+ "url(\"data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%231D1D1F' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")",
255
+ backgroundRepeat: 'no-repeat',
256
+ backgroundPosition: 'right 12px center',
257
+ paddingRight: '32px',
258
+ }}
259
+ >
260
+ {allProducts.map(p => (
261
+ <option key={p.shopifyId} value={p.shopifyId}>
262
+ {p.title.length > 30 ? `${p.title.slice(0, 30)}...` : p.title}
263
+ </option>
264
+ ))}
265
+ </select>
266
+ </div>
267
+
268
+ {/* 产品图片 */}
269
+ <a
270
+ href={product.productUrl}
271
+ target="_blank"
272
+ rel="noopener noreferrer"
273
+ className="mb-4 block w-full max-w-[160px]"
274
+ >
275
+ <div className="aspect-square w-full overflow-hidden rounded-lg">
276
+ {product.imageUrl ? (
277
+ <img
278
+ src={product.imageUrl}
279
+ alt={product.title}
280
+ className="size-full object-contain"
281
+ loading="lazy"
282
+ />
283
+ ) : (
284
+ <div className="flex size-full items-center justify-center text-gray-400">
285
+ <svg className="size-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
286
+ <path
287
+ strokeLinecap="round"
288
+ strokeLinejoin="round"
289
+ strokeWidth={2}
290
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
291
+ />
292
+ </svg>
293
+ </div>
294
+ )}
295
+ </div>
296
+ </a>
297
+
298
+ {/* 价格展示(带划线价) */}
299
+ <div className="mb-4 flex flex-col items-center gap-1">
300
+ <div className="flex items-center gap-2">
301
+ {/* 当前价格(折扣价或原价) */}
302
+ <span className="text-base font-bold leading-[1.2] tracking-[-0.02em] text-[#1D1D1F]">
303
+ {formatPrice(currentPrice, priceInfo?.currency || product.price.currency)}
304
+ </span>
305
+ {/* 划线价 - 仅在有折扣时显示 */}
306
+ {hasDiscount && discountPrice && (
307
+ <span className="text-base font-bold leading-[1.2] tracking-[-0.02em] text-[#4A4C56] line-through">
308
+ {formatPrice(originalPrice, product.price.currency)}
309
+ </span>
310
+ )}
311
+ </div>
312
+ </div>
313
+
314
+ {/* Add to Cart 按钮 */}
315
+ {onAddToCart && (
316
+ <button
317
+ type="button"
318
+ onClick={handleAddToCart}
319
+ className="mb-3 w-fit rounded-full px-[20px] py-[10px] text-center text-sm font-bold leading-[1.2] tracking-[-0.04em] text-white"
320
+ style={{ backgroundColor: '#1D1D1F' }}
321
+ >
322
+ {mergedText.addToCart}
323
+ </button>
324
+ )}
325
+
326
+ {/* 颜色选项(如果有variants) */}
327
+ {product.variants && product.variants.length > 1 && (
328
+ <div className="flex gap-2">
329
+ {product.variants.slice(0, 3).map((variant, idx) => (
330
+ <div
331
+ key={variant.id || idx}
332
+ className="size-6 rounded-full border-2 border-[#DADCE0]"
333
+ style={{ backgroundColor: variant.color || '#000' }}
334
+ title={variant.title}
335
+ />
336
+ ))}
337
+ </div>
338
+ )}
339
+ </div>
340
+ )
341
+ })}
342
+ </div>
343
+
344
+ {/* 对比表格区域 */}
345
+ <div className="flex flex-col gap-4 p-4">
346
+ {/* 遍历所有维度并渲染 */}
347
+ {Object.entries(dimensions).map(([key, dimension]) => {
348
+ if (!dimension) return null // price 已在顶部显示
349
+ return <div key={key}>{renderComparisonRow(dimension.label, dimension)}</div>
350
+ })}
351
+ </div>
352
+ </div>
353
+ )
354
+ }
355
+
356
+ /**
357
+ * 创建产品对比渲染器
358
+ */
359
+ export const ProductComparisonRenderer: MessageRenderer = {
360
+ render: (content, isUser, isSystem) => {
361
+ if (content.type !== 'product_comparison' || !content.data) {
362
+ return null
363
+ }
364
+
365
+ const comparisonData = content.data as ProductComparisonData & { onAddToCart?: (product: Product) => void }
366
+
367
+ return (
368
+ <ProductComparison
369
+ data={comparisonData}
370
+ isUser={isUser}
371
+ isSystem={isSystem}
372
+ onAddToCart={comparisonData.onAddToCart}
373
+ commonText={comparisonData.commonText}
374
+ />
375
+ )
376
+ },
377
+ }