@ -21,34 +21,55 @@
< el -alert v -if = " classId & & ! canOperateSelectedClass " title = "只能配置自己创建的教学班,当前班级仅可查看。" type = "info" :closable ="false" show -icon / >
< el -table
ref = "tableRef"
: data = "tasks"
row - key = "taskKey"
v - loading = "loading"
class = "task-table"
@ selection - change = "handleSelectionChange"
>
< el -table -column type = "selection" width = "58" :reserve-selection ="true" : selectable = "() => canOperateSelectedClass" / >
< el -table -column prop = "taskName" label = "实训任务" min -width = " 280 " header -align = " left " align = "left" >
< template # default = "{ row }" >
< div class = "name-cell" >
< span class = "task-badge" > { { ( row . taskName || "?" ) . charAt ( 0 ) } } < / span >
< span class = "name-text" > { { row . taskName } } < / span >
<!-- 树形结构展示 -- >
< section v-loading ="loading" class="task-tree" >
< div v-for ="group in groupedTasks" :key="group.projectName" class="tree-project" >
<!-- 模块标题行 -- >
< div class = "project-row" >
< div class = "project-checkbox" @ click.stop >
< input
type = "checkbox"
: checked = "isProjectFullySelected(group)"
: indeterminate = "isProjectPartiallySelected(group)"
: disabled = "!canOperateSelectedClass"
@ change = "toggleProjectSelection(group)"
/ >
< / div >
< / template >
< / e l - t a b l e - c o l u m n >
< el -table -column prop = "projectName" label = "所属模块" min -width = " 180 " align = "center" header -align = " center " / >
< template # empty >
< div class = "table-empty" >
< div class = "project-header" @click ="toggleProject(group.projectName)" >
< svg class = "collapse-icon" : class = "{ collapsed: !isProjectExpanded(group.projectName) }" width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke -width = " 2 " >
< polyline points = "6 9 12 15 18 9" / >
< / svg >
< span class = "project-label" > { { group . projectName } } < / span >
< span class = "project-count" > ( { { group . tasks . length } } ) < / span >
< / div >
< / div >
<!-- 任务列表 -- >
< div v-show ="isProjectExpanded(group.projectName)" class="task-list" >
< div v-for ="task in group.tasks" :key="task.taskKey" class="task-row" :class="{ disabled: !canOperateSelectedClass }" >
< div class = "task-checkbox" >
< input
type = "checkbox"
: checked = "isTaskSelected(task.taskKey)"
: disabled = "!canOperateSelectedClass"
@ change = "toggleTaskSelection(task.taskKey)"
/ >
< / div >
< span class = "task-badge" > { { ( task . taskName || "?" ) . charAt ( 0 ) } } < / span >
< span class = "task-name" > { { task . taskName } } < / span >
< span class = "task-hint" v-if ="task.taskKey === comprehensiveTaskKey" > ( 默 认 不 发 布 ) < / span >
< / div >
< / div >
< / div >
< el -empty v-if ="!groupedTasks.length && !loading" class="task-empty" description="暂无实训任务" >
< template # image >
< el -icon class = "empty-icon" >
< FolderOpened / >
< / e l - i c o n >
< p class = "empty-title" > 暂无实训任务 < / p >
< p class = "empty-tip" > 可通过顶部选择教学班加载实训任务列表 < / p >
< / div >
< / template >
< / e l - t a b l e >
< / template >
< / e l - e m p t y >
< / section >
< / div >
< / template >
@ -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 : 8 px ;
border : 1 px solid # e5e7eb ;
padding : 16 px ;
min - height : 300 px ;
}
. tree - project {
margin - bottom : 12 px ;
& : last - child {
margin - bottom : 0 ;
}
}
. task - badge {
flex : 0 0 auto ;
width : 28 px ;
height : 28 px ;
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 : 8 px ;
padding : 10 px 12 px ;
border - radius : 6 px ;
transition : background - color 0.2 s ;
& : 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 : 16 px ;
height : 16 px ;
cursor : pointer ;
accent - color : # 3 b82f6 ;
& : disabled {
cursor : not - allowed ;
}
& : indeterminate {
accent - color : # 60 a5fa ;
}
}
}
/* ===== 自定义空状态 ===== */
. table - empty {
padding : var ( -- sp - 5 ) 0 ;
. project - header {
flex : 1 ;
display : flex ;
flex - direction : column ;
align - items : center ;
gap : var ( -- sp - 1 ) ;
gap : 8 px ;
cursor : pointer ;
user - select : none ;
}
. empty - icon {
font - size : 28 px ;
color : var ( -- text - muted ) ;
. collapse - icon {
flex - shrink : 0 ;
color : # 6 b7280 ;
transition : transform 0.2 s ;
& . collapsed {
transform : rotate ( - 90 deg ) ;
}
}
. empty - title {
margin : var ( -- sp - 1 ) 0 0 ;
font - size : var ( -- fs - body ) ;
font - weight : 600 ;
color : var ( -- text - body ) ;
. project - label {
font - size : 15 px ;
font - weight : 600 ;
color : # 1 f2937 ;
flex : 1 ;
}
. project - count {
font - size : 13 px ;
color : # 9 ca3af ;
font - weight : normal ;
}
. task - list {
margin - left : 24 px ;
margin - top : 4 px ;
}
. task - row {
display : flex ;
align - items : center ;
gap : 8 px ;
padding : 8 px 12 px ;
border - radius : 4 px ;
transition : background - color 0.2 s ;
& : 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 : 1 px 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 : 16 px ;
height : 16 px ;
cursor : pointer ;
accent - color : # 3 b82f6 ;
: 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 : 28 px ;
height : 28 px ;
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 : 14 px ;
color : # 374151 ;
font - weight : 600 ;
}
. task - hint {
font - size : 12 px ;
color : # 9 ca3af ;
}
/* ===== 自定义空状态 ===== */
. task - empty {
padding : var ( -- sp - 5 ) 0 ;
. empty - icon {
font - size : 28 px ;
color : var ( -- text - muted ) ;
}
}
}