fix: 学校端家长管理选择孩子列表优化
- 学生列表分页参数修复:page 改为 pageNum - StudentResponse 添加 className 字段,显示班级名称 - 性别显示逻辑简化,兼容空值 - 修复 TeacherListView 中 Modal 导入错误 feat: 排课管理支持删除已取消的排课 - 新增 ScheduleRepeatType 和 ScheduleStatus 枚举 - 添加物理删除接口 /force,仅允许删除已取消的排课 - ScheduleList 和 TimetableView 增加删除按钮
This commit is contained in:
parent
342456347e
commit
e2547daa63
@ -613,6 +613,9 @@ export const updateSchedule = (id: number, data: UpdateScheduleDto) =>
|
||||
export const cancelSchedule = (id: number) =>
|
||||
http.delete<{ message: string }>(`/v1/school/schedules/${id}`);
|
||||
|
||||
export const deleteSchedule = (id: number) =>
|
||||
http.delete<{ message: string }>(`/v1/school/schedules/${id}/force`);
|
||||
|
||||
export const getTimetable = (params: TimetableQueryParams) =>
|
||||
http.get<{
|
||||
byDate: Record<string, SchedulePlan[]>;
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="课程封面">
|
||||
<a-form-item label="课程封面" required>
|
||||
<a-upload
|
||||
v-model:file-list="coverImages"
|
||||
list-type="picture-card"
|
||||
@ -283,6 +283,12 @@ const handleChange = () => {
|
||||
const validate = async () => {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
|
||||
// 校验课程封面
|
||||
if (!formData.coverImagePath) {
|
||||
return { valid: false, errors: ['请上传课程封面'] };
|
||||
}
|
||||
|
||||
return { valid: true, errors: [] as string[] };
|
||||
} catch (err: any) {
|
||||
const errorFields = err?.errorFields || [];
|
||||
|
||||
@ -246,7 +246,10 @@
|
||||
@change="handleStudentTableChange" style="margin-top: 16px;">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'gender'">
|
||||
{{ record.gender === 'MALE' ? '男' : record.gender === 'FEMALE' ? '女' : '-' }}
|
||||
{{ record.gender || '-' }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'className'">
|
||||
{{ record.className || '-' }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
@ -627,7 +630,7 @@ const loadStudentsForSelect = async () => {
|
||||
studentsLoading.value = true;
|
||||
try {
|
||||
const result = await getStudents({
|
||||
page: studentPagination.current,
|
||||
pageNum: studentPagination.current,
|
||||
pageSize: studentPagination.pageSize,
|
||||
keyword: studentSearchKeyword.value || undefined,
|
||||
classId: studentClassFilter.value || undefined,
|
||||
|
||||
@ -77,6 +77,13 @@
|
||||
>
|
||||
<a-button type="link" size="small" danger>取消</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm
|
||||
v-if="isScheduleCancelled(record.status)"
|
||||
title="确定要删除此排课吗?删除后不可恢复!"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
@ -166,6 +173,7 @@ import {
|
||||
getSchedules,
|
||||
updateSchedule,
|
||||
cancelSchedule,
|
||||
deleteSchedule,
|
||||
getClasses,
|
||||
getTeachers,
|
||||
getSchoolCourses,
|
||||
@ -195,6 +203,20 @@ const isScheduleActive = (status: string): boolean => {
|
||||
return status === 'ACTIVE' || status === 'scheduled';
|
||||
};
|
||||
|
||||
const isScheduleCancelled = (status: string): boolean => {
|
||||
return status === 'CANCELLED' || status === 'cancelled';
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteSchedule(id);
|
||||
message.success('删除成功');
|
||||
loadSchedules();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 数据
|
||||
const loading = ref(false);
|
||||
const schedules = ref<SchedulePlan[]>([]);
|
||||
|
||||
@ -121,6 +121,15 @@
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div class="detail-actions" style="margin-top: 16px; text-align: right;">
|
||||
<a-popconfirm
|
||||
v-if="isScheduleCancelled(selectedSchedule.status)"
|
||||
title="确定要删除此排课吗?删除后不可恢复!"
|
||||
@confirm="handleDelete(selectedSchedule.id)"
|
||||
>
|
||||
<a-button type="primary" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
@ -138,6 +147,7 @@ import {
|
||||
getTimetable,
|
||||
getClasses,
|
||||
getTeachers,
|
||||
deleteSchedule,
|
||||
type TimetableItem,
|
||||
type SchedulePlan,
|
||||
type ClassInfo,
|
||||
@ -162,6 +172,21 @@ const isScheduleActive = (status: string): boolean => {
|
||||
return status === 'ACTIVE' || status === 'scheduled';
|
||||
};
|
||||
|
||||
const isScheduleCancelled = (status: string): boolean => {
|
||||
return status === 'CANCELLED' || status === 'cancelled';
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await deleteSchedule(id);
|
||||
message.success('删除成功');
|
||||
detailVisible.value = false;
|
||||
loadTimetable();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 日程来源辅助函数
|
||||
const getScheduleSourceText = (source: string): string => {
|
||||
return translateScheduleSourceType(source) || source;
|
||||
|
||||
@ -172,6 +172,12 @@
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status" v-if="isEdit">
|
||||
<a-radio-group v-model:value="formState.status">
|
||||
<a-radio value="ACTIVE">在职</a-radio>
|
||||
<a-radio value="INACTIVE">离职</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
@ -216,7 +222,7 @@ import {
|
||||
LockOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { translateGenericStatus, getGenericStatusStyle } from '@/utils/tagMaps';
|
||||
import {
|
||||
@ -285,13 +291,14 @@ const activeCount = computed(() => {
|
||||
return teachers.value.filter(t => isTeacherActive(t.status)).length;
|
||||
});
|
||||
|
||||
const formState = reactive<CreateTeacherDto & { id?: number }>({
|
||||
const formState = reactive<CreateTeacherDto & { id?: number; status?: string }>({
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
loginAccount: '',
|
||||
password: '',
|
||||
classIds: [],
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
|
||||
const rules: Record<string, any[]> = {
|
||||
@ -350,6 +357,7 @@ const resetForm = () => {
|
||||
formState.loginAccount = '';
|
||||
formState.password = '';
|
||||
formState.classIds = [];
|
||||
formState.status = 'ACTIVE';
|
||||
};
|
||||
|
||||
const showAddModal = () => {
|
||||
@ -366,6 +374,7 @@ const handleEdit = (record: Teacher) => {
|
||||
formState.email = record.email || '';
|
||||
formState.loginAccount = record.loginAccount;
|
||||
formState.classIds = record.classIds || [];
|
||||
formState.status = record.status || 'ACTIVE';
|
||||
modalVisible.value = true;
|
||||
};
|
||||
|
||||
@ -375,11 +384,38 @@ const handleModalOk = async () => {
|
||||
submitting.value = true;
|
||||
|
||||
if (isEdit.value && formState.id) {
|
||||
// 检查是否要将状态设为离职
|
||||
if (formState.status === 'INACTIVE') {
|
||||
// 获取教师当前状态(用于判断是否是从 ACTIVE 变为 INACTIVE)
|
||||
const teacher = teachers.value.find(t => t.id === formState.id);
|
||||
const wasActive = teacher?.status === 'ACTIVE';
|
||||
|
||||
if (wasActive) {
|
||||
// 弹出确认框
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
Modal.confirm({
|
||||
title: '确认离职操作',
|
||||
content: '确定要将此教师设为离职状态吗?离职后将无法登录系统。',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
submitting.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateTeacher(formState.id, {
|
||||
name: formState.name,
|
||||
phone: formState.phone,
|
||||
email: formState.email,
|
||||
classIds: formState.classIds,
|
||||
status: formState.status,
|
||||
});
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
package com.reading.platform.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 排课重复类型枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum ScheduleRepeatType {
|
||||
|
||||
NONE("NONE", "不重复"),
|
||||
DAILY("DAILY", "每日"),
|
||||
WEEKLY("WEEKLY", "每周"),
|
||||
BIWEEKLY("BIWEEKLY", "双周");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
ScheduleRepeatType(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码值转换为枚举
|
||||
*/
|
||||
public static ScheduleRepeatType fromCode(String code) {
|
||||
if (code == null) {
|
||||
return NONE;
|
||||
}
|
||||
for (ScheduleRepeatType type : values()) {
|
||||
if (type.getCode().equalsIgnoreCase(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效的代码值
|
||||
*/
|
||||
public static boolean isValidCode(String code) {
|
||||
if (code == null) {
|
||||
return false;
|
||||
}
|
||||
for (ScheduleRepeatType type : values()) {
|
||||
if (type.getCode().equalsIgnoreCase(code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.reading.platform.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 排课状态枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum ScheduleStatus {
|
||||
|
||||
ACTIVE("ACTIVE", "有效"),
|
||||
SCHEDULED("SCHEDULED", "已排期"),
|
||||
CANCELLED("CANCELLED", "已取消");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
ScheduleStatus(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代码值转换为枚举
|
||||
*/
|
||||
public static ScheduleStatus fromCode(String code) {
|
||||
if (code == null) {
|
||||
return CANCELLED;
|
||||
}
|
||||
for (ScheduleStatus status : values()) {
|
||||
if (status.getCode().equalsIgnoreCase(code)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return CANCELLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有效/激活状态
|
||||
*/
|
||||
public static boolean isActive(String status) {
|
||||
if (status == null) {
|
||||
return false;
|
||||
}
|
||||
return ACTIVE.getCode().equalsIgnoreCase(status) ||
|
||||
SCHEDULED.getCode().equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为已取消状态
|
||||
*/
|
||||
public static boolean isCancelled(String status) {
|
||||
if (status == null) {
|
||||
return false;
|
||||
}
|
||||
return CANCELLED.getCode().equalsIgnoreCase(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前状态是否为激活状态
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return this == ACTIVE || this == SCHEDULED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前状态是否为已取消状态
|
||||
*/
|
||||
public boolean isCancelled() {
|
||||
return this == CANCELLED;
|
||||
}
|
||||
|
||||
}
|
||||
@ -116,6 +116,15 @@ public class SchoolScheduleController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/force")
|
||||
@Operation(summary = "删除排课(物理删除)")
|
||||
@Log(module = LogModule.SCHEDULE, type = LogOperationType.DELETE, description = "删除排课记录")
|
||||
public Result<Void> deleteSchedule(@PathVariable Long id) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
schoolScheduleService.deleteSchedule(id, tenantId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/batch")
|
||||
@Operation(summary = "批量创建排课")
|
||||
@Log(module = LogModule.SCHEDULE, type = LogOperationType.CREATE, description = "批量创建排课计划")
|
||||
|
||||
@ -102,6 +102,7 @@ public class SchoolStudentController {
|
||||
// 设置班级
|
||||
var clazz = classService.getPrimaryClassByStudentId(vo.getId());
|
||||
vo.setClassId(clazz != null ? clazz.getId() : null);
|
||||
vo.setClassName(clazz != null ? clazz.getName() : null);
|
||||
|
||||
// 设置家长信息(查询主要监护人)
|
||||
var parentRelation = parentStudentMapper.selectOne(
|
||||
|
||||
@ -55,6 +55,9 @@ public class StudentResponse {
|
||||
@Schema(description = "所在班级 ID(从 student_class_history 当前关联获取)")
|
||||
private Long classId;
|
||||
|
||||
@Schema(description = "所在班级名称(从 student_class_history 当前关联获取)")
|
||||
private String className;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
|
||||
@ -47,6 +47,14 @@ public interface SchoolScheduleService extends IService<SchedulePlan> {
|
||||
*/
|
||||
void cancelSchedule(Long id, Long tenantId);
|
||||
|
||||
/**
|
||||
* 删除排课(物理删除,仅限已取消状态)
|
||||
*
|
||||
* @param id 排课 ID
|
||||
* @param tenantId 租户 ID
|
||||
*/
|
||||
void deleteSchedule(Long id, Long tenantId);
|
||||
|
||||
/**
|
||||
* 获取排课详情
|
||||
*
|
||||
|
||||
@ -8,6 +8,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.reading.platform.common.enums.GenericStatus;
|
||||
import com.reading.platform.common.enums.ScheduleSourceType;
|
||||
import com.reading.platform.common.enums.ScheduleStatus;
|
||||
import com.reading.platform.common.enums.ScheduleRepeatType;
|
||||
import com.reading.platform.common.enums.ErrorCode;
|
||||
import com.reading.platform.common.exception.BusinessException;
|
||||
import com.reading.platform.dto.request.SchedulePlanCreateRequest;
|
||||
@ -182,12 +184,29 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
log.info("取消排课: id={}, tenantId={}", id, tenantId);
|
||||
|
||||
SchedulePlan plan = getScheduleById(id, tenantId);
|
||||
plan.setStatus("cancelled");
|
||||
plan.setStatus(ScheduleStatus.CANCELLED.getCode());
|
||||
schedulePlanMapper.updateById(plan);
|
||||
|
||||
log.info("排课取消成功: id={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteSchedule(Long id, Long tenantId) {
|
||||
log.info("删除排课:id={}, tenantId={}", id, tenantId);
|
||||
|
||||
SchedulePlan plan = getScheduleById(id, tenantId);
|
||||
|
||||
// 只允许删除已取消的排课
|
||||
if (!ScheduleStatus.isCancelled(plan.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_PARAMETER,
|
||||
"只能删除已取消的排课");
|
||||
}
|
||||
|
||||
schedulePlanMapper.deleteById(plan);
|
||||
log.info("排课删除成功:id={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchedulePlan getScheduleById(Long id, Long tenantId) {
|
||||
SchedulePlan plan = schedulePlanMapper.selectById(id);
|
||||
@ -225,7 +244,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
}
|
||||
if (StringUtils.hasText(status)) {
|
||||
if (GenericStatus.ACTIVE.getCode().equalsIgnoreCase(status)) {
|
||||
wrapper.in(SchedulePlan::getStatus, GenericStatus.ACTIVE.getCode(), "scheduled");
|
||||
wrapper.in(SchedulePlan::getStatus, GenericStatus.ACTIVE.getCode(), ScheduleStatus.SCHEDULED.getCode());
|
||||
} else {
|
||||
wrapper.eq(SchedulePlan::getStatus, status);
|
||||
}
|
||||
@ -254,7 +273,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
wrapper.eq(SchedulePlan::getTenantId, tenantId)
|
||||
.ge(SchedulePlan::getScheduledDate, startDate)
|
||||
.le(SchedulePlan::getScheduledDate, endDate)
|
||||
.ne(SchedulePlan::getStatus, GenericStatus.fromCode("CANCELLED").getCode());
|
||||
.ne(SchedulePlan::getStatus, ScheduleStatus.CANCELLED.getCode());
|
||||
|
||||
if (classId != null) {
|
||||
wrapper.eq(SchedulePlan::getClassId, classId);
|
||||
@ -444,7 +463,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
createRequest.setScheduledTime(request.getScheduledTime());
|
||||
createRequest.setRepeatType(request.getRepeatType());
|
||||
createRequest.setRepeatEndDate(request.getRepeatEndDate());
|
||||
createRequest.setSource("SCHOOL");
|
||||
createRequest.setSource(ScheduleSourceType.SCHOOL.getCode());
|
||||
createRequest.setNote(request.getNote());
|
||||
|
||||
// 检测冲突
|
||||
@ -484,7 +503,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
wrapper.eq(SchedulePlan::getTenantId, tenantId)
|
||||
.ge(SchedulePlan::getScheduledDate, startDate)
|
||||
.le(SchedulePlan::getScheduledDate, endDate)
|
||||
.ne(SchedulePlan::getStatus, GenericStatus.fromCode("CANCELLED").getCode());
|
||||
.ne(SchedulePlan::getStatus, ScheduleStatus.CANCELLED.getCode());
|
||||
|
||||
if (classId != null) {
|
||||
wrapper.eq(SchedulePlan::getClassId, classId);
|
||||
@ -640,7 +659,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
}
|
||||
|
||||
String repeatType = request.getRepeatType();
|
||||
if (!StringUtils.hasText(repeatType) || "NONE".equals(repeatType)) {
|
||||
if (!StringUtils.hasText(repeatType) || ScheduleRepeatType.NONE.getCode().equals(repeatType)) {
|
||||
// 单次排课
|
||||
dates.add(startDate);
|
||||
return dates;
|
||||
@ -659,21 +678,21 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
|
||||
"重复周数不能超过 " + MAX_REPEAT_WEEKS + " 周");
|
||||
}
|
||||
|
||||
if ("WEEKLY".equals(repeatType)) {
|
||||
if (ScheduleRepeatType.WEEKLY.getCode().equals(repeatType)) {
|
||||
// 每周重复
|
||||
LocalDate current = startDate;
|
||||
while (!current.isAfter(repeatEndDate)) {
|
||||
dates.add(current);
|
||||
current = current.plusWeeks(1);
|
||||
}
|
||||
} else if ("BIWEEKLY".equals(repeatType)) {
|
||||
} else if (ScheduleRepeatType.BIWEEKLY.getCode().equals(repeatType)) {
|
||||
// 双周重复
|
||||
LocalDate current = startDate;
|
||||
while (!current.isAfter(repeatEndDate)) {
|
||||
dates.add(current);
|
||||
current = current.plusWeeks(2);
|
||||
}
|
||||
} else if ("DAILY".equals(repeatType)) {
|
||||
} else if (ScheduleRepeatType.DAILY.getCode().equals(repeatType)) {
|
||||
// 每日重复
|
||||
LocalDate current = startDate;
|
||||
while (!current.isAfter(repeatEndDate)) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user