514 lines
15 KiB
Markdown
514 lines
15 KiB
Markdown
# 学习记录功能完善 - 实施步骤
|
||
|
||
## 🎯 目标
|
||
将"学习记录"功能统一到"成长记录",消除功能重复,明确数据来源
|
||
|
||
## 📋 实施步骤
|
||
|
||
### 步骤1:修改前端入口(5分钟)
|
||
|
||
**文件**:`peidu/uniapp/src/pages/user/index.vue`
|
||
|
||
找到学习记录入口,修改跳转路径:
|
||
|
||
```vue
|
||
<!-- 修改前 -->
|
||
<view class="menu-item" @click="goPage('/user-package/pages/user/learning-record')">
|
||
<text class="menu-icon">📚</text>
|
||
<text>学习记录</text>
|
||
<text class="arrow">></text>
|
||
</view>
|
||
|
||
<!-- 修改后 -->
|
||
<view class="menu-item" @click="goPage('/user-package/pages/growth/list')">
|
||
<text class="menu-icon">📚</text>
|
||
<text>成长记录</text>
|
||
<text class="arrow">></text>
|
||
</view>
|
||
```
|
||
|
||
### 步骤2:完善成长记录列表页(15分钟)
|
||
|
||
**文件**:`peidu/uniapp/src/user-package/pages/growth/list.vue`
|
||
|
||
#### 2.1 添加统计数据
|
||
|
||
在 `<template>` 的 `page-header` 后面添加:
|
||
|
||
```vue
|
||
<!-- 统计卡片 -->
|
||
<view class="stats-card">
|
||
<view class="stat-item">
|
||
<text class="stat-value">{{ stats.totalHours }}</text>
|
||
<text class="stat-label">累计学习(小时)</text>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<text class="stat-value">{{ stats.totalSessions }}</text>
|
||
<text class="stat-label">服务次数</text>
|
||
</view>
|
||
<view class="stat-divider"></view>
|
||
<view class="stat-item">
|
||
<text class="stat-value">{{ stats.avgScore }}</text>
|
||
<text class="stat-label">平均评分</text>
|
||
</view>
|
||
</view>
|
||
```
|
||
|
||
#### 2.2 添加data属性
|
||
|
||
```javascript
|
||
data() {
|
||
return {
|
||
// 新增统计数据
|
||
stats: {
|
||
totalHours: '0',
|
||
totalSessions: 0,
|
||
avgScore: '0'
|
||
},
|
||
// ... 保持原有数据
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.3 添加loadStats方法
|
||
|
||
```javascript
|
||
methods: {
|
||
// 新增:加载统计数据
|
||
async loadStats() {
|
||
try {
|
||
const res = await request.get('/api/growth-record/parent/stats', {
|
||
studentId: this.studentId
|
||
})
|
||
|
||
if (res.code === 200 && res.data) {
|
||
this.stats = {
|
||
totalHours: res.data.totalHours || '0',
|
||
totalSessions: res.data.totalSessions || 0,
|
||
avgScore: res.data.avgScore || '0'
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('加载统计数据失败:', e)
|
||
}
|
||
},
|
||
|
||
// ... 保持原有方法
|
||
}
|
||
```
|
||
|
||
#### 2.4 在onLoad中调用
|
||
|
||
```javascript
|
||
onLoad(options) {
|
||
console.log('=== 成长记录页面加载 ===')
|
||
this.studentId = options.studentId || this.getDefaultStudentId()
|
||
console.log('studentId:', this.studentId)
|
||
this.loadStats() // 新增这一行
|
||
this.loadRecordList()
|
||
}
|
||
```
|
||
|
||
#### 2.5 添加样式
|
||
|
||
在 `<style>` 中添加:
|
||
|
||
```scss
|
||
.stats-card {
|
||
background: linear-gradient(135deg, #2d9687 0%, #3da896 100%);
|
||
padding: 40rpx 30rpx;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
.stat-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
}
|
||
|
||
.stat-divider {
|
||
width: 2rpx;
|
||
height: 60rpx;
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 步骤3:添加后端统计接口(20分钟)
|
||
|
||
**文件**:`peidu/backend/src/main/java/com/peidu/controller/GrowthRecordController.java`
|
||
|
||
在类的开头添加必要的依赖注入:
|
||
|
||
```java
|
||
@Autowired
|
||
private com.peidu.mapper.StudentMapper studentMapper;
|
||
|
||
@Autowired
|
||
private com.peidu.mapper.TeacherMapper teacherMapper;
|
||
|
||
@Autowired
|
||
private com.peidu.mapper.ReviewMapper reviewMapper;
|
||
```
|
||
|
||
然后添加两个新接口(在文件末尾添加):
|
||
|
||
```java
|
||
/**
|
||
* 获取家长端统计数据
|
||
*/
|
||
@ApiOperation("获取家长端统计数据")
|
||
@GetMapping("/parent/stats")
|
||
public Result<Map<String, Object>> getParentStats(
|
||
@RequestParam Long studentId) {
|
||
try {
|
||
log.info("获取家长端统计数据: studentId={}", studentId);
|
||
|
||
// 查询该学生的所有成长记录
|
||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.peidu.entity.GrowthRecord> wrapper =
|
||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||
wrapper.eq("student_id", studentId)
|
||
.eq("record_type", "daily")
|
||
.orderByDesc("record_date");
|
||
|
||
java.util.List<com.peidu.entity.GrowthRecord> records = growthRecordService.list(wrapper);
|
||
|
||
// 统计数据
|
||
int totalMinutes = 0;
|
||
int totalSessions = records.size();
|
||
double totalRating = 0;
|
||
int ratingCount = 0;
|
||
|
||
for (com.peidu.entity.GrowthRecord record : records) {
|
||
// 计算服务时长
|
||
if (record.getOrderId() != null) {
|
||
com.peidu.entity.Order order = orderService.getById(record.getOrderId());
|
||
if (order != null) {
|
||
// 从签到记录获取实际服务时长
|
||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.peidu.entity.CheckInRecord> checkInWrapper =
|
||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||
checkInWrapper.eq("order_id", record.getOrderId())
|
||
.orderByAsc("check_time");
|
||
java.util.List<com.peidu.entity.CheckInRecord> checkInRecords = checkInRecordMapper.selectList(checkInWrapper);
|
||
|
||
java.time.LocalDateTime checkInTime = null;
|
||
java.time.LocalDateTime checkOutTime = null;
|
||
|
||
for (com.peidu.entity.CheckInRecord checkRecord : checkInRecords) {
|
||
if ("checkin".equals(checkRecord.getCheckType()) && checkInTime == null) {
|
||
checkInTime = checkRecord.getCheckTime();
|
||
} else if ("checkout".equals(checkRecord.getCheckType())) {
|
||
checkOutTime = checkRecord.getCheckTime();
|
||
}
|
||
}
|
||
|
||
if (checkInTime != null && checkOutTime != null) {
|
||
java.time.Duration duration = java.time.Duration.between(checkInTime, checkOutTime);
|
||
totalMinutes += (int) duration.toMinutes();
|
||
}
|
||
|
||
// 获取评分
|
||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.peidu.entity.Review> reviewWrapper =
|
||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||
reviewWrapper.eq("order_id", record.getOrderId());
|
||
com.peidu.entity.Review review = reviewMapper.selectOne(reviewWrapper);
|
||
if (review != null && review.getRating() != null) {
|
||
totalRating += review.getRating();
|
||
ratingCount++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 构建返回数据
|
||
java.util.Map<String, Object> stats = new java.util.HashMap<>();
|
||
stats.put("totalHours", String.format("%.1f", totalMinutes / 60.0));
|
||
stats.put("totalSessions", totalSessions);
|
||
stats.put("avgScore", ratingCount > 0 ?
|
||
String.format("%.1f", totalRating / ratingCount) : "0");
|
||
|
||
log.info("统计结果: {}", stats);
|
||
return Result.success(stats);
|
||
} catch (Exception e) {
|
||
log.error("获取统计数据失败", e);
|
||
return Result.error(e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取家长端成长记录列表
|
||
*/
|
||
@ApiOperation("获取家长端成长记录列表")
|
||
@GetMapping("/parent/list")
|
||
public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<GrowthRecordVO>> getParentList(
|
||
@RequestParam Long studentId,
|
||
@RequestParam(required = false) String recordType,
|
||
@RequestParam(defaultValue = "1") Integer page,
|
||
@RequestParam(defaultValue = "10") Integer size) {
|
||
try {
|
||
log.info("获取家长端成长记录列表: studentId={}, recordType={}, page={}, size={}",
|
||
studentId, recordType, page, size);
|
||
|
||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.peidu.entity.GrowthRecord> pageParam =
|
||
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
|
||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.peidu.entity.GrowthRecord> wrapper =
|
||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||
wrapper.eq("student_id", studentId);
|
||
|
||
if (recordType != null && !"all".equals(recordType)) {
|
||
wrapper.eq("record_type", recordType);
|
||
}
|
||
|
||
wrapper.orderByDesc("record_date");
|
||
|
||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.peidu.entity.GrowthRecord> recordPage =
|
||
growthRecordService.page(pageParam, wrapper);
|
||
|
||
// 转换为VO
|
||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<GrowthRecordVO> voPage =
|
||
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>();
|
||
voPage.setCurrent(recordPage.getCurrent());
|
||
voPage.setSize(recordPage.getSize());
|
||
voPage.setTotal(recordPage.getTotal());
|
||
|
||
java.util.List<GrowthRecordVO> voList = new java.util.ArrayList<>();
|
||
for (com.peidu.entity.GrowthRecord record : recordPage.getRecords()) {
|
||
GrowthRecordVO vo = convertToVO(record);
|
||
voList.add(vo);
|
||
}
|
||
voPage.setRecords(voList);
|
||
|
||
log.info("查询成功,共{}条记录", voPage.getTotal());
|
||
return Result.success(voPage);
|
||
} catch (Exception e) {
|
||
log.error("获取家长端成长记录列表失败", e);
|
||
return Result.error(e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 转换为VO对象
|
||
*/
|
||
private GrowthRecordVO convertToVO(com.peidu.entity.GrowthRecord record) {
|
||
GrowthRecordVO vo = new GrowthRecordVO();
|
||
vo.setId(record.getId());
|
||
vo.setRecordDate(record.getRecordDate());
|
||
vo.setRecordType(record.getRecordType());
|
||
vo.setContent(record.getContent());
|
||
vo.setImageList(record.getImageList());
|
||
vo.setVideoList(record.getVideoList());
|
||
|
||
// 设置类型名称
|
||
String typeName = "每日反馈";
|
||
if ("weekly".equals(record.getRecordType())) {
|
||
typeName = "周反馈";
|
||
} else if ("monthly".equals(record.getRecordType())) {
|
||
typeName = "月反馈";
|
||
}
|
||
vo.setRecordTypeName(typeName);
|
||
|
||
// 获取学生信息
|
||
if (record.getStudentId() != null) {
|
||
com.peidu.entity.Student student = studentMapper.selectById(record.getStudentId());
|
||
if (student != null) {
|
||
vo.setStudentName(student.getName());
|
||
}
|
||
}
|
||
|
||
// 获取教师信息
|
||
if (record.getTeacherId() != null) {
|
||
com.peidu.entity.Teacher teacher = teacherMapper.selectById(record.getTeacherId());
|
||
if (teacher != null) {
|
||
vo.setTeacherName(teacher.getName());
|
||
}
|
||
}
|
||
|
||
// 获取服务时长
|
||
if (record.getOrderId() != null) {
|
||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<com.peidu.entity.CheckInRecord> checkInWrapper =
|
||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||
checkInWrapper.eq("order_id", record.getOrderId())
|
||
.orderByAsc("check_time");
|
||
java.util.List<com.peidu.entity.CheckInRecord> checkInRecords = checkInRecordMapper.selectList(checkInWrapper);
|
||
|
||
java.time.LocalDateTime checkInTime = null;
|
||
java.time.LocalDateTime checkOutTime = null;
|
||
|
||
for (com.peidu.entity.CheckInRecord checkRecord : checkInRecords) {
|
||
if ("checkin".equals(checkRecord.getCheckType()) && checkInTime == null) {
|
||
checkInTime = checkRecord.getCheckTime();
|
||
} else if ("checkout".equals(checkRecord.getCheckType())) {
|
||
checkOutTime = checkRecord.getCheckTime();
|
||
}
|
||
}
|
||
|
||
if (checkInTime != null && checkOutTime != null) {
|
||
java.time.Duration duration = java.time.Duration.between(checkInTime, checkOutTime);
|
||
int minutes = (int) duration.toMinutes();
|
||
int hours = minutes / 60;
|
||
int mins = minutes % 60;
|
||
vo.setDurationText(hours > 0 ?
|
||
String.format("%d小时%d分钟", hours, mins) :
|
||
String.format("%d分钟", mins));
|
||
}
|
||
}
|
||
|
||
return vo;
|
||
}
|
||
```
|
||
|
||
### 步骤4:更新VO类(5分钟)
|
||
|
||
**文件**:`peidu/backend/src/main/java/com/peidu/vo/GrowthRecordVO.java`
|
||
|
||
添加新字段:
|
||
|
||
```java
|
||
/**
|
||
* 学生姓名
|
||
*/
|
||
private String studentName;
|
||
|
||
/**
|
||
* 教师姓名
|
||
*/
|
||
private String teacherName;
|
||
|
||
/**
|
||
* 记录类型名称
|
||
*/
|
||
private String recordTypeName;
|
||
|
||
/**
|
||
* 服务时长文本
|
||
*/
|
||
private String durationText;
|
||
```
|
||
|
||
### 步骤5:清理冗余代码(5分钟)
|
||
|
||
#### 5.1 删除学习记录页面文件
|
||
|
||
```bash
|
||
# 在项目根目录执行
|
||
del peidu\uniapp\src\user-package\pages\user\learning-record.vue
|
||
del peidu\uniapp\src\user-package\pages\user\learning-record-detail.vue
|
||
```
|
||
|
||
#### 5.2 清理API定义
|
||
|
||
**文件**:`peidu/uniapp/src/api/index.js`
|
||
|
||
找到并删除 `recordApi` 的定义:
|
||
|
||
```javascript
|
||
// 删除这部分代码(大约在436行)
|
||
export const recordApi = {
|
||
// 获取学习记录列表
|
||
getRecordList(params) {
|
||
return request.get('/api/record/list', { params })
|
||
},
|
||
|
||
// 获取学习记录详情
|
||
getRecordDetail(id) {
|
||
return request.get(`/api/record/detail/${id}`)
|
||
},
|
||
|
||
// 获取学习统计
|
||
getStats() {
|
||
return request.get('/api/record/stats')
|
||
}
|
||
}
|
||
```
|
||
|
||
同时删除导出列表中的 `recordApi`:
|
||
|
||
```javascript
|
||
// 找到export default,删除recordApi
|
||
export default {
|
||
// ... 其他API
|
||
// recordApi, // 删除这一行
|
||
// ... 其他API
|
||
}
|
||
```
|
||
|
||
### 步骤6:编译和测试(10分钟)
|
||
|
||
#### 6.1 编译后端
|
||
|
||
```bash
|
||
cd peidu/backend
|
||
mvn clean compile
|
||
```
|
||
|
||
#### 6.2 重启后端服务
|
||
|
||
```bash
|
||
# 停止现有服务
|
||
# 启动新服务
|
||
```
|
||
|
||
#### 6.3 编译前端
|
||
|
||
在HBuilderX中:
|
||
1. 右键项目
|
||
2. 选择"运行" → "运行到小程序模拟器"
|
||
3. 等待编译完成
|
||
|
||
#### 6.4 测试功能
|
||
|
||
1. **测试入口**
|
||
- 打开家长端
|
||
- 进入"我的"页面
|
||
- 点击"成长记录"
|
||
- ✅ 应该跳转到成长记录列表页
|
||
|
||
2. **测试统计数据**
|
||
- 查看顶部统计卡片
|
||
- ✅ 应该显示:累计学习时长、服务次数、平均评分
|
||
|
||
3. **测试列表数据**
|
||
- 查看记录列表
|
||
- ✅ 应该显示所有成长记录
|
||
- ✅ 每条记录应包含:日期、类型、学生、陪伴员、服务时长、内容预览
|
||
|
||
4. **测试详情页**
|
||
- 点击任意记录
|
||
- ✅ 应该显示完整的成长记录详情
|
||
|
||
## ✅ 验证清单
|
||
|
||
- [ ] 用户中心"成长记录"入口正常
|
||
- [ ] 统计数据显示正确
|
||
- [ ] 列表数据加载正常
|
||
- [ ] 详情页信息完整
|
||
- [ ] 服务时长计算准确
|
||
- [ ] 评分显示正确(如果有评价)
|
||
- [ ] 照片/视频正常显示
|
||
- [ ] 学习记录页面已删除
|
||
- [ ] recordApi已清理
|
||
- [ ] 后端编译无错误
|
||
- [ ] 前端编译无错误
|
||
|
||
## 🎉 完成标志
|
||
|
||
当所有验证项都通过后,学习记录功能完善工作即完成!
|
||
|
||
家长端现在拥有统一的"成长记录"功能,数据来源清晰,信息完整。
|