diff --git a/src/views/components/TrainingMaterialButton.vue b/src/views/components/TrainingMaterialButton.vue index 80833f6..5c9d780 100644 --- a/src/views/components/TrainingMaterialButton.vue +++ b/src/views/components/TrainingMaterialButton.vue @@ -5,25 +5,26 @@ 预览案例资料 - + + 案例资料 - + - 实训数据 + 案例视频 - + 实训指导手册 - + - AI辅助指导手册 + AI排错指导手册 @@ -45,13 +46,13 @@ const props = defineProps({ materialUrl: { type: String, default: "" }, caseMaterialName: { type: String, default: "" }, caseMaterialUrl: { type: String, default: "" }, - // 实训数据 + // 案例视频(内部字段沿用 trainingData 命名) trainingDataName: { type: String, default: "" }, trainingDataUrl: { type: String, default: "" }, // 实训指导手册 guideManualName: { type: String, default: "" }, guideManualUrl: { type: String, default: "" }, - // AI 辅助指导手册 + // AI 排错指导手册(内部字段沿用 aiManual 命名) aiManualName: { type: String, default: "" }, aiManualUrl: { type: String, default: "" } }) diff --git a/src/views/schoolAdmin/trainingTask/index.vue b/src/views/schoolAdmin/trainingTask/index.vue index 68d0628..dae2a16 100644 --- a/src/views/schoolAdmin/trainingTask/index.vue +++ b/src/views/schoolAdmin/trainingTask/index.vue @@ -352,36 +352,46 @@ 03 -
+
- {{ taskForm.materialName }} - 暂未上传案例资料 + {{ slot.label }} + {{ taskForm[`${slot.key}Name`] }} + 暂未上传
- - - - - - - - {{ materialUploading ? '上传中...' : '上传案例资料' }} - - +
+ + + + + + + + {{ uploadingField === slot.key ? '上传中...' : '上传' }} + + + 清除 +
@@ -453,14 +463,20 @@ const activeProject = ref(""); const keyword = ref(""); const loading = ref(false); const saving = ref(false); -const materialUploading = ref(false); +const uploadingField = ref(""); +// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 +const materialSlots = [ + { key: "material", label: "案例资料" }, + { key: "trainingData", label: "案例视频" }, + { key: "guideManual", label: "实训指导手册" }, + { key: "aiManual", label: "AI排错指导手册" }, +]; const tasks = ref([]); const dialogVisible = ref(false); const dialogMode = ref("view"); const currentTaskId = ref(""); const taskForm = reactive(createEmptyTask()); const isReadonly = computed(() => dialogMode.value === "view"); -const hasTaskMaterial = computed(() => isRealMaterial(taskForm.materialName, taskForm.materialUrl)); const enabledCount = computed(() => tasks.value.filter(t => t.enabled).length); @@ -542,14 +558,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, @@ -560,6 +593,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, @@ -664,24 +703,29 @@ async function handleImportChange(file) { queryTasks(); } -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 = res?.url || res?.fileName || ""; - proxy?.$modal?.msgSuccess("案例资料上传成功"); + taskForm[`${slotKey}Name`] = res?.originalFilename || rawFile.name || file?.name || taskForm[`${slotKey}Name`]; + taskForm[`${slotKey}Url`] = 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 downloadTemplate() { const blob = await downloadTrainingTaskTemplate(); const url = URL.createObjectURL(new Blob([blob])); @@ -721,6 +765,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, }; @@ -1628,15 +1678,32 @@ function buildPayload() { display: flex; align-items: center; gap: 12px; + min-width: 0; > svg { flex-shrink: 0; color: #00aaff; } + .material-text { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + + .material-label { + flex-shrink: 0; + color: #e2f3ff; + font-weight: 600; + } + .material-link { + overflow: hidden; color: #00aaff; font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; } .material-placeholder { @@ -1645,6 +1712,18 @@ function buildPayload() { } } +.material-ops { + display: flex; + align-items: center; + flex-shrink: 0; + gap: 8px; + + .clear-material-btn { + height: 32px; + font-weight: 600; + } +} + .upload-btn { display: flex; align-items: center; diff --git a/src/views/teacherEnd/trainingTask/index.vue b/src/views/teacherEnd/trainingTask/index.vue index f268177..397f9ce 100644 --- a/src/views/teacherEnd/trainingTask/index.vue +++ b/src/views/teacherEnd/trainingTask/index.vue @@ -308,10 +308,10 @@ 03
-
+
@@ -319,23 +319,29 @@
- {{ taskForm.materialName - }} - 暂未上传案例资料 + {{ slot.label }} + {{ + taskForm[`${slot.key}Name`] }} + 暂未上传
- - - - - - - - {{ materialUploading ? "上传中..." : "上传案例资料" }} - - +
+ + + + + + + + {{ uploadingField === slot.key ? "上传中..." : "上传" }} + + + 清除 +
@@ -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("") +// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 +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: 32px; + font-size: var(--fs-body); + font-weight: 600; + } +} + .step-list { display: flex; flex-direction: column;