Files
simple-react-app-kit/apps/web/src/pages/chats-demo-page.tsx
T

257 lines
7.6 KiB
TypeScript
Raw Normal View History

import * as React from "react"
import {
ChatWorkspace,
type ChatConversation,
type ChatMessage,
} from "@workspace/blocks/chats"
const mockMediaUrls: Record<string, string> = {
"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<string>())
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<React.ComponentProps<typeof ChatWorkspace>["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<string, string> = {}
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 (
<div className="flex min-w-0 flex-col gap-6">
<div>
<h1 className="text-xl font-medium">聊天模拟器</h1>
<p className="mt-1 text-sm text-muted-foreground">
所有数据仅保存在浏览器内,可模拟微信小程序客服的文本、图片、图文链接和小程序卡片消息。
</p>
</div>
<ChatWorkspace
activeConversationId={activeConversationId}
conversations={conversations}
hasMoreMessages={
hasMoreHistory && activeConversationId === "customer-service"
}
maxHeight="calc(100svh - 15rem)"
minHeight="min(36rem, calc(100svh - 15rem))"
onActiveConversationChange={(conversation) =>
setActiveConversationId(conversation.id)
}
onLoadEarlierMessages={handleLoadEarlierMessages}
onSend={handleSend}
resolveMediaUrl={(mediaId) => mediaUrls[mediaId]}
/>
</div>
)
}
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
}
}