Files
012-kaopeilian/frontend/src/views/trainee/duo-practice-report.vue
yuliang_guo d44111e712
All checks were successful
continuous-integration/drone/push Build is passing
fix: 修复 duo-practice-report.vue 语法错误
移除多余的 try-catch 块
2026-01-30 15:20:18 +08:00

591 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="duo-practice-report">
<!-- 页头 -->
<div class="report-header">
<el-button text @click="handleBack">
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
<h1>对练报告</h1>
</div>
<div class="report-content" v-loading="isLoading">
<!-- 概览卡片 -->
<div class="overview-section">
<div class="overview-card">
<div class="overview-item">
<div class="label">场景</div>
<div class="value">{{ roomInfo?.scene_name || '双人对练' }}</div>
</div>
<div class="overview-item">
<div class="label">时长</div>
<div class="value">{{ formatDuration(roomInfo?.duration_seconds || 0) }}</div>
</div>
<div class="overview-item">
<div class="label">对话轮次</div>
<div class="value">{{ roomInfo?.total_turns || 0 }} </div>
</div>
<div class="overview-item">
<div class="label">互动质量</div>
<div class="value score">{{ analysisResult?.overall_evaluation?.interaction_quality || '--' }}</div>
</div>
</div>
</div>
<!-- 双人评估对比 -->
<div class="evaluation-section">
<h2>双方表现</h2>
<div class="evaluation-cards">
<!-- 用户A评估 -->
<div class="evaluation-card" v-if="analysisResult?.user_a_evaluation">
<div class="card-header">
<div class="user-info">
<el-avatar :size="48">{{ analysisResult.user_a_evaluation.user_name?.[0] }}</el-avatar>
<div>
<div class="user-name">{{ analysisResult.user_a_evaluation.user_name }}</div>
<div class="role-name">{{ analysisResult.user_a_evaluation.role_name }}</div>
</div>
</div>
<div class="total-score">
<div class="score-value">{{ analysisResult.user_a_evaluation.total_score }}</div>
<div class="score-label">综合评分</div>
</div>
</div>
<div class="card-body">
<!-- 维度评分 -->
<div class="dimensions">
<div
class="dimension-item"
v-for="(dim, key) in analysisResult.user_a_evaluation.dimensions"
:key="key"
>
<div class="dim-header">
<span class="dim-name">{{ getDimensionName(key) }}</span>
<span class="dim-score">{{ dim.score }}</span>
</div>
<el-progress
:percentage="dim.score"
:stroke-width="8"
:show-text="false"
:color="getScoreColor(dim.score)"
/>
<div class="dim-comment">{{ dim.comment }}</div>
</div>
</div>
<!-- 亮点 -->
<div class="highlights" v-if="analysisResult.user_a_evaluation.highlights?.length">
<h4>亮点</h4>
<ul>
<li v-for="(h, i) in analysisResult.user_a_evaluation.highlights" :key="i">
{{ h }}
</li>
</ul>
</div>
<!-- 改进建议 -->
<div class="improvements" v-if="analysisResult.user_a_evaluation.improvements?.length">
<h4>改进建议</h4>
<div
class="improvement-item"
v-for="(imp, i) in analysisResult.user_a_evaluation.improvements"
:key="i"
>
<div class="issue">{{ imp.issue }}</div>
<div class="suggestion">{{ imp.suggestion }}</div>
<div class="example" v-if="imp.example">示例{{ imp.example }}</div>
</div>
</div>
</div>
</div>
<!-- 用户B评估 -->
<div class="evaluation-card" v-if="analysisResult?.user_b_evaluation">
<div class="card-header">
<div class="user-info">
<el-avatar :size="48">{{ analysisResult.user_b_evaluation.user_name?.[0] }}</el-avatar>
<div>
<div class="user-name">{{ analysisResult.user_b_evaluation.user_name }}</div>
<div class="role-name">{{ analysisResult.user_b_evaluation.role_name }}</div>
</div>
</div>
<div class="total-score">
<div class="score-value">{{ analysisResult.user_b_evaluation.total_score }}</div>
<div class="score-label">综合评分</div>
</div>
</div>
<div class="card-body">
<!-- 维度评分 -->
<div class="dimensions">
<div
class="dimension-item"
v-for="(dim, key) in analysisResult.user_b_evaluation.dimensions"
:key="key"
>
<div class="dim-header">
<span class="dim-name">{{ getDimensionName(key) }}</span>
<span class="dim-score">{{ dim.score }}</span>
</div>
<el-progress
:percentage="dim.score"
:stroke-width="8"
:show-text="false"
:color="getScoreColor(dim.score)"
/>
<div class="dim-comment">{{ dim.comment }}</div>
</div>
</div>
<!-- 亮点 -->
<div class="highlights" v-if="analysisResult.user_b_evaluation.highlights?.length">
<h4>亮点</h4>
<ul>
<li v-for="(h, i) in analysisResult.user_b_evaluation.highlights" :key="i">
{{ h }}
</li>
</ul>
</div>
<!-- 改进建议 -->
<div class="improvements" v-if="analysisResult.user_b_evaluation.improvements?.length">
<h4>改进建议</h4>
<div
class="improvement-item"
v-for="(imp, i) in analysisResult.user_b_evaluation.improvements"
:key="i"
>
<div class="issue">{{ imp.issue }}</div>
<div class="suggestion">{{ imp.suggestion }}</div>
<div class="example" v-if="imp.example">示例{{ imp.example }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 整体评价 -->
<div class="overall-section" v-if="analysisResult?.overall_evaluation?.overall_comment">
<h2>整体评价</h2>
<div class="overall-comment">
{{ analysisResult.overall_evaluation.overall_comment }}
</div>
</div>
<!-- 加载中或无数据 -->
<el-empty v-if="!isLoading && !analysisResult" description="暂无报告数据" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getPracticeReport } from '@/api/duoPractice'
const route = useRoute()
const router = useRouter()
// 状态
const isLoading = ref(false)
const roomInfo = ref<any>(null)
const analysisResult = ref<any>(null)
// 方法
const handleBack = () => {
router.push('/trainee/duo-practice')
}
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}${secs}`
}
const getDimensionName = (key: string) => {
const map: Record<string, string> = {
'role_immersion': '角色代入',
'communication': '沟通表达',
'professional_knowledge': '专业知识',
'response_quality': '回应质量',
'goal_achievement': '目标达成'
}
return map[key] || key
}
const getScoreColor = (score: number) => {
if (score >= 80) return '#67c23a'
if (score >= 60) return '#e6a23c'
return '#f56c6c'
}
// 加载报告数据
const loadReport = async () => {
const roomCode = route.params.id as string
if (!roomCode) return
isLoading.value = true
try {
// 调用 API 获取报告
const res = await getPracticeReport(roomCode)
if (res.data) {
roomInfo.value = res.data.room
// 转换分析数据格式以兼容现有模板
const analysis = res.data.analysis
analysisResult.value = {
overall_evaluation: {
interaction_quality: analysis?.quality?.engagement_score || 0,
scene_restoration: analysis?.quality?.response_quality || 0,
overall_comment: analysis?.summary || ''
},
user_a_evaluation: {
user_name: res.data.participants?.host?.username || '用户A',
role_name: res.data.room?.role_a_name || '角色A',
total_score: analysis?.quality?.overall_score || 0,
dimensions: {
role_immersion: { score: analysis?.participation?.balance_score || 0, comment: '' },
communication: { score: analysis?.quality?.engagement_score || 0, comment: '' },
professional_knowledge: { score: analysis?.quality?.response_quality || 0, comment: '' },
response_quality: { score: analysis?.quality?.overall_score || 0, comment: '' },
goal_achievement: { score: analysis?.quality?.overall_score || 0, comment: '' }
},
highlights: analysis?.suggestions?.filter((s: string) => s.includes('良好') || s.includes('保持')) || [],
improvements: analysis?.suggestions?.filter((s: string) => !s.includes('良好') && !s.includes('保持')).map((s: string) => ({
issue: s,
suggestion: s
})) || []
},
user_b_evaluation: {
user_name: res.data.participants?.guest?.username || '用户B',
role_name: res.data.room?.role_b_name || '角色B',
total_score: analysis?.quality?.overall_score || 0,
dimensions: {
role_immersion: { score: analysis?.participation?.balance_score || 0, comment: '' },
communication: { score: analysis?.quality?.engagement_score || 0, comment: '' },
professional_knowledge: { score: analysis?.quality?.response_quality || 0, comment: '' },
response_quality: { score: analysis?.quality?.overall_score || 0, comment: '' },
goal_achievement: { score: analysis?.quality?.overall_score || 0, comment: '' }
},
highlights: [],
improvements: []
}
}
return
}
} catch (error: any) {
console.error('加载报告失败:', error)
ElMessage.warning('加载报告数据失败,使用演示数据')
}
// 降级到模拟数据
roomInfo.value = {
scene_name: '销售场景对练',
duration_seconds: 300,
total_turns: 15
}
analysisResult.value = {
overall_evaluation: {
interaction_quality: 85,
scene_restoration: 82,
overall_comment: '本次双人对练整体表现良好,双方都能够较好地代入角色,对话流畅自然。销售顾问在产品介绍和需求挖掘方面表现出色,顾客也能够提出合理的疑问和需求。建议在处理异议时可以更加灵活,增加更多情感共鸣的表达。'
},
user_a_evaluation: {
user_name: '张三',
role_name: '销售顾问',
total_score: 86,
dimensions: {
role_immersion: { score: 88, comment: '完全进入角色状态,语言风格符合销售顾问身份' },
communication: { score: 85, comment: '表达清晰,逻辑通顺,用词专业' },
professional_knowledge: { score: 82, comment: '产品知识展示较为全面' },
response_quality: { score: 88, comment: '回应及时准确,针对性强' },
goal_achievement: { score: 85, comment: '有效推进了销售进程' }
},
highlights: [
'开场白自然得体,快速建立信任',
'善于使用提问技巧挖掘客户需求',
'产品利益点阐述清晰有力'
],
improvements: [
{
issue: '处理价格异议时略显被动',
suggestion: '可以先肯定客户的关注点,再引导关注价值',
example: '您说得对,预算确实是重要的考虑因素。不过您有没有想过...'
}
]
},
user_b_evaluation: {
user_name: '李四',
role_name: '顾客',
total_score: 83,
dimensions: {
role_immersion: { score: 80, comment: '基本符合顾客角色设定' },
communication: { score: 85, comment: '表达自己的需求和疑虑' },
professional_knowledge: { score: 78, comment: '对产品有基本了解' },
response_quality: { score: 82, comment: '能够合理回应销售话术' },
goal_achievement: { score: 80, comment: '配合完成了对练场景' }
},
highlights: [
'提出的问题具有代表性',
'表现出真实顾客的犹豫和考虑'
],
improvements: [
{
issue: '可以增加更多挑战性的问题',
suggestion: '尝试提出一些竞品对比、售后保障等深度问题',
example: '我听说XX品牌的产品价格更低你们有什么优势'
}
]
}
}
isLoading.value = false
}
onMounted(() => {
loadReport()
})
</script>
<style scoped lang="scss">
.duo-practice-report {
min-height: 100vh;
background: #f5f7fa;
}
.report-header {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 24px;
background: #fff;
border-bottom: 1px solid #eee;
h1 {
font-size: 20px;
font-weight: 600;
margin: 0;
}
}
.report-content {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.overview-section {
margin-bottom: 32px;
.overview-card {
display: flex;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: #fff;
.overview-item {
flex: 1;
text-align: center;
&:not(:last-child) {
border-right: 1px solid rgba(255, 255, 255, 0.2);
}
.label {
font-size: 13px;
opacity: 0.8;
margin-bottom: 8px;
}
.value {
font-size: 20px;
font-weight: 600;
&.score {
font-size: 32px;
}
}
}
}
}
.evaluation-section {
margin-bottom: 32px;
h2 {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.evaluation-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.evaluation-card {
background: #fff;
border-radius: 16px;
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8f9fc;
.user-info {
display: flex;
align-items: center;
gap: 12px;
.user-name {
font-weight: 600;
font-size: 16px;
}
.role-name {
font-size: 13px;
color: #666;
}
}
.total-score {
text-align: center;
.score-value {
font-size: 36px;
font-weight: 700;
color: #667eea;
line-height: 1;
}
.score-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
}
.card-body {
padding: 20px;
.dimensions {
.dimension-item {
margin-bottom: 16px;
.dim-header {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
.dim-name {
font-size: 14px;
font-weight: 500;
}
.dim-score {
font-size: 14px;
font-weight: 600;
color: #667eea;
}
}
.dim-comment {
font-size: 13px;
color: #666;
margin-top: 6px;
}
}
}
.highlights, .improvements {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
h4 {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
}
}
.highlights {
ul {
margin: 0;
padding-left: 20px;
li {
font-size: 14px;
color: #67c23a;
margin-bottom: 6px;
}
}
}
.improvements {
.improvement-item {
background: #fdf6ec;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
.issue {
font-size: 14px;
font-weight: 500;
color: #e6a23c;
margin-bottom: 8px;
}
.suggestion {
font-size: 14px;
color: #333;
margin-bottom: 6px;
}
.example {
font-size: 13px;
color: #666;
font-style: italic;
background: #fff;
padding: 8px;
border-radius: 4px;
}
}
}
}
}
}
.overall-section {
h2 {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
}
.overall-comment {
background: #fff;
border-radius: 16px;
padding: 24px;
font-size: 15px;
line-height: 1.8;
color: #333;
}
}
</style>