@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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 商品列表渲染器
|
|
3
|
+
* 显示多个商品的纵向列表,支持展开/收起
|
|
4
|
+
* 基于 specs/livechat-widget/data-model.md 的商品数据模型
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState } from 'react'
|
|
8
|
+
import type { MessageRenderer, ProductListContent, 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">
|
|
92
|
+
<div className="block">
|
|
93
|
+
<div className="flex gap-2 p-4">
|
|
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
|
+
const ProductListComponent: React.FC<{
|
|
171
|
+
products: Product[]
|
|
172
|
+
title?: string
|
|
173
|
+
onAddToCart?: (product: Product) => void
|
|
174
|
+
commonText?: CommonText
|
|
175
|
+
}> = ({ products, title, onAddToCart, commonText }) => {
|
|
176
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
177
|
+
|
|
178
|
+
// 合并默认文案和自定义文案
|
|
179
|
+
const mergedText = { ...DEFAULT_COMMON_TEXT, ...commonText }
|
|
180
|
+
|
|
181
|
+
// 过滤掉 null 或无效的产品
|
|
182
|
+
const validProducts = products.filter(p => p && p.shopifyId)
|
|
183
|
+
|
|
184
|
+
// 默认显示前3个产品
|
|
185
|
+
const INITIAL_DISPLAY_COUNT = 3
|
|
186
|
+
const hasMore = validProducts.length > INITIAL_DISPLAY_COUNT
|
|
187
|
+
const displayedProducts = isExpanded ? validProducts : validProducts.slice(0, INITIAL_DISPLAY_COUNT)
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex w-full flex-col gap-2">
|
|
191
|
+
{/* 列表标题(可选) */}
|
|
192
|
+
{title && <h3 className="text-sm font-semibold text-gray-900">{title}</h3>}
|
|
193
|
+
|
|
194
|
+
{/* 纵向排列的商品列表 */}
|
|
195
|
+
<div className="flex flex-col gap-1.5">
|
|
196
|
+
{displayedProducts.map(product => {
|
|
197
|
+
if (!product || !product.shopifyId) return null
|
|
198
|
+
return (
|
|
199
|
+
<CompactProductCard
|
|
200
|
+
key={product.shopifyId}
|
|
201
|
+
product={product}
|
|
202
|
+
onAddToCart={onAddToCart}
|
|
203
|
+
addToCartText={mergedText.addToCart}
|
|
204
|
+
offText={mergedText.off}
|
|
205
|
+
/>
|
|
206
|
+
)
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Learn More 按钮 */}
|
|
211
|
+
{hasMore && (
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
215
|
+
className="flex items-center justify-center gap-1.5 px-3 py-2 text-[14px] font-bold leading-[1.2] tracking-tighter text-[#080A0F]"
|
|
216
|
+
>
|
|
217
|
+
<span>{isExpanded ? mergedText.showLess : mergedText.learnMore}</span>
|
|
218
|
+
<svg
|
|
219
|
+
width="14"
|
|
220
|
+
height="14"
|
|
221
|
+
viewBox="0 0 24 24"
|
|
222
|
+
fill="none"
|
|
223
|
+
stroke="currentColor"
|
|
224
|
+
strokeWidth="2"
|
|
225
|
+
strokeLinecap="round"
|
|
226
|
+
strokeLinejoin="round"
|
|
227
|
+
className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
228
|
+
>
|
|
229
|
+
<polyline points="6 9 12 15 18 9" />
|
|
230
|
+
</svg>
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 商品列表渲染器
|
|
239
|
+
*
|
|
240
|
+
* 功能:
|
|
241
|
+
* - 纵向展示多个商品
|
|
242
|
+
* - 默认显示前3个产品
|
|
243
|
+
* - 支持展开/收起查看全部
|
|
244
|
+
* - 紧凑型卡片设计
|
|
245
|
+
* - 可选标题
|
|
246
|
+
*
|
|
247
|
+
* 布局:
|
|
248
|
+
* ```
|
|
249
|
+
* 标题(可选)
|
|
250
|
+
* ┌─────────────────┐
|
|
251
|
+
* │ [图] 商品标题 │
|
|
252
|
+
* │ $29.99 │
|
|
253
|
+
* └─────────────────┘
|
|
254
|
+
* ┌─────────────────┐
|
|
255
|
+
* │ [图] 商品标题 │
|
|
256
|
+
* │ $39.99 │
|
|
257
|
+
* └─────────────────┘
|
|
258
|
+
* ┌─────────────────┐
|
|
259
|
+
* │ [图] 商品标题 │
|
|
260
|
+
* │ $49.99 │
|
|
261
|
+
* └─────────────────┘
|
|
262
|
+
* [ Learn More ↓ ]
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```tsx
|
|
267
|
+
* const content: ProductListContent = {
|
|
268
|
+
* type: 'product_list',
|
|
269
|
+
* data: {
|
|
270
|
+
* title: '相关商品推荐',
|
|
271
|
+
* products: [product1, product2, product3, product4, product5]
|
|
272
|
+
* }
|
|
273
|
+
* }
|
|
274
|
+
* <ProductList.render(content, false, false) />
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export const ProductList: MessageRenderer = {
|
|
278
|
+
render: content => {
|
|
279
|
+
const productListContent = content as ProductListContent
|
|
280
|
+
const { products, title, onAddToCart, commonText } = productListContent.data
|
|
281
|
+
|
|
282
|
+
// 过滤掉 null 或无效的产品
|
|
283
|
+
const validProducts = products?.filter(p => p && p.shopifyId) || []
|
|
284
|
+
|
|
285
|
+
if (validProducts.length === 0) {
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<ProductListComponent products={validProducts} title={title} onAddToCart={onAddToCart} commonText={commonText} />
|
|
291
|
+
)
|
|
292
|
+
},
|
|
293
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 促销活动列表组件
|
|
3
|
+
* 显示当前进行中的促销活动信息
|
|
4
|
+
* 基于后端数据结构规范:promotion_list
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import type { MessageRenderer, CommonText } from '../../types'
|
|
9
|
+
import { DEFAULT_COMMON_TEXT } from '../../constants'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 促销活动数据结构
|
|
13
|
+
*/
|
|
14
|
+
export interface PromotionItem {
|
|
15
|
+
id: string // 活动 ID
|
|
16
|
+
title: string // 活动标题
|
|
17
|
+
subtitle?: string // 副标题(如 "Up to 30% off")
|
|
18
|
+
description?: string // 活动描述
|
|
19
|
+
banner_url?: string // Banner 图片 URL
|
|
20
|
+
url?: string // 活动详情页 URL
|
|
21
|
+
time_range: {
|
|
22
|
+
start: string // 开始时间 (ISO 8601)
|
|
23
|
+
end?: string | null // 结束时间,null 表示无结束日期
|
|
24
|
+
is_active: boolean // 是否当前活跃
|
|
25
|
+
}
|
|
26
|
+
priority?: number // 优先级(数字越大越靠前)
|
|
27
|
+
product_count?: number // 参与商品数量
|
|
28
|
+
metadata?: {
|
|
29
|
+
display_order?: number
|
|
30
|
+
target_audience?: string
|
|
31
|
+
highlight_color?: string
|
|
32
|
+
banner_url?:string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 促销活动列表数据结构
|
|
38
|
+
*/
|
|
39
|
+
export interface PromotionListData {
|
|
40
|
+
found: boolean // 是否找到活动
|
|
41
|
+
count: number // 返回的活动数量
|
|
42
|
+
total?: number // 总活动数量(可选)
|
|
43
|
+
results: PromotionItem[] // 活动列表
|
|
44
|
+
commonText?: CommonText // 通用文案配置
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PromotionListProps {
|
|
48
|
+
/**
|
|
49
|
+
* 促销活动列表数据
|
|
50
|
+
*/
|
|
51
|
+
data: PromotionListData
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 是否为用户消息
|
|
55
|
+
*/
|
|
56
|
+
isUser?: boolean
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 是否为系统消息
|
|
60
|
+
*/
|
|
61
|
+
isSystem?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 格式化日期显示
|
|
66
|
+
*/
|
|
67
|
+
const formatDate = (dateStr: string): string => {
|
|
68
|
+
const date = new Date(dateStr)
|
|
69
|
+
return date.toLocaleDateString('en-US', {
|
|
70
|
+
year: 'numeric',
|
|
71
|
+
month: 'short',
|
|
72
|
+
day: 'numeric',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 促销活动列表组件
|
|
78
|
+
*
|
|
79
|
+
* 功能:
|
|
80
|
+
* - 显示当前进行中的促销活动
|
|
81
|
+
* - 支持活动 Banner 图片展示
|
|
82
|
+
* - 显示活动时间范围
|
|
83
|
+
* - 显示参与商品数量
|
|
84
|
+
* - 支持跳转到活动详情页
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* <PromotionList
|
|
89
|
+
* data={{
|
|
90
|
+
* found: true,
|
|
91
|
+
* count: 2,
|
|
92
|
+
* results: [...]
|
|
93
|
+
* }}
|
|
94
|
+
* />
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export const PromotionList: React.FC<PromotionListProps> = ({ data, isUser = false, isSystem = false }) => {
|
|
98
|
+
const { found, results, commonText } = data
|
|
99
|
+
|
|
100
|
+
// 合并默认文案和自定义文案
|
|
101
|
+
const mergedText = { ...DEFAULT_COMMON_TEXT, ...commonText }
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="space-y-3">
|
|
105
|
+
{results.map(promotion => {
|
|
106
|
+
const bannerUrl = promotion.banner_url ||promotion?.metadata?.banner_url
|
|
107
|
+
|
|
108
|
+
// 没有图片则不展示
|
|
109
|
+
if (!bannerUrl) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div key={promotion.id} className="relative overflow-hidden rounded-2xl bg-[#F5F6F7]">
|
|
115
|
+
{/* Banner 图片 */}
|
|
116
|
+
<div className="aspect-[16/9] w-full overflow-hidden bg-gray-100">
|
|
117
|
+
<img
|
|
118
|
+
src={bannerUrl}
|
|
119
|
+
alt={promotion.title}
|
|
120
|
+
className="size-full object-cover object-center"
|
|
121
|
+
loading="lazy"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* 活动信息 - 叠加在图片上 */}
|
|
126
|
+
<div
|
|
127
|
+
className="absolute inset-0 flex flex-col justify-end p-4 text-[#080A0F]"
|
|
128
|
+
style={{ color: promotion?.metadata?.highlight_color }}
|
|
129
|
+
>
|
|
130
|
+
<div className="mb-2">
|
|
131
|
+
<h3 className="text-xl font-bold leading-[1.2] tracking-[-0.04em]">{promotion.title}</h3>
|
|
132
|
+
{promotion.subtitle && (
|
|
133
|
+
<p className="text-sm font-bold leading-[1.4] tracking-[-0.02em]">{promotion.subtitle}</p>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* 查看详情按钮 */}
|
|
138
|
+
{promotion.url && (
|
|
139
|
+
<a
|
|
140
|
+
href={promotion.url + '?ref=LiveChat'}
|
|
141
|
+
target="_blank"
|
|
142
|
+
rel="noopener noreferrer"
|
|
143
|
+
className="inline-flex items-center gap-1 text-sm font-bold tracking-[-0.04em]"
|
|
144
|
+
>
|
|
145
|
+
{mergedText.learnMore}
|
|
146
|
+
<svg className="size-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
147
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
148
|
+
</svg>
|
|
149
|
+
</a>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 创建促销活动列表渲染器
|
|
161
|
+
*/
|
|
162
|
+
export const PromotionListRenderer: MessageRenderer = {
|
|
163
|
+
render: (content, isUser, isSystem) => {
|
|
164
|
+
if (content.type !== 'promotion_list' || !content.data) {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return <PromotionList data={content.data as PromotionListData} isUser={isUser} isSystem={isSystem} />
|
|
169
|
+
},
|
|
170
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 快捷回复按钮渲染器
|
|
3
|
+
* 显示可点击的快捷回复选项
|
|
4
|
+
* 基于 specs/livechat-widget/data-model.md 的快捷回复数据模型
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import type { MessageRenderer, QuickRepliesContent, QuickReply } from '../../types'
|
|
9
|
+
|
|
10
|
+
export interface QuickRepliesProps {
|
|
11
|
+
/**
|
|
12
|
+
* 快捷回复点击回调
|
|
13
|
+
*/
|
|
14
|
+
onReplyClick?: (reply: QuickReply) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 快捷回复按钮渲染器
|
|
19
|
+
*
|
|
20
|
+
* 功能:
|
|
21
|
+
* - 显示多个快捷回复按钮
|
|
22
|
+
* - 支持图标(可选)
|
|
23
|
+
* - 点击后发送对应的消息
|
|
24
|
+
*
|
|
25
|
+
* 使用场景:
|
|
26
|
+
* - 欢迎消息后的常见问题
|
|
27
|
+
* - 引导用户选择
|
|
28
|
+
* - 快速操作按钮
|
|
29
|
+
*
|
|
30
|
+
* 布局:
|
|
31
|
+
* ```
|
|
32
|
+
* ┌──────────┐ ┌──────────┐
|
|
33
|
+
* │ 🛒 查价格 │ │ 📦 查物流 │
|
|
34
|
+
* └──────────┘ └──────────┘
|
|
35
|
+
* ┌──────────┐ ┌──────────┐
|
|
36
|
+
* │ 📞 联系客服│ │ 💬 其他问题│
|
|
37
|
+
* └──────────┘ └──────────┘
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const content: QuickRepliesContent = {
|
|
43
|
+
* type: 'quick_replies',
|
|
44
|
+
* data: {
|
|
45
|
+
* replies: [
|
|
46
|
+
* { id: '1', label: '查价格', value: '我想查询商品价格', icon: '🛒' },
|
|
47
|
+
* { id: '2', label: '查物流', value: '我想查询物流信息', icon: '📦' }
|
|
48
|
+
* ]
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* <QuickReplies.render(content, false, false) />
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const createQuickRepliesRenderer = (onReplyClick?: (reply: QuickReply) => void): MessageRenderer => ({
|
|
55
|
+
render: (content, isUser, isSystem) => {
|
|
56
|
+
const quickRepliesContent = content as QuickRepliesContent
|
|
57
|
+
const { replies } = quickRepliesContent.data
|
|
58
|
+
|
|
59
|
+
if (!replies || replies.length === 0) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handleClick = (reply: QuickReply) => {
|
|
64
|
+
onReplyClick?.(reply)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-wrap gap-2">
|
|
69
|
+
{replies.map(reply => (
|
|
70
|
+
<button
|
|
71
|
+
key={reply.id}
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => handleClick(reply)}
|
|
74
|
+
className="livechat-quick-reply-button inline-flex font-bold items-center gap-1 rounded-[19px] px-3 py-[6px] text-sm leading-[140%] tracking-[-0.02em] transition-transform"
|
|
75
|
+
>
|
|
76
|
+
{/* 图标(可选) */}
|
|
77
|
+
{reply.icon && <span className="text-base">{reply.icon}</span>}
|
|
78
|
+
|
|
79
|
+
{/* 标签 */}
|
|
80
|
+
<span className='text-left'>{reply.label}</span>
|
|
81
|
+
</button>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 默认快捷回复渲染器(无回调)
|
|
90
|
+
*/
|
|
91
|
+
export const QuickReplies: MessageRenderer = createQuickRepliesRenderer()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本消息内容渲染器
|
|
3
|
+
* 支持 Markdown 格式
|
|
4
|
+
* 基于 specs/livechat-widget/plan.md 的文本消息设计
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import ReactMarkdown from 'react-markdown'
|
|
9
|
+
import remarkGfm from 'remark-gfm'
|
|
10
|
+
import type { MessageRenderer, TextContent } from '../../types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 文本消息渲染器
|
|
14
|
+
*
|
|
15
|
+
* 功能:
|
|
16
|
+
* - 支持 Markdown 语法(粗体、斜体、链接、列表等)
|
|
17
|
+
* - 安全渲染(React Markdown 自动防护 XSS)
|
|
18
|
+
* - 响应式文本样式
|
|
19
|
+
*
|
|
20
|
+
* Markdown 支持:
|
|
21
|
+
* - 粗体:**text** 或 __text__
|
|
22
|
+
* - 斜体:*text* 或 _text_
|
|
23
|
+
* - 链接:[text](url)
|
|
24
|
+
* - 列表:- item 或 1. item
|
|
25
|
+
* - 代码:`code` 或 ```code block```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const content: TextContent = {
|
|
30
|
+
* type: 'text',
|
|
31
|
+
* text: '您好!**这是粗体**,*这是斜体*。'
|
|
32
|
+
* }
|
|
33
|
+
* <TextBlock.render(content, false, false) />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const TextBlock: MessageRenderer = {
|
|
37
|
+
render: (content, isUser, isSystem) => {
|
|
38
|
+
const textContent = content as TextContent
|
|
39
|
+
|
|
40
|
+
if (!textContent.text) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="livechat-markdown text-base md:text-sm">
|
|
46
|
+
<ReactMarkdown
|
|
47
|
+
remarkPlugins={[remarkGfm]}
|
|
48
|
+
components={{
|
|
49
|
+
// 自定义链接样式
|
|
50
|
+
a: ({ node, ...props }) => (
|
|
51
|
+
<a
|
|
52
|
+
{...props}
|
|
53
|
+
className={`underline ${isUser ? 'text-blue-200 hover:text-blue-100' : 'text-blue-600 hover:text-blue-700'}`}
|
|
54
|
+
target="_blank"
|
|
55
|
+
rel="noopener noreferrer"
|
|
56
|
+
/>
|
|
57
|
+
),
|
|
58
|
+
// 自定义代码块样式
|
|
59
|
+
code: ({ node, ...props }: any) =>
|
|
60
|
+
props.inline ? (
|
|
61
|
+
<code
|
|
62
|
+
{...props}
|
|
63
|
+
className={`rounded px-1.5 py-0.5 font-mono text-xs ${isUser ? 'bg-[#004A6E] text-white' : 'bg-gray-200 text-gray-800'}`}
|
|
64
|
+
/>
|
|
65
|
+
) : (
|
|
66
|
+
<code
|
|
67
|
+
{...props}
|
|
68
|
+
className={`block overflow-x-auto rounded px-3 py-2 font-mono text-xs ${isUser ? 'bg-[#004A6E] text-white' : 'bg-gray-200 text-gray-800'}`}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
// 自定义段落样式
|
|
72
|
+
p: ({ node, ...props }) => <p {...props} className="last:mb-0" />,
|
|
73
|
+
// 自定义列表样式
|
|
74
|
+
ul: ({ node, ...props }) => <ul {...props} className="ml-4 list-disc" />,
|
|
75
|
+
ol: ({ node, ...props }) => <ol {...props} className="mb-2 ml-4 list-decimal" />,
|
|
76
|
+
li: ({ node, ...props }) => <li {...props} className="mb-1" />,
|
|
77
|
+
// 自定义标题样式
|
|
78
|
+
h1: ({ node, ...props }) => <h1 {...props} className="mb-2 font-bold" />,
|
|
79
|
+
h2: ({ node, ...props }) => <h2 {...props} className="mb-2 font-bold" />,
|
|
80
|
+
h3: ({ node, ...props }) => <h3 {...props} className="mb-1 font-bold" />,
|
|
81
|
+
// 自定义强调样式
|
|
82
|
+
strong: ({ node, ...props }) => <strong {...props} className="font-bold" />,
|
|
83
|
+
em: ({ node, ...props }) => <em {...props} className="italic" />,
|
|
84
|
+
// 表格样式
|
|
85
|
+
table: ({ node, ...props }) => (
|
|
86
|
+
<div className="my-2 overflow-x-auto">
|
|
87
|
+
<table {...props} className="min-w-full border-collapse border border-gray-300 text-base md:text-sm" />
|
|
88
|
+
</div>
|
|
89
|
+
),
|
|
90
|
+
thead: ({ node, ...props }) => (
|
|
91
|
+
<thead {...props} className="bg-gray-100" />
|
|
92
|
+
),
|
|
93
|
+
tbody: ({ node, ...props }) => <tbody {...props} />,
|
|
94
|
+
tr: ({ node, ...props }) => (
|
|
95
|
+
<tr {...props} className="border-b border-gray-300" />
|
|
96
|
+
),
|
|
97
|
+
th: ({ node, ...props }) => (
|
|
98
|
+
<th {...props} className="border border-gray-300 px-3 py-2 text-left font-semibold" />
|
|
99
|
+
),
|
|
100
|
+
td: ({ node, ...props }) => (
|
|
101
|
+
<td {...props} className="border border-gray-300 px-3 py-2" />
|
|
102
|
+
),
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{textContent.text}
|
|
106
|
+
</ReactMarkdown>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
}
|