打分评价
This commit is contained in:
@@ -30,7 +30,7 @@ DROP TABLE IF EXISTS workcase.tb_chat_room CASCADE;
|
||||
CREATE TABLE workcase.tb_chat_room(
|
||||
optsn VARCHAR(50) NOT NULL, -- 流水号
|
||||
room_id VARCHAR(50) NOT NULL, -- 聊天室ID
|
||||
workcase_id VARCHAR(50) DEFAULT NULL, -- 关联工单ID
|
||||
workcase_id VARCHAR(50) DEFAULT NULL, -- 关联工单ID
|
||||
room_name VARCHAR(200) NOT NULL, -- 聊天室名称(如:工单#12345的客服支持)
|
||||
room_type VARCHAR(20) NOT NULL DEFAULT 'workcase', -- 聊天室类型:workcase-工单客服
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 状态:active-活跃 closed-已关闭 archived-已归档
|
||||
@@ -40,6 +40,7 @@ CREATE TABLE workcase.tb_chat_room(
|
||||
message_count INTEGER NOT NULL DEFAULT 0, -- 消息总数
|
||||
last_message_time TIMESTAMPTZ DEFAULT NULL, -- 最后消息时间
|
||||
last_message TEXT DEFAULT NULL, -- 最后一条消息内容(用于列表展示)
|
||||
comment_level INTEGER DEFAULT 0, -- 服务评分(1-5)
|
||||
closed_by VARCHAR(50) DEFAULT NULL, -- 关闭人
|
||||
closed_time TIMESTAMPTZ DEFAULT NULL, -- 关闭时间
|
||||
creator VARCHAR(50) NOT NULL, -- 创建人(系统自动创建)
|
||||
|
||||
@@ -60,6 +60,9 @@ public class TbChatRoomDTO extends BaseDTO {
|
||||
@Schema(description = "最后一条消息内容")
|
||||
private String lastMessage;
|
||||
|
||||
@Schema(description = "服务评分(1-5星)")
|
||||
private Integer commentLevel;
|
||||
|
||||
@Schema(description = "关闭人")
|
||||
private String closedBy;
|
||||
|
||||
|
||||
@@ -208,4 +208,16 @@ public interface ChatRoomService {
|
||||
*/
|
||||
ResultDomain<CustomerServiceVO> assignCustomerService(String roomId);
|
||||
|
||||
// ========================= 聊天室评分管理 ==========================
|
||||
|
||||
/**
|
||||
* @description 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
* @param userId 评分用户ID
|
||||
* @author cascade
|
||||
* @since 2025-12-29
|
||||
*/
|
||||
ResultDomain<Boolean> submitCommentLevel(String roomId, Integer commentLevel, String userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ public class ChatRoomVO extends BaseVO {
|
||||
|
||||
@Schema(description = "最后一条消息内容")
|
||||
private String lastMessage;
|
||||
|
||||
|
||||
@Schema(description = "服务评分(1-5星)")
|
||||
private Integer commentLevel;
|
||||
|
||||
@Schema(description = "关闭人")
|
||||
private String closedBy;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.xyzh.workcase.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -40,6 +41,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -109,6 +111,31 @@ public class WorkcaseChatContorller {
|
||||
return chatRoomService.closeChatRoom(roomId, closedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
* @return 提交结果
|
||||
*/
|
||||
@Operation(summary = "提交聊天室服务评分")
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@PostMapping("/room/{roomId}/comment")
|
||||
public ResultDomain<Boolean> submitComment(
|
||||
@PathVariable("roomId") String roomId,
|
||||
@RequestBody Map<String, Integer> body) {
|
||||
|
||||
Integer commentLevel = body.get("commentLevel");
|
||||
if (commentLevel == null || commentLevel < 1 || commentLevel > 5) {
|
||||
return ResultDomain.failure("评分必须在1-5之间");
|
||||
}
|
||||
|
||||
// 获取当前登录用户ID
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
// 调用服务层提交评分
|
||||
return chatRoomService.submitCommentLevel(roomId, commentLevel, userId);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取聊天室详情")
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@GetMapping("/room/{roomId}")
|
||||
|
||||
@@ -764,4 +764,60 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
||||
logger.error("发布列表更新到Redis失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= 聊天室评分管理 ==========================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<Boolean> submitCommentLevel(String roomId, Integer commentLevel, String userId) {
|
||||
logger.info("提交聊天室服务评分: roomId={}, commentLevel={}, userId={}", roomId, commentLevel, userId);
|
||||
|
||||
// 参数校验
|
||||
if (NonUtils.isEmpty(roomId)) {
|
||||
return ResultDomain.failure("聊天室ID不能为空");
|
||||
}
|
||||
if (commentLevel == null || commentLevel < 1 || commentLevel > 5) {
|
||||
return ResultDomain.failure("评分必须在1-5星之间");
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 检查聊天室是否存在
|
||||
TbChatRoomDTO chatRoom = chatRoomMapper.selectChatRoomById(roomId);
|
||||
if (chatRoom == null) {
|
||||
return ResultDomain.failure("聊天室不存在");
|
||||
}
|
||||
|
||||
// 2. 检查用户是否是聊天室成员(来客)
|
||||
if (!userId.equals(chatRoom.getGuestId())) {
|
||||
return ResultDomain.failure("只有来客可以对服务进行评分");
|
||||
}
|
||||
|
||||
// 3. 检查是否已评分
|
||||
if (chatRoom.getCommentLevel() != null && chatRoom.getCommentLevel() > 0) {
|
||||
return ResultDomain.failure("已经评分过了,不能重复评分");
|
||||
}
|
||||
|
||||
// 4. 更新评分
|
||||
TbChatRoomDTO updateRoom = new TbChatRoomDTO();
|
||||
updateRoom.setRoomId(roomId);
|
||||
updateRoom.setCommentLevel(commentLevel);
|
||||
|
||||
int rows = chatRoomMapper.updateChatRoom(updateRoom);
|
||||
if (rows > 0) {
|
||||
logger.info("聊天室服务评分成功: roomId={}, commentLevel={}", roomId, commentLevel);
|
||||
|
||||
// TODO: 后续可以在这里更新客服人员的平均满意度评分
|
||||
// updateCustomerServiceSatisfaction(chatRoom, commentLevel);
|
||||
|
||||
return ResultDomain.success("评分成功",true);
|
||||
} else {
|
||||
return ResultDomain.failure("评分提交失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("提交聊天室服务评分失败: roomId={}, commentLevel={}", roomId, commentLevel, e);
|
||||
return ResultDomain.failure("评分提交失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -370,10 +370,38 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
workcaseMapper.updateWorkcase(workcase);
|
||||
}
|
||||
} else if (WorkcaseProcessAction.FINISH.getName().equals(action)) {
|
||||
// 1. 更新工单状态为已完成
|
||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
|
||||
workcase.setStatus("done");
|
||||
workcaseMapper.updateWorkcase(workcase);
|
||||
|
||||
// 2. 发送系统评分消息到聊天室
|
||||
try {
|
||||
TbWorkcaseDTO workcaseData = workcaseMapper.selectWorkcaseById(workcaseProcess.getWorkcaseId());
|
||||
if (workcaseData != null && workcaseData.getRoomId() != null) {
|
||||
// 创建系统评分消息
|
||||
org.xyzh.api.workcase.dto.TbChatRoomMessageDTO commentMessage = new org.xyzh.api.workcase.dto.TbChatRoomMessageDTO();
|
||||
commentMessage.setMessageId(IdUtil.generateUUID());
|
||||
commentMessage.setOptsn(IdUtil.getOptsn());
|
||||
commentMessage.setRoomId(workcaseData.getRoomId());
|
||||
commentMessage.setSenderId("system");
|
||||
commentMessage.setSenderType("system"); // 系统消息
|
||||
commentMessage.setSenderName("系统");
|
||||
commentMessage.setMessageType("comment"); // 评分消息
|
||||
commentMessage.setContent("请为本次服务评分");
|
||||
commentMessage.setStatus("sent");
|
||||
commentMessage.setCreator("system");
|
||||
|
||||
// 发送消息到聊天室
|
||||
chatRoomService.sendMessage(commentMessage);
|
||||
logger.info("工单完成,已发送系统评分消息: workcaseId={}, roomId={}",
|
||||
workcaseProcess.getWorkcaseId(), workcaseData.getRoomId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("发送系统评分消息失败: workcaseId={}", workcaseProcess.getWorkcaseId(), e);
|
||||
// 不影响工单完成流程,只记录错误日志
|
||||
}
|
||||
} else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) {
|
||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<result column="message_count" property="messageCount" jdbcType="INTEGER"/>
|
||||
<result column="last_message_time" property="lastMessageTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="last_message" property="lastMessage" jdbcType="VARCHAR"/>
|
||||
<result column="comment_level" property="commentLevel" jdbcType="INTEGER"/>
|
||||
<result column="closed_by" property="closedBy" jdbcType="VARCHAR"/>
|
||||
<result column="closed_time" property="closedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
@@ -38,6 +39,7 @@
|
||||
<result column="unread_count" property="unreadCount" jdbcType="INTEGER"/>
|
||||
<result column="last_message_time" property="lastMessageTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="last_message" property="lastMessage" jdbcType="VARCHAR"/>
|
||||
<result column="comment_level" property="commentLevel" jdbcType="INTEGER"/>
|
||||
<result column="closed_by" property="closedBy" jdbcType="VARCHAR"/>
|
||||
<result column="closed_time" property="closedTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="creator" property="creator" jdbcType="VARCHAR"/>
|
||||
@@ -49,7 +51,7 @@
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
room_id, optsn, workcase_id, room_name, room_type, status, guest_id, guest_name,
|
||||
ai_session_id, message_count, last_message_time, last_message, closed_by, closed_time,
|
||||
ai_session_id, message_count, last_message_time, last_message, comment_level, closed_by, closed_time,
|
||||
creator, create_time, update_time, delete_time, deleted
|
||||
</sql>
|
||||
|
||||
@@ -84,6 +86,7 @@
|
||||
<if test="messageCount != null">message_count = #{messageCount},</if>
|
||||
<if test="lastMessageTime != null">last_message_time = #{lastMessageTime},</if>
|
||||
<if test="lastMessage != null">last_message = #{lastMessage},</if>
|
||||
<if test="commentLevel != null">comment_level = #{commentLevel},</if>
|
||||
<if test="closedBy != null">closed_by = #{closedBy},</if>
|
||||
<if test="closedTime != null">closed_time = #{closedTime},</if>
|
||||
update_time = now()
|
||||
@@ -120,9 +123,9 @@
|
||||
</select>
|
||||
|
||||
<select id="selectChatRoomPage" resultMap="VOResultMap">
|
||||
SELECT r.room_id, r.optsn, r.workcase_id, r.room_name, r.room_type, r.status,
|
||||
r.guest_id, r.guest_name, r.ai_session_id, r.message_count,
|
||||
r.last_message_time, r.last_message, r.closed_by, r.closed_time,
|
||||
SELECT r.room_id, r.optsn, r.workcase_id, r.room_name, r.room_type, r.status,
|
||||
r.guest_id, r.guest_name, r.ai_session_id, r.message_count,
|
||||
r.last_message_time, r.last_message, r.comment_level, r.closed_by, r.closed_time,
|
||||
r.creator, r.create_time, r.update_time, r.delete_time, r.deleted,
|
||||
COALESCE(m.unread_count, 0) as unread_count
|
||||
FROM workcase.tb_chat_room r
|
||||
|
||||
@@ -272,5 +272,19 @@ export const workcaseChatAPI = {
|
||||
async endVideoMeeting(meetingId: string): Promise<ResultDomain<VideoMeetingVO>> {
|
||||
const response = await api.post<VideoMeetingVO>(`${this.baseUrl}/meeting/${meetingId}/end`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 聊天室评分管理 ======================
|
||||
|
||||
/**
|
||||
* 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
*/
|
||||
async submitComment(roomId: string, commentLevel: number): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>(`/urban-lifeline/workcase/chat/room/${roomId}/comment`, null, {
|
||||
params: { commentLevel }
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface TbChatRoomDTO extends BaseDTO {
|
||||
agentCount?: number
|
||||
messageCount?: number
|
||||
unreadCount?: number
|
||||
commentLevel?: number
|
||||
lastMessageTime?: string
|
||||
lastMessage?: string
|
||||
closedBy?: string
|
||||
@@ -169,6 +170,7 @@ export interface ChatRoomVO extends BaseVO {
|
||||
agentCount?: number
|
||||
messageCount?: number
|
||||
unreadCount?: number
|
||||
commentLevel?: number
|
||||
lastMessageTime?: string
|
||||
lastMessage?: string
|
||||
closedBy?: string
|
||||
|
||||
@@ -94,11 +94,14 @@
|
||||
:room-name="currentRoom?.roomName"
|
||||
:file-download-url="FILE_DOWNLOAD_URL"
|
||||
:has-more="hasMore"
|
||||
:guest-id="currentRoom?.guestId"
|
||||
:comment-level="currentRoom?.commentLevel"
|
||||
:loading-more="loadingMore"
|
||||
@send-message="handleSendMessage"
|
||||
@start-meeting="startMeeting"
|
||||
@start-meeting="startMeeting()"
|
||||
@download-file="downloadFile"
|
||||
@load-more="loadMoreMessages"
|
||||
@submit-comment="handleSubmitComment"
|
||||
>
|
||||
<template #header>
|
||||
<div class="chat-room-header">
|
||||
@@ -171,6 +174,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
|
||||
import { Search, FileText, MessageSquare, MessageCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||
import ChatRoom from './chatRoom/ChatRoom.vue'
|
||||
@@ -185,12 +189,12 @@ import { Client } from '@stomp/stompjs'
|
||||
// WebSocket配置 (通过Nginx代理访问网关,再到workcase服务)
|
||||
// SockJS URL (http://)
|
||||
const getWsUrl = () => {
|
||||
const token = JSON.parse(localStorage.getItem('token')).value || ''
|
||||
const token = JSON.parse(localStorage.getItem('token') || '').value || ''
|
||||
const protocol = window.location.protocol
|
||||
const host = window.location.host
|
||||
return `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}`
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
// STOMP客户端
|
||||
let stompClient: any = null
|
||||
let roomSubscription: any = null
|
||||
@@ -488,6 +492,23 @@ const handleSendMessage = async (content: string, files: File[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理评分提交(从ChatRoom组件触发)
|
||||
const handleSubmitComment = async (rating: number) => {
|
||||
if (!currentRoomId.value) return
|
||||
|
||||
try {
|
||||
const result = await workcaseChatAPI.submitComment(currentRoomId.value, rating)
|
||||
if (result.success) {
|
||||
ElMessage.success('感谢您的评分!')
|
||||
} else {
|
||||
ElMessage.error(result.message || '评分提交失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('评分提交失败:', error)
|
||||
ElMessage.error('评分提交失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (fileId: string) => {
|
||||
window.open(`${FILE_DOWNLOAD_URL}/${fileId}`, '_blank')
|
||||
@@ -525,11 +546,11 @@ const startMeeting = async () => {
|
||||
const activeResult = await workcaseChatAPI.getActiveMeeting(currentRoomId.value)
|
||||
if (activeResult.success && activeResult.data) {
|
||||
// 已有活跃会议,直接加入
|
||||
currentMeetingId.value = activeResult.data.meetingId!
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId.value!)
|
||||
const currentMeetingId = activeResult.data.meetingId!
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId)
|
||||
if (joinResult.success && joinResult.data?.iframeUrl) {
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId.value}`
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId}`
|
||||
router.push(meetingUrl)
|
||||
} else {
|
||||
ElMessage.error(joinResult.message || '加入会议失败')
|
||||
@@ -544,13 +565,13 @@ const startMeeting = async () => {
|
||||
})
|
||||
|
||||
if (createResult.success && createResult.data) {
|
||||
currentMeetingId.value = createResult.data.meetingId!
|
||||
const currentMeetingId = createResult.data.meetingId!
|
||||
|
||||
// 开始会议
|
||||
await workcaseChatAPI.startVideoMeeting(currentMeetingId.value!)
|
||||
await workcaseChatAPI.startVideoMeeting(currentMeetingId)
|
||||
|
||||
// 加入会议获取会议页面URL
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId.value!)
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId)
|
||||
if (joinResult.success && joinResult.data?.iframeUrl) {
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId.value}`
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import url('./CommentMessageCard.scss');
|
||||
</style>
|
||||
@@ -137,6 +137,35 @@ $brand-color-hover: #004488;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
// 系统消息样式(居中显示)
|
||||
&.is-system {
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.system-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 80%;
|
||||
|
||||
.system-message-text {
|
||||
padding: 8px 16px;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
|
||||
@@ -21,53 +21,76 @@
|
||||
v-for="message in messages"
|
||||
:key="message.messageId"
|
||||
class="message-row"
|
||||
:class="message.senderId === currentUserId ? 'is-me' : 'other'"
|
||||
:class="getMessageClass(message)"
|
||||
>
|
||||
<div>
|
||||
<!-- 头像 -->
|
||||
<div class="message-avatar">
|
||||
<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 class="message-content-wrapper">
|
||||
<!-- 会议消息卡片 -->
|
||||
<template v-if="message.messageType === 'meet'">
|
||||
<MeetingCard :meetingId="getMeetingId(message.contentExtra)" @join="handleJoinMeeting" />
|
||||
<!-- 系统消息(居中显示) -->
|
||||
<template v-if="message.senderType === 'system'">
|
||||
<div class="system-message-container">
|
||||
<!-- 评分消息卡片 -->
|
||||
<template v-if="message.messageType === 'comment'">
|
||||
<CommentMessageCard
|
||||
:room-id="roomId"
|
||||
:can-comment="canComment"
|
||||
:initial-rating="commentLevel"
|
||||
@submit="handleCommentSubmit"
|
||||
/>
|
||||
</template>
|
||||
<!-- 其他系统消息 -->
|
||||
<template v-else>
|
||||
<div class="system-message-text">{{ message.content }}</div>
|
||||
</template>
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 普通消息气泡 -->
|
||||
<template v-else>
|
||||
<div class="message-bubble">
|
||||
<div
|
||||
class="message-text"
|
||||
v-html="renderMarkdown(message.content || '')"
|
||||
></div>
|
||||
<!-- 普通用户/客服消息 -->
|
||||
<template v-else>
|
||||
<div>
|
||||
<!-- 头像 -->
|
||||
<div class="message-avatar">
|
||||
<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 v-if="message.files && message.files.length > 0" class="message-files">
|
||||
<!-- 消息内容 -->
|
||||
<div class="message-content-wrapper">
|
||||
<!-- 会议消息卡片 -->
|
||||
<template v-if="message.messageType === 'meet'">
|
||||
<MeetingCard :meetingId="getMeetingId(message.contentExtra)" @join="handleJoinMeeting" />
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</template>
|
||||
|
||||
<!-- 普通消息气泡 -->
|
||||
<template v-else>
|
||||
<div class="message-bubble">
|
||||
<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>
|
||||
class="message-text"
|
||||
v-html="renderMarkdown(message.content || '')"
|
||||
></div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<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>
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="message-time">{{ formatTime(message.sendTime) }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,15 +164,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ref, nextTick, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
|
||||
import MeetingCreate from '../MeetingCreate/MeetingCreate.vue'
|
||||
import MeetingCard from '../MeetingCard/MeetingCard.vue'
|
||||
import CommentMessageCard from './CommentMessageCard/CommentMessageCard.vue'
|
||||
import type { ChatRoomMessageVO, VideoMeetingVO } from '@/types/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
const router = useRouter()
|
||||
|
||||
interface Props {
|
||||
@@ -158,25 +182,31 @@ interface Props {
|
||||
roomId: string
|
||||
roomName?: string
|
||||
workcaseId?: string
|
||||
fileDownloadUrl?: string
|
||||
commentLevel?: number
|
||||
hasMore?: boolean
|
||||
loadingMore?: boolean
|
||||
guestId?: string // 聊天室访客ID,用于判断评价权限
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
roomName: '聊天室',
|
||||
fileDownloadUrl: '',
|
||||
hasMore: true,
|
||||
loadingMore: false
|
||||
loadingMore: false,
|
||||
guestId: ''
|
||||
})
|
||||
|
||||
const FILE_DOWNLOAD_URL = props.fileDownloadUrl
|
||||
|
||||
// 计算当前用户是否可以评价(只有访客可以评价)
|
||||
const canComment = computed(() => {
|
||||
return props.currentUserId === props.guestId
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'send-message': [content: string, files: File[]]
|
||||
'download-file': [fileId: string]
|
||||
'load-more': []
|
||||
'start-meeting': []
|
||||
'submit-comment': [rating: number]
|
||||
}>()
|
||||
|
||||
// 会议相关状态
|
||||
@@ -333,6 +363,25 @@ function getMeetingId(contentExtra: Record<string, any> | undefined): string {
|
||||
return contentExtra.meetingId as string
|
||||
}
|
||||
|
||||
// 获取消息的CSS类
|
||||
const getMessageClass = (message: ChatRoomMessageVO) => {
|
||||
if (message.senderType === 'system') {
|
||||
return 'is-system'
|
||||
}
|
||||
return message.senderId === props.currentUserId ? 'is-me' : 'other'
|
||||
}
|
||||
|
||||
// 处理评分提交
|
||||
const handleCommentSubmit = async (rating: number) => {
|
||||
try {
|
||||
emit('submit-comment', rating)
|
||||
ElMessage.success('感谢您的评分!')
|
||||
} catch (error) {
|
||||
console.error('提交评分失败:', error)
|
||||
ElMessage.error('提交评分失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown渲染函数
|
||||
const renderMarkdown = (text: string): string => {
|
||||
if (!text) return ''
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
.comment-message-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
max-width: 360px;
|
||||
margin: 0 auto;
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
|
||||
.comment-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.star-item {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-desc {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submitted-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-permission {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 10px 28px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 24px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #667eea;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="comment-message-card">
|
||||
<div class="comment-header">
|
||||
<span class="comment-title">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="comment-body">
|
||||
<!-- 星级评分 -->
|
||||
<div class="star-rating">
|
||||
<div
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
class="star-item"
|
||||
:class="{
|
||||
'is-active': star <= currentRating,
|
||||
'is-disabled': !canComment || isSubmitted
|
||||
}"
|
||||
@click="handleStarClick(star)"
|
||||
@mouseenter="handleStarHover(star)"
|
||||
@mouseleave="handleStarLeave"
|
||||
>
|
||||
<Star
|
||||
:size="28"
|
||||
:fill="star <= (hoverRating || currentRating) ? '#FFD700' : 'none'"
|
||||
:stroke="star <= (hoverRating || currentRating) ? '#FFD700' : '#d0d0d0'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评分描述 -->
|
||||
<div v-if="currentRating > 0" class="rating-desc">
|
||||
{{ getRatingDesc(currentRating) }}
|
||||
</div>
|
||||
|
||||
<!-- 不可评价提示 -->
|
||||
<div v-if="!canComment && !isSubmitted" class="no-permission">
|
||||
仅访客可评价
|
||||
</div>
|
||||
|
||||
<!-- 已评分状态 -->
|
||||
<div v-else-if="isSubmitted" class="submitted-status">
|
||||
<Check :size="16" />
|
||||
已评分
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
v-else-if="canComment"
|
||||
class="submit-btn"
|
||||
:class="{ 'is-active': currentRating > 0 }"
|
||||
:disabled="currentRating === 0 || submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitting ? '提交中...' : '提交评分' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Star, Check } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
initialRating?: number
|
||||
canComment?: boolean // 是否可以评价(当前用户是否为guestId)
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialRating: 0,
|
||||
canComment: false,
|
||||
title: '请为本次服务评分'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'submit': [rating: number]
|
||||
}>()
|
||||
|
||||
const currentRating = ref(props.initialRating)
|
||||
const hoverRating = ref(0)
|
||||
const submitting = ref(false)
|
||||
const isSubmitted = ref(props.initialRating > 0)
|
||||
|
||||
// 监听 initialRating 变化
|
||||
watch(() => props.initialRating, (newVal) => {
|
||||
currentRating.value = newVal
|
||||
isSubmitted.value = newVal > 0
|
||||
})
|
||||
|
||||
// 星级描述映射
|
||||
const ratingDescriptions: Record<number, string> = {
|
||||
1: '非常不满意',
|
||||
2: '不满意',
|
||||
3: '一般',
|
||||
4: '满意',
|
||||
5: '非常满意'
|
||||
}
|
||||
|
||||
const getRatingDesc = (rating: number): string => {
|
||||
return ratingDescriptions[rating] || ''
|
||||
}
|
||||
|
||||
const handleStarClick = (star: number) => {
|
||||
if (!props.canComment || isSubmitted.value) return
|
||||
currentRating.value = star
|
||||
}
|
||||
|
||||
const handleStarHover = (star: number) => {
|
||||
if (!props.canComment || isSubmitted.value) return
|
||||
hoverRating.value = star
|
||||
}
|
||||
|
||||
const handleStarLeave = () => {
|
||||
hoverRating.value = 0
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (currentRating.value === 0 || !props.canComment || isSubmitted.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
emit('submit', currentRating.value)
|
||||
isSubmitted.value = true
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url('./CommentMessageCard.scss');
|
||||
</style>
|
||||
@@ -263,5 +263,20 @@ export const workcaseChatAPI = {
|
||||
*/
|
||||
endVideoMeeting(meetingId: string): Promise<ResultDomain<any>> {
|
||||
return request({ url: `${this.baseUrl}/meeting/${meetingId}/end`, method: 'POST' })
|
||||
},
|
||||
|
||||
// ====================== 聊天室评分管理 ======================
|
||||
|
||||
/**
|
||||
* 提交聊天室服务评分
|
||||
* @param roomId 聊天室ID
|
||||
* @param commentLevel 评分(1-5星)
|
||||
*/
|
||||
submitComment(roomId: string, commentLevel: number): Promise<ResultDomain<boolean>> {
|
||||
return request({
|
||||
url: `/urban-lifeline/workcase/chat/room/${roomId}/comment`,
|
||||
method: 'POST',
|
||||
data: { commentLevel }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
.comment-message-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
|
||||
max-width: 600rpx;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.comment-header {
|
||||
margin-bottom: 32rpx;
|
||||
text-align: center;
|
||||
|
||||
.comment-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
|
||||
.star-item {
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:active:not(.is-disabled) {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
|
||||
&.star-filled {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
&.star-empty {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-desc {
|
||||
.rating-desc-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.submitted-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 40rpx;
|
||||
|
||||
.submitted-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.no-permission {
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 40rpx;
|
||||
|
||||
.no-permission-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 20rpx 56rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 48rpx;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.is-active {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: transparent;
|
||||
|
||||
.submit-btn-text {
|
||||
color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.submit-btn-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<view class="comment-message-card">
|
||||
<view class="comment-header">
|
||||
<text class="comment-title">{{ title }}</text>
|
||||
</view>
|
||||
|
||||
<view class="comment-body">
|
||||
<!-- 星级评分 -->
|
||||
<view class="star-rating">
|
||||
<view
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
class="star-item"
|
||||
:class="{
|
||||
'is-active': star <= currentRating,
|
||||
'is-disabled': !canComment || isSubmitted
|
||||
}"
|
||||
@tap="handleStarClick(star)"
|
||||
>
|
||||
<text class="star-icon" :class="star <= currentRating ? 'star-filled' : 'star-empty'">★</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评分描述 -->
|
||||
<view v-if="currentRating > 0" class="rating-desc">
|
||||
<text class="rating-desc-text">{{ getRatingDesc(currentRating) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 不可评价提示 -->
|
||||
<view v-if="!canComment && !isSubmitted" class="no-permission">
|
||||
<text class="no-permission-text">仅访客可评价</text>
|
||||
</view>
|
||||
|
||||
<!-- 已评分状态 -->
|
||||
<view v-else-if="isSubmitted" class="submitted-status">
|
||||
<text class="submitted-text">✓ 已评分</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button
|
||||
v-else-if="canComment"
|
||||
class="submit-btn"
|
||||
:class="{ 'is-active': currentRating > 0 }"
|
||||
:disabled="currentRating === 0 || submitting"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
<text class="submit-btn-text">{{ submitting ? '提交中...' : '提交评分' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
initialRating?: number
|
||||
canComment?: boolean // 是否可以评价(当前用户是否为guestId)
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialRating: 0,
|
||||
canComment: false,
|
||||
title: '请为本次服务评分'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [rating: number]
|
||||
}>()
|
||||
|
||||
const currentRating = ref(props.initialRating)
|
||||
const submitting = ref(false)
|
||||
const isSubmitted = ref(props.initialRating > 0)
|
||||
|
||||
// 监听 initialRating 变化
|
||||
watch(() => props.initialRating, (newVal) => {
|
||||
currentRating.value = newVal
|
||||
isSubmitted.value = newVal > 0
|
||||
})
|
||||
|
||||
// 星级描述映射
|
||||
const ratingDescriptions = {
|
||||
1: '非常不满意',
|
||||
2: '不满意',
|
||||
3: '一般',
|
||||
4: '满意',
|
||||
5: '非常满意'
|
||||
}
|
||||
|
||||
const getRatingDesc = (rating: number): string => {
|
||||
return ratingDescriptions[rating as keyof typeof ratingDescriptions] || ''
|
||||
}
|
||||
|
||||
const handleStarClick = (star: number) => {
|
||||
if (!props.canComment || isSubmitted.value) return
|
||||
currentRating.value = star
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (currentRating.value === 0 || !props.canComment || isSubmitted.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
emit('submit', currentRating.value)
|
||||
isSubmitted.value = true
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './CommentMessageCard.scss';
|
||||
</style>
|
||||
@@ -461,4 +461,26 @@
|
||||
.send-icon {
|
||||
font-size: 36rpx;
|
||||
color: #4b87ff;
|
||||
}
|
||||
}
|
||||
// ==================== 系统消息样式 ====================
|
||||
.system-row {
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.system-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.system-message-text {
|
||||
padding: 16rpx 32rpx;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border-radius: 32rpx;
|
||||
font-size: 26rpx;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -54,8 +54,31 @@
|
||||
<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-item" v-for="msg in messages" :key="msg.messageId">
|
||||
<!-- 系统消息(居中显示) -->
|
||||
<view class="message-row system-row" v-if="msg.senderType === 'system'">
|
||||
<view class="system-message-container">
|
||||
<!-- 评分消息卡片 -->
|
||||
<template v-if="msg.messageType === 'comment'">
|
||||
<CommentMessageCard
|
||||
:room-id="roomId"
|
||||
:can-comment="getCanComment()"
|
||||
:initial-rating="commentLevel"
|
||||
@submit="handleCommentSubmit"
|
||||
/>
|
||||
</template>
|
||||
<!-- 其他系统消息 -->
|
||||
<template v-else>
|
||||
<view class="system-message-text">
|
||||
<text>{{ msg.content }}</text>
|
||||
</view>
|
||||
</template>
|
||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 普通用户/客服消息 -->
|
||||
<view v-else :class="msg.senderType === 'guest' ? 'self' : 'other'">
|
||||
<!-- 对方消息(左侧) -->
|
||||
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
|
||||
<view>
|
||||
@@ -95,6 +118,7 @@
|
||||
<text class="avatar-text">我</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -120,6 +144,7 @@
|
||||
import { ref, reactive, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import MeetingCard from '../../meeting/meetingCard/MeetingCard.uvue'
|
||||
import CommentMessageCard from './CommentMessageCard/CommentMessageCard.uvue'
|
||||
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO, VideoMeetingVO } from '@/types/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
@@ -130,6 +155,8 @@ const headerTotalHeight = ref<number>(88)
|
||||
const roomId = ref<string>('')
|
||||
const workcaseId = ref<string>('')
|
||||
const roomName = ref<string>('聊天室')
|
||||
const guestId = ref<string>('') // 聊天室访客ID
|
||||
const commentLevel = ref<number>(0) // 已有评分
|
||||
const inputText = ref<string>('')
|
||||
const scrollTop = ref<number>(0)
|
||||
const loading = ref<boolean>(false)
|
||||
@@ -211,6 +238,9 @@ const totalMembers = computed<MemberDisplay[]>(() => {
|
||||
return Array.from(memberMap.values())
|
||||
})
|
||||
|
||||
function getCanComment(): boolean {
|
||||
return currentUserId.value === guestId.value
|
||||
}
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
@@ -280,6 +310,8 @@ async function refreshChatRoomInfo() {
|
||||
if (roomRes.success && roomRes.data) {
|
||||
roomName.value = roomRes.data.roomName || '聊天室'
|
||||
workcaseId.value = roomRes.data.workcaseId || ''
|
||||
guestId.value = roomRes.data.guestId || ''
|
||||
commentLevel.value = roomRes.data.commentLevel || 0
|
||||
messageTotal.value = roomRes.data.messageCount || 0
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -311,7 +343,9 @@ async function loadChatRoom() {
|
||||
if (roomRes.success && roomRes.data) {
|
||||
roomName.value = roomRes.data.roomName || '聊天室'
|
||||
workcaseId.value = roomRes.data.workcaseId || ''
|
||||
guestId.value = roomRes.data.guestId || ''
|
||||
messageTotal.value = roomRes.data.messageCount || 0
|
||||
commentLevel.value = roomRes.data.commentLevel!
|
||||
}
|
||||
// 后端是降序查询,page1是最新消息
|
||||
currentPage.value = 1
|
||||
@@ -644,6 +678,31 @@ async function handleJoinMeeting(meetingId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理评分提交
|
||||
async function handleCommentSubmit(rating: number) {
|
||||
console.log('[handleCommentSubmit] 提交评分:', rating)
|
||||
try {
|
||||
const result = await workcaseChatAPI.submitComment(roomId.value, rating)
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '感谢您的评分!',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '评分提交失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[handleCommentSubmit] 评分提交失败:', error)
|
||||
uni.showToast({
|
||||
title: '评分提交失败,请稍后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface TbChatRoomDTO extends BaseDTO {
|
||||
roomType?: string
|
||||
status?: string
|
||||
guestId?: string
|
||||
commentLevel?: number
|
||||
guestName?: string
|
||||
aiSessionId?: string
|
||||
currentAgentId?: string
|
||||
@@ -163,6 +164,7 @@ export interface ChatRoomVO extends BaseVO {
|
||||
roomType?: string
|
||||
status?: string
|
||||
guestId?: string
|
||||
commentLevel?: string
|
||||
guestName?: string
|
||||
aiSessionId?: string
|
||||
currentAgentId?: string
|
||||
|
||||
Reference in New Issue
Block a user