diff --git a/reading-platform-frontend/src/api/collections.ts b/reading-platform-frontend/src/api/collections.ts index 95a9be8..17d62f3 100644 --- a/reading-platform-frontend/src/api/collections.ts +++ b/reading-platform-frontend/src/api/collections.ts @@ -230,6 +230,17 @@ export function formatPrice(price: number | null | undefined): string { return `¥${(price / 100).toFixed(2)}`; } +// 优惠类型映射(与套餐列表、租户选择保持一致) +export const DISCOUNT_TYPE_MAP: Record = { + PERCENTAGE: '折扣', + FIXED: '立减', +}; + +export function getDiscountTypeText(type?: string): string { + if (!type) return '-'; + return DISCOUNT_TYPE_MAP[type] || type; +} + // 格式化日期 export function formatDate(date: string): string { return new Date(date).toLocaleString('zh-CN'); diff --git a/reading-platform-frontend/src/api/course-center.ts b/reading-platform-frontend/src/api/course-center.ts index a31f656..83da8c0 100644 --- a/reading-platform-frontend/src/api/course-center.ts +++ b/reading-platform-frontend/src/api/course-center.ts @@ -52,10 +52,18 @@ export interface LessonTypeOption { count: number; } +/** 筛选元数据 - 课程包主题选项 */ +export interface ThemeOption { + themeId: number; + name: string; + count: number; +} + /** 筛选元数据响应 */ export interface FilterMetaResponse { grades: GradeOption[]; lessonTypes: LessonTypeOption[]; + themes?: ThemeOption[]; } // ============= API 接口 ============= @@ -75,6 +83,7 @@ export function getPackages( params?: { grade?: string; lessonType?: string; + themeId?: number; keyword?: string; } ): Promise { diff --git a/reading-platform-frontend/src/api/course.ts b/reading-platform-frontend/src/api/course.ts index bd9cb6d..a957f50 100644 --- a/reading-platform-frontend/src/api/course.ts +++ b/reading-platform-frontend/src/api/course.ts @@ -48,6 +48,7 @@ export interface Course { // 新增字段 themeId?: number; theme?: { id: number; name: string }; + themeName?: string; coreContent?: string; coverImagePath?: string; domainTags?: string[]; 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/generated/model/courseResponse.ts b/reading-platform-frontend/src/api/generated/model/courseResponse.ts index fe341b3..62ab810 100644 --- a/reading-platform-frontend/src/api/generated/model/courseResponse.ts +++ b/reading-platform-frontend/src/api/generated/model/courseResponse.ts @@ -62,6 +62,8 @@ export interface CourseResponse { environmentConstruction?: string; /** 主题 ID */ themeId?: number; + /** 主题名称 */ + themeName?: string; /** 绘本名称 */ pictureBookName?: string; /** 封面图片路径 */ 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/collections/CollectionDetailView.vue b/reading-platform-frontend/src/views/admin/collections/CollectionDetailView.vue index 1515906..82bc92d 100644 --- a/reading-platform-frontend/src/views/admin/collections/CollectionDetailView.vue +++ b/reading-platform-frontend/src/views/admin/collections/CollectionDetailView.vue @@ -208,14 +208,7 @@ const getStatusText = (status: string) => { return collectionsApi.getCollectionStatusInfo(status).label; }; -const getDiscountTypeText = (type?: string) => { - if (!type) return '-'; - const typeMap: Record = { - PERCENTAGE: '折扣', - FIXED: '立减', - }; - return typeMap[type] || type; -}; +const getDiscountTypeText = collectionsApi.getDiscountTypeText; // 删除套餐 const handleDelete = async () => { diff --git a/reading-platform-frontend/src/views/admin/collections/CollectionEditView.vue b/reading-platform-frontend/src/views/admin/collections/CollectionEditView.vue index 941a864..02cee91 100644 --- a/reading-platform-frontend/src/views/admin/collections/CollectionEditView.vue +++ b/reading-platform-frontend/src/views/admin/collections/CollectionEditView.vue @@ -100,6 +100,7 @@ v-model:open="showPackageSelector" title="选择课程包" width="800px" + :confirm-loading="addingPackages" @ok="handleAddPackages" >