style: 优化陪练记录页面UI设计
All checks were successful
continuous-integration/drone/push Build is passing

- 统计卡片改用 el-statistic 组件,与错题分析保持一致
- 搜索框改为圆角胶囊形状,添加 hover 聚焦效果
- 下拉选择框使用统一的圆角灰底设计,添加 emoji 图标前缀
- 筛选标签改为胶囊形状
- 重置按钮仅在有筛选条件时显示
- 添加表格行悬浮效果和操作按钮悬浮样式
- 优化响应式布局

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuliang_guo
2026-02-03 16:38:45 +08:00
parent 4c1b70e9d6
commit ed47286955

View File

@@ -2,118 +2,134 @@
<div class="practice-records-container"> <div class="practice-records-container">
<div class="page-header"> <div class="page-header">
<h1 class="page-title">陪练记录</h1> <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>
<!-- 陪练统计概览 --> <!-- 陪练统计概览 -->
<div class="stats-overview"> <div class="stats-section">
<div class="stat-card card" v-for="stat in practiceStats" :key="stat.label"> <div class="stat-card card">
<div class="stat-icon" :style="{ backgroundColor: stat.bgColor }"> <el-statistic title="总陪练次数" :value="practiceStatsData.totalCount">
<el-icon :size="32" :color="stat.color"> <template #suffix>
<component :is="stat.icon" /> <span style="font-size: 14px"></span>
</el-icon> </template>
</div> </el-statistic>
<div class="stat-content"> </div>
<div class="stat-value">{{ stat.value }}</div> <div class="stat-card card">
<div class="stat-label">{{ stat.label }}</div> <el-statistic title="平均评分" :value="practiceStatsData.avgScore" :precision="1">
<div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'" v-if="stat.trend !== 0"> <template #suffix>
<el-icon><component :is="stat.trend > 0 ? 'Top' : 'Bottom'" /></el-icon> <span style="font-size: 14px"></span>
{{ Math.abs(stat.trend) }}% </template>
</div> </el-statistic>
</div> </div>
<div class="stat-card card">
<el-statistic title="总陪练时长" :value="practiceStatsData.totalHours" :precision="1">
<template #suffix>
<span style="font-size: 14px">小时</span>
</template>
</el-statistic>
</div>
<div class="stat-card card">
<el-statistic title="本月进步" :value="practiceStatsData.monthImprovement">
<template #prefix>+</template>
<template #suffix>%</template>
</el-statistic>
</div> </div>
</div> </div>
<!-- 筛选区域 --> <!-- 搜索和筛选 -->
<div class="filter-section card"> <div class="filter-section card">
<el-form :inline="true" :model="filterForm" class="filter-form"> <div class="filter-toolbar">
<el-form-item label="关键词"> <!-- 搜索框 -->
<div class="search-box">
<el-input <el-input
v-model="filterForm.keyword" v-model="filterForm.keyword"
placeholder="搜索陪练内容或场景" placeholder="搜索陪练内容或场景..."
clearable clearable
@input="handleRealTimeSearch" @input="handleRealTimeSearch"
style="width: 200px" class="search-input"
> >
<template #prefix> <template #prefix>
<el-icon><Search /></el-icon> <el-icon class="search-icon"><Search /></el-icon>
</template> </template>
</el-input> </el-input>
</el-form-item> </div>
<el-form-item label="陪练场景">
<!-- 筛选项 -->
<div class="filter-items">
<el-select <el-select
v-model="filterForm.scene" v-model="filterForm.scene"
placeholder="全部场景" placeholder="陪练场景"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon">🎯</span>
</template>
<el-option label="客户咨询" value="customer_consultation" /> <el-option label="客户咨询" value="customer_consultation" />
<el-option label="美容护理" value="beauty_care" /> <el-option label="美容护理" value="beauty_care" />
<el-option label="产品介绍" value="product_introduction" /> <el-option label="产品介绍" value="product_introduction" />
<el-option label="问题处理" value="problem_handling" /> <el-option label="问题处理" value="problem_handling" />
<el-option label="服务礼仪" value="service_etiquette" /> <el-option label="服务礼仪" value="service_etiquette" />
</el-select> </el-select>
</el-form-item>
<el-form-item label="陪练结果">
<el-select <el-select
v-model="filterForm.result" v-model="filterForm.result"
placeholder="全部结果" placeholder="陪练结果"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon"></span>
</template>
<el-option label="优秀" value="excellent" /> <el-option label="优秀" value="excellent" />
<el-option label="良好" value="good" /> <el-option label="良好" value="good" />
<el-option label="一般" value="average" /> <el-option label="一般" value="average" />
<el-option label="需改进" value="needs_improvement" /> <el-option label="需改进" value="needs_improvement" />
</el-select> </el-select>
</el-form-item>
<el-form-item label="时间周期">
<el-select <el-select
v-model="filterForm.timePeriod" v-model="filterForm.timePeriod"
placeholder="全部时间" placeholder="时间周期"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon">📅</span>
</template>
<el-option label="最近一周" value="week" /> <el-option label="最近一周" value="week" />
<el-option label="最近一月" value="month" /> <el-option label="最近一月" value="month" />
<el-option label="最近三月" value="quarter" /> <el-option label="最近三月" value="quarter" />
<el-option label="自定义" value="custom" /> <el-option label="自定义" value="custom" />
</el-select> </el-select>
</el-form-item>
<el-form-item v-if="filterForm.timePeriod === 'custom'" label="自定义时间">
<el-date-picker <el-date-picker
v-if="filterForm.timePeriod === 'custom'"
v-model="customDateRange" v-model="customDateRange"
type="daterange" type="daterange"
range-separator="" range-separator="~"
start-placeholder="开始日期" start-placeholder="开始"
end-placeholder="结束日期" end-placeholder="结束"
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 240px" class="date-picker"
format="MM/DD"
value-format="YYYY-MM-DD"
/> />
</el-form-item> </div>
<el-form-item>
<el-button @click="handleReset"> <!-- 重置按钮 -->
<el-icon class="el-icon--left"><Refresh /></el-icon> <el-button
重置 v-if="hasActiveFilters"
</el-button> @click="handleReset"
</el-form-item> class="reset-btn"
</el-form> type="info"
plain
>
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
<!-- 当前筛选条件显示 --> <!-- 当前筛选条件显示 -->
<div v-if="hasActiveFilters" class="filter-tags"> <div v-if="hasActiveFilters" class="filter-tags">
@@ -123,6 +139,7 @@
closable closable
@close="clearKeyword" @close="clearKeyword"
type="primary" type="primary"
effect="light"
> >
关键词{{ filterForm.keyword }} 关键词{{ filterForm.keyword }}
</el-tag> </el-tag>
@@ -381,6 +398,17 @@ interface PracticeStat {
// 陪练统计数据 // 陪练统计数据
const practiceStats = ref<PracticeStat[]>([]) const practiceStats = ref<PracticeStat[]>([])
// 简化的统计数据(用于 el-statistic 组件)
const practiceStatsData = computed(() => {
const stats = practiceStats.value
return {
totalCount: parseInt(stats.find(s => s.label === '总陪练次数')?.value || '0'),
avgScore: parseFloat(stats.find(s => s.label === '平均评分')?.value || '0'),
totalHours: parseFloat(stats.find(s => s.label === '总陪练时长')?.value?.replace('h', '') || '0'),
monthImprovement: parseInt(stats.find(s => s.label === '本月进步')?.value?.replace('+', '').replace('%', '') || '0')
}
})
// 陪练记录数据(直接使用后端返回的已筛选、已分页的数据) // 陪练记录数据(直接使用后端返回的已筛选、已分页的数据)
const recordsList = ref([]) const recordsList = ref([])
@@ -769,95 +797,143 @@ const loadRecords = async () => {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
.card {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.page-header { .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px; margin-bottom: 24px;
.page-title { .page-title {
font-size: 24px; font-size: 24px;
font-weight: 600; font-weight: 600;
color: #333; color: #1f2937;
}
.header-actions {
display: flex;
gap: 12px;
} }
} }
.stats-overview { // 统计区域 - 参考错题分析风格
.stats-section {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px; gap: 20px;
margin-bottom: 24px; margin-bottom: 24px;
.stat-card { .stat-card {
padding: 24px; text-align: center;
display: flex; transition: transform 0.2s ease, box-shadow 0.2s ease;
align-items: center;
gap: 20px;
.stat-icon { &:hover {
width: 64px; transform: translateY(-2px);
height: 64px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);
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 { .filter-section {
padding: 20px; padding: 16px 20px;
margin-bottom: 20px;
.filter-form { .filter-toolbar {
.el-form-item { display: flex;
margin-bottom: 0; align-items: center;
gap: 12px;
flex-wrap: wrap;
.search-box {
flex: 0 0 280px;
.search-input {
:deep(.el-input__wrapper) {
border-radius: 20px;
background: #f5f7fa;
box-shadow: none;
border: 1px solid transparent;
padding: 4px 16px;
transition: all 0.3s ease;
&:hover, &:focus-within {
background: #fff;
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.15);
}
}
.search-icon {
color: #909399;
font-size: 16px;
}
}
}
.filter-items {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
flex: 1;
.filter-select {
width: 130px;
:deep(.el-select__wrapper) {
border-radius: 8px;
background: #f5f7fa;
box-shadow: none;
border: 1px solid transparent;
transition: all 0.2s ease;
min-height: 36px;
&:hover {
background: #fff;
border-color: #dcdfe6;
}
&.is-focused {
background: #fff;
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
}
.select-prefix-icon {
font-size: 14px;
margin-right: 4px;
}
}
.date-picker {
:deep(.el-input__wrapper) {
border-radius: 8px;
background: #f5f7fa;
box-shadow: none;
border: 1px solid transparent;
&:hover {
background: #fff;
border-color: #dcdfe6;
}
}
}
}
.reset-btn {
border-radius: 8px;
padding: 8px 16px;
font-size: 13px;
.el-icon {
margin-right: 4px;
}
} }
} }
.filter-tags { .filter-tags {
margin-top: 16px; margin-top: 14px;
padding-top: 16px; padding-top: 14px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -865,34 +941,54 @@ const loadRecords = async () => {
gap: 8px; gap: 8px;
.filter-label { .filter-label {
color: #666; color: #606266;
font-size: 14px; font-size: 13px;
margin-right: 8px; font-weight: 500;
}
.el-tag {
border-radius: 16px;
padding: 0 12px;
height: 28px;
line-height: 26px;
font-size: 12px;
} }
.clear-all-btn { .clear-all-btn {
margin-left: 8px; margin-left: auto;
font-size: 12px;
} }
} }
.search-result-info { .search-result-info {
margin-top: 12px; margin-top: 14px;
padding-top: 12px; padding-top: 14px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.result-count { .result-count {
color: #666; color: #606266;
font-size: 14px; font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
strong { strong {
color: #409eff; color: #409eff;
font-weight: 600; font-weight: 600;
font-size: 18px;
} }
} }
.filter-hint { .filter-hint {
color: #e6a23c; color: #e6a23c;
font-size: 12px; font-size: 12px;
background: #fdf6ec;
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
} }
} }
} }
@@ -922,6 +1018,26 @@ const loadRecords = async () => {
justify-content: center; justify-content: center;
margin-top: 24px; margin-top: 24px;
} }
// 表格行悬浮效果
:deep(.el-table__row) {
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background-color: #f5f7fa;
}
}
// 操作按钮样式优化
:deep(.el-button--link) {
padding: 4px 8px;
&:hover {
background-color: rgba(64, 158, 255, 0.1);
border-radius: 4px;
}
}
} }
.replay-content { .replay-content {
@@ -1032,25 +1148,33 @@ const loadRecords = async () => {
// 响应式设计 // 响应式设计
@media (max-width: 768px) { @media (max-width: 768px) {
.practice-records-container { .practice-records-container {
.page-header { .stats-section {
flex-direction: column; grid-template-columns: repeat(2, 1fr);
align-items: flex-start; gap: 12px;
gap: 16px;
.header-actions {
width: 100%;
justify-content: flex-end;
}
} }
.stats-overview { .filter-section {
grid-template-columns: 1fr; .filter-toolbar {
} flex-direction: column;
align-items: stretch;
.filter-form { .search-box {
.el-form-item { flex: 1;
display: block; width: 100%;
margin-bottom: 16px !important;
.search-input {
width: 100%;
}
}
.filter-items {
width: 100%;
.filter-select {
flex: 1;
min-width: 100px;
}
}
} }
} }
@@ -1063,4 +1187,12 @@ const loadRecords = async () => {
} }
} }
} }
@media (max-width: 480px) {
.practice-records-container {
.stats-section {
grid-template-columns: 1fr;
}
}
}
</style> </style>