Files
urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue
2025-12-23 18:57:41 +08:00

450 lines
14 KiB
Plaintext
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.

<template>
<!-- #ifdef APP -->
<scroll-view style="flex:1">
<!-- #endif -->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">{{ roomName }}</text>
</view>
<!-- 聊天室人员和操作 -->
<view class="room-toolbar" :style="{ top: headerPaddingTop + 44 + 'px' }">
<view class="member-count" @tap="showMembers = !showMembers">
<text class="member-count-text">{{ totalMembers.length > 0 ? totalMembers.length + ' 人在线' : '暂无人员' }}</text>
</view>
<view class="toolbar-right">
<button class="toolbar-btn" @tap="handleWorkcaseAction">
<text class="toolbar-btn-text">{{ workcaseId ? '查看工单' : '创建工单' }}</text>
</button>
<button class="toolbar-btn meeting-btn" @tap="startMeeting">
<text class="toolbar-btn-text meeting-text">发起会议</text>
</button>
</view>
</view>
<!-- 弹窗显示人员列表和在线情况 -->
<view v-if="showMembers" class="members-popup-mask" @tap="showMembers = false">
<view class="members-popup" :style="{ top: headerPaddingTop + 88 + 'px' }" @tap.stop>
<view class="members-list">
<view v-if="totalMembers.length === 0" class="members-empty">
<text class="members-empty-text">暂无人员在线</text>
</view>
<view v-else class="member-item" v-for="member in totalMembers" :key="member.oderId">
<view class="member-avatar">
<text class="member-avatar-text">{{ member.userName?.charAt(0) || '客' }}</text>
</view>
<text class="member-name">{{ member.userName || '未知' }}</text>
<view class="member-status" :class="member.isOnline ? 'online' : 'offline'"></view>
</view>
</view>
</view>
</view>
<!-- 聊天消息区域 -->
<scroll-view class="chat-area" scroll-y="true" :scroll-top="scrollTop"
:style="{ top: (headerPaddingTop + 88) + 'px' }"
@scrolltoupper="loadMoreMessages"
upper-threshold="50">
<!-- 加载更多提示 -->
<view v-if="loadingMore" class="loading-more">
<text class="loading-more-text">加载中...</text>
</view>
<view v-else-if="!hasMore" class="loading-more">
<text class="loading-more-text">没有更多消息了</text>
</view>
<view class="message-list">
<view class="message-item" v-for="msg in messages" :key="msg.messageId"
:class="msg.senderType === 'guest' ? 'self' : 'other'">
<!-- 对方消息(左侧) -->
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
<view>
<view class="avatar">
<text class="avatar-text">{{ msg.senderName?.charAt(0) || '客' }}</text>
</view>
<text class="sender-name">{{ msg.senderName || '客服' }}</text>
</view>
<view class="message-content">
<view class="bubble other-bubble">
<text class="message-text">{{ msg.content }}</text>
</view>
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
</view>
<!-- 自己消息(右侧) -->
<view class="message-row self-row" v-else>
<view class="message-content">
<view class="bubble self-bubble">
<text class="message-text">{{ msg.content }}</text>
</view>
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
<view class="avatar self-avatar">
<text class="avatar-text">我</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部输入区 -->
<view class="footer">
<view class="input-row">
<input class="chat-input" v-model="inputText" placeholder="输入消息..."
@confirm="sendMessage" />
<view class="send-btn" @tap="sendMessage">
<text class="send-text">发送</text>
</view>
</view>
</view>
<!-- 工单创建弹窗 -->
<WorkcaseCreator v-if="showWorkcaseCreator" :show="showWorkcaseCreator"
@close="hideCreator" @success="onWorkcaseCreated" />
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO } from '@/types/workcase'
import { workcaseChatAPI } from '@/api/workcase'
// 响应式数据
const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88)
const roomId = ref<string>('')
const workcaseId = ref<string>('')
const roomName = ref<string>('聊天室')
const inputText = ref<string>('')
const scrollTop = ref<number>(0)
const showWorkcaseCreator = ref<boolean>(false)
const loading = ref<boolean>(false)
const sending = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const currentPage = ref<number>(1)
const hasMore = ref<boolean>(true)
// 用户信息从storage获取
const currentUserId = ref<string>('')
const currentUserName = ref<string>('我')
function loadUserInfo() {
try {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
const user = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
currentUserId.value = user.userId || user.id || ''
currentUserName.value = user.username || user.nickName || '我'
}
} catch (e) {
console.error('获取用户信息失败:', e)
}
}
// 消息列表
const messages = reactive<ChatRoomMessageVO[]>([])
// 所有默认客服
const defaultWorkers = reactive<CustomerVO[]>([])
async function loadDefaultWorkers() {
const res = await workcaseChatAPI.getAvailableCustomerServices()
if(res.success && res.dataList) {
defaultWorkers.splice(0, defaultWorkers.length, ...res.dataList)
}
}
// 查询进入聊天室的人员, 包含了访客
const chatMembers = reactive<ChatMemberVO[]>([])
async function loadChatMembers() {
const res = await workcaseChatAPI.getChatRoomMemberList(roomId.value)
if(res.success && res.dataList) {
chatMembers.splice(0, chatMembers.length, ...res.dataList)
}
}
const showMembers = ref(false)
// 计算聊天室人数: 默认客服转成ChatMemberVO + 进入聊天室的人员,去重,进入聊天室的人员是在线状态
interface MemberDisplay {
oderId: string
userId: string
userName: string
isOnline: boolean
}
const totalMembers = computed<MemberDisplay[]>(() => {
const memberMap = new Map<string, MemberDisplay>()
// 先添加默认客服(离线状态)
defaultWorkers.forEach((worker, index) => {
memberMap.set(worker.userId || '', {
oderId: `worker-${index}`,
userId: worker.userId || '',
userName: worker.username || '客服',
isOnline: false
})
})
// 再添加聊天室成员(在线状态),覆盖同一用户
chatMembers.forEach((member, index) => {
memberMap.set(member.userId || '', {
oderId: member.memberId || `member-${index}`,
userId: member.userId || '',
userName: member.userName || '未知',
isOnline: true
})
})
return Array.from(memberMap.values())
})
// 生命周期
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
// #endif
}
})
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
if (currentPage && currentPage.options) {
roomId.value = currentPage.options.roomId || ''
workcaseId.value = currentPage.options.workcaseId || ''
}
loadUserInfo()
loadChatRoom()
loadDefaultWorkers()
loadChatMembers()
})
// 加载聊天室
const PAGE_SIZE = 5
const messageTotal = ref<number>(0)
async function loadChatRoom() {
if (!roomId.value) return
loading.value = true
try {
// 获取聊天室信息
const roomRes = await workcaseChatAPI.getChatRoomById(roomId.value)
if (roomRes.success && roomRes.data) {
roomName.value = roomRes.data.roomName || '聊天室'
workcaseId.value = roomRes.data.workcaseId || ''
messageTotal.value = roomRes.data.messageCount || 0
}
// 后端是降序查询page1是最新消息
currentPage.value = 1
hasMore.value = true // 默认有更多loadMessages中会根据实际情况更新
// 获取消息列表
await loadMessages()
} catch (e) {
console.error('加载聊天室失败:', e)
} finally {
loading.value = false
}
}
// 加载消息列表后端降序page1是最新消息需要反转显示
async function loadMessages() {
console.log('[loadMessages] 开始加载, currentPage:', currentPage.value, 'roomId:', roomId.value)
if (!roomId.value) return
try {
const msgRes = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: roomId.value },
pageParam: { page: currentPage.value, pageSize: PAGE_SIZE }
})
console.log('[loadMessages] 响应:', msgRes)
if (msgRes.success && msgRes.dataList) {
const pageInfo = msgRes.pageDomain?.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
hasMore.value = actualTotalPages > currentPage.value
console.log('[loadMessages] pageInfo:', pageInfo, 'actualTotalPages:', actualTotalPages, 'hasMore:', hasMore.value)
// 后端降序返回,需要反转后显示(早的在上,新的在下)
const reversedList = [...msgRes.dataList].reverse()
messages.splice(0, messages.length, ...reversedList)
console.log('[loadMessages] 加载完成, 消息数:', messages.length)
nextTick(() => scrollToBottom())
}
} catch (e) {
console.error('加载消息列表失败:', e)
}
}
// 加载更多历史消息(滚动到顶部触发,加载下一页更早的消息)
async function loadMoreMessages() {
console.log('[loadMoreMessages] 触发, roomId:', roomId.value, 'loadingMore:', loadingMore.value, 'hasMore:', hasMore.value, 'currentPage:', currentPage.value)
if (!roomId.value || loadingMore.value || !hasMore.value) {
console.log('[loadMoreMessages] 跳过加载 - roomId:', !roomId.value, 'loadingMore:', loadingMore.value, '!hasMore:', !hasMore.value)
return
}
// 加载下一页(更早的消息)
const nextPage = currentPage.value + 1
console.log('[loadMoreMessages] 准备加载页:', nextPage)
loadingMore.value = true
try {
const msgRes = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: roomId.value },
pageParam: { page: nextPage, pageSize: PAGE_SIZE }
})
console.log('[loadMoreMessages] 响应:', msgRes)
if (msgRes.success && msgRes.dataList && msgRes.dataList.length > 0) {
const pageInfo = msgRes.pageDomain?.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
console.log('[loadMoreMessages] pageInfo:', pageInfo, 'actualTotalPages:', actualTotalPages)
currentPage.value = nextPage
hasMore.value = actualTotalPages > currentPage.value
console.log('[loadMoreMessages] 更新后 currentPage:', currentPage.value, 'hasMore:', hasMore.value)
// 后端降序返回,反转后插入到列表前面
const reversedList = [...msgRes.dataList].reverse()
messages.unshift(...reversedList)
console.log('[loadMoreMessages] 加载完成, 消息数:', messages.length)
} else {
console.log('[loadMoreMessages] 没有更多数据')
hasMore.value = false
}
} catch (e) {
console.error('加载更多消息失败:', e)
} finally {
loadingMore.value = false
}
}
// 格式化时间(兼容 iOS
function formatTime(time?: string): string {
if (!time) return ''
// iOS 不支持 "yyyy-MM-dd HH:mm:ss" 格式,需要转换为 "yyyy-MM-ddTHH:mm:ss" 或 "yyyy/MM/dd HH:mm:ss"
const iosCompatibleTime = time.replace(' ', 'T')
const date = new Date(iosCompatibleTime)
if (isNaN(date.getTime())) return ''
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 发送消息
async function sendMessage() {
const text = inputText.value.trim()
if (!text || sending.value) return
sending.value = true
const tempId = Date.now().toString()
// 先添加临时消息到界面
const tempMsg: ChatRoomMessageVO = {
messageId: tempId,
roomId: roomId.value,
senderId: currentUserId.value,
senderType: 'guest',
senderName: currentUserName.value,
content: text,
sendTime: new Date().toISOString(),
status: 'sending'
}
messages.push(tempMsg)
inputText.value = ''
nextTick(() => scrollToBottom())
try {
// 调用API发送消息
const msgDTO: TbChatRoomMessageDTO = {
roomId: roomId.value,
senderId: currentUserId.value,
senderType: 'guest',
senderName: currentUserName.value,
messageType: 'text',
content: text
}
const res = await workcaseChatAPI.sendMessage(msgDTO)
if (res.success && res.data) {
// 更新临时消息为真实消息
const idx = messages.findIndex(m => m.messageId === tempId)
if (idx !== -1) {
messages[idx] = { ...res.data, status: 'sent' }
}
} else {
// 发送失败,标记状态
const idx = messages.findIndex(m => m.messageId === tempId)
if (idx !== -1) {
messages[idx].status = 'failed'
}
uni.showToast({ title: res.message || '发送失败', icon: 'none' })
}
} catch (e) {
console.error('发送消息失败:', e)
const idx = messages.findIndex(m => m.messageId === tempId)
if (idx !== -1) {
messages[idx].status = 'failed'
}
uni.showToast({ title: '发送失败', icon: 'none' })
} finally {
sending.value = false
}
}
// 滚动到底部
function scrollToBottom() {
scrollTop.value = 999999
}
// 处理工单操作
function handleWorkcaseAction() {
if (workcaseId.value) {
uni.navigateTo({
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId.value}`
})
} else {
showWorkcaseCreator.value = true
}
}
// 隐藏工单创建器
function hideCreator() {
showWorkcaseCreator.value = false
}
// 工单创建成功
function onWorkcaseCreated(data: any) {
hideCreator()
workcaseId.value = data.workcaseId || 'new-workcase'
uni.showToast({
title: '工单创建成功',
icon: 'success'
})
}
// 发起会议
function startMeeting() {
uni.navigateTo({
url: `/pages/meeting/Meeting/Meeting?roomId=${roomId.value}&workcaseId=${workcaseId.value}`
})
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
@import "./chatRoom.scss";
</style>