|
|
|
@ -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;
|
|
|
|
|