feat: 阅读任务模块 - 关联绘本、任务模板与教师端优化

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-20 15:55:33 +08:00
parent de742d9acf
commit 463c3d9922
9 changed files with 189 additions and 147 deletions

View File

@ -819,9 +819,9 @@ export interface CreateTaskFromTemplateDto {
} }
export const getTaskTemplates = (params?: { pageNum?: number; pageSize?: number }) => export const getTaskTemplates = (params?: { pageNum?: number; pageSize?: number }) =>
http.get<{ records: any[]; total: number }>('/v1/teacher/task-templates', { params }) http.get<{ list: any[]; total: number }>('/v1/teacher/task-templates', { params })
.then(res => ({ .then(res => ({
items: res.records || [], items: res.list || [],
total: res.total || 0, total: res.total || 0,
})); }));

View File

@ -2,7 +2,9 @@
<div class="task-list-view"> <div class="task-list-view">
<div class="page-header"> <div class="page-header">
<div class="header-left"> <div class="header-left">
<h2><CheckSquareOutlined /> 阅读任务</h2> <h2>
<CheckSquareOutlined /> 阅读任务
</h2>
<p class="page-desc">管理班级阅读任务跟踪学生完成情况</p> <p class="page-desc">管理班级阅读任务跟踪学生完成情况</p>
</div> </div>
<a-button type="primary" @click="openCreateModal"> <a-button type="primary" @click="openCreateModal">
@ -44,35 +46,20 @@
<!-- 筛选区域 --> <!-- 筛选区域 -->
<div class="filter-section"> <div class="filter-section">
<a-space :size="16"> <a-space :size="16">
<a-select <a-select v-model:value="filters.status" placeholder="任务状态" style="width: 120px;" allowClear
v-model:value="filters.status" @change="loadTasks">
placeholder="任务状态"
style="width: 120px;"
allowClear
@change="loadTasks"
>
<a-select-option value="PUBLISHED">进行中</a-select-option> <a-select-option value="PUBLISHED">进行中</a-select-option>
<a-select-option value="DRAFT">草稿</a-select-option> <a-select-option value="DRAFT">草稿</a-select-option>
<a-select-option value="ARCHIVED">已归档</a-select-option> <a-select-option value="ARCHIVED">已归档</a-select-option>
</a-select> </a-select>
<a-select <a-select v-model:value="filters.taskType" placeholder="任务类型" style="width: 120px;" allowClear
v-model:value="filters.taskType" @change="loadTasks">
placeholder="任务类型"
style="width: 120px;"
allowClear
@change="loadTasks"
>
<a-select-option value="READING">阅读</a-select-option> <a-select-option value="READING">阅读</a-select-option>
<a-select-option value="ACTIVITY">活动</a-select-option> <a-select-option value="ACTIVITY">活动</a-select-option>
<a-select-option value="HOMEWORK">作业</a-select-option> <a-select-option value="HOMEWORK">作业</a-select-option>
</a-select> </a-select>
<a-input-search <a-input-search v-model:value="filters.keyword" placeholder="搜索任务标题" style="width: 200px;" @search="loadTasks"
v-model:value="filters.keyword" allow-clear />
placeholder="搜索任务标题"
style="width: 200px;"
@search="loadTasks"
allow-clear
/>
</a-space> </a-space>
</div> </div>
@ -105,12 +92,8 @@
<div class="card-footer"> <div class="card-footer">
<div class="progress-info"> <div class="progress-info">
<a-progress <a-progress :percent="getCompletionRate(task)" :stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
:percent="getCompletionRate(task)" size="small" style="width: 150px;" />
:stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
size="small"
style="width: 150px;"
/>
<span class="progress-text">{{ getCompletedCount(task) }}/{{ task.targetCount || 0 }} 人完成</span> <span class="progress-text">{{ getCompletedCount(task) }}/{{ task.targetCount || 0 }} 人完成</span>
</div> </div>
<div class="card-actions"> <div class="card-actions">
@ -154,34 +137,18 @@
</a-spin> </a-spin>
<div class="pagination-section" v-if="total > pageSize"> <div class="pagination-section" v-if="total > pageSize">
<a-pagination <a-pagination v-model:current="currentPage" :total="total" :page-size="pageSize" @change="onPageChange"
v-model:current="currentPage" show-quick-jumper :show-total="(total: number) => `共 ${total} 条`" />
:total="total"
:page-size="pageSize"
@change="onPageChange"
show-quick-jumper
:show-total="(total: number) => `共 ${total} 条`"
/>
</div> </div>
<!-- 创建/编辑任务弹窗 --> <!-- 创建/编辑任务弹窗 -->
<a-modal <a-modal v-model:open="createModalVisible" :title="isEdit ? '编辑任务' : '新建阅读任务'" @ok="handleCreate"
v-model:open="createModalVisible" :confirm-loading="creating" width="600px">
:title="isEdit ? '编辑任务' : '新建阅读任务'"
@ok="handleCreate"
:confirm-loading="creating"
width="600px"
>
<a-form :model="createForm" layout="vertical"> <a-form :model="createForm" layout="vertical">
<!-- 模板选择仅新建时显示 --> <!-- 模板选择仅新建时显示 -->
<a-form-item label="使用模板" v-if="!isEdit"> <a-form-item label="使用模板" v-if="!isEdit">
<a-select <a-select v-model:value="selectedTemplateId" placeholder="选择模板快速填充(可选)" style="width: 100%;" allowClear
v-model:value="selectedTemplateId" @change="onTemplateSelect as any">
placeholder="选择模板快速填充(可选)"
style="width: 100%;"
allowClear
@change="onTemplateSelect as any"
>
<a-select-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id"> <a-select-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id">
{{ tpl.name }} {{ tpl.name }}
<a-tag size="small" :color="getTaskTypeColor(tpl.taskType)" style="margin-left: 8px;"> <a-tag size="small" :color="getTaskTypeColor(tpl.taskType)" style="margin-left: 8px;">
@ -216,77 +183,44 @@
</a-col> </a-col>
</a-row> </a-row>
<a-form-item label="选择目标" required v-if="createForm.targetType === 'CLASS'"> <a-form-item label="选择目标" required v-if="createForm.targetType === 'CLASS'">
<a-select <a-select v-model:value="createForm.targetIds" mode="multiple" placeholder="请选择班级" style="width: 100%;">
v-model:value="createForm.targetIds"
mode="multiple"
placeholder="请选择班级"
style="width: 100%;"
>
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }} ({{ cls.grade }}) {{ cls.name }} ({{ cls.grade }})
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="选择学生" required v-if="createForm.targetType === 'STUDENT'"> <a-form-item label="选择学生" required v-if="createForm.targetType === 'STUDENT'">
<a-select <a-select v-model:value="createForm.targetIds" mode="multiple" placeholder="请选择学生" style="width: 100%;"
v-model:value="createForm.targetIds" :filter-option="filterStudentOption" show-search>
mode="multiple"
placeholder="请选择学生"
style="width: 100%;"
:filter-option="filterStudentOption"
show-search
>
<a-select-option v-for="student in students" :key="student.id" :value="student.id"> <a-select-option v-for="student in students" :key="student.id" :value="student.id">
{{ student.name }} - {{ student.class?.name }} {{ student.name }} - {{ student.class?.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="关联课程"> <a-form-item label="关联课程">
<a-select <a-select v-model:value="createForm.relatedCourseId" placeholder="可选,关联课程包" style="width: 100%;" allowClear
v-model:value="createForm.relatedCourseId" show-search :filter-option="filterCourseOption">
placeholder="可选,关联课程包"
style="width: 100%;"
allowClear
show-search
:filter-option="filterCourseOption"
>
<a-select-option v-for="course in courses" :key="course.id" :value="course.id"> <a-select-option v-for="course in courses" :key="course.id" :value="course.id">
{{ course.name }} {{ course.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="关联绘本"> <a-form-item label="关联绘本">
<a-input <a-input v-model:value="createForm.relatedBookName" placeholder="请输入关联的绘本名称(可选)" allow-clear />
v-model:value="createForm.relatedBookName"
placeholder="请输入关联的绘本名称(可选)"
allow-clear
/>
</a-form-item> </a-form-item>
<a-form-item label="任务时间" required> <a-form-item label="任务时间" required>
<a-range-picker <a-range-picker v-model:value="createForm.dateRange as any" style="width: 100%;" />
v-model:value="createForm.dateRange as any"
style="width: 100%;"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<!-- 完成情况详情弹窗 --> <!-- 完成情况详情弹窗 -->
<a-modal <a-modal v-model:open="completionModalVisible" :title="`完成情况 - ${selectedTask?.title || ''}`" width="800px"
v-model:open="completionModalVisible" :footer="null">
:title="`完成情况 - ${selectedTask?.title || ''}`"
width="800px"
:footer="null"
>
<div class="completion-header"> <div class="completion-header">
<a-space> <a-space>
<a-select <a-select v-model:value="completionFilter.status" placeholder="筛选状态" style="width: 120px;" allowClear
v-model:value="completionFilter.status" @change="loadCompletions">
placeholder="筛选状态"
style="width: 120px;"
allowClear
@change="loadCompletions"
>
<a-select-option value="PENDING">待提交</a-select-option> <a-select-option value="PENDING">待提交</a-select-option>
<a-select-option value="SUBMITTED">已提交</a-select-option> <a-select-option value="SUBMITTED">已提交</a-select-option>
<a-select-option value="REVIEWED">已评价</a-select-option> <a-select-option value="REVIEWED">已评价</a-select-option>
@ -308,7 +242,9 @@
</a-avatar> </a-avatar>
<div class="student-detail"> <div class="student-detail">
<div class="student-name">{{ completion.student.name }}</div> <div class="student-name">{{ completion.student.name }}</div>
<div class="student-class">{{ completion.student.class?.name || completion.student.classInfo?.name || '-' }}</div> <div class="student-class">{{ completion.student.class?.name || completion.student.classInfo?.name ||
'-' }}
</div>
</div> </div>
</div> </div>
<div class="completion-status"> <div class="completion-status">
@ -331,7 +267,10 @@
<div v-if="completion.parentFeedback || completion.feedback?.comment" class="parent-feedback"> <div v-if="completion.parentFeedback || completion.feedback?.comment" class="parent-feedback">
<MessageOutlined class="feedback-icon" /> <MessageOutlined class="feedback-icon" />
<a-tooltip :title="completion.parentFeedback || completion.feedback?.comment"> <a-tooltip :title="completion.parentFeedback || completion.feedback?.comment">
<span class="feedback-text">{{ (completion.parentFeedback || completion.feedback?.comment || '').substring(0, 30) }}{{ (completion.parentFeedback || completion.feedback?.comment || '').length > 30 ? '...' : '' }}</span> <span class="feedback-text">{{ (completion.parentFeedback || completion.feedback?.comment ||
'').substring(0, 30) }}{{ (completion.parentFeedback || completion.feedback?.comment || '').length >
30
? '...' : '' }}</span>
</a-tooltip> </a-tooltip>
</div> </div>
<span v-else class="no-feedback">暂无反馈</span> <span v-else class="no-feedback">暂无反馈</span>
@ -351,38 +290,29 @@
</div> </div>
</div> </div>
</div> </div>
<div class="empty-state" v-else style="padding: 40px 0;"> <div class="completion-empty-state" v-else>
<InboxOutlined style="font-size: 48px; color: #d9d9d9;" /> <InboxOutlined class="empty-icon" />
<p>暂无完成记录</p> <p>暂无完成记录</p>
</div> </div>
</a-spin> </a-spin>
<div class="pagination-section" v-if="completionTotal > completionPageSize" style="margin-top: 16px;"> <div class="pagination-section" v-if="completionTotal > completionPageSize" style="margin-top: 16px;">
<a-pagination <a-pagination v-model:current="completionPage" :total="completionTotal" :page-size="completionPageSize"
v-model:current="completionPage" size="small" @change="onCompletionPageChange" />
:total="completionTotal"
:page-size="completionPageSize"
size="small"
@change="onCompletionPageChange"
/>
</div> </div>
</a-modal> </a-modal>
<!-- 评价弹窗 --> <!-- 评价弹窗 -->
<a-modal <a-modal v-model:open="feedbackModalVisible"
v-model:open="feedbackModalVisible"
:title="selectedCompletionForFeedback ? `评价 - ${selectedCompletionForFeedback.student?.name || ''}` : '评价'" :title="selectedCompletionForFeedback ? `评价 - ${selectedCompletionForFeedback.student?.name || ''}` : '评价'"
width="600px" width="600px" :confirm-loading="submittingFeedback" @ok="submitFeedback" okText="提交评价" cancelText="取消">
:confirm-loading="submittingFeedback"
@ok="submitFeedback"
okText="提交评价"
cancelText="取消"
>
<a-form layout="vertical" v-if="selectedCompletionForFeedback"> <a-form layout="vertical" v-if="selectedCompletionForFeedback">
<!-- 提交内容预览 --> <!-- 提交内容预览 -->
<div class="submission-preview" style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;"> <div class="submission-preview"
style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;">
<h4 style="margin-bottom: 12px;">提交内容</h4> <h4 style="margin-bottom: 12px;">提交内容</h4>
<div v-if="selectedCompletionForFeedback.photos && selectedCompletionForFeedback.photos.length > 0" style="margin-bottom: 12px;"> <div v-if="selectedCompletionForFeedback.photos && selectedCompletionForFeedback.photos.length > 0"
style="margin-bottom: 12px;">
<PictureOutlined style="margin-right: 8px;" /> <PictureOutlined style="margin-right: 8px;" />
<span>{{ selectedCompletionForFeedback.photos.length }} 张照片</span> <span>{{ selectedCompletionForFeedback.photos.length }} 张照片</span>
</div> </div>
@ -420,13 +350,8 @@
</a-form-item> </a-form-item>
<a-form-item label="评语"> <a-form-item label="评语">
<a-textarea <a-textarea v-model:value="feedbackForm.comment" placeholder="请输入评语选填最多500字" :rows="4" :maxlength="500"
v-model:value="feedbackForm.comment" show-count />
placeholder="请输入评语选填最多500字"
:rows="4"
:maxlength="500"
show-count
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@ -461,6 +386,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { import {
getTeacherTasks, getTeacherTasks,
getTeacherTask,
createTeacherTask, createTeacherTask,
updateTeacherTask, updateTeacherTask,
deleteTeacherTask, deleteTeacherTask,
@ -694,21 +620,32 @@ const getTaskTypeText = (type: string) => {
return texts[type] || type; return texts[type] || type;
}; };
// //
const openEditModal = (task: TeacherTask) => { const openEditModal = async (task: TeacherTask) => {
isEdit.value = true; isEdit.value = true;
editTaskId.value = task.id; editTaskId.value = task.id;
createForm.title = task.title;
createForm.description = task.description || '';
createForm.taskType = task.taskType;
createForm.targetType = task.targetType;
createForm.relatedCourseId = task.relatedCourseId;
createForm.relatedBookName = task.relatedBookName || ''; //
createForm.dateRange = [
dayjs(task.startDate),
dayjs(task.endDate),
];
createModalVisible.value = true; createModalVisible.value = true;
try {
await loadOptions(); //
const detail = await getTeacherTask(task.id);
//
createForm.title = detail.title || '';
createForm.description = detail.description || '';
createForm.taskType = (detail.type || detail.taskType || 'READING') as 'READING' | 'ACTIVITY' | 'HOMEWORK';
createForm.targetType = (detail.targetType?.toUpperCase?.() || 'CLASS') as 'CLASS' | 'STUDENT';
createForm.targetIds = detail.targetIds || [];
// number API string a-select
const rawCourseId = detail.courseId ?? detail.relatedCourseId;
createForm.relatedCourseId = rawCourseId;
createForm.relatedBookName = detail.relatedBookName || '';
// dueDate startDate
createForm.dateRange = detail.startDate
? [dayjs(detail.startDate), dayjs(detail.dueDate || detail.startDate)]
: null;
} catch (error: any) {
message.error(error.message || '加载任务详情失败');
createModalVisible.value = false;
}
}; };
// / // /
@ -1104,6 +1041,26 @@ onMounted(() => {
} }
} }
.completion-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 60px 20px;
color: #999;
.empty-icon {
font-size: 48px;
color: #d9d9d9;
margin-bottom: 16px;
}
p {
margin: 0;
}
}
.completion-list { .completion-list {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;

View File

@ -51,8 +51,9 @@ public class TeacherTaskController {
@Operation(summary = "根据 ID 获取任务") @Operation(summary = "根据 ID 获取任务")
@GetMapping("/{id}") @GetMapping("/{id}")
public Result<TaskResponse> getTask(@PathVariable Long id) { public Result<TaskResponse> getTask(@PathVariable Long id) {
Task task = taskService.getTaskById(id); Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(taskMapper.toVO(task)); TaskResponse detail = taskService.getTaskDetailWithTargets(id, tenantId);
return Result.success(detail);
} }
@Operation(summary = "获取任务分页列表") @Operation(summary = "获取任务分页列表")
@ -62,9 +63,11 @@ public class TeacherTaskController {
@RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword, @RequestParam(required = false) String keyword,
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@RequestParam(required = false) String taskType,
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Page<Task> page = taskService.getTaskPage(tenantId, pageNum, pageSize, keyword, type, status); String typeFilter = (type != null && !type.isEmpty()) ? type : taskType;
Page<Task> page = taskService.getTaskPage(tenantId, pageNum, pageSize, keyword, typeFilter, status);
List<TaskResponse> voList = taskMapper.toVO(page.getRecords()); List<TaskResponse> voList = taskMapper.toVO(page.getRecords());
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
} }

View File

@ -41,8 +41,9 @@ public class TeacherTaskTemplateController {
@RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String type, @RequestParam(required = false) String type,
@RequestParam(required = false) String keyword) { @RequestParam(required = false) String keyword) {
// 教师查询其关联学校tenant下的所有模板包括学校端建立的阅读模版
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Page<TaskTemplate> page = taskTemplateService.getTemplates(tenantId, pageNum, pageSize, type, keyword, true); Page<TaskTemplate> page = taskTemplateService.getTemplates(tenantId, pageNum, pageSize, type, keyword, false);
List<TaskTemplateResponse> voList = taskTemplateMapper.toVO(page.getRecords()); List<TaskTemplateResponse> voList = taskTemplateMapper.toVO(page.getRecords());
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
} }

View File

@ -21,18 +21,27 @@ public class TaskCreateRequest {
@Schema(description = "任务类型reading-阅读homework-作业activity-活动") @Schema(description = "任务类型reading-阅读homework-作业activity-活动")
private String type; private String type;
@Schema(description = "任务类型(前端兼容 taskType")
private String taskType;
@Schema(description = "关联绘本名称(手动填写)") @Schema(description = "关联绘本名称(手动填写)")
private String relatedBookName; private String relatedBookName;
@Schema(description = "课程 ID") @Schema(description = "课程 ID")
private Long courseId; private Long courseId;
@Schema(description = "课程 ID前端兼容 relatedCourseId")
private Long relatedCourseId;
@Schema(description = "开始日期") @Schema(description = "开始日期")
private LocalDate startDate; private LocalDate startDate;
@Schema(description = "截止日期") @Schema(description = "截止日期")
private LocalDate dueDate; private LocalDate dueDate;
@Schema(description = "截止日期(前端兼容 endDate")
private LocalDate endDate;
@Schema(description = "附件JSON 数组)") @Schema(description = "附件JSON 数组)")
private String attachments; private String attachments;

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
@Data @Data
@Schema(description = "任务更新请求") @Schema(description = "任务更新请求")
@ -18,6 +19,18 @@ public class TaskUpdateRequest {
@Schema(description = "任务类型") @Schema(description = "任务类型")
private String type; private String type;
@Schema(description = "任务类型(前端兼容 taskType")
private String taskType;
@Schema(description = "关联绘本名称")
private String relatedBookName;
@Schema(description = "课程 ID")
private Long courseId;
@Schema(description = "课程 ID前端兼容 relatedCourseId")
private Long relatedCourseId;
@Schema(description = "开始日期") @Schema(description = "开始日期")
private LocalDate startDate; private LocalDate startDate;
@ -30,4 +43,10 @@ public class TaskUpdateRequest {
@Schema(description = "附件JSON 数组)") @Schema(description = "附件JSON 数组)")
private String attachments; private String attachments;
@Schema(description = "目标类型CLASS-班级STUDENT-学生")
private String targetType;
@Schema(description = "目标 IDs")
private List<Long> targetIds;
} }

View File

@ -55,6 +55,12 @@ public class TaskResponse {
@Schema(description = "附件") @Schema(description = "附件")
private String attachments; private String attachments;
@Schema(description = "目标类型CLASS-班级STUDENT-学生")
private String targetType;
@Schema(description = "目标 IDs班级或学生")
private java.util.List<Long> targetIds;
@Schema(description = "创建时间") @Schema(description = "创建时间")
private LocalDateTime createdAt; private LocalDateTime createdAt;

View File

@ -71,4 +71,9 @@ public interface TaskService extends com.baomidou.mybatisplus.extension.service.
*/ */
TaskResponse getTaskDetailForSchool(Long taskId, Long tenantId); TaskResponse getTaskDetailForSchool(Long taskId, Long tenantId);
/**
* 获取任务详情含目标类型目标列表用于编辑回显
*/
TaskResponse getTaskDetailWithTargets(Long taskId, Long tenantId);
} }

View File

@ -25,6 +25,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -51,13 +52,16 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
task.setTenantId(tenantId); task.setTenantId(tenantId);
task.setTitle(request.getTitle()); task.setTitle(request.getTitle());
task.setDescription(request.getDescription()); task.setDescription(request.getDescription());
task.setType(request.getType() != null ? request.getType() : "homework"); String taskType = request.getType() != null ? request.getType() : request.getTaskType();
task.setType(taskType != null ? taskType : "homework");
task.setRelatedBookName(request.getRelatedBookName()); // 添加关联绘本名称 task.setRelatedBookName(request.getRelatedBookName()); // 添加关联绘本名称
task.setCourseId(request.getCourseId()); Long courseId = request.getCourseId() != null ? request.getCourseId() : request.getRelatedCourseId();
task.setCourseId(courseId);
task.setCreatorId(creatorId); task.setCreatorId(creatorId);
task.setCreatorRole(creatorRole); task.setCreatorRole(creatorRole);
task.setStartDate(request.getStartDate()); task.setStartDate(request.getStartDate());
task.setDueDate(request.getDueDate()); LocalDate dueDate = request.getDueDate() != null ? request.getDueDate() : request.getEndDate();
task.setDueDate(dueDate);
task.setStatus("pending"); task.setStatus("pending");
task.setAttachments(request.getAttachments()); task.setAttachments(request.getAttachments());
@ -89,8 +93,16 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
if (request.getDescription() != null) { if (request.getDescription() != null) {
task.setDescription(request.getDescription()); task.setDescription(request.getDescription());
} }
if (request.getType() != null) { String taskType = request.getType() != null ? request.getType() : request.getTaskType();
task.setType(request.getType()); if (taskType != null) {
task.setType(taskType);
}
if (request.getRelatedBookName() != null) {
task.setRelatedBookName(request.getRelatedBookName());
}
Long courseId = request.getCourseId() != null ? request.getCourseId() : request.getRelatedCourseId();
if (courseId != null) {
task.setCourseId(courseId);
} }
if (request.getStartDate() != null) { if (request.getStartDate() != null) {
task.setStartDate(request.getStartDate()); task.setStartDate(request.getStartDate());
@ -106,6 +118,22 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
} }
taskMapper.updateById(task); taskMapper.updateById(task);
// 更新任务目标
if (request.getTargetIds() != null) {
taskTargetMapper.delete(new LambdaQueryWrapper<TaskTarget>().eq(TaskTarget::getTaskId, id));
if (!request.getTargetIds().isEmpty()) {
String targetType = StringUtils.hasText(request.getTargetType()) ? request.getTargetType() : "CLASS";
for (Long targetId : request.getTargetIds()) {
TaskTarget target = new TaskTarget();
target.setTaskId(id);
target.setTargetType(targetType);
target.setTargetId(targetId);
taskTargetMapper.insert(target);
}
}
}
log.info("任务更新成功id={}", id); log.info("任务更新成功id={}", id);
return task; return task;
} }
@ -518,10 +546,22 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
@Override @Override
public TaskResponse getTaskDetailForSchool(Long taskId, Long tenantId) { public TaskResponse getTaskDetailForSchool(Long taskId, Long tenantId) {
log.info("获取学校端任务详情taskId={}, tenantId={}", taskId, tenantId); return buildTaskDetailResponse(taskId, tenantId);
}
// 验证任务存在且属于该租户 /**
* 获取任务详情含目标类型目标列表用于编辑回显
*/
public TaskResponse getTaskDetailWithTargets(Long taskId, Long tenantId) {
return buildTaskDetailResponse(taskId, tenantId);
}
private TaskResponse buildTaskDetailResponse(Long taskId, Long tenantId) {
Task task = getTaskByIdWithTenantCheck(taskId, tenantId); Task task = getTaskByIdWithTenantCheck(taskId, tenantId);
List<TaskTarget> targets = taskTargetMapper.selectList(
new LambdaQueryWrapper<TaskTarget>().eq(TaskTarget::getTaskId, taskId));
String targetType = targets.isEmpty() ? null : targets.get(0).getTargetType();
List<Long> targetIds = targets.stream().map(TaskTarget::getTargetId).toList();
return TaskResponse.builder() return TaskResponse.builder()
.id(task.getId()) .id(task.getId())
@ -537,6 +577,8 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
.dueDate(task.getDueDate()) .dueDate(task.getDueDate())
.status(task.getStatus()) .status(task.getStatus())
.attachments(task.getAttachments()) .attachments(task.getAttachments())
.targetType(targetType)
.targetIds(targetIds)
.createdAt(task.getCreatedAt()) .createdAt(task.getCreatedAt())
.updatedAt(task.getUpdatedAt()) .updatedAt(task.getUpdatedAt())
.build(); .build();