Files
xinlong-shop-taro/src/pages/customer-service.tsx
赵忠林 f3886664f7 fix(api): 替换所有接口请求的域名为 guilixu-api.websoft.top
- 将获取 STS Token 的接口地址由 paopao-api 改为 guilixu-api
- 更新图片上传接口地址为新的 guilixu-api 域名
- 修改用户推广页面中邀请码链接和二维码接口的域名
- 更改注册页微信登录接口请求的域名为 guilixu-api
2026-06-16 17:15:59 +08:00

407 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useCallback, useRef } from 'react'
import { View, Text, Button, Input, ScrollView } from '@tarojs/components'
import Taro, { useReachBottom } from '@tarojs/taro'
import NavBar from '@/components/NavBar'
import LoadMore from '@/components/common/LoadMore'
import EmptyState from '@/components/common/EmptyState'
import { pageShopChatConversation, addShopChatConversation } from '@/api/shop/shopChatConversation'
import { pageShopChatMessage, addShopChatMessage } from '@/api/shop/shopChatMessage'
import type { ShopChatConversation, ShopChatConversationParam } from '@/api/shop/shopChatConversation/model'
import type { ShopChatMessage } from '@/api/shop/shopChatMessage/model'
import type { PageResult } from '@/api'
definePageConfig({
navigationBarTitleText: '在线客服',
})
const SERVICE_ONLINE_HOURS = '9:00-21:00'
const SERVICE_HOTLINE = '400-888-8888'
const SERVICE_WECHAT = 'shop_service'
const CustomerServicePage: React.FC = () => {
const [conversations, setConversations] = useState<ShopChatConversation[]>([])
const [currentConversation, setCurrentConversation] = useState<ShopChatConversation | null>(null)
const [messages, setMessages] = useState<ShopChatMessage[]>([])
const [inputMessage, setInputMessage] = useState('')
const [loading, setLoading] = useState(true)
const [sending, setSending] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [msgPage, setMsgPage] = useState(1)
const [showHistory, setShowHistory] = useState(false)
const scrollToBottom = useRef(false)
const isOnline = () => {
const now = new Date()
const h = now.getHours()
return h >= 9 && h < 21
}
// 加载会话列表
const loadConversations = useCallback(async () => {
setLoading(true)
try {
const params: ShopChatConversationParam = {
page: 1,
limit: 20,
order: 'desc',
sort: 'updateTime',
}
const result: PageResult<ShopChatConversation> = await pageShopChatConversation(params)
if (result?.list) {
setConversations(result.list)
if (result.list.length > 0) {
const latest = result.list[0]
setCurrentConversation(latest)
loadMessages(latest.id!, 1, false)
}
}
} catch (e) {
console.error('获取会话列表失败:', e)
setConversations([])
} finally {
setLoading(false)
}
}, [])
// 加载消息列表
const loadMessages = useCallback(async (conversationId: number, pageNum: number = 1, isLoadMore = false) => {
if (isLoadMore) setLoadingMore(true)
try {
const result = await pageShopChatMessage({
conversationId,
page: pageNum,
limit: 20,
order: 'asc',
sort: 'createTime',
} as any)
if (result?.list) {
if (isLoadMore) {
setMessages(prev => [...result.list!, ...prev])
} else {
setMessages(result.list)
scrollToBottom.current = true
}
setHasMore((result.list.length ?? 0) >= 20)
setMsgPage(pageNum)
}
} catch (e) {
console.error('获取消息列表失败:', e)
if (!isLoadMore) setMessages([])
} finally {
setLoadingMore(false)
}
}, [])
useEffect(() => {
loadConversations()
}, [loadConversations])
useReachBottom(() => {
if (hasMore && currentConversation && !loadingMore) {
loadMessages(currentConversation.id!, msgPage + 1, true)
}
})
// 创建新会话
const ensureConversation = async (): Promise<ShopChatConversation | null> => {
if (currentConversation) return currentConversation
try {
await addShopChatConversation({ type: 0 })
// 重新拉取会话列表获取新会话
const result = await pageShopChatConversation({ page: 1, limit: 20, order: 'desc', sort: 'updateTime' })
if (result?.list && result.list.length > 0) {
const conv = result.list[0]
setConversations(result.list)
setCurrentConversation(conv)
return conv
}
} catch (e) {
console.error('创建会话失败:', e)
}
return null
}
// 发送消息
const handleSendMessage = async () => {
const text = inputMessage.trim()
if (!text) {
Taro.showToast({ title: '请输入消息内容', icon: 'none' })
return
}
setSending(true)
setInputMessage('')
try {
const conv = await ensureConversation()
if (!conv?.id) {
Taro.showToast({ title: '无法创建会话,请稍后重试', icon: 'none' })
setInputMessage(text)
return
}
await addShopChatMessage({
content: text,
type: 'text',
toUserId: 0, // 发送给客服(服务端路由)
} as any)
// 消息发送成功后刷新消息列表
await loadMessages(conv.id, 1, false)
// 同步更新会话列表 lastMessage
setConversations(prev => prev.map(c =>
c.id === conv.id ? { ...c, lastMessage: text, updateTime: new Date().toISOString() } : c
))
} catch (e: any) {
console.error('发送消息失败:', e)
setInputMessage(text)
Taro.showToast({ title: e?.message || '发送失败,请重试', icon: 'none' })
} finally {
setSending(false)
}
}
// 拨打热线
const handleCallHotline = () => {
Taro.makePhoneCall({ phoneNumber: SERVICE_HOTLINE })
}
// 复制微信号
const handleCopyWechat = () => {
Taro.setClipboardData({
data: SERVICE_WECHAT,
success: () => Taro.showToast({ title: '已复制微信号', icon: 'success' }),
})
}
// 跳转帮助中心
const handleGoToHelp = () => {
Taro.navigateTo({ url: '/pages/user/help-center' })
}
// 选择会话
const handleSelectConversation = (conv: ShopChatConversation) => {
setCurrentConversation(conv)
setMessages([])
setMsgPage(1)
setHasMore(true)
loadMessages(conv.id!, 1, false)
}
const formatTime = (timeStr?: string) => {
if (!timeStr) return ''
const date = new Date(timeStr)
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
return `${m}-${d} ${h}:${min}`
}
const online = isOnline()
return (
<View className="flex flex-col min-h-screen bg-gray-100">
<NavBar title="在线客服" />
<ScrollView scrollY className="flex-1">
{/* 客服状态栏 */}
<View className="bg-white px-4 py-3 flex items-center justify-between">
<View className="flex items-center gap-2">
<View
className="w-2 h-2 rounded-full"
style={{ backgroundColor: online ? '#52c41a' : '#d1d5db' }}
/>
<Text className="text-sm text-gray-700">{online ? '客服在线' : '客服离线'}</Text>
</View>
<Text className="text-xs text-gray-400">: {SERVICE_ONLINE_HOURS}</Text>
</View>
{/* 微信在线客服按钮 */}
<View className="px-4 py-3 bg-white border-t border-gray-50">
<Button
openType="contact"
className="w-full h-12 rounded-lg text-white font-medium text-base border-0"
style={{ backgroundColor: '#07c160', lineHeight: '48px' }}
>
线
</Button>
</View>
{/* 消息记录入口 */}
{!showHistory && (
<View className="bg-white mx-4 mt-4 p-4 rounded-lg">
<View
className="flex items-center justify-between"
onClick={() => setShowHistory(true)}
>
<View className="flex items-center gap-2">
<Text className="text-lg">💬</Text>
<Text className="text-sm text-gray-700">
{conversations.length > 0 ? `${conversations.length}条会话)` : ''}
</Text>
</View>
<Text className="text-gray-400 text-sm"></Text>
</View>
</View>
)}
{/* 历史会话与消息列表 */}
{showHistory && (
<View className="mx-4 mt-4">
<View className="flex items-center justify-between mb-2">
<Text className="text-sm font-medium text-gray-700"></Text>
<Text
className="text-sm"
style={{ color: '#3b82f6' }}
onClick={() => setShowHistory(false)}
>
</Text>
</View>
{loading ? (
<LoadMore loading />
) : conversations.length === 0 ? (
<EmptyState message="暂无历史会话" small />
) : (
<View>
{conversations.map(conv => (
<View
key={conv.id}
className="bg-white p-3 rounded-lg mb-2"
style={{ border: currentConversation?.id === conv.id ? '2px solid #10b981' : 'none' }}
onClick={() => handleSelectConversation(conv)}
>
<View className="flex justify-between items-center">
<Text className="text-sm text-gray-700" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{conv.content || conv.lastMessage || '暂无消息'}
</Text>
<Text className="text-xs text-gray-400 ml-2">{formatTime(conv.updateTime)}</Text>
</View>
</View>
))}
</View>
)}
{/* 当前会话的消息 */}
{currentConversation && messages.length > 0 && (
<View className="bg-white rounded-lg p-3 mb-4">
<Text className="text-xs text-gray-400 mb-3 block"></Text>
{loadingMore && <LoadMore loading />}
{messages.map(msg => (
<View
key={msg.id}
className={`flex mb-3 ${msg.formUserId === currentConversation.userId ? 'justify-end' : 'justify-start'}`}
>
<View
className="px-3 py-2 rounded-lg max-w-xs"
style={{
backgroundColor: msg.formUserId === currentConversation.userId ? '#10b981' : '#f3f4f6',
maxWidth: '70%',
}}
>
<Text
className="text-sm"
style={{ color: msg.formUserId === currentConversation.userId ? '#fff' : '#374151' }}
>
{msg.content}
</Text>
<Text
className="text-xs block mt-1"
style={{ color: msg.formUserId === currentConversation.userId ? 'rgba(255,255,255,0.7)' : '#9ca3af' }}
>
{formatTime(msg.createTime)}
</Text>
</View>
</View>
))}
</View>
)}
</View>
)}
{/* 其他联系方式 */}
<View className="bg-white mx-4 mt-4 p-4 rounded-lg">
<Text className="text-base font-medium text-gray-800 mb-3 block"></Text>
<View
className="flex items-center justify-between py-3 border-b border-gray-50"
onClick={handleCallHotline}
>
<View className="flex items-center gap-2">
<Text className="text-lg">📞</Text>
<View>
<Text className="text-sm text-gray-700 block">线</Text>
<Text className="text-xs text-gray-400">{SERVICE_HOTLINE}</Text>
</View>
</View>
<Text className="text-xs" style={{ color: '#3b82f6' }}></Text>
</View>
<View
className="flex items-center justify-between py-3"
onClick={handleCopyWechat}
>
<View className="flex items-center gap-2">
<Text className="text-lg">💬</Text>
<View>
<Text className="text-sm text-gray-700 block"></Text>
<Text className="text-xs text-gray-400">{SERVICE_WECHAT}</Text>
</View>
</View>
<Text className="text-xs" style={{ color: '#3b82f6' }}></Text>
</View>
</View>
{/* 帮助中心入口 */}
<View className="bg-white mx-4 mt-4 p-4 rounded-lg mb-4">
<Text className="text-base font-medium text-gray-800 mb-3 block"></Text>
<View
className="flex items-center justify-between py-2"
onClick={handleGoToHelp}
>
<Text className="text-sm text-gray-700"></Text>
<Text className="text-gray-400 text-sm"></Text>
</View>
</View>
{/* 留言(离线时) */}
{!online && (
<View className="bg-white mx-4 mb-4 p-4 rounded-lg">
<Text className="text-base font-medium text-gray-800 mb-3 block">线</Text>
<Text className="text-sm text-gray-400 block">
线线
</Text>
</View>
)}
</ScrollView>
{/* 消息输入区 - 始终显示,支持发送留言 */}
<View className="bg-white border-t border-gray-200 px-4 py-3 flex items-center gap-2">
<Input
className="flex-1 border border-gray-200 rounded-full text-sm"
style={{ paddingLeft: '16px', paddingRight: '16px', height: '40px', lineHeight: '40px' }}
placeholder={online ? '输入消息...' : '留言给客服...'}
value={inputMessage}
onInput={(e: any) => setInputMessage(e.detail.value)}
onConfirm={handleSendMessage}
disabled={sending}
/>
<View
className="rounded-full flex items-center justify-center"
style={{
width: '72px',
height: '40px',
backgroundColor: sending ? '#d1d5db' : '#10b981',
}}
onClick={sending ? undefined : handleSendMessage}
>
<Text className="text-sm text-white">{sending ? '发送中' : '发送'}</Text>
</View>
</View>
</View>
)
}
export default CustomerServicePage