打分评价

This commit is contained in:
2025-12-29 16:17:27 +08:00
parent 23b4383563
commit a33720b9f6
23 changed files with 880 additions and 72 deletions

View File

@@ -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;
}
}
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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()