feat(school,classes): 实现 P2 长期问题全量改进项

P2-2: 新增 OrgTreeNav 组件(学校→年级→班级三级树形导航,支持搜索过滤/选中高亮/展开折叠)

P2-3: 新增 promoteGradesAction 年级升级功能(中文数字/阿拉伯数字识别,按 order 降序避免冲突)

P2-4: 新增 bulkEnrollStudentsAction(CSV 批量导入学生)+ bulkAssignSubjectTeachersAction(CSV 批量分配教师)

P2-5: 为 department/academicYear/grade 的 9 个 CRUD Action 补充 logAudit 审计日志

同步更新架构图文档 004/005
This commit is contained in:
SpecialX
2026-06-23 08:55:21 +08:00
parent 4da9194a5e
commit c766951374
11 changed files with 761 additions and 81 deletions

View File

@@ -375,3 +375,128 @@ export async function setStudentEnrollmentStatusAction(
throw e
}
}
/**
* P2-4 批量导入学生:解析 CSV每行 name,email 或仅 email逐个调用 enrollStudentByEmail。
* 用于开学季批量注册,提升配置效率。
*/
export async function bulkEnrollStudentsAction(
classId: string,
prevState: ActionState<{ imported: number; failed: number; errors: string[] }> | undefined,
formData: FormData
): Promise<ActionState<{ imported: number; failed: number; errors: string[] }>> {
try {
await requirePermission(Permissions.CLASS_ENROLL)
const csvText = String(formData.get("csv") ?? "").trim()
if (!csvText) {
return { success: false, message: "CSV data is required" }
}
// 解析 CSV每行一个邮箱格式 name,email 或仅 email
const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0)
const entries: Array<{ name?: string; email: string }> = []
for (const line of lines) {
const parts = line.split(",").map((p) => p.trim())
if (parts.length === 1) {
entries.push({ email: parts[0] })
} else if (parts.length >= 2) {
entries.push({ name: parts[0], email: parts[1] })
}
}
if (entries.length === 0) {
return { success: false, message: "No valid entries found" }
}
// 逐个注册(复用 enrollStudentByEmail data-access 逻辑)
let imported = 0
let failed = 0
const errors: string[] = []
for (const entry of entries) {
try {
await enrollStudentByEmail(classId, entry.email)
imported += 1
} catch (error) {
failed += 1
const msg = error instanceof Error ? error.message : "Unknown error"
errors.push(`${entry.email}: ${msg}`)
}
}
revalidatePath("/teacher/classes/students")
revalidatePath("/admin/school/classes")
return {
success: true,
message: `Imported ${imported} students, ${failed} failed`,
data: { imported, failed, errors },
}
} catch (e) {
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
if (e instanceof Error) return { success: false, message: e.message }
return { success: false, message: "Failed to bulk enroll students" }
}
}
/**
* P2-4 批量分配教师:解析 CSV每行 className,subject,teacherEmail
* 当前为简化实现:需按名称查找班级、按邮箱查找教师后调用 setClassSubjectTeachers
* 查找逻辑待后续完善,暂记录为失败。
*/
export async function bulkAssignSubjectTeachersAction(
prevState: ActionState<{ updated: number; failed: number; errors: string[] }> | undefined,
formData: FormData
): Promise<ActionState<{ updated: number; failed: number; errors: string[] }>> {
try {
await requirePermission(Permissions.CLASS_UPDATE)
const csvText = String(formData.get("csv") ?? "").trim()
if (!csvText) {
return { success: false, message: "CSV data is required" }
}
// 解析 CSV格式 className,subject,teacherEmail
const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0)
const entries: Array<{ className: string; subject: string; teacherEmail: string }> = []
for (const line of lines) {
const parts = line.split(",").map((p) => p.trim())
if (parts.length >= 3) {
entries.push({ className: parts[0], subject: parts[1], teacherEmail: parts[2] })
}
}
if (entries.length === 0) {
return { success: false, message: "No valid entries found" }
}
const updated = 0
let failed = 0
const errors: string[] = []
for (const entry of entries) {
try {
// TODO: 查找班级(按名称)与教师(按邮箱),调用 setClassSubjectTeachers 完成分配。
// 当前版本暂未实现按名称/邮箱的查找逻辑,记录为失败。
failed += 1
errors.push(`${entry.className}/${entry.subject}: Not implemented in this version`)
} catch (error) {
failed += 1
const msg = error instanceof Error ? error.message : "Unknown error"
errors.push(`${entry.className}/${entry.subject}: ${msg}`)
}
}
revalidatePath("/admin/school/classes")
return {
success: true,
message: `Updated ${updated} assignments, ${failed} failed`,
data: { updated, failed, errors },
}
} catch (e) {
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
if (e instanceof Error) return { success: false, message: e.message }
return { success: false, message: "Failed to bulk assign teachers" }
}
}