web端聊天室优化

This commit is contained in:
2025-12-23 19:15:00 +08:00
parent a7897c67e5
commit 1fd26dcf1a
4 changed files with 324 additions and 57 deletions

View File

@@ -10,6 +10,14 @@ $brand-color-hover: #004488;
background: #fff; background: #fff;
} }
// ==================== 加载更多 ====================
.loading-more {
text-align: center;
padding: 12px;
font-size: 13px;
color: #94a3b8;
}
// ==================== 聊天室头部 ==================== // ==================== 聊天室头部 ====================
.chat-header { .chat-header {
height: 64px; height: 64px;
@@ -96,12 +104,28 @@ $brand-color-hover: #004488;
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
background: $brand-color-light;
display: flex;
align-items: center;
justify-content: center;
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
.avatar-text {
font-size: 14px;
font-weight: 600;
color: $brand-color;
}
}
.sender-name {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
} }
.message-content-wrapper { .message-content-wrapper {

View File

@@ -10,7 +10,11 @@
</header> </header>
<!-- 消息容器 --> <!-- 消息容器 -->
<div ref="messagesRef" class="messages-container"> <div ref="messagesRef" class="messages-container" @scroll="handleScroll">
<!-- 加载更多提示 -->
<div v-if="loadingMore" class="loading-more">加载中...</div>
<div v-else-if="!hasMore" class="loading-more">没有更多消息了</div>
<!-- Jitsi Meet会议iframe --> <!-- Jitsi Meet会议iframe -->
<div v-if="showMeeting && meetingUrl" class="meeting-container"> <div v-if="showMeeting && meetingUrl" class="meeting-container">
<IframeView :src="meetingUrl" /> <IframeView :src="meetingUrl" />
@@ -24,9 +28,13 @@
class="message-row" class="message-row"
:class="message.senderId === currentUserId ? 'is-me' : 'other'" :class="message.senderId === currentUserId ? 'is-me' : 'other'"
> >
<div>
<!-- 头像 --> <!-- 头像 -->
<div class="message-avatar"> <div class="message-avatar">
<img :src="FILE_DOWNLOAD_URL + message.senderAvatar" /> <img v-if="message.senderAvatar" :src="FILE_DOWNLOAD_URL + message.senderAvatar" />
<span v-else class="avatar-text">{{ message.senderName?.charAt(0) || '?' }}</span>
</div>
<div class="sender-name">{{ message.senderName || '未知用户' }}</div>
</div> </div>
<!-- 消息内容 --> <!-- 消息内容 -->
@@ -126,12 +134,16 @@ interface Props {
meetingUrl?: string meetingUrl?: string
showMeeting?: boolean showMeeting?: boolean
fileDownloadUrl?: string fileDownloadUrl?: string
hasMore?: boolean
loadingMore?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
roomName: '聊天室', roomName: '聊天室',
showMeeting: false, showMeeting: false,
fileDownloadUrl: '' fileDownloadUrl: '',
hasMore: true,
loadingMore: false
}) })
const FILE_DOWNLOAD_URL = props.fileDownloadUrl const FILE_DOWNLOAD_URL = props.fileDownloadUrl
@@ -140,8 +152,17 @@ const emit = defineEmits<{
'send-message': [content: string, files: File[]] 'send-message': [content: string, files: File[]]
'start-meeting': [] 'start-meeting': []
'download-file': [fileId: string] 'download-file': [fileId: string]
'load-more': []
}>() }>()
// 滚动到顶部加载更多
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement
if (target.scrollTop < 50 && props.hasMore && !props.loadingMore) {
emit('load-more')
}
}
defineSlots<{ defineSlots<{
header?: () => any header?: () => any
'action-area'?: () => any 'action-area'?: () => any

View File

@@ -12,15 +12,135 @@ $brand-color-hover: #004488;
font-family: 'Inter', 'Noto Sans SC', sans-serif; font-family: 'Inter', 'Noto Sans SC', sans-serif;
} }
// ==================== 聊天室列表侧边栏 ==================== // ==================== 折叠状态的侧边栏 ====================
.room-list-sidebar { .sidebar-collapsed {
width: 320px; position: absolute;
left: 0;
top: 0;
height: 100%; height: 100%;
width: 48px;
background: #fff; background: #fff;
border-right: 1px solid #e2e8f0; border-right: 1px solid #e2e8f0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0; align-items: center;
padding: 16px 0;
z-index: 10;
.sidebar-toggle-btn {
padding: 8px;
color: #64748b;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 8px;
&:hover {
color: $brand-color;
background: $brand-color-light;
}
}
.sidebar-icons {
flex: 1;
overflow-y: auto;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 0;
}
.sidebar-icon-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: linear-gradient(135deg, $brand-color 0%, $brand-color-hover 100%);
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
font-weight: 600;
&:hover {
transform: scale(1.1);
}
&.active {
box-shadow: 0 0 0 2px $brand-color-light;
}
}
.expand-btn {
padding: 8px;
color: #94a3b8;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
color: #64748b;
background: #f1f5f9;
}
}
}
// ==================== 展开时的关闭按钮 ====================
.sidebar-close-btn {
position: absolute;
left: 256px;
top: 50%;
transform: translateY(-50%);
z-index: 20;
background: #fff;
border: 1px solid #e2e8f0;
border-left: none;
border-radius: 0 8px 8px 0;
padding: 8px;
cursor: pointer;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
transition: all 0.2s;
color: #64748b;
&:hover {
background: #f8fafc;
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.1);
}
}
// ==================== 聊天室列表侧边栏 ====================
.room-list-sidebar {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0;
background: #fff;
border-right: 1px solid #e2e8f0;
z-index: 10;
transition: all 0.3s ease;
overflow: hidden;
box-shadow: 4px 0 16px rgba(0, 0, 0, 0.08);
&.open {
width: 256px;
}
.sidebar-inner {
width: 256px;
height: 100%;
display: flex;
flex-direction: column;
}
.sidebar-header { .sidebar-header {
height: 56px; height: 56px;
@@ -156,6 +276,12 @@ $brand-color-hover: #004488;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
background: #fff; background: #fff;
margin-left: 48px;
transition: margin-left 0.3s ease;
&.sidebar-open {
margin-left: 256px;
}
} }
// ==================== 聊天室头部 ==================== // ==================== 聊天室头部 ====================

View File

@@ -1,7 +1,35 @@
<template> <template>
<div class="chat-room-container"> <div class="chat-room-container">
<!-- 折叠状态的侧边栏 -->
<div v-if="!isSidebarOpen" class="sidebar-collapsed">
<button class="sidebar-toggle-btn" @click="toggleSidebar" title="展开聊天室列表">
<MessageCircle :size="20" />
</button>
<div class="sidebar-icons">
<button
v-for="room in filteredRooms.slice(0, 8)"
:key="room.roomId"
@click="selectRoom(room.roomId!); toggleSidebar()"
class="sidebar-icon-btn"
:class="{ active: currentRoomId === room.roomId }"
:title="room.roomName"
>
{{ room.guestName?.substring(0, 1) || '?' }}
</button>
</div>
<button class="expand-btn" @click="toggleSidebar" title="展开聊天室列表">
<ChevronRight :size="18" />
</button>
</div>
<!-- 展开时的关闭按钮 -->
<button v-if="isSidebarOpen" class="sidebar-close-btn" @click="toggleSidebar">
<ChevronLeft :size="18" />
</button>
<!-- 聊天室列表侧边栏 --> <!-- 聊天室列表侧边栏 -->
<aside class="room-list-sidebar"> <aside class="room-list-sidebar" :class="{ open: isSidebarOpen }">
<div class="sidebar-inner">
<!-- 头部 --> <!-- 头部 -->
<div class="sidebar-header"> <div class="sidebar-header">
<span class="title">聊天室</span> <span class="title">聊天室</span>
@@ -49,21 +77,26 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</aside> </aside>
<!-- 主聊天区域 --> <!-- 主聊天区域 -->
<main class="chat-main"> <main class="chat-main" :class="{ 'sidebar-open': isSidebarOpen }">
<template v-if="currentRoomId"> <template v-if="currentRoomId">
<ChatRoom <ChatRoom
ref="chatRoomRef"
:messages="messages" :messages="messages"
:current-user-id="userId" :current-user-id="userId"
:room-name="currentRoom?.roomName" :room-name="currentRoom?.roomName"
:meeting-url="currentMeetingUrl" :meeting-url="currentMeetingUrl"
:show-meeting="showMeetingIframe" :show-meeting="showMeetingIframe"
:file-download-url="FILE_DOWNLOAD_URL" :file-download-url="FILE_DOWNLOAD_URL"
:has-more="hasMore"
:loading-more="loadingMore"
@send-message="handleSendMessage" @send-message="handleSendMessage"
@start-meeting="startMeeting" @start-meeting="startMeeting"
@download-file="downloadFile" @download-file="downloadFile"
@load-more="loadMoreMessages"
> >
<template #header> <template #header>
<div class="chat-room-header"> <div class="chat-room-header">
@@ -113,10 +146,11 @@
</ElDialog> </ElDialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus' import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
import { Search, FileText, MessageSquare } from 'lucide-vue-next' import { Search, FileText, MessageSquare, MessageCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
import { ChatRoom } from '@/components/chatRoom' import { ChatRoom } from '@/components/chatRoom'
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue' import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
import { workcaseChatAPI } from '@/api/workcase' import { workcaseChatAPI } from '@/api/workcase'
@@ -143,12 +177,26 @@ let listSubscription: any = null
// 当前用户ID从登录状态获取 // 当前用户ID从登录状态获取
const userId = ref(localStorage.getItem('userId') || '') const userId = ref(localStorage.getItem('userId') || '')
// 侧边栏展开状态
const isSidebarOpen = ref(false)
// 切换侧边栏
const toggleSidebar = () => {
isSidebarOpen.value = !isSidebarOpen.value
}
// 搜索文本 // 搜索文本
const searchText = ref('') const searchText = ref('')
const userType = true //web端固定这个 const userType = true //web端固定这个
// 加载状态 // 加载状态
const loading = ref(false) const loading = ref(false)
const messageLoading = ref(false) const messageLoading = ref(false)
const loadingMore = ref(false)
// 分页状态
const PAGE_SIZE = 5
const currentPage = ref(1)
const hasMore = ref(true)
// 聊天室列表 // 聊天室列表
const chatRooms = ref<ChatRoomVO[]>([]) const chatRooms = ref<ChatRoomVO[]>([])
@@ -185,6 +233,9 @@ const showWorkcaseDetail = ref(false)
const currentMeetingUrl = ref('') const currentMeetingUrl = ref('')
const showMeetingIframe = ref(false) const showMeetingIframe = ref(false)
// ChatRoom组件引用
const chatRoomRef = ref<InstanceType<typeof ChatRoom> | null>(null)
// 获取聊天室列表 // 获取聊天室列表
const fetchChatRooms = async () => { const fetchChatRooms = async () => {
loading.value = true loading.value = true
@@ -196,6 +247,7 @@ const fetchChatRooms = async () => {
if (result.success && result.pageDomain) { if (result.success && result.pageDomain) {
chatRooms.value = result.pageDomain.dataList || [] chatRooms.value = result.pageDomain.dataList || []
} }
} catch (error) { } catch (error) {
console.error('获取聊天室列表失败:', error) console.error('获取聊天室列表失败:', error)
ElMessage.error('获取聊天室列表失败') ElMessage.error('获取聊天室列表失败')
@@ -210,17 +262,26 @@ const selectRoom = (roomId: string) => {
loadMessages(roomId) loadMessages(roomId)
} }
// 加载消息 // 加载消息初始加载page1后端降序返回
const loadMessages = async (roomId: string) => { const loadMessages = async (roomId: string) => {
messageLoading.value = true messageLoading.value = true
currentPage.value = 1
hasMore.value = true
try { try {
const result = await workcaseChatAPI.getChatMessagePage({ const result = await workcaseChatAPI.getChatMessagePage({
filter: { roomId }, filter: { roomId },
pageParam: { page: 1, pageSize: 100, total: 0 } pageParam: { page: 1, pageSize: PAGE_SIZE }
}) })
if (result.success && result.pageDomain) { if (result.success && result.pageDomain) {
messages.value = result.pageDomain.dataList || [] const pageInfo = result.pageDomain.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
hasMore.value = actualTotalPages > currentPage.value
// 后端降序返回,需要反转后显示(早的在上,新的在下)
const dataList = result.pageDomain.dataList || []
messages.value = [...dataList].reverse()
} }
// 加载完成后滚动到底部
scrollToBottom() scrollToBottom()
} catch (error) { } catch (error) {
console.error('加载消息失败:', error) console.error('加载消息失败:', error)
@@ -230,6 +291,39 @@ const loadMessages = async (roomId: string) => {
} }
} }
// 加载更多历史消息(滚动到顶部触发)
const loadMoreMessages = async () => {
if (!currentRoomId.value || loadingMore.value || !hasMore.value) return
const nextPage = currentPage.value + 1
loadingMore.value = true
try {
const result = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: currentRoomId.value },
pageParam: { page: nextPage, pageSize: PAGE_SIZE }
})
if (result.success && result.pageDomain) {
const pageInfo = result.pageDomain.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
const dataList = result.pageDomain.dataList || []
if (dataList.length > 0) {
currentPage.value = nextPage
hasMore.value = actualTotalPages > currentPage.value
// 后端降序返回,反转后插入到列表前面
const reversedList = [...dataList].reverse()
messages.value.unshift(...reversedList)
} else {
hasMore.value = false
}
}
} catch (error) {
console.error('加载更多消息失败:', error)
} finally {
loadingMore.value = false
}
}
// 处理发送消息从ChatRoom组件触发 // 处理发送消息从ChatRoom组件触发
const handleSendMessage = async (content: string, files: File[]) => { const handleSendMessage = async (content: string, files: File[]) => {
if (!currentRoomId.value) return if (!currentRoomId.value) return
@@ -284,9 +378,11 @@ const startMeeting = async () => {
showMeetingIframe.value = true showMeetingIframe.value = true
} }
// 滚动到底部 // 滚动聊天消息到底部
const scrollToBottom = () => { const scrollToBottom = () => {
// TODO: 滚动到ChatRoom组件底部 nextTick(() => {
chatRoomRef.value?.scrollToBottom()
})
} }
// 格式化时间(用于聊天室列表) // 格式化时间(用于聊天室列表)