@@ -1,406 +0,0 @@
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