From f425209abe2805b9828888afd79c0090f9a6cdf5 Mon Sep 17 00:00:00 2001 From: zhonghua Date: Thu, 19 Mar 2026 15:07:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=A1=E5=9B=AD=E7=AB=AF=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E5=88=97=E8=A1=A8=E7=AD=9B=E9=80=89=E4=B8=8E=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=B1=95=E7=A4=BA=E5=8F=82=E8=80=83=E6=95=99=E5=B8=88?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 筛选栏:年级/领域/课程类型下拉、搜索框,保留授权新课程按钮 - 卡片:新增 lessonTags 展示(导入课、集体课、领域等) - 后端:学校课程 API 支持 domain、lessonType 参数及 lessonTags 返回 - 主题保持校园端绿色 Made-with: Cursor --- reading-platform-frontend/src/api/school.ts | 4 +- .../views/school/courses/CourseListView.vue | 237 +++++++++++------- .../school/SchoolCourseController.java | 24 +- .../teacher/TeacherCourseController.java | 2 +- .../dto/response/SchoolCourseResponse.java | 4 + .../service/CoursePackageService.java | 11 +- .../impl/CoursePackageServiceImpl.java | 12 +- 7 files changed, 196 insertions(+), 98 deletions(-) diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts index a5b0cd7..2203858 100644 --- a/reading-platform-frontend/src/api/school.ts +++ b/reading-platform-frontend/src/api/school.ts @@ -396,7 +396,9 @@ export interface Course { export interface SchoolCourseQueryParams { keyword?: string; - grade?: string; // 小班|中班|大班 或 small|middle|big + grade?: string; // 小班|中班|大班 + domain?: string; // 健康|语言|社会|科学|艺术(传英文码) + lessonType?: string; // INTRODUCTION|COLLECTIVE|LANGUAGE|HEALTH|SCIENCE|SOCIAL|ART } export const getSchoolCourses = (params?: SchoolCourseQueryParams) => diff --git a/reading-platform-frontend/src/views/school/courses/CourseListView.vue b/reading-platform-frontend/src/views/school/courses/CourseListView.vue index 5bc287b..462132c 100644 --- a/reading-platform-frontend/src/views/school/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/school/courses/CourseListView.vue @@ -25,26 +25,52 @@ - +
-
- 年级筛选 -
-
- {{ grade.label }} -
+
+
+ 年级 + + 小班 + 中班 + 大班 +
-
-
-
@@ -69,14 +95,12 @@

《{{ course.pictureBookName }}》

- - {{ translateGradeTag(tag) }} - - - {{ translateDomainTag(tag) }} - + {{ tag }} + {{ tag }} + {{ getLessonTypeName(lt.lessonType) }}
@@ -116,7 +140,7 @@
-

{{ (searchKeyword || selectedGrade) ? '未找到匹配的课程' : '暂无课程数据' }}

+

{{ hasFilters ? '暂无符合条件的课程,试试调整筛选条件吧' : '暂无课程数据' }}

授权第一门课程 @@ -200,8 +224,8 @@ import { } from '@ant-design/icons-vue'; import { message } from 'ant-design-vue'; import { - translateGradeTag, - translateDomainTag, + translateGradeTags, + translateDomainTags, getGradeTagStyle, getDomainTagStyle, } from '@/utils/tagMaps'; @@ -214,27 +238,55 @@ const authLoading = ref(false); const authModalVisible = ref(false); const searchKeyword = ref(''); const selectedCourseIds = ref([]); -const selectedGrade = ref(''); // 选中的年级 -// 年级选项 -const gradeOptions = [ - { label: '全部', value: '' }, - { label: '小班', value: '小班' }, - { label: '中班', value: '中班' }, - { label: '大班', value: '大班' }, -]; +// 筛选条件(参考教师端) +const filters = reactive({ + grade: undefined as string | undefined, + domain: undefined as string | undefined, + lessonType: undefined as string | undefined, + keyword: '', +}); const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length); const totalUsage = computed(() => courses.value.reduce((sum, c) => sum + (c.usageCount || 0), 0)); +const hasFilters = computed(() => + !!(filters.grade || filters.domain || filters.lessonType || filters.keyword?.trim()) +); -// 年级切换:请求后端筛选 -const handleGradeChange = (value: string) => { - selectedGrade.value = value; - loadCourses(); +// 五大领域:中文 -> 后端英文码 +const DOMAIN_TO_CODE: Record = { + 健康: 'HEALTH', + 语言: 'LANGUAGE', + 社会: 'SOCIAL', + 科学: 'SCIENCE', + 艺术: 'ART', }; -// 搜索:请求后端筛选 -const handleSearch = () => { +// 课程环节类型映射 +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' }; +}; + +const handleFilterChange = () => { loadCourses(); }; @@ -267,20 +319,54 @@ const pagination = reactive({ const courses = ref([]); const availableCourses = ref([]); -// 加载课程列表(支持后端搜索与年级筛选) +// 解析标签 +const parseTags = (val: any): string[] => { + if (!val) return []; + if (Array.isArray(val)) { + if (val.length === 0) return []; + if (val[0]?.toString().startsWith('[')) { + try { + return JSON.parse(val.join('')); + } catch { + return []; + } + } + return val; + } + if (typeof val === 'string') { + try { + const parsed = JSON.parse(val); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + return []; +}; + +// 加载课程列表(支持年级、领域、课程类型、关键词筛选) const loadCourses = async () => { loading.value = true; try { - const params: { keyword?: string; grade?: string } = {}; - if (searchKeyword.value?.trim()) params.keyword = searchKeyword.value.trim(); - if (selectedGrade.value) params.grade = selectedGrade.value; + const params: schoolApi.SchoolCourseQueryParams = {}; + if (filters.keyword?.trim()) params.keyword = filters.keyword.trim(); + if (filters.grade) params.grade = filters.grade; + if (filters.domain) params.domain = DOMAIN_TO_CODE[filters.domain] ?? filters.domain; + if (filters.lessonType) params.lessonType = filters.lessonType; const data = await schoolApi.getSchoolCourses(params); - courses.value = (data || []).map((course: any) => ({ - ...course, - duration: course.duration ?? course.durationMinutes ?? 0, - pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl, - authorized: course.authorized ?? true, - })); + courses.value = (data || []).map((course: any) => { + const gradeTags = parseTags(course.gradeTags); + const domainTags = parseTags(course.domainTags); + return { + ...course, + gradeTags: translateGradeTags(gradeTags), + domainTags: translateDomainTags(domainTags), + lessonTags: course.lessonTags || [], + duration: course.duration ?? course.durationMinutes ?? 0, + pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl, + authorized: course.authorized ?? true, + }; + }); pagination.total = courses.value.length; } catch (error: any) { message.error(error.response?.data?.message || '加载课程列表失败'); @@ -447,11 +533,8 @@ onMounted(() => { color: rgba(255, 255, 255, 0.8); } -/* 筛选操作栏 */ +/* 筛选操作栏(参考教师端布局) */ .filter-action-bar { - display: flex; - flex-direction: column; - gap: 16px; margin-bottom: 24px; padding: 20px 24px; background: white; @@ -459,51 +542,33 @@ onMounted(() => { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); } -.grade-tabs { +.filter-row { display: flex; align-items: center; + flex-wrap: wrap; gap: 16px; } -.tab-label { - font-size: 14px; - font-weight: 600; - color: #333; - white-space: nowrap; -} - -.tab-buttons { +.filter-item { display: flex; + align-items: center; gap: 8px; } -.grade-tab { - padding: 8px 20px; - border-radius: 10px; +.filter-label { font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - border: none; - background: #F5F5F5; color: #666; + font-weight: 500; } -.grade-tab:hover { - background: #E8F5E9; - color: #43e97b; +.filter-right { + margin-left: auto; } -.grade-tab.active { - background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); - color: white; - box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3); -} - -.action-row { - display: flex; - justify-content: space-between; - align-items: center; +.search-box { + flex: 1; + min-width: 200px; + max-width: 280px; } /* 操作栏 */ 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 db9a7d2..da08a7b 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 @@ -3,8 +3,11 @@ package com.reading.platform.controller.school; import com.alibaba.fastjson2.JSON; import com.reading.platform.common.response.Result; import com.reading.platform.common.security.SecurityUtils; +import com.reading.platform.dto.response.LessonTagResponse; import com.reading.platform.dto.response.SchoolCourseResponse; +import com.reading.platform.entity.CourseLesson; import com.reading.platform.entity.CoursePackage; +import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CoursePackageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -28,18 +31,31 @@ import java.util.stream.Collectors; public class SchoolCourseController { private final CoursePackageService courseService; + private final CourseLessonService courseLessonService; @GetMapping @Operation(summary = "获取学校课程包列表") public Result> getSchoolCourses( @RequestParam(required = false) String keyword, - @RequestParam(required = false) String grade) { - log.info("获取学校课程包列表,keyword={}, grade={}", keyword, grade); + @RequestParam(required = false) String grade, + @RequestParam(required = false) String domain, + @RequestParam(required = false) String lessonType) { + log.info("获取学校课程包列表,keyword={}, grade={}, domain={}, lessonType={}", keyword, grade, domain, lessonType); Long tenantId = SecurityUtils.getCurrentTenantId(); - List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, null); + List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, lessonType); List list = courses.stream() - .map(this::toSchoolCourseResponse) + .map(pkg -> toSchoolCourseResponse(pkg)) .collect(Collectors.toList()); + // 填充 lessonTags + for (SchoolCourseResponse vo : list) { + 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(list); } 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 2307eb9..5577303 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 @@ -100,7 +100,7 @@ public class TeacherCourseController { @RequestParam(required = false) String domain) { Long tenantId = SecurityUtils.getCurrentTenantId(); // 按 学校 -> 套餐 -> 课程包 层级查询 - List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain); + List courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, null); return Result.success(courseMapper.toVO(courses)); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java index 9bf0b10..a88fcc1 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/SchoolCourseResponse.java @@ -6,6 +6,7 @@ import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.List; /** * 学校端课程响应(gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐) @@ -65,4 +66,7 @@ public class SchoolCourseResponse { @Schema(description = "更新时间") private LocalDateTime updatedAt; + + @Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType)") + private List lessonTags; } 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 838f5e9..86b8dbf 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 @@ -59,12 +59,13 @@ public interface CoursePackageService extends com.baomidou.mybatisplus.extension /** * 查询租户套餐下的课程 * - * @param tenantId 租户 ID - * @param keyword 关键词(课程名称、绘本名称,可选) - * @param grade 年级筛选(小班/中班/大班,可选) - * @param domain 领域筛选(健康/语言/社会/科学/艺术 或对应英文码,可选) + * @param tenantId 租户 ID + * @param keyword 关键词(课程名称、绘本名称,可选) + * @param grade 年级筛选(小班/中班/大班,可选) + * @param domain 领域筛选(健康/语言/社会/科学/艺术 或对应英文码,可选) + * @param lessonType 课程环节类型筛选(可选) */ - List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain); + List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType); /** * 按 学校 -> 套餐 -> 课程包 层级分页查询教师可用课程 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 032ccc1..a651828 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 @@ -227,7 +227,7 @@ public class CoursePackageServiceImpl extends ServiceImpl getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain) { + public List getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType) { List collectionIds = tenantPackageMapper.selectList( new LambdaQueryWrapper() .eq(TenantPackage::getTenantId, tenantId) @@ -254,6 +254,16 @@ public class CoursePackageServiceImpl extends ServiceImpl(); } + // 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 ArrayList<>(); + } + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(CoursePackage::getId, packageIds) .eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode());