From 20c500e921c750a0130e115eaba71e27c306c984 Mon Sep 17 00:00:00 2001 From: zhonghua Date: Thu, 19 Mar 2026 15:01:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=99=E5=B8=88=E8=AF=BE=E7=A8=8B?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=20-=20=E5=B9=B4=E7=BA=A7/=E9=A2=86=E5=9F=9F/?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E7=B1=BB=E5=9E=8B=E7=AD=9B=E9=80=89=E4=B8=8E?= =?UTF-8?q?=20lessonTags=20=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 lessonType 筛选参数,支持 SOCIAL/SOCIETY/DOMAIN_* 等格式兼容 - 列表接口返回 lessonTags(name,lessonType) 供 tag 展示 - 新增 LessonTagResponse DTO - 完善 tagMaps 与 LESSON_TYPE_NAMES 映射(INTRO/DOMAIN_*) - 修复筛选参数未传递到接口的问题 Made-with: Cursor --- reading-platform-frontend/src/api/teacher.ts | 13 +- .../src/utils/tagMaps.ts | 420 ++++++++++-------- .../views/teacher/courses/CourseListView.vue | 207 ++++----- .../school/SchoolCourseController.java | 2 +- .../teacher/TeacherCourseController.java | 27 +- .../platform/dto/response/CourseResponse.java | 3 + .../dto/response/LessonTagResponse.java | 24 + .../platform/service/CourseLessonService.java | 46 ++ .../service/CoursePackageService.java | 21 +- .../impl/CoursePackageServiceImpl.java | 39 +- 10 files changed, 488 insertions(+), 314 deletions(-) create mode 100644 reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java diff --git a/reading-platform-frontend/src/api/teacher.ts b/reading-platform-frontend/src/api/teacher.ts index fbd3221..110332b 100644 --- a/reading-platform-frontend/src/api/teacher.ts +++ b/reading-platform-frontend/src/api/teacher.ts @@ -13,9 +13,17 @@ export interface TeacherCourseQueryParams { pageNum?: number; pageSize?: number; grade?: string; + domain?: string; + lessonType?: string; keyword?: string; } +/** 课程环节标签(列表展示) */ +export interface LessonTag { + name: string; + lessonType: string; +} + export interface TeacherCourse { id: number; name: string; @@ -23,6 +31,7 @@ export interface TeacherCourse { coverImagePath?: string; gradeTags: string[]; domainTags: string[]; + lessonTags?: LessonTag[]; duration: number; avgRating: number; usageCount: number; @@ -62,7 +71,9 @@ export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{ pageNum: params.pageNum, pageSize: params.pageSize, keyword: params.keyword, - category: params.grade, + grade: params.grade, + domain: params.domain, + lessonType: params.lessonType, }, }).then(res => { const list = res.list ?? res.records ?? []; diff --git a/reading-platform-frontend/src/utils/tagMaps.ts b/reading-platform-frontend/src/utils/tagMaps.ts index af5f394..c9b4512 100644 --- a/reading-platform-frontend/src/utils/tagMaps.ts +++ b/reading-platform-frontend/src/utils/tagMaps.ts @@ -6,114 +6,121 @@ // 年级标签映射(英文 → 中文) export const GRADE_TAG_MAP: Record = { // 大写格式 - SMALL: '小班', - MIDDLE: '中班', - BIG: '大班', + SMALL: "小班", + MIDDLE: "中班", + BIG: "大班", // 小写格式 - small: '小班', - middle: '中班', - big: '大班', + small: "小班", + middle: "中班", + big: "大班", }; // 领域标签映射(英文 → 中文)- 导出供其他模块使用 export const DOMAIN_TAG_MAP: Record = { // 旧格式(大写) - LANGUAGE: '语言', - SCIENCE: '科学', - SOCIAL: '社会', - ART: '艺术', - HEALTH: '健康', - MATH: '数学', + LANGUAGE: "语言", + SCIENCE: "科学", + SOCIAL: "社会", + ART: "艺术", + HEALTH: "健康", + MATH: "数学", + + // 课程环节/领域 DOMAIN_ 前缀格式(后端返回) + DOMAIN_HEALTH: "健康", + DOMAIN_LANGUAGE: "语言", + DOMAIN_SOCIAL: "社会", + DOMAIN_SCIENCE: "科学", + DOMAIN_ART: "艺术", // 活动类型作为领域的兼容映射(历史数据兼容) - family: '亲子活动', - FAMILY: '亲子活动', - class: '课堂活动', - CLASS: '课堂活动', - outdoor: '户外活动', - OUTDOOR: '户外活动', - handicraft: '手工活动', - HANDICRAFT: '手工活动', - game: '游戏活动', - GAME: '游戏活动', - music: '音乐活动', - MUSIC: '音乐活动', - exploration: '探索活动', - EXPLORATION: '探索活动', - sports: '运动活动', - SPORTS: '运动活动', - art: '艺术活动', + family: "亲子活动", + FAMILY: "亲子活动", + class: "课堂活动", + CLASS: "课堂活动", + outdoor: "户外活动", + OUTDOOR: "户外活动", + handicraft: "手工活动", + HANDICRAFT: "手工活动", + game: "游戏活动", + GAME: "游戏活动", + music: "音乐活动", + MUSIC: "音乐活动", + exploration: "探索活动", + EXPLORATION: "探索活动", + sports: "运动活动", + SPORTS: "运动活动", + art: "艺术活动", // 新格式(细分领域) // 健康领域 - health_motor: '身体动作发展', - health_hygiene: '生活习惯与能力', - HEALTH_MOTOR: '身体动作发展', - HEALTH_HYGIENE: '生活习惯与能力', + health_motor: "身体动作发展", + health_hygiene: "生活习惯与能力", + HEALTH_MOTOR: "身体动作发展", + HEALTH_HYGIENE: "生活习惯与能力", // 语言领域 - lang_listen: '倾听与表达', - lang_read: '早期阅读', - LANG_LISTEN: '倾听与表达', - LANG_READ: '早期阅读', - language_communication: '语言', - LANGUAGE_COMMUNICATION: '语言', + lang_listen: "倾听与表达", + lang_read: "早期阅读", + LANG_LISTEN: "倾听与表达", + LANG_READ: "早期阅读", + language_communication: "语言", + LANGUAGE_COMMUNICATION: "语言", // 社会领域 - social_interact: '人际交往', - social_adapt: '社会适应', - SOCIAL_INTERACT: '人际交往', - SOCIAL_ADAPT: '社会适应', - social_emotional: '社会', - SOCIAL_EMOTIONAL: '社会', + social_interact: "人际交往", + social_adapt: "社会适应", + SOCIAL_INTERACT: "人际交往", + SOCIAL_ADAPT: "社会适应", + social_emotional: "社会", + SOCIAL_EMOTIONAL: "社会", // 科学领域 - science_explore: '科学探究', - math_cog: '数学认知', - SCIENCE_EXPLORE: '科学探究', - MATH_COG: '数学认知', - science_exploration: '科学', - SCIENCE_EXPLORATION: '科学', + science_explore: "科学探究", + math_cog: "数学认知", + SCIENCE_EXPLORE: "科学探究", + MATH_COG: "数学认知", + science_exploration: "科学", + SCIENCE_EXPLORATION: "科学", // 艺术领域 - art_music: '音乐表现', - art_create: '美术创作', - ART_MUSIC: '音乐表现', - ART_CREATE: '美术创作', - art_creativity: '艺术', - ART_CREATIVITY: '艺术', + art_music: "音乐表现", + art_create: "美术创作", + ART_MUSIC: "音乐表现", + ART_CREATE: "美术创作", + art_creativity: "艺术", + ART_CREATIVITY: "艺术", }; // 年级标签颜色配置 export const GRADE_TAG_COLORS: Record = { - '小班': { bg: '#FFE4E8', text: '#E85A71' }, - '中班': { bg: '#E3F2FD', text: '#1976D2' }, - '大班': { bg: '#FFF8E1', text: '#F9A825' }, + 小班: { bg: "#FFE4E8", text: "#E85A71" }, + 中班: { bg: "#E3F2FD", text: "#1976D2" }, + 大班: { bg: "#FFF8E1", text: "#F9A825" }, }; // 领域标签颜色配置 export const DOMAIN_TAG_COLORS: Record = { - '语言': { bg: '#F3E5F5', text: '#8E24AA' }, - '倾听与表达': { bg: '#F3E5F5', text: '#8E24AA' }, - '早期阅读': { bg: '#EDE7F6', text: '#7B1FA2' }, + 语言: { bg: "#F3E5F5", text: "#8E24AA" }, + 倾听与表达: { bg: "#F3E5F5", text: "#8E24AA" }, + 早期阅读: { bg: "#EDE7F6", text: "#7B1FA2" }, - '科学': { bg: '#E8F5E9', text: '#43A047' }, - '科学探究': { bg: '#E8F5E9', text: '#43A047' }, - '数学认知': { bg: '#F1F8E9', text: '#558B2F' }, + 科学: { bg: "#E8F5E9", text: "#43A047" }, + 科学探究: { bg: "#E8F5E9", text: "#43A047" }, + 数学认知: { bg: "#F1F8E9", text: "#558B2F" }, - '社会': { bg: '#E0F7FA', text: '#0097A7' }, - '人际交往': { bg: '#E0F7FA', text: '#0097A7' }, - '社会适应': { bg: '#E0F2F1', text: '#00695C' }, + 社会: { bg: "#E0F7FA", text: "#0097A7" }, + 人际交往: { bg: "#E0F7FA", text: "#0097A7" }, + 社会适应: { bg: "#E0F2F1", text: "#00695C" }, - '艺术': { bg: '#FFF3E0', text: '#FB8C00' }, - '音乐表现': { bg: '#FFF3E0', text: '#FB8C00' }, - '美术创作': { bg: '#FBE9E7', text: '#E64A19' }, + 艺术: { bg: "#FFF3E0", text: "#FB8C00" }, + 音乐表现: { bg: "#FFF3E0", text: "#FB8C00" }, + 美术创作: { bg: "#FBE9E7", text: "#E64A19" }, - '健康': { bg: '#FFEBEE', text: '#E53935' }, - '身体动作发展': { bg: '#FFEBEE', text: '#E53935' }, - '生活习惯与能力': { bg: '#FFCDD2', text: '#C62828' }, + 健康: { bg: "#FFEBEE", text: "#E53935" }, + 身体动作发展: { bg: "#FFEBEE", text: "#E53935" }, + 生活习惯与能力: { bg: "#FFCDD2", text: "#C62828" }, - '数学': { bg: '#FFF8E1', text: '#F9A825' }, + 数学: { bg: "#FFF8E1", text: "#F9A825" }, }; /** @@ -147,24 +154,32 @@ export function translateDomainTags(tags: string[]): string[] { /** * 获取年级标签样式 */ -export function getGradeTagStyle(tag: string): { background: string; color: string; border: string } { - const colors = GRADE_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' }; +export function getGradeTagStyle(tag: string): { + background: string; + color: string; + border: string; +} { + const colors = GRADE_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } /** * 获取领域标签样式 */ -export function getDomainTagStyle(tag: string): { background: string; color: string; border: string } { - const colors = DOMAIN_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' }; +export function getDomainTagStyle(tag: string): { + background: string; + color: string; + border: string; +} { + const colors = DOMAIN_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -173,45 +188,48 @@ export function getDomainTagStyle(tag: string): { background: string; color: str // 活动类型映射(英文 → 中文) export const ACTIVITY_TYPE_MAP: Record = { // 大写格式 - HANDICRAFT: '手工活动', - GAME: '游戏活动', - MUSIC: '音乐活动', - EXPLORATION: '探索活动', - SPORTS: '运动活动', - OUTDOOR: '户外活动', - FAMILY: '家庭延伸', - ART: '美工活动', - OTHER: '其他', + HANDICRAFT: "手工活动", + GAME: "游戏活动", + MUSIC: "音乐活动", + EXPLORATION: "探索活动", + SPORTS: "运动活动", + OUTDOOR: "户外活动", + FAMILY: "家庭延伸", + ART: "美工活动", + OTHER: "其他", // 小写格式 - handicraft: '手工活动', - game: '游戏活动', - music: '音乐活动', - exploration: '探索活动', - sports: '运动活动', - outdoor: '户外活动', - family: '家庭延伸', - art: '美工活动', - other: '其他', + handicraft: "手工活动", + game: "游戏活动", + music: "音乐活动", + exploration: "探索活动", + sports: "运动活动", + outdoor: "户外活动", + family: "家庭延伸", + art: "美工活动", + other: "其他", // 其他格式 - class: '课堂活动', + class: "课堂活动", }; // 活动类型颜色配置 -export const ACTIVITY_TYPE_COLORS: Record = { - '手工活动': { bg: '#F3E5F5', text: '#8E24AA' }, - '美工活动': { bg: '#F3E5F5', text: '#8E24AA' }, - '游戏活动': { bg: '#FFF3E0', text: '#FB8C00' }, - '音乐活动': { bg: '#E3F2FD', text: '#1976D2' }, - '运动活动': { bg: '#E8F5E9', text: '#43A047' }, - '探索活动': { bg: '#E0F7FA', text: '#0097A7' }, - '户外活动': { bg: '#F1F8E9', text: '#558B2F' }, - '亲子活动': { bg: '#FCE4EC', text: '#C2185B' }, - '家庭延伸': { bg: '#E3F2FD', text: '#1976D2' }, - '课堂活动': { bg: '#FFF8E1', text: '#F9A825' }, - '艺术活动': { bg: '#FBE9E7', text: '#E64A19' }, - '其他': { bg: '#F5F5F5', text: '#666666' }, +export const ACTIVITY_TYPE_COLORS: Record< + string, + { bg: string; text: string } +> = { + 手工活动: { bg: "#F3E5F5", text: "#8E24AA" }, + 美工活动: { bg: "#F3E5F5", text: "#8E24AA" }, + 游戏活动: { bg: "#FFF3E0", text: "#FB8C00" }, + 音乐活动: { bg: "#E3F2FD", text: "#1976D2" }, + 运动活动: { bg: "#E8F5E9", text: "#43A047" }, + 探索活动: { bg: "#E0F7FA", text: "#0097A7" }, + 户外活动: { bg: "#F1F8E9", text: "#558B2F" }, + 亲子活动: { bg: "#FCE4EC", text: "#C2185B" }, + 家庭延伸: { bg: "#E3F2FD", text: "#1976D2" }, + 课堂活动: { bg: "#FFF8E1", text: "#F9A825" }, + 艺术活动: { bg: "#FBE9E7", text: "#E64A19" }, + 其他: { bg: "#F5F5F5", text: "#666666" }, }; /** @@ -224,12 +242,16 @@ export function translateActivityType(type: string): string { /** * 获取活动类型样式 */ -export function getActivityTypeStyle(type: string): { background: string; color: string; border: string } { - const colors = ACTIVITY_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' }; +export function getActivityTypeStyle(type: string): { + background: string; + color: string; + border: string; +} { + const colors = ACTIVITY_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -245,50 +267,50 @@ export function translateActivityDomain(domain: string): string { // 步骤类型映射(英文 → 中文) export const STEP_TYPE_MAP: Record = { // 大写格式 - TEACHING: '教学', - READING: '共读', - DISCUSSION: '讨论', - ACTIVITY: '活动', - GAME: '游戏', - SUMMARY: '总结', - WARMUP: '热身', - PRACTICE: '练习', - INTERACTION: '互动', + TEACHING: "教学", + READING: "共读", + DISCUSSION: "讨论", + ACTIVITY: "活动", + GAME: "游戏", + SUMMARY: "总结", + WARMUP: "热身", + PRACTICE: "练习", + INTERACTION: "互动", // 新增环节类型 - INTRODUCTION: '导入', - CREATIVE: '创作', - CUSTOM: '自定义', + INTRODUCTION: "导入", + CREATIVE: "创作", + CUSTOM: "自定义", // 小写格式 - teaching: '教学', - reading: '共读', - discussion: '讨论', - activity: '活动', - game: '游戏', - summary: '总结', - warmup: '热身', - practice: '练习', - interaction: '互动', - introduction: '导入', - creative: '创作', - custom: '自定义', + teaching: "教学", + reading: "共读", + discussion: "讨论", + activity: "活动", + game: "游戏", + summary: "总结", + warmup: "热身", + practice: "练习", + interaction: "互动", + introduction: "导入", + creative: "创作", + custom: "自定义", }; // 步骤类型颜色配置 export const STEP_TYPE_COLORS: Record = { - '教学': { bg: '#E3F2FD', text: '#1976D2' }, - '共读': { bg: '#F3E5F5', text: '#8E24AA' }, - '阅读': { bg: '#F3E5F5', text: '#8E24AA' }, - '讨论': { bg: '#E0F7FA', text: '#0097A7' }, - '活动': { bg: '#FFF3E0', text: '#FB8C00' }, - '游戏': { bg: '#FCE4EC', text: '#C2185B' }, - '总结': { bg: '#E8F5E9', text: '#43A047' }, - '热身': { bg: '#FFF8E1', text: '#F9A825' }, - '练习': { bg: '#EDE7F6', text: '#673AB7' }, - '互动': { bg: '#FBE9E7', text: '#E64A19' }, - '导入': { bg: '#E8F5E9', text: '#4CAF50' }, - '创作': { bg: '#FCE4EC', text: '#E91E63' }, - '自定义': { bg: '#ECEFF1', text: '#607D8B' }, + 教学: { bg: "#E3F2FD", text: "#1976D2" }, + 共读: { bg: "#F3E5F5", text: "#8E24AA" }, + 阅读: { bg: "#F3E5F5", text: "#8E24AA" }, + 讨论: { bg: "#E0F7FA", text: "#0097A7" }, + 活动: { bg: "#FFF3E0", text: "#FB8C00" }, + 游戏: { bg: "#FCE4EC", text: "#C2185B" }, + 总结: { bg: "#E8F5E9", text: "#43A047" }, + 热身: { bg: "#FFF8E1", text: "#F9A825" }, + 练习: { bg: "#EDE7F6", text: "#673AB7" }, + 互动: { bg: "#FBE9E7", text: "#E64A19" }, + 导入: { bg: "#E8F5E9", text: "#4CAF50" }, + 创作: { bg: "#FCE4EC", text: "#E91E63" }, + 自定义: { bg: "#ECEFF1", text: "#607D8B" }, }; /** @@ -301,12 +323,16 @@ export function translateStepType(type: string): string { /** * 获取步骤类型样式 */ -export function getStepTypeStyle(type: string): { background: string; color: string; border: string } { - const colors = STEP_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' }; +export function getStepTypeStyle(type: string): { + background: string; + color: string; + border: string; +} { + const colors = STEP_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -314,29 +340,32 @@ export function getStepTypeStyle(type: string): { background: string; color: str // 课程状态映射(英文 → 中文) export const COURSE_STATUS_MAP: Record = { - DRAFT: '草稿', - PENDING: '审核中', - REJECTED: '已驳回', - PUBLISHED: '已发布', - ARCHIVED: '已下架', - REVIEWING: '审核中', + DRAFT: "草稿", + PENDING: "审核中", + REJECTED: "已驳回", + PUBLISHED: "已发布", + ARCHIVED: "已下架", + REVIEWING: "审核中", // 小写格式 - draft: '草稿', - pending: '审核中', - rejected: '已驳回', - published: '已发布', - archived: '已下架', - reviewing: '审核中', + draft: "草稿", + pending: "审核中", + rejected: "已驳回", + published: "已发布", + archived: "已下架", + reviewing: "审核中", }; // 课程状态颜色配置 -export const COURSE_STATUS_COLORS: Record = { - '草稿': { bg: '#F5F5F5', text: '#666666' }, - '审核中': { bg: '#E3F2FD', text: '#1976D2' }, - '已驳回': { bg: '#FFEBEE', text: '#E53935' }, - '已发布': { bg: '#E8F5E9', text: '#43A047' }, - '已下架': { bg: '#FFF8E1', text: '#F9A825' }, +export const COURSE_STATUS_COLORS: Record< + string, + { bg: string; text: string } +> = { + 草稿: { bg: "#F5F5F5", text: "#666666" }, + 审核中: { bg: "#E3F2FD", text: "#1976D2" }, + 已驳回: { bg: "#FFEBEE", text: "#E53935" }, + 已发布: { bg: "#E8F5E9", text: "#43A047" }, + 已下架: { bg: "#FFF8E1", text: "#F9A825" }, }; /** @@ -349,13 +378,20 @@ export function translateCourseStatus(status: string): string { /** * 获取课程状态样式 */ -export function getCourseStatusStyle(status: string): { background: string; color: string; border: string } { +export function getCourseStatusStyle(status: string): { + background: string; + color: string; + border: string; +} { const chineseStatus = COURSE_STATUS_MAP[status] || status; - const colors = COURSE_STATUS_COLORS[chineseStatus] || { bg: '#F0F0F0', text: '#666' }; + const colors = COURSE_STATUS_COLORS[chineseStatus] || { + bg: "#F0F0F0", + text: "#666", + }; return { background: colors.bg, color: colors.text, - border: 'none', + border: "none", }; } @@ -363,22 +399,22 @@ export function getCourseStatusStyle(status: string): { background: string; colo // 资源类型映射 export const RESOURCE_TYPE_MAP: Record = { - EBOOK: '电子绘本', - AUDIO: '音频', - VIDEO: '视频', - PPT: 'PPT课件', - POSTER: '教学挂图', - OTHER: '其他资源', - IMAGE: '图片', + EBOOK: "电子绘本", + AUDIO: "音频", + VIDEO: "视频", + PPT: "PPT课件", + POSTER: "教学挂图", + OTHER: "其他资源", + IMAGE: "图片", // 已中文的不转换 - '电子绘本': '电子绘本', - '音频': '音频', - '视频': '视频', - 'PPT课件': 'PPT课件', - '教学挂图': '教学挂图', - '其他资源': '其他资源', - '图片': '图片', + 电子绘本: "电子绘本", + 音频: "音频", + 视频: "视频", + PPT课件: "PPT课件", + 教学挂图: "教学挂图", + 其他资源: "其他资源", + 图片: "图片", }; /** diff --git a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue index c108e9f..3335f68 100644 --- a/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/teacher/courses/CourseListView.vue @@ -22,13 +22,8 @@
年级 - + 小班 @@ -38,42 +33,36 @@ 大班 - 混合
- 领域 - - 健康 - 语言 - 社会 - 科学 - 艺术 + 课程类型 + + 导入课 + 集体课 + 语言 + 健康 + 科学 + 社会 + 艺术
- - 最受欢迎 - 最新发布 - 评分最高 + + + 最受欢迎 + + + 最新发布 + + + 评分最高 +
@@ -81,26 +70,21 @@
-
+
- +
-
+
+ +
精彩绘本
- + + + {{ (course.avgRating ?? 0).toFixed(1) }}
@@ -112,21 +96,15 @@ {{ course.pictureBookName }}

- +
- - {{ tag }} - - - {{ tag }} + {{ tag }} + {{ tag }} + + {{ getLessonTypeName(lt.lessonType) }}
@@ -144,7 +122,9 @@
@@ -162,13 +142,8 @@
- +
@@ -203,6 +178,7 @@ const loading = ref(false); const filters = reactive({ grade: undefined as string | undefined, domain: undefined as string | undefined, + lessonType: undefined as string | undefined, keyword: '', sort: 'popular', }); @@ -215,23 +191,6 @@ const pagination = reactive({ const courses = ref([]); -// 年级映射(用于筛选) -const gradeMap: Record = { - SMALL: '小班', small: '小班', - MIDDLE: '中班', middle: '中班', - BIG: '大班', big: '大班', -}; - -// 领域映射(用于筛选) -const domainMap: Record = { - LANGUAGE: '语言', language: '语言', lang_listen: '语言', lang_read: '语言', - SCIENCE: '科学', science: '科学', science_explore: '科学', math_cog: '数学', - SOCIAL: '社会', social: '社会', social_interact: '社会', social_adapt: '社会', - ART: '艺术', art: '艺术', art_music: '艺术', art_create: '艺术', - HEALTH: '健康', health: '健康', health_motor: '健康', health_hygiene: '健康', - MATH: '数学', math: '数学', -}; - // 解析标签(与套餐管理 parseGradeLevels 对齐,兼容多种格式) const parseTags = (val: any): string[] => { if (!val) return []; @@ -273,32 +232,79 @@ const handlePageChange = () => { loadCourses(); }; +// 课程环节类型:英文码 -> 中文展示名(兼容 INTRODUCTION/INTRO、DOMAIN_* 等后端格式) +const LESSON_TYPE_NAMES: Record = { + INTRODUCTION: '导入课', + INTRO: '导入课', + COLLECTIVE: '集体课', + LANGUAGE: '语言', + HEALTH: '健康', + SCIENCE: '科学', + SOCIAL: '社会', + ART: '艺术', + DOMAIN_HEALTH: '健康', + DOMAIN_LANGUAGE: '语言', + DOMAIN_SOCIAL: '社会', + DOMAIN_SCIENCE: '科学', + DOMAIN_ART: '艺术', +}; +const getLessonTypeName = (type: string) => LESSON_TYPE_NAMES[type] || type; + +// 课程环节标签样式(按类型区分颜色) +const getLessonTagStyle = (type: string) => { + const colors: Record = { + INTRODUCTION: { background: '#E8F5E9', color: '#2E7D32' }, + INTRO: { background: '#E8F5E9', color: '#2E7D32' }, + COLLECTIVE: { background: '#E3F2FD', color: '#1565C0' }, + LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, + HEALTH: { background: '#FFEBEE', color: '#C62828' }, + SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, + SOCIAL: { background: '#E0F7FA', color: '#00838F' }, + ART: { background: '#FFF3E0', color: '#E65100' }, + DOMAIN_HEALTH: { background: '#FFEBEE', color: '#C62828' }, + DOMAIN_LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, + DOMAIN_SOCIAL: { background: '#E0F7FA', color: '#00838F' }, + DOMAIN_SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, + DOMAIN_ART: { background: '#FFF3E0', color: '#E65100' }, + }; + const c = colors[type] || { background: '#F5F5F5', color: '#666' }; + return { background: c.background, color: c.color, border: 'none' }; +}; + +// 五大领域:中文 -> 后端 domainTags 存储的英文码(用于筛选) +const DOMAIN_TO_CODE: Record = { + 健康: 'HEALTH', + 语言: 'LANGUAGE', + 社会: 'SOCIAL', + 科学: 'SCIENCE', + 艺术: 'ART', +}; + const loadCourses = async () => { loading.value = true; try { - const params: any = { + const params: teacherApi.TeacherCourseQueryParams = { pageNum: pagination.current, pageSize: pagination.pageSize, }; - if (filters.keyword) { - params.keyword = filters.keyword; + if (filters.keyword?.trim()) { + params.keyword = filters.keyword.trim(); } - // 年级筛选映射 + // 年级筛选:直接传小班/中班/大班,后端匹配 gradeTags if (filters.grade) { - const gradeKey = Object.keys(gradeMap).find((key) => gradeMap[key] === filters.grade); - if (gradeKey) { - params.grade = gradeKey; - } + params.grade = filters.grade; } - // 领域筛选映射 + // 领域筛选:传英文码,后端匹配 domainTags(五大领域课配置) if (filters.domain) { - const domainKey = Object.keys(domainMap).find((key) => domainMap[key] === filters.domain); - if (domainKey) { - params.domain = domainKey; - } + params.domain = DOMAIN_TO_CODE[filters.domain] ?? filters.domain; + } + + // 课程类型筛选:传 lessonType,后端匹配 course_lesson + if (filters.lessonType) { + params.lessonType = filters.lessonType; } const data = await teacherApi.getTeacherCourses(params); @@ -309,6 +315,7 @@ const loadCourses = async () => { ...item, gradeTags: translateGradeTags(gradeTags), domainTags: translateDomainTags(domainTags), + lessonTags: item.lessonTags || [], duration: item.duration ?? item.durationMinutes ?? 0, usageCount: item.usageCount ?? 0, avgRating: item.avgRating ?? 0, diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java index d0042fd..db9a7d2 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolCourseController.java @@ -36,7 +36,7 @@ public class SchoolCourseController { @RequestParam(required = false) String grade) { log.info("获取学校课程包列表,keyword={}, grade={}", keyword, grade); Long tenantId = SecurityUtils.getCurrentTenantId(); - List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade); + List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, null); List list = courses.stream() .map(this::toSchoolCourseResponse) .collect(Collectors.toList()); diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java index 91567ac..2307eb9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherCourseController.java @@ -11,6 +11,7 @@ import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.dto.response.ClassResponse; import com.reading.platform.dto.response.CourseResponse; +import com.reading.platform.dto.response.LessonTagResponse; import com.reading.platform.dto.response.StudentResponse; import com.reading.platform.dto.response.TeacherResponse; import com.reading.platform.entity.ClassTeacher; @@ -18,7 +19,9 @@ import com.reading.platform.entity.Clazz; import com.reading.platform.entity.CoursePackage; import com.reading.platform.entity.Student; import com.reading.platform.entity.Teacher; +import com.reading.platform.entity.CourseLesson; import com.reading.platform.service.ClassService; +import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CoursePackageService; import com.reading.platform.service.StudentService; import com.reading.platform.service.TeacherService; @@ -39,6 +42,7 @@ import java.util.stream.Collectors; public class TeacherCourseController { private final CoursePackageService courseService; + private final CourseLessonService courseLessonService; private final ClassService classService; private final StudentService studentService; private final TeacherService teacherService; @@ -67,12 +71,24 @@ public class TeacherCourseController { @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, - @RequestParam(required = false) String category) { + @RequestParam(required = false) String grade, + @RequestParam(required = false) String domain, + @RequestParam(required = false) String lessonType) { Long tenantId = SecurityUtils.getCurrentTenantId(); - // 按 学校 -> 套餐 -> 课程包 层级查询教师可用课程 + // 按 学校 -> 套餐 -> 课程包 层级查询,支持 grade/domain/lessonType 筛选 Page page = courseService.getTenantPackageCoursePage( - tenantId, pageNum, pageSize, keyword, category, CourseStatus.PUBLISHED.getCode()); + tenantId, pageNum, pageSize, keyword, grade, domain, lessonType, CourseStatus.PUBLISHED.getCode()); List voList = courseMapper.toVO(page.getRecords()); + // 填充 lessonTags(仅 name、lessonType)供列表 tag 展示 + for (CourseResponse vo : voList) { + List lessons = courseLessonService.findByCourseId(vo.getId()); + vo.setLessonTags(lessons.stream() + .map(l -> LessonTagResponse.builder() + .name(l.getName()) + .lessonType(l.getLessonType()) + .build()) + .collect(Collectors.toList())); + } return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); } @@ -80,10 +96,11 @@ public class TeacherCourseController { @GetMapping("/courses/all") public Result> getAllCourses( @RequestParam(required = false) String keyword, - @RequestParam(required = false) String category) { + @RequestParam(required = false) String grade, + @RequestParam(required = false) String domain) { Long tenantId = SecurityUtils.getCurrentTenantId(); // 按 学校 -> 套餐 -> 课程包 层级查询 - List courses = courseService.getTenantPackageCourses(tenantId, keyword, category); + List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain); return Result.success(courseMapper.toVO(courses)); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java index 2c856f6..d9f7c95 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/CourseResponse.java @@ -197,4 +197,7 @@ public class CourseResponse { @Schema(description = "关联的课程环节") private List courseLessons; + + @Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType)") + private List lessonTags; } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java new file mode 100644 index 0000000..9b5d853 --- /dev/null +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/LessonTagResponse.java @@ -0,0 +1,24 @@ +package com.reading.platform.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 课程环节标签(列表展示用,仅 name 和 lessonType) + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "课程环节标签") +public class LessonTagResponse { + + @Schema(description = "环节名称") + private String name; + + @Schema(description = "环节类型:INTRODUCTION、COLLECTIVE、LANGUAGE、HEALTH、SCIENCE、SOCIAL、ART") + private String lessonType; +} diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java index 4610006..edf955a 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java @@ -14,7 +14,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * 课程环节服务 @@ -57,6 +60,49 @@ public class CourseLessonService extends ServiceImpl findCourseIdsByLessonType(String lessonType) { + List typesToMatch = resolveLessonTypeVariants(lessonType); + if (typesToMatch.isEmpty()) { + return Collections.emptyList(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(CourseLesson::getCourseId); + if (typesToMatch.size() == 1) { + wrapper.eq(CourseLesson::getLessonType, typesToMatch.get(0)); + } else { + wrapper.in(CourseLesson::getLessonType, typesToMatch); + } + return courseLessonMapper.selectList(wrapper).stream() + .map(CourseLesson::getCourseId) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 将前端传入的 lessonType 解析为数据库中可能存储的多种格式 + */ + private List resolveLessonTypeVariants(String lessonType) { + if (lessonType == null || lessonType.isBlank()) { + return Collections.emptyList(); + } + return switch (lessonType.toUpperCase()) { + case "SOCIAL" -> Arrays.asList("SOCIAL", "SOCIETY", "DOMAIN_SOCIAL"); + case "SOCIETY" -> Arrays.asList("SOCIAL", "SOCIETY", "DOMAIN_SOCIAL"); + case "SCIENCE" -> Arrays.asList("SCIENCE", "DOMAIN_SCIENCE"); + case "LANGUAGE" -> Arrays.asList("LANGUAGE", "DOMAIN_LANGUAGE"); + case "HEALTH" -> Arrays.asList("HEALTH", "DOMAIN_HEALTH"); + case "ART" -> Arrays.asList("ART", "DOMAIN_ART"); + case "INTRODUCTION" -> Arrays.asList("INTRODUCTION", "INTRO"); + case "INTRO" -> Arrays.asList("INTRODUCTION", "INTRO"); + case "COLLECTIVE" -> Collections.singletonList("COLLECTIVE"); + default -> Collections.singletonList(lessonType); + }; + } + /** * 按类型查询课程环节 */ diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java index 28f789c..838f5e9 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/CoursePackageService.java @@ -61,22 +61,25 @@ public interface CoursePackageService extends com.baomidou.mybatisplus.extension * * @param tenantId 租户 ID * @param keyword 关键词(课程名称、绘本名称,可选) - * @param grade 年级筛选(小班/中班/大班 或 small/middle/big,可选) + * @param grade 年级筛选(小班/中班/大班,可选) + * @param domain 领域筛选(健康/语言/社会/科学/艺术 或对应英文码,可选) */ - List getTenantPackageCourses(Long tenantId, String keyword, String grade); + List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain); /** * 按 学校 -> 套餐 -> 课程包 层级分页查询教师可用课程 * 教师端课程中心应使用此方法,通过租户已购买的套餐获取课程包 * - * @param tenantId 租户(学校)ID - * @param pageNum 页码 - * @param pageSize 每页数量 - * @param keyword 关键词(课程名称、绘本名称,可选) - * @param grade 年级/领域筛选(可选) - * @param status 课程状态(如 PUBLISHED) + * @param tenantId 租户(学校)ID + * @param pageNum 页码 + * @param pageSize 每页数量 + * @param keyword 关键词(课程名称、绘本名称,可选) + * @param grade 年级筛选(小班/中班/大班 或 SMALL/MIDDLE/BIG,可选) + * @param domain 领域筛选(健康/语言/社会/科学/艺术 或 HEALTH/LANGUAGE/SOCIAL/SCIENCE/ART,可选) + * @param lessonType 课程环节类型筛选(INTRODUCTION、COLLECTIVE、LANGUAGE、HEALTH、SCIENCE、SOCIAL、ART,可选) + * @param status 课程状态(如 PUBLISHED) */ Page getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize, - String keyword, String grade, String status); + String keyword, String grade, String domain, String lessonType, String status); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java index 4f51583..032ccc1 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/CoursePackageServiceImpl.java @@ -30,6 +30,7 @@ import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** @@ -226,7 +227,7 @@ public class CoursePackageServiceImpl extends ServiceImpl getTenantPackageCourses(Long tenantId, String keyword, String grade) { + public List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain) { List collectionIds = tenantPackageMapper.selectList( new LambdaQueryWrapper() .eq(TenantPackage::getTenantId, tenantId) @@ -271,13 +272,21 @@ public class CoursePackageServiceImpl extends ServiceImpl w + .like(CoursePackage::getDomainTags, domain) + .or().like(CoursePackage::getDomainTags, domainLower) + .or().like(CoursePackage::getDomainTags, "\"" + domain + "\"") + .or().like(CoursePackage::getDomainTags, "\"" + domainLower + "\"")); + } wrapper.orderByDesc(CoursePackage::getUsageCount); return coursePackageMapper.selectList(wrapper); } @Override public Page getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize, - String keyword, String grade, String status) { + String keyword, String grade, String domain, String lessonType, String status) { int current = pageNum != null && pageNum > 0 ? pageNum : 1; int size = pageSize != null && pageSize > 0 ? pageSize : 10; @@ -309,7 +318,17 @@ public class CoursePackageServiceImpl extends ServiceImpl(current, size, 0); } - // 3. 分页查询课程包 + // 2.5 lessonType 筛选:仅保留包含该类型环节的课程包 + if (StringUtils.hasText(lessonType)) { + Set courseIdsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType).stream() + .collect(Collectors.toSet()); + packageIds = packageIds.stream().filter(courseIdsWithLesson::contains).collect(Collectors.toList()); + if (packageIds.isEmpty()) { + return new Page<>(current, size, 0); + } + } + + // 3. 分页查询课程包(gradeTags 年级筛选,domainTags 领域筛选) Page page = new Page<>(current, size); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(CoursePackage::getId, packageIds) @@ -321,15 +340,23 @@ public class CoursePackageServiceImpl extends ServiceImpl w .like(CoursePackage::getGradeTags, grade) .or().like(CoursePackage::getGradeTags, gradeLower) .or().like(CoursePackage::getGradeTags, "\"" + grade + "\"") - .or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\"") - .or().like(CoursePackage::getDomainTags, grade) - .or().like(CoursePackage::getDomainTags, gradeLower)); + .or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\"")); + } + // 领域筛选:匹配 domainTags(支持五大领域中文或英文码 HEALTH/LANGUAGE/SOCIAL/SCIENCE/ART) + if (StringUtils.hasText(domain)) { + String domainLower = domain.toLowerCase(); + wrapper.and(w -> w + .like(CoursePackage::getDomainTags, domain) + .or().like(CoursePackage::getDomainTags, domainLower) + .or().like(CoursePackage::getDomainTags, "\"" + domain + "\"") + .or().like(CoursePackage::getDomainTags, "\"" + domainLower + "\"")); } wrapper.orderByDesc(CoursePackage::getUsageCount); return coursePackageMapper.selectPage(page, wrapper);