完善功能

This commit is contained in:
2025-12-01 10:14:44 +08:00
parent d8011065d9
commit 2681ccc94b
31 changed files with 1281 additions and 302 deletions

View File

@@ -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;