diff --git a/src/views/teacherEnd/task/index.vue b/src/views/teacherEnd/task/index.vue index 54cb7d2..a02d497 100644 --- a/src/views/teacherEnd/task/index.vue +++ b/src/views/teacherEnd/task/index.vue @@ -21,34 +21,55 @@ - - - - - - - + + @@ -67,16 +88,99 @@ const selectedTaskKeys = ref([]); const configured = ref(false); const loading = ref(false); const saving = ref(false); -const tableRef = ref(null); const comprehensiveTaskKey = "comprehensive-case-training"; +const expandedProjects = ref(new Set()); // 追踪展开的项目 let activeLoadGeneration = 0; -let selectionSyncGeneration = -1; const currentUserId = computed(() => userStore.userInfo.userId); 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 groupedTasks = computed(() => { + const projectMap = new Map(); + + tasks.value.forEach((task) => { + const projectName = task.projectName || "其他"; + + if (!projectMap.has(projectName)) { + projectMap.set(projectName, { + projectName, + tasks: [] + }); + } + + projectMap.get(projectName).tasks.push(task); + }); + + return Array.from(projectMap.values()); +}); + +// 判断项目是否展开 +function isProjectExpanded(projectName) { + return expandedProjects.value.has(projectName); +} + +// 切换项目展开/收起 +function toggleProject(projectName) { + if (expandedProjects.value.has(projectName)) { + expandedProjects.value.delete(projectName); + } else { + expandedProjects.value.add(projectName); + } +} + +// 判断任务是否被选中 +function isTaskSelected(taskKey) { + return selectedTaskKeys.value.includes(taskKey); +} + +// 判断项目是否完全选中 +function isProjectFullySelected(group) { + return group.tasks.every(task => isTaskSelected(task.taskKey)); +} + +// 判断项目是否部分选中 +function isProjectPartiallySelected(group) { + const selectedCount = group.tasks.filter(task => isTaskSelected(task.taskKey)).length; + return selectedCount > 0 && selectedCount < group.tasks.length; +} + +// 切换项目选中状态(全选/全不选) +function toggleProjectSelection(group) { + if (!canOperateSelectedClass.value) return; + + const isFullySelected = isProjectFullySelected(group); + + group.tasks.forEach(task => { + const index = selectedTaskKeys.value.indexOf(task.taskKey); + if (isFullySelected) { + // 如果已全选,则全不选 + if (index > -1) { + selectedTaskKeys.value.splice(index, 1); + } + } else { + // 如果未全选,则全选 + if (index === -1) { + selectedTaskKeys.value.push(task.taskKey); + } + } + }); +} + +// 切换任务选中状态 +function toggleTaskSelection(taskKey) { + if (!canOperateSelectedClass.value) return; + + const index = selectedTaskKeys.value.indexOf(taskKey); + if (index > -1) { + selectedTaskKeys.value.splice(index, 1); + } else { + selectedTaskKeys.value.push(taskKey); + } +} + async function loadClasses() { const res = await indexApi.getClassListBySchoolId({ schoolId: userStore.userInfo.schoolId }); classList.value = res.data || []; @@ -87,19 +191,6 @@ function isActiveLoad(requestedClassId, requestGeneration) { return classId.value === requestedClassId && requestGeneration === activeLoadGeneration; } -function syncTableSelection(taskKeys, requestGeneration) { - selectionSyncGeneration = requestGeneration; - tableRef.value?.clearSelection(); - proxy.$nextTick(() => { - if (requestGeneration !== activeLoadGeneration) return; - const selected = new Set(taskKeys); - tasks.value.forEach((task) => tableRef.value?.toggleRowSelection(task, selected.has(task.taskKey))); - proxy.$nextTick(() => { - if (requestGeneration === activeLoadGeneration) selectionSyncGeneration = -1; - }); - }); -} - function sortTasksWithComprehensiveLast(taskList) { return [...taskList].sort((left, right) => { const leftIsComprehensive = left?.taskKey === comprehensiveTaskKey; @@ -119,18 +210,17 @@ async function loadTasks(requestGeneration) { const res = await listClassTrainingTasks(classId.value, { enabledOnly: false }); if (!isActiveLoad(requestedClassId, requestGeneration)) return false; tasks.value = sortTasksWithComprehensiveLast(res.data || []); + + // 初始化展开状态:默认展开所有模块 + const projectNames = [...new Set(tasks.value.map(t => t.projectName || "其他"))]; + expandedProjects.value = new Set(projectNames); return true; } -function applySelection(requestGeneration) { - syncTableSelection(selectedTaskKeys.value, requestGeneration); -} - async function loadPublication() { const requestGeneration = ++activeLoadGeneration; selectedTaskKeys.value = []; configured.value = false; - syncTableSelection([], requestGeneration); if (!classId.value) { tasks.value = []; return; @@ -148,17 +238,11 @@ async function loadPublication() { : tasks.value .filter((task) => task.taskKey !== comprehensiveTaskKey) .map((task) => task.taskKey); - applySelection(requestGeneration); } finally { if (requestGeneration === activeLoadGeneration) loading.value = false; } } -function handleSelectionChange(selection) { - if (selectionSyncGeneration === activeLoadGeneration) return; - selectedTaskKeys.value = selection.map((task) => task.taskKey); -} - async function savePublication() { if (!canOperateSelectedClass.value || !classId.value) return; saving.value = true; @@ -218,91 +302,162 @@ onMounted(async () => { } } - /* ===== 实训任务名称首字徽标(对齐班级/学生/院系管理基准)===== */ - .name-cell { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - max-width: 100%; - vertical-align: middle; - - .name-text { - min-width: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; + /* ===== 树形结构 ===== */ + .task-tree { + background: #fff; + border-radius: 8px; + border: 1px solid #e5e7eb; + padding: 16px; + min-height: 300px; + } + + .tree-project { + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; } + } - .task-badge { - flex: 0 0 auto; - width: 28px; - height: 28px; - display: inline-flex; - align-items: center; - justify-content: center; - background: var(--edu-primary-soft); - color: var(--edu-primary); - border-radius: var(--r-control); - font-size: var(--fs-body); - font-weight: 600; + .project-row { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-radius: 6px; + transition: background-color 0.2s; + + &:hover { + background: #f9fafb; } + } - .name-text { - font-weight: 600; - color: var(--text-strong); + .project-checkbox { + flex-shrink: 0; + display: flex; + align-items: center; + + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #3b82f6; + + &:disabled { + cursor: not-allowed; + } + + &:indeterminate { + accent-color: #60a5fa; + } } } - /* ===== 自定义空状态 ===== */ - .table-empty { - padding: var(--sp-5) 0; + .project-header { + flex: 1; display: flex; - flex-direction: column; align-items: center; - gap: var(--sp-1); + gap: 8px; + cursor: pointer; + user-select: none; + } - .empty-icon { - font-size: 28px; - color: var(--text-muted); + .collapse-icon { + flex-shrink: 0; + color: #6b7280; + transition: transform 0.2s; + + &.collapsed { + transform: rotate(-90deg); } + } - .empty-title { - margin: var(--sp-1) 0 0; - font-size: var(--fs-body); - font-weight: 600; - color: var(--text-body); + .project-label { + font-size: 15px; + font-weight: 600; + color: #1f2937; + flex: 1; + } + + .project-count { + font-size: 13px; + color: #9ca3af; + font-weight: normal; + } + + .task-list { + margin-left: 24px; + margin-top: 4px; + } + + .task-row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 4px; + transition: background-color 0.2s; + + &:hover:not(.disabled) { + background: #f3f4f6; } - .empty-tip { - margin: 0; - font-size: var(--fs-tag); - color: var(--text-muted); + &.disabled { + opacity: 0.6; + cursor: not-allowed; } } - /* ===== 数据表格(纯白 + 灰线)===== */ - .task-table { - width: 100%; - background: #ffffff; - border: 1px solid var(--border-color); - border-radius: var(--r-card); - overflow: hidden; + .task-checkbox { + flex-shrink: 0; + display: flex; + align-items: center; - :deep(.el-table__header-wrapper th) { - background: #ffffff; - color: var(--text-strong); - font-weight: 600; - font-size: var(--fs-body); - } + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #3b82f6; - :deep(.el-table__body tr:hover > td) { - background: #f9fafb !important; + &:disabled { + cursor: not-allowed; + } } + } - :deep(.el-table__cell) { - font-size: var(--fs-body); - color: var(--text-strong); - border-color: var(--border-color); + .task-badge { + flex-shrink: 0; + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--edu-primary-soft); + color: var(--edu-primary); + border-radius: var(--r-control); + font-size: var(--fs-body); + font-weight: 600; + } + + .task-name { + flex: 1; + font-size: 14px; + color: #374151; + font-weight: 600; + } + + .task-hint { + font-size: 12px; + color: #9ca3af; + } + + /* ===== 自定义空状态 ===== */ + .task-empty { + padding: var(--sp-5) 0; + + .empty-icon { + font-size: 28px; + color: var(--text-muted); } } }