fix: task_feedback 表字段对齐与完成情况图片展示
- 新增 V47 迁移:为 task_feedback 添加 create_by、update_by、deleted 字段 - 修复 TaskFeedbackMapper 使用 deleted 替代 deleted_at - 教师端完成情况:将「X张照片」改为图片缩略图展示,支持点击预览 - 评价弹窗提交内容预览区显示图片网格,参考家长端提交页面 Made-with: Cursor
This commit is contained in:
parent
5cb903d7ed
commit
c55b2266fb
@ -264,8 +264,9 @@
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="completion-content">
|
||||
<div v-if="completion.photos && completion.photos.length > 0" class="content-preview">
|
||||
<PictureOutlined /> {{ completion.photos.length }} 张照片
|
||||
<div v-if="completion.photos && completion.photos.length > 0" class="content-photos">
|
||||
<img v-for="(photo, idx) in completion.photos" :key="idx" :src="photo" alt="照片"
|
||||
class="photo-thumb" @click="previewImage(photo)" />
|
||||
</div>
|
||||
<div v-if="completion.videoUrl" class="content-preview">
|
||||
<VideoCameraOutlined /> 有视频
|
||||
@ -323,9 +324,11 @@
|
||||
style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;">
|
||||
<h4 style="margin-bottom: 12px;">提交内容</h4>
|
||||
<div v-if="selectedCompletionForFeedback.photos && selectedCompletionForFeedback.photos.length > 0"
|
||||
style="margin-bottom: 12px;">
|
||||
<PictureOutlined style="margin-right: 8px;" />
|
||||
<span>{{ selectedCompletionForFeedback.photos.length }} 张照片</span>
|
||||
class="review-photos" style="margin-bottom: 12px;">
|
||||
<div class="photos-grid">
|
||||
<img v-for="(photo, idx) in selectedCompletionForFeedback.photos" :key="idx" :src="photo" alt="照片"
|
||||
@click="previewImage(photo)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedCompletionForFeedback.videoUrl" style="margin-bottom: 12px;">
|
||||
<VideoCameraOutlined style="margin-right: 8px;" />
|
||||
@ -366,6 +369,12 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<a-image :style="{ display: 'none' }" :preview="{
|
||||
visible: imagePreviewVisible,
|
||||
onVisibleChange: (visible: boolean) => { imagePreviewVisible = visible; },
|
||||
}" :src="previewImageUrl" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -900,6 +909,14 @@ const onCompletionPageChange = (page: number) => {
|
||||
loadCompletions();
|
||||
};
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
const previewImage = (url: string) => {
|
||||
previewImageUrl.value = url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadTasks();
|
||||
loadOptions();
|
||||
@ -1189,5 +1206,46 @@ onMounted(() => {
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.completion-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.content-photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
|
||||
.photo-thumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 评价弹窗 - 提交内容照片网格
|
||||
.review-photos .photos-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -17,7 +17,7 @@ public interface TaskFeedbackMapper extends BaseMapper<TaskFeedback> {
|
||||
/**
|
||||
* 根据完成记录ID查询评价
|
||||
*/
|
||||
@Select("SELECT * FROM task_feedback WHERE completion_id = #{completionId} AND deleted_at IS NULL")
|
||||
@Select("SELECT * FROM task_feedback WHERE completion_id = #{completionId} AND deleted = 0")
|
||||
Optional<TaskFeedback> findByCompletionId(@Param("completionId") Long completionId);
|
||||
|
||||
/**
|
||||
@ -25,7 +25,7 @@ public interface TaskFeedbackMapper extends BaseMapper<TaskFeedback> {
|
||||
*/
|
||||
@Select("SELECT tf.* FROM task_feedback tf " +
|
||||
"INNER JOIN task_completion tc ON tf.completion_id = tc.id " +
|
||||
"WHERE tf.task_id = #{taskId} AND tc.student_id = #{studentId} AND tf.deleted_at IS NULL")
|
||||
"WHERE tf.task_id = #{taskId} AND tc.student_id = #{studentId} AND tf.deleted = 0")
|
||||
Optional<TaskFeedback> findByTaskIdAndStudentId(@Param("taskId") Long taskId, @Param("studentId") Long studentId);
|
||||
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.dto.response.TaskWithCompletionResponse;
|
||||
import com.reading.platform.entity.*;
|
||||
import com.reading.platform.mapper.CoursePackageMapper;
|
||||
import com.reading.platform.mapper.ParentStudentMapper;
|
||||
import com.reading.platform.mapper.TaskCompletionMapper;
|
||||
import com.reading.platform.mapper.TaskMapper;
|
||||
import com.reading.platform.mapper.TaskTargetMapper;
|
||||
@ -45,6 +46,7 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
private final TaskTargetMapper taskTargetMapper;
|
||||
private final TaskCompletionMapper taskCompletionMapper;
|
||||
private final CoursePackageMapper coursePackageMapper;
|
||||
private final ParentStudentMapper parentStudentMapper;
|
||||
private final StudentService studentService;
|
||||
private final ClassService classService;
|
||||
private final TaskFeedbackService taskFeedbackService;
|
||||
@ -392,6 +394,11 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
// 验证任务存在且属于该租户
|
||||
Task task = getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
|
||||
// status=PENDING 待提交:返回参与任务且与家长关联、尚未提交的学生(无 task_completion 记录)
|
||||
if ("PENDING".equalsIgnoreCase(status)) {
|
||||
return getPendingCompletions(taskId, tenantId, pageNum, pageSize, task);
|
||||
}
|
||||
|
||||
Page<TaskCompletion> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<TaskCompletion> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TaskCompletion::getTaskId, taskId);
|
||||
@ -410,6 +417,105 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
return PageResult.of(responses, completionPage.getTotal(), completionPage.getCurrent(), completionPage.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待提交学生列表:参与任务(班级或指定学生)且与家长关联、尚未提交的学生
|
||||
*/
|
||||
private PageResult<TaskCompletionDetailResponse> getPendingCompletions(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, Task task) {
|
||||
List<Long> targetStudentIds = getTargetStudentIdsForTask(taskId);
|
||||
if (targetStudentIds.isEmpty()) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
|
||||
// 仅保留与家长关联的学生(parent_student 有记录)
|
||||
List<Long> studentIdsWithParent = parentStudentMapper.selectList(
|
||||
new LambdaQueryWrapper<ParentStudent>()
|
||||
.in(ParentStudent::getStudentId, targetStudentIds)
|
||||
).stream().map(ParentStudent::getStudentId).distinct().toList();
|
||||
|
||||
if (studentIdsWithParent.isEmpty()) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
|
||||
// 排除已提交或已评价的学生(已有 task_completion 且 status 非 PENDING)
|
||||
List<TaskCompletion> existing = taskCompletionMapper.selectList(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStudentId, studentIdsWithParent)
|
||||
.in(TaskCompletion::getStatus, "SUBMITTED", "REVIEWED", "submitted", "reviewed")
|
||||
);
|
||||
List<Long> submittedStudentIds = existing.stream().map(TaskCompletion::getStudentId).distinct().toList();
|
||||
List<Long> pendingStudentIds = studentIdsWithParent.stream()
|
||||
.filter(id -> !submittedStudentIds.contains(id))
|
||||
.toList();
|
||||
|
||||
// 分页
|
||||
int from = (pageNum - 1) * pageSize;
|
||||
int to = Math.min(from + pageSize, pendingStudentIds.size());
|
||||
List<Long> pagedIds = from < pendingStudentIds.size() ? pendingStudentIds.subList(from, to) : List.of();
|
||||
|
||||
List<TaskCompletionDetailResponse> responses = pagedIds.stream()
|
||||
.map(studentId -> buildPendingCompletionResponse(taskId, task.getTitle(), studentId))
|
||||
.toList();
|
||||
|
||||
return PageResult.of(responses, (long) pendingStudentIds.size(), (long) pageNum, (long) pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务目标学生 ID 列表(班级学生或指定学生)
|
||||
*/
|
||||
private List<Long> getTargetStudentIdsForTask(Long taskId) {
|
||||
List<TaskTarget> targets = taskTargetMapper.selectList(
|
||||
new LambdaQueryWrapper<TaskTarget>().eq(TaskTarget::getTaskId, taskId)
|
||||
);
|
||||
if (targets.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<Long> studentIds = new ArrayList<>();
|
||||
for (TaskTarget t : targets) {
|
||||
String tt = t.getTargetType() != null ? t.getTargetType().toUpperCase() : "";
|
||||
if ("CLASS".equals(tt) || "class".equals(t.getTargetType())) {
|
||||
studentIds.addAll(studentService.getStudentListByClassId(t.getTargetId()).stream()
|
||||
.map(Student::getId).toList());
|
||||
} else if ("STUDENT".equals(tt) || "student".equals(t.getTargetType())) {
|
||||
studentIds.add(t.getTargetId());
|
||||
}
|
||||
}
|
||||
return studentIds.stream().distinct().toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建待提交的完成详情(无 task_completion 记录)
|
||||
*/
|
||||
private TaskCompletionDetailResponse buildPendingCompletionResponse(Long taskId, String taskTitle, Long studentId) {
|
||||
Student student = studentService.getById(studentId);
|
||||
if (student == null) {
|
||||
return null;
|
||||
}
|
||||
TaskCompletionDetailResponse.StudentInfo studentInfo = TaskCompletionDetailResponse.StudentInfo.builder()
|
||||
.id(student.getId())
|
||||
.name(student.getName())
|
||||
.avatar(student.getAvatarUrl())
|
||||
.gender(student.getGender())
|
||||
.build();
|
||||
Clazz clazz = classService.getPrimaryClassByStudentId(student.getId());
|
||||
if (clazz != null) {
|
||||
studentInfo.setClassInfo(TaskCompletionDetailResponse.ClassInfo.builder()
|
||||
.id(clazz.getId())
|
||||
.name(clazz.getName())
|
||||
.grade(clazz.getGrade())
|
||||
.build());
|
||||
}
|
||||
return TaskCompletionDetailResponse.builder()
|
||||
.id(null)
|
||||
.taskId(taskId)
|
||||
.taskTitle(taskTitle)
|
||||
.student(studentInfo)
|
||||
.status("PENDING")
|
||||
.statusText("待提交")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskCompletionDetailResponse getCompletionDetail(Long completionId, Long tenantId) {
|
||||
TaskCompletion completion = taskCompletionMapper.selectById(completionId);
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
-- =====================================================
|
||||
-- 修复 task_feedback 表与 BaseEntity 字段对齐
|
||||
-- 版本: V47
|
||||
-- 日期: 2026-03-20
|
||||
-- 说明: 添加 create_by、update_by,将 deleted_at 改为 deleted
|
||||
-- 解决 POST /api/v1/teacher/tasks/completions/{id}/feedback 报错
|
||||
-- "Unknown column 'create_by' in 'field list'"
|
||||
-- =====================================================
|
||||
|
||||
-- 1. 添加 create_by、update_by 审计字段
|
||||
ALTER TABLE task_feedback ADD COLUMN create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人';
|
||||
ALTER TABLE task_feedback ADD COLUMN update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人';
|
||||
|
||||
-- 2. 添加 deleted 字段(与 BaseEntity 一致)
|
||||
ALTER TABLE task_feedback ADD COLUMN deleted TINYINT DEFAULT 0 COMMENT '删除标识(0-未删除,1-已删除)';
|
||||
|
||||
-- 3. 迁移 deleted_at 数据到 deleted
|
||||
UPDATE task_feedback SET deleted = 1 WHERE deleted_at IS NOT NULL;
|
||||
|
||||
-- 4. 删除 deleted_at 列
|
||||
ALTER TABLE task_feedback DROP COLUMN deleted_at;
|
||||
Loading…
Reference in New Issue
Block a user