import * as React from "react" import { ChatWorkspace, type ChatConversation, type ChatMessage, } from "@workspace/blocks/chats" const mockMediaUrls: Record = { "campaign-banner": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?auto=format&fit=crop&w=960&q=80", "order-card-thumbnail": "https://images.unsplash.com/photo-1556740749-887f6717d7e4?auto=format&fit=crop&w=320&q=80", } const initialConversations: readonly ChatConversation[] = [ { avatarUrl: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-1.webp", description: "微信小程序客服会话", id: "customer-service", lastActivityLabel: "刚刚", lastMessage: "我的订单", messages: [ { author: { id: "customer-zhang", name: "张晓敏" }, createdAt: "2026-07-31T09:30:00+08:00", direction: "incoming", id: "welcome", msgtype: "text", text: { content: "你好,我想了解一下订单的配送进度。" }, }, { createdAt: "2026-07-31T09:31:00+08:00", direction: "outgoing", id: "campaign-image", image: { media_id: "campaign-banner" }, msgtype: "image", }, { createdAt: "2026-07-31T09:32:00+08:00", direction: "outgoing", id: "help-link", link: { description: "查看商品、订单与售后服务说明。", thumb_url: "https://images.unsplash.com/photo-1556740758-90de374c12ad?auto=format&fit=crop&w=640&q=80", title: "帮助中心", url: "https://example.com/help", }, msgtype: "link", }, { createdAt: "2026-07-31T09:33:00+08:00", direction: "outgoing", id: "orders-card", miniprogrampage: { pagepath: "pages/orders/index", thumb_media_id: "order-card-thumbnail", title: "我的订单", }, msgtype: "miniprogrampage", }, ], title: "张晓敏", unreadCount: 3, }, { avatarUrl: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-4.webp", description: "微信小程序客服会话", id: "shipping", lastActivityLabel: "昨天", lastMessage: "请问我的包裹什么时候可以送到?", messages: [ { author: { id: "customer-li", name: "李雨晴" }, createdAt: "2026-07-30T16:20:00+08:00", direction: "incoming", id: "shipping-status", msgtype: "text", text: { content: "请问我的包裹什么时候可以送到?" }, }, ], title: "李雨晴", }, ] export function ChatsDemoPage() { const [activeConversationId, setActiveConversationId] = React.useState( initialConversations[0]!.id ) const [conversations, setConversations] = React.useState(initialConversations) const [hasMoreHistory, setHasMoreHistory] = React.useState(true) const [mediaUrls, setMediaUrls] = React.useState(mockMediaUrls) const objectUrlsRef = React.useRef(new Set()) React.useEffect( () => () => { for (const url of objectUrlsRef.current) URL.revokeObjectURL(url) }, [] ) const appendMessages = React.useCallback( (conversationId: string, messages: readonly ChatMessage[]) => { if (messages.length === 0) return const lastMessage = messages.at(-1)! setConversations((current) => current.map((conversation) => conversation.id === conversationId ? { ...conversation, lastActivityLabel: "刚刚", lastMessage: getMessagePreview(lastMessage), messages: [...conversation.messages, ...messages], unreadCount: 0, } : conversation ) ) }, [] ) const handleSend = React.useCallback( async ({ conversation, files, text, }: Parameters< NonNullable["onSend"]> >[0]) => { const createdAt = new Date().toISOString() const prefix = `${Date.now()}` const messages: ChatMessage[] = [] if (text) { messages.push({ createdAt, direction: "outgoing", id: `${prefix}:text`, msgtype: "text", status: "sent", text: { content: text }, }) } const newMediaUrls: Record = {} for (const [index, file] of files.entries()) { const url = URL.createObjectURL(file) objectUrlsRef.current.add(url) if (file.type.startsWith("image/")) { const mediaId = `${prefix}:image:${index}` newMediaUrls[mediaId] = url messages.push({ createdAt, direction: "outgoing", id: mediaId, image: { media_id: mediaId }, msgtype: "image", status: "sent", }) continue } messages.push({ createdAt, direction: "outgoing", id: `${prefix}:link:${index}`, link: { description: "本地选择的文件(仅用于模拟)。", thumb_url: "", title: file.name, url, }, msgtype: "link", status: "sent", }) } if (Object.keys(newMediaUrls).length > 0) { setMediaUrls((current) => ({ ...current, ...newMediaUrls })) } appendMessages(conversation.id, messages) }, [appendMessages] ) const handleLoadEarlierMessages = React.useCallback( (conversation: ChatConversation) => { setHasMoreHistory(false) setConversations((current) => current.map((currentConversation) => currentConversation.id === conversation.id ? { ...currentConversation, messages: [ { author: { id: "customer-zhang", name: "张晓敏" }, createdAt: "2026-07-31T09:20:00+08:00", direction: "incoming", id: `${conversation.id}:earlier-message`, msgtype: "text", text: { content: "我昨天已经联系过客服了。" }, }, ...currentConversation.messages, ], } : currentConversation ) ) }, [] ) return (

聊天模拟器

所有数据仅保存在浏览器内,可模拟微信小程序客服的文本、图片、图文链接和小程序卡片消息。

setActiveConversationId(conversation.id) } onLoadEarlierMessages={handleLoadEarlierMessages} onSend={handleSend} resolveMediaUrl={(mediaId) => mediaUrls[mediaId]} />
) } function getMessagePreview(message: ChatMessage) { switch (message.msgtype) { case "text": return message.text.content case "image": return "[图片]" case "link": return message.link.title case "miniprogrampage": return message.miniprogrampage.title } }