@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.
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +23 -2
- package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
- package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
- package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
- package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
- package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent.js +1 -1
- package/dist/cjs/components/LiveChatWidget/components/MessageContent.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageList.js +2 -2
- package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/constants.d.ts +5 -0
- package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
- package/dist/cjs/components/LiveChatWidget/constants.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +35 -2
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/index.d.ts +1 -1
- package/dist/cjs/components/LiveChatWidget/index.js +1 -1
- package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
- package/dist/cjs/components/LiveChatWidget/types.d.ts +212 -3
- package/dist/cjs/components/LiveChatWidget/types.js +1 -1
- package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
- package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
- package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
- package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
- package/dist/cjs/components/chat/markdown.js +1 -1
- package/dist/cjs/components/chat/markdown.js.map +2 -2
- package/dist/cjs/components/credits/creditsBanner/index.js +2 -2
- package/dist/cjs/components/credits/creditsBanner/index.js.map +2 -2
- package/dist/cjs/components/index.d.ts +2 -0
- package/dist/cjs/components/index.js +1 -1
- package/dist/cjs/components/index.js.map +3 -3
- package/dist/cjs/stories/LiveChatWidget.stories.d.ts +1 -79
- package/dist/cjs/stories/LiveChatWidget.stories.js +8 -47
- package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/api/chat.d.ts +23 -2
- package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
- package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatHeader.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
- package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
- package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
- package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
- package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent.js +1 -1
- package/dist/esm/components/LiveChatWidget/components/MessageContent.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageList.js +2 -2
- package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/constants.d.ts +5 -0
- package/dist/esm/components/LiveChatWidget/constants.js +1 -1
- package/dist/esm/components/LiveChatWidget/constants.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +35 -2
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
- package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
- package/dist/esm/components/LiveChatWidget/index.js +1 -1
- package/dist/esm/components/LiveChatWidget/index.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/types.d.ts +212 -3
- package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
- package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
- package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
- package/dist/esm/components/chat/markdown.js +1 -1
- package/dist/esm/components/chat/markdown.js.map +2 -2
- package/dist/esm/components/credits/creditsBanner/index.js +2 -2
- package/dist/esm/components/credits/creditsBanner/index.js.map +2 -2
- package/dist/esm/components/index.d.ts +2 -0
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/components/index.js.map +3 -3
- package/dist/esm/stories/LiveChatWidget.stories.d.ts +1 -79
- package/dist/esm/stories/LiveChatWidget.stories.js +8 -47
- package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
- package/dist/index.d.mts +1305 -0
- package/dist/index.d.ts +1305 -0
- package/dist/index.js +26656 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +26641 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +8 -1
- package/src/components/LiveChatWidget/LiveChatWidget.tsx +887 -0
- package/src/components/LiveChatWidget/api/chat.ts +175 -0
- package/src/components/LiveChatWidget/components/ChatBubble.tsx +152 -0
- package/src/components/LiveChatWidget/components/ChatHeader.tsx +150 -0
- package/src/components/LiveChatWidget/components/ChatInput.tsx +253 -0
- package/src/components/LiveChatWidget/components/ChatMessage.tsx +190 -0
- package/src/components/LiveChatWidget/components/ChatWindow.tsx +363 -0
- package/src/components/LiveChatWidget/components/ComplianceDialog.tsx +216 -0
- package/src/components/LiveChatWidget/components/MessageContent/CartCard.tsx +202 -0
- package/src/components/LiveChatWidget/components/MessageContent/ErrorBlock.tsx +75 -0
- package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +128 -0
- package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +152 -0
- package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +227 -0
- package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +377 -0
- package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +293 -0
- package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +170 -0
- package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +91 -0
- package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +110 -0
- package/src/components/LiveChatWidget/components/MessageContent/ThinkingBlock.tsx +53 -0
- package/src/components/LiveChatWidget/components/MessageContent/index.ts +16 -0
- package/src/components/LiveChatWidget/components/MessageContent.tsx +113 -0
- package/src/components/LiveChatWidget/components/MessageList.tsx +261 -0
- package/src/components/LiveChatWidget/components/ScrollAnchor.tsx +75 -0
- package/src/components/LiveChatWidget/constants.ts +36 -0
- package/src/components/LiveChatWidget/hooks/useChatAPI.ts +146 -0
- package/src/components/LiveChatWidget/hooks/useChatState.ts +1090 -0
- package/src/components/LiveChatWidget/hooks/useSession.ts +123 -0
- package/src/components/LiveChatWidget/index.tsx +63 -0
- package/src/components/LiveChatWidget/types.ts +1011 -0
- package/src/components/LiveChatWidget/utils/cartTransformers.ts +72 -0
- package/src/components/LiveChatWidget/utils/fetcher.ts +131 -0
- package/src/components/LiveChatWidget/utils/messageRenderers.ts +120 -0
- package/src/components/LiveChatWidget/utils/productTransformers.ts +149 -0
- package/src/components/LiveChatWidget/utils/userId.ts +140 -0
- package/src/components/LiveChatWidget/utils/validation.ts +99 -0
- package/src/components/chat/markdown.tsx +1 -1
- package/src/components/credits/creditsBanner/index.tsx +5 -5
- package/src/components/index.ts +23 -0
- package/src/stories/LiveChatWidget.stories.tsx +322 -0
- package/src/styles/livechat.css +317 -0
- package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
- package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
- package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/atobID.d.ts +0 -1
- package/dist/cjs/components/credits/context/utils/atobID.js +0 -2
- package/dist/cjs/components/credits/context/utils/atobID.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
- package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js +0 -2
- package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
- package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
- package/dist/cjs/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
- package/dist/cjs/components/credits/context/utils/variantGetCoupon.js +0 -2
- package/dist/cjs/components/credits/context/utils/variantGetCoupon.js.map +0 -7
- package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
- package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
- package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
- package/dist/esm/components/credits/context/utils/atobID.d.ts +0 -1
- package/dist/esm/components/credits/context/utils/atobID.js +0 -2
- package/dist/esm/components/credits/context/utils/atobID.js.map +0 -7
- package/dist/esm/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
- package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js +0 -2
- package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
- package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
- package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
- package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
- package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
- package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
- package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
- package/dist/esm/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
- package/dist/esm/components/credits/context/utils/variantGetCoupon.js +0 -2
- 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
|
+
}
|