学校课程管理: 前后端对齐,实现搜索与年级筛选功能
Made-with: Cursor
This commit is contained in:
parent
6b5d0e171b
commit
7fe2c319ee
@ -380,8 +380,13 @@ export interface Course {
|
||||
publishedAt?: string;
|
||||
}
|
||||
|
||||
export const getSchoolCourses = () =>
|
||||
http.get<Course[]>('/v1/school/courses');
|
||||
export interface SchoolCourseQueryParams {
|
||||
keyword?: string;
|
||||
grade?: string; // 小班|中班|大班 或 small|middle|big
|
||||
}
|
||||
|
||||
export const getSchoolCourses = (params?: SchoolCourseQueryParams) =>
|
||||
http.get<Course[]>('/v1/school/courses', { params });
|
||||
|
||||
export const getSchoolCourse = (id: number) =>
|
||||
http.get<Course>(`/v1/school/courses/${id}`);
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
<span class="tab-label">年级筛选</span>
|
||||
<div class="tab-buttons">
|
||||
<div v-for="grade in gradeOptions" :key="grade.value" class="grade-tab"
|
||||
:class="{ active: selectedGrade === grade.value }" @click="selectedGrade = grade.value">
|
||||
:class="{ active: selectedGrade === grade.value }" @click="handleGradeChange(grade.value)">
|
||||
{{ grade.label }}
|
||||
</div>
|
||||
</div>
|
||||
@ -49,8 +49,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 课程卡片网格 -->
|
||||
<div class="course-grid" v-if="!loading && filteredCourses.length > 0">
|
||||
<div v-for="course in filteredCourses" :key="course.id" class="course-card"
|
||||
<div class="course-grid" v-if="!loading && courses.length > 0">
|
||||
<div v-for="course in courses" :key="course.id" class="course-card"
|
||||
:class="{ 'unauthorized': !course.authorized }">
|
||||
<div class="card-cover">
|
||||
<div class="cover-placeholder" v-if="!course.pictureUrl">
|
||||
@ -69,11 +69,11 @@
|
||||
<p class="course-book">《{{ course.pictureBookName }}》</p>
|
||||
|
||||
<div class="course-tags">
|
||||
<span v-for="tag in course.gradeTags.slice(0, 2)" :key="tag" class="tag grade"
|
||||
<span v-for="tag in (course.gradeTags || []).slice(0, 2)" :key="tag" class="tag grade"
|
||||
:style="getGradeTagStyle(translateGradeTag(tag))">
|
||||
{{ translateGradeTag(tag) }}
|
||||
</span>
|
||||
<span v-for="tag in course.domainTags.slice(0, 2)" :key="tag" class="tag domain"
|
||||
<span v-for="tag in (course.domainTags || []).slice(0, 2)" :key="tag" class="tag domain"
|
||||
:style="getDomainTagStyle(translateDomainTag(tag))">
|
||||
{{ translateDomainTag(tag) }}
|
||||
</span>
|
||||
@ -112,11 +112,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state" v-if="!loading && filteredCourses.length === 0">
|
||||
<div class="empty-state" v-if="!loading && courses.length === 0">
|
||||
<div class="empty-icon-wrapper">
|
||||
<BookOutlined class="empty-icon" />
|
||||
</div>
|
||||
<p>{{ searchKeyword ? '未找到匹配的课程' : '暂无课程数据' }}</p>
|
||||
<p>{{ (searchKeyword || selectedGrade) ? '未找到匹配的课程' : '暂无课程数据' }}</p>
|
||||
<a-button type="primary" @click="showAuthModal">
|
||||
授权第一门课程
|
||||
</a-button>
|
||||
@ -214,6 +214,21 @@ const searchKeyword = ref('');
|
||||
const selectedCourseIds = ref<number[]>([]);
|
||||
const selectedGrade = ref(''); // 选中的年级
|
||||
|
||||
// 解析标签(后端返回 JSON 字符串,需解析为数组)
|
||||
const parseTags = (val: any): string[] => {
|
||||
if (!val) return [];
|
||||
if (Array.isArray(val)) return val;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 年级选项
|
||||
const gradeOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
@ -223,35 +238,17 @@ const gradeOptions = [
|
||||
];
|
||||
|
||||
const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length);
|
||||
const totalUsage = computed(() => courses.value.reduce((sum, c) => sum + c.usageCount, 0));
|
||||
const totalUsage = computed(() => courses.value.reduce((sum, c) => sum + (c.usageCount || 0), 0));
|
||||
|
||||
// 过滤后的课程列表(用于搜索和年级筛选)
|
||||
const filteredCourses = computed(() => {
|
||||
let result = courses.value;
|
||||
// 年级切换:请求后端筛选
|
||||
const handleGradeChange = (value: string) => {
|
||||
selectedGrade.value = value;
|
||||
loadCourses();
|
||||
};
|
||||
|
||||
// 按年级筛选
|
||||
if (selectedGrade.value) {
|
||||
result = result.filter(c => {
|
||||
const gradeTags = c.gradeTags || [];
|
||||
return gradeTags.some((tag: string) => translateGradeTag(tag) === selectedGrade.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
result = result.filter(c =>
|
||||
c.name.toLowerCase().includes(keyword) ||
|
||||
c.pictureBookName.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// 年级切换处理
|
||||
const handleGradeChange = () => {
|
||||
// 年级切换时自动触发筛选
|
||||
// 搜索:请求后端筛选
|
||||
const handleSearch = () => {
|
||||
loadCourses();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
@ -283,15 +280,21 @@ const pagination = reactive({
|
||||
const courses = ref<any[]>([]);
|
||||
const availableCourses = ref<any[]>([]);
|
||||
|
||||
// 加载课程列表
|
||||
// 加载课程列表(支持后端搜索与年级筛选)
|
||||
const loadCourses = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await schoolApi.getSchoolCourses();
|
||||
courses.value = data.map((course: any) => ({
|
||||
const params: { keyword?: string; grade?: string } = {};
|
||||
if (searchKeyword.value?.trim()) params.keyword = searchKeyword.value.trim();
|
||||
if (selectedGrade.value) params.grade = selectedGrade.value;
|
||||
const data = await schoolApi.getSchoolCourses(params);
|
||||
courses.value = (data || []).map((course: any) => ({
|
||||
...course,
|
||||
gradeTags: course.gradeTags || [],
|
||||
domainTags: course.domainTags || [],
|
||||
gradeTags: parseTags(course.gradeTags),
|
||||
domainTags: parseTags(course.domainTags),
|
||||
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) {
|
||||
@ -301,10 +304,6 @@ const loadCourses = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
// 搜索功能已通过 filteredCourses computed 实现
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
|
||||
@ -27,10 +27,12 @@ public class SchoolCourseController {
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取学校课程包列表")
|
||||
public Result<List<Course>> getSchoolCourses() {
|
||||
log.info("获取学校课程包列表");
|
||||
public Result<List<Course>> getSchoolCourses(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String grade) {
|
||||
log.info("获取学校课程包列表,keyword={}, grade={}", keyword, grade);
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
List<Course> courses = courseService.getTenantPackageCourses(tenantId);
|
||||
List<Course> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade);
|
||||
return Result.success(courses);
|
||||
}
|
||||
|
||||
|
||||
@ -53,7 +53,11 @@ public interface CourseService extends com.baomidou.mybatisplus.extension.servic
|
||||
|
||||
/**
|
||||
* 查询租户套餐下的课程
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @param keyword 关键词(课程名称、绘本名称,可选)
|
||||
* @param grade 年级筛选(小班/中班/大班 或 small/middle/big,可选)
|
||||
*/
|
||||
List<Course> getTenantPackageCourses(Long tenantId);
|
||||
List<Course> getTenantPackageCourses(Long tenantId, String keyword, String grade);
|
||||
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import com.reading.platform.dto.response.CourseResponse;
|
||||
import com.reading.platform.dto.response.LessonStepResponse;
|
||||
import com.reading.platform.entity.Course;
|
||||
import com.reading.platform.entity.CourseLesson;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.reading.platform.entity.CoursePackage;
|
||||
import com.reading.platform.entity.CoursePackageCourse;
|
||||
import com.reading.platform.entity.LessonStep;
|
||||
@ -479,8 +480,8 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Course> getTenantPackageCourses(Long tenantId) {
|
||||
log.info("查询租户套餐下的课程,tenantId={}", tenantId);
|
||||
public List<Course> getTenantPackageCourses(Long tenantId, String keyword, String grade) {
|
||||
log.info("查询租户套餐下的课程,tenantId={}, keyword={}, grade={}", tenantId, keyword, grade);
|
||||
|
||||
// 1. 查询租户的套餐关联
|
||||
List<TenantPackage> tenantPackages = tenantPackageMapper.selectList(
|
||||
@ -538,10 +539,55 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
|
||||
.ne(Course::getStatus, CourseStatus.ARCHIVED.getCode())
|
||||
);
|
||||
|
||||
// 7. 按关键词和年级筛选
|
||||
String kw = StringUtils.hasText(keyword) ? keyword.trim().toLowerCase() : null;
|
||||
List<String> gradeKeys = parseGradeFilter(grade);
|
||||
if (kw != null || !gradeKeys.isEmpty()) {
|
||||
courses = courses.stream()
|
||||
.filter(c -> matchKeyword(c, kw))
|
||||
.filter(c -> matchGrade(c, gradeKeys))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
log.info("查询租户套餐下的课程包成功,tenantId={}, count={}", tenantId, courses.size());
|
||||
return courses;
|
||||
}
|
||||
|
||||
/** 解析年级筛选:小班/中班/大班 -> small,middle,big(含大小写) */
|
||||
private List<String> parseGradeFilter(String grade) {
|
||||
if (!StringUtils.hasText(grade)) return List.of();
|
||||
String g = grade.trim();
|
||||
return switch (g) {
|
||||
case "小班" -> List.of("small", "SMALL");
|
||||
case "中班" -> List.of("middle", "MIDDLE");
|
||||
case "大班" -> List.of("big", "BIG");
|
||||
case "small", "SMALL" -> List.of("small", "SMALL");
|
||||
case "middle", "MIDDLE" -> List.of("middle", "MIDDLE");
|
||||
case "big", "BIG" -> List.of("big", "BIG");
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
private boolean matchKeyword(Course c, String keyword) {
|
||||
if (keyword == null) return true;
|
||||
String name = c.getName() != null ? c.getName().toLowerCase() : "";
|
||||
String book = c.getPictureBookName() != null ? c.getPictureBookName().toLowerCase() : "";
|
||||
return name.contains(keyword) || book.contains(keyword);
|
||||
}
|
||||
|
||||
private boolean matchGrade(Course c, List<String> gradeKeys) {
|
||||
if (gradeKeys.isEmpty()) return true;
|
||||
String tags = c.getGradeTags();
|
||||
if (!StringUtils.hasText(tags)) return false;
|
||||
try {
|
||||
List<String> list = JSON.parseArray(tags, String.class);
|
||||
if (list == null) return false;
|
||||
return list.stream().anyMatch(gradeKeys::contains);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将空字符串转为 null,避免 MySQL JSON 列报错(空串不是有效 JSON)
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user