feat: scope teacher training tasks to classes

dev-QQq
chenyuan 2 months ago
parent d7c8eb8488
commit c0191fe84e

@ -1,6 +1,23 @@
<template> <template>
<div class="training-task-page app-container"> <div class="training-task-page app-container">
<section class="task-filter"> <section class="task-filter">
<div class="class-filter">
<span class="filter-label">教学班</span>
<el-select
v-model="selectedTeachingClassId"
:loading="classesLoading"
placeholder="请选择教学班"
class="class-select"
@change="queryTasks"
>
<el-option
v-for="schoolClass in teachingClassList"
:key="schoolClass.schoolClassId"
:label="schoolClass.className"
:value="schoolClass.schoolClassId"
/>
</el-select>
</div>
<div class="project-filter"> <div class="project-filter">
<span class="filter-label">所属项目</span> <span class="filter-label">所属项目</span>
<button <button
@ -17,10 +34,7 @@
<el-input v-model.trim="keyword" clearable placeholder="请输入实训任务名称" @keyup.enter="queryTasks" /> <el-input v-model.trim="keyword" clearable placeholder="请输入实训任务名称" @keyup.enter="queryTasks" />
<el-button type="primary" @click="queryTasks"></el-button> <el-button type="primary" @click="queryTasks"></el-button>
<div class="toolbar-actions"> <div class="toolbar-actions">
<button type="button" class="tool-link" @click="downloadTemplate"></button> <el-tag v-if="selectedTeachingClass" type="success"></el-tag>
<el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="handleImportChange">
<button type="button" class="tool-link">导入配置</button>
</el-upload>
</div> </div>
</div> </div>
</section> </section>
@ -94,10 +108,6 @@
<el-input v-model="taskForm.title" placeholder="保存后会同步为学生端左侧导航名称" /> <el-input v-model="taskForm.title" placeholder="保存后会同步为学生端左侧导航名称" />
</el-form-item> </el-form-item>
<el-form-item label="任务分析:">
<el-input v-model="taskForm.knowledge" type="textarea" :rows="4" placeholder="请输入任务分析" />
</el-form-item>
<el-form-item label="实训背景:"> <el-form-item label="实训背景:">
<el-input v-model="taskForm.background" type="textarea" :rows="5" placeholder="请输入实训背景" /> <el-input v-model="taskForm.background" type="textarea" :rows="5" placeholder="请输入实训背景" />
</el-form-item> </el-form-item>
@ -172,29 +182,40 @@
<script setup> <script setup>
import { import {
createTrainingTask, listClassTrainingTasks,
downloadTrainingTaskTemplate, saveClassTrainingTask,
importTrainingTasks,
listTrainingTasks,
updateTrainingTask,
uploadTrainingTaskMaterial, uploadTrainingTaskMaterial,
} from "@/api/trainingTask"; } from "@/api/trainingTask";
import * as indexApi from "@/api/teacher";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const activeProject = ref(""); const activeProject = ref("");
const keyword = ref(""); const keyword = ref("");
const loading = ref(false); const loading = ref(false);
const classesLoading = ref(false);
const saving = ref(false); const saving = ref(false);
const materialUploading = ref(false); const materialUploading = ref(false);
const tasks = ref([]); const tasks = ref([]);
const classList = ref([]);
const selectedTeachingClassId = ref("");
const dialogVisible = ref(false); const dialogVisible = ref(false);
const dialogMode = ref("view"); const dialogMode = ref("view");
const currentTaskId = ref("");
const taskForm = reactive(createEmptyTask()); const taskForm = reactive(createEmptyTask());
const currentUserId = computed(() => userStore.userInfo.userId);
const isReadonly = computed(() => dialogMode.value === "view"); const isReadonly = computed(() => dialogMode.value === "view");
const hasTaskMaterial = computed(() => isRealMaterial(taskForm.materialName, taskForm.materialUrl)); const hasTaskMaterial = computed(() => isRealMaterial(taskForm.materialName, taskForm.materialUrl));
const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))]); const projects = computed(() => [...new Set(tasks.value.map((task) => task.project).filter(Boolean))]);
const teachingClassList = computed(() => {
return classList.value.filter((schoolClass) => {
return normalizeClassType(schoolClass) === "TEACHING" && String(schoolClass.createdBy || "") === String(currentUserId.value || "");
});
});
const selectedTeachingClass = computed(() => {
return teachingClassList.value.find((schoolClass) => schoolClass.schoolClassId === selectedTeachingClassId.value);
});
const filteredTasks = computed(() => { const filteredTasks = computed(() => {
const text = keyword.value.trim(); const text = keyword.value.trim();
@ -206,7 +227,7 @@ const filteredTasks = computed(() => {
}); });
onMounted(() => { onMounted(() => {
queryTasks(); loadTeachingClasses();
}); });
function toggleProject(project) { function toggleProject(project) {
@ -214,9 +235,14 @@ function toggleProject(project) {
} }
async function queryTasks() { async function queryTasks() {
if (!selectedTeachingClassId.value) {
tasks.value = [];
activeProject.value = "";
return;
}
loading.value = true; loading.value = true;
try { try {
const res = await listTrainingTasks({ enabledOnly: false }); const res = await listClassTrainingTasks(selectedTeachingClassId.value, { enabledOnly: false });
tasks.value = (res.data || []).map(normalizeTask); tasks.value = (res.data || []).map(normalizeTask);
if (activeProject.value && !projects.value.includes(activeProject.value)) { if (activeProject.value && !projects.value.includes(activeProject.value)) {
activeProject.value = ""; activeProject.value = "";
@ -228,6 +254,27 @@ async function queryTasks() {
} }
} }
async function loadTeachingClasses() {
classesLoading.value = true;
try {
const res = await indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId });
classList.value = res.data || [];
selectedTeachingClassId.value = teachingClassList.value[0]?.schoolClassId || "";
await queryTasks();
} catch (error) {
classList.value = [];
selectedTeachingClassId.value = "";
tasks.value = [];
} finally {
classesLoading.value = false;
}
}
function normalizeClassType(schoolClass) {
if (schoolClass?.classType) return schoolClass.classType;
return schoolClass?.createdBy ? "TEACHING" : "ADMIN";
}
function viewTask(task) { function viewTask(task) {
openTaskDialog(task, "view"); openTaskDialog(task, "view");
} }
@ -242,7 +289,6 @@ function createEmptyTask() {
taskKey: "", taskKey: "",
project: "", project: "",
title: "", title: "",
knowledge: "",
background: "", background: "",
objectives: [""], objectives: [""],
requirements: "", requirements: "",
@ -261,7 +307,6 @@ function normalizeTask(task) {
taskKey: task.taskKey, taskKey: task.taskKey,
project: task.projectName, project: task.projectName,
title: task.taskName, title: task.taskName,
knowledge: task.knowledge || "",
background: task.background || "", background: task.background || "",
objectives: parseArray(task.objectives, [""]), objectives: parseArray(task.objectives, [""]),
requirements: task.requirements || "", requirements: task.requirements || "",
@ -316,7 +361,6 @@ function normalizeSteps(steps) {
function openTaskDialog(task, mode) { function openTaskDialog(task, mode) {
dialogMode.value = mode; dialogMode.value = mode;
currentTaskId.value = task.id;
Object.assign(taskForm, cloneTask(task)); Object.assign(taskForm, cloneTask(task));
dialogVisible.value = true; dialogVisible.value = true;
} }
@ -338,13 +382,6 @@ function removeObjective(index) {
taskForm.objectives.splice(index, 1); taskForm.objectives.splice(index, 1);
} }
async function handleImportChange(file) {
if (!file?.raw) return;
await importTrainingTasks(file.raw);
proxy?.$modal?.msgSuccess("导入成功");
queryTasks();
}
async function handleMaterialChange(file) { async function handleMaterialChange(file) {
const rawFile = file?.raw; const rawFile = file?.raw;
if (!rawFile) return; if (!rawFile) return;
@ -363,26 +400,14 @@ async function handleMaterialChange(file) {
} }
} }
async function downloadTemplate() {
const blob = await downloadTrainingTaskTemplate();
const url = URL.createObjectURL(new Blob([blob]));
const link = document.createElement("a");
link.href = url;
link.download = "实训任务导入模板.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
async function submitTask() { async function submitTask() {
if (!selectedTeachingClass.value || !taskForm.taskKey) {
proxy?.$modal?.msgError?.("请选择教学班后再保存");
return;
}
saving.value = true; saving.value = true;
try { try {
if (currentTaskId.value) { await saveClassTrainingTask(selectedTeachingClassId.value, taskForm.taskKey, buildPayload());
await updateTrainingTask(currentTaskId.value, buildPayload());
} else {
await createTrainingTask(buildPayload());
}
dialogVisible.value = false; dialogVisible.value = false;
proxy?.$modal?.msgSuccess("保存成功"); proxy?.$modal?.msgSuccess("保存成功");
await queryTasks(); await queryTasks();
@ -396,7 +421,6 @@ function buildPayload() {
projectName: taskForm.project, projectName: taskForm.project,
taskKey: taskForm.taskKey, taskKey: taskForm.taskKey,
taskName: taskForm.title, taskName: taskForm.title,
knowledge: taskForm.knowledge,
background: taskForm.background, background: taskForm.background,
objectives: JSON.stringify(taskForm.objectives.map((item) => item.trim()).filter(Boolean)), objectives: JSON.stringify(taskForm.objectives.map((item) => item.trim()).filter(Boolean)),
requirements: taskForm.requirements, requirements: taskForm.requirements,
@ -421,6 +445,7 @@ function buildPayload() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
gap: 24px; gap: 24px;
padding: 20px 22px; padding: 20px 22px;
margin: 0 0 32px; margin: 0 0 32px;
@ -455,6 +480,21 @@ function buildPayload() {
} }
} }
.class-filter {
position: relative;
z-index: 1;
display: flex;
align-items: center;
flex: 0 0 300px;
gap: 10px;
min-width: 280px;
}
.class-select {
flex: 1;
min-width: 180px;
}
.project-filter { .project-filter {
display: flex; display: flex;
align-items: center; align-items: center;

Loading…
Cancel
Save