All checks were successful
continuous-integration/drone/push Build is passing
- 回放对话时调用 API 获取对话详情 - 添加加载状态显示 - 添加空数据提示 Co-authored-by: Cursor <cursoragent@cursor.com>
1067 lines
27 KiB
Vue
1067 lines
27 KiB
Vue
<template>
|
||
<div class="practice-records-container">
|
||
<div class="page-header">
|
||
<h1 class="page-title">陪练记录</h1>
|
||
<div class="header-actions">
|
||
<el-date-picker
|
||
v-model="dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
@change="handleDateChange"
|
||
/>
|
||
<el-button type="primary" @click="refreshData">
|
||
<el-icon class="el-icon--left"><Refresh /></el-icon>
|
||
刷新数据
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 陪练统计概览 -->
|
||
<div class="stats-overview">
|
||
<div class="stat-card card" v-for="stat in practiceStats" :key="stat.label">
|
||
<div class="stat-icon" :style="{ backgroundColor: stat.bgColor }">
|
||
<el-icon :size="32" :color="stat.color">
|
||
<component :is="stat.icon" />
|
||
</el-icon>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-value">{{ stat.value }}</div>
|
||
<div class="stat-label">{{ stat.label }}</div>
|
||
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'" v-if="stat.trend !== 0">
|
||
<el-icon><component :is="stat.trend > 0 ? 'Top' : 'Bottom'" /></el-icon>
|
||
{{ Math.abs(stat.trend) }}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 筛选区域 -->
|
||
<div class="filter-section card">
|
||
<el-form :inline="true" :model="filterForm" class="filter-form">
|
||
<el-form-item label="关键词">
|
||
<el-input
|
||
v-model="filterForm.keyword"
|
||
placeholder="搜索陪练内容或场景"
|
||
clearable
|
||
@input="handleRealTimeSearch"
|
||
style="width: 200px"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="陪练场景">
|
||
<el-select
|
||
v-model="filterForm.scene"
|
||
placeholder="全部场景"
|
||
clearable
|
||
@change="handleRealTimeSearch"
|
||
style="width: 120px"
|
||
>
|
||
<el-option label="客户咨询" value="customer_consultation" />
|
||
<el-option label="美容护理" value="beauty_care" />
|
||
<el-option label="产品介绍" value="product_introduction" />
|
||
<el-option label="问题处理" value="problem_handling" />
|
||
<el-option label="服务礼仪" value="service_etiquette" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="陪练结果">
|
||
<el-select
|
||
v-model="filterForm.result"
|
||
placeholder="全部结果"
|
||
clearable
|
||
@change="handleRealTimeSearch"
|
||
style="width: 120px"
|
||
>
|
||
<el-option label="优秀" value="excellent" />
|
||
<el-option label="良好" value="good" />
|
||
<el-option label="一般" value="average" />
|
||
<el-option label="需改进" value="needs_improvement" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="时间周期">
|
||
<el-select
|
||
v-model="filterForm.timePeriod"
|
||
placeholder="全部时间"
|
||
clearable
|
||
@change="handleRealTimeSearch"
|
||
style="width: 120px"
|
||
>
|
||
<el-option label="最近一周" value="week" />
|
||
<el-option label="最近一月" value="month" />
|
||
<el-option label="最近三月" value="quarter" />
|
||
<el-option label="自定义" value="custom" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item v-if="filterForm.timePeriod === 'custom'" label="自定义时间">
|
||
<el-date-picker
|
||
v-model="customDateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
@change="handleRealTimeSearch"
|
||
style="width: 240px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button @click="handleReset">
|
||
<el-icon class="el-icon--left"><Refresh /></el-icon>
|
||
重置
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 当前筛选条件显示 -->
|
||
<div v-if="hasActiveFilters" class="filter-tags">
|
||
<span class="filter-label">当前筛选:</span>
|
||
<el-tag
|
||
v-if="filterForm.keyword"
|
||
closable
|
||
@close="clearKeyword"
|
||
type="primary"
|
||
>
|
||
关键词:{{ filterForm.keyword }}
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="filterForm.scene"
|
||
closable
|
||
@close="clearScene"
|
||
type="success"
|
||
>
|
||
场景:{{ getSceneText(filterForm.scene) }}
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="filterForm.result"
|
||
closable
|
||
@close="clearResult"
|
||
type="warning"
|
||
>
|
||
结果:{{ getResultText(filterForm.result) }}
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="filterForm.timePeriod"
|
||
closable
|
||
@close="clearTimePeriod"
|
||
type="info"
|
||
>
|
||
时间:{{ getTimePeriodText(filterForm.timePeriod) }}
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="customDateRange && customDateRange.length === 2"
|
||
closable
|
||
@close="clearCustomDate"
|
||
type="danger"
|
||
>
|
||
自定义:{{ formatDateRange(customDateRange) }}
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="dateRange && dateRange.length === 2"
|
||
closable
|
||
@close="clearHeaderDate"
|
||
type="danger"
|
||
>
|
||
日期:{{ formatDateRange(dateRange) }}
|
||
</el-tag>
|
||
<el-button
|
||
link
|
||
type="danger"
|
||
size="small"
|
||
@click="handleReset"
|
||
class="clear-all-btn"
|
||
>
|
||
清空全部
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 搜索结果统计 -->
|
||
<div class="search-result-info">
|
||
<span class="result-count">
|
||
共找到 <strong>{{ total }}</strong> 条陪练记录
|
||
<span v-if="hasActiveFilters" class="filter-hint">(已筛选)</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 陪练记录列表 -->
|
||
<div class="records-list card">
|
||
<el-table
|
||
:data="recordsList"
|
||
style="width: 100%"
|
||
v-loading="loading"
|
||
@row-click="viewPracticeReport"
|
||
>
|
||
<el-table-column prop="sessionId" label="会话ID" width="120" />
|
||
<el-table-column prop="sceneName" label="陪练场景" min-width="150">
|
||
<template #default="scope">
|
||
<div class="scene-info">
|
||
<el-tag :type="getSceneTypeTag(scope.row.sceneType)" size="small">
|
||
{{ scope.row.sceneName }}
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="duration" label="陪练时长" width="120">
|
||
<template #default="scope">
|
||
<span>{{ formatDuration(scope.row.duration) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="roundCount" label="对话轮数" width="100" />
|
||
<el-table-column prop="score" label="综合评分" width="120" sortable>
|
||
<template #default="scope">
|
||
<div class="score-display">
|
||
<el-progress
|
||
:percentage="scope.row.score"
|
||
:color="getScoreColor(scope.row.score)"
|
||
:stroke-width="8"
|
||
/>
|
||
<span class="score-text">{{ scope.row.score }}分</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="result" label="陪练结果" width="120">
|
||
<template #default="scope">
|
||
<el-tag :type="getResultTagType(scope.row.result)">
|
||
{{ getResultText(scope.row.result) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="practiceTime" label="陪练时间" width="180" sortable />
|
||
<el-table-column label="操作" width="200" fixed="right">
|
||
<template #default="scope">
|
||
<el-button link type="primary" size="small" @click.stop="viewPracticeReport(scope.row)">
|
||
<el-icon><View /></el-icon>
|
||
查看报告
|
||
</el-button>
|
||
<el-button link type="primary" size="small" @click.stop="replayPractice(scope.row)">
|
||
<el-icon><VideoPlay /></el-icon>
|
||
回放对话
|
||
</el-button>
|
||
<el-button link type="primary" size="small" @click.stop="exportRecord(scope.row)">
|
||
<el-icon><Download /></el-icon>
|
||
导出记录
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination-wrapper">
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 对话回放弹窗 -->
|
||
<el-dialog
|
||
v-model="replayDialogVisible"
|
||
:title="`${currentRecord?.sceneName} - 对话回放`"
|
||
width="800px"
|
||
top="5vh"
|
||
>
|
||
<div class="replay-content" v-if="currentRecord">
|
||
<!-- 陪练信息 -->
|
||
<div class="replay-header">
|
||
<div class="session-info">
|
||
<h3>{{ currentRecord.sceneName }}</h3>
|
||
<div class="session-meta">
|
||
<span>时长:{{ formatDuration(currentRecord.duration) }}</span>
|
||
<span>轮数:{{ currentRecord.roundCount }}</span>
|
||
<span>评分:{{ currentRecord.score }}分</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 对话记录 -->
|
||
<div class="conversation-replay" v-loading="replayLoading">
|
||
<!-- 空状态提示 -->
|
||
<el-empty
|
||
v-if="!replayLoading && (!currentRecord.conversation || currentRecord.conversation.length === 0)"
|
||
description="暂无对话记录"
|
||
/>
|
||
|
||
<!-- 对话列表 -->
|
||
<div class="conversation-list" v-else>
|
||
<div
|
||
v-for="(message, index) in currentRecord.conversation"
|
||
:key="index"
|
||
class="message-item"
|
||
:class="message.role"
|
||
>
|
||
<div class="message-avatar">
|
||
<el-avatar v-if="message.role === 'user'" :size="32">
|
||
{{ currentRecord.userName?.charAt(0) || 'U' }}
|
||
</el-avatar>
|
||
<el-avatar v-else :size="32" style="background: #409eff">
|
||
AI
|
||
</el-avatar>
|
||
</div>
|
||
<div class="message-content">
|
||
<div class="message-header">
|
||
<span class="sender-name">
|
||
{{ message.role === 'user' ? (currentRecord.userName || '学员') : 'AI陪练师' }}
|
||
</span>
|
||
<span class="message-time">{{ message.timestamp }}</span>
|
||
</div>
|
||
<div class="message-text">{{ message.content }}</div>
|
||
<div v-if="message.feedback" class="ai-feedback">
|
||
<el-tag type="info" size="small">AI反馈</el-tag>
|
||
<span>{{ message.feedback }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Refresh, Search } from '@element-plus/icons-vue'
|
||
import { practiceApi } from '@/api/practice'
|
||
|
||
const router = useRouter()
|
||
|
||
// 加载状态
|
||
const loading = ref(false)
|
||
|
||
// 日期范围
|
||
const dateRange = ref<[Date, Date] | null>(null)
|
||
|
||
// 分页
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(20)
|
||
const total = ref(0)
|
||
|
||
// 弹窗状态
|
||
const replayDialogVisible = ref(false)
|
||
const replayLoading = ref(false)
|
||
const currentRecord = ref<any>(null)
|
||
|
||
// 筛选表单
|
||
const filterForm = reactive({
|
||
keyword: '',
|
||
scene: '',
|
||
result: '',
|
||
timePeriod: ''
|
||
})
|
||
|
||
// 自定义日期范围
|
||
const customDateRange = ref<[Date, Date] | null>(null)
|
||
|
||
// 是否有活跃的筛选条件
|
||
const hasActiveFilters = computed(() => {
|
||
return !!(filterForm.keyword || filterForm.scene || filterForm.result || filterForm.timePeriod || (customDateRange.value && customDateRange.value.length === 2) || (dateRange.value && dateRange.value.length === 2))
|
||
})
|
||
|
||
// 定义统计数据接口
|
||
interface PracticeStat {
|
||
label: string
|
||
value: string
|
||
icon: string
|
||
color: string
|
||
bgColor: string
|
||
trend: number
|
||
}
|
||
|
||
// 陪练统计数据
|
||
const practiceStats = ref<PracticeStat[]>([])
|
||
|
||
// 陪练记录数据(直接使用后端返回的已筛选、已分页的数据)
|
||
const recordsList = ref([])
|
||
|
||
/**
|
||
* 获取场景类型标签样式
|
||
*/
|
||
const getSceneTypeTag = (type: string) => {
|
||
const tagMap: Record<string, string> = {
|
||
customer_consultation: 'primary',
|
||
beauty_care: 'success',
|
||
product_introduction: 'warning',
|
||
problem_handling: 'danger',
|
||
service_etiquette: 'info'
|
||
}
|
||
return tagMap[type] || ''
|
||
}
|
||
|
||
/**
|
||
* 获取评分颜色
|
||
*/
|
||
const getScoreColor = (score: number) => {
|
||
if (score >= 90) return '#67c23a'
|
||
if (score >= 80) return '#409eff'
|
||
if (score >= 70) return '#e6a23c'
|
||
return '#f56c6c'
|
||
}
|
||
|
||
/**
|
||
* 获取结果标签类型
|
||
*/
|
||
const getResultTagType = (result: string) => {
|
||
const tagMap: Record<string, string> = {
|
||
excellent: 'success',
|
||
good: 'primary',
|
||
average: 'warning',
|
||
needs_improvement: 'danger'
|
||
}
|
||
return tagMap[result] || ''
|
||
}
|
||
|
||
/**
|
||
* 获取结果文本
|
||
*/
|
||
const getResultText = (result: string) => {
|
||
const textMap: Record<string, string> = {
|
||
excellent: '优秀',
|
||
good: '良好',
|
||
average: '一般',
|
||
needs_improvement: '需改进'
|
||
}
|
||
return textMap[result] || result
|
||
}
|
||
|
||
/**
|
||
* 格式化时长
|
||
*/
|
||
const formatDuration = (seconds: number) => {
|
||
const hours = Math.floor(seconds / 3600)
|
||
const minutes = Math.floor((seconds % 3600) / 60)
|
||
const secs = seconds % 60
|
||
|
||
if (hours > 0) {
|
||
return `${hours}小时${minutes}分钟`
|
||
} else if (minutes > 0) {
|
||
return `${minutes}分钟${secs}秒`
|
||
} else {
|
||
return `${secs}秒`
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 日期变化处理
|
||
*/
|
||
const handleDateChange = () => {
|
||
handleSearch()
|
||
}
|
||
|
||
/**
|
||
* 刷新数据
|
||
*/
|
||
const refreshData = async () => {
|
||
await loadStats()
|
||
await loadRecords()
|
||
ElMessage.success('数据已刷新')
|
||
}
|
||
|
||
/**
|
||
* 搜索处理
|
||
*/
|
||
const handleSearch = async () => {
|
||
currentPage.value = 1
|
||
await loadRecords()
|
||
}
|
||
|
||
/**
|
||
* 实时搜索处理
|
||
*/
|
||
const handleRealTimeSearch = async () => {
|
||
currentPage.value = 1
|
||
await loadRecords()
|
||
}
|
||
|
||
/**
|
||
* 重置筛选
|
||
*/
|
||
const handleReset = async () => {
|
||
filterForm.keyword = ''
|
||
filterForm.scene = ''
|
||
filterForm.result = ''
|
||
filterForm.timePeriod = ''
|
||
customDateRange.value = null
|
||
dateRange.value = null
|
||
currentPage.value = 1
|
||
await loadRecords()
|
||
ElMessage.success('已重置所有筛选条件')
|
||
}
|
||
|
||
/**
|
||
* 清除关键词筛选
|
||
*/
|
||
const clearKeyword = () => {
|
||
filterForm.keyword = ''
|
||
currentPage.value = 1
|
||
}
|
||
|
||
/**
|
||
* 清除场景筛选
|
||
*/
|
||
const clearScene = () => {
|
||
filterForm.scene = ''
|
||
currentPage.value = 1
|
||
}
|
||
|
||
/**
|
||
* 清除结果筛选
|
||
*/
|
||
const clearResult = () => {
|
||
filterForm.result = ''
|
||
currentPage.value = 1
|
||
}
|
||
|
||
/**
|
||
* 清除时间周期筛选
|
||
*/
|
||
const clearTimePeriod = () => {
|
||
filterForm.timePeriod = ''
|
||
currentPage.value = 1
|
||
}
|
||
|
||
/**
|
||
* 清除自定义日期筛选
|
||
*/
|
||
const clearCustomDate = () => {
|
||
customDateRange.value = null
|
||
currentPage.value = 1
|
||
}
|
||
|
||
/**
|
||
* 清除头部日期筛选
|
||
*/
|
||
const clearHeaderDate = () => {
|
||
dateRange.value = null
|
||
currentPage.value = 1
|
||
}
|
||
|
||
/**
|
||
* 获取场景文本
|
||
*/
|
||
const getSceneText = (scene: string) => {
|
||
const map: Record<string, string> = {
|
||
customer_consultation: '客户咨询',
|
||
beauty_care: '美容护理',
|
||
product_introduction: '产品介绍',
|
||
problem_handling: '问题处理',
|
||
service_etiquette: '服务礼仪'
|
||
}
|
||
return map[scene] || '未知'
|
||
}
|
||
|
||
|
||
/**
|
||
* 获取时间周期文本
|
||
*/
|
||
const getTimePeriodText = (period: string) => {
|
||
const map: Record<string, string> = {
|
||
week: '最近一周',
|
||
month: '最近一月',
|
||
quarter: '最近三月',
|
||
custom: '自定义时间'
|
||
}
|
||
return map[period] || '全部时间'
|
||
}
|
||
|
||
/**
|
||
* 格式化日期范围
|
||
*/
|
||
const formatDateRange = (range: Date[]) => {
|
||
if (!range || range.length !== 2) return ''
|
||
const start = range[0].toLocaleDateString()
|
||
const end = range[1].toLocaleDateString()
|
||
return start + ' 至 ' + end
|
||
}
|
||
|
||
/**
|
||
* 分页大小改变
|
||
*/
|
||
const handleSizeChange = (val: number) => {
|
||
pageSize.value = val
|
||
currentPage.value = 1
|
||
handleSearch()
|
||
}
|
||
|
||
/**
|
||
* 当前页改变
|
||
*/
|
||
const handleCurrentChange = (val: number) => {
|
||
currentPage.value = val
|
||
handleSearch()
|
||
}
|
||
|
||
/**
|
||
* 查看陪练报告(跳转到详细分析页面)
|
||
*/
|
||
const viewPracticeReport = (record: any) => {
|
||
// 跳转到陪练分析报告页面,传递记录ID
|
||
router.push({
|
||
name: 'PracticeReport',
|
||
params: { id: record.id },
|
||
query: { sessionId: record.sessionId }
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 回放陪练对话
|
||
*/
|
||
const replayPractice = async (record: any) => {
|
||
try {
|
||
// 先设置基本信息并打开弹窗
|
||
currentRecord.value = { ...record, conversation: [] }
|
||
replayDialogVisible.value = true
|
||
replayLoading.value = true
|
||
|
||
// 调用 API 获取对话详情
|
||
const response: any = await practiceApi.getPracticeReport(record.sessionId)
|
||
|
||
if (response.code === 200 && response.data?.analysis?.dialogue_review) {
|
||
// 转换对话数据格式
|
||
const dialogueReview = response.data.analysis.dialogue_review
|
||
currentRecord.value.conversation = dialogueReview.map((item: any) => ({
|
||
role: item.speaker === 'user' ? 'user' : 'ai',
|
||
content: item.content,
|
||
timestamp: item.time || '',
|
||
feedback: item.comment || ''
|
||
}))
|
||
}
|
||
} catch (error: any) {
|
||
console.error('获取对话详情失败:', error)
|
||
ElMessage.error('获取对话详情失败,请稍后重试')
|
||
} finally {
|
||
replayLoading.value = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导出陪练记录
|
||
*/
|
||
const exportRecord = (record: any) => {
|
||
ElMessage.success(`正在导出陪练记录 ${record.sessionId}...`)
|
||
// 这里可以调用导出 API
|
||
}
|
||
|
||
// 组件挂载时初始化数据
|
||
onMounted(async () => {
|
||
// 加载统计数据
|
||
await loadStats()
|
||
|
||
// 加载陪练记录
|
||
await loadRecords()
|
||
})
|
||
|
||
/**
|
||
* 加载统计数据
|
||
*/
|
||
const loadStats = async () => {
|
||
try {
|
||
const response: any = await practiceApi.getPracticeStats()
|
||
|
||
if (response.code === 200 && response.data) {
|
||
const data = response.data
|
||
|
||
practiceStats.value = [
|
||
{
|
||
label: '总陪练次数',
|
||
value: data.total_count.toString(),
|
||
icon: 'ChatLineRound',
|
||
color: '#667eea',
|
||
bgColor: 'rgba(102, 126, 234, 0.1)',
|
||
trend: 0
|
||
},
|
||
{
|
||
label: '平均评分',
|
||
value: data.avg_score.toFixed(1),
|
||
icon: 'TrendCharts',
|
||
color: '#67c23a',
|
||
bgColor: 'rgba(103, 194, 58, 0.1)',
|
||
trend: 0
|
||
},
|
||
{
|
||
label: '总陪练时长',
|
||
value: data.total_duration_hours + 'h',
|
||
icon: 'Timer',
|
||
color: '#e6a23c',
|
||
bgColor: 'rgba(230, 162, 60, 0.1)',
|
||
trend: 0
|
||
},
|
||
{
|
||
label: '本月进步',
|
||
value: '+' + data.month_improvement + '%',
|
||
icon: 'Top',
|
||
color: '#f56c6c',
|
||
bgColor: 'rgba(245, 108, 108, 0.1)',
|
||
trend: data.month_improvement
|
||
}
|
||
]
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计数据失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载陪练记录
|
||
*/
|
||
const loadRecords = async () => {
|
||
loading.value = true
|
||
try {
|
||
// 格式化日期参数
|
||
let startDate: string | undefined
|
||
let endDate: string | undefined
|
||
|
||
if (dateRange.value && dateRange.value.length === 2) {
|
||
startDate = dateRange.value[0].toISOString().split('T')[0]
|
||
endDate = dateRange.value[1].toISOString().split('T')[0]
|
||
}
|
||
|
||
const response: any = await practiceApi.getPracticeSessions({
|
||
page: currentPage.value,
|
||
size: pageSize.value,
|
||
keyword: filterForm.keyword || undefined,
|
||
scene_type: filterForm.scene || undefined,
|
||
start_date: startDate,
|
||
end_date: endDate
|
||
})
|
||
|
||
if (response.code === 200 && response.data) {
|
||
const data = response.data
|
||
|
||
// 转换数据格式
|
||
recordsList.value = data.items.map((item: any) => ({
|
||
id: item.session_id,
|
||
sessionId: item.session_id,
|
||
sceneName: item.scene_name,
|
||
sceneType: item.scene_type || 'customer_consultation',
|
||
duration: item.duration_seconds,
|
||
roundCount: item.turns,
|
||
score: item.total_score || 0,
|
||
result: item.result,
|
||
practiceTime: item.start_time,
|
||
userName: '我',
|
||
conversation: [] // 对话详情暂不加载,点击时再加载
|
||
}))
|
||
|
||
total.value = data.total
|
||
}
|
||
} catch (error) {
|
||
console.error('加载陪练记录失败:', error)
|
||
ElMessage.error('加载陪练记录失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.practice-records-container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.stats-overview {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 24px;
|
||
|
||
.stat-card {
|
||
padding: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
|
||
.stat-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.stat-content {
|
||
flex: 1;
|
||
|
||
.stat-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: #333;
|
||
line-height: 1;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.stat-trend {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
|
||
&.up {
|
||
color: #67c23a;
|
||
}
|
||
|
||
&.down {
|
||
color: #f56c6c;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.filter-section {
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
|
||
.filter-form {
|
||
.el-form-item {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.filter-tags {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
|
||
.filter-label {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.clear-all-btn {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
|
||
.search-result-info {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #f0f0f0;
|
||
|
||
.result-count {
|
||
color: #666;
|
||
font-size: 14px;
|
||
|
||
strong {
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.filter-hint {
|
||
color: #e6a23c;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.records-list {
|
||
padding: 24px;
|
||
|
||
.scene-info {
|
||
.el-tag {
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.score-display {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.score-text {
|
||
font-weight: 600;
|
||
min-width: 50px;
|
||
}
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 24px;
|
||
}
|
||
}
|
||
|
||
.replay-content {
|
||
.replay-header {
|
||
margin-bottom: 24px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid #eee;
|
||
|
||
.session-info {
|
||
h3 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.session-meta {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
|
||
span {
|
||
padding: 4px 8px;
|
||
background: #f5f7fa;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.conversation-replay {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
|
||
.conversation-list {
|
||
.message-item {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
|
||
&.user {
|
||
flex-direction: row-reverse;
|
||
|
||
.message-content {
|
||
background: #409eff;
|
||
color: white;
|
||
border-radius: 18px 18px 4px 18px;
|
||
}
|
||
}
|
||
|
||
&.ai {
|
||
.message-content {
|
||
background: #f5f7fa;
|
||
color: #333;
|
||
border-radius: 18px 18px 18px 4px;
|
||
}
|
||
}
|
||
|
||
.message-avatar {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.message-content {
|
||
max-width: 70%;
|
||
padding: 12px 16px;
|
||
|
||
.message-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 4px;
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
|
||
.sender-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
.message-text {
|
||
line-height: 1.4;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.ai-feedback {
|
||
margin-top: 8px;
|
||
padding: 8px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
|
||
.el-tag {
|
||
margin-right: 8px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 响应式设计
|
||
@media (max-width: 768px) {
|
||
.practice-records-container {
|
||
.page-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
|
||
.header-actions {
|
||
width: 100%;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
.stats-overview {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.filter-form {
|
||
.el-form-item {
|
||
display: block;
|
||
margin-bottom: 16px !important;
|
||
}
|
||
}
|
||
|
||
.conversation-replay {
|
||
.message-item {
|
||
.message-content {
|
||
max-width: 85%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|