完善功能
This commit is contained in:
@@ -1,158 +1,284 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import {useState, useEffect, useRef} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {
|
||||
Loading,
|
||||
InfiniteLoading,
|
||||
Empty,
|
||||
Space,
|
||||
Input,
|
||||
Avatar,
|
||||
Tag,
|
||||
Divider,
|
||||
Button
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import { Voice, FaceMild, AddCircle } from '@nutui/icons-react-taro'
|
||||
import {getClinicDoctorUser} from "@/api/clinic/clinicDoctorUser";
|
||||
import {ClinicDoctorUser} from "@/api/clinic/clinicDoctorUser/model";
|
||||
import {ClinicPatientUser} from "@/api/clinic/clinicPatientUser/model";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageShopChatMessage} from "@/api/shop/shopChatMessage";
|
||||
import Taro, {useDidShow, useRouter, useLoad} from '@tarojs/taro'
|
||||
import {Input, Button} from '@nutui/nutui-react-taro'
|
||||
import {Voice, FaceMild} from '@nutui/icons-react-taro'
|
||||
import {getClinicDoctorUserByUserId} from "@/api/clinic/clinicDoctorUser";
|
||||
import {addShopChatMessage, listShopChatMessage} from "@/api/shop/shopChatMessage";
|
||||
import {ShopChatMessage} from "@/api/shop/shopChatMessage/model";
|
||||
import Line from "@/components/Gap";
|
||||
import {addShopChatConversation, chatConversationByBothUserId} from "@/api/shop/shopChatConversation";
|
||||
// @ts-ignore
|
||||
import {WS_URL} from "@/config/env";
|
||||
import {clinicPatientUserByPatientUserId} from "@/api/clinic/clinicPatientUser";
|
||||
|
||||
const CustomerIndex = () => {
|
||||
const {params} = useRouter();
|
||||
const [doctor, setDoctor] = useState<ClinicDoctorUser>()
|
||||
const [list, setList] = useState<ShopChatMessage[]>([])
|
||||
|
||||
const {params} = useRouter()
|
||||
const [messages, setMessages] = useState<ShopChatMessage[]>([])
|
||||
const [conversationId, setConversationId] = useState<number | null>(null)
|
||||
const [friendUserId, setFriendUserId] = useState<string>('')
|
||||
const [messageInput, setMessageInput] = useState<string>('')
|
||||
const [sending, setSending] = useState<boolean>(false)
|
||||
const [isDoctor, setIsDoctor] = useState<boolean>(false)
|
||||
const [doctors, setDoctors] = useState<ClinicDoctorUser[]>([])
|
||||
const [patientUsers, setPatientUsers] = useState<ClinicPatientUser[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const socketRef = useRef<Taro.SocketTask | null>(null)
|
||||
|
||||
// 获取列表数据
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
if (Taro.getStorageSync('Doctor')) {
|
||||
setIsDoctor(true)
|
||||
}
|
||||
const doctorUser = await getClinicDoctorUser(Number(params.id))
|
||||
if (doctorUser) {
|
||||
setDoctor(doctorUser)
|
||||
Taro.setNavigationBarTitle({title: `${doctorUser.realName}`});
|
||||
}
|
||||
const messages = await pageShopChatMessage({})
|
||||
const quickActions = [
|
||||
{label: '开方', type: 'prescription'},
|
||||
{label: '快捷回复', type: 'quickReply'},
|
||||
{label: '发问诊单', type: 'sendInquiry'}
|
||||
]
|
||||
|
||||
const handleQuickAction = (actionType: string) => {
|
||||
switch (actionType) {
|
||||
case 'prescription':
|
||||
if (friendUserId) {
|
||||
Taro.navigateTo({url: `/doctor/orders/add?id=${friendUserId}`})
|
||||
} else {
|
||||
Taro.showToast({title: '请选择患者', icon: 'none'})
|
||||
}
|
||||
break
|
||||
case 'quickReply':
|
||||
Taro.showToast({title: '快捷回复敬请期待', icon: 'none'})
|
||||
break
|
||||
case 'sendInquiry':
|
||||
Taro.showToast({title: '问诊单功能敬请期待', icon: 'none'})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const fetchData = async (userId?: string) => {
|
||||
if (!userId) return
|
||||
try {
|
||||
console.log("Taro.getStorageSync('Doctor')", Taro.getStorageSync('Doctor'))
|
||||
if (Taro.getStorageSync('Doctor')) {
|
||||
setIsDoctor(true)
|
||||
}
|
||||
const isDoctorData = Taro.getStorageSync('Doctor')
|
||||
if (!isDoctorData) {
|
||||
const doctorUser = await getClinicDoctorUserByUserId(Number(params.userId))
|
||||
if (doctorUser?.realName) {
|
||||
await Taro.setNavigationBarTitle({title: `${doctorUser.realName}`})
|
||||
}
|
||||
} else {
|
||||
const patient = await clinicPatientUserByPatientUserId(Number(params.userId))
|
||||
if (patient?.realName) {
|
||||
await Taro.setNavigationBarTitle({title: `${patient.realName}`})
|
||||
}
|
||||
}
|
||||
|
||||
let conversation = await chatConversationByBothUserId(userId)
|
||||
if (!conversation) {
|
||||
conversation = await addShopChatConversation({
|
||||
friendId: parseInt(userId, 10),
|
||||
content: ''
|
||||
})
|
||||
}
|
||||
|
||||
if (conversation?.id) {
|
||||
setConversationId(conversation.id)
|
||||
const messageList = await listShopChatMessage({conversationId: conversation.id})
|
||||
setMessages(messageList || [])
|
||||
} else {
|
||||
setMessages([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载聊天数据失败:', error)
|
||||
Taro.showToast({title: '聊天加载失败', icon: 'none'})
|
||||
}
|
||||
}
|
||||
|
||||
const connectWebSocket = async (id: number) => {
|
||||
const base = (WS_URL || '').replace(/\/$/, '')
|
||||
if (!base) {
|
||||
console.warn('WS_URL 未配置')
|
||||
return
|
||||
}
|
||||
|
||||
if (socketRef.current) {
|
||||
socketRef.current.close({})
|
||||
}
|
||||
|
||||
const userId = Taro.getStorageSync('UserId')
|
||||
const result = Taro.connectSocket({
|
||||
url: `${base}/${id}_${userId}`
|
||||
})
|
||||
|
||||
const socketTask = typeof (result as Promise<any>)?.then === 'function'
|
||||
? await (result as Promise<Taro.SocketTask>)
|
||||
: (result as Taro.SocketTask)
|
||||
|
||||
if (!socketTask) {
|
||||
return
|
||||
}
|
||||
|
||||
socketRef.current = socketTask
|
||||
|
||||
socketTask.onOpen(() => {
|
||||
console.log('WebSocket连接成功')
|
||||
})
|
||||
|
||||
socketTask.onMessage((event) => {
|
||||
console.log('收到消息:', event,)
|
||||
try {
|
||||
if (event.data === '连接成功' || event.data === 'pong') return
|
||||
const payload = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
|
||||
if (!payload) return
|
||||
if (Array.isArray(payload)) {
|
||||
setMessages(prev => [...prev, ...payload])
|
||||
} else {
|
||||
payload.isMine = parseInt(payload.fromUserId) !== parseInt(params?.userId)
|
||||
setMessages(prev => [...prev, payload])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('解析消息失败:', err)
|
||||
}
|
||||
})
|
||||
|
||||
socketTask.onError((err) => {
|
||||
console.error('WebSocket异常:', err)
|
||||
})
|
||||
|
||||
socketTask.onClose(() => {
|
||||
socketRef.current = null
|
||||
})
|
||||
}
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const content = messageInput.trim()
|
||||
if (!content) {
|
||||
Taro.showToast({title: '请输入内容', icon: 'none'})
|
||||
return
|
||||
}
|
||||
if (!conversationId || !friendUserId) {
|
||||
Taro.showToast({title: '会话未初始化', icon: 'none'})
|
||||
return
|
||||
}
|
||||
if (sending) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setSending(true)
|
||||
await addShopChatMessage({
|
||||
content,
|
||||
conversationId,
|
||||
toUserId: parseInt(friendUserId, 10),
|
||||
type: 'text'
|
||||
})
|
||||
|
||||
// const localMessage: ShopChatMessage = {
|
||||
// id: Date.now(),
|
||||
// content,
|
||||
// conversationId,
|
||||
// toUserId: parseInt(friendUserId, 10),
|
||||
// type: 'text',
|
||||
// isMine: true
|
||||
// }
|
||||
//
|
||||
// setMessages(prev => [...prev, localMessage])
|
||||
setMessageInput('')
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
Taro.showToast({title: '发送失败,请重试', icon: 'none'})
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
useLoad((options) => {
|
||||
if (options?.userId) {
|
||||
const userId = String(options.userId)
|
||||
setFriendUserId(userId)
|
||||
fetchData(userId)
|
||||
}
|
||||
console.log('Taro.getStorageSync(\'UserId\')', Taro.getStorageSync('UserId'))
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().then()
|
||||
}, []);
|
||||
|
||||
// 监听页面显示,当从其他页面返回时刷新数据
|
||||
useDidShow(() => {
|
||||
// 刷新当前tab的数据和统计信息
|
||||
fetchData().then();
|
||||
});
|
||||
|
||||
// 渲染医师项
|
||||
const renderDoctorItem = (item: ClinicDoctorUser) => (
|
||||
<View key={item.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center">
|
||||
<View className="flex-1 flex justify-between items-center">
|
||||
<View className="flex justify-between">
|
||||
<Avatar src={item.avatar} size={'large'}/>
|
||||
<View className={'flex flex-col mx-3'}>
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{item.realName} 医师
|
||||
</Text>
|
||||
<View>
|
||||
<Tag background="#f3f3f3" color="#999999">中医内科</Tag>
|
||||
</View>
|
||||
<View className={'my-1'}>
|
||||
<Text className={'text-gray-400 text-xs'}>问诊 1 次</Text>
|
||||
<Divider direction="vertical"/>
|
||||
<Text className={'text-gray-400 text-xs'}>开方 3 次</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Button type="warning">咨询医生</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 渲染患者项
|
||||
const renderPatientUserItem = (item: ClinicPatientUser) => (
|
||||
<View key={item.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
<View className="flex items-center">
|
||||
<View className="flex-1 flex justify-between items-center">
|
||||
<View className="flex justify-between">
|
||||
<Avatar src={item.avatar} size={'large'}/>
|
||||
<View className={'flex flex-col mx-3'}>
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
{item.realName}
|
||||
</Text>
|
||||
<View>
|
||||
{
|
||||
<Text
|
||||
className={'text-gray-400 text-xs'}>{item.sex}</Text>
|
||||
}
|
||||
{
|
||||
item.age && (
|
||||
<>
|
||||
<Divider direction="vertical"/>
|
||||
<Text className={'text-gray-400 text-xs'}>{item.age}岁</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
item.weight && (
|
||||
<>
|
||||
<Divider direction="vertical"/>
|
||||
<Text className={'text-gray-400 text-xs'}>{item.weight}</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
<View>
|
||||
<Text className={'text-gray-400 text-xs'}>{item.allergyHistory}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Button type="warning" onClick={() => navTo(`/doctor/orders/add?id=${item.userId}`)}>开方</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (conversationId) {
|
||||
connectWebSocket(conversationId).catch(err => {
|
||||
console.error('WebSocket连接失败:', err)
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
socketRef.current?.close()
|
||||
socketRef.current = null
|
||||
}
|
||||
}, [conversationId])
|
||||
|
||||
return (
|
||||
<View className="min-h-screen bg-gray-50 w-full">
|
||||
<View className={'p-4'}>
|
||||
{list?.map(renderPatientUserItem)}
|
||||
<View className="min-h-screen bg-gray-50 w-full pb-24">
|
||||
<View className="px-4 pt-4 pb-24">
|
||||
{messages.length === 0 ? (
|
||||
<View className="mt-10 text-center text-gray-400 text-sm">
|
||||
<Text>暂时还没有聊天记录</Text>
|
||||
</View>
|
||||
) : (
|
||||
messages.map((item, index) => (
|
||||
<View
|
||||
key={item.id || `msg-${index}`}
|
||||
className={`flex mb-4 ${item.isMine ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<View
|
||||
className={`px-3 py-2 rounded-2xl text-sm leading-relaxed break-words ${item.isMine ? 'bg-amber-400 text-white' : 'bg-white text-gray-800'}`}
|
||||
style={{maxWidth: '75%'}}
|
||||
>
|
||||
<Text>{item.content}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
<View className={'fixed bottom-0 w-full bg-orange-50 pt-2 pb-8'}>
|
||||
<View className={'flex flex-1 items-center justify-between'}>
|
||||
<Voice className={'mx-2'} />
|
||||
<Input className={'w-full'} style={{
|
||||
borderRadius: '6px',
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px'
|
||||
}} />
|
||||
<FaceMild size={26} className={'ml-2'} />
|
||||
<AddCircle size={26} className={'mx-2'} />
|
||||
|
||||
<View className="fixed bottom-0 left-0 right-0 bg-orange-50 pt-2 pb-8 px-3 border-t border-orange-100">
|
||||
<View className="flex gap-3 mb-2 overflow-x-auto">
|
||||
{quickActions.map(action => (
|
||||
<View
|
||||
key={action.type}
|
||||
onClick={() => handleQuickAction(action.type)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '20px',
|
||||
backgroundColor: '#fff',
|
||||
fontSize: '14px',
|
||||
color: '#8b5a2b',
|
||||
boxShadow: '0 4px 10px rgba(0,0,0,0.05)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Text>{action.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View className="flex flex-1 items-center justify-between">
|
||||
<Voice className="mx-2"/>
|
||||
<Input
|
||||
className="w-full"
|
||||
style={{
|
||||
borderRadius: '6px',
|
||||
paddingLeft: '12px',
|
||||
paddingRight: '12px'
|
||||
}}
|
||||
value={messageInput}
|
||||
onChange={(value) => setMessageInput(value)}
|
||||
confirmType="send"
|
||||
onConfirm={handleSendMessage}
|
||||
/>
|
||||
<FaceMild size={26} className="ml-2"/>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
className="ml-2"
|
||||
loading={sending}
|
||||
onClick={handleSendMessage}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerIndex;
|
||||
|
||||
Reference in New Issue
Block a user