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 @@
-
-
-
-
-
-
{{ (row.taskName || "?").charAt(0) }}
-
{{ row.taskName }}
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
{{ (task.taskName || "?").charAt(0) }}
+
{{ task.taskName }}
+
(默认不发布)
+
+
+
+
+
+
- 暂无实训任务
- 可通过顶部选择教学班加载实训任务列表
-
-
-
+
+
+
@@ -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);
}
}
}