@anker-in/campaign-ui 0.4.0-beta.7 → 0.4.0
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.js +1 -1
- package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +1 -0
- 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/hooks/useChatState.d.ts +11 -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 +14 -0
- package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
- package/dist/cjs/components/LiveChatWidget/utils/userId.d.ts +7 -2
- package/dist/cjs/components/LiveChatWidget/utils/userId.js +1 -1
- package/dist/cjs/components/LiveChatWidget/utils/userId.js.map +3 -3
- package/dist/cjs/stories/LiveChatWidget.stories.js +1 -1
- package/dist/cjs/stories/LiveChatWidget.stories.js.map +2 -2
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
- package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
- package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +1 -0
- 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/hooks/useChatState.d.ts +11 -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 +3 -3
- package/dist/esm/components/LiveChatWidget/types.d.ts +14 -0
- package/dist/esm/components/LiveChatWidget/utils/userId.d.ts +7 -2
- package/dist/esm/components/LiveChatWidget/utils/userId.js +1 -1
- package/dist/esm/components/LiveChatWidget/utils/userId.js.map +3 -3
- package/dist/esm/stories/LiveChatWidget.stories.js +1 -1
- package/dist/esm/stories/LiveChatWidget.stories.js.map +2 -2
- package/package.json +1 -1
- package/src/components/LiveChatWidget/LiveChatWidget.tsx +44 -8
- package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +6 -13
- package/src/components/LiveChatWidget/hooks/useChatState.ts +26 -7
- package/src/components/LiveChatWidget/index.tsx +1 -1
- package/src/components/LiveChatWidget/types.ts +15 -0
- package/src/components/LiveChatWidget/utils/userId.ts +13 -62
- package/src/stories/LiveChatWidget.stories.tsx +1 -1
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
* 策略:
|
|
6
6
|
* 1. 优先从 localStorage 读取
|
|
7
7
|
* 2. 尝试获取 Google Analytics ID (GAID)
|
|
8
|
-
* 3.
|
|
8
|
+
* 3. 返回空字符串,由后端生成
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* 获取用户唯一标识符(异步版本)
|
|
12
|
-
* @returns userId (GAID
|
|
12
|
+
* @returns userId (GAID 或空字符串,空字符串由后端生成)
|
|
13
13
|
*/
|
|
14
14
|
export declare function getUserId(): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* 保存后端返回的 userId 到 localStorage
|
|
17
|
+
* @param id 后端生成的 userId
|
|
18
|
+
*/
|
|
19
|
+
export declare function saveUserId(id: string): void;
|
|
15
20
|
/**
|
|
16
21
|
* 清除保存的 userId(用于测试或重置)
|
|
17
22
|
*/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const
|
|
1
|
+
const n="livechat_user_id";async function i(){if(typeof window<"u"){const t=localStorage.getItem(n);if(t)return t}const e=r();return e?(typeof window<"u"&&localStorage.setItem(n,e),e):""}function a(e){e&&typeof window<"u"&&localStorage.setItem(n,e)}function r(){if(typeof window>"u")return null;if(typeof window.gtag!="function")return console.warn("[LiveChat userId] Google Analytics gtag is not available"),null;try{const e=window.dataLayer||[];for(const t of e)if(t&&t[1]&&typeof t[1]=="object"){const o=t[1].client_id||t[1].clientId;if(o&&typeof o=="string")return`G-${o}`}return null}catch(e){return console.error("[LiveChat userId] Failed to get GAID:",e),null}}function d(){typeof window<"u"&&localStorage.removeItem(n)}export{d as clearUserId,i as getUserId,a as saveUserId};
|
|
2
2
|
//# sourceMappingURL=userId.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/components/LiveChatWidget/utils/userId.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * userId \u751F\u6210\u548C\u7BA1\u7406\u5DE5\u5177\n * \u57FA\u4E8E specs/livechat-widget/research.md \u7684\u51B3\u7B56\n *\n * \u7B56\u7565\uFF1A\n * 1. \u4F18\u5148\u4ECE localStorage \u8BFB\u53D6\n * 2. \u5C1D\u8BD5\u83B7\u53D6 Google Analytics ID (GAID)\n * 3. \
|
|
5
|
-
"mappings": "AAUA,MAAMA,EAAc,mBAMpB,eAAsBC,GAA6B,CAEjD,GAAI,OAAO,OAAW,IAAa,CACjC,MAAMC,EAAS,aAAa,QAAQF,CAAW,EAC/C,GAAIE,EAAQ,OAAOA,CACrB,CAGA,MAAMC,EAAOC,EAAQ,EACrB,
|
|
6
|
-
"names": ["STORAGE_KEY", "getUserId", "stored", "gaid", "getGAID", "
|
|
4
|
+
"sourcesContent": ["/**\n * userId \u751F\u6210\u548C\u7BA1\u7406\u5DE5\u5177\n * \u57FA\u4E8E specs/livechat-widget/research.md \u7684\u51B3\u7B56\n *\n * \u7B56\u7565\uFF1A\n * 1. \u4F18\u5148\u4ECE localStorage \u8BFB\u53D6\n * 2. \u5C1D\u8BD5\u83B7\u53D6 Google Analytics ID (GAID)\n * 3. \u8FD4\u56DE\u7A7A\u5B57\u7B26\u4E32\uFF0C\u7531\u540E\u7AEF\u751F\u6210\n */\n\nconst STORAGE_KEY = 'livechat_user_id'\n\n/**\n * \u83B7\u53D6\u7528\u6237\u552F\u4E00\u6807\u8BC6\u7B26\uFF08\u5F02\u6B65\u7248\u672C\uFF09\n * @returns userId (GAID \u6216\u7A7A\u5B57\u7B26\u4E32\uFF0C\u7A7A\u5B57\u7B26\u4E32\u7531\u540E\u7AEF\u751F\u6210)\n */\nexport async function getUserId(): Promise<string> {\n // 1. \u5C1D\u8BD5\u4ECE localStorage \u8BFB\u53D6\n if (typeof window !== 'undefined') {\n const stored = localStorage.getItem(STORAGE_KEY)\n if (stored) return stored\n }\n\n // 2. \u5C1D\u8BD5\u83B7\u53D6 GAID\n const gaid = getGAID()\n if (gaid) {\n if (typeof window !== 'undefined') {\n localStorage.setItem(STORAGE_KEY, gaid)\n }\n return gaid\n }\n\n // 3. \u8FD4\u56DE\u7A7A\u5B57\u7B26\u4E32\uFF0C\u7531\u540E\u7AEF\u751F\u6210\n return ''\n}\n\n/**\n * \u4FDD\u5B58\u540E\u7AEF\u8FD4\u56DE\u7684 userId \u5230 localStorage\n * @param id \u540E\u7AEF\u751F\u6210\u7684 userId\n */\nexport function saveUserId(id: string): void {\n if (id && typeof window !== 'undefined') {\n localStorage.setItem(STORAGE_KEY, id)\n }\n}\n\n/**\n * \u4ECE Google Analytics \u83B7\u53D6 Client ID (GAID)\n * @returns GAID \u6216 null\n */\nfunction getGAID(): string | null {\n if (typeof window === 'undefined') return null\n\n // \u68C0\u67E5 gtag \u662F\u5426\u53EF\u7528\n if (typeof (window as any).gtag !== 'function') {\n console.warn('[LiveChat userId] Google Analytics gtag is not available')\n return null\n }\n\n try {\n // Google Analytics 4 \u65B9\u6CD5\n // \u6CE8\u610F\uFF1Agtag \u7684 'get' \u547D\u4EE4\u662F\u5F02\u6B65\u7684\uFF0C\u8FD9\u91CC\u4F7F\u7528\u540C\u6B65 fallback\n // \u5728\u5B9E\u9645\u9879\u76EE\u4E2D\uFF0C\u5982\u679C GA \u5DF2\u521D\u59CB\u5316\uFF0C\u53EF\u4EE5\u4ECE dataLayer \u8BFB\u53D6\n const dataLayer = (window as any).dataLayer || []\n\n // \u5C1D\u8BD5\u4ECE dataLayer \u4E2D\u67E5\u627E\u5DF2\u5B58\u5728\u7684 client_id\n for (const item of dataLayer) {\n if (item && item[1] && typeof item[1] === 'object') {\n const clientId = item[1].client_id || item[1].clientId\n if (clientId && typeof clientId === 'string') {\n return `G-${clientId}`\n }\n }\n }\n\n // \u5982\u679C\u6CA1\u6709\u627E\u5230\uFF0C\u8FD4\u56DE null\uFF0C\u4F7F\u7528\u515C\u5E95\u65B9\u6848\n return null\n } catch (error) {\n console.error('[LiveChat userId] Failed to get GAID:', error)\n return null\n }\n}\n\n/**\n * \u6E05\u9664\u4FDD\u5B58\u7684 userId\uFF08\u7528\u4E8E\u6D4B\u8BD5\u6216\u91CD\u7F6E\uFF09\n */\nexport function clearUserId(): void {\n if (typeof window !== 'undefined') {\n localStorage.removeItem(STORAGE_KEY)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAUA,MAAMA,EAAc,mBAMpB,eAAsBC,GAA6B,CAEjD,GAAI,OAAO,OAAW,IAAa,CACjC,MAAMC,EAAS,aAAa,QAAQF,CAAW,EAC/C,GAAIE,EAAQ,OAAOA,CACrB,CAGA,MAAMC,EAAOC,EAAQ,EACrB,OAAID,GACE,OAAO,OAAW,KACpB,aAAa,QAAQH,EAAaG,CAAI,EAEjCA,GAIF,EACT,CAMO,SAASE,EAAWC,EAAkB,CACvCA,GAAM,OAAO,OAAW,KAC1B,aAAa,QAAQN,EAAaM,CAAE,CAExC,CAMA,SAASF,GAAyB,CAChC,GAAI,OAAO,OAAW,IAAa,OAAO,KAG1C,GAAI,OAAQ,OAAe,MAAS,WAClC,eAAQ,KAAK,0DAA0D,EAChE,KAGT,GAAI,CAIF,MAAMG,EAAa,OAAe,WAAa,CAAC,EAGhD,UAAWC,KAAQD,EACjB,GAAIC,GAAQA,EAAK,CAAC,GAAK,OAAOA,EAAK,CAAC,GAAM,SAAU,CAClD,MAAMC,EAAWD,EAAK,CAAC,EAAE,WAAaA,EAAK,CAAC,EAAE,SAC9C,GAAIC,GAAY,OAAOA,GAAa,SAClC,MAAO,KAAKA,CAAQ,EAExB,CAIF,OAAO,IACT,OAASC,EAAO,CACd,eAAQ,MAAM,wCAAyCA,CAAK,EACrD,IACT,CACF,CAKO,SAASC,GAAoB,CAC9B,OAAO,OAAW,KACpB,aAAa,WAAWX,CAAW,CAEvC",
|
|
6
|
+
"names": ["STORAGE_KEY", "getUserId", "stored", "gaid", "getGAID", "saveUserId", "id", "dataLayer", "item", "clientId", "error", "clearUserId"]
|
|
7
7
|
}
|
|
@@ -48,5 +48,5 @@ const customRenderers = {
|
|
|
48
48
|
customRenderers={customRenderers}
|
|
49
49
|
/>
|
|
50
50
|
\`\`\`
|
|
51
|
-
`}}},tags:["autodocs"],argTypes:{apiBaseUrl:{control:"text",description:"API \u57FA\u7840 URL"},headers:{control:"object",description:"\u81EA\u5B9A\u4E49\u8BF7\u6C42\u5934\uFF0C\u5C06\u5728\u6240\u6709 API \u8BF7\u6C42\u4E2D\u6DFB\u52A0",table:{defaultValue:{summary:"undefined"}}},recaptchaSitekey:{control:"text",description:"Google reCAPTCHA v3 site key\uFF0C\u63D0\u4F9B\u6B64\u53C2\u6570\u5C06\u81EA\u52A8\u542F\u7528 reCAPTCHA v3 \u9A8C\u8BC1",table:{defaultValue:{summary:"undefined"}}},recaptchaAction:{control:"text",description:"reCAPTCHA action \u540D\u79F0\uFF0C\u7528\u4E8E\u533A\u5206\u4E0D\u540C\u7684\u9A8C\u8BC1\u573A\u666F",table:{defaultValue:{summary:'"activity"'}}},site:{control:"text",description:"Shopify \u5E97\u94FA URL"},channelCode:{control:"text",description:"\u6E20\u9053\u7F16\u7801\uFF0C\u7528\u4E8E\u6807\u8BC6\u6765\u6E90\u6E20\u9053",table:{defaultValue:{summary:"undefined"}}},welcomeMessage:{control:"text",description:"\u6B22\u8FCE\u6D88\u606F",table:{defaultValue:{summary:"\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F"}}},logoUrl:{control:"text",description:"Logo URL"},position:{control:"object",description:"\u6C14\u6CE1\u6309\u94AE\u4F4D\u7F6E\u5BF9\u8C61",table:{defaultValue:{summary:'{ bottom: "1.5rem", right: "1.5rem" }'}}}},args:{apiBaseUrl:"http://172.16.38.183:3003",site:"www.eufy.com",loginUserId:"test_test",welcomeMessage:"\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F"}};var g=c;const m={args:{loginUserId:"test_test1",apiBaseUrl:"
|
|
51
|
+
`}}},tags:["autodocs"],argTypes:{apiBaseUrl:{control:"text",description:"API \u57FA\u7840 URL"},headers:{control:"object",description:"\u81EA\u5B9A\u4E49\u8BF7\u6C42\u5934\uFF0C\u5C06\u5728\u6240\u6709 API \u8BF7\u6C42\u4E2D\u6DFB\u52A0",table:{defaultValue:{summary:"undefined"}}},recaptchaSitekey:{control:"text",description:"Google reCAPTCHA v3 site key\uFF0C\u63D0\u4F9B\u6B64\u53C2\u6570\u5C06\u81EA\u52A8\u542F\u7528 reCAPTCHA v3 \u9A8C\u8BC1",table:{defaultValue:{summary:"undefined"}}},recaptchaAction:{control:"text",description:"reCAPTCHA action \u540D\u79F0\uFF0C\u7528\u4E8E\u533A\u5206\u4E0D\u540C\u7684\u9A8C\u8BC1\u573A\u666F",table:{defaultValue:{summary:'"activity"'}}},site:{control:"text",description:"Shopify \u5E97\u94FA URL"},channelCode:{control:"text",description:"\u6E20\u9053\u7F16\u7801\uFF0C\u7528\u4E8E\u6807\u8BC6\u6765\u6E90\u6E20\u9053",table:{defaultValue:{summary:"undefined"}}},welcomeMessage:{control:"text",description:"\u6B22\u8FCE\u6D88\u606F",table:{defaultValue:{summary:"\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F"}}},logoUrl:{control:"text",description:"Logo URL"},position:{control:"object",description:"\u6C14\u6CE1\u6309\u94AE\u4F4D\u7F6E\u5BF9\u8C61",table:{defaultValue:{summary:'{ bottom: "1.5rem", right: "1.5rem" }'}}}},args:{apiBaseUrl:"http://172.16.38.183:3003",site:"www.eufy.com",loginUserId:"test_test",welcomeMessage:"\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F"}};var g=c;const m={args:{loginUserId:"test_test1",apiBaseUrl:"https://beta-api-v2-livechat.anker.com",site:"beta.eufy.com",channelCode:"dtc",title:"eufy AI Assistant",cartId:"gid://shopify/Cart/hWN7wB3Pa12gh78d8hPOAUBI?key=0e73db1d3fb5ac21da19099c45033253",accessToken:"47b1aa2c0797043f9baba39388029d70",position:{bottom:"24px",right:"30px"},welcomeMessage:"",quickReplies:[{id:"1",label:"Product Info",value:"Tell me about your products",icon:"\u{1F4E6}"},{id:"2",label:"Track Order",value:"I want to track my order",icon:"\u{1F69A}"},{id:"3",label:"Support",value:"I need help with my device",icon:"\u{1F527}"},{id:"4",label:"Recommendations",value:"Recommend products for me",icon:"\u2B50"}],complianceConfig:{title:"Hi! I'm your eufy AI assistant.",content:"AI-generated responses can be inaccurate. Please verify important info. Do not input sensitive personal data.",checkboxText:`By starting to use "Live Chat", you agree to Anker's <a href="https://www.anker.com/pages/privacy-policy" target="_blank" rel="noopener noreferrer" style="text-decoration: underline;">LIVE CHAT PRIVACY NOTICE</a>.`,agreeButtonText:"Agree"},recaptchaAction:"livechat",showNewSessionButton:!0,commonText:{learnMore:"Learn More",total:"Total"},customRenderers:{video:{render:t=>{const e=t;return r("div",{className:"w-full",children:[o("video",{src:e.url,controls:!0,className:"w-full rounded-lg",poster:e.poster,children:"Your browser does not support video playback"}),e.title&&o("p",{className:"mt-2 text-sm text-gray-600",children:e.title})]})}},image_gallery:{render:t=>{const a=t.images||[];return o("div",{className:"grid grid-cols-2 gap-2",children:a.map((i,n)=>o("div",{className:"relative aspect-square",children:o("img",{src:i.url,alt:i.alt||`Image ${n+1}`,className:"size-full rounded-lg object-cover"})},n))})}}},productCardRender:(t,e)=>{if(!t)return o("div",{style:{padding:"16px",border:"1px dashed #ccc",borderRadius:"8px",textAlign:"center"},children:r("p",{children:["Product loading... (handle: ",e,")"]})});const a=t?.featured_image||"",i=t?.title||"",n=t?.description||"",s=t?.average_rating;return o("div",{style:{border:"2px solid #4CAF50",borderRadius:"16px",padding:"16px",margin:"12px 0",backgroundColor:"#f0f9ff",boxShadow:"0 4px 12px rgba(76, 175, 80, 0.15)"},children:r("div",{style:{display:"flex",gap:"12px",alignItems:"center"},children:[a&&o("img",{src:a,alt:i,style:{width:"60px",height:"60px",borderRadius:"8px",objectFit:"cover"}}),r("div",{style:{flex:1},children:[o("h4",{style:{margin:"0 0 4px 0",fontSize:"16px",fontWeight:"bold"},children:i}),n&&r("p",{style:{margin:0,fontSize:"12px",color:"#666",lineHeight:1.4},children:[n.slice(0,80),"..."]}),s&&r("span",{style:{fontSize:"12px",color:"#FFB800"},children:["Rating: ",s.toFixed(1)]})]}),o("button",{style:{padding:"8px 16px",backgroundColor:"#4CAF50",color:"white",borderRadius:"8px",border:"none",cursor:"pointer"},onClick:()=>console.log("View product:",e,t),children:"View"})]})})}},render:t=>o(l,{...t,onOpen:()=>console.log("[LiveChat] Chat opened"),onClose:()=>console.log("[LiveChat] Chat closed"),onMessageSend:e=>console.log("[LiveChat] Message sent:",e),onError:e=>console.error("[LiveChat] Error:",e),onTextMessage:()=>console.log("[LiveChat] AI text message received"),onProductList:()=>console.log("[LiveChat] Product list received"),onPromotionList:()=>console.log("[LiveChat] Promotion list received"),onAddToCart:e=>{console.log("[LiveChat] Add to cart:",e),alert(`Added "${e.title}" to cart!`)},onCart:(e,a)=>{console.log("[LiveChat] Cart clicked:",{cartId:e,checkoutUrl:a}),alert(`Cart ID: ${e}`)}})};export{m as Default,g as default};
|
|
52
52
|
//# sourceMappingURL=LiveChatWidget.stories.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/stories/LiveChatWidget.stories.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * LiveChatWidget Storybook Stories\n * \u5C55\u793A LiveChat \u7EC4\u4EF6\u7684\u5404\u79CD\u4F7F\u7528\u573A\u666F\u548C\u914D\u7F6E\n */\n\nimport type { Meta, StoryObj } from '@storybook/react'\nimport { LiveChatWidget } from '../components/LiveChatWidget'\nimport type { MessageRenderer, MessageContent } from '../components/LiveChatWidget'\nimport '../styles/livechat.css'\n\nconst meta: Meta<typeof LiveChatWidget> = {\n title: 'Campaign/LiveChatWidget',\n component: LiveChatWidget,\n parameters: {\n layout: 'fullscreen',\n docs: {\n story: {\n inline: false,\n iframeHeight: 500,\n },\n description: {\n component: `\n# LiveChat \u804A\u5929\u7EC4\u4EF6\n\n\u53EF\u590D\u7528\u7684\u6C14\u6CE1\u5F39\u7A97\u804A\u5929\u7EC4\u4EF6\uFF0C\u652F\u6301 SSE \u6D41\u5F0F\u6D88\u606F\u3001\u81EA\u5B9A\u4E49\u6E32\u67D3\u5668\u548C\u591A\u79CD\u6D88\u606F\u7C7B\u578B\u3002\n\n## \u529F\u80FD\u7279\u6027\n\n- \uD83C\uDF88 **\u6C14\u6CE1\u5F39\u7A97**: \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E\u7684\u60AC\u6D6E\u6C14\u6CE1\u6309\u94AE\n- \uD83D\uDCAC **\u6D41\u5F0F\u6D88\u606F**: \u57FA\u4E8E SSE \u7684\u5B9E\u65F6\u6D41\u5F0F\u54CD\u5E94\n- \uD83D\uDCE6 **\u591A\u79CD\u6D88\u606F\u7C7B\u578B**: \u6587\u672C\u3001\u5546\u54C1\u5361\u7247\u3001\u5546\u54C1\u5217\u8868\u3001\u653F\u7B56\u3001\u5FEB\u6377\u56DE\u590D\u7B49\n- \uD83C\uDFA8 **\u53EF\u5B9A\u5236**: \u652F\u6301\u81EA\u5B9A\u4E49\u54C1\u724C\u989C\u8272\u3001Logo\u3001\u6E32\u67D3\u5668\n- \uD83D\uDCF1 **\u54CD\u5E94\u5F0F**: \u79FB\u52A8\u7AEF\u5168\u5C4F\uFF0C\u684C\u9762\u7AEF\u56FA\u5B9A\u5C3A\u5BF8\n- \uD83D\uDCBE **\u4F1A\u8BDD\u7BA1\u7406**: \u81EA\u52A8\u7BA1\u7406 userId \u548C sessionId\n- \uD83D\uDD12 **\u5B89\u5168\u9632\u62A4**: \u5185\u7F6E XSS \u9632\u62A4\u548C\u8F93\u5165\u9A8C\u8BC1\n\n## \u57FA\u7840\u7528\u6CD5\n\n\\`\\`\\`tsx\nimport { LiveChatWidget } from '@anker-in/campaign-ui'\nimport '@anker-in/campaign-ui/livechat.css'\n\nfunction App() {\n return (\n <LiveChatWidget\n apiBaseUrl=\"https://beta-api-livechat.anker.com\"\n site=\"www.eufy.com\"\n channel_code=\"web_homepage\"\n welcomeMessage=\"\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\"\n />\n )\n}\n\\`\\`\\`\n\n## \u81EA\u5B9A\u4E49\u6E32\u67D3\u5668\n\n\\`\\`\\`tsx\nconst customRenderers = {\n video: {\n render: (content) => (\n <video src={content.url} controls className=\"w-full rounded\" />\n )\n }\n}\n\n<LiveChatWidget\n apiBaseUrl=\"...\"\n site=\"...\"\n customRenderers={customRenderers}\n/>\n\\`\\`\\`\n `,\n },\n },\n },\n tags: ['autodocs'],\n argTypes: {\n apiBaseUrl: {\n control: 'text',\n description: 'API \u57FA\u7840 URL',\n },\n headers: {\n control: 'object',\n description: '\u81EA\u5B9A\u4E49\u8BF7\u6C42\u5934\uFF0C\u5C06\u5728\u6240\u6709 API \u8BF7\u6C42\u4E2D\u6DFB\u52A0',\n table: {\n defaultValue: { summary: 'undefined' },\n },\n },\n recaptchaSitekey: {\n control: 'text',\n description: 'Google reCAPTCHA v3 site key\uFF0C\u63D0\u4F9B\u6B64\u53C2\u6570\u5C06\u81EA\u52A8\u542F\u7528 reCAPTCHA v3 \u9A8C\u8BC1',\n table: {\n defaultValue: { summary: 'undefined' },\n },\n },\n recaptchaAction: {\n control: 'text',\n description: 'reCAPTCHA action \u540D\u79F0\uFF0C\u7528\u4E8E\u533A\u5206\u4E0D\u540C\u7684\u9A8C\u8BC1\u573A\u666F',\n table: {\n defaultValue: { summary: '\"activity\"' },\n },\n },\n site: {\n control: 'text',\n description: 'Shopify \u5E97\u94FA URL',\n },\n channelCode: {\n control: 'text',\n description: '\u6E20\u9053\u7F16\u7801\uFF0C\u7528\u4E8E\u6807\u8BC6\u6765\u6E90\u6E20\u9053',\n table: {\n defaultValue: { summary: 'undefined' },\n },\n },\n welcomeMessage: {\n control: 'text',\n description: '\u6B22\u8FCE\u6D88\u606F',\n table: {\n defaultValue: { summary: '\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F' },\n },\n },\n logoUrl: {\n control: 'text',\n description: 'Logo URL',\n },\n position: {\n control: 'object',\n description: '\u6C14\u6CE1\u6309\u94AE\u4F4D\u7F6E\u5BF9\u8C61',\n table: {\n defaultValue: { summary: '{ bottom: \"1.5rem\", right: \"1.5rem\" }' },\n },\n },\n },\n args: {\n apiBaseUrl: 'http://172.16.38.183:3003',\n site: 'www.eufy.com',\n loginUserId: 'test_test',\n welcomeMessage: '\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F',\n },\n}\n\nexport default meta\ntype Story = StoryObj<typeof LiveChatWidget>\n\n/**\n * \u9ED8\u8BA4\u914D\u7F6E - \u5C55\u793A\u6240\u6709\u529F\u80FD\n */\nexport const Default: Story = {\n args: {\n // \u57FA\u7840\u914D\u7F6E\n loginUserId: 'test_test1',\n apiBaseUrl: 'http://172.16.38.183:3003',\n site: 'beta.eufy.com',\n channelCode: 'dtc',\n title: 'eufy AI Assistant',\n cartId: 'gid://shopify/Cart/hWN7wB3Pa12gh78d8hPOAUBI?key=0e73db1d3fb5ac21da19099c45033253',\n accessToken: '47b1aa2c0797043f9baba39388029d70',\n\n // \u81EA\u5B9A\u4E49\u4F4D\u7F6E\n position: { bottom: '24px', right: '30px' },\n\n // \u6B22\u8FCE\u6D88\u606F\n welcomeMessage: '',\n\n // \u5FEB\u6377\u56DE\u590D\n quickReplies: [\n { id: '1', label: 'Product Info', value: 'Tell me about your products', icon: '\uD83D\uDCE6' },\n { id: '2', label: 'Track Order', value: 'I want to track my order', icon: '\uD83D\uDE9A' },\n { id: '3', label: 'Support', value: 'I need help with my device', icon: '\uD83D\uDD27' },\n { id: '4', label: 'Recommendations', value: 'Recommend products for me', icon: '\u2B50' },\n ],\n\n // \u6CD5\u89C4\u534F\u8BAE\u5F39\u7A97\n complianceConfig: {\n title: \"Hi! I'm your eufy AI assistant.\",\n content:\n 'AI-generated responses can be inaccurate. Please verify important info. Do not input sensitive personal data.',\n checkboxText:\n 'By starting to use \"Live Chat\", you agree to Anker\\'s <a href=\"https://www.anker.com/pages/privacy-policy\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"text-decoration: underline;\">LIVE CHAT PRIVACY NOTICE</a>.',\n agreeButtonText: 'Agree',\n },\n\n // reCAPTCHA \u914D\u7F6E\uFF08\u53D6\u6D88\u6CE8\u91CA\u4EE5\u542F\u7528\uFF09\n // recaptchaSitekey: '6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14',\n recaptchaAction: 'livechat',\n\n // \u663E\u793A\u65B0\u4F1A\u8BDD\u6309\u94AE\n showNewSessionButton: true,\n\n // \u81EA\u5B9A\u4E49\u6587\u6848\n commonText: {\n learnMore: 'Learn More',\n total: 'Total',\n },\n\n // \u81EA\u5B9A\u4E49\u6D88\u606F\u6E32\u67D3\u5668\n customRenderers: {\n video: {\n render: (content: MessageContent) => {\n const videoContent = content as any\n return (\n <div className=\"w-full\">\n <video src={videoContent.url} controls className=\"w-full rounded-lg\" poster={videoContent.poster}>\n Your browser does not support video playback\n </video>\n {videoContent.title && <p className=\"mt-2 text-sm text-gray-600\">{videoContent.title}</p>}\n </div>\n )\n },\n } as MessageRenderer,\n image_gallery: {\n render: (content: MessageContent) => {\n const galleryContent = content as any\n const images = galleryContent.images || []\n return (\n <div className=\"grid grid-cols-2 gap-2\">\n {images.map((image: any, index: number) => (\n <div key={index} className=\"relative aspect-square\">\n <img\n src={image.url}\n alt={image.alt || `Image ${index + 1}`}\n className=\"size-full rounded-lg object-cover\"\n />\n </div>\n ))}\n </div>\n )\n },\n } as MessageRenderer,\n },\n\n // \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\n productCardRender: (product, productHandle) => {\n // product \u53EF\u80FD\u4E3A undefined\uFF0C\u6B64\u65F6\u53EF\u7528 productHandle \u67E5\u8BE2\n if (!product) {\n return (\n <div style={{ padding: '16px', border: '1px dashed #ccc', borderRadius: '8px', textAlign: 'center' }}>\n <p>Product loading... (handle: {productHandle})</p>\n </div>\n )\n }\n\n const imageUrl = product?.featured_image || ''\n const title = product?.title || ''\n const description = product?.description || ''\n const averageRating = product?.average_rating\n\n return (\n <div\n style={{\n border: '2px solid #4CAF50',\n borderRadius: '16px',\n padding: '16px',\n margin: '12px 0',\n backgroundColor: '#f0f9ff',\n boxShadow: '0 4px 12px rgba(76, 175, 80, 0.15)',\n }}\n >\n <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>\n {imageUrl && (\n <img\n src={imageUrl}\n alt={title}\n style={{ width: '60px', height: '60px', borderRadius: '8px', objectFit: 'cover' }}\n />\n )}\n <div style={{ flex: 1 }}>\n <h4 style={{ margin: '0 0 4px 0', fontSize: '16px', fontWeight: 'bold' }}>{title}</h4>\n {description && (\n <p style={{ margin: 0, fontSize: '12px', color: '#666', lineHeight: 1.4 }}>\n {description.slice(0, 80)}...\n </p>\n )}\n {averageRating && (\n <span style={{ fontSize: '12px', color: '#FFB800' }}>Rating: {averageRating.toFixed(1)}</span>\n )}\n </div>\n <button\n style={{\n padding: '8px 16px',\n backgroundColor: '#4CAF50',\n color: 'white',\n borderRadius: '8px',\n border: 'none',\n cursor: 'pointer',\n }}\n onClick={() => console.log('View product:', productHandle, product)}\n >\n View\n </button>\n </div>\n </div>\n )\n },\n },\n\n render: args => (\n <LiveChatWidget\n {...args}\n // \u6240\u6709\u4E8B\u4EF6\u56DE\u8C03\n onOpen={() => console.log('[LiveChat] Chat opened')}\n onClose={() => console.log('[LiveChat] Chat closed')}\n onMessageSend={(message: string) => console.log('[LiveChat] Message sent:', message)}\n onError={(error: Error) => console.error('[LiveChat] Error:', error)}\n onTextMessage={() => console.log('[LiveChat] AI text message received')}\n onProductList={() => console.log('[LiveChat] Product list received')}\n onPromotionList={() => console.log('[LiveChat] Promotion list received')}\n onAddToCart={(product: any) => {\n console.log('[LiveChat] Add to cart:', product)\n alert(`Added \"${product.title}\" to cart!`)\n }}\n onCart={(cartId: string, checkoutUrl?: string) => {\n console.log('[LiveChat] Cart clicked:', { cartId, checkoutUrl })\n alert(`Cart ID: ${cartId}`)\n }}\n />\n ),\n}\n"],
|
|
5
|
-
"mappings": "AAwMY,OACE,OAAAA,EADF,QAAAC,MAAA,oBAlMZ,OAAS,kBAAAC,MAAsB,+BAE/B,MAAO,yBAEP,MAAMC,EAAoC,CACxC,MAAO,0BACP,UAAWD,EACX,WAAY,CACV,OAAQ,aACR,KAAM,CACJ,MAAO,CACL,OAAQ,GACR,aAAc,GAChB,EACA,YAAa,CACX,UAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAmDb,CACF,CACF,EACA,KAAM,CAAC,UAAU,EACjB,SAAU,CACR,WAAY,CACV,QAAS,OACT,YAAa,sBACf,EACA,QAAS,CACP,QAAS,SACT,YAAa,wGACb,MAAO,CACL,aAAc,CAAE,QAAS,WAAY,CACvC,CACF,EACA,iBAAkB,CAChB,QAAS,OACT,YAAa,2HACb,MAAO,CACL,aAAc,CAAE,QAAS,WAAY,CACvC,CACF,EACA,gBAAiB,CACf,QAAS,OACT,YAAa,wGACb,MAAO,CACL,aAAc,CAAE,QAAS,YAAa,CACxC,CACF,EACA,KAAM,CACJ,QAAS,OACT,YAAa,0BACf,EACA,YAAa,CACX,QAAS,OACT,YAAa,iFACb,MAAO,CACL,aAAc,CAAE,QAAS,WAAY,CACvC,CACF,EACA,eAAgB,CACd,QAAS,OACT,YAAa,2BACb,MAAO,CACL,aAAc,CAAE,QAAS,wHAA0B,CACrD,CACF,EACA,QAAS,CACP,QAAS,OACT,YAAa,UACf,EACA,SAAU,CACR,QAAS,SACT,YAAa,mDACb,MAAO,CACL,aAAc,CAAE,QAAS,uCAAwC,CACnE,CACF,CACF,EACA,KAAM,CACJ,WAAY,4BACZ,KAAM,eACN,YAAa,YACb,eAAgB,wHAClB,CACF,EAEA,IAAOE,EAAQD,EAMR,MAAME,EAAiB,CAC5B,KAAM,CAEJ,YAAa,aACb,WAAY,
|
|
4
|
+
"sourcesContent": ["/**\n * LiveChatWidget Storybook Stories\n * \u5C55\u793A LiveChat \u7EC4\u4EF6\u7684\u5404\u79CD\u4F7F\u7528\u573A\u666F\u548C\u914D\u7F6E\n */\n\nimport type { Meta, StoryObj } from '@storybook/react'\nimport { LiveChatWidget } from '../components/LiveChatWidget'\nimport type { MessageRenderer, MessageContent } from '../components/LiveChatWidget'\nimport '../styles/livechat.css'\n\nconst meta: Meta<typeof LiveChatWidget> = {\n title: 'Campaign/LiveChatWidget',\n component: LiveChatWidget,\n parameters: {\n layout: 'fullscreen',\n docs: {\n story: {\n inline: false,\n iframeHeight: 500,\n },\n description: {\n component: `\n# LiveChat \u804A\u5929\u7EC4\u4EF6\n\n\u53EF\u590D\u7528\u7684\u6C14\u6CE1\u5F39\u7A97\u804A\u5929\u7EC4\u4EF6\uFF0C\u652F\u6301 SSE \u6D41\u5F0F\u6D88\u606F\u3001\u81EA\u5B9A\u4E49\u6E32\u67D3\u5668\u548C\u591A\u79CD\u6D88\u606F\u7C7B\u578B\u3002\n\n## \u529F\u80FD\u7279\u6027\n\n- \uD83C\uDF88 **\u6C14\u6CE1\u5F39\u7A97**: \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E\u7684\u60AC\u6D6E\u6C14\u6CE1\u6309\u94AE\n- \uD83D\uDCAC **\u6D41\u5F0F\u6D88\u606F**: \u57FA\u4E8E SSE \u7684\u5B9E\u65F6\u6D41\u5F0F\u54CD\u5E94\n- \uD83D\uDCE6 **\u591A\u79CD\u6D88\u606F\u7C7B\u578B**: \u6587\u672C\u3001\u5546\u54C1\u5361\u7247\u3001\u5546\u54C1\u5217\u8868\u3001\u653F\u7B56\u3001\u5FEB\u6377\u56DE\u590D\u7B49\n- \uD83C\uDFA8 **\u53EF\u5B9A\u5236**: \u652F\u6301\u81EA\u5B9A\u4E49\u54C1\u724C\u989C\u8272\u3001Logo\u3001\u6E32\u67D3\u5668\n- \uD83D\uDCF1 **\u54CD\u5E94\u5F0F**: \u79FB\u52A8\u7AEF\u5168\u5C4F\uFF0C\u684C\u9762\u7AEF\u56FA\u5B9A\u5C3A\u5BF8\n- \uD83D\uDCBE **\u4F1A\u8BDD\u7BA1\u7406**: \u81EA\u52A8\u7BA1\u7406 userId \u548C sessionId\n- \uD83D\uDD12 **\u5B89\u5168\u9632\u62A4**: \u5185\u7F6E XSS \u9632\u62A4\u548C\u8F93\u5165\u9A8C\u8BC1\n\n## \u57FA\u7840\u7528\u6CD5\n\n\\`\\`\\`tsx\nimport { LiveChatWidget } from '@anker-in/campaign-ui'\nimport '@anker-in/campaign-ui/livechat.css'\n\nfunction App() {\n return (\n <LiveChatWidget\n apiBaseUrl=\"https://beta-api-livechat.anker.com\"\n site=\"www.eufy.com\"\n channel_code=\"web_homepage\"\n welcomeMessage=\"\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\"\n />\n )\n}\n\\`\\`\\`\n\n## \u81EA\u5B9A\u4E49\u6E32\u67D3\u5668\n\n\\`\\`\\`tsx\nconst customRenderers = {\n video: {\n render: (content) => (\n <video src={content.url} controls className=\"w-full rounded\" />\n )\n }\n}\n\n<LiveChatWidget\n apiBaseUrl=\"...\"\n site=\"...\"\n customRenderers={customRenderers}\n/>\n\\`\\`\\`\n `,\n },\n },\n },\n tags: ['autodocs'],\n argTypes: {\n apiBaseUrl: {\n control: 'text',\n description: 'API \u57FA\u7840 URL',\n },\n headers: {\n control: 'object',\n description: '\u81EA\u5B9A\u4E49\u8BF7\u6C42\u5934\uFF0C\u5C06\u5728\u6240\u6709 API \u8BF7\u6C42\u4E2D\u6DFB\u52A0',\n table: {\n defaultValue: { summary: 'undefined' },\n },\n },\n recaptchaSitekey: {\n control: 'text',\n description: 'Google reCAPTCHA v3 site key\uFF0C\u63D0\u4F9B\u6B64\u53C2\u6570\u5C06\u81EA\u52A8\u542F\u7528 reCAPTCHA v3 \u9A8C\u8BC1',\n table: {\n defaultValue: { summary: 'undefined' },\n },\n },\n recaptchaAction: {\n control: 'text',\n description: 'reCAPTCHA action \u540D\u79F0\uFF0C\u7528\u4E8E\u533A\u5206\u4E0D\u540C\u7684\u9A8C\u8BC1\u573A\u666F',\n table: {\n defaultValue: { summary: '\"activity\"' },\n },\n },\n site: {\n control: 'text',\n description: 'Shopify \u5E97\u94FA URL',\n },\n channelCode: {\n control: 'text',\n description: '\u6E20\u9053\u7F16\u7801\uFF0C\u7528\u4E8E\u6807\u8BC6\u6765\u6E90\u6E20\u9053',\n table: {\n defaultValue: { summary: 'undefined' },\n },\n },\n welcomeMessage: {\n control: 'text',\n description: '\u6B22\u8FCE\u6D88\u606F',\n table: {\n defaultValue: { summary: '\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F' },\n },\n },\n logoUrl: {\n control: 'text',\n description: 'Logo URL',\n },\n position: {\n control: 'object',\n description: '\u6C14\u6CE1\u6309\u94AE\u4F4D\u7F6E\u5BF9\u8C61',\n table: {\n defaultValue: { summary: '{ bottom: \"1.5rem\", right: \"1.5rem\" }' },\n },\n },\n },\n args: {\n apiBaseUrl: 'http://172.16.38.183:3003',\n site: 'www.eufy.com',\n loginUserId: 'test_test',\n welcomeMessage: '\u4F60\u597D\uFF01\u6211\u662F AI \u52A9\u624B\uFF0C\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u4F60\u7684\u5417\uFF1F',\n },\n}\n\nexport default meta\ntype Story = StoryObj<typeof LiveChatWidget>\n\n/**\n * \u9ED8\u8BA4\u914D\u7F6E - \u5C55\u793A\u6240\u6709\u529F\u80FD\n */\nexport const Default: Story = {\n args: {\n // \u57FA\u7840\u914D\u7F6E\n loginUserId: 'test_test1',\n apiBaseUrl: 'https://beta-api-v2-livechat.anker.com',\n site: 'beta.eufy.com',\n channelCode: 'dtc',\n title: 'eufy AI Assistant',\n cartId: 'gid://shopify/Cart/hWN7wB3Pa12gh78d8hPOAUBI?key=0e73db1d3fb5ac21da19099c45033253',\n accessToken: '47b1aa2c0797043f9baba39388029d70',\n\n // \u81EA\u5B9A\u4E49\u4F4D\u7F6E\n position: { bottom: '24px', right: '30px' },\n\n // \u6B22\u8FCE\u6D88\u606F\n welcomeMessage: '',\n\n // \u5FEB\u6377\u56DE\u590D\n quickReplies: [\n { id: '1', label: 'Product Info', value: 'Tell me about your products', icon: '\uD83D\uDCE6' },\n { id: '2', label: 'Track Order', value: 'I want to track my order', icon: '\uD83D\uDE9A' },\n { id: '3', label: 'Support', value: 'I need help with my device', icon: '\uD83D\uDD27' },\n { id: '4', label: 'Recommendations', value: 'Recommend products for me', icon: '\u2B50' },\n ],\n\n // \u6CD5\u89C4\u534F\u8BAE\u5F39\u7A97\n complianceConfig: {\n title: \"Hi! I'm your eufy AI assistant.\",\n content:\n 'AI-generated responses can be inaccurate. Please verify important info. Do not input sensitive personal data.',\n checkboxText:\n 'By starting to use \"Live Chat\", you agree to Anker\\'s <a href=\"https://www.anker.com/pages/privacy-policy\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"text-decoration: underline;\">LIVE CHAT PRIVACY NOTICE</a>.',\n agreeButtonText: 'Agree',\n },\n\n // reCAPTCHA \u914D\u7F6E\uFF08\u53D6\u6D88\u6CE8\u91CA\u4EE5\u542F\u7528\uFF09\n // recaptchaSitekey: '6LfS4J4pAAAAACX1e_WrxutmxxzCK7FU4WzVqL14',\n recaptchaAction: 'livechat',\n\n // \u663E\u793A\u65B0\u4F1A\u8BDD\u6309\u94AE\n showNewSessionButton: true,\n\n // \u81EA\u5B9A\u4E49\u6587\u6848\n commonText: {\n learnMore: 'Learn More',\n total: 'Total',\n },\n\n // \u81EA\u5B9A\u4E49\u6D88\u606F\u6E32\u67D3\u5668\n customRenderers: {\n video: {\n render: (content: MessageContent) => {\n const videoContent = content as any\n return (\n <div className=\"w-full\">\n <video src={videoContent.url} controls className=\"w-full rounded-lg\" poster={videoContent.poster}>\n Your browser does not support video playback\n </video>\n {videoContent.title && <p className=\"mt-2 text-sm text-gray-600\">{videoContent.title}</p>}\n </div>\n )\n },\n } as MessageRenderer,\n image_gallery: {\n render: (content: MessageContent) => {\n const galleryContent = content as any\n const images = galleryContent.images || []\n return (\n <div className=\"grid grid-cols-2 gap-2\">\n {images.map((image: any, index: number) => (\n <div key={index} className=\"relative aspect-square\">\n <img\n src={image.url}\n alt={image.alt || `Image ${index + 1}`}\n className=\"size-full rounded-lg object-cover\"\n />\n </div>\n ))}\n </div>\n )\n },\n } as MessageRenderer,\n },\n\n // \u81EA\u5B9A\u4E49\u4EA7\u54C1\u5361\u7247\u6E32\u67D3\n productCardRender: (product, productHandle) => {\n // product \u53EF\u80FD\u4E3A undefined\uFF0C\u6B64\u65F6\u53EF\u7528 productHandle \u67E5\u8BE2\n if (!product) {\n return (\n <div style={{ padding: '16px', border: '1px dashed #ccc', borderRadius: '8px', textAlign: 'center' }}>\n <p>Product loading... (handle: {productHandle})</p>\n </div>\n )\n }\n\n const imageUrl = product?.featured_image || ''\n const title = product?.title || ''\n const description = product?.description || ''\n const averageRating = product?.average_rating\n\n return (\n <div\n style={{\n border: '2px solid #4CAF50',\n borderRadius: '16px',\n padding: '16px',\n margin: '12px 0',\n backgroundColor: '#f0f9ff',\n boxShadow: '0 4px 12px rgba(76, 175, 80, 0.15)',\n }}\n >\n <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>\n {imageUrl && (\n <img\n src={imageUrl}\n alt={title}\n style={{ width: '60px', height: '60px', borderRadius: '8px', objectFit: 'cover' }}\n />\n )}\n <div style={{ flex: 1 }}>\n <h4 style={{ margin: '0 0 4px 0', fontSize: '16px', fontWeight: 'bold' }}>{title}</h4>\n {description && (\n <p style={{ margin: 0, fontSize: '12px', color: '#666', lineHeight: 1.4 }}>\n {description.slice(0, 80)}...\n </p>\n )}\n {averageRating && (\n <span style={{ fontSize: '12px', color: '#FFB800' }}>Rating: {averageRating.toFixed(1)}</span>\n )}\n </div>\n <button\n style={{\n padding: '8px 16px',\n backgroundColor: '#4CAF50',\n color: 'white',\n borderRadius: '8px',\n border: 'none',\n cursor: 'pointer',\n }}\n onClick={() => console.log('View product:', productHandle, product)}\n >\n View\n </button>\n </div>\n </div>\n )\n },\n },\n\n render: args => (\n <LiveChatWidget\n {...args}\n // \u6240\u6709\u4E8B\u4EF6\u56DE\u8C03\n onOpen={() => console.log('[LiveChat] Chat opened')}\n onClose={() => console.log('[LiveChat] Chat closed')}\n onMessageSend={(message: string) => console.log('[LiveChat] Message sent:', message)}\n onError={(error: Error) => console.error('[LiveChat] Error:', error)}\n onTextMessage={() => console.log('[LiveChat] AI text message received')}\n onProductList={() => console.log('[LiveChat] Product list received')}\n onPromotionList={() => console.log('[LiveChat] Promotion list received')}\n onAddToCart={(product: any) => {\n console.log('[LiveChat] Add to cart:', product)\n alert(`Added \"${product.title}\" to cart!`)\n }}\n onCart={(cartId: string, checkoutUrl?: string) => {\n console.log('[LiveChat] Cart clicked:', { cartId, checkoutUrl })\n alert(`Cart ID: ${cartId}`)\n }}\n />\n ),\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["jsx", "jsxs", "LiveChatWidget", "meta", "LiveChatWidget_stories_default", "Default", "content", "videoContent", "images", "image", "index", "product", "productHandle", "imageUrl", "title", "description", "averageRating", "args", "message", "error", "cartId", "checkoutUrl"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import { useChatState } from './hooks/useChatState'
|
|
|
23
23
|
import { useChatAPI } from './hooks/useChatAPI'
|
|
24
24
|
import { MessageRendererRegistry } from './utils/messageRenderers'
|
|
25
25
|
import { sanitizeInput } from './utils/validation'
|
|
26
|
+
import { saveUserId } from './utils/userId'
|
|
26
27
|
import { transformProducts } from './utils/productTransformers.js'
|
|
27
28
|
import { transformCartData } from './utils/cartTransformers.js'
|
|
28
29
|
import Cookies from 'js-cookie'
|
|
@@ -126,6 +127,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
126
127
|
showNewSessionButton,
|
|
127
128
|
commonText,
|
|
128
129
|
productCardRender,
|
|
130
|
+
productComparisonRender,
|
|
129
131
|
bottomTips,
|
|
130
132
|
complianceConfig,
|
|
131
133
|
}) => {
|
|
@@ -163,6 +165,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
163
165
|
onAddToCart,
|
|
164
166
|
onCart,
|
|
165
167
|
productCardRender,
|
|
168
|
+
productComparisonRender,
|
|
166
169
|
})
|
|
167
170
|
|
|
168
171
|
const {
|
|
@@ -175,6 +178,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
175
178
|
openChat,
|
|
176
179
|
closeChat,
|
|
177
180
|
setInputValue,
|
|
181
|
+
setUserId,
|
|
178
182
|
addMessage,
|
|
179
183
|
setMessages,
|
|
180
184
|
clearMessages,
|
|
@@ -232,6 +236,9 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
232
236
|
return registry
|
|
233
237
|
}, [customRenderers])
|
|
234
238
|
|
|
239
|
+
// 标记是否已经初始化过会话,防止重复调用
|
|
240
|
+
const sessionInitializedRef = React.useRef(false)
|
|
241
|
+
|
|
235
242
|
/**
|
|
236
243
|
* T043: 打开聊天窗口时初始化会话
|
|
237
244
|
* 使用 API v2.0.0 的统一接口:
|
|
@@ -239,7 +246,18 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
239
246
|
* - 如果有 sessionId,恢复会话并加载历史消息
|
|
240
247
|
*/
|
|
241
248
|
useEffect(() => {
|
|
242
|
-
|
|
249
|
+
// 窗口关闭时重置初始化标记
|
|
250
|
+
if (!isOpen) {
|
|
251
|
+
sessionInitializedRef.current = false
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// userId 为 undefined 表示尚未初始化,等待初始化完成
|
|
256
|
+
if (userId === undefined) return
|
|
257
|
+
|
|
258
|
+
// 如果已经初始化过,不再重复调用
|
|
259
|
+
if (sessionInitializedRef.current) return
|
|
260
|
+
sessionInitializedRef.current = true
|
|
243
261
|
|
|
244
262
|
const currentSessionId = sessionId
|
|
245
263
|
|
|
@@ -309,7 +327,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
309
327
|
},
|
|
310
328
|
})
|
|
311
329
|
} else if (block.type === 'product_comparison' && block.data?.products && block.data?.dimensions) {
|
|
312
|
-
// 转换产品对比数据结构并注入 onAddToCart
|
|
330
|
+
// 转换产品对比数据结构并注入 onAddToCart 回调和自定义渲染函数
|
|
313
331
|
contentBlocks.push({
|
|
314
332
|
type: 'product_comparison',
|
|
315
333
|
data: {
|
|
@@ -317,6 +335,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
317
335
|
dimensions: block.data.dimensions,
|
|
318
336
|
onAddToCart: onAddToCart,
|
|
319
337
|
commonText: mergedText,
|
|
338
|
+
productComparisonRender: productComparisonRender,
|
|
320
339
|
},
|
|
321
340
|
})
|
|
322
341
|
} else if (block.type === 'faq_list' && block.data?.found !== undefined) {
|
|
@@ -372,7 +391,8 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
372
391
|
* 创建新会话
|
|
373
392
|
*/
|
|
374
393
|
const handleCreateNewSession = useCallback(async () => {
|
|
375
|
-
|
|
394
|
+
// userId 为 undefined 表示尚未初始化,等待初始化完成
|
|
395
|
+
if (userId === undefined) return
|
|
376
396
|
|
|
377
397
|
// 如果用户没有配置欢迎语,显示 loading 状态
|
|
378
398
|
if (!welcomeMessage) {
|
|
@@ -381,16 +401,23 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
381
401
|
|
|
382
402
|
try {
|
|
383
403
|
const response = await createSession({
|
|
384
|
-
user_id: userId,
|
|
404
|
+
user_id: userId ?? '',
|
|
385
405
|
site: site,
|
|
386
406
|
channel_code: channelCode,
|
|
387
407
|
real_user_id: loginUserId,
|
|
408
|
+
page_url: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
388
409
|
})
|
|
389
410
|
|
|
390
411
|
if (response.success) {
|
|
391
412
|
// 保存新会话 ID
|
|
392
413
|
saveSession(response.sessionId)
|
|
393
414
|
|
|
415
|
+
// 保存后端返回的 userId(如果有)
|
|
416
|
+
if (response.userId) {
|
|
417
|
+
saveUserId(response.userId)
|
|
418
|
+
setUserId(response.userId)
|
|
419
|
+
}
|
|
420
|
+
|
|
394
421
|
// 清空消息列表
|
|
395
422
|
clearMessages()
|
|
396
423
|
|
|
@@ -488,16 +515,23 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
488
515
|
|
|
489
516
|
try {
|
|
490
517
|
const response = await createSession({
|
|
491
|
-
user_id: userId,
|
|
518
|
+
user_id: userId ?? '',
|
|
492
519
|
session_id: existingSessionId,
|
|
493
520
|
site: site,
|
|
494
521
|
channel_code: channelCode,
|
|
495
522
|
real_user_id: loginUserId,
|
|
523
|
+
page_url: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
496
524
|
})
|
|
497
525
|
|
|
498
526
|
if (response.success && response.resumed) {
|
|
499
527
|
// 会话恢复成功
|
|
500
528
|
|
|
529
|
+
// 保存后端返回的 userId(如果有)
|
|
530
|
+
if (response.userId) {
|
|
531
|
+
saveUserId(response.userId)
|
|
532
|
+
setUserId(response.userId)
|
|
533
|
+
}
|
|
534
|
+
|
|
501
535
|
// 准备欢迎消息(无论是否有历史消息都需要)
|
|
502
536
|
const messageText = response.welcomeMessage || welcomeMessage
|
|
503
537
|
const welcomeContent: MessageContent[] = messageText ? [{ type: 'text', text: messageText }] : []
|
|
@@ -683,10 +717,11 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
683
717
|
if (!currentSessionId) {
|
|
684
718
|
// 没有会话,创建新会话
|
|
685
719
|
const response = await createSession({
|
|
686
|
-
user_id: userId,
|
|
720
|
+
user_id: userId ?? '',
|
|
687
721
|
site: site,
|
|
688
722
|
channel_code: channelCode,
|
|
689
723
|
real_user_id: loginUserId,
|
|
724
|
+
page_url: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
690
725
|
})
|
|
691
726
|
if (response.success) {
|
|
692
727
|
currentSessionId = response.sessionId
|
|
@@ -699,7 +734,7 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
699
734
|
// 构建请求参数(session_id 现在是必填的)
|
|
700
735
|
const requestPayload: ChatStreamRequest = {
|
|
701
736
|
message: sanitized,
|
|
702
|
-
user_id: userId,
|
|
737
|
+
user_id: userId ?? '',
|
|
703
738
|
session_id: currentSessionId,
|
|
704
739
|
context: {
|
|
705
740
|
cartId: cartId,
|
|
@@ -730,10 +765,11 @@ export const LiveChatWidget: React.FC<LiveChatWidgetProps> = ({
|
|
|
730
765
|
|
|
731
766
|
// 创建新会话
|
|
732
767
|
const response = await createSession({
|
|
733
|
-
user_id: userId,
|
|
768
|
+
user_id: userId ?? '',
|
|
734
769
|
site: site,
|
|
735
770
|
channel_code: channelCode,
|
|
736
771
|
real_user_id: loginUserId,
|
|
772
|
+
page_url: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
737
773
|
})
|
|
738
774
|
|
|
739
775
|
if (response.success) {
|
|
@@ -33,6 +33,7 @@ export interface ProductComparisonData {
|
|
|
33
33
|
[key: string]: ComparisonDimension | undefined
|
|
34
34
|
}
|
|
35
35
|
commonText?: CommonText
|
|
36
|
+
productComparisonRender?: (data: ProductComparisonData) => React.ReactNode
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface ProductComparisonProps {
|
|
@@ -323,19 +324,6 @@ export const ProductComparison: React.FC<ProductComparisonProps> = ({ data, onAd
|
|
|
323
324
|
</button>
|
|
324
325
|
)}
|
|
325
326
|
|
|
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
327
|
</div>
|
|
340
328
|
)
|
|
341
329
|
})}
|
|
@@ -364,6 +352,11 @@ export const ProductComparisonRenderer: MessageRenderer = {
|
|
|
364
352
|
|
|
365
353
|
const comparisonData = content.data as ProductComparisonData & { onAddToCart?: (product: Product) => void }
|
|
366
354
|
|
|
355
|
+
// 如果提供了自定义渲染函数,使用自定义渲染
|
|
356
|
+
if (comparisonData.productComparisonRender) {
|
|
357
|
+
return <>{comparisonData.productComparisonRender(comparisonData)}</>
|
|
358
|
+
}
|
|
359
|
+
|
|
367
360
|
return (
|
|
368
361
|
<ProductComparison
|
|
369
362
|
data={comparisonData}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
8
8
|
import type { Message, MessageContent, SSEEvent, StatusData, ErrorData, MessageStartData, BackendCartData, Product, TextContent, ProductCardContent, ProductListContent } from '../types'
|
|
9
|
-
import { getUserId } from '../utils/userId'
|
|
9
|
+
import { getUserId, saveUserId } from '../utils/userId'
|
|
10
10
|
import { useSession } from './useSession'
|
|
11
11
|
import { transformProducts } from '../utils/productTransformers'
|
|
12
12
|
import { transformCartData } from '../utils/cartTransformers'
|
|
@@ -377,6 +377,12 @@ export interface UseChatStateOptions {
|
|
|
377
377
|
* @param productHandle 文本占位符中的产品 ID,可用于应用层查询产品数据
|
|
378
378
|
*/
|
|
379
379
|
productCardRender?: (product: any, productHandle: string) => React.ReactNode
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 自定义产品对比卡片渲染函数
|
|
383
|
+
* @param data 产品对比数据(包含 products 和 dimensions)
|
|
384
|
+
*/
|
|
385
|
+
productComparisonRender?: (data: any) => React.ReactNode
|
|
380
386
|
}
|
|
381
387
|
|
|
382
388
|
export interface UseChatStateReturn {
|
|
@@ -391,9 +397,9 @@ export interface UseChatStateReturn {
|
|
|
391
397
|
isOpen: boolean
|
|
392
398
|
|
|
393
399
|
/**
|
|
394
|
-
* 用户 ID
|
|
400
|
+
* 用户 ID(undefined 表示尚未初始化,空字符串表示由后端生成)
|
|
395
401
|
*/
|
|
396
|
-
userId: string
|
|
402
|
+
userId: string | undefined
|
|
397
403
|
|
|
398
404
|
/**
|
|
399
405
|
* 会话 ID
|
|
@@ -430,6 +436,11 @@ export interface UseChatStateReturn {
|
|
|
430
436
|
*/
|
|
431
437
|
setInputValue: (value: string) => void
|
|
432
438
|
|
|
439
|
+
/**
|
|
440
|
+
* 设置用户 ID(用于保存后端返回的 userId)
|
|
441
|
+
*/
|
|
442
|
+
setUserId: (id: string) => void
|
|
443
|
+
|
|
433
444
|
/**
|
|
434
445
|
* 添加消息到列表
|
|
435
446
|
*/
|
|
@@ -490,13 +501,14 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
|
|
|
490
501
|
onAddToCart,
|
|
491
502
|
onCart,
|
|
492
503
|
productCardRender,
|
|
504
|
+
productComparisonRender,
|
|
493
505
|
} = options
|
|
494
506
|
|
|
495
507
|
// 会话管理
|
|
496
508
|
const { sessionId, saveSession, clearSession } = useSession()
|
|
497
509
|
|
|
498
|
-
// 用户 ID (
|
|
499
|
-
const [userId, setUserId] = useState<string>(
|
|
510
|
+
// 用户 ID (初始化时异步生成,undefined 表示尚未初始化)
|
|
511
|
+
const [userId, setUserId] = useState<string | undefined>(undefined)
|
|
500
512
|
|
|
501
513
|
// 初始化 userId
|
|
502
514
|
useEffect(() => {
|
|
@@ -646,11 +658,16 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
|
|
|
646
658
|
// 重置卡片缓存队列
|
|
647
659
|
pendingCardsRef.current = []
|
|
648
660
|
|
|
649
|
-
// T039: 保存 sessionId(如果后端返回)
|
|
650
|
-
const messageStartData = data as MessageStartData
|
|
661
|
+
// T039: 保存 sessionId 和 userId(如果后端返回)
|
|
662
|
+
const messageStartData = data as MessageStartData & { userId?: string }
|
|
651
663
|
if (messageStartData.sessionId && messageStartData.sessionId !== sessionId) {
|
|
652
664
|
saveSession(messageStartData.sessionId)
|
|
653
665
|
}
|
|
666
|
+
// 保存后端返回的 userId(如果有)
|
|
667
|
+
if (messageStartData.userId) {
|
|
668
|
+
saveUserId(messageStartData.userId)
|
|
669
|
+
setUserId(messageStartData.userId)
|
|
670
|
+
}
|
|
654
671
|
|
|
655
672
|
// 检查最后一条消息是否是 thinking 消息(用户发送消息时已添加)
|
|
656
673
|
setMessagesState(prev => {
|
|
@@ -820,6 +837,7 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
|
|
|
820
837
|
data: {
|
|
821
838
|
products: transformedProducts,
|
|
822
839
|
dimensions: contentData.dimensions || {},
|
|
840
|
+
productComparisonRender: productComparisonRender,
|
|
823
841
|
},
|
|
824
842
|
} as MessageContent
|
|
825
843
|
}
|
|
@@ -1081,6 +1099,7 @@ export function useChatState(options: UseChatStateOptions = {}): UseChatStateRet
|
|
|
1081
1099
|
closeChat,
|
|
1082
1100
|
toggleChat,
|
|
1083
1101
|
setInputValue,
|
|
1102
|
+
setUserId,
|
|
1084
1103
|
addMessage,
|
|
1085
1104
|
setMessages,
|
|
1086
1105
|
clearMessages,
|
|
@@ -44,7 +44,7 @@ export { useSession } from './hooks/useSession'
|
|
|
44
44
|
|
|
45
45
|
// 导出工具类
|
|
46
46
|
export { MessageRendererRegistry } from './utils/messageRenderers'
|
|
47
|
-
export { getUserId } from './utils/userId'
|
|
47
|
+
export { getUserId, saveUserId, clearUserId } from './utils/userId'
|
|
48
48
|
export { sanitizeInput, isValidUrl, isValidUUID, isValidMessageContent, escapeHtml } from './utils/validation'
|
|
49
49
|
|
|
50
50
|
// 导出消息渲染器(供自定义使用)
|
|
@@ -147,6 +147,10 @@ export interface ProductComparisonContent {
|
|
|
147
147
|
}
|
|
148
148
|
onAddToCart?: (product: Product) => void
|
|
149
149
|
commonText?: CommonText
|
|
150
|
+
productComparisonRender?: (data: {
|
|
151
|
+
products: Product[]
|
|
152
|
+
dimensions: ProductComparisonContent['data']['dimensions']
|
|
153
|
+
}) => React.ReactNode
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
|
|
@@ -637,11 +641,13 @@ export interface NewSessionRequest {
|
|
|
637
641
|
site?: string
|
|
638
642
|
channel_code?: string
|
|
639
643
|
real_user_id?: string
|
|
644
|
+
page_url?: string
|
|
640
645
|
}
|
|
641
646
|
|
|
642
647
|
export interface NewSessionResponse {
|
|
643
648
|
success: boolean
|
|
644
649
|
sessionId: string
|
|
650
|
+
userId?: string // 后端生成的 userId(当请求的 user_id 为空时返回)
|
|
645
651
|
message: string
|
|
646
652
|
resumed?: boolean
|
|
647
653
|
messages?: Message[]
|
|
@@ -976,6 +982,15 @@ export interface LiveChatWidgetProps {
|
|
|
976
982
|
*/
|
|
977
983
|
productCardRender?: (product: any, productHandle: string) => React.ReactNode
|
|
978
984
|
|
|
985
|
+
/**
|
|
986
|
+
* 自定义产品对比卡片渲染函数
|
|
987
|
+
* 用于自定义渲染 product_comparison 类型的产品对比卡片
|
|
988
|
+
* 当提供此函数时,将替代默认的产品对比卡片渲染逻辑
|
|
989
|
+
* @param data 产品对比数据(包含 products 和 dimensions)
|
|
990
|
+
* @returns React 可渲染的内容
|
|
991
|
+
*/
|
|
992
|
+
productComparisonRender?: (data: ProductComparisonContent['data']) => React.ReactNode
|
|
993
|
+
|
|
979
994
|
/**
|
|
980
995
|
* 输入框底部提示文本
|
|
981
996
|
* 不传入则不显示
|