feat: 支持教学班任务发布配置

fix/non-blocking-route-navigation
chenyuan 3 months ago
parent 83abcba523
commit 8fdbee994d

@ -548,6 +548,14 @@ export function getTaskList(query) {
params: query,
});
}
// 查询教学班实训任务发布状态;未保存过配置时 configured=false,学生端默认全部可见。
export function getTaskPublication(query) {
return request({
url: "/api/taskAllocation/selectTaskPublicationByClassId",
method: "POST",
params: query,
});
}
export function getStudentTaskList(query) {
return request({
url: "/api/taskAllocation/selectTaskAllocationByStudentUserId",

@ -376,7 +376,10 @@ const trainingTaskKeyByPath = {
"/assessment/diagnosis": "product-diagnosis-iteration",
};
const trainingTaskTitleByPath = ref({});
const currentCourseNav = computed(() => (isTeacherRole.value ? filterTeacherManageNavigation(teacherManageNav) : studentCourseNav));
const publishedTaskKeys = ref(null);
const currentCourseNav = computed(() => (isTeacherRole.value
? filterTeacherManageNavigation(teacherManageNav)
: filterStudentCourseNavigation(studentCourseNav, publishedTaskKeys.value)));
const studentIconByPath = {
"/assistant/index": "MagicStick",
"/foundation/new-product-survey": "Search",
@ -459,6 +462,7 @@ function getCourseNavTitle(item) {
async function loadTrainingTaskTitles() {
if (!isStudentRole.value) {
trainingTaskTitleByPath.value = {};
publishedTaskKeys.value = null;
return;
}
try {
@ -469,6 +473,7 @@ async function loadTrainingTaskTitles() {
titleByKey[task.taskKey] = task.taskName;
}
});
publishedTaskKeys.value = new Set(Object.keys(titleByKey));
const titleByPath = {};
Object.entries(trainingTaskKeyByPath).forEach(([path, taskKey]) => {
if (titleByKey[taskKey]) {
@ -478,8 +483,22 @@ async function loadTrainingTaskTitles() {
trainingTaskTitleByPath.value = titleByPath;
} catch (error) {
trainingTaskTitleByPath.value = {};
publishedTaskKeys.value = new Set();
}
}
function filterStudentCourseNavigation(sections, allowedTaskKeys) {
if (allowedTaskKeys == null) return sections;
return sections
.map((section) => {
if (section.path) return section;
const children = (section.children || []).filter((child) => {
const taskKey = trainingTaskKeyByPath[child.path];
return !taskKey || allowedTaskKeys.has(taskKey);
});
return { ...section, children };
})
.filter((section) => section.path || section.children?.length);
}
function groupAndSortModules(modules) {
const grouped = modules.reduce((acc, item) => {
const prefix = item.module.split("-")[0].trim();
@ -570,13 +589,8 @@ const getTaskList = async () => {
await userStore.ensureSchoolProductConfig().catch(() => null);
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(schoolAdminRoutes)];
} else if (userStore.userInfo.roleId != 3) {
indexApi.getStudentTaskList({ userId: userStore.userInfo.userId }).then((res) => {
const disabledTitles = processModuleStatus(groupAndSortModules(res.data)).disabledParents;
console.log(processModuleStatus(groupAndSortModules(res.data)));
proxy.$cache.session.setJSON("disabledChildren", processModuleStatus(groupAndSortModules(res.data)).disabledChildren);
proxy.$cache.session.setJSON("disabledTitles", disabledTitles);
await loadTrainingTaskTitles();
sidebarRouters.value = [...constantRoutes, ...dynamicRoutes];
});
} else {
await userStore.ensureSchoolProductConfig().catch(() => null);
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(teacherRoutes)];
@ -622,6 +636,5 @@ watch(
);
onMounted(() => {
getTaskList();
loadTrainingTaskTitles();
});
</script>

@ -1,189 +1,132 @@
<template>
<div class="task app-container">
<div class="task-toolbar">
<div class="online">
<span>教学班:</span>
<el-select placeholder="请选择教学班" v-model="classId" clearable class="ml10 class-select" @change="getTaskList">
<el-option v-for="school in teachingClassList" :key="school.schoolClassId" :label="school.className" :value="school.schoolClassId" />
<section class="task-toolbar">
<div class="class-picker">
<span>教学班</span>
<el-select v-model="classId" placeholder="请选择教学班" class="class-select" @change="loadPublication">
<el-option v-for="item in teachingClassList" :key="item.schoolClassId" :label="item.className" :value="item.schoolClassId" />
</el-select>
</div>
<el-tag v-if="selectedClass" :type="canOperateSelectedClass ? 'success' : 'info'">
{{ canOperateSelectedClass ? "可分配" : "仅查看" }}
<el-tag v-if="classId" :type="configured ? 'success' : 'warning'">
{{ configured ? '已保存发布配置' : '默认全部发布(未配置)' }}
</el-tag>
</div>
<span v-if="classId" class="task-tip">{{ configured ? '仅勾选的实训任务会出现在学生端。' : '当前所有实训任务均已默认勾选,保存后将按勾选结果发布。' }}</span>
<el-button type="primary" :disabled="!canOperateSelectedClass || !classId" :loading="saving" @click="savePublication">保存发布配置</el-button>
</section>
<el-alert v-if="classId && !canOperateSelectedClass" title="只能配置自己创建的教学班,当前班级仅可查看。" type="info" :closable="false" show-icon />
<el-table
:data="tableData"
ref="tableRef"
:data="tasks"
row-key="taskKey"
border
v-loading="loading"
class="task-table"
height="600"
ref="tableRef"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
row-key="id"
@select="handleSelect"
@select-all="handleSelectAll"
@selection-change="handleSelectionChange"
>
<el-table-column label="勾选" type="selection" width="100" align="center" :reserve-selection="true" :selectable="canSelectTask"></el-table-column>
<el-table-column label="实训任务" prop="module" align="center"></el-table-column>
<el-table-column type="selection" width="58" :reserve-selection="true" :selectable="() => canOperateSelectedClass" />
<el-table-column prop="taskName" label="实训任务" min-width="240" align="center" header-align="center" />
</el-table>
</div>
</template>
<script setup>
const { proxy } = getCurrentInstance();
import { listTrainingTasks } from "@/api/trainingTask";
import * as indexApi from "@/api/teacher";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
import * as indexApi from "@/api/teacher";
import { onMounted } from "vue";
const classList = ref([]);
const tableData = ref([]);
const tasks = ref([]);
const classId = ref("");
const selectedTaskKeys = ref([]);
const configured = ref(false);
const loading = ref(false);
const saving = ref(false);
const tableRef = ref(null);
const classId = ref("");
const currentUserId = computed(() => userStore.userInfo.userId);
const selectedClass = computed(() => classList.value.find((item) => item.schoolClassId === classId.value));
const teachingClassList = computed(() => classList.value.filter((item) => normalizeClassType(item) === "TEACHING"));
const teachingClassList = computed(() => classList.value.filter((item) => item.classType === "TEACHING" || !item.classType && item.createdBy));
const selectedClass = computed(() => teachingClassList.value.find((item) => item.schoolClassId === classId.value));
const canOperateSelectedClass = computed(() => selectedClass.value?.createdBy === currentUserId.value);
const normalizeClassType = (schoolClass) => {
if (schoolClass?.classType) return schoolClass.classType;
return schoolClass?.createdBy ? "TEACHING" : "ADMIN";
};
// 获取班级列表
const getClassList = () => {
indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }).then((res) => {
async function loadClasses() {
const res = await indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId });
classList.value = res.data || [];
classId.value = teachingClassList.value[0]?.schoolClassId || "";
getTaskList();
});
};
// 获取任务列表
const getTaskList = () => {
tableRef.value?.clearSelection?.();
if (!classId.value) {
tableData.value = [];
return;
}
loading.value = true;
indexApi
.getTaskList({ classId: classId.value })
.then((res) => {
tableData.value = res.data || [];
setInitialSelection();
})
.finally(() => {
loading.value = false;
});
};
// 其他引入和定义保持不变
}
const isInitializing = ref(true); // 添加初始化标志
async function loadTasks() {
const res = await listTrainingTasks({ enabledOnly: false });
tasks.value = res.data || [];
}
// 修改初始化方法
const setInitialSelection = () => {
isInitializing.value = true;
function applySelection() {
tableRef.value?.clearSelection();
proxy.$nextTick(() => {
tableData.value.forEach((row) => {
if (row.disabledStatus == 0) {
tableRef.value.toggleRowSelection(row, true);
}
const selected = new Set(selectedTaskKeys.value);
tasks.value.forEach((task) => tableRef.value?.toggleRowSelection(task, selected.has(task.taskKey)));
});
isInitializing.value = false; // 初始化完成
});
};
// 修改单选处理方法
const handleSelect = async (selection, row) => {
if (isInitializing.value) return; // 初始化时不处理
if (!canOperateSelectedClass.value) return;
row.disabledStatus = row.disabledStatus == 0 ? 1 : 0;
handleTaskDistribution(tableData.value);
};
// 修改全选处理方法
const handleSelectAll = async (selection) => {
if (isInitializing.value) return; // 初始化时不处理
if (!canOperateSelectedClass.value) return;
const newStatus = selection.length > 0 ? 0 : 1; // 全选为0,取消为1
try {
}
async function loadPublication() {
selectedTaskKeys.value = [];
configured.value = false;
if (!classId.value) return;
loading.value = true;
// 更新本地状态
tableData.value.forEach((item) => {
item.disabledStatus = newStatus;
});
handleTaskDistribution(tableData.value);
selection.length = 0;
} catch (error) {
console.error("批量更新失败:", error);
// 恢复选中状态
proxy.$nextTick(() => {
if (newStatus === 0) {
tableRef.value.clearSelection();
} else {
tableRef.value.toggleAllSelection();
}
});
try {
const res = await indexApi.getTaskPublication({ classId: classId.value });
configured.value = res.data?.configured === true;
selectedTaskKeys.value = configured.value
? (res.data?.taskKeys || [])
: tasks.value.map((task) => task.taskKey);
applySelection();
} finally {
loading.value = false;
}
// let { missingInArr2, missingInArr1 } = findMissingItems(selection, tableData.value);
// missingInArr1.forEach((item) => {
// item.disabledStatus = 1;
// });
// selection.forEach((item) => {
// item.disabledStatus = 0;
// });
// console.log(missingInArr1, selection, selection.length > 0 ? "selection" : "missingInArr1");
// handleTaskDistribution(selection.length > 0 ? selection : missingInArr1);
};
function findMissingItems(arr1, arr2) {
// 找出arr1中有但arr2中没有的元素
const missingInArr2 = arr1.filter((item) => !arr2.includes(item));
// 找出arr2中有但arr1中没有的元素
const missingInArr1 = arr2.filter((item) => !arr1.includes(item));
return {
missingInArr2,
missingInArr1,
};
}
// 修改任务分配
const handleTaskDistribution = (selection) => {
if (!canOperateSelectedClass.value) {
return;
}
const uniqueUsers = [...new Map(selection.map((item) => [item.module, item])).values()];
indexApi.addTask({ taskAllocationList: uniqueUsers, classId: classId.value, schoolId: userStore.userInfo.schoolId, userId: currentUserId.value }).then((res) => {
getTaskList();
proxy.$modal.msgSuccess("分配成功");
});
};
const canSelectTask = () => canOperateSelectedClass.value;
// 恢复选中状态
onMounted(() => {
getClassList();
});
</script>
<style lang='scss' scoped>
.task {
height: calc(100vh - 70px);
background: #ffffff;
function handleSelectionChange(selection) {
selectedTaskKeys.value = selection.map((task) => task.taskKey);
}
.task-toolbar {
display: flex;
align-items: center;
gap: 14px;
async function savePublication() {
if (!canOperateSelectedClass.value || !classId.value) return;
saving.value = true;
try {
const taskAllocationList = selectedTaskKeys.value.map((taskKey, index) => ({ module: taskKey, disabledStatus: 0, sort: index + 1 }));
await indexApi.addTask({
taskAllocationList,
classId: classId.value,
schoolId: userStore.userInfo.schoolId,
userId: currentUserId.value,
});
configured.value = true;
proxy.$modal.msgSuccess("发布配置已保存");
} finally {
saving.value = false;
}
}
.class-select {
width: 240px;
onMounted(async () => {
loading.value = true;
try {
await Promise.all([loadClasses(), loadTasks()]);
await loadPublication();
} finally {
loading.value = false;
}
});
</script>
.task-table {
width: 80%;
margin-top: 20px;
}
}
<style lang="scss" scoped>
.task { min-height: calc(100vh - 70px); }
.task-toolbar { display:flex; align-items:center; flex-wrap:wrap; gap:12px; margin-bottom:16px; padding:16px 18px; border:1px solid rgba(31,185,244,.32); border-radius:10px; background:rgba(3,31,48,.7); }
.class-picker { display:flex; align-items:center; gap:10px; color:#d7f7ff; font-weight:700; }
.class-select { width:260px; }
.task-tip { flex:1; min-width:280px; color:#8cc9df; font-size:13px; }
.task-table { width:100%; }
@media (max-width: 760px) { .class-picker,.class-select { width:100%; } .task-toolbar :deep(.el-button) { width:100%; } }
</style>

@ -0,0 +1,19 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const root = path.resolve(__dirname, "..");
const read = (file) => fs.readFileSync(path.join(root, file), "utf8");
const taskPage = read("src/views/teacherEnd/task/index.vue");
const sidebar = read("src/layout/components/Sidebar/index.vue");
const api = read("src/api/teacher.js");
assert(taskPage.includes("listTrainingTasks"), "task allocation must use training tasks as its data source");
assert(taskPage.includes("getTaskPublication"), "task allocation must load the selected class publication state");
assert(taskPage.includes("tasks.value.map((task) => task.taskKey)"), "an unconfigured class must default to all tasks selected");
assert(taskPage.includes("保存发布配置"), "task allocation must require an explicit save");
assert(api.includes("selectTaskPublicationByClassId"), "publication API missing");
assert(sidebar.includes("filterStudentCourseNavigation"), "student menu must be filtered by published training tasks");
assert(sidebar.includes("publishedTaskKeys"), "student menu must retain published task keys");
console.log("task publication front-end contract passed");
Loading…
Cancel
Save