Files
xinlong-shop-taro/src_bak/pages/user/customer-service/index.tsx
赵忠林 1fa58040f3 feat(user): 新增收货地址管理及售后申请页面
- 新增地址类型定义,增强前端地址数据结构
- 新增地址编辑页面,支持地址智能识别和定位选点功能
- 地址编辑支持省市区选择及默认地址设置
- 新增地址列表页面,支持地址展示、删除、编辑和选择功能
- 实现售后申请页面,支持选择售后类型和退款原因
- 售后申请支持商品选择、退款金额计算和凭证上传
- 新增售后详情页面,支持售后状态展示及申请取消
- 优化页面加载和用户交互体验,增加错误提示和权限处理
2026-07-01 12:11:56 +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 = '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