feat: 课程中心主题筛选改为课程配置筛选,课程包卡片展示课程配置

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-23 11:12:09 +08:00
parent 4122bcd240
commit ac8e07c784
10 changed files with 207 additions and 157 deletions

View File

@ -14,6 +14,13 @@ export interface CourseCollection {
endDate?: string; endDate?: string;
} }
/** 课程包中的课程项(用于提取课程配置) */
export interface CoursePackageCourseItem {
id?: number;
name?: string;
lessonType?: string;
}
/** 课程包信息 */ /** 课程包信息 */
export interface CoursePackage { export interface CoursePackage {
id: number; id: number;
@ -22,7 +29,8 @@ export interface CoursePackage {
coverImagePath?: string; coverImagePath?: string;
pictureBookName?: string; pictureBookName?: string;
gradeTags: string[]; gradeTags: string[];
domainTags?: string[]; domainTags?: string[]; // 不再展示,由课程配置替代
courses?: CoursePackageCourseItem[]; // 用于课程配置展示
themeId?: number; themeId?: number;
themeName?: string; themeName?: string;
durationMinutes?: number; durationMinutes?: number;
@ -37,9 +45,9 @@ export interface GradeOption {
count: number; count: number;
} }
/** 筛选元数据 - 主题选项 */ /** 筛选元数据 - 课程配置选项 */
export interface ThemeOption { export interface LessonTypeOption {
id: number; lessonType: string;
name: string; name: string;
count: number; count: number;
} }
@ -47,7 +55,7 @@ export interface ThemeOption {
/** 筛选元数据响应 */ /** 筛选元数据响应 */
export interface FilterMetaResponse { export interface FilterMetaResponse {
grades: GradeOption[]; grades: GradeOption[];
themes: ThemeOption[]; lessonTypes: LessonTypeOption[];
} }
// ============= API 接口 ============= // ============= API 接口 =============
@ -66,7 +74,7 @@ export function getPackages(
collectionId: number, collectionId: number,
params?: { params?: {
grade?: string; grade?: string;
themeId?: number; lessonType?: string;
keyword?: string; keyword?: string;
} }
): Promise<CoursePackage[]> { ): Promise<CoursePackage[]> {

View File

@ -7,12 +7,9 @@
</div> </div>
<a-spin :spinning="loadingCollections"> <a-spin :spinning="loadingCollections">
<div class="collection-list"> <div class="collection-list">
<div <div v-for="collection in collections" :key="collection.id"
v-for="collection in collections"
:key="collection.id"
:class="['collection-item', { active: selectedCollectionId === collection.id }]" :class="['collection-item', { active: selectedCollectionId === collection.id }]"
@click="selectCollection(collection)" @click="selectCollection(collection)">
>
<div class="collection-name">{{ collection.name }}</div> <div class="collection-name">{{ collection.name }}</div>
<div class="collection-count">{{ collection.packageCount || 0 }}个课程包</div> <div class="collection-count">{{ collection.packageCount || 0 }}个课程包</div>
</div> </div>
@ -34,11 +31,7 @@
<div ref="descRef" :class="['desc-text', { expanded: descExpanded }]"> <div ref="descRef" :class="['desc-text', { expanded: descExpanded }]">
{{ selectedCollection.description }} {{ selectedCollection.description }}
</div> </div>
<button <button v-if="showExpandBtn" class="expand-btn" @click="descExpanded = !descExpanded">
v-if="showExpandBtn"
class="expand-btn"
@click="descExpanded = !descExpanded"
>
{{ descExpanded ? '收起' : '展开更多' }} {{ descExpanded ? '收起' : '展开更多' }}
<DownOutlined :class="{ rotated: descExpanded }" /> <DownOutlined :class="{ rotated: descExpanded }" />
</button> </button>
@ -52,18 +45,12 @@
<div class="filter-group"> <div class="filter-group">
<span class="filter-label">年级</span> <span class="filter-label">年级</span>
<div class="grade-tags"> <div class="grade-tags">
<span <span :class="['grade-tag', { active: !selectedGrade }]" @click="selectedGrade = ''">
:class="['grade-tag', { active: !selectedGrade }]"
@click="selectedGrade = ''"
>
全部 全部
</span> </span>
<span <span v-for="grade in filterMeta.grades" :key="grade.label"
v-for="grade in filterMeta.grades"
:key="grade.label"
:class="['grade-tag', { active: selectedGrade === grade.label }]" :class="['grade-tag', { active: selectedGrade === grade.label }]"
@click="selectedGrade = grade.label" @click="selectedGrade = grade.label">
>
{{ grade.label }} {{ grade.label }}
<span class="count">({{ grade.count }})</span> <span class="count">({{ grade.count }})</span>
</span> </span>
@ -72,36 +59,23 @@
</div> </div>
<div class="filter-row"> <div class="filter-row">
<!-- 主题筛选 --> <!-- 课程配置筛选 -->
<div class="filter-group"> <div class="filter-group">
<span class="filter-label">主题</span> <span class="filter-label">课程配置</span>
<a-select <a-select v-model:value="selectedLessonType" placeholder="全部课程配置" style="width: 180px" allowClear
v-model:value="selectedThemeId" @change="loadPackages">
placeholder="全部主题" <a-select-option :value="undefined">全部课程配置</a-select-option>
style="width: 180px" <a-select-option v-for="opt in (filterMeta.lessonTypes || [])" :key="opt.lessonType"
allowClear :value="opt.lessonType">
@change="loadPackages" {{ opt.name }} ({{ opt.count }})
>
<a-select-option :value="undefined">全部主题</a-select-option>
<a-select-option
v-for="theme in filterMeta.themes"
:key="theme.id"
:value="theme.id"
>
{{ theme.name }} ({{ theme.count }})
</a-select-option> </a-select-option>
</a-select> </a-select>
</div> </div>
<!-- 搜索 --> <!-- 搜索 -->
<div class="filter-group search-group"> <div class="filter-group search-group">
<a-input-search <a-input-search v-model:value="searchKeyword" placeholder="搜索课程包..." style="width: 220px" allowClear
v-model:value="searchKeyword" @search="loadPackages" />
placeholder="搜索课程包..."
style="width: 220px"
allowClear
@search="loadPackages"
/>
</div> </div>
</div> </div>
</section> </section>
@ -110,13 +84,8 @@
<section class="packages-section"> <section class="packages-section">
<a-spin :spinning="loadingPackages"> <a-spin :spinning="loadingPackages">
<div v-if="packages.length > 0" class="packages-grid"> <div v-if="packages.length > 0" class="packages-grid">
<CoursePackageCard <CoursePackageCard v-for="pkg in packages" :key="pkg.id" :pkg="pkg" @click="handlePackageClick"
v-for="pkg in packages" @view="handlePackageView" />
:key="pkg.id"
:pkg="pkg"
@click="handlePackageClick"
@view="handlePackageView"
/>
</div> </div>
<div v-else class="empty-packages"> <div v-else class="empty-packages">
<InboxOutlined class="empty-icon" /> <InboxOutlined class="empty-icon" />
@ -200,13 +169,13 @@ const loadCollections = async () => {
loadingCollections.value = false; loadingCollections.value = false;
} }
}; };
const selectedLessonType = ref<string | undefined>(undefined);
// //
const selectCollection = async (collection: CourseCollection) => { const selectCollection = async (collection: CourseCollection) => {
selectedCollectionId.value = collection.id; selectedCollectionId.value = collection.id;
// //
selectedGrade.value = ''; selectedGrade.value = '';
selectedThemeId.value = undefined; selectedLessonType.value = undefined;
searchKeyword.value = ''; searchKeyword.value = '';
descExpanded.value = false; descExpanded.value = false;
@ -236,7 +205,7 @@ const loadFilterMeta = async () => {
filterMeta.value = await getFilterMeta(selectedCollectionId.value); filterMeta.value = await getFilterMeta(selectedCollectionId.value);
} catch (error) { } catch (error) {
console.error('获取筛选元数据失败', error); console.error('获取筛选元数据失败', error);
filterMeta.value = { grades: [], themes: [] }; filterMeta.value = { grades: [], lessonTypes: [] };
} }
}; };
@ -247,7 +216,7 @@ const loadPackages = async () => {
try { try {
packages.value = await getPackages(selectedCollectionId.value, { packages.value = await getPackages(selectedCollectionId.value, {
grade: selectedGrade.value || undefined, grade: selectedGrade.value || undefined,
themeId: selectedThemeId.value, lessonType: selectedLessonType.value,
keyword: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
}); });
} catch (error: any) { } catch (error: any) {
@ -258,8 +227,8 @@ const loadPackages = async () => {
} }
}; };
// //
watch(selectedGrade, () => { watch([selectedGrade, selectedLessonType], () => {
loadPackages(); loadPackages();
}); });
@ -429,7 +398,7 @@ onMounted(() => {
gap: 16px; gap: 16px;
} }
.filter-row + .filter-row { .filter-row+.filter-row {
margin-top: 12px; margin-top: 12px;
} }

