模块取出

This commit is contained in:
2025-12-23 16:16:47 +08:00
parent 68daf391af
commit e75b2f3bab
22 changed files with 164 additions and 399 deletions

View File

@@ -0,0 +1,2 @@
export * from './workcase'
export * from './workcaseChat'

View File

@@ -0,0 +1,185 @@
import { api } from 'shared/api'
import type { ResultDomain, PageRequest } from 'shared/types'
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbWorkcaseDeviceDTO } from '@/types/workcase'
/**
* @description 工单管理接口
* @filename workcase.ts
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
export const workcaseAPI = {
baseUrl: '/urban-lifeline/workcase',
// ========================= 工单管理 =========================
/**
* 创建工单
* @param workcase 工单信息
*/
async createWorkcase(workcase: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.post<TbWorkcaseDTO>(`${this.baseUrl}`, workcase)
return response.data
},
/**
* 更新工单
* @param workcase 工单信息
*/
async updateWorkcase(workcase: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.put<TbWorkcaseDTO>(`${this.baseUrl}`, workcase)
return response.data
},
/**
* 删除工单
* @param workcaseId 工单ID
*/
async deleteWorkcase(workcaseId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.delete<TbWorkcaseDTO>(`${this.baseUrl}/${workcaseId}`)
return response.data
},
/**
* 获取工单详情
* @param workcaseId 工单ID
*/
async getWorkcaseById(workcaseId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.get<TbWorkcaseDTO>(`${this.baseUrl}/${workcaseId}`)
return response.data
},
/**
* 查询工单列表
* @param filter 筛选条件
*/
async getWorkcaseList(filter?: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.post<TbWorkcaseDTO>(`${this.baseUrl}/list`, filter || {})
return response.data
},
/**
* 分页查询工单
* @param pageRequest 分页请求
*/
async getWorkcasePage(pageRequest: PageRequest<TbWorkcaseDTO>): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.post<TbWorkcaseDTO>(`${this.baseUrl}/page`, pageRequest)
return response.data
},
// ========================= CRM同步接口 =========================
/**
* 同步工单到CRM
* @param workcase 工单信息
*/
async syncWorkcaseToCrm(workcase: TbWorkcaseDTO): Promise<ResultDomain<void>> {
const response = await api.post<void>(`${this.baseUrl}/sync/crm`, workcase)
return response.data
},
/**
* 接收CRM工单更新CRM回调
* @param jsonBody JSON字符串
*/
async receiveWorkcaseFromCrm(jsonBody: string): Promise<ResultDomain<void>> {
const response = await api.post<void>(`${this.baseUrl}/receive/crm`, jsonBody)
return response.data
},
// ========================= 工单处理过程 =========================
/**
* 创建工单处理过程
* @param process 处理过程信息
*/
async createWorkcaseProcess(process: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
const response = await api.post<TbWorkcaseProcessDTO>(`${this.baseUrl}/process`, process)
return response.data
},
/**
* 更新工单处理过程
* @param process 处理过程信息
*/
async updateWorkcaseProcess(process: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
const response = await api.put<TbWorkcaseProcessDTO>(`${this.baseUrl}/process`, process)
return response.data
},
/**
* 删除工单处理过程
* @param processId 处理过程ID
*/
async deleteWorkcaseProcess(processId: string): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
const response = await api.delete<TbWorkcaseProcessDTO>(`${this.baseUrl}/process/${processId}`)
return response.data
},
/**
* 查询工单处理过程列表
* @param filter 筛选条件
*/
async getWorkcaseProcessList(filter?: TbWorkcaseProcessDTO): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
const response = await api.post<TbWorkcaseProcessDTO>(`${this.baseUrl}/process/list`, filter || {})
return response.data
},
/**
* 分页查询工单处理过程
* @param pageRequest 分页请求
*/
async getWorkcaseProcessPage(pageRequest: PageRequest<TbWorkcaseProcessDTO>): Promise<ResultDomain<TbWorkcaseProcessDTO>> {
const response = await api.post<TbWorkcaseProcessDTO>(`${this.baseUrl}/process/page`, pageRequest)
return response.data
},
// ========================= 工单设备管理 =========================
/**
* 创建工单设备
* @param device 设备信息
*/
async createWorkcaseDevice(device: TbWorkcaseDeviceDTO): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device`, device)
return response.data
},
/**
* 更新工单设备
* @param device 设备信息
*/
async updateWorkcaseDevice(device: TbWorkcaseDeviceDTO): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
const response = await api.put<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device`, device)
return response.data
},
/**
* 删除工单设备
* @param workcaseId 工单ID
* @param device 设备名称
*/
async deleteWorkcaseDevice(workcaseId: string, device: string): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
const response = await api.delete<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/${workcaseId}/${device}`)
return response.data
},
/**
* 查询工单设备列表
* @param filter 筛选条件
*/
async getWorkcaseDeviceList(filter?: TbWorkcaseDeviceDTO): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/list`, filter || {})
return response.data
},
/**
* 分页查询工单设备
* @param pageRequest 分页请求
*/
async getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/page`, pageRequest)
return response.data
}
}

View File

@@ -0,0 +1,214 @@
import { api } from 'shared/api'
import type { ResultDomain, PageRequest } from 'shared/types'
import type {
TbChatRoomDTO,
TbChatRoomMemberDTO,
TbChatRoomMessageDTO,
TbCustomerServiceDTO,
TbWordCloudDTO,
ChatRoomVO,
ChatMemberVO,
ChatRoomMessageVO,
CustomerServiceVO
} from '@/types/workcase'
/**
* @description 工单对话相关接口ChatRoom、客服、词云管理
* @filename workcaseChat.ts
* @author cascade
* @copyright xyzh
* @since 2025-12-22
*/
export const workcaseChatAPI = {
baseUrl: '/urban-lifeline/workcase/chat',
// ====================== ChatRoom聊天室管理 ======================
/**
* 创建聊天室
*/
async createChatRoom(chatRoom: TbChatRoomDTO): Promise<ResultDomain<TbChatRoomDTO>> {
const response = await api.post<TbChatRoomDTO>(`${this.baseUrl}/room`, chatRoom)
return response.data
},
/**
* 更新聊天室
*/
async updateChatRoom(chatRoom: TbChatRoomDTO): Promise<ResultDomain<TbChatRoomDTO>> {
const response = await api.put<TbChatRoomDTO>(`${this.baseUrl}/room`, chatRoom)
return response.data
},
/**
* 关闭聊天室
*/
async closeChatRoom(roomId: string, closedBy: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`${this.baseUrl}/room/${roomId}/close`, null, {
params: { closedBy }
})
return response.data
},
/**
* 获取聊天室详情
*/
async getChatRoomById(roomId: string): Promise<ResultDomain<TbChatRoomDTO>> {
const response = await api.get<TbChatRoomDTO>(`${this.baseUrl}/room/${roomId}`)
return response.data
},
/**
* 分页查询聊天室
*/
async getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>): Promise<ResultDomain<ChatRoomVO>> {
const response = await api.post<ChatRoomVO>(`${this.baseUrl}/room/page`, pageRequest)
return response.data
},
// ====================== ChatRoom成员管理 ======================
/**
* 添加聊天室成员
*/
async addChatRoomMember(member: TbChatRoomMemberDTO): Promise<ResultDomain<TbChatRoomMemberDTO>> {
const response = await api.post<TbChatRoomMemberDTO>(`${this.baseUrl}/room/member`, member)
return response.data
},
/**
* 移除聊天室成员
*/
async removeChatRoomMember(memberId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/room/member/${memberId}`)
return response.data
},
/**
* 获取聊天室成员列表
*/
async getChatRoomMemberList(roomId: string): Promise<ResultDomain<ChatMemberVO>> {
const response = await api.get<ChatMemberVO>(`${this.baseUrl}/room/${roomId}/members`)
return response.data
},
// ====================== ChatRoom消息管理 ======================
/**
* 发送聊天室消息
*/
async sendMessage(message: TbChatRoomMessageDTO): Promise<ResultDomain<TbChatRoomMessageDTO>> {
const response = await api.post<TbChatRoomMessageDTO>(`${this.baseUrl}/room/message`, message)
return response.data
},
/**
* 分页查询聊天室消息
*/
async getChatMessagePage(pageRequest: PageRequest<TbChatRoomMessageDTO>): Promise<ResultDomain<ChatRoomMessageVO>> {
const response = await api.post<ChatRoomMessageVO>(`${this.baseUrl}/room/message/page`, pageRequest)
return response.data
},
/**
* 删除聊天室消息
*/
async deleteMessage(messageId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/room/message/${messageId}`)
return response.data
},
// ====================== 客服人员管理 ======================
/**
* 添加客服人员
*/
async addCustomerService(customerService: TbCustomerServiceDTO): Promise<ResultDomain<TbCustomerServiceDTO>> {
const response = await api.post<TbCustomerServiceDTO>(`${this.baseUrl}/customer-service`, customerService)
return response.data
},
/**
* 更新客服人员
*/
async updateCustomerService(customerService: TbCustomerServiceDTO): Promise<ResultDomain<TbCustomerServiceDTO>> {
const response = await api.put<TbCustomerServiceDTO>(`${this.baseUrl}/customer-service`, customerService)
return response.data
},
/**
* 删除客服人员
*/
async deleteCustomerService(userId: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`${this.baseUrl}/customer-service/${userId}`)
return response.data
},
/**
* 分页查询客服人员
*/
async getCustomerServicePage(pageRequest: PageRequest<TbCustomerServiceDTO>): Promise<ResultDomain<CustomerServiceVO>> {
const response = await api.post<CustomerServiceVO>(`${this.baseUrl}/customer-service/page`, pageRequest)
return response.data
},
/**
* 更新客服在线状态
*/
async updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>(`${this.baseUrl}/customer-service/${userId}/status`, null, {
params: { status }
})
return response.data
},
/**
* 获取可接待客服列表
*/
async getAvailableCustomerServices(): Promise<ResultDomain<CustomerServiceVO>> {
const response = await api.get<CustomerServiceVO>(`${this.baseUrl}/customer-service/available`)
return response.data
},
/**
* 自动分配客服
*/
async assignCustomerService(roomId: string): Promise<ResultDomain<CustomerServiceVO>> {
const response = await api.post<CustomerServiceVO>(`${this.baseUrl}/room/${roomId}/assign`)
return response.data
},
// ====================== 词云管理 ======================
/**
* 添加词云
*/
async addWordCloud(wordCloud: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
const response = await api.post<TbWordCloudDTO>(`${this.baseUrl}/wordcloud`, wordCloud)
return response.data
},
/**
* 更新词云
*/
async updateWordCloud(wordCloud: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
const response = await api.put<TbWordCloudDTO>(`${this.baseUrl}/wordcloud`, wordCloud)
return response.data
},
/**
* 查询词云列表
*/
async getWordCloudList(filter: TbWordCloudDTO): Promise<ResultDomain<TbWordCloudDTO>> {
const response = await api.post<TbWordCloudDTO>(`${this.baseUrl}/wordcloud/list`, filter)
return response.data
},
/**
* 分页查询词云
*/
async getWordCloudPage(pageRequest: PageRequest<TbWordCloudDTO>): Promise<ResultDomain<TbWordCloudDTO>> {
const response = await api.post<TbWordCloudDTO>(`${this.baseUrl}/wordcloud/page`, pageRequest)
return response.data
}
}

View File

@@ -0,0 +1,323 @@
// 品牌色
$brand-color: #0055AA;
$brand-color-light: #EBF5FF;
$brand-color-hover: #004488;
.chat-room-main {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
// ==================== 聊天室头部 ====================
.chat-header {
height: 64px;
display: flex;
align-items: center;
padding: 0 24px;
border-bottom: 1px solid #e2e8f0;
flex-shrink: 0;
.header-default {
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
}
}
// ==================== 消息容器 ====================
.messages-container {
flex: 1;
overflow-y: auto;
background: #f8fafc;
position: relative;
}
// ==================== Jitsi Meet会议容器 ====================
.meeting-container {
position: sticky;
top: 0;
z-index: 10;
height: 400px;
background: #000;
border-bottom: 2px solid $brand-color;
margin-bottom: 16px;
iframe {
width: 100%;
height: 100%;
border: none;
}
}
// ==================== 消息列表 ====================
.messages-list {
max-width: 900px;
margin: 0 auto;
padding: 24px 16px;
.message-row {
display: flex;
gap: 12px;
margin-bottom: 24px;
&.is-me {
flex-direction: row-reverse;
.message-bubble {
background: $brand-color;
color: #fff;
border-radius: 16px 16px 4px 16px;
.message-time {
text-align: right;
color: rgba(255, 255, 255, 0.7);
}
}
}
&.other {
.message-bubble {
background: #fff;
border: 1px solid #f1f5f9;
border-radius: 16px 16px 16px 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
}
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.message-content-wrapper {
max-width: 70%;
display: flex;
flex-direction: column;
gap: 8px;
}
.message-bubble {
padding: 12px 16px;
.message-text {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
}
.message-files {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.file-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid rgba(255, 255, 255, 0.2);
&:hover {
background: rgba(255, 255, 255, 0.2);
}
.file-icon {
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.file-info {
.file-name {
font-size: 13px;
font-weight: 500;
}
}
}
.other .file-item {
background: #f8fafc;
border-color: #e2e8f0;
&:hover {
background: #f1f5f9;
}
.file-icon {
background: $brand-color-light;
color: $brand-color;
}
.file-info {
color: #374151;
}
}
.message-time {
font-size: 12px;
color: #94a3b8;
padding: 0 4px;
}
}
// ==================== 输入区域 ====================
.input-area {
padding: 16px 24px 24px;
background: #fff;
border-top: 1px solid #e2e8f0;
flex-shrink: 0;
.action-buttons {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: transparent;
border: 1px solid #e2e8f0;
border-radius: 8px;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
&:hover {
border-color: $brand-color;
color: $brand-color;
background: $brand-color-light;
}
}
}
.input-wrapper {
max-width: 900px;
margin: 0 auto;
}
.input-card {
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e2e8f0;
overflow: hidden;
transition: all 0.2s;
&:focus-within {
border-color: $brand-color;
background: #fff;
}
}
.input-row {
padding: 12px 16px;
}
.chat-textarea {
width: 100%;
border: none;
outline: none;
resize: none;
font-size: 14px;
color: #374151;
background: transparent;
line-height: 1.5;
min-height: 60px;
max-height: 150px;
font-family: inherit;
&::placeholder {
color: #94a3b8;
}
}
.toolbar-row {
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e2e8f0;
background: #fff;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 4px;
}
.tool-btn {
padding: 8px;
color: #94a3b8;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: $brand-color;
background: $brand-color-light;
}
}
.send-btn {
padding: 8px 16px;
background: #e2e8f0;
color: #94a3b8;
border: none;
border-radius: 8px;
cursor: not-allowed;
transition: all 0.2s;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
&.active {
background: $brand-color;
color: #fff;
cursor: pointer;
box-shadow: 0 2px 8px rgba($brand-color, 0.3);
&:hover {
background: $brand-color-hover;
}
}
}
}

View File

@@ -0,0 +1,233 @@
<template>
<div class="chat-room-main">
<!-- 聊天室头部 -->
<header class="chat-header">
<slot name="header">
<div class="header-default">
<h3>{{ roomName }}</h3>
</div>
</slot>
</header>
<!-- 消息容器 -->
<div ref="messagesRef" class="messages-container">
<!-- Jitsi Meet会议iframe -->
<div v-if="showMeeting && meetingUrl" class="meeting-container">
<IframeView :src="meetingUrl" />
</div>
<!-- 聊天消息列表 -->
<div class="messages-list">
<div
v-for="message in messages"
:key="message.messageId"
class="message-row"
:class="message.senderId === currentUserId ? 'is-me' : 'other'"
>
<!-- 头像 -->
<div class="message-avatar">
<img :src="FILE_DOWNLOAD_URL + message.senderAvatar" />
</div>
<!-- 消息内容 -->
<div class="message-content-wrapper">
<div class="message-bubble">
<p class="message-text">{{ message.content }}</p>
<!-- 文件列表 -->
<div v-if="message.files && message.files.length > 0" class="message-files">
<div
v-for="file in message.files"
:key="file"
class="file-item"
@click="$emit('download-file', file)"
>
<div class="file-icon">
<FileText :size="16" />
</div>
<div class="file-info">
<div class="file-name">附件</div>
</div>
</div>
</div>
</div>
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<footer class="input-area">
<!-- 操作按钮区域 -->
<div class="action-buttons">
<!-- 发起会议按钮始终显示 -->
<button class="action-btn" @click="$emit('start-meeting')">
<Video :size="18" />
发起会议
</button>
<!-- 额外的操作按钮插槽 -->
<slot name="action-area"></slot>
</div>
<!-- 输入框 -->
<div class="input-wrapper">
<div class="input-card">
<div class="input-row">
<textarea
ref="textareaRef"
v-model="inputText"
@input="adjustHeight"
@keydown="handleKeyDown"
placeholder="输入消息..."
class="chat-textarea"
/>
</div>
<div class="toolbar-row">
<div class="toolbar-left">
<button class="tool-btn" @click="selectFiles" title="上传文件">
<Paperclip :size="18" />
</button>
<input
ref="fileInputRef"
type="file"
multiple
style="display: none"
@change="handleFileSelect"
/>
</div>
<button
class="send-btn"
:class="{ active: inputText.trim() }"
:disabled="!inputText.trim()"
@click="sendMessage"
>
<Send :size="18" />
发送
</button>
</div>
</div>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
import IframeView from 'shared/components/iframe/IframeView.vue'
import type { ChatRoomMessageVO } from '@/types/workcase'
interface Props {
messages: ChatRoomMessageVO[]
currentUserId: string
roomName?: string
meetingUrl?: string
showMeeting?: boolean
fileDownloadUrl?: string
}
const props = withDefaults(defineProps<Props>(), {
roomName: '聊天室',
showMeeting: false,
fileDownloadUrl: ''
})
const FILE_DOWNLOAD_URL = props.fileDownloadUrl
const emit = defineEmits<{
'send-message': [content: string, files: File[]]
'start-meeting': []
'download-file': [fileId: string]
}>()
defineSlots<{
header?: () => any
'action-area'?: () => any
}>()
const inputText = ref('')
const selectedFiles = ref<File[]>([])
const messagesRef = ref<HTMLElement | null>(null)
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const fileInputRef = ref<HTMLInputElement | null>(null)
// 发送消息
const sendMessage = () => {
if (!inputText.value.trim() && selectedFiles.value.length === 0) return
emit('send-message', inputText.value.trim(), selectedFiles.value)
inputText.value = ''
selectedFiles.value = []
if (textareaRef.value) {
textareaRef.value.style.height = 'auto'
}
scrollToBottom()
}
// 选择文件
const selectFiles = () => {
fileInputRef.value?.click()
}
// 处理文件选择
const handleFileSelect = (e: Event) => {
const files = (e.target as HTMLInputElement).files
if (!files || files.length === 0) return
selectedFiles.value = Array.from(files)
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
})
}
// 自动调整输入框高度
const adjustHeight = () => {
const el = textareaRef.value
if (el) {
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, 150) + 'px'
}
}
// 键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
sendMessage()
}
}
// 格式化时间
const formatTime = (time?: string) => {
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' })
}
// 暴露方法给父组件
defineExpose({
scrollToBottom
})
</script>
<style scoped lang="scss">
@import url("./ChatRoom.scss");
</style>

View File

@@ -0,0 +1 @@
export { default as ChatRoom } from './chatRoom/ChatRoom.vue';

View File

@@ -0,0 +1 @@
export * from './chatRoom'

View File

@@ -46,40 +46,23 @@ declare module 'shared/components/ai/knowledge/DocumentDetail.vue' {
export default DocumentDetail
}
declare module 'shared/components/chatRoom/ChatRoom.vue' {
import { DefineComponent } from 'vue'
interface ChatMessageVO {
messageId: string
senderId: string
senderName: string
senderAvatar: string
content: string
files: string[]
sendTime: string
}
const ChatRoom: DefineComponent<{
messages: ChatMessageVO[]
currentUserId: string
roomName?: string
meetingUrl?: string
showMeeting?: boolean
fileDownloadUrl?: string
}, {}, {}, {}, {}, {}, {}, {
header?: () => any
'action-area'?: () => any
}>
export default ChatRoom
}
// ========== API 模块 ==========
declare module 'shared/api' {
export const api: any
import type { AxiosResponse, AxiosRequestConfig } from 'axios'
interface ApiInstance {
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
uploadPut<T = any>(url: string, data: FormData, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>
}
export const api: ApiInstance
export const TokenManager: any
export const authAPI: any
export const fileAPI: any
export const workcaseAPI: any
}
declare module 'shared/api/auth' {
@@ -96,18 +79,9 @@ declare module 'shared/api/ai' {
export const aiChatAPI: any
}
declare module 'shared/api/workcase' {
export const workcaseAPI: any
export const workcaseChatAPI: any
}
// ============ types模块 ==================
declare module 'shared/types' {
import type { BaseDTO } from '../../../shared/src/types/base'
// 重新导出 base
export type { BaseDTO }
export type { BaseDTO, BaseVO } from '../../../shared/src/types/base'
// 重新导出 response
export type { ResultDomain } from '../../../shared/src/types/response'
@@ -138,39 +112,6 @@ declare module 'shared/types' {
CommentMessageParams
} from '../../../shared/src/types/ai'
// 重新导出 workcase
export type {
TbWorkcaseDTO,
TbWorkcaseProcessDTO,
TbWorkcaseDeviceDTO,
// 聊天室相关
TbChatRoomDTO,
TbChatRoomMessageDTO,
TbChatRoomMemberDTO,
TbVideoMeetingDTO,
TbMeetingParticipantDTO,
TbMeetingTranscriptionDTO,
ChatRoomVO,
ChatMessageVO,
ChatRoomMessageVO,
ChatMemberVO,
VideoMeetingVO,
MeetingParticipantVO,
SendMessageParam,
CreateMeetingParam,
MarkReadParam,
// 客服相关
TbCustomerServiceDTO,
CustomerServiceVO,
// 词云
TbWordCloudDTO,
// 来客相关
TbGuestDTO,
GuestVO,
CustomerVO,
ConversationVO
} from '../../../shared/src/types/workcase'
// 重新导出 menu
export type { MenuItem, toMenuItem, toMenuItems } from '../../../shared/src/types/menu'
}

View File

@@ -0,0 +1,293 @@
import type { BaseVO, BaseDTO } from 'shared/types'
// ==================== DTO ====================
/**
* 聊天室DTO
*/
export interface TbChatRoomDTO extends BaseDTO {
roomId?: string
workcaseId?: string
roomName?: string
roomType?: string
status?: string
guestId?: string
guestName?: string
aiSessionId?: string
currentAgentId?: string
agentCount?: number
messageCount?: number
unreadCount?: number
lastMessageTime?: string
lastMessage?: string
closedBy?: string
closedTime?: string
}
/**
* 聊天消息DTO
*/
export interface TbChatRoomMessageDTO extends BaseDTO {
messageId?: string
roomId?: string
senderId?: string
senderType?: string
senderName?: string
messageType?: string
content?: string
files?: string[]
contentExtra?: Record<string, any>
replyToMsgId?: string
isAiMessage?: boolean
aiMessageId?: string
status?: string
readCount?: number
sendTime?: string
}
/**
* 聊天室成员DTO
*/
export interface TbChatRoomMemberDTO extends BaseDTO {
memberId?: string
roomId?: string
userId?: string
userType?: string
userName?: string
status?: string
unreadCount?: number
lastReadTime?: string
lastReadMsgId?: string
joinTime?: string
leaveTime?: string
}
/**
* 视频会议DTO
*/
export interface TbVideoMeetingDTO extends BaseDTO {
meetingId?: string
roomId?: string
workcaseId?: string
meetingName?: string
meetingPassword?: string
jwtToken?: string
jitsiRoomName?: string
jitsiServerUrl?: string
status?: string
creatorId?: string
creatorType?: string
creatorName?: string
participantCount?: number
maxParticipants?: number
actualStartTime?: string
actualEndTime?: string
durationSeconds?: number
iframeUrl?: string
config?: Record<string, any>
}
/**
* 会议参与记录DTO
*/
export interface TbMeetingParticipantDTO extends BaseDTO {
participantId?: string
meetingId?: string
userId?: string
userType?: string
userName?: string
joinTime?: string
leaveTime?: string
durationSeconds?: number
isModerator?: boolean
joinMethod?: string
deviceInfo?: string
}
/**
* 会议转录记录表数据对象DTO
*/
export interface TbMeetingTranscriptionDTO extends BaseDTO {
/** 转录记录ID */
transcriptionId?: string
/** 会议ID */
meetingId?: string
/** 说话人ID */
speakerId?: string
/** 说话人名称 */
speakerName?: string
/** 说话人类型guest-来客 agent-客服 */
speakerType?: string
/** 转录文本内容 */
content?: string
/** 原始转录结果 */
contentRaw?: string
/** 语言 */
language?: string
/** 识别置信度0-1 */
confidence?: number
/** 语音开始时间 */
speechStartTime?: string
/** 语音结束时间 */
speechEndTime?: string
/** 语音时长(毫秒) */
durationMs?: number
/** 音频片段URL */
audioUrl?: string
/** 片段序号 */
segmentIndex?: number
/** 是否最终结果 */
isFinal?: boolean
/** 服务提供商 */
serviceProvider?: string
}
// ==================== VO ====================
/**
* 聊天室VO
* 用于前端展示聊天室信息
*/
export interface ChatRoomVO extends BaseVO {
roomId?: string
workcaseId?: string
roomName?: string
roomType?: string
status?: string
guestId?: string
guestName?: string
aiSessionId?: string
currentAgentId?: string
currentAgentName?: string
agentCount?: number
messageCount?: number
unreadCount?: number
lastMessageTime?: string
lastMessage?: string
closedBy?: string
closedByName?: string
closedTime?: string
}
/**
* 聊天消息VO
* 用于前端展示聊天消息
*/
export interface ChatRoomMessageVO extends BaseVO {
messageId?: string
roomId?: string
senderId?: string
senderType?: string
senderName?: string
senderAvatar?: string
messageType?: string
content?: string
files?: string[]
fileCount?: number
contentExtra?: Record<string, any>
replyToMsgId?: string
replyToMsgContent?: string
isAiMessage?: boolean
aiMessageId?: string
status?: string
readCount?: number
sendTime?: string
}
/**
* 聊天室成员VO
* 用于前端展示聊天室成员信息
*/
export interface ChatMemberVO extends BaseVO {
memberId?: string
roomId?: string
userId?: string
userType?: string
userName?: string
userAvatar?: string
status?: string
unreadCount?: number
lastReadTime?: string
lastReadMsgId?: string
joinTime?: string
leaveTime?: string
}
/**
* 视频会议VO
* 用于前端展示Jitsi Meet会议信息
*/
export interface VideoMeetingVO extends BaseVO {
meetingId?: string
roomId?: string
workcaseId?: string
meetingName?: string
meetingPassword?: string
jwtToken?: string
jitsiRoomName?: string
jitsiServerUrl?: string
status?: string
creatorId?: string
creatorType?: string
creatorName?: string
participantCount?: number
maxParticipants?: number
startTime?: string
endTime?: string
durationSeconds?: number
durationFormatted?: string
iframeUrl?: string
config?: Record<string, any>
}
/**
* 会议参与记录VO
* 用于前端展示会议参与者信息
*/
export interface MeetingParticipantVO extends BaseVO {
participantId?: string
meetingId?: string
userId?: string
userType?: string
userName?: string
userAvatar?: string
joinTime?: string
leaveTime?: string
durationSeconds?: number
durationFormatted?: string
isModerator?: boolean
joinMethod?: string
joinMethodName?: string
deviceInfo?: string
isOnline?: boolean
}
/**
* 发送消息参数
*/
export interface SendMessageParam {
roomId: string
content: string
files?: string[]
messageType?: string
replyToMsgId?: string
}
/**
* 创建会议参数
*/
export interface CreateMeetingParam {
roomId: string
workcaseId: string
meetingName?: string
meetingPassword?: string
maxParticipants?: number
}
/**
* 标记已读参数
*/
export interface MarkReadParam {
roomId: string
messageIds?: string[]
}

View File

@@ -0,0 +1,66 @@
import type { BaseVO } from 'shared/types'
// ==================== VO ====================
/**
* 会话VO
* 用于前端展示会话信息
*/
export interface ConversationVO extends BaseVO {
/** 会话ID */
conversationId?: string
/** 客户ID */
customerId?: string
/** 客户姓名 */
customerName?: string
/** 客户头像 */
customerAvatar?: string
/** 会话类型 */
conversationType?: string
/** 会话类型名称 */
conversationTypeName?: string
/** 渠道 */
channel?: string
/** 渠道名称 */
channelName?: string
/** 智能体ID或客服人员ID */
agentId?: string
/** 座席名称 */
agentName?: string
/** 座席类型 */
agentType?: string
/** 座席类型名称 */
agentTypeName?: string
/** 会话开始时间 */
sessionStartTime?: string
/** 会话结束时间 */
sessionEndTime?: string
/** 会话时长(秒) */
durationSeconds?: number
/** 会话时长格式化显示 */
durationFormatted?: string
/** 消息数量 */
messageCount?: number
/** 会话状态 */
conversationStatus?: string
/** 会话状态名称 */
conversationStatusName?: string
/** 会话状态颜色 */
statusColor?: string
/** 满意度评分1-5星 */
satisfactionRating?: number
/** 满意度反馈 */
satisfactionFeedback?: string
/** 会话摘要 */
summary?: string
/** 会话标签 */
tags?: string[]
/** 会话元数据 */
metadata?: Record<string, any>
/** 最后一条消息内容 */
lastMessageContent?: string
/** 最后一条消息时间 */
lastMessageTime?: string
/** 创建者姓名 */
creatorName?: string
}

View File

@@ -0,0 +1,133 @@
import type { BaseDTO, BaseVO } from 'shared/types'
// ==================== DTO ====================
/**
* 客服人员配置表数据对象DTO
*/
export interface TbCustomerServiceDTO extends BaseDTO {
/** 员工ID关联sys用户ID */
userId?: string
/** 员工姓名 */
username?: string
/** 员工工号 */
userCode?: string
/** 状态online-在线 busy-忙碌 offline-离线 */
status?: string
/** 技能标签 */
skillTags?: string[]
/** 最大并发接待数 */
maxConcurrent?: number
/** 当前工作量 */
currentWorkload?: number
/** 累计服务次数 */
totalServed?: number
/** 平均响应时间(秒) */
avgResponseTime?: number
/** 满意度评分0-5 */
satisfactionScore?: number
}
// ==================== VO ====================
/**
* 客服人员配置VO
* 用于前端展示客服人员信息
*/
export interface CustomerServiceVO extends BaseVO {
/** 员工ID关联sys用户ID */
userId?: string
/** 员工姓名 */
username?: string
/** 员工工号 */
userCode?: string
/** 员工头像 */
avatar?: string
/** 状态online-在线 busy-忙碌 offline-离线 */
status?: string
/** 状态名称 */
statusName?: string
/** 技能标签 */
skillTags?: string[]
/** 最大并发接待数 */
maxConcurrent?: number
/** 当前工作量 */
currentWorkload?: number
/** 累计服务次数 */
totalServed?: number
/** 平均响应时间(秒) */
avgResponseTime?: number
/** 平均响应时间(格式化) */
avgResponseTimeFormatted?: string
/** 满意度评分0-5 */
satisfactionScore?: number
/** 是否可接待(工作量未满) */
isAvailable?: boolean
}
/**
* 客户信息VO
* 用于前端展示客户信息
*/
export interface CustomerVO extends BaseVO {
/** 客户ID */
customerId?: string
/** 客户编号 */
customerNo?: string
/** 客户姓名 */
customerName?: string
/** 客户类型 */
customerType?: string
/** 客户类型名称 */
customerTypeName?: string
/** 公司名称 */
companyName?: string
/** 电话 */
phone?: string
/** 邮箱 */
email?: string
/** 微信OpenID */
wechatOpenid?: string
/** 头像URL */
avatar?: string
/** 性别 */
gender?: number
/** 性别名称 */
genderName?: string
/** 地址 */
address?: string
/** 客户等级 */
customerLevel?: string
/** 客户等级名称 */
customerLevelName?: string
/** 客户来源 */
customerSource?: string
/** 客户来源名称 */
customerSourceName?: string
/** 客户标签 */
tags?: string[]
/** 备注 */
notes?: string
/** CRM系统客户ID */
crmCustomerId?: string
/** 最后联系时间 */
lastContactTime?: string
/** 咨询总次数 */
totalConsultations?: number
/** 订单总数 */
totalOrders?: number
/** 总消费金额 */
totalAmount?: number
/** 满意度评分 */
satisfactionScore?: number
/** 状态 */
status?: string
/** 状态名称 */
statusName?: string
/** 状态颜色 */
statusColor?: string
/** 创建者姓名 */
creatorName?: string
/** 更新者姓名 */
updaterName?: string
}

View File

@@ -0,0 +1,5 @@
export * from "./workcase"
export * from "./chatRoom"
export * from "./customer"
export * from "./conversation"
export * from "./wordCloud"

View File

@@ -0,0 +1,19 @@
import type { BaseDTO } from 'shared/types'
// ==================== DTO ====================
/**
* 词云表数据对象DTO
*/
export interface TbWordCloudDTO extends BaseDTO {
/** 词条ID */
wordId?: string
/** 词语 */
word?: string
/** 词频 */
frequency?: string
/** 分类 */
category?: string
/** 统计日期 */
statDate?: string
}

View File

@@ -0,0 +1,65 @@
import type { BaseDTO } from 'shared/types'
/**
* 工单表对象
*/
export interface TbWorkcaseDTO extends BaseDTO {
/** 工单ID */
workcaseId?: string
/** 来客ID */
userId?: string
/** 来客姓名 */
username?: string
/** 来客电话 */
phone?: string
/** 故障类型 */
type?: string
/** 设备名称 */
device?: string
/** 设备代码 */
deviceCode?: string
/** 工单图片列表 */
imgs?: string[]
/** 紧急程度 normal-普通 emergency-紧急 */
emergency?: 'normal' | 'emergency'
/** 状态 pending-待处理 processing-处理中 done-已完成 */
status?: 'pending' | 'processing' | 'done'
/** 处理人ID */
processor?: string
}
/**
* 工单过程表DTO
*/
export interface TbWorkcaseProcessDTO extends BaseDTO {
/** 工单ID */
workcaseId?: string
/** 过程ID */
processId?: string
/** 动作 info记录assign指派redeploy转派repeal撤销finish完成 */
action?: 'info' | 'assign' | 'redeploy' | 'repeal' | 'finish'
/** 消息 */
message?: string
/** 携带文件列表 */
files?: string[]
/** 处理人(指派、转派专属) */
processor?: string
}
/**
* 工单设备涉及的文件DTO
*/
export interface TbWorkcaseDeviceDTO extends BaseDTO {
/** 工单ID */
workcaseId?: string
/** 设备名称 */
device?: string
/** 设备代码 */
deviceCode?: string
/** 文件ID */
fileId?: string
/** 文件名 */
fileName?: string
/** 文件根ID */
fileRootId?: string
}

View File

@@ -27,7 +27,7 @@
:key="room.roomId"
class="room-item"
:class="{ active: currentRoomId === room.roomId }"
@click="selectRoom(room.roomId)"
@click="selectRoom(room.roomId!)"
>
<!-- 头像 -->
<div class="room-avatar">
@@ -44,8 +44,8 @@
</div>
<!-- 未读红点 -->
<div v-if="room.unreadCount > 0" class="unread-badge">
{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}
<div v-if="(room.unreadCount ?? 0) > 0" class="unread-badge">
{{ (room.unreadCount ?? 0) > 99 ? '99+' : room.unreadCount }}
</div>
</div>
</div>
@@ -117,12 +117,12 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
import { Search, FileText, MessageSquare } from 'lucide-vue-next'
import ChatRoom from 'shared/components/chatRoom/ChatRoom.vue'
import { ChatRoom } from '@/components/chatRoom'
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
import { workcaseChatAPI } from 'shared/api/workcase'
import { workcaseChatAPI } from '@/api/workcase'
import { fileAPI } from 'shared/api/file'
import { FILE_DOWNLOAD_URL } from '@/config'
import type { ChatRoomVO, ChatRoomMessageVO, TbChatRoomMessageDTO } from 'shared/types'
import type { ChatRoomVO, ChatRoomMessageVO, TbChatRoomMessageDTO } from '@/types/workcase'
import SockJS from 'sockjs-client'
import { Client } from '@stomp/stompjs'