From 877acf33b8d1e1ceba0e3f8ae126adde093fdd6a Mon Sep 17 00:00:00 2001 From: zhonghua Date: Mon, 23 Mar 2026 15:15:46 +0800 Subject: [PATCH 01/10] =?UTF-8?q?fix(admin):=20=E8=AF=BE=E7=A8=8B=E5=8C=85?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A1=B5=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 封面回显与保存:使用 getFileUrl 统一处理,修复 watch 逻辑 - 课程介绍/排课参考/环创建设回显:修复 API 字段映射和解析 - 测评内容 JSON 格式:新增 parseAssessmentDataForDisplay 前后端统一 - 保存后跳转列表:修复新建/编辑流程的 router.replace - 表单校验:导入课、集体课、领域课各必填一条,下一步时校验 - 保存按钮:修复 @click 将 event 误传为 isDraft 导致不跳转 - Lesson API:updateLesson/updateStep 传入正确的 courseId Made-with: Cursor --- reading-platform-frontend/src/api/file.ts | 15 +- reading-platform-frontend/src/api/lesson.ts | 8 +- .../components/course-edit/Step1BasicInfo.vue | 22 +-- .../course-edit/Step3ScheduleRef.vue | 25 ++-- .../course-edit/Step4IntroLesson.vue | 7 +- .../course-edit/Step5CollectiveLesson.vue | 7 +- .../course-edit/Step6DomainLessons.vue | 10 +- .../src/utils/assessmentData.ts | 19 +++ .../views/admin/courses/CourseEditView.vue | 136 ++++++------------ 9 files changed, 115 insertions(+), 134 deletions(-) create mode 100644 reading-platform-frontend/src/utils/assessmentData.ts diff --git a/reading-platform-frontend/src/api/file.ts b/reading-platform-frontend/src/api/file.ts index b6a0f13..0b96b7e 100644 --- a/reading-platform-frontend/src/api/file.ts +++ b/reading-platform-frontend/src/api/file.ts @@ -211,11 +211,18 @@ export const fileApi = { /** * 获取文件URL + * 支持:完整 OSS URL、以 / 开头的路径、相对路径 */ - getFileUrl: (filePath: string): string => { - // filePath 格式: /uploads/courses/covers/xxx.png - // 直接返回相对路径,由 nginx 或后端静态服务处理 - return filePath; + getFileUrl: (filePath: string | null | undefined): string => { + if (!filePath) return ''; + if (filePath.startsWith('http://') || filePath.startsWith('https://')) { + return filePath; + } + const SERVER_BASE = import.meta.env.VITE_SERVER_BASE_URL || '/api'; + if (filePath.startsWith('/')) { + return `${SERVER_BASE}${filePath}`; + } + return `${SERVER_BASE}/uploads/${filePath}`; }, }; diff --git a/reading-platform-frontend/src/api/lesson.ts b/reading-platform-frontend/src/api/lesson.ts index e75b4b9..748e3f2 100644 --- a/reading-platform-frontend/src/api/lesson.ts +++ b/reading-platform-frontend/src/api/lesson.ts @@ -105,8 +105,8 @@ export function createLesson(courseId: number, data: CreateLessonData) { } // 更新课程 -export function updateLesson(lessonId: number, data: Partial) { - return http.put(`/v1/admin/courses/0/lessons/${lessonId}`, data); +export function updateLesson(courseId: number, lessonId: number, data: Partial) { + return http.put(`/v1/admin/courses/${courseId}/lessons/${lessonId}`, data); } // 删除课程 @@ -132,8 +132,8 @@ export function createStep(courseId: number, lessonId: number, data: CreateStepD } // 更新环节 -export function updateStep(stepId: number, data: Partial) { - return http.put(`/v1/admin/courses/0/lessons/steps/${stepId}`, data); +export function updateStep(courseId: number, stepId: number, data: Partial) { + return http.put(`/v1/admin/courses/${courseId}/lessons/steps/${stepId}`, data); } // 删除环节 diff --git a/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue b/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue index 3294135..e11570e 100644 --- a/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue +++ b/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue @@ -126,7 +126,7 @@ import { ref, reactive, watch, onMounted } from 'vue'; import { message } from 'ant-design-vue'; import { PlusOutlined } from '@ant-design/icons-vue'; import { getThemeList } from '@/api/theme'; -import { uploadFile } from '@/api/file'; +import { uploadFile, getFileUrl } from '@/api/file'; import type { Theme } from '@/api/theme'; interface BasicInfoData { @@ -206,19 +206,16 @@ watch( if (newVal) { Object.assign(formData, newVal); - // 处理封面图片 - if (newVal.coverImagePath && coverImages.value.length === 0) { - // 构建正确的图片URL - let imageUrl = newVal.coverImagePath; - if (!imageUrl.startsWith('http') && !imageUrl.startsWith('/uploads') && !imageUrl.includes('/uploads/')) { - imageUrl = `/uploads/${imageUrl}`; - } + // 处理封面图片回显 + if (newVal.coverImagePath) { coverImages.value = [{ uid: '-1', name: 'cover', status: 'done', - url: imageUrl, + url: getFileUrl(newVal.coverImagePath), }]; + } else { + coverImages.value = []; } } }, @@ -255,16 +252,11 @@ const beforeCoverUpload = async (file: any) => { try { const result = await uploadFile(file, 'cover'); formData.coverImagePath = result.filePath; - // 构建正确的图片URL - 后端返回的filePath已经包含完整路径 - let imageUrl = result.filePath; - if (!imageUrl.startsWith('http') && !imageUrl.startsWith('/uploads') && !imageUrl.includes('/uploads/')) { - imageUrl = `/uploads/${imageUrl}`; - } coverImages.value = [{ uid: file.uid, name: file.name, status: 'done', - url: imageUrl, + url: getFileUrl(result.filePath), }]; handleChange(); message.success('封面上传成功'); diff --git a/reading-platform-frontend/src/components/course-edit/Step3ScheduleRef.vue b/reading-platform-frontend/src/components/course-edit/Step3ScheduleRef.vue index e734601..379aa7f 100644 --- a/reading-platform-frontend/src/components/course-edit/Step3ScheduleRef.vue +++ b/reading-platform-frontend/src/components/course-edit/Step3ScheduleRef.vue @@ -133,17 +133,20 @@ const tableData = ref([]); watch( () => props.modelValue, (newVal) => { - if (newVal) { - try { - const parsed = JSON.parse(newVal); - tableData.value = parsed.map((row: any, index: number) => ({ - ...row, - key: row.key || `row_${index}`, - })); - } catch (e) { - console.error('解析排课数据失败', e); - tableData.value = []; - } + if (!newVal || typeof newVal !== 'string') { + tableData.value = []; + return; + } + try { + const parsed = JSON.parse(newVal); + const rows = Array.isArray(parsed) ? parsed : []; + tableData.value = rows.map((row: any, index: number) => ({ + ...row, + key: row.key || `row_${index}`, + })); + } catch (e) { + console.error('解析排课数据失败', e); + tableData.value = []; } }, { immediate: true } diff --git a/reading-platform-frontend/src/components/course-edit/Step4IntroLesson.vue b/reading-platform-frontend/src/components/course-edit/Step4IntroLesson.vue index 70d1626..09dd546 100644 --- a/reading-platform-frontend/src/components/course-edit/Step4IntroLesson.vue +++ b/reading-platform-frontend/src/components/course-edit/Step4IntroLesson.vue @@ -60,6 +60,7 @@ import { PlusOutlined } from '@ant-design/icons-vue'; import LessonConfigPanel from '@/components/course/LessonConfigPanel.vue'; import type { LessonData } from '@/components/course/LessonConfigPanel.vue'; import { getLessonByType, createLesson, updateLesson, deleteLesson as deleteLessonApi } from '@/api/lesson'; +import { parseAssessmentDataForDisplay } from '@/utils/assessmentData'; interface Props { courseId: number; @@ -100,7 +101,7 @@ const fetchLesson = async () => { preparation: lesson.preparation || '', extension: lesson.extension || '', reflection: lesson.reflection || '', - assessmentData: lesson.assessmentData || '', + assessmentData: parseAssessmentDataForDisplay(lesson.assessmentData), useTemplate: lesson.useTemplate || false, steps: lesson.steps || [], isNew: false, @@ -159,10 +160,10 @@ const handleLessonChange = () => { emit('change'); }; -// 验证:若配置了导入课,则通过 formRules 校验 +// 验证:导入课为必填,至少配置一条 const validate = async () => { if (!lessonData.value) { - return { valid: true, errors: [] as string[], warnings: ['未配置导入课'] }; + return { valid: false, errors: ['请配置导入课(至少一条)'] }; } return configPanelRef.value?.validate() ?? { valid: true, errors: [] as string[] }; }; diff --git a/reading-platform-frontend/src/components/course-edit/Step5CollectiveLesson.vue b/reading-platform-frontend/src/components/course-edit/Step5CollectiveLesson.vue index 0505752..484a7fd 100644 --- a/reading-platform-frontend/src/components/course-edit/Step5CollectiveLesson.vue +++ b/reading-platform-frontend/src/components/course-edit/Step5CollectiveLesson.vue @@ -60,6 +60,7 @@ import { PlusOutlined } from '@ant-design/icons-vue'; import LessonConfigPanel from '@/components/course/LessonConfigPanel.vue'; import type { LessonData } from '@/components/course/LessonConfigPanel.vue'; import { getLessonByType, createLesson as createLessonApi, updateLesson, deleteLesson as deleteLessonApi } from '@/api/lesson'; +import { parseAssessmentDataForDisplay } from '@/utils/assessmentData'; interface Props { courseId: number; @@ -101,7 +102,7 @@ const fetchLesson = async () => { preparation: lesson.preparation || '', extension: lesson.extension || '', reflection: lesson.reflection || '', - assessmentData: lesson.assessmentData || '', + assessmentData: parseAssessmentDataForDisplay(lesson.assessmentData), useTemplate: lesson.useTemplate || false, steps: lesson.steps || [], isNew: false, @@ -160,10 +161,10 @@ const handleLessonChange = () => { emit('change'); }; -// 验证:若配置了集体课,则通过 formRules 校验 +// 验证:集体课为必填,至少配置一条 const validate = async () => { if (!lessonData.value) { - return { valid: true, errors: [] as string[], warnings: ['未配置集体课'] }; + return { valid: false, errors: ['请配置集体课(至少一条)'] }; } return configPanelRef.value?.validate() ?? { valid: true, errors: [] as string[] }; }; diff --git a/reading-platform-frontend/src/components/course-edit/Step6DomainLessons.vue b/reading-platform-frontend/src/components/course-edit/Step6DomainLessons.vue index b310834..22ede33 100644 --- a/reading-platform-frontend/src/components/course-edit/Step6DomainLessons.vue +++ b/reading-platform-frontend/src/components/course-edit/Step6DomainLessons.vue @@ -93,6 +93,7 @@ import { import LessonConfigPanel from '@/components/course/LessonConfigPanel.vue'; import type { LessonData } from '@/components/course/LessonConfigPanel.vue'; import { getLessonList, createLesson, updateLesson, deleteLesson } from '@/api/lesson'; +import { parseAssessmentDataForDisplay } from '@/utils/assessmentData'; interface DomainConfig { type: string; @@ -204,7 +205,7 @@ const fetchLessons = async () => { preparation: lesson.preparation || '', extension: lesson.extension || '', reflection: lesson.reflection || '', - assessmentData: lesson.assessmentData || '', + assessmentData: parseAssessmentDataForDisplay(lesson.assessmentData), useTemplate: lesson.useTemplate || false, steps: lesson.steps || [], isNew: false, @@ -266,8 +267,13 @@ const handleLessonChange = () => { emit('change'); }; -// 验证:若启用某领域,则通过 formRules 校验各领域 +// 验证:领域课为必填,至少配置一条,且已启用的领域需通过 formRules 校验 const validate = async () => { + const saveData = getSaveData(); + if (!saveData || saveData.length === 0) { + return { valid: false, errors: ['请配置领域课(至少一条)'] }; + } + const enabledDomains = domains.filter((d) => d.enabled); const allErrors: string[] = []; diff --git a/reading-platform-frontend/src/utils/assessmentData.ts b/reading-platform-frontend/src/utils/assessmentData.ts new file mode 100644 index 0000000..23ae746 --- /dev/null +++ b/reading-platform-frontend/src/utils/assessmentData.ts @@ -0,0 +1,19 @@ +/** + * 测评内容(assessmentData)前后端格式统一 + * 后端将纯文本存储为 JSON 字符串(如 "核心内容"),加载时需解析为明文展示 + */ +export function parseAssessmentDataForDisplay(value: string | null | undefined): string { + if (value == null || value === '') return ''; + const trimmed = value.trim(); + if (!trimmed) return ''; + // 若是 JSON 字符串格式(如 "核心内容"),解析后返回明文 + if (trimmed.startsWith('"') && trimmed.endsWith('"')) { + try { + const parsed = JSON.parse(trimmed); + return typeof parsed === 'string' ? parsed : trimmed; + } catch { + return trimmed; + } + } + return trimmed; +} diff --git a/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue b/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue index 0905ce1..994412a 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue @@ -1,21 +1,13 @@