View File

@ -28,10 +28,15 @@
</span> </span>
</div> </div>
<!-- 主题标签行 --> <!-- 课程配置标签行参考管理端 -->
<div v-if="pkg.themeName" class="tag-row theme-row"> <div v-if="lessonTypes.length > 0" class="tag-row config-row">
<span class="theme-tag"> <span
{{ pkg.themeName }} v-for="lt in lessonTypes"
:key="lt"
class="config-tag"
:style="getLessonTagStyle(lt)"
>
{{ getLessonTypeName(lt) }}
</span> </span>
</div> </div>
@ -66,6 +71,7 @@ import {
EyeOutlined, EyeOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import type { CoursePackage } from '@/api/course-center'; import type { CoursePackage } from '@/api/course-center';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
const props = defineProps<{ const props = defineProps<{
pkg: CoursePackage; pkg: CoursePackage;
@ -82,6 +88,17 @@ const gradeText = computed(() => {
return grades.join(' · '); return grades.join(' · ');
}); });
// courses
const lessonTypes = computed(() => {
const courses = props.pkg.courses || [];
const types = new Set<string>();
for (const c of courses) {
const t = c.lessonType;
if (t) types.add(t);
}
return Array.from(types);
});
// URL // URL
const getImageUrl = (path: string) => { const getImageUrl = (path: string) => {
if (!path) return ''; if (!path) return '';
@ -199,14 +216,17 @@ const handleView = () => {
border: 1px solid #91d5ff; border: 1px solid #91d5ff;
} }
.theme-tag { .config-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.config-tag {
display: inline-block; display: inline-block;
padding: 2px 10px; padding: 2px 8px;
background: #f6ffed; font-size: 11px;
color: #52c41a;
font-size: 12px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #b7eb8f;
} }
/* 统计信息 */ /* 统计信息 */

View File

@ -137,16 +137,9 @@
<!-- 分页组件 --> <!-- 分页组件 -->
<div class="pagination-wrapper" v-if="!loading && pagination.total > 0"> <div class="pagination-wrapper" v-if="!loading && pagination.total > 0">
<a-pagination <a-pagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize"
v-model:current="pagination.current" :total="pagination.total" show-size-changer show-quick-jumper :show-total="(total: number) => `共 ${total} 条`"
v-model:page-size="pagination.pageSize" @change="handlePageChange" @showSizeChange="handlePageSizeChange" />
:total="pagination.total"
show-size-changer
show-quick-jumper
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
/>
</div> </div>
<!-- 空状态 --> <!-- 空状态 -->

View File

@ -72,23 +72,23 @@
</div> </div>
<div class="filter-row"> <div class="filter-row">
<!-- 主题筛选 --> <!-- 课程配置筛选 -->
<div class="filter-group"> <div class="filter-group">
<span class="filter-label">主题</span> <span class="filter-label">课程配置</span>
<a-select <a-select
v-model:value="selectedThemeId" v-model:value="selectedLessonType"
placeholder="全部主题" placeholder="全部课程配置"
style="width: 180px" style="width: 180px"
allowClear allowClear
@change="loadPackages" @change="loadPackages"
> >
<a-select-option :value="undefined">全部主题</a-select-option> <a-select-option :value="undefined">全部课程配置</a-select-option>
<a-select-option <a-select-option
v-for="theme in filterMeta.themes" v-for="opt in (filterMeta.lessonTypes || [])"
:key="theme.id" :key="opt.lessonType"
:value="theme.id" :value="opt.lessonType"
> >
{{ theme.name }} ({{ theme.count }}) {{ opt.name }} ({{ opt.count }})
</a-select-option> </a-select-option>
</a-select> </a-select>
</div> </div>
@ -166,7 +166,7 @@ const selectedCollection = computed(() =>
); );
// //
const filterMeta = ref<FilterMetaResponse>({ grades: [], themes: [] }); const filterMeta = ref<FilterMetaResponse>({ grades: [], lessonTypes: [] });
// //
const packages = ref<CoursePackage[]>([]); const packages = ref<CoursePackage[]>([]);
@ -174,7 +174,7 @@ const loadingPackages = ref(false);
// //
const selectedGrade = ref(''); const selectedGrade = ref('');
const selectedThemeId = ref<number | undefined>(undefined); const selectedLessonType = ref<string | undefined>(undefined);
const searchKeyword = ref(''); const searchKeyword = ref('');
// //
@ -204,7 +204,7 @@ const selectCollection = async (collection: CourseCollection) => {
selectedCollectionId.value = collection.id; selectedCollectionId.value = collection.id;
// //
selectedGrade.value = ''; selectedGrade.value = '';
selectedThemeId.value = undefined; selectedLessonType.value = undefined;
searchKeyword.value = ''; searchKeyword.value = '';
descExpanded.value = false; descExpanded.value = false;
@ -234,7 +234,7 @@ const loadFilterMeta = async () => {
filterMeta.value = await getFilterMeta(selectedCollectionId.value); filterMeta.value = await getFilterMeta(selectedCollectionId.value);
} catch (error) { } catch (error) {
console.error('获取筛选元数据失败', error); console.error('获取筛选元数据失败', error);
filterMeta.value = { grades: [], themes: [] }; filterMeta.value = { grades: [], lessonTypes: [] };
} }
}; };
@ -245,7 +245,7 @@ const loadPackages = async () => {
try { try {
packages.value = await getPackages(selectedCollectionId.value, { packages.value = await getPackages(selectedCollectionId.value, {
grade: selectedGrade.value || undefined, grade: selectedGrade.value || undefined,
themeId: selectedThemeId.value, lessonType: selectedLessonType.value,
keyword: searchKeyword.value || undefined, keyword: searchKeyword.value || undefined,
}); });
} catch (error: any) { } catch (error: any) {
@ -266,8 +266,8 @@ const handlePrepare = (pkg: CoursePackage) => {
router.push(`/teacher/courses/${pkg.id}/prepare`); router.push(`/teacher/courses/${pkg.id}/prepare`);
}; };
// //
watch(selectedGrade, () => { watch([selectedGrade, selectedLessonType], () => {
loadPackages(); loadPackages();
}); });

View File

@ -28,10 +28,15 @@
</span> </span>
</div> </div>
<!-- 主题标签行 --> <!-- 课程配置标签行参考管理端 -->
<div v-if="pkg.themeName" class="tag-row theme-row"> <div v-if="lessonTypes.length > 0" class="tag-row config-row">
<span class="theme-tag"> <span
{{ pkg.themeName }} v-for="lt in lessonTypes"
:key="lt"
class="config-tag"
:style="getLessonTagStyle(lt)"
>
{{ getLessonTypeName(lt) }}
</span> </span>
</div> </div>
@ -66,6 +71,7 @@ import {
EditOutlined, EditOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import type { CoursePackage } from '@/api/course-center'; import type { CoursePackage } from '@/api/course-center';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
const props = defineProps<{ const props = defineProps<{
pkg: CoursePackage; pkg: CoursePackage;
@ -82,6 +88,17 @@ const gradeText = computed(() => {
return grades.join(' · '); return grades.join(' · ');
}); });
// courses
const lessonTypes = computed(() => {
const courses = props.pkg.courses || [];
const types = new Set<string>();
for (const c of courses) {
const t = c.lessonType;
if (t) types.add(t);
}
return Array.from(types);
});
// URL // URL
const getImageUrl = (path: string) => { const getImageUrl = (path: string) => {
if (!path) return ''; if (!path) return '';
@ -203,18 +220,17 @@ const handlePrepare = () => {
border: 1px solid #FFD591; border: 1px solid #FFD591;
} }
.theme-row { .config-row {
/* 主题标签样式 */ display: flex;
flex-wrap: wrap;
gap: 4px;
} }
.theme-tag { .config-tag {
display: inline-block; display: inline-block;
padding: 2px 10px; padding: 2px 8px;
background: #E6F7FF; font-size: 11px;
color: #096DD9;
font-size: 12px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #91D5FF;
} }
/* 统计信息 */ /* 统计信息 */

View File

@ -50,13 +50,13 @@ public class SchoolPackageController {
public Result<List<CoursePackageResponse>> getPackagesByCollection( public Result<List<CoursePackageResponse>> getPackagesByCollection(
@PathVariable Long collectionId, @PathVariable Long collectionId,
@RequestParam(required = false) String grade, @RequestParam(required = false) String grade,
@RequestParam(required = false) Long themeId, @RequestParam(required = false) String lessonType,
@RequestParam(required = false) String keyword) { @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") @GetMapping("/{collectionId}/filter-meta")
@Operation(summary = "获取套餐筛选元数据(年级、主题选项)") @Operation(summary = "获取套餐筛选元数据(年级、课程配置选项)")
@RequireRole({UserRole.SCHOOL, UserRole.TEACHER}) @RequireRole({UserRole.SCHOOL, UserRole.TEACHER})
public Result<PackageFilterMetaResponse> getFilterMeta(@PathVariable Long collectionId) { public Result<PackageFilterMetaResponse> getFilterMeta(@PathVariable Long collectionId) {
return Result.success(collectionService.getPackageFilterMeta(collectionId)); return Result.success(collectionService.getPackageFilterMeta(collectionId));

View File

@ -10,7 +10,7 @@ import java.util.List;
/** /**
* 套餐筛选元数据响应 * 套餐筛选元数据响应
* 用于返回套餐下课程包的筛选选项年级主题 * 用于返回套餐下课程包的筛选选项年级课程配置
*/ */
@Data @Data
@Builder @Builder
@ -22,8 +22,8 @@ public class PackageFilterMetaResponse {
@Schema(description = "年级选项列表") @Schema(description = "年级选项列表")
private List<GradeOption> grades; private List<GradeOption> grades;
@Schema(description = "主题选项列表") @Schema(description = "课程配置选项列表(导入课、集体课、健康、科学等)")
private List<ThemeOption> themes; private List<LessonTypeOption> lessonTypes;
/** /**
* 年级选项 * 年级选项
@ -42,21 +42,21 @@ public class PackageFilterMetaResponse {
} }
/** /**
* 主题选项 * 课程配置选项课程环节类型
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Schema(description = "主题选项") @Schema(description = "课程配置选项")
public static class ThemeOption { public static class LessonTypeOption {
@Schema(description = "主题ID") @Schema(description = "课程类型编码,如 INTRODUCTION、COLLECTIVE、HEALTH")
private Long id; private String lessonType;
@Schema(description = "主题名称") @Schema(description = "课程类型中文名称,如 导入课、集体课、健康")
private String name; private String name;
@Schema(description = "该主题下的课程包数量") @Schema(description = "包含该类型环节的课程包数量")
private Integer count; private Integer count;
} }
} }

View File

@ -39,14 +39,14 @@ public interface CourseCollectionService extends IService<CourseCollection> {
* 获取课程套餐下的课程包列表支持筛选 * 获取课程套餐下的课程包列表支持筛选
* @param collectionId 套餐ID * @param collectionId 套餐ID
* @param grade 年级筛选 * @param grade 年级筛选
* @param themeId 主题ID筛选 * @param lessonType 课程配置筛选INTRODUCTIONCOLLECTIVEHEALTHLANGUAGESCIENCESOCIALART
* @param keyword 关键词搜索 * @param keyword 关键词搜索
* @return 课程包列表 * @return 课程包列表
*/ */
List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword); List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword);
/** /**
* 获取套餐的筛选元数据年级主题选项 * 获取套餐的筛选元数据年级课程配置选项
* @param collectionId 套餐ID * @param collectionId 套餐ID
* @return 筛选元数据 * @return 筛选元数据
*/ */

View File

@ -11,7 +11,12 @@ import com.reading.platform.common.response.PageResult;
import com.reading.platform.dto.response.CourseCollectionResponse; import com.reading.platform.dto.response.CourseCollectionResponse;
import com.reading.platform.dto.response.CoursePackageResponse; import com.reading.platform.dto.response.CoursePackageResponse;
import com.reading.platform.dto.response.PackageFilterMetaResponse; 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.mapper.*;
import com.reading.platform.service.CourseCollectionService; import com.reading.platform.service.CourseCollectionService;
import com.reading.platform.service.CourseLessonService; import com.reading.platform.service.CourseLessonService;
@ -208,8 +213,8 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
* 获取课程套餐下的课程包列表支持筛选 * 获取课程套餐下的课程包列表支持筛选
*/ */
@Override @Override
public List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword) { public List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword) {
log.info("获取课程套餐的课程包列表筛选collectionId={}, grade={}, themeId={}, keyword={}", collectionId, grade, themeId, keyword); log.info("获取课程套餐的课程包列表筛选collectionId={}, grade={}, lessonType={}, keyword={}", collectionId, grade, lessonType, keyword);
// 查询关联关系 // 查询关联关系
List<CourseCollectionPackage> associations = collectionPackageMapper.selectList( List<CourseCollectionPackage> associations = collectionPackageMapper.selectList(
@ -227,6 +232,16 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
.map(CourseCollectionPackage::getPackageId) .map(CourseCollectionPackage::getPackageId)
.collect(Collectors.toList()); .collect(Collectors.toList());
// 课程配置筛选仅保留包含该课程环节类型的课程包
if (StringUtils.hasText(lessonType)) {
List<Long> idsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType);
Set<Long> idSet = new HashSet<>(idsWithLesson);
packageIds = packageIds.stream().filter(idSet::contains).collect(Collectors.toList());
if (packageIds.isEmpty()) {
return new ArrayList<>();
}
}
// 构建查询条件 // 构建查询条件
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<CoursePackage>() LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<CoursePackage>()
.in(CoursePackage::getId, packageIds) .in(CoursePackage::getId, packageIds)
@ -237,11 +252,6 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
wrapper.apply("JSON_CONTAINS(grade_tags, {0})", "\"" + grade + "\""); wrapper.apply("JSON_CONTAINS(grade_tags, {0})", "\"" + grade + "\"");
} }
// 主题筛选
if (themeId != null) {
wrapper.eq(CoursePackage::getThemeId, themeId);
}
// 关键词搜索 // 关键词搜索
if (StringUtils.hasText(keyword)) { if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w wrapper.and(w -> w
@ -304,7 +314,7 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
if (associations.isEmpty()) { if (associations.isEmpty()) {
return PackageFilterMetaResponse.builder() return PackageFilterMetaResponse.builder()
.grades(new ArrayList<>()) .grades(new ArrayList<>())
.themes(new ArrayList<>()) .lessonTypes(new ArrayList<>())
.build(); .build();
} }
@ -337,36 +347,70 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
.build()) .build())
.collect(Collectors.toList()); .collect(Collectors.toList());
// 统计主题分布 // 统计课程配置课程环节类型分布查询所有课程环节按规范化类型统计课程包数量
Map<Long, Integer> themeCountMap = new HashMap<>(); List<CourseLesson> allLessons = courseLessonService.list(
Set<Long> themeIds = new HashSet<>(); new LambdaQueryWrapper<CourseLesson>()
for (CoursePackage pkg : packages) { .in(CourseLesson::getCourseId, packageIds)
if (pkg.getThemeId() != null) { .select(CourseLesson::getCourseId, CourseLesson::getLessonType)
themeCountMap.merge(pkg.getThemeId(), 1, Integer::sum); );
themeIds.add(pkg.getThemeId()); // 规范化类型 -> 包含该类型的课程包ID集合
Map<String, Set<Long>> 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<String> lessonTypeOrder = List.of("INTRODUCTION", "COLLECTIVE", "HEALTH", "LANGUAGE", "SCIENCE", "SOCIAL", "ART");
List<PackageFilterMetaResponse.ThemeOption> themes = new ArrayList<>(); Map<String, String> typeToName = getLessonTypeDisplayNames();
if (!themeIds.isEmpty()) { List<PackageFilterMetaResponse.LessonTypeOption> lessonTypes = lessonTypeOrder.stream()
List<Theme> themeList = themeMapper.selectBatchIds(themeIds); .filter(typeToPackageIds::containsKey)
themes = themeList.stream() .map(type -> PackageFilterMetaResponse.LessonTypeOption.builder()
.filter(t -> themeCountMap.containsKey(t.getId())) .lessonType(type)
.map(t -> PackageFilterMetaResponse.ThemeOption.builder() .name(typeToName.getOrDefault(type, type))
.id(t.getId()) .count(typeToPackageIds.get(type).size())
.name(t.getName())
.count(themeCountMap.get(t.getId()))
.build()) .build())
.collect(Collectors.toList()); .collect(Collectors.toList());
}
return PackageFilterMetaResponse.builder() return PackageFilterMetaResponse.builder()
.grades(grades) .grades(grades)
.themes(themes) .lessonTypes(lessonTypes)
.build(); .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<String, String> getLessonTypeDisplayNames() {
Map<String, String> 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;
}
/** /**
* 解析年级标签 * 解析年级标签
*/ */