342 lines
7.9 KiB
Vue
342 lines
7.9 KiB
Vue
<!--
|
||
企微IT智能服务台 — H5用户端满意度评价弹窗
|
||
说明:会话结束后弹出的满意度评价对话框
|
||
功能:
|
||
1. 5星评分(必选)
|
||
2. 表情选择:满意/一般/不满意(必选)
|
||
3. 文字反馈输入框(可选,限200字)
|
||
4. 提交评价
|
||
-->
|
||
|
||
<template>
|
||
<van-popup
|
||
v-model:show="visible"
|
||
round
|
||
:close-on-click-overlay="false"
|
||
class="evaluation-popup"
|
||
>
|
||
<div class="evaluation-dialog">
|
||
<div class="evaluation-dialog__header">
|
||
<h3 class="evaluation-dialog__title">请对本次服务进行评价</h3>
|
||
<p class="evaluation-dialog__subtitle">您的评价对我们非常重要</p>
|
||
</div>
|
||
|
||
<div class="evaluation-dialog__section">
|
||
<div class="evaluation-dialog__label">服务评分</div>
|
||
<div class="star-rating">
|
||
<van-rate
|
||
v-model="formData.star_rating"
|
||
:count="5"
|
||
size="32"
|
||
color="#FFD21E"
|
||
void-color="#E5E5E5"
|
||
@change="handleStarChange"
|
||
/>
|
||
<span class="star-label">{{ starLabel }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="evaluation-dialog__section">
|
||
<div class="evaluation-dialog__label">整体感受</div>
|
||
<div class="emoji-selector">
|
||
<div class="emoji-item" :class="{ 'emoji-item--selected': formData.emoji === 'satisfied' }" @click="selectEmoji('satisfied')">
|
||
<span class="emoji-icon">😀</span>
|
||
<span class="emoji-text">满意</span>
|
||
</div>
|
||
<div class="emoji-item" :class="{ 'emoji-item--selected': formData.emoji === 'neutral' }" @click="selectEmoji('neutral')">
|
||
<span class="emoji-icon">😐</span>
|
||
<span class="emoji-text">一般</span>
|
||
</div>
|
||
<div class="emoji-item" :class="{ 'emoji-item--selected': formData.emoji === 'dissatisfied' }" @click="selectEmoji('dissatisfied')">
|
||
<span class="emoji-icon">😞</span>
|
||
<span class="emoji-text">不满意</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="evaluation-dialog__section">
|
||
<div class="evaluation-dialog__label">
|
||
改进建议
|
||
<span class="evaluation-dialog__optional">(选填)</span>
|
||
</div>
|
||
<van-field
|
||
v-model="formData.feedback_text"
|
||
type="textarea"
|
||
placeholder="您对本次服务有什么建议?"
|
||
maxlength="200"
|
||
show-word-limit
|
||
rows="3"
|
||
class="evaluation-dialog__textarea"
|
||
/>
|
||
</div>
|
||
|
||
<div class="evaluation-dialog__actions">
|
||
<van-button
|
||
type="primary"
|
||
size="large"
|
||
:loading="submitting"
|
||
:disabled="!canSubmit"
|
||
@click="handleSubmit"
|
||
>
|
||
提交评价
|
||
</van-button>
|
||
</div>
|
||
|
||
<div class="evaluation-dialog__skip">
|
||
<van-button size="small" type="default" :disabled="submitting" @click="handleSkip">
|
||
暂不评价
|
||
</van-button>
|
||
</div>
|
||
</div>
|
||
</van-popup>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import { showToast } from 'vant'
|
||
import { submitEvaluation, getEvaluation } from '@/api/conversation'
|
||
|
||
interface Props {
|
||
modelValue: boolean
|
||
conversationId: string
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
modelValue: false,
|
||
conversationId: '',
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', value: boolean): void
|
||
(e: 'submitted'): void
|
||
}>()
|
||
|
||
const visible = computed({
|
||
get: () => props.modelValue,
|
||
set: (val) => emit('update:modelValue', val),
|
||
})
|
||
|
||
const formData = ref({
|
||
star_rating: 0,
|
||
emoji: '',
|
||
feedback_text: '',
|
||
})
|
||
|
||
const submitting = ref(false)
|
||
|
||
const starLabel = computed(() => {
|
||
const labels: Record<number, string> = {
|
||
0: '',
|
||
1: '非常差',
|
||
2: '较差',
|
||
3: '一般',
|
||
4: '满意',
|
||
5: '非常满意',
|
||
}
|
||
return labels[formData.value.star_rating] || ''
|
||
})
|
||
|
||
const canSubmit = computed(() => {
|
||
return formData.value.star_rating > 0 && formData.value.emoji !== ''
|
||
})
|
||
|
||
watch(visible, async (val) => {
|
||
if (val && props.conversationId) {
|
||
try {
|
||
const evaluation = await getEvaluation(props.conversationId)
|
||
if (evaluation) {
|
||
visible.value = false
|
||
showToast('您已评价过该会话')
|
||
}
|
||
} catch (error) {
|
||
console.warn('[EvaluationDialog] 检查评价状态失败:', error)
|
||
}
|
||
}
|
||
})
|
||
|
||
function selectEmoji(emoji: string): void {
|
||
formData.value.emoji = emoji
|
||
}
|
||
|
||
function handleStarChange(value: number): void {
|
||
if (value >= 4) {
|
||
formData.value.emoji = 'satisfied'
|
||
} else if (value >= 2) {
|
||
formData.value.emoji = 'neutral'
|
||
} else {
|
||
formData.value.emoji = 'dissatisfied'
|
||
}
|
||
}
|
||
|
||
async function handleSubmit(): Promise<void> {
|
||
if (!canSubmit.value || submitting.value) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
await submitEvaluation(props.conversationId, {
|
||
star_rating: formData.value.star_rating,
|
||
emoji: formData.value.emoji,
|
||
feedback_text: formData.value.feedback_text || undefined,
|
||
})
|
||
|
||
showToast('评价成功,感谢您的反馈!')
|
||
visible.value = false
|
||
emit('submitted')
|
||
} catch (error: any) {
|
||
console.error('[EvaluationDialog] 提交评价失败:', error)
|
||
// 修复:H5 拦截器已将错误对象格式化为 {code, message},
|
||
// 不再有 error.response.data.message 结构。
|
||
// 拦截器已在 showToast 中显示具体错误消息,此处不再重复。
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
function handleSkip(): void {
|
||
visible.value = false
|
||
}
|
||
|
||
function resetForm(): void {
|
||
formData.value = {
|
||
star_rating: 0,
|
||
emoji: '',
|
||
feedback_text: '',
|
||
}
|
||
submitting.value = false
|
||
}
|
||
|
||
defineExpose({
|
||
resetForm,
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.evaluation-popup {
|
||
width: 90%;
|
||
max-width: 360px;
|
||
}
|
||
|
||
.evaluation-dialog {
|
||
padding: 24px 20px 20px;
|
||
}
|
||
|
||
.evaluation-dialog__header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.evaluation-dialog__title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--text-primary, #333);
|
||
margin: 0 0 6px;
|
||
}
|
||
|
||
.evaluation-dialog__subtitle {
|
||
font-size: 13px;
|
||
color: var(--text-tertiary, #999);
|
||
margin: 0;
|
||
}
|
||
|
||
.evaluation-dialog__section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.evaluation-dialog__label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text-primary, #333);
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.evaluation-dialog__optional {
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: var(--text-tertiary, #999);
|
||
margin-left: 6px;
|
||
}
|
||
|
||
.star-rating {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.star-label {
|
||
font-size: 14px;
|
||
color: var(--text-secondary, #666);
|
||
min-width: 48px;
|
||
}
|
||
|
||
.emoji-selector {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.emoji-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 12px 8px;
|
||
background: var(--bg-tertiary, #f5f5f5);
|
||
border-radius: 12px;
|
||
border: 2px solid transparent;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.emoji-item:hover {
|
||
background: var(--bg-secondary, #f0f0f0);
|
||
}
|
||
|
||
.emoji-item--selected {
|
||
background: var(--accent-soft, rgba(7, 193, 96, 0.1));
|
||
border-color: var(--accent, #07C160);
|
||
}
|
||
|
||
.emoji-icon {
|
||
font-size: 28px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.emoji-text {
|
||
font-size: 12px;
|
||
color: var(--text-secondary, #666);
|
||
}
|
||
|
||
.emoji-item--selected .emoji-text {
|
||
color: var(--accent, #07C160);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.evaluation-dialog__textarea {
|
||
background: var(--bg-tertiary, #f5f5f5);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.evaluation-dialog__actions {
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.evaluation-dialog__actions .van-button--primary {
|
||
background: var(--accent, #07C160);
|
||
border-color: var(--accent, #07C160);
|
||
}
|
||
|
||
.evaluation-dialog__skip {
|
||
margin-top: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.evaluation-dialog__skip .van-button--default {
|
||
color: var(--text-tertiary, #999);
|
||
border: none;
|
||
background: transparent;
|
||
}
|
||
</style>
|