feat: 课程中心主题筛选改为课程配置筛选,课程包卡片展示课程配置
Made-with: Cursor
This commit is contained in:
parent
4122bcd240
commit
ac8e07c784
@ -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<CoursePackage[]> {
|
||||
|
||||
@ -7,12 +7,9 @@
|
||||
</div>
|
||||
<a-spin :spinning="loadingCollections">
|
||||
<div class="collection-list">
|
||||
<div
|
||||
v-for="collection in collections"
|
||||
:key="collection.id"
|
||||
<div v-for="collection in collections" :key="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-count">{{ collection.packageCount || 0 }}个课程包</div>
|
||||
</div>
|
||||
@ -34,11 +31,7 @@
|
||||
<div ref="descRef" :class="['desc-text', { expanded: descExpanded }]">
|
||||
{{ selectedCollection.description }}
|
||||
</div>
|
||||
<button
|
||||
v-if="showExpandBtn"
|
||||
class="expand-btn"
|
||||
@click="descExpanded = !descExpanded"
|
||||
>
|
||||
<button v-if="showExpandBtn" class="expand-btn" @click="descExpanded = !descExpanded">
|
||||
{{ descExpanded ? '收起' : '展开更多' }}
|
||||
<DownOutlined :class="{ rotated: descExpanded }" />
|
||||
</button>
|
||||
@ -52,18 +45,12 @@
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">年级:</span>
|
||||
<div class="grade-tags">
|
||||
<span
|
||||
:class="['grade-tag', { active: !selectedGrade }]"
|
||||
@click="selectedGrade = ''"
|
||||
>
|
||||
<span :class="['grade-tag', { active: !selectedGrade }]" @click="selectedGrade = ''">
|
||||
全部
|
||||
</span>
|
||||
<span
|
||||
v-for="grade in filterMeta.grades"
|
||||
:key="grade.label"
|
||||
<span v-for="grade in filterMeta.grades" :key="grade.label"
|
||||
:class="['grade-tag', { active: selectedGrade === grade.label }]"
|
||||
@click="selectedGrade = grade.label"
|
||||
>
|
||||
@click="selectedGrade = grade.label">
|
||||
{{ grade.label }}
|
||||
<span class="count">({{ grade.count }})</span>
|
||||
</span>
|
||||
@ -72,36 +59,23 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-row">
|
||||
<!-- 主题筛选 -->
|
||||
<!-- 课程配置筛选 -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">主题:</span>
|
||||
<a-select
|
||||
v-model:value="selectedThemeId"
|
||||
placeholder="全部主题"
|
||||
style="width: 180px"
|
||||
allowClear
|
||||
@change="loadPackages"
|
||||
>
|
||||
<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 }})
|
||||
<span class="filter-label">课程配置:</span>
|
||||
<a-select v-model:value="selectedLessonType" placeholder="全部课程配置" style="width: 180px" allowClear
|
||||
@change="loadPackages">
|
||||
<a-select-option :value="undefined">全部课程配置</a-select-option>
|
||||
<a-select-option v-for="opt in (filterMeta.lessonTypes || [])" :key="opt.lessonType"
|
||||
:value="opt.lessonType">
|
||||
{{ opt.name }} ({{ opt.count }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<div class="filter-group search-group">
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索课程包..."
|
||||
style="width: 220px"
|
||||
allowClear
|
||||
@search="loadPackages"
|
||||
/>
|
||||
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程包..." style="width: 220px" allowClear
|
||||
@search="loadPackages" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -110,13 +84,8 @@
|
||||
<section class="packages-section">
|
||||
<a-spin :spinning="loadingPackages">
|
||||
<div v-if="packages.length > 0" class="packages-grid">
|
||||
<CoursePackageCard
|
||||
v-for="pkg in packages"
|
||||
:key="pkg.id"
|
||||
:pkg="pkg"
|
||||
@click="handlePackageClick"
|
||||
@view="handlePackageView"
|
||||
/>
|
||||
<CoursePackageCard v-for="pkg in packages" :key="pkg.id" :pkg="pkg" @click="handlePackageClick"
|
||||
@view="handlePackageView" />
|
||||
</div>
|
||||
<div v-else class="empty-packages">
|
||||
<InboxOutlined class="empty-icon" />
|
||||
@ -200,13 +169,13 @@ const loadCollections = async () => {
|
||||
loadingCollections.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedLessonType = ref<string | undefined>(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;
|
||||
}
|
||||
|
||||
|
||||
@ -28,10 +28,15 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 主题标签行 -->
|
||||
<div v-if="pkg.themeName" class="tag-row theme-row">
|
||||
<span class="theme-tag">
|
||||
{{ pkg.themeName }}
|
||||
<!-- 课程配置标签行(参考管理端) -->
|
||||
<div v-if="lessonTypes.length > 0" class="tag-row config-row">
|
||||
<span
|
||||
v-for="lt in lessonTypes"
|
||||
:key="lt"
|
||||
class="config-tag"
|
||||
:style="getLessonTagStyle(lt)"
|
||||
>
|
||||
{{ getLessonTypeName(lt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
|
||||
@ -137,16 +137,9 @@
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div class="pagination-wrapper" v-if="!loading && pagination.total > 0">
|
||||
<a-pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
show-size-changer
|
||||
show-quick-jumper
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
/>
|
||||
<a-pagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total" show-size-changer show-quick-jumper :show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageChange" @showSizeChange="handlePageSizeChange" />
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
||||
@ -72,23 +72,23 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-row">
|
||||
<!-- 主题筛选 -->
|
||||
<!-- 课程配置筛选 -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">主题:</span>
|
||||
<span class="filter-label">课程配置:</span>
|
||||
<a-select
|
||||
v-model:value="selectedThemeId"
|
||||
placeholder="全部主题"
|
||||
v-model:value="selectedLessonType"
|
||||
placeholder="全部课程配置"
|
||||
style="width: 180px"
|
||||
allowClear
|
||||
@change="loadPackages"
|
||||
>
|
||||
<a-select-option :value="undefined">全部主题</a-select-option>
|
||||
<a-select-option :value="undefined">全部课程配置</a-select-option>
|
||||
<a-select-option
|
||||
v-for="theme in filterMeta.themes"
|
||||
:key="theme.id"
|
||||
:value="theme.id"
|
||||
v-for="opt in (filterMeta.lessonTypes || [])"
|
||||
:key="opt.lessonType"
|
||||
:value="opt.lessonType"
|
||||
>
|
||||
{{ theme.name }} ({{ theme.count }})
|
||||
{{ opt.name }} ({{ opt.count }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
@ -166,7 +166,7 @@ const selectedCollection = computed(() =>
|
||||
);
|
||||
|
||||
// 筛选元数据
|
||||
const filterMeta = ref<FilterMetaResponse>({ grades: [], themes: [] });
|
||||
const filterMeta = ref<FilterMetaResponse>({ grades: [], lessonTypes: [] });
|
||||
|
||||
// 课程包列表
|
||||
const packages = ref<CoursePackage[]>([]);
|
||||
@ -174,7 +174,7 @@ const loadingPackages = ref(false);
|
||||
|
||||
// 筛选条件
|
||||
const selectedGrade = ref('');
|
||||
const selectedThemeId = ref<number | undefined>(undefined);
|
||||
const selectedLessonType = ref<string | undefined>(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();
|
||||
});
|
||||
|
||||
|
||||
@ -28,10 +28,15 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 主题标签行 -->
|
||||
<div v-if="pkg.themeName" class="tag-row theme-row">
|
||||
<span class="theme-tag">
|
||||
{{ pkg.themeName }}
|
||||
<!-- 课程配置标签行(参考管理端) -->
|
||||
<div v-if="lessonTypes.length > 0" class="tag-row config-row">
|
||||
<span
|
||||
v-for="lt in lessonTypes"
|
||||
:key="lt"
|
||||
class="config-tag"
|
||||
:style="getLessonTagStyle(lt)"
|
||||
>
|
||||
{{ getLessonTypeName(lt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
|
||||
@ -50,13 +50,13 @@ public class SchoolPackageController {
|
||||
public Result<List<CoursePackageResponse>> 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<PackageFilterMetaResponse> getFilterMeta(@PathVariable Long collectionId) {
|
||||
return Result.success(collectionService.getPackageFilterMeta(collectionId));
|
||||
|
||||
@ -10,7 +10,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* 套餐筛选元数据响应
|
||||
* 用于返回套餐下课程包的筛选选项(年级、主题)
|
||||
* 用于返回套餐下课程包的筛选选项(年级、课程配置)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ -22,8 +22,8 @@ public class PackageFilterMetaResponse {
|
||||
@Schema(description = "年级选项列表")
|
||||
private List<GradeOption> grades;
|
||||
|
||||
@Schema(description = "主题选项列表")
|
||||
private List<ThemeOption> themes;
|
||||
@Schema(description = "课程配置选项列表(导入课、集体课、健康、科学等)")
|
||||
private List<LessonTypeOption> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,14 +39,14 @@ public interface CourseCollectionService extends IService<CourseCollection> {
|
||||
* 获取课程套餐下的课程包列表(支持筛选)
|
||||
* @param collectionId 套餐ID
|
||||
* @param grade 年级筛选
|
||||
* @param themeId 主题ID筛选
|
||||
* @param lessonType 课程配置筛选(INTRODUCTION、COLLECTIVE、HEALTH、LANGUAGE、SCIENCE、SOCIAL、ART)
|
||||
* @param keyword 关键词搜索
|
||||
* @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
|
||||
* @return 筛选元数据
|
||||
*/
|
||||
|
||||
@ -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<CourseCollectionMap
|
||||
* 获取课程套餐下的课程包列表(支持筛选)
|
||||
*/
|
||||
@Override
|
||||
public List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword) {
|
||||
log.info("获取课程套餐的课程包列表(筛选),collectionId={}, grade={}, themeId={}, keyword={}", collectionId, grade, themeId, keyword);
|
||||
public List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword) {
|
||||
log.info("获取课程套餐的课程包列表(筛选),collectionId={}, grade={}, lessonType={}, keyword={}", collectionId, grade, lessonType, keyword);
|
||||
|
||||
// 查询关联关系
|
||||
List<CourseCollectionPackage> associations = collectionPackageMapper.selectList(
|
||||
@ -227,6 +232,16 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
.map(CourseCollectionPackage::getPackageId)
|
||||
.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>()
|
||||
.in(CoursePackage::getId, packageIds)
|
||||
@ -237,11 +252,6 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
wrapper.apply("JSON_CONTAINS(grade_tags, {0})", "\"" + grade + "\"");
|
||||
}
|
||||
|
||||
// 主题筛选
|
||||
if (themeId != null) {
|
||||
wrapper.eq(CoursePackage::getThemeId, themeId);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(w -> w
|
||||
@ -304,7 +314,7 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
if (associations.isEmpty()) {
|
||||
return PackageFilterMetaResponse.builder()
|
||||
.grades(new ArrayList<>())
|
||||
.themes(new ArrayList<>())
|
||||
.lessonTypes(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -337,36 +347,70 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 统计主题分布
|
||||
Map<Long, Integer> themeCountMap = new HashMap<>();
|
||||
Set<Long> themeIds = new HashSet<>();
|
||||
for (CoursePackage pkg : packages) {
|
||||
if (pkg.getThemeId() != null) {
|
||||
themeCountMap.merge(pkg.getThemeId(), 1, Integer::sum);
|
||||
themeIds.add(pkg.getThemeId());
|
||||
// 统计课程配置(课程环节类型)分布:查询所有课程环节,按规范化类型统计课程包数量
|
||||
List<CourseLesson> allLessons = courseLessonService.list(
|
||||
new LambdaQueryWrapper<CourseLesson>()
|
||||
.in(CourseLesson::getCourseId, packageIds)
|
||||
.select(CourseLesson::getCourseId, CourseLesson::getLessonType)
|
||||
);
|
||||
// 规范化类型 -> 包含该类型的课程包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<PackageFilterMetaResponse.ThemeOption> themes = new ArrayList<>();
|
||||
if (!themeIds.isEmpty()) {
|
||||
List<Theme> 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<String> lessonTypeOrder = List.of("INTRODUCTION", "COLLECTIVE", "HEALTH", "LANGUAGE", "SCIENCE", "SOCIAL", "ART");
|
||||
Map<String, String> typeToName = getLessonTypeDisplayNames();
|
||||
List<PackageFilterMetaResponse.LessonTypeOption> 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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析年级标签
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user