@ -308,10 +308,10 @@
< span class = "section-number" > 03 < / span >
< div class = "section-info" >
< h4 > 资料与步骤 < / h4 >
< p > 上传案例资料 , 配置实训步骤 < / p >
< p > 上传案例资料 、案例视频 、 实训指导手册与AI排错指导手册 , 配置实训步骤 < / p >
< / div >
< / div >
< div class = "material-row" >
< div v-for ="slot in materialSlots" :key="slot.key" class="material-row" >
< div class = "material-info" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke - width = "2" >
@ -319,23 +319,29 @@
< polyline points = "14 2 14 8 20 8" / >
< / svg >
< div class = "material-text" >
< span v-if ="hasTaskMaterial" class="material-link" > {{ taskForm.materialName
} } < / span >
< span v -else class = "material-placeholder" > 暂未上传案例资料 < / span >
< span class = "material-label" > { { slot . label } } < / span >
< span v-if ="hasMaterial(slot.key)" class="material-link" > {{
taskForm [ ` ${ slot . key } Name ` ] } } < / span >
< span v -else class = "material-placeholder" > 暂未上传 < / span >
< / div >
< / div >
< el -upload v -if = " ! isReadonly " action = "#" :auto-upload ="false" :show-file-list ="false"
: on - change = "handleMaterialChange" >
< el -button type = "primary" :loading ="materialUploading" class = "upload-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor"
stroke - width = "2" >
< path d = "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" / >
< polyline points = "17 8 12 3 7 8" / >
< line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / >
< / svg >
{ { materialUploading ? "上传中..." : "上传案例资料" } }
< / e l - b u t t o n >
< / e l - u p l o a d >
< div class = "material-ops" >
< el -upload v -if = " ! isReadonly " action = "#" :auto-upload ="false" :show-file-list ="false"
: on - change = "(file) => handleMaterialChange(slot.key, file)" >
< el -button type = "primary" : loading = "uploadingField === slot.key"
class = "upload-btn" >
< svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none"
stroke = "currentColor" stroke - width = "2" >
< path d = "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" / >
< polyline points = "17 8 12 3 7 8" / >
< line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / >
< / svg >
{ { uploadingField === slot . key ? "上传中..." : "上传" } }
< / e l - b u t t o n >
< / e l - u p l o a d >
< el -button v -if = " ! isReadonly & & hasMaterial ( slot.key ) " type = "danger" plain
class = "clear-material-btn" @ click = "clearMaterial(slot.key)" > 清除 < / e l - b u t t o n >
< / div >
< / div >
< el -form -item label = "步骤名称:" >
@ -417,7 +423,14 @@ const loading = ref(false)
const classesLoading = ref ( false )
const saving = ref ( false )
const restoring = ref ( false )
const materialUploading = ref ( false )
const uploadingField = ref ( "" )
/ / 四 类 资 料 槽 位 : k e y 对 应 t a s k F o r m 的 ` $ { k e y } N a m e ` / ` $ { k e y } U r l ` 字 段
const materialSlots = [
{ key : "material" , label : "案例资料" } ,
{ key : "trainingData" , label : "案例视频" } ,
{ key : "guideManual" , label : "实训指导手册" } ,
{ key : "aiManual" , label : "AI排错指导手册" } ,
]
const tasks = ref ( [ ] )
const classList = ref ( [ ] )
const selectedTeachingClassId = ref ( "" )
@ -426,7 +439,6 @@ const dialogMode = ref("view")
const taskForm = reactive ( createEmptyTask ( ) )
const currentUserId = computed ( ( ) => userStore . userInfo . userId )
const isReadonly = computed ( ( ) => dialogMode . value === "view" )
const hasTaskMaterial = computed ( ( ) => isRealMaterial ( taskForm . materialName , taskForm . materialUrl ) )
const projects = computed ( ( ) => [ ... new Set ( tasks . value . map ( ( task ) => task . project ) . filter ( Boolean ) ) ] )
const projectOptions = computed ( ( ) => {
return projects . value . map ( ( project ) => ( {
@ -564,14 +576,31 @@ function createEmptyTask() {
requirements : "" ,
materialName : "" ,
materialUrl : "" ,
trainingDataName : "" ,
trainingDataUrl : "" ,
guideManualName : "" ,
guideManualUrl : "" ,
aiManualName : "" ,
aiManualUrl : "" ,
steps : createBuiltinSteps ( ) ,
sort : 0 ,
enabled : true ,
}
}
function hasMaterial ( slotKey ) {
return isRealMaterial ( taskForm [ ` ${ slotKey } Name ` ] , taskForm [ ` ${ slotKey } Url ` ] )
}
function materialLabel ( slotKey ) {
return materialSlots . find ( ( slot ) => slot . key === slotKey ) ? . label || "资料"
}
function normalizeTask ( task ) {
const material = normalizeMaterial ( task . materialName , task . materialUrl )
const trainingData = normalizeMaterial ( task . trainingDataName , task . trainingDataUrl )
const guideManual = normalizeMaterial ( task . guideManualName , task . guideManualUrl )
const aiManual = normalizeMaterial ( task . aiManualName , task . aiManualUrl )
return {
id : task . id ,
taskKey : task . taskKey ,
@ -582,6 +611,12 @@ function normalizeTask(task) {
requirements : task . requirements || "" ,
materialName : material . name ,
materialUrl : material . url ,
trainingDataName : trainingData . name ,
trainingDataUrl : trainingData . url ,
guideManualName : guideManual . name ,
guideManualUrl : guideManual . url ,
aiManualName : aiManual . name ,
aiManualUrl : aiManual . url ,
steps : normalizeSteps ( parseArray ( task . steps , createBuiltinSteps ( ) ) ) ,
sort : task . sort || 0 ,
enabled : task . enabled !== false ,
@ -693,24 +728,29 @@ function removeStep(index) {
taskForm . steps . splice ( index , 1 )
}
async function handleMaterialChange ( file) {
async function handleMaterialChange ( slotKey, file) {
const rawFile = file ? . raw
if ( ! rawFile ) return
const data = new FormData ( )
data . append ( "file" , rawFile )
materialUploading. value = true
uploadingField. value = slotKey
try {
const res = await uploadTrainingTaskMaterial ( data )
taskForm .materialName = res ? . originalFilename || rawFile . name || file ? . name || taskForm .materialName
taskForm .materialUrl = normalizeMaterialUrl ( res ? . url || res ? . fileName || "" )
proxy ? . $modal ? . msgSuccess ( "案例资料上传成功" )
taskForm [` ${ slotKey } Name ` ] = res ? . originalFilename || rawFile . name || file ? . name || taskForm [` ${ slotKey } Name ` ]
taskForm [` ${ slotKey } Url ` ] = normalizeMaterialUrl ( res ? . url || res ? . fileName || "" )
proxy ? . $modal ? . msgSuccess ( ` ${ materialLabel ( slotKey ) } 上传成功 ` )
} catch ( error ) {
proxy ? . $modal ? . msgError ? . ( "案例资料上传失败" )
proxy ? . $modal ? . msgError ? . ( ` ${ materialLabel ( slotKey ) } 上传失败 ` )
} finally {
materialUploading. value = false
uploadingField. value = ""
}
}
function clearMaterial ( slotKey ) {
taskForm [ ` ${ slotKey } Name ` ] = ""
taskForm [ ` ${ slotKey } Url ` ] = ""
}
async function submitTask ( ) {
if ( ! selectedTeachingClass . value || ! taskForm . taskKey ) {
proxy ? . $modal ? . msgError ? . ( "请选择教学班后再保存" )
@ -812,6 +852,12 @@ function buildPayload() {
steps : JSON . stringify ( taskForm . steps . map ( ( item ) => ( { ... item , name : item . name . trim ( ) } ) ) . filter ( ( item ) => item . name ) ) ,
materialName : taskForm . materialName ,
materialUrl : taskForm . materialUrl ,
trainingDataName : taskForm . trainingDataName ,
trainingDataUrl : taskForm . trainingDataUrl ,
guideManualName : taskForm . guideManualName ,
guideManualUrl : taskForm . guideManualUrl ,
aiManualName : taskForm . aiManualName ,
aiManualUrl : taskForm . aiManualUrl ,
sort : taskForm . sort ,
enabled : taskForm . enabled ,
}
@ -1467,16 +1513,34 @@ function buildPayload() {
display : flex ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
min - width : 0 ;
> svg {
flex - shrink : 0 ;
color : var ( -- edu - primary ) ;
}
. material - text {
display : flex ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
min - width : 0 ;
}
. material - label {
flex - shrink : 0 ;
color : var ( -- text - strong ) ;
font - size : var ( -- fs - body ) ;
font - weight : 600 ;
}
. material - link {
overflow : hidden ;
color : var ( -- edu - primary ) ;
font - weight : 600 ;
font - size : var ( -- fs - body ) ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
. material - placeholder {
@ -1486,6 +1550,19 @@ function buildPayload() {
}
}
. material - ops {
display : flex ;
align - items : center ;
flex - shrink : 0 ;
gap : var ( -- sp - 2 ) ;
. clear - material - btn {
height : 32 px ;
font - size : var ( -- fs - body ) ;
font - weight : 600 ;
}
}
. step - list {
display : flex ;
flex - direction : column ;