- 新增地址类型定义,增强前端地址数据结构 - 新增地址编辑页面,支持地址智能识别和定位选点功能 - 地址编辑支持省市区选择及默认地址设置 - 新增地址列表页面,支持地址展示、删除、编辑和选择功能 - 实现售后申请页面,支持选择售后类型和退款原因 - 售后申请支持商品选择、退款金额计算和凭证上传 - 新增售后详情页面,支持售后状态展示及申请取消 - 优化页面加载和用户交互体验,增加错误提示和权限处理
407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
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 = 'paopao_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/index' })
|
||
}
|
||
|
||
// 选择会话
|
||
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
|