302 lines
9.6 KiB
Vue
302 lines
9.6 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="chat-room-container">
|
|||
|
|
<!-- 聊天室列表侧边栏 -->
|
|||
|
|
<aside class="room-list-sidebar">
|
|||
|
|
<!-- 头部 -->
|
|||
|
|
<div class="sidebar-header">
|
|||
|
|
<span class="title">聊天室</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 搜索框 -->
|
|||
|
|
<div class="search-box">
|
|||
|
|
<ElInput
|
|||
|
|
v-model="searchText"
|
|||
|
|
placeholder="搜索工单号、来客姓名、电话..."
|
|||
|
|
:prefix-icon="Search"
|
|||
|
|
clearable
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- chatRoom列表 -->
|
|||
|
|
<div class="room-list-container">
|
|||
|
|
<div v-if="filteredRooms.length === 0" class="empty-tip">
|
|||
|
|
暂无聊天室
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
v-for="room in filteredRooms"
|
|||
|
|
:key="room.roomId"
|
|||
|
|
class="room-item"
|
|||
|
|
:class="{ active: currentRoomId === room.roomId }"
|
|||
|
|
@click="selectRoom(room.roomId)"
|
|||
|
|
>
|
|||
|
|
<!-- 头像 -->
|
|||
|
|
<div class="room-avatar">
|
|||
|
|
{{ room.guestName.substring(0, 1) }}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 信息 -->
|
|||
|
|
<div class="room-info">
|
|||
|
|
<div class="room-header">
|
|||
|
|
<div class="room-name">{{ room.roomName }}</div>
|
|||
|
|
<div class="room-time">{{ formatTime(room.lastMessageTime) }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="last-message">{{ room.lastMessage || '暂无消息' }}</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 未读红点 -->
|
|||
|
|
<div v-if="room.unreadCount > 0" class="unread-badge">
|
|||
|
|
{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<!-- 主聊天区域 -->
|
|||
|
|
<main class="chat-main">
|
|||
|
|
<template v-if="currentRoomId">
|
|||
|
|
<ChatRoom
|
|||
|
|
:messages="messages"
|
|||
|
|
:current-user-id="userId"
|
|||
|
|
:room-name="currentRoom?.roomName"
|
|||
|
|
:meeting-url="currentMeetingUrl"
|
|||
|
|
:show-meeting="showMeetingIframe"
|
|||
|
|
:file-download-url="FILE_DOWNLOAD_URL"
|
|||
|
|
@send-message="handleSendMessage"
|
|||
|
|
@start-meeting="startMeeting"
|
|||
|
|
@download-file="downloadFile"
|
|||
|
|
>
|
|||
|
|
<template #header>
|
|||
|
|
<div class="chat-room-header">
|
|||
|
|
<div class="header-left">
|
|||
|
|
<div class="room-avatar-small">
|
|||
|
|
{{ currentRoom?.guestName?.substring(0, 1) }}
|
|||
|
|
</div>
|
|||
|
|
<div class="room-title-group">
|
|||
|
|
<div class="room-name-text">{{ currentRoom?.roomName }}</div>
|
|||
|
|
<div class="room-subtitle">
|
|||
|
|
工单 #{{ currentRoom?.workcaseId }} · {{ currentRoom?.guestName }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<template #action-area>
|
|||
|
|
<ElButton type="primary" @click="showWorkcaseDetail = true">
|
|||
|
|
<FileText :size="16" />
|
|||
|
|
查看工单
|
|||
|
|
</ElButton>
|
|||
|
|
</template>
|
|||
|
|
</ChatRoom>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<!-- 空状态 -->
|
|||
|
|
<div v-else class="empty-state">
|
|||
|
|
<div class="empty-content">
|
|||
|
|
<div class="empty-icon">
|
|||
|
|
<MessageSquare :size="40" />
|
|||
|
|
</div>
|
|||
|
|
<h3 class="empty-title">选择一个聊天室开始对话</h3>
|
|||
|
|
<p class="empty-desc">从左侧列表中选择一个聊天室查看消息</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<!-- 工单详情对话框 -->
|
|||
|
|
<ElDialog
|
|||
|
|
v-model="showWorkcaseDetail"
|
|||
|
|
title="工单详情"
|
|||
|
|
width="800px"
|
|||
|
|
class="workcase-dialog"
|
|||
|
|
>
|
|||
|
|
<WorkcaseDetail :workcase-id="currentWorkcaseId" />
|
|||
|
|
</ElDialog>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
import { ElButton, ElInput, ElDialog } from 'element-plus'
|
|||
|
|
import { Search, FileText, MessageSquare } from 'lucide-vue-next'
|
|||
|
|
import ChatRoom from 'shared/components/chatRoom/ChatRoom.vue'
|
|||
|
|
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
|
|||
|
|
import { FILE_DOWNLOAD_URL } from '@/config'
|
|||
|
|
|
|||
|
|
interface ChatRoomVO {
|
|||
|
|
roomId: string
|
|||
|
|
workcaseId: string
|
|||
|
|
roomName: string
|
|||
|
|
guestName: string
|
|||
|
|
lastMessage: string | null
|
|||
|
|
lastMessageTime: string | null
|
|||
|
|
unreadCount: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ChatMessageVO {
|
|||
|
|
messageId: string
|
|||
|
|
senderId: string
|
|||
|
|
senderName: string
|
|||
|
|
senderAvatar: string
|
|||
|
|
content: string
|
|||
|
|
files: string[]
|
|||
|
|
sendTime: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 当前用户ID
|
|||
|
|
const userId = ref('CURRENT_USER_ID')
|
|||
|
|
|
|||
|
|
// 搜索文本
|
|||
|
|
const searchText = ref('')
|
|||
|
|
|
|||
|
|
// 聊天室列表
|
|||
|
|
const chatRooms = ref<ChatRoomVO[]>([
|
|||
|
|
{
|
|||
|
|
roomId: 'ROOM001',
|
|||
|
|
workcaseId: 'WC001',
|
|||
|
|
roomName: '工单#WC001 - 电源故障',
|
|||
|
|
guestName: '张三',
|
|||
|
|
lastMessage: '好的,谢谢您的帮助',
|
|||
|
|
lastMessageTime: new Date().toISOString(),
|
|||
|
|
unreadCount: 3
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
roomId: 'ROOM002',
|
|||
|
|
workcaseId: 'WC002',
|
|||
|
|
roomName: '工单#WC002 - 设备维修',
|
|||
|
|
guestName: '李四',
|
|||
|
|
lastMessage: '请问什么时候能来处理?',
|
|||
|
|
lastMessageTime: new Date(Date.now() - 3600000).toISOString(),
|
|||
|
|
unreadCount: 0
|
|||
|
|
}
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
// 当前选中的聊天室ID
|
|||
|
|
const currentRoomId = ref<string | null>(null)
|
|||
|
|
|
|||
|
|
// 当前聊天室
|
|||
|
|
const currentRoom = computed(() =>
|
|||
|
|
chatRooms.value.find(r => r.roomId === currentRoomId.value)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 当前工单ID
|
|||
|
|
const currentWorkcaseId = computed(() => currentRoom.value?.workcaseId || '')
|
|||
|
|
|
|||
|
|
// 过滤后的聊天室列表
|
|||
|
|
const filteredRooms = computed(() => {
|
|||
|
|
if (!searchText.value) return chatRooms.value
|
|||
|
|
const keyword = searchText.value.toLowerCase()
|
|||
|
|
return chatRooms.value.filter(room =>
|
|||
|
|
room.roomName.toLowerCase().includes(keyword) ||
|
|||
|
|
room.guestName.toLowerCase().includes(keyword) ||
|
|||
|
|
room.workcaseId.toLowerCase().includes(keyword)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 消息列表
|
|||
|
|
const messages = ref<ChatMessageVO[]>([])
|
|||
|
|
|
|||
|
|
// 工单详情对话框
|
|||
|
|
const showWorkcaseDetail = ref(false)
|
|||
|
|
|
|||
|
|
// Jitsi Meet会议相关
|
|||
|
|
const currentMeetingUrl = ref('')
|
|||
|
|
const showMeetingIframe = ref(false)
|
|||
|
|
|
|||
|
|
// 选择聊天室
|
|||
|
|
const selectRoom = (roomId: string) => {
|
|||
|
|
currentRoomId.value = roomId
|
|||
|
|
// TODO: 加载该聊天室的消息
|
|||
|
|
loadMessages(roomId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载消息
|
|||
|
|
const loadMessages = async (roomId: string) => {
|
|||
|
|
// TODO: 调用API加载消息
|
|||
|
|
messages.value = [
|
|||
|
|
{
|
|||
|
|
messageId: 'MSG001',
|
|||
|
|
senderId: 'OTHER_USER',
|
|||
|
|
senderName: '张三',
|
|||
|
|
senderAvatar: 'avatar.jpg',
|
|||
|
|
content: '你好,我的设备出现故障了',
|
|||
|
|
files: [],
|
|||
|
|
sendTime: new Date().toISOString()
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
messageId: 'MSG002',
|
|||
|
|
senderId: userId.value,
|
|||
|
|
senderName: '客服',
|
|||
|
|
senderAvatar: 'avatar.jpg',
|
|||
|
|
content: '您好,请问是什么故障?',
|
|||
|
|
files: [],
|
|||
|
|
sendTime: new Date().toISOString()
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
scrollToBottom()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理发送消息(从ChatRoom组件触发)
|
|||
|
|
const handleSendMessage = async (content: string, files: File[]) => {
|
|||
|
|
if (!currentRoomId.value) return
|
|||
|
|
|
|||
|
|
// TODO: 上传文件获取fileIds
|
|||
|
|
const fileIds: string[] = []
|
|||
|
|
|
|||
|
|
const newMessage: ChatMessageVO = {
|
|||
|
|
messageId: 'MSG' + Date.now(),
|
|||
|
|
senderId: userId.value,
|
|||
|
|
senderName: '客服',
|
|||
|
|
senderAvatar: 'avatar.jpg',
|
|||
|
|
content,
|
|||
|
|
files: fileIds,
|
|||
|
|
sendTime: new Date().toISOString()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
messages.value.push(newMessage)
|
|||
|
|
|
|||
|
|
// TODO: 通过WebSocket发送到服务器
|
|||
|
|
console.log('发送消息:', { content, files })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 下载文件
|
|||
|
|
const downloadFile = (fileId: string) => {
|
|||
|
|
// TODO: 下载文件
|
|||
|
|
console.log('下载文件:', fileId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发起会议
|
|||
|
|
const startMeeting = async () => {
|
|||
|
|
if (!currentRoomId.value) return
|
|||
|
|
|
|||
|
|
// TODO: 调用后端API创建Jitsi会议
|
|||
|
|
// const meeting = await createMeeting(currentRoomId.value)
|
|||
|
|
|
|||
|
|
// 模拟会议URL
|
|||
|
|
const meetingId = 'meeting-' + Date.now()
|
|||
|
|
currentMeetingUrl.value = `https://meet.jit.si/${meetingId}`
|
|||
|
|
showMeetingIframe.value = true
|
|||
|
|
|
|||
|
|
console.log('发起会议:', currentMeetingUrl.value)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 滚动到底部
|
|||
|
|
const scrollToBottom = () => {
|
|||
|
|
// TODO: 滚动到ChatRoom组件底部
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化时间(用于聊天室列表)
|
|||
|
|
const formatTime = (time: string | null) => {
|
|||
|
|
if (!time) return ''
|
|||
|
|
const date = new Date(time)
|
|||
|
|
const now = new Date()
|
|||
|
|
const diff = now.getTime() - date.getTime()
|
|||
|
|
|
|||
|
|
if (diff < 60000) return '刚刚'
|
|||
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
|||
|
|
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
|||
|
|
|
|||
|
|
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
@import url("./ChatRoomView.scss");
|
|||
|
|
</style>
|