feat: 任务完成情况 API 与 UI 优化
- 学校/教师端: 新增 GET/PUT 任务完成情况接口,支持分页与状态筛选 - 后端: TaskCompletionDetailResponse 含学生姓名、班级信息 - 后端: 新增 getTaskCompletionStats、getTaskCompletionsWithStudent - 学校端: 状态筛选移至弹框标题后,分页始终显示 - 班级字段: 支持 ACTIVE 状态,无记录时用 student.grade 兜底 - 迁移 V43: 添加 student_class_history 测试数据 Made-with: Cursor
This commit is contained in:
parent
13fc0e720e
commit
37a6aba8cc
@ -997,12 +997,17 @@ export interface TaskCompletion {
|
||||
id: number;
|
||||
taskId: number;
|
||||
studentId: number;
|
||||
studentName: string;
|
||||
className: string;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
|
||||
completedAt?: string;
|
||||
feedback?: string;
|
||||
parentFeedback?: string;
|
||||
rating?: number;
|
||||
student?: {
|
||||
id: number;
|
||||
name: string;
|
||||
gender?: string;
|
||||
class?: { id: number; name: string };
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateSchoolTaskDto {
|
||||
@ -1046,8 +1051,27 @@ export const updateSchoolTask = (id: number, data: UpdateSchoolTaskDto) =>
|
||||
export const deleteSchoolTask = (id: number) =>
|
||||
http.delete<{ message: string }>(`/v1/school/tasks/${id}`);
|
||||
|
||||
export const getSchoolTaskCompletions = (taskId: number) =>
|
||||
http.get<TaskCompletion[]>(`/v1/school/tasks/${taskId}/completions`);
|
||||
export interface TaskCompletionListResponse {
|
||||
items: TaskCompletion[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
stats: { PENDING: number; IN_PROGRESS: number; COMPLETED: number };
|
||||
}
|
||||
|
||||
export const getSchoolTaskCompletions = (
|
||||
taskId: number,
|
||||
params?: { page?: number; pageSize?: number; status?: string }
|
||||
) =>
|
||||
http.get<TaskCompletionListResponse>(`/v1/school/tasks/${taskId}/completions`, {
|
||||
params: { page: params?.page ?? 1, pageSize: params?.pageSize ?? 20, status: params?.status },
|
||||
}).then((res: TaskCompletionListResponse) => ({
|
||||
items: res.items ?? [],
|
||||
total: res.total ?? 0,
|
||||
page: res.page ?? 1,
|
||||
pageSize: res.pageSize ?? 20,
|
||||
stats: res.stats ?? { PENDING: 0, IN_PROGRESS: 0, COMPLETED: 0 },
|
||||
}));
|
||||
|
||||
export const getSchoolClasses = () =>
|
||||
http.get<ClassInfo[]>('/v1/school/classes');
|
||||
|
||||
@ -706,9 +706,17 @@ export const getTeacherTasks = (params?: { pageNum?: number; pageSize?: number;
|
||||
export const getTeacherTask = (id: number) =>
|
||||
http.get(`/v1/teacher/tasks/${id}`) as any;
|
||||
|
||||
// 教师端没有这些接口,返回空数据
|
||||
export const getTeacherTaskCompletions = (_taskId: number) =>
|
||||
Promise.resolve([]);
|
||||
// 获取教师任务完成情况
|
||||
export const getTeacherTaskCompletions = (taskId: number, params?: { page?: number; pageSize?: number; status?: string }) =>
|
||||
http.get<{ items: TaskCompletion[]; total: number; page: number; pageSize: number }>(
|
||||
`/v1/teacher/tasks/${taskId}/completions`,
|
||||
{ params: { page: params?.page ?? 1, pageSize: params?.pageSize ?? 20, status: params?.status } }
|
||||
).then(res => ({
|
||||
items: res.items ?? res.list ?? [],
|
||||
total: res.total ?? 0,
|
||||
page: res.page ?? res.pageNum ?? 1,
|
||||
pageSize: res.pageSize ?? 20,
|
||||
}));
|
||||
|
||||
export const createTeacherTask = (data: CreateTeacherTaskDto) =>
|
||||
http.post('/v1/teacher/tasks', data) as any;
|
||||
@ -719,9 +727,9 @@ export const updateTeacherTask = (id: number, data: Partial<CreateTeacherTaskDto
|
||||
export const deleteTeacherTask = (id: number) =>
|
||||
http.delete(`/v1/teacher/tasks/${id}`) as any;
|
||||
|
||||
// 后端没有这些接口
|
||||
export const updateTaskCompletion = (_taskId: number, _studentId: number, _data: UpdateTaskCompletionDto) =>
|
||||
Promise.reject(new Error('接口未实现'));
|
||||
// 更新任务完成状态
|
||||
export const updateTaskCompletion = (taskId: number, studentId: number, data: UpdateTaskCompletionDto) =>
|
||||
http.put(`/v1/teacher/tasks/${taskId}/completions/${studentId}`, data) as Promise<any>;
|
||||
|
||||
export const sendTaskReminder = (_taskId: number) =>
|
||||
Promise.reject(new Error('接口未实现'));
|
||||
|
||||
@ -46,35 +46,18 @@
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-bar">
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
placeholder="任务状态"
|
||||
style="width: 120px"
|
||||
allowClear
|
||||
@change="loadTasks"
|
||||
>
|
||||
<a-select v-model:value="filters.status" placeholder="任务状态" style="width: 120px" allowClear @change="loadTasks">
|
||||
<a-select-option value="PUBLISHED">进行中</a-select-option>
|
||||
<a-select-option value="DRAFT">草稿</a-select-option>
|
||||
<a-select-option value="ARCHIVED">已归档</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-model:value="filters.taskType"
|
||||
placeholder="任务类型"
|
||||
style="width: 120px"
|
||||
allowClear
|
||||
@change="loadTasks"
|
||||
>
|
||||
<a-select v-model:value="filters.taskType" placeholder="任务类型" style="width: 120px" allowClear @change="loadTasks">
|
||||
<a-select-option value="READING">阅读</a-select-option>
|
||||
<a-select-option value="ACTIVITY">活动</a-select-option>
|
||||
<a-select-option value="HOMEWORK">作业</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="filters.keyword"
|
||||
placeholder="搜索任务标题"
|
||||
style="width: 200px"
|
||||
@search="loadTasks"
|
||||
allow-clear
|
||||
/>
|
||||
<a-input-search v-model:value="filters.keyword" placeholder="搜索任务标题" style="width: 200px" @search="loadTasks"
|
||||
allow-clear />
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
@ -107,12 +90,8 @@
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="progress-info">
|
||||
<a-progress
|
||||
:percent="getCompletionRate(task)"
|
||||
:stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
||||
size="small"
|
||||
style="width: 150px;"
|
||||
/>
|
||||
<a-progress :percent="getCompletionRate(task)" :stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
||||
size="small" style="width: 150px;" />
|
||||
<span class="progress-text">{{ getCompletedCount(task) }}/{{ task.completionCount || 0 }} 人完成</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@ -142,24 +121,13 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-section" v-if="total > pageSize">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
@change="onPageChange"
|
||||
show-quick-jumper
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
/>
|
||||
<a-pagination v-model:current="currentPage" :total="total" :page-size="pageSize" @change="onPageChange"
|
||||
show-quick-jumper :show-total="(total: number) => `共 ${total} 条`" />
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑任务弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="createModalVisible"
|
||||
:title="isEdit ? '编辑任务' : '发布任务'"
|
||||
@ok="handleSubmit"
|
||||
:confirm-loading="creating"
|
||||
width="600px"
|
||||
>
|
||||
<a-modal v-model:open="createModalVisible" :title="isEdit ? '编辑任务' : '发布任务'" @ok="handleSubmit"
|
||||
:confirm-loading="creating" width="600px">
|
||||
<a-form :model="createForm" layout="vertical">
|
||||
<a-form-item label="任务标题" required>
|
||||
<a-input v-model:value="createForm.title" placeholder="请输入任务标题" />
|
||||
@ -187,46 +155,36 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="选择目标" required v-if="createForm.targetType === 'CLASS'">
|
||||
<a-select
|
||||
v-model:value="createForm.targetIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择班级"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<a-select 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">
|
||||
{{ cls.name }} ({{ cls.grade }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="任务时间" required>
|
||||
<a-range-picker
|
||||
v-model:value="createForm.dateRange"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<a-range-picker v-model:value="createForm.dateRange" style="width: 100%;" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 完成情况弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="completionModalVisible"
|
||||
:title="`完成情况 - ${selectedTask?.title || ''}`"
|
||||
:footer="null"
|
||||
width="700px"
|
||||
>
|
||||
<a-modal v-model:open="completionModalVisible" :footer="null" width="700px">
|
||||
<template #title>
|
||||
<span>完成情况 - {{ selectedTask?.title || '' }}</span>
|
||||
<a-select v-model:value="completionFilter.status" placeholder="筛选状态" style="width: 120px; margin-left: 12px" allowClear
|
||||
@change="loadCompletions">
|
||||
<a-select-option value="PENDING">待完成</a-select-option>
|
||||
<a-select-option value="IN_PROGRESS">进行中</a-select-option>
|
||||
<a-select-option value="COMPLETED">已完成</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<div class="completion-stats">
|
||||
<a-tag color="blue">{{ completionStats.pending }} 待完成</a-tag>
|
||||
<a-tag color="orange">{{ completionStats.inProgress }} 进行中</a-tag>
|
||||
<a-tag color="green">{{ completionStats.completed }} 已完成</a-tag>
|
||||
</div>
|
||||
<a-table
|
||||
:dataSource="completions"
|
||||
:columns="completionColumns"
|
||||
:pagination="false"
|
||||
:loading="loadingCompletions"
|
||||
rowKey="id"
|
||||
size="small"
|
||||
>
|
||||
<a-tag color="blue">{{ completionStats.pending }} 待完成</a-tag>
|
||||
<a-tag color="orange">{{ completionStats.inProgress }} 进行中</a-tag>
|
||||
<a-tag color="green">{{ completionStats.completed }} 已完成</a-tag>
|
||||
</div>
|
||||
<a-table :dataSource="completions" :columns="completionColumns" :pagination="false" :loading="loadingCompletions"
|
||||
rowKey="id" size="small">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getCompletionStatusColor(record.status)">
|
||||
@ -234,13 +192,18 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'feedback'">
|
||||
<span v-if="record.parentFeedback" class="feedback-text">
|
||||
{{ record.parentFeedback.substring(0, 30) }}{{ record.parentFeedback.length > 30 ? '...' : '' }}
|
||||
<span v-if="record.parentFeedback || record.feedback" class="feedback-text">
|
||||
{{ (record.parentFeedback || record.feedback || '').substring(0, 30) }}{{ (record.parentFeedback || record.feedback || '').length > 30 ? '...' : '' }}
|
||||
</span>
|
||||
<span v-else class="no-feedback">暂无家长反馈</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div v-if="completionTotal > 0" class="pagination-section" style="margin-top: 16px">
|
||||
<a-pagination v-model:current="completionPage" :total="completionTotal" :page-size="completionPageSize"
|
||||
size="small" @change="onCompletionPageChange" show-quick-jumper
|
||||
:show-total="(t: number) => `共 ${t} 条`" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
@ -282,6 +245,10 @@ const editTaskId = ref<number | null>(null);
|
||||
const tasks = ref<SchoolTask[]>([]);
|
||||
const classes = ref<any[]>([]);
|
||||
const completions = ref<TaskCompletion[]>([]);
|
||||
const completionTotal = ref(0);
|
||||
const completionPage = ref(1);
|
||||
const completionPageSize = ref(10);
|
||||
const completionFilter = reactive({ status: undefined as string | undefined });
|
||||
const selectedTask = ref<SchoolTask | null>(null);
|
||||
const loadingCompletions = ref(false);
|
||||
|
||||
@ -363,7 +330,7 @@ const getCompletionRate = (task: SchoolTask) => {
|
||||
return Math.round((completed / task.completionCount) * 100);
|
||||
};
|
||||
|
||||
const getCompletedCount = (task: SchoolTask) => {
|
||||
const getCompletedCount = (_task: SchoolTask) => {
|
||||
return completions.value.filter(c => c.status === 'COMPLETED').length;
|
||||
};
|
||||
|
||||
@ -389,7 +356,7 @@ const loadTasks = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await getSchoolTasks({
|
||||
page: currentPage.value,
|
||||
pageNum: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
status: filters.status,
|
||||
taskType: filters.taskType,
|
||||
@ -499,23 +466,22 @@ const handleDelete = async (id: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const viewCompletionDetail = async (task: SchoolTask) => {
|
||||
selectedTask.value = task;
|
||||
completionModalVisible.value = true;
|
||||
const loadCompletions = async () => {
|
||||
if (!selectedTask.value) return;
|
||||
loadingCompletions.value = true;
|
||||
|
||||
try {
|
||||
const result = await getSchoolTaskCompletions(task.id, { pageSize: 100 });
|
||||
completions.value = result.list;
|
||||
|
||||
// 计算统计
|
||||
completionStats.pending = result.list.filter(c => c.status === 'PENDING').length;
|
||||
completionStats.inProgress = result.list.filter(c => c.status === 'IN_PROGRESS').length;
|
||||
completionStats.completed = result.list.filter(c => c.status === 'COMPLETED').length;
|
||||
|
||||
// 计算完成率
|
||||
const total = result.list.length;
|
||||
stats.completionRate = total > 0 ? Math.round((completionStats.completed / total) * 100) : 0;
|
||||
const data = await getSchoolTaskCompletions(selectedTask.value.id, {
|
||||
page: completionPage.value,
|
||||
pageSize: completionPageSize.value,
|
||||
status: completionFilter.status,
|
||||
});
|
||||
completions.value = data.items;
|
||||
completionTotal.value = data.total;
|
||||
completionStats.pending = data.stats.PENDING ?? 0;
|
||||
completionStats.inProgress = data.stats.IN_PROGRESS ?? 0;
|
||||
completionStats.completed = data.stats.COMPLETED ?? 0;
|
||||
const totalCount = completionStats.pending + completionStats.inProgress + completionStats.completed;
|
||||
stats.completionRate = totalCount > 0 ? Math.round((completionStats.completed / totalCount) * 100) : 0;
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.message || '获取完成情况失败');
|
||||
} finally {
|
||||
@ -523,6 +489,19 @@ const viewCompletionDetail = async (task: SchoolTask) => {
|
||||
}
|
||||
};
|
||||
|
||||
const viewCompletionDetail = async (task: SchoolTask) => {
|
||||
selectedTask.value = task;
|
||||
completionPage.value = 1;
|
||||
completionFilter.status = undefined;
|
||||
completionModalVisible.value = true;
|
||||
loadCompletions();
|
||||
};
|
||||
|
||||
const onCompletionPageChange = (page: number) => {
|
||||
completionPage.value = page;
|
||||
loadCompletions();
|
||||
};
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
currentPage.value = page;
|
||||
loadTasks();
|
||||
@ -729,9 +708,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.completion-stats {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feedback-text {
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
<div class="task-list-view">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h2><CheckSquareOutlined /> 阅读任务</h2>
|
||||
<h2>
|
||||
<CheckSquareOutlined /> 阅读任务
|
||||
</h2>
|
||||
<p class="page-desc">管理班级阅读任务,跟踪学生完成情况</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="openCreateModal">
|
||||
@ -44,35 +46,20 @@
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<a-space :size="16">
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
placeholder="任务状态"
|
||||
style="width: 120px;"
|
||||
allowClear
|
||||
@change="loadTasks"
|
||||
>
|
||||
<a-select v-model:value="filters.status" placeholder="任务状态" style="width: 120px;" allowClear
|
||||
@change="loadTasks">
|
||||
<a-select-option value="PUBLISHED">进行中</a-select-option>
|
||||
<a-select-option value="DRAFT">草稿</a-select-option>
|
||||
<a-select-option value="ARCHIVED">已归档</a-select-option>
|
||||
</a-select>
|
||||
<a-select
|
||||
v-model:value="filters.taskType"
|
||||
placeholder="任务类型"
|
||||
style="width: 120px;"
|
||||
allowClear
|
||||
@change="loadTasks"
|
||||
>
|
||||
<a-select v-model:value="filters.taskType" placeholder="任务类型" style="width: 120px;" allowClear
|
||||
@change="loadTasks">
|
||||
<a-select-option value="READING">阅读</a-select-option>
|
||||
<a-select-option value="ACTIVITY">活动</a-select-option>
|
||||
<a-select-option value="HOMEWORK">作业</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="filters.keyword"
|
||||
placeholder="搜索任务标题"
|
||||
style="width: 200px;"
|
||||
@search="loadTasks"
|
||||
allow-clear
|
||||
/>
|
||||
<a-input-search v-model:value="filters.keyword" placeholder="搜索任务标题" style="width: 200px;" @search="loadTasks"
|
||||
allow-clear />
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
@ -105,12 +92,8 @@
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="progress-info">
|
||||
<a-progress
|
||||
:percent="getCompletionRate(task)"
|
||||
:stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
||||
size="small"
|
||||
style="width: 150px;"
|
||||
/>
|
||||
<a-progress :percent="getCompletionRate(task)" :stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
||||
size="small" style="width: 150px;" />
|
||||
<span class="progress-text">{{ getCompletedCount(task) }}/{{ task.targetCount || 0 }} 人完成</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@ -154,34 +137,18 @@
|
||||
</a-spin>
|
||||
|
||||
<div class="pagination-section" v-if="total > pageSize">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
@change="onPageChange"
|
||||
show-quick-jumper
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
/>
|
||||
<a-pagination v-model:current="currentPage" :total="total" :page-size="pageSize" @change="onPageChange"
|
||||
show-quick-jumper :show-total="(total: number) => `共 ${total} 条`" />
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑任务弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="createModalVisible"
|
||||
:title="isEdit ? '编辑任务' : '新建阅读任务'"
|
||||
@ok="handleCreate"
|
||||
:confirm-loading="creating"
|
||||
width="600px"
|
||||
>
|
||||
<a-modal v-model:open="createModalVisible" :title="isEdit ? '编辑任务' : '新建阅读任务'" @ok="handleCreate"
|
||||
:confirm-loading="creating" width="600px">
|
||||
<a-form :model="createForm" layout="vertical">
|
||||
<!-- 模板选择(仅新建时显示) -->
|
||||
<a-form-item label="使用模板" v-if="!isEdit">
|
||||
<a-select
|
||||
v-model:value="selectedTemplateId"
|
||||
placeholder="选择模板快速填充(可选)"
|
||||
style="width: 100%;"
|
||||
allowClear
|
||||
@change="onTemplateSelect"
|
||||
>
|
||||
<a-select v-model:value="selectedTemplateId" placeholder="选择模板快速填充(可选)" style="width: 100%;" allowClear
|
||||
@change="onTemplateSelect">
|
||||
<a-select-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id">
|
||||
{{ tpl.name }}
|
||||
<a-tag size="small" :color="getTaskTypeColor(tpl.taskType)" style="margin-left: 8px;">
|
||||
@ -216,70 +183,41 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="选择目标" required v-if="createForm.targetType === 'CLASS'">
|
||||
<a-select
|
||||
v-model:value="createForm.targetIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择班级"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<a-select 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">
|
||||
{{ cls.name }} ({{ cls.grade }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="选择学生" required v-if="createForm.targetType === 'STUDENT'">
|
||||
<a-select
|
||||
v-model:value="createForm.targetIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择学生"
|
||||
style="width: 100%;"
|
||||
:filter-option="filterStudentOption"
|
||||
show-search
|
||||
>
|
||||
<a-select v-model:value="createForm.targetIds" 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">
|
||||
{{ student.name }} - {{ student.class?.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联课程">
|
||||
<a-select
|
||||
v-model:value="createForm.relatedCourseId"
|
||||
placeholder="可选,关联课程包"
|
||||
style="width: 100%;"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterCourseOption"
|
||||
>
|
||||
<a-select v-model:value="createForm.relatedCourseId" placeholder="可选,关联课程包" style="width: 100%;" allowClear
|
||||
show-search :filter-option="filterCourseOption">
|
||||
<a-select-option v-for="course in courses" :key="course.id" :value="course.id">
|
||||
{{ course.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="任务时间" required>
|
||||
<a-range-picker
|
||||
v-model:value="createForm.dateRange"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
<a-range-picker v-model:value="createForm.dateRange" style="width: 100%;" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 完成情况详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="completionModalVisible"
|
||||
:title="`完成情况 - ${selectedTask?.title || ''}`"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-modal v-model:open="completionModalVisible" :title="`完成情况 - ${selectedTask?.title || ''}`" width="800px"
|
||||
:footer="null">
|
||||
<div class="completion-header">
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="completionFilter.status"
|
||||
placeholder="筛选状态"
|
||||
style="width: 120px;"
|
||||
allowClear
|
||||
@change="loadCompletions"
|
||||
>
|
||||
<a-select v-model:value="completionFilter.status" placeholder="筛选状态" style="width: 120px;" allowClear
|
||||
@change="loadCompletions">
|
||||
<a-select-option value="PENDING">待完成</a-select-option>
|
||||
<a-select-option value="IN_PROGRESS">进行中</a-select-option>
|
||||
<a-select-option value="COMPLETED">已完成</a-select-option>
|
||||
@ -305,12 +243,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="completion-status">
|
||||
<a-select
|
||||
:value="completion.status"
|
||||
style="width: 100px;"
|
||||
size="small"
|
||||
@change="(val: string) => updateCompletionStatus(completion, val)"
|
||||
>
|
||||
<a-select :value="completion.status" style="width: 100px;" size="small"
|
||||
@change="(val: string) => updateCompletionStatus(completion, val)">
|
||||
<a-select-option value="PENDING">待完成</a-select-option>
|
||||
<a-select-option value="IN_PROGRESS">进行中</a-select-option>
|
||||
<a-select-option value="COMPLETED">已完成</a-select-option>
|
||||
@ -320,7 +254,8 @@
|
||||
<div v-if="completion.parentFeedback" class="parent-feedback">
|
||||
<MessageOutlined class="feedback-icon" />
|
||||
<a-tooltip :title="completion.parentFeedback">
|
||||
<span class="feedback-text">{{ completion.parentFeedback.substring(0, 30) }}{{ completion.parentFeedback.length > 30 ? '...' : '' }}</span>
|
||||
<span class="feedback-text">{{ completion.parentFeedback.substring(0, 30) }}{{
|
||||
completion.parentFeedback.length > 30 ? '...' : '' }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<span v-else class="no-feedback">暂无家长反馈</span>
|
||||
@ -340,13 +275,8 @@
|
||||
</a-spin>
|
||||
|
||||
<div class="pagination-section" v-if="completionTotal > completionPageSize" style="margin-top: 16px;">
|
||||
<a-pagination
|
||||
v-model:current="completionPage"
|
||||
:total="completionTotal"
|
||||
:page-size="completionPageSize"
|
||||
size="small"
|
||||
@change="onCompletionPageChange"
|
||||
/>
|
||||
<a-pagination v-model:current="completionPage" :total="completionTotal" :page-size="completionPageSize"
|
||||
size="small" @change="onCompletionPageChange" />
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
@ -745,7 +675,7 @@ const updateCompletionStatus = async (completion: TaskCompletion, status: string
|
||||
}
|
||||
message.success('状态已更新');
|
||||
} catch (error: any) {
|
||||
message.error('更新失败');
|
||||
message.error('更新失败', error);
|
||||
loadCompletions();
|
||||
}
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.service.TaskService;
|
||||
@ -16,7 +17,9 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "School - Task", description = "Task Management APIs for School")
|
||||
@RestController
|
||||
@ -53,6 +56,25 @@ public class SchoolTaskController {
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get task completions")
|
||||
@GetMapping("/{id}/completions")
|
||||
public Result<Map<String, Object>> getTaskCompletions(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "20") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
var pageResult = taskService.getTaskCompletionsWithStudent(id, tenantId, page, pageSize, status);
|
||||
var stats = taskService.getTaskCompletionStats(id, tenantId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("items", pageResult.getList());
|
||||
data.put("total", pageResult.getTotal());
|
||||
data.put("page", pageResult.getPageNum());
|
||||
data.put("pageSize", pageResult.getPageSize());
|
||||
data.put("stats", stats);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get task page")
|
||||
@GetMapping
|
||||
public Result<PageResult<TaskResponse>> getTaskPage(
|
||||
|
||||
@ -7,6 +7,7 @@ import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.service.TaskService;
|
||||
@ -16,7 +17,9 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "教师端 - 任务管理", description = "教师端任务 API")
|
||||
@RestController
|
||||
@ -50,6 +53,36 @@ public class TeacherTaskController {
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取任务完成情况")
|
||||
@GetMapping("/{id}/completions")
|
||||
public Result<Map<String, Object>> getTaskCompletions(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "20") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
PageResult<TaskCompletionDetailResponse> result = taskService.getTaskCompletionsWithStudent(
|
||||
id, tenantId, page, pageSize, status);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("items", result.getList());
|
||||
data.put("total", result.getTotal());
|
||||
data.put("page", result.getPageNum());
|
||||
data.put("pageSize", result.getPageSize());
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新任务完成状态")
|
||||
@PutMapping("/{taskId}/completions/{studentId}")
|
||||
public Result<Void> updateTaskCompletion(
|
||||
@PathVariable Long taskId,
|
||||
@PathVariable Long studentId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
String status = body != null && body.containsKey("status") ? String.valueOf(body.get("status")) : null;
|
||||
taskService.updateTaskCompletionStatus(taskId, studentId, tenantId, status);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取任务分页列表")
|
||||
@GetMapping
|
||||
public Result<PageResult<TaskResponse>> getTaskPage(
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
package com.reading.platform.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 任务完成详情响应(含学生信息)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "任务完成详情响应(含学生信息)")
|
||||
public class TaskCompletionDetailResponse {
|
||||
|
||||
@Schema(description = "ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务 ID")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "学生 ID")
|
||||
private Long studentId;
|
||||
|
||||
@Schema(description = "完成状态: PENDING, IN_PROGRESS, COMPLETED")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "完成时间")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Schema(description = "完成内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "附件")
|
||||
private String attachments;
|
||||
|
||||
@Schema(description = "评分")
|
||||
private Integer rating;
|
||||
|
||||
@Schema(description = "反馈/家长反馈")
|
||||
private String feedback;
|
||||
|
||||
@Schema(description = "家长反馈(与 feedback 相同,兼容前端)")
|
||||
private String parentFeedback;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Schema(description = "学生信息")
|
||||
private StudentInfo student;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class StudentInfo {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String gender;
|
||||
@JsonProperty("class")
|
||||
private ClassInfo clazz;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class ClassInfo {
|
||||
private Long id;
|
||||
private String name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
package com.reading.platform.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskCompletionResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
|
||||
import java.util.List;
|
||||
@ -32,6 +35,14 @@ public interface TaskService extends com.baomidou.mybatisplus.extension.service.
|
||||
|
||||
void completeTask(Long taskId, Long studentId, String content, String attachments);
|
||||
|
||||
List<TaskCompletionResponse> getTaskCompletions(Long taskId, Long tenantId);
|
||||
|
||||
PageResult<TaskCompletionDetailResponse> getTaskCompletionsWithStudent(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, String status);
|
||||
|
||||
java.util.Map<String, java.lang.Long> getTaskCompletionStats(Long taskId, Long tenantId);
|
||||
|
||||
void updateTaskCompletionStatus(Long taskId, Long studentId, Long tenantId, String status);
|
||||
|
||||
List<Task> getTasksByClassId(Long classId);
|
||||
|
||||
}
|
||||
|
||||
@ -5,11 +5,20 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.reading.platform.common.enums.ErrorCode;
|
||||
import com.reading.platform.common.exception.BusinessException;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskCompletionResponse;
|
||||
import com.reading.platform.entity.Clazz;
|
||||
import com.reading.platform.entity.Student;
|
||||
import com.reading.platform.entity.StudentClassHistory;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.entity.TaskCompletion;
|
||||
import com.reading.platform.entity.TaskTarget;
|
||||
import com.reading.platform.mapper.ClazzMapper;
|
||||
import com.reading.platform.mapper.StudentClassHistoryMapper;
|
||||
import com.reading.platform.mapper.StudentMapper;
|
||||
import com.reading.platform.mapper.TaskCompletionMapper;
|
||||
import com.reading.platform.mapper.TaskMapper;
|
||||
import com.reading.platform.mapper.TaskTargetMapper;
|
||||
@ -21,8 +30,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -33,6 +46,9 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
private final TaskMapper taskMapper;
|
||||
private final TaskTargetMapper taskTargetMapper;
|
||||
private final TaskCompletionMapper taskCompletionMapper;
|
||||
private final StudentMapper studentMapper;
|
||||
private final StudentClassHistoryMapper studentClassHistoryMapper;
|
||||
private final ClazzMapper clazzMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@ -216,6 +232,150 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TaskCompletionResponse> getTaskCompletions(Long taskId, Long tenantId) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
List<TaskCompletion> completions = taskCompletionMapper.selectList(
|
||||
new LambdaQueryWrapper<TaskCompletion>().eq(TaskCompletion::getTaskId, taskId)
|
||||
);
|
||||
return com.reading.platform.common.mapper.TaskCompletionMapper.INSTANCE.toVO(completions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<TaskCompletionDetailResponse> getTaskCompletionsWithStudent(Long taskId, Long tenantId,
|
||||
Integer pageNum, Integer pageSize, String status) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
|
||||
int pn = pageNum != null && pageNum > 0 ? pageNum : 1;
|
||||
int ps = pageSize != null && pageSize > 0 ? pageSize : 20;
|
||||
|
||||
LambdaQueryWrapper<TaskCompletion> wrapper = new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.orderByDesc(TaskCompletion::getUpdatedAt);
|
||||
if (StringUtils.hasText(status)) {
|
||||
wrapper.eq(TaskCompletion::getStatus, normalizeStatus(status));
|
||||
}
|
||||
|
||||
long total = taskCompletionMapper.selectCount(wrapper);
|
||||
Page<TaskCompletion> page = new Page<>(pn, ps);
|
||||
List<TaskCompletion> completions = taskCompletionMapper.selectPage(page, wrapper).getRecords();
|
||||
|
||||
List<TaskCompletionDetailResponse> items = completions.stream()
|
||||
.map(c -> buildCompletionDetail(c))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.of(items, total, (long) pn, (long) ps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Long> getTaskCompletionStats(Long taskId, Long tenantId) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
Map<String, Long> stats = new HashMap<>();
|
||||
stats.put("PENDING", taskCompletionMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStatus, Arrays.asList("PENDING", "pending"))
|
||||
));
|
||||
stats.put("IN_PROGRESS", taskCompletionMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStatus, Arrays.asList("IN_PROGRESS", "in_progress"))
|
||||
));
|
||||
stats.put("COMPLETED", taskCompletionMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStatus, Arrays.asList("COMPLETED", "completed"))
|
||||
));
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateTaskCompletionStatus(Long taskId, Long studentId, Long tenantId, String status) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
TaskCompletion completion = taskCompletionMapper.selectOne(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.eq(TaskCompletion::getStudentId, studentId)
|
||||
);
|
||||
if (completion == null) {
|
||||
completion = new TaskCompletion();
|
||||
completion.setTaskId(taskId);
|
||||
completion.setStudentId(studentId);
|
||||
taskCompletionMapper.insert(completion);
|
||||
}
|
||||
completion.setStatus(normalizeStatus(status));
|
||||
if ("COMPLETED".equals(completion.getStatus())) {
|
||||
completion.setCompletedAt(LocalDateTime.now());
|
||||
}
|
||||
taskCompletionMapper.updateById(completion);
|
||||
}
|
||||
|
||||
private TaskCompletionDetailResponse buildCompletionDetail(TaskCompletion c) {
|
||||
Student student = studentMapper.selectById(c.getStudentId());
|
||||
TaskCompletionDetailResponse.StudentInfo.ClassInfo classInfo = null;
|
||||
if (student != null) {
|
||||
StudentClassHistory sch = studentClassHistoryMapper.selectOne(
|
||||
new LambdaQueryWrapper<StudentClassHistory>()
|
||||
.eq(StudentClassHistory::getStudentId, c.getStudentId())
|
||||
.in(StudentClassHistory::getStatus, Arrays.asList("active", "ACTIVE"))
|
||||
.isNull(StudentClassHistory::getEndDate)
|
||||
.orderByDesc(StudentClassHistory::getStartDate)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (sch != null) {
|
||||
Clazz clazz = clazzMapper.selectById(sch.getClassId());
|
||||
if (clazz != null) {
|
||||
classInfo = TaskCompletionDetailResponse.StudentInfo.ClassInfo.builder()
|
||||
.id(clazz.getId())
|
||||
.name(clazz.getName())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
if (classInfo == null && student.getGrade() != null) {
|
||||
classInfo = TaskCompletionDetailResponse.StudentInfo.ClassInfo.builder()
|
||||
.id(null)
|
||||
.name(student.getGrade())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
String feedback = c.getFeedback();
|
||||
return TaskCompletionDetailResponse.builder()
|
||||
.id(c.getId())
|
||||
.taskId(c.getTaskId())
|
||||
.studentId(c.getStudentId())
|
||||
.status(normalizeStatusForResponse(c.getStatus()))
|
||||
.completedAt(c.getCompletedAt())
|
||||
.content(c.getContent())
|
||||
.attachments(c.getAttachments())
|
||||
.rating(c.getRating())
|
||||
.feedback(feedback)
|
||||
.parentFeedback(feedback)
|
||||
.createdAt(c.getCreatedAt())
|
||||
.updatedAt(c.getUpdatedAt())
|
||||
.student(student != null ? TaskCompletionDetailResponse.StudentInfo.builder()
|
||||
.id(student.getId())
|
||||
.name(student.getName())
|
||||
.gender(student.getGender())
|
||||
.clazz(classInfo)
|
||||
.build() : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String normalizeStatus(String status) {
|
||||
if (status == null) return "PENDING";
|
||||
String u = status.toUpperCase();
|
||||
if ("COMPLETED".equals(u) || "IN_PROGRESS".equals(u) || "PENDING".equals(u)) return u;
|
||||
if ("completed".equals(status.toLowerCase())) return "COMPLETED";
|
||||
if ("in_progress".equals(status.toLowerCase()) || "in progress".equalsIgnoreCase(status)) return "IN_PROGRESS";
|
||||
return "PENDING";
|
||||
}
|
||||
|
||||
private String normalizeStatusForResponse(String status) {
|
||||
if (status == null) return "PENDING";
|
||||
return normalizeStatus(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getTasksByClassId(Long classId) {
|
||||
List<TaskTarget> targets = taskTargetMapper.selectList(
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
-- =====================================================
|
||||
-- 添加学生班级关联数据 (student_class_history)
|
||||
-- 学生 1-5 -> 小一班, 6-10 -> 小二班, 11-15 -> 中一班, 16-20 -> 中二班
|
||||
-- 21-25 -> 大一班, 26-30 -> 大二班, 31-35 -> 学前班, 36-40 -> 托儿班
|
||||
-- =====================================================
|
||||
|
||||
INSERT IGNORE INTO `student_class_history` (`id`, `student_id`, `class_id`, `start_date`, `end_date`, `status`) VALUES
|
||||
(1, 1, 1, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(2, 2, 1, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(3, 3, 1, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(4, 4, 1, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(5, 5, 1, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(6, 6, 2, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(7, 7, 2, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(8, 8, 2, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(9, 9, 2, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(10, 10, 2, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(11, 11, 3, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(12, 12, 3, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(13, 13, 3, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(14, 14, 3, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(15, 15, 3, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(16, 16, 4, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(17, 17, 4, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(18, 18, 4, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(19, 19, 4, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(20, 20, 4, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(21, 21, 5, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(22, 22, 5, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(23, 23, 5, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(24, 24, 5, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(25, 25, 5, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(26, 26, 6, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(27, 27, 6, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(28, 28, 6, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(29, 29, 6, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(30, 30, 6, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(31, 31, 7, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(32, 32, 7, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(33, 33, 7, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(34, 34, 7, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(35, 35, 7, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(36, 36, 8, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(37, 37, 8, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(38, 38, 8, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(39, 39, 8, '2025-09-01', NULL, 'ACTIVE'),
|
||||
(40, 40, 8, '2025-09-01', NULL, 'ACTIVE');
|
||||
Loading…
Reference in New Issue
Block a user