From ac8e07c7849816b22bf20a4c76c2cb86654cd742 Mon Sep 17 00:00:00 2001 From: zhonghua Date: Mon, 23 Mar 2026 11:12:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=BE=E7=A8=8B=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E7=AD=9B=E9=80=89=E6=94=B9=E4=B8=BA=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E9=85=8D=E7=BD=AE=E7=AD=9B=E9=80=89=EF=BC=8C=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E5=8C=85=E5=8D=A1=E7=89=87=E5=B1=95=E7=A4=BA=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .../src/api/course-center.ts | 20 +++- .../school/courses-new/CourseCenterView.vue | 81 +++++-------- .../components/CoursePackageCard.vue | 40 +++++-- .../views/school/courses/CourseListView.vue | 13 +-- .../teacher/courses-new/CourseCenterView.vue | 32 +++--- .../components/CoursePackageCard.vue | 40 +++++-- .../school/SchoolPackageController.java | 6 +- .../response/PackageFilterMetaResponse.java | 20 ++-- .../service/CourseCollectionService.java | 6 +- .../impl/CourseCollectionServiceImpl.java | 106 +++++++++++++----- 10 files changed, 207 insertions(+), 157 deletions(-) diff --git a/reading-platform-frontend/src/api/course-center.ts b/reading-platform-frontend/src/api/course-center.ts index ee8b86a..a31f656 100644 --- a/reading-platform-frontend/src/api/course-center.ts +++ b/reading-platform-frontend/src/api/course-center.ts @@ -14,6 +14,13 @@ export interface CourseCollection { endDate?: string; } +/** 课程包中的课程项(用于提取课程配置) */ +export interface CoursePackageCourseItem { + id?: number; + name?: string; + lessonType?: string; +} + /** 课程包信息 */ export interface CoursePackage { id: number; @@ -22,7 +29,8 @@ export interface CoursePackage { coverImagePath?: string; pictureBookName?: string; gradeTags: string[]; - domainTags?: string[]; + domainTags?: string[]; // 不再展示,由课程配置替代 + courses?: CoursePackageCourseItem[]; // 用于课程配置展示 themeId?: number; themeName?: string; durationMinutes?: number; @@ -37,9 +45,9 @@ export interface GradeOption { count: number; } -/** 筛选元数据 - 主题选项 */ -export interface ThemeOption { - id: number; +/** 筛选元数据 - 课程配置选项 */ +export interface LessonTypeOption { + lessonType: string; name: string; count: number; } @@ -47,7 +55,7 @@ export interface ThemeOption { /** 筛选元数据响应 */ export interface FilterMetaResponse { grades: GradeOption[]; - themes: ThemeOption[]; + lessonTypes: LessonTypeOption[]; } // ============= API 接口 ============= @@ -66,7 +74,7 @@ export function getPackages( collectionId: number, params?: { grade?: string; - themeId?: number; + lessonType?: string; keyword?: string; } ): Promise { diff --git a/reading-platform-frontend/src/views/school/courses-new/CourseCenterView.vue b/reading-platform-frontend/src/views/school/courses-new/CourseCenterView.vue index 4730ce6..6214ffe 100644 --- a/reading-platform-frontend/src/views/school/courses-new/CourseCenterView.vue +++ b/reading-platform-frontend/src/views/school/courses-new/CourseCenterView.vue @@ -7,12 +7,9 @@
-
+ @click="selectCollection(collection)">
{{ collection.name }}
{{ collection.packageCount || 0 }}个课程包
@@ -34,11 +31,7 @@
{{ selectedCollection.description }}
- @@ -52,18 +45,12 @@
年级:
- + 全部 - + @click="selectedGrade = grade.label"> {{ grade.label }} ({{ grade.count }}) @@ -72,36 +59,23 @@
- +
- 主题: - - 全部主题 - - {{ theme.name }} ({{ theme.count }}) + 课程配置: + + 全部课程配置 + + {{ opt.name }} ({{ opt.count }})
- +
@@ -110,13 +84,8 @@
- +
@@ -200,13 +169,13 @@ const loadCollections = async () => { loadingCollections.value = false; } }; - +const selectedLessonType = ref(undefined); // 选择套餐 const selectCollection = async (collection: CourseCollection) => { selectedCollectionId.value = collection.id; // 重置筛选条件 selectedGrade.value = ''; - selectedThemeId.value = undefined; + selectedLessonType.value = undefined; searchKeyword.value = ''; descExpanded.value = false; @@ -236,7 +205,7 @@ const loadFilterMeta = async () => { filterMeta.value = await getFilterMeta(selectedCollectionId.value); } catch (error) { console.error('获取筛选元数据失败', error); - filterMeta.value = { grades: [], themes: [] }; + filterMeta.value = { grades: [], lessonTypes: [] }; } }; @@ -247,7 +216,7 @@ const loadPackages = async () => { try { packages.value = await getPackages(selectedCollectionId.value, { grade: selectedGrade.value || undefined, - themeId: selectedThemeId.value, + lessonType: selectedLessonType.value, keyword: searchKeyword.value || undefined, }); } catch (error: any) { @@ -258,8 +227,8 @@ const loadPackages = async () => { } }; -// 监听年级变化 -watch(selectedGrade, () => { +// 监听年级、课程配置变化 +watch([selectedGrade, selectedLessonType], () => { loadPackages(); }); @@ -429,7 +398,7 @@ onMounted(() => { gap: 16px; } -.filter-row + .filter-row { +.filter-row+.filter-row { margin-top: 12px; } diff --git a/reading-platform-frontend/src/views/school/courses-new/components/CoursePackageCard.vue b/reading-platform-frontend/src/views/school/courses-new/components/CoursePackageCard.vue index 3b89663..28cc633 100644 --- a/reading-platform-frontend/src/views/school/courses-new/components/CoursePackageCard.vue +++ b/reading-platform-frontend/src/views/school/courses-new/components/CoursePackageCard.vue @@ -28,10 +28,15 @@
- -
- - {{ pkg.themeName }} + +
+ + {{ getLessonTypeName(lt) }}
@@ -66,6 +71,7 @@ import { EyeOutlined, } from '@ant-design/icons-vue'; import type { CoursePackage } from '@/api/course-center'; +import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps'; const props = defineProps<{ pkg: CoursePackage; @@ -82,6 +88,17 @@ const gradeText = computed(() => { return grades.join(' · '); }); +// 从 courses 提取课程类型列表(去重,与管理端一致) +const lessonTypes = computed(() => { + const courses = props.pkg.courses || []; + const types = new Set(); + for (const c of courses) { + const t = c.lessonType; + if (t) types.add(t); + } + return Array.from(types); +}); + // 获取图片完整 URL const getImageUrl = (path: string) => { if (!path) return ''; @@ -199,14 +216,17 @@ const handleView = () => { border: 1px solid #91d5ff; } -.theme-tag { +.config-row { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.config-tag { display: inline-block; - padding: 2px 10px; - background: #f6ffed; - color: #52c41a; - font-size: 12px; + padding: 2px 8px; + font-size: 11px; border-radius: 4px; - border: 1px solid #b7eb8f; } /* 统计信息 */ diff --git a/reading-platform-frontend/src/views/school/courses/CourseListView.vue b/reading-platform-frontend/src/views/school/courses/CourseListView.vue index b534242..8ea2c29 100644 --- a/reading-platform-frontend/src/views/school/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/school/courses/CourseListView.vue @@ -137,16 +137,9 @@
- +
diff --git a/reading-platform-frontend/src/views/teacher/courses-new/CourseCenterView.vue b/reading-platform-frontend/src/views/teacher/courses-new/CourseCenterView.vue index 20f95fe..b5dadf9 100644 --- a/reading-platform-frontend/src/views/teacher/courses-new/CourseCenterView.vue +++ b/reading-platform-frontend/src/views/teacher/courses-new/CourseCenterView.vue @@ -72,23 +72,23 @@
- +
- 主题: + 课程配置: - 全部主题 + 全部课程配置 - {{ theme.name }} ({{ theme.count }}) + {{ opt.name }} ({{ opt.count }})
@@ -166,7 +166,7 @@ const selectedCollection = computed(() => ); // 筛选元数据 -const filterMeta = ref({ grades: [], themes: [] }); +const filterMeta = ref({ grades: [], lessonTypes: [] }); // 课程包列表 const packages = ref([]); @@ -174,7 +174,7 @@ const loadingPackages = ref(false); // 筛选条件 const selectedGrade = ref(''); -const selectedThemeId = ref(undefined); +const selectedLessonType = ref(undefined); const searchKeyword = ref(''); // 描述展开 @@ -204,7 +204,7 @@ const selectCollection = async (collection: CourseCollection) => { selectedCollectionId.value = collection.id; // 重置筛选条件 selectedGrade.value = ''; - selectedThemeId.value = undefined; + selectedLessonType.value = undefined; searchKeyword.value = ''; descExpanded.value = false; @@ -234,7 +234,7 @@ const loadFilterMeta = async () => { filterMeta.value = await getFilterMeta(selectedCollectionId.value); } catch (error) { console.error('获取筛选元数据失败', error); - filterMeta.value = { grades: [], themes: [] }; + filterMeta.value = { grades: [], lessonTypes: [] }; } }; @@ -245,7 +245,7 @@ const loadPackages = async () => { try { packages.value = await getPackages(selectedCollectionId.value, { grade: selectedGrade.value || undefined, - themeId: selectedThemeId.value, + lessonType: selectedLessonType.value, keyword: searchKeyword.value || undefined, }); } catch (error: any) { @@ -266,8 +266,8 @@ const handlePrepare = (pkg: CoursePackage) => { router.push(`/teacher/courses/${pkg.id}/prepare`); }; -// 监听年级变化 -watch(selectedGrade, () => { +// 监听年级、课程配置变化 +watch([selectedGrade, selectedLessonType], () => { loadPackages(); }); diff --git a/reading-platform-frontend/src/views/teacher/courses-new/components/CoursePackageCard.vue b/reading-platform-frontend/src/views/teacher/courses-new/components/CoursePackageCard.vue index 2527a4a..3fbb3f2 100644 --- a/reading-platform-frontend/src/views/teacher/courses-new/components/CoursePackageCard.vue +++ b/reading-platform-frontend/src/views/teacher/courses-new/components/CoursePackageCard.vue @@ -28,10 +28,15 @@
- -
- - {{ pkg.themeName }} + +
+ + {{ getLessonTypeName(lt) }}
@@ -66,6 +71,7 @@ import { EditOutlined, } from '@ant-design/icons-vue'; import type { CoursePackage } from '@/api/course-center'; +import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps'; const props = defineProps<{ pkg: CoursePackage; @@ -82,6 +88,17 @@ const gradeText = computed(() => { return grades.join(' · '); }); +// 从 courses 提取课程类型列表(去重,与管理端一致) +const lessonTypes = computed(() => { + const courses = props.pkg.courses || []; + const types = new Set(); + for (const c of courses) { + const t = c.lessonType; + if (t) types.add(t); + } + return Array.from(types); +}); + // 获取图片完整 URL const getImageUrl = (path: string) => { if (!path) return ''; @@ -203,18 +220,17 @@ const handlePrepare = () => { border: 1px solid #FFD591; } -.theme-row { - /* 主题标签样式 */ +.config-row { + display: flex; + flex-wrap: wrap; + gap: 4px; } -.theme-tag { +.config-tag { display: inline-block; - padding: 2px 10px; - background: #E6F7FF; - color: #096DD9; - font-size: 12px; + padding: 2px 8px; + font-size: 11px; border-radius: 4px; - border: 1px solid #91D5FF; } /* 统计信息 */ diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolPackageController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolPackageController.java index 550c48a..d017ddb 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolPackageController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolPackageController.java @@ -50,13 +50,13 @@ public class SchoolPackageController { public Result> getPackagesByCollection( @PathVariable Long collectionId, @RequestParam(required = false) String grade, - @RequestParam(required = false) Long themeId, + @RequestParam(required = false) String lessonType, @RequestParam(required = false) String keyword) { - return Result.success(collectionService.getPackagesByCollection(collectionId, grade, themeId, keyword)); + return Result.success(collectionService.getPackagesByCollection(collectionId, grade, lessonType, keyword)); } @GetMapping("/{collectionId}/filter-meta") - @Operation(summary = "获取套餐筛选元数据(年级、主题选项)") + @Operation(summary = "获取套餐筛选元数据(年级、课程配置选项)") @RequireRole({UserRole.SCHOOL, UserRole.TEACHER}) public Result getFilterMeta(@PathVariable Long collectionId) { return Result.success(collectionService.getPackageFilterMeta(collectionId)); diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/PackageFilterMetaResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/PackageFilterMetaResponse.java index 4cb040b..3ed02e1 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/PackageFilterMetaResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/PackageFilterMetaResponse.java @@ -10,7 +10,7 @@ import java.util.List; /** * 套餐筛选元数据响应 - * 用于返回套餐下课程包的筛选选项(年级、主题) + * 用于返回套餐下课程包的筛选选项(年级、课程配置) */ @Data @Builder @@ -22,8 +22,8 @@ public class PackageFilterMetaResponse { @Schema(description = "年级选项列表") private List grades; - @Schema(description = "主题选项列表") - private List themes; + @Schema(description = "课程配置选项列表(导入课、集体课、健康、科学等)") + private List lessonTypes; /** * 年级选项 @@ -42,21 +42,21 @@ public class PackageFilterMetaResponse { } /** - * 主题选项 + * 课程配置选项(课程环节类型) */ @Data @Builder @NoArgsConstructor @AllArgsConstructor - @Schema(description = "主题选项") - public static class ThemeOption { - @Schema(description = "主题ID") - private Long id; + @Schema(description = "课程配置选项") + public static class LessonTypeOption { + @Schema(description = "课程类型编码,如 INTRODUCTION、COLLECTIVE、HEALTH") + private String lessonType; - @Schema(description = "主题名称") + @Schema(description = "课程类型中文名称,如 导入课、集体课、健康") private String name; - @Schema(description = "该主题下的课程包数量") + @Schema(description = "包含该类型环节的课程包数量") private Integer count; } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CourseCollectionService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CourseCollectionService.java index e728027..01a0d87 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CourseCollectionService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CourseCollectionService.java @@ -39,14 +39,14 @@ public interface CourseCollectionService extends IService { * 获取课程套餐下的课程包列表(支持筛选) * @param collectionId 套餐ID * @param grade 年级筛选 - * @param themeId 主题ID筛选 + * @param lessonType 课程配置筛选(INTRODUCTION、COLLECTIVE、HEALTH、LANGUAGE、SCIENCE、SOCIAL、ART) * @param keyword 关键词搜索 * @return 课程包列表 */ - List getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword); + List getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword); /** - * 获取套餐的筛选元数据(年级、主题选项) + * 获取套餐的筛选元数据(年级、课程配置选项) * @param collectionId 套餐ID * @return 筛选元数据 */ diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseCollectionServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseCollectionServiceImpl.java index 973ca2f..026fdd6 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseCollectionServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CourseCollectionServiceImpl.java @@ -11,7 +11,12 @@ import com.reading.platform.common.response.PageResult; import com.reading.platform.dto.response.CourseCollectionResponse; import com.reading.platform.dto.response.CoursePackageResponse; import com.reading.platform.dto.response.PackageFilterMetaResponse; -import com.reading.platform.entity.*; +import com.reading.platform.entity.CourseCollection; +import com.reading.platform.entity.CourseCollectionPackage; +import com.reading.platform.entity.CourseLesson; +import com.reading.platform.entity.CoursePackage; +import com.reading.platform.entity.TenantPackage; +import com.reading.platform.entity.Theme; import com.reading.platform.mapper.*; import com.reading.platform.service.CourseCollectionService; import com.reading.platform.service.CourseLessonService; @@ -208,8 +213,8 @@ public class CourseCollectionServiceImpl extends ServiceImpl getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword) { - log.info("获取课程套餐的课程包列表(筛选),collectionId={}, grade={}, themeId={}, keyword={}", collectionId, grade, themeId, keyword); + public List getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword) { + log.info("获取课程套餐的课程包列表(筛选),collectionId={}, grade={}, lessonType={}, keyword={}", collectionId, grade, lessonType, keyword); // 查询关联关系 List associations = collectionPackageMapper.selectList( @@ -227,6 +232,16 @@ public class CourseCollectionServiceImpl extends ServiceImpl idsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType); + Set idSet = new HashSet<>(idsWithLesson); + packageIds = packageIds.stream().filter(idSet::contains).collect(Collectors.toList()); + if (packageIds.isEmpty()) { + return new ArrayList<>(); + } + } + // 构建查询条件 LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .in(CoursePackage::getId, packageIds) @@ -237,11 +252,6 @@ public class CourseCollectionServiceImpl extends ServiceImpl w @@ -304,7 +314,7 @@ public class CourseCollectionServiceImpl extends ServiceImpl()) - .themes(new ArrayList<>()) + .lessonTypes(new ArrayList<>()) .build(); } @@ -337,36 +347,70 @@ public class CourseCollectionServiceImpl extends ServiceImpl themeCountMap = new HashMap<>(); - Set themeIds = new HashSet<>(); - for (CoursePackage pkg : packages) { - if (pkg.getThemeId() != null) { - themeCountMap.merge(pkg.getThemeId(), 1, Integer::sum); - themeIds.add(pkg.getThemeId()); + // 统计课程配置(课程环节类型)分布:查询所有课程环节,按规范化类型统计课程包数量 + List allLessons = courseLessonService.list( + new LambdaQueryWrapper() + .in(CourseLesson::getCourseId, packageIds) + .select(CourseLesson::getCourseId, CourseLesson::getLessonType) + ); + // 规范化类型 -> 包含该类型的课程包ID集合 + Map> typeToPackageIds = new HashMap<>(); + for (CourseLesson lesson : allLessons) { + String canonical = normalizeLessonTypeForFilter(lesson.getLessonType()); + if (canonical != null) { + typeToPackageIds.computeIfAbsent(canonical, k -> new HashSet<>()).add(lesson.getCourseId()); } } - - // 批量查询主题名称 - List themes = new ArrayList<>(); - if (!themeIds.isEmpty()) { - List themeList = themeMapper.selectBatchIds(themeIds); - themes = themeList.stream() - .filter(t -> themeCountMap.containsKey(t.getId())) - .map(t -> PackageFilterMetaResponse.ThemeOption.builder() - .id(t.getId()) - .name(t.getName()) - .count(themeCountMap.get(t.getId())) - .build()) - .collect(Collectors.toList()); - } + // 按固定顺序生成课程配置选项(与管理端一致) + List lessonTypeOrder = List.of("INTRODUCTION", "COLLECTIVE", "HEALTH", "LANGUAGE", "SCIENCE", "SOCIAL", "ART"); + Map typeToName = getLessonTypeDisplayNames(); + List lessonTypes = lessonTypeOrder.stream() + .filter(typeToPackageIds::containsKey) + .map(type -> PackageFilterMetaResponse.LessonTypeOption.builder() + .lessonType(type) + .name(typeToName.getOrDefault(type, type)) + .count(typeToPackageIds.get(type).size()) + .build()) + .collect(Collectors.toList()); return PackageFilterMetaResponse.builder() .grades(grades) - .themes(themes) + .lessonTypes(lessonTypes) .build(); } + /** + * 将课程环节类型规范化为筛选用的标准编码(与管理端、前端 LESSON_TYPE_NAMES 一致) + */ + private String normalizeLessonTypeForFilter(String lessonType) { + if (!StringUtils.hasText(lessonType)) return null; + return switch (lessonType.toUpperCase()) { + case "INTRODUCTION", "INTRO" -> "INTRODUCTION"; + case "COLLECTIVE" -> "COLLECTIVE"; + case "LANGUAGE", "DOMAIN_LANGUAGE" -> "LANGUAGE"; + case "HEALTH", "DOMAIN_HEALTH" -> "HEALTH"; + case "SCIENCE", "DOMAIN_SCIENCE" -> "SCIENCE"; + case "SOCIAL", "SOCIETY", "DOMAIN_SOCIAL" -> "SOCIAL"; + case "ART", "DOMAIN_ART" -> "ART"; + default -> null; + }; + } + + /** + * 课程类型编码 -> 中文名称(与管理端、前端 tagMaps 一致) + */ + private Map getLessonTypeDisplayNames() { + Map m = new HashMap<>(); + m.put("INTRODUCTION", "导入课"); + m.put("COLLECTIVE", "集体课"); + m.put("LANGUAGE", "语言"); + m.put("HEALTH", "健康"); + m.put("SCIENCE", "科学"); + m.put("SOCIAL", "社会"); + m.put("ART", "艺术"); + return m; + } + /** * 解析年级标签 */