Merge remote-tracking branch 'origin/master'
# Conflicts: # reading-platform-java/src/main/java/com/reading/platform/service/CourseLessonService.java
This commit is contained in:
commit
a2751d7aa5
@ -375,16 +375,20 @@ export interface Course {
|
|||||||
description?: string;
|
description?: string;
|
||||||
coverUrl?: string;
|
coverUrl?: string;
|
||||||
coverImagePath?: string;
|
coverImagePath?: string;
|
||||||
|
pictureBookName?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
ageRange?: string;
|
ageRange?: string;
|
||||||
difficultyLevel?: string;
|
difficultyLevel?: string;
|
||||||
durationMinutes?: number;
|
durationMinutes?: number;
|
||||||
|
duration?: number;
|
||||||
objectives?: string;
|
objectives?: string;
|
||||||
status: string;
|
status: string;
|
||||||
isSystem: number;
|
isSystem: number;
|
||||||
version?: string;
|
version?: string;
|
||||||
usageCount?: number;
|
usageCount?: number;
|
||||||
teacherCount?: number;
|
teacherCount?: number;
|
||||||
|
gradeTags?: string[];
|
||||||
|
domainTags?: string[];
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
publishedAt?: string;
|
publishedAt?: string;
|
||||||
@ -392,7 +396,9 @@ export interface Course {
|
|||||||
|
|
||||||
export interface SchoolCourseQueryParams {
|
export interface SchoolCourseQueryParams {
|
||||||
keyword?: string;
|
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) =>
|
export const getSchoolCourses = (params?: SchoolCourseQueryParams) =>
|
||||||
|
|||||||
@ -13,9 +13,17 @@ export interface TeacherCourseQueryParams {
|
|||||||
pageNum?: number;
|
pageNum?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
grade?: string;
|
grade?: string;
|
||||||
|
domain?: string;
|
||||||
|
lessonType?: string;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 课程环节标签(列表展示) */
|
||||||
|
export interface LessonTag {
|
||||||
|
name: string;
|
||||||
|
lessonType: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TeacherCourse {
|
export interface TeacherCourse {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -23,6 +31,7 @@ export interface TeacherCourse {
|
|||||||
coverImagePath?: string;
|
coverImagePath?: string;
|
||||||
gradeTags: string[];
|
gradeTags: string[];
|
||||||
domainTags: string[];
|
domainTags: string[];
|
||||||
|
lessonTags?: LessonTag[];
|
||||||
duration: number;
|
duration: number;
|
||||||
avgRating: number;
|
avgRating: number;
|
||||||
usageCount: number;
|
usageCount: number;
|
||||||
@ -62,7 +71,9 @@ export function getTeacherCourses(params: TeacherCourseQueryParams): Promise<{
|
|||||||
pageNum: params.pageNum,
|
pageNum: params.pageNum,
|
||||||
pageSize: params.pageSize,
|
pageSize: params.pageSize,
|
||||||
keyword: params.keyword,
|
keyword: params.keyword,
|
||||||
category: params.grade,
|
grade: params.grade,
|
||||||
|
domain: params.domain,
|
||||||
|
lessonType: params.lessonType,
|
||||||
},
|
},
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
const list = res.list ?? res.records ?? [];
|
const list = res.list ?? res.records ?? [];
|
||||||
|
|||||||
@ -6,114 +6,121 @@
|
|||||||
// 年级标签映射(英文 → 中文)
|
// 年级标签映射(英文 → 中文)
|
||||||
export const GRADE_TAG_MAP: Record<string, string> = {
|
export const GRADE_TAG_MAP: Record<string, string> = {
|
||||||
// 大写格式
|
// 大写格式
|
||||||
SMALL: '小班',
|
SMALL: "小班",
|
||||||
MIDDLE: '中班',
|
MIDDLE: "中班",
|
||||||
BIG: '大班',
|
BIG: "大班",
|
||||||
// 小写格式
|
// 小写格式
|
||||||
small: '小班',
|
small: "小班",
|
||||||
middle: '中班',
|
middle: "中班",
|
||||||
big: '大班',
|
big: "大班",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 领域标签映射(英文 → 中文)- 导出供其他模块使用
|
// 领域标签映射(英文 → 中文)- 导出供其他模块使用
|
||||||
export const DOMAIN_TAG_MAP: Record<string, string> = {
|
export const DOMAIN_TAG_MAP: Record<string, string> = {
|
||||||
// 旧格式(大写)
|
// 旧格式(大写)
|
||||||
LANGUAGE: '语言',
|
LANGUAGE: "语言",
|
||||||
SCIENCE: '科学',
|
SCIENCE: "科学",
|
||||||
SOCIAL: '社会',
|
SOCIAL: "社会",
|
||||||
ART: '艺术',
|
ART: "艺术",
|
||||||
HEALTH: '健康',
|
HEALTH: "健康",
|
||||||
MATH: '数学',
|
MATH: "数学",
|
||||||
|
|
||||||
|
// 课程环节/领域 DOMAIN_ 前缀格式(后端返回)
|
||||||
|
DOMAIN_HEALTH: "健康",
|
||||||
|
DOMAIN_LANGUAGE: "语言",
|
||||||
|
DOMAIN_SOCIAL: "社会",
|
||||||
|
DOMAIN_SCIENCE: "科学",
|
||||||
|
DOMAIN_ART: "艺术",
|
||||||
|
|
||||||
// 活动类型作为领域的兼容映射(历史数据兼容)
|
// 活动类型作为领域的兼容映射(历史数据兼容)
|
||||||
family: '亲子活动',
|
family: "亲子活动",
|
||||||
FAMILY: '亲子活动',
|
FAMILY: "亲子活动",
|
||||||
class: '课堂活动',
|
class: "课堂活动",
|
||||||
CLASS: '课堂活动',
|
CLASS: "课堂活动",
|
||||||
outdoor: '户外活动',
|
outdoor: "户外活动",
|
||||||
OUTDOOR: '户外活动',
|
OUTDOOR: "户外活动",
|
||||||
handicraft: '手工活动',
|
handicraft: "手工活动",
|
||||||
HANDICRAFT: '手工活动',
|
HANDICRAFT: "手工活动",
|
||||||
game: '游戏活动',
|
game: "游戏活动",
|
||||||
GAME: '游戏活动',
|
GAME: "游戏活动",
|
||||||
music: '音乐活动',
|
music: "音乐活动",
|
||||||
MUSIC: '音乐活动',
|
MUSIC: "音乐活动",
|
||||||
exploration: '探索活动',
|
exploration: "探索活动",
|
||||||
EXPLORATION: '探索活动',
|
EXPLORATION: "探索活动",
|
||||||
sports: '运动活动',
|
sports: "运动活动",
|
||||||
SPORTS: '运动活动',
|
SPORTS: "运动活动",
|
||||||
art: '艺术活动',
|
art: "艺术活动",
|
||||||
|
|
||||||
// 新格式(细分领域)
|
// 新格式(细分领域)
|
||||||
// 健康领域
|
// 健康领域
|
||||||
health_motor: '身体动作发展',
|
health_motor: "身体动作发展",
|
||||||
health_hygiene: '生活习惯与能力',
|
health_hygiene: "生活习惯与能力",
|
||||||
HEALTH_MOTOR: '身体动作发展',
|
HEALTH_MOTOR: "身体动作发展",
|
||||||
HEALTH_HYGIENE: '生活习惯与能力',
|
HEALTH_HYGIENE: "生活习惯与能力",
|
||||||
|
|
||||||
// 语言领域
|
// 语言领域
|
||||||
lang_listen: '倾听与表达',
|
lang_listen: "倾听与表达",
|
||||||
lang_read: '早期阅读',
|
lang_read: "早期阅读",
|
||||||
LANG_LISTEN: '倾听与表达',
|
LANG_LISTEN: "倾听与表达",
|
||||||
LANG_READ: '早期阅读',
|
LANG_READ: "早期阅读",
|
||||||
language_communication: '语言',
|
language_communication: "语言",
|
||||||
LANGUAGE_COMMUNICATION: '语言',
|
LANGUAGE_COMMUNICATION: "语言",
|
||||||
|
|
||||||
// 社会领域
|
// 社会领域
|
||||||
social_interact: '人际交往',
|
social_interact: "人际交往",
|
||||||
social_adapt: '社会适应',
|
social_adapt: "社会适应",
|
||||||
SOCIAL_INTERACT: '人际交往',
|
SOCIAL_INTERACT: "人际交往",
|
||||||
SOCIAL_ADAPT: '社会适应',
|
SOCIAL_ADAPT: "社会适应",
|
||||||
social_emotional: '社会',
|
social_emotional: "社会",
|
||||||
SOCIAL_EMOTIONAL: '社会',
|
SOCIAL_EMOTIONAL: "社会",
|
||||||
|
|
||||||
// 科学领域
|
// 科学领域
|
||||||
science_explore: '科学探究',
|
science_explore: "科学探究",
|
||||||
math_cog: '数学认知',
|
math_cog: "数学认知",
|
||||||
SCIENCE_EXPLORE: '科学探究',
|
SCIENCE_EXPLORE: "科学探究",
|
||||||
MATH_COG: '数学认知',
|
MATH_COG: "数学认知",
|
||||||
science_exploration: '科学',
|
science_exploration: "科学",
|
||||||
SCIENCE_EXPLORATION: '科学',
|
SCIENCE_EXPLORATION: "科学",
|
||||||
|
|
||||||
// 艺术领域
|
// 艺术领域
|
||||||
art_music: '音乐表现',
|
art_music: "音乐表现",
|
||||||
art_create: '美术创作',
|
art_create: "美术创作",
|
||||||
ART_MUSIC: '音乐表现',
|
ART_MUSIC: "音乐表现",
|
||||||
ART_CREATE: '美术创作',
|
ART_CREATE: "美术创作",
|
||||||
art_creativity: '艺术',
|
art_creativity: "艺术",
|
||||||
ART_CREATIVITY: '艺术',
|
ART_CREATIVITY: "艺术",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 年级标签颜色配置
|
// 年级标签颜色配置
|
||||||
export const GRADE_TAG_COLORS: Record<string, { bg: string; text: string }> = {
|
export const GRADE_TAG_COLORS: Record<string, { bg: string; text: string }> = {
|
||||||
'小班': { bg: '#FFE4E8', text: '#E85A71' },
|
小班: { bg: "#FFE4E8", text: "#E85A71" },
|
||||||
'中班': { bg: '#E3F2FD', text: '#1976D2' },
|
中班: { bg: "#E3F2FD", text: "#1976D2" },
|
||||||
'大班': { bg: '#FFF8E1', text: '#F9A825' },
|
大班: { bg: "#FFF8E1", text: "#F9A825" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 领域标签颜色配置
|
// 领域标签颜色配置
|
||||||
export const DOMAIN_TAG_COLORS: Record<string, { bg: string; text: string }> = {
|
export const DOMAIN_TAG_COLORS: Record<string, { bg: string; text: string }> = {
|
||||||
'语言': { bg: '#F3E5F5', text: '#8E24AA' },
|
语言: { bg: "#F3E5F5", text: "#8E24AA" },
|
||||||
'倾听与表达': { bg: '#F3E5F5', text: '#8E24AA' },
|
倾听与表达: { bg: "#F3E5F5", text: "#8E24AA" },
|
||||||
'早期阅读': { bg: '#EDE7F6', text: '#7B1FA2' },
|
早期阅读: { bg: "#EDE7F6", text: "#7B1FA2" },
|
||||||
|
|
||||||
'科学': { bg: '#E8F5E9', text: '#43A047' },
|
科学: { bg: "#E8F5E9", text: "#43A047" },
|
||||||
'科学探究': { bg: '#E8F5E9', text: '#43A047' },
|
科学探究: { bg: "#E8F5E9", text: "#43A047" },
|
||||||
'数学认知': { bg: '#F1F8E9', text: '#558B2F' },
|
数学认知: { bg: "#F1F8E9", text: "#558B2F" },
|
||||||
|
|
||||||
'社会': { bg: '#E0F7FA', text: '#0097A7' },
|
社会: { bg: "#E0F7FA", text: "#0097A7" },
|
||||||
'人际交往': { bg: '#E0F7FA', text: '#0097A7' },
|
人际交往: { bg: "#E0F7FA", text: "#0097A7" },
|
||||||
'社会适应': { bg: '#E0F2F1', text: '#00695C' },
|
社会适应: { bg: "#E0F2F1", text: "#00695C" },
|
||||||
|
|
||||||
'艺术': { bg: '#FFF3E0', text: '#FB8C00' },
|
艺术: { bg: "#FFF3E0", text: "#FB8C00" },
|
||||||
'音乐表现': { bg: '#FFF3E0', text: '#FB8C00' },
|
音乐表现: { bg: "#FFF3E0", text: "#FB8C00" },
|
||||||
'美术创作': { bg: '#FBE9E7', text: '#E64A19' },
|
美术创作: { bg: "#FBE9E7", text: "#E64A19" },
|
||||||
|
|
||||||
'健康': { bg: '#FFEBEE', text: '#E53935' },
|
健康: { bg: "#FFEBEE", text: "#E53935" },
|
||||||
'身体动作发展': { bg: '#FFEBEE', text: '#E53935' },
|
身体动作发展: { bg: "#FFEBEE", text: "#E53935" },
|
||||||
'生活习惯与能力': { bg: '#FFCDD2', text: '#C62828' },
|
生活习惯与能力: { 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 } {
|
export function getGradeTagStyle(tag: string): {
|
||||||
const colors = GRADE_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' };
|
background: string;
|
||||||
|
color: string;
|
||||||
|
border: string;
|
||||||
|
} {
|
||||||
|
const colors = GRADE_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" };
|
||||||
return {
|
return {
|
||||||
background: colors.bg,
|
background: colors.bg,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
border: 'none',
|
border: "none",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取领域标签样式
|
* 获取领域标签样式
|
||||||
*/
|
*/
|
||||||
export function getDomainTagStyle(tag: string): { background: string; color: string; border: string } {
|
export function getDomainTagStyle(tag: string): {
|
||||||
const colors = DOMAIN_TAG_COLORS[tag] || { bg: '#F0F0F0', text: '#666' };
|
background: string;
|
||||||
|
color: string;
|
||||||
|
border: string;
|
||||||
|
} {
|
||||||
|
const colors = DOMAIN_TAG_COLORS[tag] || { bg: "#F0F0F0", text: "#666" };
|
||||||
return {
|
return {
|
||||||
background: colors.bg,
|
background: colors.bg,
|
||||||
color: colors.text,
|
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<string, string> = {
|
export const ACTIVITY_TYPE_MAP: Record<string, string> = {
|
||||||
// 大写格式
|
// 大写格式
|
||||||
HANDICRAFT: '手工活动',
|
HANDICRAFT: "手工活动",
|
||||||
GAME: '游戏活动',
|
GAME: "游戏活动",
|
||||||
MUSIC: '音乐活动',
|
MUSIC: "音乐活动",
|
||||||
EXPLORATION: '探索活动',
|
EXPLORATION: "探索活动",
|
||||||
SPORTS: '运动活动',
|
SPORTS: "运动活动",
|
||||||
OUTDOOR: '户外活动',
|
OUTDOOR: "户外活动",
|
||||||
FAMILY: '家庭延伸',
|
FAMILY: "家庭延伸",
|
||||||
ART: '美工活动',
|
ART: "美工活动",
|
||||||
OTHER: '其他',
|
OTHER: "其他",
|
||||||
|
|
||||||
// 小写格式
|
// 小写格式
|
||||||
handicraft: '手工活动',
|
handicraft: "手工活动",
|
||||||
game: '游戏活动',
|
game: "游戏活动",
|
||||||
music: '音乐活动',
|
music: "音乐活动",
|
||||||
exploration: '探索活动',
|
exploration: "探索活动",
|
||||||
sports: '运动活动',
|
sports: "运动活动",
|
||||||
outdoor: '户外活动',
|
outdoor: "户外活动",
|
||||||
family: '家庭延伸',
|
family: "家庭延伸",
|
||||||
art: '美工活动',
|
art: "美工活动",
|
||||||
other: '其他',
|
other: "其他",
|
||||||
|
|
||||||
// 其他格式
|
// 其他格式
|
||||||
class: '课堂活动',
|
class: "课堂活动",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 活动类型颜色配置
|
// 活动类型颜色配置
|
||||||
export const ACTIVITY_TYPE_COLORS: Record<string, { bg: string; text: string }> = {
|
export const ACTIVITY_TYPE_COLORS: Record<
|
||||||
'手工活动': { bg: '#F3E5F5', text: '#8E24AA' },
|
string,
|
||||||
'美工活动': { bg: '#F3E5F5', text: '#8E24AA' },
|
{ bg: string; text: string }
|
||||||
'游戏活动': { bg: '#FFF3E0', text: '#FB8C00' },
|
> = {
|
||||||
'音乐活动': { bg: '#E3F2FD', text: '#1976D2' },
|
手工活动: { bg: "#F3E5F5", text: "#8E24AA" },
|
||||||
'运动活动': { bg: '#E8F5E9', text: '#43A047' },
|
美工活动: { bg: "#F3E5F5", text: "#8E24AA" },
|
||||||
'探索活动': { bg: '#E0F7FA', text: '#0097A7' },
|
游戏活动: { bg: "#FFF3E0", text: "#FB8C00" },
|
||||||
'户外活动': { bg: '#F1F8E9', text: '#558B2F' },
|
音乐活动: { bg: "#E3F2FD", text: "#1976D2" },
|
||||||
'亲子活动': { bg: '#FCE4EC', text: '#C2185B' },
|
运动活动: { bg: "#E8F5E9", text: "#43A047" },
|
||||||
'家庭延伸': { bg: '#E3F2FD', text: '#1976D2' },
|
探索活动: { bg: "#E0F7FA", text: "#0097A7" },
|
||||||
'课堂活动': { bg: '#FFF8E1', text: '#F9A825' },
|
户外活动: { bg: "#F1F8E9", text: "#558B2F" },
|
||||||
'艺术活动': { bg: '#FBE9E7', text: '#E64A19' },
|
亲子活动: { bg: "#FCE4EC", text: "#C2185B" },
|
||||||
'其他': { bg: '#F5F5F5', text: '#666666' },
|
家庭延伸: { 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 } {
|
export function getActivityTypeStyle(type: string): {
|
||||||
const colors = ACTIVITY_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' };
|
background: string;
|
||||||
|
color: string;
|
||||||
|
border: string;
|
||||||
|
} {
|
||||||
|
const colors = ACTIVITY_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" };
|
||||||
return {
|
return {
|
||||||
background: colors.bg,
|
background: colors.bg,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
border: 'none',
|
border: "none",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,50 +267,50 @@ export function translateActivityDomain(domain: string): string {
|
|||||||
// 步骤类型映射(英文 → 中文)
|
// 步骤类型映射(英文 → 中文)
|
||||||
export const STEP_TYPE_MAP: Record<string, string> = {
|
export const STEP_TYPE_MAP: Record<string, string> = {
|
||||||
// 大写格式
|
// 大写格式
|
||||||
TEACHING: '教学',
|
TEACHING: "教学",
|
||||||
READING: '共读',
|
READING: "共读",
|
||||||
DISCUSSION: '讨论',
|
DISCUSSION: "讨论",
|
||||||
ACTIVITY: '活动',
|
ACTIVITY: "活动",
|
||||||
GAME: '游戏',
|
GAME: "游戏",
|
||||||
SUMMARY: '总结',
|
SUMMARY: "总结",
|
||||||
WARMUP: '热身',
|
WARMUP: "热身",
|
||||||
PRACTICE: '练习',
|
PRACTICE: "练习",
|
||||||
INTERACTION: '互动',
|
INTERACTION: "互动",
|
||||||
// 新增环节类型
|
// 新增环节类型
|
||||||
INTRODUCTION: '导入',
|
INTRODUCTION: "导入",
|
||||||
CREATIVE: '创作',
|
CREATIVE: "创作",
|
||||||
CUSTOM: '自定义',
|
CUSTOM: "自定义",
|
||||||
|
|
||||||
// 小写格式
|
// 小写格式
|
||||||
teaching: '教学',
|
teaching: "教学",
|
||||||
reading: '共读',
|
reading: "共读",
|
||||||
discussion: '讨论',
|
discussion: "讨论",
|
||||||
activity: '活动',
|
activity: "活动",
|
||||||
game: '游戏',
|
game: "游戏",
|
||||||
summary: '总结',
|
summary: "总结",
|
||||||
warmup: '热身',
|
warmup: "热身",
|
||||||
practice: '练习',
|
practice: "练习",
|
||||||
interaction: '互动',
|
interaction: "互动",
|
||||||
introduction: '导入',
|
introduction: "导入",
|
||||||
creative: '创作',
|
creative: "创作",
|
||||||
custom: '自定义',
|
custom: "自定义",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 步骤类型颜色配置
|
// 步骤类型颜色配置
|
||||||
export const STEP_TYPE_COLORS: Record<string, { bg: string; text: string }> = {
|
export const STEP_TYPE_COLORS: Record<string, { bg: string; text: string }> = {
|
||||||
'教学': { bg: '#E3F2FD', text: '#1976D2' },
|
教学: { bg: "#E3F2FD", text: "#1976D2" },
|
||||||
'共读': { bg: '#F3E5F5', text: '#8E24AA' },
|
共读: { bg: "#F3E5F5", text: "#8E24AA" },
|
||||||
'阅读': { bg: '#F3E5F5', text: '#8E24AA' },
|
阅读: { bg: "#F3E5F5", text: "#8E24AA" },
|
||||||
'讨论': { bg: '#E0F7FA', text: '#0097A7' },
|
讨论: { bg: "#E0F7FA", text: "#0097A7" },
|
||||||
'活动': { bg: '#FFF3E0', text: '#FB8C00' },
|
活动: { bg: "#FFF3E0", text: "#FB8C00" },
|
||||||
'游戏': { bg: '#FCE4EC', text: '#C2185B' },
|
游戏: { bg: "#FCE4EC", text: "#C2185B" },
|
||||||
'总结': { bg: '#E8F5E9', text: '#43A047' },
|
总结: { bg: "#E8F5E9", text: "#43A047" },
|
||||||
'热身': { bg: '#FFF8E1', text: '#F9A825' },
|
热身: { bg: "#FFF8E1", text: "#F9A825" },
|
||||||
'练习': { bg: '#EDE7F6', text: '#673AB7' },
|
练习: { bg: "#EDE7F6", text: "#673AB7" },
|
||||||
'互动': { bg: '#FBE9E7', text: '#E64A19' },
|
互动: { bg: "#FBE9E7", text: "#E64A19" },
|
||||||
'导入': { bg: '#E8F5E9', text: '#4CAF50' },
|
导入: { bg: "#E8F5E9", text: "#4CAF50" },
|
||||||
'创作': { bg: '#FCE4EC', text: '#E91E63' },
|
创作: { bg: "#FCE4EC", text: "#E91E63" },
|
||||||
'自定义': { bg: '#ECEFF1', text: '#607D8B' },
|
自定义: { 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 } {
|
export function getStepTypeStyle(type: string): {
|
||||||
const colors = STEP_TYPE_COLORS[type] || { bg: '#F0F0F0', text: '#666' };
|
background: string;
|
||||||
|
color: string;
|
||||||
|
border: string;
|
||||||
|
} {
|
||||||
|
const colors = STEP_TYPE_COLORS[type] || { bg: "#F0F0F0", text: "#666" };
|
||||||
return {
|
return {
|
||||||
background: colors.bg,
|
background: colors.bg,
|
||||||
color: colors.text,
|
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<string, string> = {
|
export const COURSE_STATUS_MAP: Record<string, string> = {
|
||||||
DRAFT: '草稿',
|
DRAFT: "草稿",
|
||||||
PENDING: '审核中',
|
PENDING: "审核中",
|
||||||
REJECTED: '已驳回',
|
REJECTED: "已驳回",
|
||||||
PUBLISHED: '已发布',
|
PUBLISHED: "已发布",
|
||||||
ARCHIVED: '已下架',
|
ARCHIVED: "已下架",
|
||||||
REVIEWING: '审核中',
|
REVIEWING: "审核中",
|
||||||
|
|
||||||
// 小写格式
|
// 小写格式
|
||||||
draft: '草稿',
|
draft: "草稿",
|
||||||
pending: '审核中',
|
pending: "审核中",
|
||||||
rejected: '已驳回',
|
rejected: "已驳回",
|
||||||
published: '已发布',
|
published: "已发布",
|
||||||
archived: '已下架',
|
archived: "已下架",
|
||||||
reviewing: '审核中',
|
reviewing: "审核中",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 课程状态颜色配置
|
// 课程状态颜色配置
|
||||||
export const COURSE_STATUS_COLORS: Record<string, { bg: string; text: string }> = {
|
export const COURSE_STATUS_COLORS: Record<
|
||||||
'草稿': { bg: '#F5F5F5', text: '#666666' },
|
string,
|
||||||
'审核中': { bg: '#E3F2FD', text: '#1976D2' },
|
{ bg: string; text: string }
|
||||||
'已驳回': { bg: '#FFEBEE', text: '#E53935' },
|
> = {
|
||||||
'已发布': { bg: '#E8F5E9', text: '#43A047' },
|
草稿: { bg: "#F5F5F5", text: "#666666" },
|
||||||
'已下架': { bg: '#FFF8E1', text: '#F9A825' },
|
审核中: { 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 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 {
|
return {
|
||||||
background: colors.bg,
|
background: colors.bg,
|
||||||
color: colors.text,
|
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<string, string> = {
|
export const RESOURCE_TYPE_MAP: Record<string, string> = {
|
||||||
EBOOK: '电子绘本',
|
EBOOK: "电子绘本",
|
||||||
AUDIO: '音频',
|
AUDIO: "音频",
|
||||||
VIDEO: '视频',
|
VIDEO: "视频",
|
||||||
PPT: 'PPT课件',
|
PPT: "PPT课件",
|
||||||
POSTER: '教学挂图',
|
POSTER: "教学挂图",
|
||||||
OTHER: '其他资源',
|
OTHER: "其他资源",
|
||||||
IMAGE: '图片',
|
IMAGE: "图片",
|
||||||
|
|
||||||
// 已中文的不转换
|
// 已中文的不转换
|
||||||
'电子绘本': '电子绘本',
|
电子绘本: "电子绘本",
|
||||||
'音频': '音频',
|
音频: "音频",
|
||||||
'视频': '视频',
|
视频: "视频",
|
||||||
'PPT课件': 'PPT课件',
|
PPT课件: "PPT课件",
|
||||||
'教学挂图': '教学挂图',
|
教学挂图: "教学挂图",
|
||||||
'其他资源': '其他资源',
|
其他资源: "其他资源",
|
||||||
'图片': '图片',
|
图片: "图片",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -517,22 +517,26 @@ const previewModalVisible = ref(false);
|
|||||||
const previewFileUrl = ref('');
|
const previewFileUrl = ref('');
|
||||||
const previewFileName = ref('');
|
const previewFileName = ref('');
|
||||||
|
|
||||||
// 年级
|
// 年级(gradeTags 规范为 String[],与套餐管理对齐)
|
||||||
const grades = computed(() => {
|
const grades = computed(() => {
|
||||||
if (!course.value.gradeTags) return [];
|
const val = course.value.gradeTags;
|
||||||
|
if (!val) return [];
|
||||||
|
if (Array.isArray(val)) return val;
|
||||||
try {
|
try {
|
||||||
const tags = JSON.parse(course.value.gradeTags);
|
const tags = JSON.parse(val);
|
||||||
return tags;
|
return Array.isArray(tags) ? tags : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 领域标签(核心发展目标,翻译为中文)
|
// 领域标签(domainTags 规范为 String[])
|
||||||
const domainTags = computed(() => {
|
const domainTags = computed(() => {
|
||||||
if (!course.value.domainTags) return [];
|
const val = course.value.domainTags;
|
||||||
|
if (!val) return [];
|
||||||
|
if (Array.isArray(val)) return translateDomainTags(val);
|
||||||
try {
|
try {
|
||||||
const tags = JSON.parse(course.value.domainTags);
|
const tags = JSON.parse(val);
|
||||||
return translateDomainTags(Array.isArray(tags) ? tags : []);
|
return translateDomainTags(Array.isArray(tags) ? tags : []);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -228,7 +228,7 @@ const fetchCourseDetail = async () => {
|
|||||||
// 基本信息
|
// 基本信息
|
||||||
formData.basic.name = course.name;
|
formData.basic.name = course.name;
|
||||||
formData.basic.themeId = course.themeId;
|
formData.basic.themeId = course.themeId;
|
||||||
formData.basic.grades = course.gradeTags ? JSON.parse(course.gradeTags) : [];
|
formData.basic.grades = Array.isArray(course.gradeTags) ? course.gradeTags : (course.gradeTags ? JSON.parse(course.gradeTags) : []);
|
||||||
formData.basic.pictureBookName = course.pictureBookName || '';
|
formData.basic.pictureBookName = course.pictureBookName || '';
|
||||||
formData.basic.coreContent = course.coreContent || '';
|
formData.basic.coreContent = course.coreContent || '';
|
||||||
formData.basic.duration = course.duration || 25;
|
formData.basic.duration = course.duration || 25;
|
||||||
|
|||||||
@ -485,11 +485,12 @@ const iterateCourse = (id: number) => {
|
|||||||
router.push(`/admin/packages/${id}/iterate`);
|
router.push(`/admin/packages/${id}/iterate`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseGradeTags = (gradeTags: string) => {
|
const parseGradeTags = (gradeTags: string | string[] | undefined): string[] => {
|
||||||
if (!gradeTags) return [];
|
if (!gradeTags) return [];
|
||||||
|
if (Array.isArray(gradeTags)) return gradeTags.map((t) => translateGradeTag(t));
|
||||||
try {
|
try {
|
||||||
const tags = JSON.parse(gradeTags);
|
const tags = JSON.parse(gradeTags);
|
||||||
return tags.map((t: string) => translateGradeTag(t));
|
return Array.isArray(tags) ? tags.map((t: string) => translateGradeTag(t)) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -311,11 +311,12 @@ const viewRejectReason = (record: any) => {
|
|||||||
rejectReasonVisible.value = true;
|
rejectReasonVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseGradeTags = (gradeTags: string) => {
|
const parseGradeTags = (gradeTags: string | string[] | undefined): string[] => {
|
||||||
if (!gradeTags) return [];
|
if (!gradeTags) return [];
|
||||||
|
if (Array.isArray(gradeTags)) return gradeTags.map((t) => translateGradeTag(t));
|
||||||
try {
|
try {
|
||||||
const tags = JSON.parse(gradeTags);
|
const tags = JSON.parse(gradeTags);
|
||||||
return tags.map((t: string) => translateGradeTag(t));
|
return Array.isArray(tags) ? tags.map((t: string) => translateGradeTag(t)) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -458,6 +458,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import * as schoolApi from '@/api/school';
|
import * as schoolApi from '@/api/school';
|
||||||
import { translateDomainTags } from '@/utils/tagMaps';
|
import { translateDomainTags } from '@/utils/tagMaps';
|
||||||
|
import { parseGradeLevels } from '@/api/collections';
|
||||||
import FilePreviewModal from '@/components/FilePreviewModal.vue';
|
import FilePreviewModal from '@/components/FilePreviewModal.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -528,29 +529,15 @@ const previewModalVisible = ref(false);
|
|||||||
const previewFileUrl = ref('');
|
const previewFileUrl = ref('');
|
||||||
const previewFileName = ref('');
|
const previewFileName = ref('');
|
||||||
|
|
||||||
// 年级
|
// 年级(统一使用 parseGradeLevels 解析,兼容多种格式)
|
||||||
const grades = computed(() => {
|
const grades = computed(() =>
|
||||||
if (!course.value.gradeTags) return [];
|
parseGradeLevels(course.value.gradeTags ?? course.value.grade_tags)
|
||||||
try {
|
);
|
||||||
const tags = JSON.parse(course.value.gradeTags);
|
|
||||||
return Array.isArray(tags) ? tags : [];
|
|
||||||
} catch {
|
|
||||||
// 可能已经是数组
|
|
||||||
return Array.isArray(course.value.gradeTags) ? course.value.gradeTags : [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 领域标签(核心发展目标,翻译为中文)
|
// 领域标签(核心发展目标,翻译为中文)
|
||||||
const domainTags = computed(() => {
|
const domainTags = computed(() =>
|
||||||
if (!course.value.domainTags) return [];
|
translateDomainTags(parseGradeLevels(course.value.domainTags ?? course.value.domain_tags))
|
||||||
try {
|
);
|
||||||
const tags = JSON.parse(course.value.domainTags);
|
|
||||||
const arr = Array.isArray(tags) ? tags : [];
|
|
||||||
return translateDomainTags(arr);
|
|
||||||
} catch {
|
|
||||||
return Array.isArray(course.value.domainTags) ? translateDomainTags(course.value.domainTags) : [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 是否有课程介绍内容
|
// 是否有课程介绍内容
|
||||||
const hasIntroContent = computed(() => {
|
const hasIntroContent = computed(() => {
|
||||||
|
|||||||
@ -25,28 +25,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 年级切换Tab + 操作栏 -->
|
<!-- 筛选栏(参考教师端) -->
|
||||||
<div class="filter-action-bar">
|
<div class="filter-action-bar">
|
||||||
<div class="grade-tabs">
|
<div class="filter-row">
|
||||||
<span class="tab-label">年级筛选</span>
|
<div class="filter-item">
|
||||||
<div class="tab-buttons">
|
<span class="filter-label">年级</span>
|
||||||
<div v-for="grade in gradeOptions" :key="grade.value" class="grade-tab"
|
<a-select v-model:value="filters.grade" placeholder="全部年级" style="width: 120px;" allowClear
|
||||||
:class="{ active: selectedGrade === grade.value }" @click="handleGradeChange(grade.value)">
|
@change="handleFilterChange">
|
||||||
{{ grade.label }}
|
<a-select-option value="小班">小班</a-select-option>
|
||||||
|
<a-select-option value="中班">中班</a-select-option>
|
||||||
|
<a-select-option value="大班">大班</a-select-option>
|
||||||
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="filter-item">
|
||||||
|
<span class="filter-label">领域</span>
|
||||||
|
<a-select v-model:value="filters.domain" placeholder="全部领域" style="width: 120px;" allowClear
|
||||||
|
@change="handleFilterChange">
|
||||||
|
<a-select-option value="健康">健康</a-select-option>
|
||||||
|
<a-select-option value="语言">语言</a-select-option>
|
||||||
|
<a-select-option value="社会">社会</a-select-option>
|
||||||
|
<a-select-option value="科学">科学</a-select-option>
|
||||||
|
<a-select-option value="艺术">艺术</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</div> -->
|
||||||
|
<div class="filter-item">
|
||||||
|
<span class="filter-label">课程类型</span>
|
||||||
|
<a-select v-model:value="filters.lessonType" placeholder="全部类型" style="width: 130px;" allowClear
|
||||||
|
@change="handleFilterChange">
|
||||||
|
<a-select-option value="INTRODUCTION">导入课</a-select-option>
|
||||||
|
<a-select-option value="COLLECTIVE">集体课</a-select-option>
|
||||||
|
<a-select-option value="LANGUAGE">语言</a-select-option>
|
||||||
|
<a-select-option value="HEALTH">健康</a-select-option>
|
||||||
|
<a-select-option value="SCIENCE">科学</a-select-option>
|
||||||
|
<a-select-option value="SOCIAL">社会</a-select-option>
|
||||||
|
<a-select-option value="ART">艺术</a-select-option>
|
||||||
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-item search-box">
|
||||||
|
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称..." style="width: 240px;"
|
||||||
|
@search="handleFilterChange" allow-clear />
|
||||||
</div>
|
</div>
|
||||||
<div class="action-row">
|
<div class="filter-item filter-right">
|
||||||
<div class="search-box">
|
|
||||||
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程名称" style="width: 280px;"
|
|
||||||
@search="handleSearch" allow-clear />
|
|
||||||
</div>
|
|
||||||
<a-button type="primary" class="auth-btn" @click="showAuthModal">
|
<a-button type="primary" class="auth-btn" @click="showAuthModal">
|
||||||
<StarFilled class="btn-icon" />
|
<StarFilled class="btn-icon" />
|
||||||
授权新课程
|
授权新课程
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 课程卡片网格 -->
|
<!-- 课程卡片网格 -->
|
||||||
<div class="course-grid" v-if="!loading && courses.length > 0">
|
<div class="course-grid" v-if="!loading && courses.length > 0">
|
||||||
@ -69,14 +95,12 @@
|
|||||||
<p class="course-book">《{{ course.pictureBookName }}》</p>
|
<p class="course-book">《{{ course.pictureBookName }}》</p>
|
||||||
|
|
||||||
<div class="course-tags">
|
<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 || [])" :key="'g-' + tag" class="tag grade"
|
||||||
:style="getGradeTagStyle(translateGradeTag(tag))">
|
:style="getGradeTagStyle(tag)">{{ tag }}</span>
|
||||||
{{ translateGradeTag(tag) }}
|
<span v-for="tag in (course.domainTags || [])" :key="'d-' + tag" class="tag domain"
|
||||||
</span>
|
:style="getDomainTagStyle(tag)">{{ tag }}</span>
|
||||||
<span v-for="tag in (course.domainTags || []).slice(0, 2)" :key="tag" class="tag domain"
|
<span v-for="(lt, idx) in (course.lessonTags || [])" :key="'l-' + idx" class="tag lesson"
|
||||||
:style="getDomainTagStyle(translateDomainTag(tag))">
|
:style="getLessonTagStyle(lt.lessonType)">{{ getLessonTypeName(lt.lessonType) }}</span>
|
||||||
{{ translateDomainTag(tag) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="course-meta">
|
<div class="course-meta">
|
||||||
@ -116,7 +140,7 @@
|
|||||||
<div class="empty-icon-wrapper">
|
<div class="empty-icon-wrapper">
|
||||||
<BookOutlined class="empty-icon" />
|
<BookOutlined class="empty-icon" />
|
||||||
</div>
|
</div>
|
||||||
<p>{{ (searchKeyword || selectedGrade) ? '未找到匹配的课程' : '暂无课程数据' }}</p>
|
<p>{{ hasFilters ? '暂无符合条件的课程,试试调整筛选条件吧' : '暂无课程数据' }}</p>
|
||||||
<a-button type="primary" @click="showAuthModal">
|
<a-button type="primary" @click="showAuthModal">
|
||||||
授权第一门课程
|
授权第一门课程
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -139,7 +163,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="auth-content">
|
<div class="auth-content">
|
||||||
<div class="auth-search">
|
<div class="auth-search">
|
||||||
<a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses" size="large" />
|
<a-input-search v-model:value="searchKeyword" placeholder="输入课程名称搜索..." @search="searchCourses"
|
||||||
|
size="large" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="available-courses" v-if="!authLoading && availableCourses.length > 0">
|
<div class="available-courses" v-if="!authLoading && availableCourses.length > 0">
|
||||||
@ -154,9 +179,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="course-info">
|
<div class="course-info">
|
||||||
<div class="course-name-small">{{ course.name }}</div>
|
<div class="course-name-small">{{ course.name }}</div>
|
||||||
<div class="course-book-small">《{{ course.pictureBookName }}》</div>
|
<div class="course-book-small" v-if="course.pictureBookName">《{{ course.pictureBookName }}》</div>
|
||||||
<div class="course-tags-small">
|
<div class="course-tags-small">
|
||||||
<span v-for="tag in course.gradeTags.slice(0, 2)" :key="tag" class="tag-small">
|
<span v-for="tag in (course.gradeTags || [])" :key="tag" class="tag-small">
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -199,11 +224,12 @@ import {
|
|||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
translateGradeTag,
|
translateGradeTags,
|
||||||
translateDomainTag,
|
translateDomainTags,
|
||||||
getGradeTagStyle,
|
getGradeTagStyle,
|
||||||
getDomainTagStyle,
|
getDomainTagStyle,
|
||||||
} from '@/utils/tagMaps';
|
} from '@/utils/tagMaps';
|
||||||
|
import { parseGradeLevels } from '@/api/collections';
|
||||||
import * as schoolApi from '@/api/school';
|
import * as schoolApi from '@/api/school';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -212,42 +238,55 @@ const authLoading = ref(false);
|
|||||||
const authModalVisible = ref(false);
|
const authModalVisible = ref(false);
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref('');
|
||||||
const selectedCourseIds = ref<number[]>([]);
|
const selectedCourseIds = ref<number[]>([]);
|
||||||
const selectedGrade = ref(''); // 选中的年级
|
|
||||||
|
|
||||||
// 解析标签(后端返回 JSON 字符串,需解析为数组)
|
// 筛选条件(参考教师端)
|
||||||
const parseTags = (val: any): string[] => {
|
const filters = reactive({
|
||||||
if (!val) return [];
|
grade: undefined as string | undefined,
|
||||||
if (Array.isArray(val)) return val;
|
domain: undefined as string | undefined,
|
||||||
if (typeof val === 'string') {
|
lessonType: undefined as string | undefined,
|
||||||
try {
|
keyword: '',
|
||||||
const parsed = JSON.parse(val);
|
});
|
||||||
return Array.isArray(parsed) ? parsed : [];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 年级选项
|
|
||||||
const gradeOptions = [
|
|
||||||
{ label: '全部', value: '' },
|
|
||||||
{ label: '小班', value: '小班' },
|
|
||||||
{ label: '中班', value: '中班' },
|
|
||||||
{ label: '大班', value: '大班' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length);
|
const authorizedCount = computed(() => courses.value.filter(c => c.authorized).length);
|
||||||
const totalUsage = computed(() => courses.value.reduce((sum, c) => sum + (c.usageCount || 0), 0));
|
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) => {
|
const DOMAIN_TO_CODE: Record<string, string> = {
|
||||||
selectedGrade.value = value;
|
健康: 'HEALTH',
|
||||||
loadCourses();
|
语言: 'LANGUAGE',
|
||||||
|
社会: 'SOCIAL',
|
||||||
|
科学: 'SCIENCE',
|
||||||
|
艺术: 'ART',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索:请求后端筛选
|
// 课程环节类型映射
|
||||||
const handleSearch = () => {
|
const LESSON_TYPE_NAMES: Record<string, string> = {
|
||||||
|
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<string, { background: string; color: string }> = {
|
||||||
|
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();
|
loadCourses();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -280,22 +319,54 @@ const pagination = reactive({
|
|||||||
const courses = ref<any[]>([]);
|
const courses = ref<any[]>([]);
|
||||||
const availableCourses = ref<any[]>([]);
|
const availableCourses = ref<any[]>([]);
|
||||||
|
|
||||||
// 加载课程列表(支持后端搜索与年级筛选)
|
// 解析标签
|
||||||
|
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 () => {
|
const loadCourses = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params: { keyword?: string; grade?: string } = {};
|
const params: schoolApi.SchoolCourseQueryParams = {};
|
||||||
if (searchKeyword.value?.trim()) params.keyword = searchKeyword.value.trim();
|
if (filters.keyword?.trim()) params.keyword = filters.keyword.trim();
|
||||||
if (selectedGrade.value) params.grade = selectedGrade.value;
|
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);
|
const data = await schoolApi.getSchoolCourses(params);
|
||||||
courses.value = (data || []).map((course: any) => ({
|
courses.value = (data || []).map((course: any) => {
|
||||||
|
const gradeTags = parseTags(course.gradeTags);
|
||||||
|
const domainTags = parseTags(course.domainTags);
|
||||||
|
return {
|
||||||
...course,
|
...course,
|
||||||
gradeTags: parseTags(course.gradeTags),
|
gradeTags: translateGradeTags(gradeTags),
|
||||||
domainTags: parseTags(course.domainTags),
|
domainTags: translateDomainTags(domainTags),
|
||||||
|
lessonTags: course.lessonTags || [],
|
||||||
duration: course.duration ?? course.durationMinutes ?? 0,
|
duration: course.duration ?? course.durationMinutes ?? 0,
|
||||||
pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl,
|
pictureUrl: course.pictureUrl ?? course.coverImagePath ?? course.coverUrl,
|
||||||
authorized: course.authorized ?? true,
|
authorized: course.authorized ?? true,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
pagination.total = courses.value.length;
|
pagination.total = courses.value.length;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error.response?.data?.message || '加载课程列表失败');
|
message.error(error.response?.data?.message || '加载课程列表失败');
|
||||||
@ -462,11 +533,8 @@ onMounted(() => {
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 筛选操作栏 */
|
/* 筛选操作栏(参考教师端布局) */
|
||||||
.filter-action-bar {
|
.filter-action-bar {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
background: white;
|
background: white;
|
||||||
@ -474,51 +542,33 @@ onMounted(() => {
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-tabs {
|
.filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-label {
|
.filter-item {
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-buttons {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-tab {
|
.filter-label {
|
||||||
padding: 8px 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: none;
|
|
||||||
background: #F5F5F5;
|
|
||||||
color: #666;
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-tab:hover {
|
.filter-right {
|
||||||
background: #E8F5E9;
|
margin-left: auto;
|
||||||
color: #43e97b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-tab.active {
|
.search-box {
|
||||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
flex: 1;
|
||||||
color: white;
|
min-width: 200px;
|
||||||
box-shadow: 0 4px 12px rgba(67, 233, 123, 0.3);
|
max-width: 280px;
|
||||||
}
|
|
||||||
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 操作栏 */
|
/* 操作栏 */
|
||||||
|
|||||||
@ -539,27 +539,29 @@ const previewModalVisible = ref(false);
|
|||||||
const previewFileUrl = ref('');
|
const previewFileUrl = ref('');
|
||||||
const previewFileName = ref('');
|
const previewFileName = ref('');
|
||||||
|
|
||||||
// 年级
|
// 年级(gradeTags 规范为 String[],与套餐管理对齐)
|
||||||
const grades = computed(() => {
|
const grades = computed(() => {
|
||||||
if (!course.value.gradeTags) return [];
|
const val = course.value.gradeTags;
|
||||||
|
if (!val) return [];
|
||||||
|
if (Array.isArray(val)) return translateGradeTags(val);
|
||||||
try {
|
try {
|
||||||
const tags = JSON.parse(course.value.gradeTags);
|
const tags = JSON.parse(val);
|
||||||
const translated = Array.isArray(tags) ? tags : [];
|
return translateGradeTags(Array.isArray(tags) ? tags : []);
|
||||||
return translateGradeTags(translated);
|
|
||||||
} catch {
|
} catch {
|
||||||
return Array.isArray(course.value.gradeTags) ? translateGradeTags(course.value.gradeTags) : [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 领域标签(核心发展目标,翻译为中文)
|
// 领域标签(domainTags 规范为 String[])
|
||||||
const domainTags = computed(() => {
|
const domainTags = computed(() => {
|
||||||
if (!course.value.domainTags) return [];
|
const val = course.value.domainTags;
|
||||||
|
if (!val) return [];
|
||||||
|
if (Array.isArray(val)) return translateDomainTags(val);
|
||||||
try {
|
try {
|
||||||
const tags = JSON.parse(course.value.domainTags);
|
const tags = JSON.parse(val);
|
||||||
const arr = Array.isArray(tags) ? tags : [];
|
return translateDomainTags(Array.isArray(tags) ? tags : []);
|
||||||
return translateDomainTags(arr);
|
|
||||||
} catch {
|
} catch {
|
||||||
return Array.isArray(course.value.domainTags) ? translateDomainTags(course.value.domainTags) : [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,8 @@
|
|||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">年级</span>
|
<span class="filter-label">年级</span>
|
||||||
<a-select
|
<a-select v-model:value="filters.grade" placeholder="全部年级" style="width: 120px;" allowClear
|
||||||
v-model:value="filters.grade"
|
@change="handleFilterChange">
|
||||||
placeholder="全部年级"
|
|
||||||
style="width: 120px;"
|
|
||||||
allowClear
|
|
||||||
@change="handleFilterChange"
|
|
||||||
>
|
|
||||||
<a-select-option value="小班">
|
<a-select-option value="小班">
|
||||||
小班
|
小班
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@ -38,42 +33,36 @@
|
|||||||
<a-select-option value="大班">
|
<a-select-option value="大班">
|
||||||
大班
|
大班
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option value="混合">混合</a-select-option>
|
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">领域</span>
|
<span class="filter-label">课程类型</span>
|
||||||
<a-select
|
<a-select v-model:value="filters.lessonType" placeholder="全部类型" style="width: 130px;" allowClear
|
||||||
v-model:value="filters.domain"
|
@change="handleFilterChange">
|
||||||
placeholder="全部领域"
|
<a-select-option value="INTRODUCTION">导入课</a-select-option>
|
||||||
style="width: 120px;"
|
<a-select-option value="COLLECTIVE">集体课</a-select-option>
|
||||||
allowClear
|
<a-select-option value="LANGUAGE">语言</a-select-option>
|
||||||
@change="handleFilterChange"
|
<a-select-option value="HEALTH">健康</a-select-option>
|
||||||
>
|
<a-select-option value="SCIENCE">科学</a-select-option>
|
||||||
<a-select-option value="健康">健康</a-select-option>
|
<a-select-option value="SOCIAL">社会</a-select-option>
|
||||||
<a-select-option value="语言">语言</a-select-option>
|
<a-select-option value="ART">艺术</a-select-option>
|
||||||
<a-select-option value="社会">社会</a-select-option>
|
|
||||||
<a-select-option value="科学">科学</a-select-option>
|
|
||||||
<a-select-option value="艺术">艺术</a-select-option>
|
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-item search-box">
|
<div class="filter-item search-box">
|
||||||
<a-input-search
|
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称..." style="width: 240px;"
|
||||||
v-model:value="filters.keyword"
|
@search="handleFilterChange" />
|
||||||
placeholder="搜索课程名称..."
|
|
||||||
style="width: 240px;"
|
|
||||||
@search="handleFilterChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-item filter-right">
|
<div class="filter-item filter-right">
|
||||||
<a-select
|
<a-select v-model:value="filters.sort" style="width: 130px;" @change="handleFilterChange">
|
||||||
v-model:value="filters.sort"
|
<a-select-option value="popular">
|
||||||
style="width: 130px;"
|
<FireOutlined /> 最受欢迎
|
||||||
@change="handleFilterChange"
|
</a-select-option>
|
||||||
>
|
<a-select-option value="newest">
|
||||||
<a-select-option value="popular"><FireOutlined /> 最受欢迎</a-select-option>
|
<StarOutlined /> 最新发布
|
||||||
<a-select-option value="newest"><StarOutlined /> 最新发布</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option value="rating"><StarFilled /> 评分最高</a-select-option>
|
<a-select-option value="rating">
|
||||||
|
<StarFilled /> 评分最高
|
||||||
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,26 +70,21 @@
|
|||||||
<!-- 课程列表 -->
|
<!-- 课程列表 -->
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<div class="course-grid">
|
<div class="course-grid">
|
||||||
<div
|
<div v-for="course in courses" :key="course.id" class="course-card" @click="viewCourseDetail(course)">
|
||||||
v-for="course in courses"
|
|
||||||
:key="course.id"
|
|
||||||
class="course-card"
|
|
||||||
@click="viewCourseDetail(course)"
|
|
||||||
>
|
|
||||||
<!-- 封面区域 -->
|
<!-- 封面区域 -->
|
||||||
<div class="course-cover">
|
<div class="course-cover">
|
||||||
<img
|
<img v-if="course.pictureUrl" :src="getImageUrl(course.pictureUrl)" class="cover-image" />
|
||||||
v-if="course.pictureUrl"
|
|
||||||
:src="getImageUrl(course.pictureUrl)"
|
|
||||||
class="cover-image"
|
|
||||||
/>
|
|
||||||
<div v-else class="cover-placeholder">
|
<div v-else class="cover-placeholder">
|
||||||
<div class="placeholder-icon"><BookFilled /></div>
|
<div class="placeholder-icon">
|
||||||
|
<BookFilled />
|
||||||
|
</div>
|
||||||
<div class="placeholder-text">精彩绘本</div>
|
<div class="placeholder-text">精彩绘本</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 评分徽章 -->
|
<!-- 评分徽章 -->
|
||||||
<div class="rating-badge" v-if="(course.avgRating ?? 0) > 0">
|
<div class="rating-badge" v-if="(course.avgRating ?? 0) > 0">
|
||||||
<span class="rating-star"><StarFilled /></span>
|
<span class="rating-star">
|
||||||
|
<StarFilled />
|
||||||
|
</span>
|
||||||
<span class="rating-value">{{ (course.avgRating ?? 0).toFixed(1) }}</span>
|
<span class="rating-value">{{ (course.avgRating ?? 0).toFixed(1) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -112,21 +96,15 @@
|
|||||||
<BookOutlined /> {{ course.pictureBookName }}
|
<BookOutlined /> {{ course.pictureBookName }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- 标签区域 -->
|
<!-- 标签区域:年级 + 领域 + 课程环节(lessonTags) -->
|
||||||
<div class="course-tags">
|
<div class="course-tags">
|
||||||
<a-tag
|
<a-tag v-for="tag in (course.gradeTags || [])" :key="'g-' + tag" size="small"
|
||||||
v-for="tag in course.gradeTags"
|
:style="getGradeTagStyle(tag)">{{ tag }}</a-tag>
|
||||||
:key="'g-' + tag"
|
<a-tag v-for="tag in (course.domainTags || [])" :key="'d-' + tag" size="small"
|
||||||
:style="getGradeTagStyle(tag)"
|
:style="getDomainTagStyle(tag)">{{ tag }}</a-tag>
|
||||||
>
|
<a-tag v-for="(lt, idx) in (course.lessonTags || [])" :key="'l-' + idx" size="small"
|
||||||
{{ tag }}
|
:style="getLessonTagStyle(lt.lessonType)">
|
||||||
</a-tag>
|
{{ getLessonTypeName(lt.lessonType) }}
|
||||||
<a-tag
|
|
||||||
v-for="tag in course.domainTags"
|
|
||||||
:key="'d-' + tag"
|
|
||||||
:style="getDomainTagStyle(tag)"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -144,7 +122,9 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<button class="prepare-btn" @click.stop="prepareCourse(course)">
|
<button class="prepare-btn" @click.stop="prepareCourse(course)">
|
||||||
<span class="btn-icon"><EditOutlined /></span>
|
<span class="btn-icon">
|
||||||
|
<EditOutlined />
|
||||||
|
</span>
|
||||||
开始备课
|
开始备课
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -162,13 +142,8 @@
|
|||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-wrapper" v-if="pagination.total > pagination.pageSize">
|
<div class="pagination-wrapper" v-if="pagination.total > pagination.pageSize">
|
||||||
<a-pagination
|
<a-pagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize"
|
||||||
v-model:current="pagination.current"
|
:total="pagination.total" show-less-items @change="handlePageChange" />
|
||||||
v-model:page-size="pagination.pageSize"
|
|
||||||
:total="pagination.total"
|
|
||||||
show-less-items
|
|
||||||
@change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
@ -203,6 +178,7 @@ const loading = ref(false);
|
|||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
grade: undefined as string | undefined,
|
grade: undefined as string | undefined,
|
||||||
domain: undefined as string | undefined,
|
domain: undefined as string | undefined,
|
||||||
|
lessonType: undefined as string | undefined,
|
||||||
keyword: '',
|
keyword: '',
|
||||||
sort: 'popular',
|
sort: 'popular',
|
||||||
});
|
});
|
||||||
@ -215,27 +191,20 @@ const pagination = reactive({
|
|||||||
|
|
||||||
const courses = ref<any[]>([]);
|
const courses = ref<any[]>([]);
|
||||||
|
|
||||||
// 年级映射(用于筛选)
|
// 解析标签(与套餐管理 parseGradeLevels 对齐,兼容多种格式)
|
||||||
const gradeMap: Record<string, string> = {
|
|
||||||
SMALL: '小班', small: '小班',
|
|
||||||
MIDDLE: '中班', middle: '中班',
|
|
||||||
BIG: '大班', big: '大班',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 领域映射(用于筛选)
|
|
||||||
const domainMap: Record<string, string> = {
|
|
||||||
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: '数学',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析标签(后端可能返回 JSON 字符串或数组)
|
|
||||||
const parseTags = (val: any): string[] => {
|
const parseTags = (val: any): string[] => {
|
||||||
if (!val) return [];
|
if (!val) return [];
|
||||||
if (Array.isArray(val)) return val;
|
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') {
|
if (typeof val === 'string') {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(val);
|
const parsed = JSON.parse(val);
|
||||||
@ -263,32 +232,79 @@ const handlePageChange = () => {
|
|||||||
loadCourses();
|
loadCourses();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 课程环节类型:英文码 -> 中文展示名(兼容 INTRODUCTION/INTRO、DOMAIN_* 等后端格式)
|
||||||
|
const LESSON_TYPE_NAMES: Record<string, string> = {
|
||||||
|
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<string, { background: string; color: string }> = {
|
||||||
|
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<string, string> = {
|
||||||
|
健康: 'HEALTH',
|
||||||
|
语言: 'LANGUAGE',
|
||||||
|
社会: 'SOCIAL',
|
||||||
|
科学: 'SCIENCE',
|
||||||
|
艺术: 'ART',
|
||||||
|
};
|
||||||
|
|
||||||
const loadCourses = async () => {
|
const loadCourses = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params: any = {
|
const params: teacherApi.TeacherCourseQueryParams = {
|
||||||
pageNum: pagination.current,
|
pageNum: pagination.current,
|
||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (filters.keyword) {
|
if (filters.keyword?.trim()) {
|
||||||
params.keyword = filters.keyword;
|
params.keyword = filters.keyword.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 年级筛选映射
|
// 年级筛选:直接传小班/中班/大班,后端匹配 gradeTags
|
||||||
if (filters.grade) {
|
if (filters.grade) {
|
||||||
const gradeKey = Object.keys(gradeMap).find((key) => gradeMap[key] === filters.grade);
|
params.grade = filters.grade;
|
||||||
if (gradeKey) {
|
|
||||||
params.grade = gradeKey;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 领域筛选映射
|
// 领域筛选:传英文码,后端匹配 domainTags(五大领域课配置)
|
||||||
if (filters.domain) {
|
if (filters.domain) {
|
||||||
const domainKey = Object.keys(domainMap).find((key) => domainMap[key] === filters.domain);
|
params.domain = DOMAIN_TO_CODE[filters.domain] ?? filters.domain;
|
||||||
if (domainKey) {
|
|
||||||
params.domain = domainKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 课程类型筛选:传 lessonType,后端匹配 course_lesson
|
||||||
|
if (filters.lessonType) {
|
||||||
|
params.lessonType = filters.lessonType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await teacherApi.getTeacherCourses(params);
|
const data = await teacherApi.getTeacherCourses(params);
|
||||||
@ -299,6 +315,7 @@ const loadCourses = async () => {
|
|||||||
...item,
|
...item,
|
||||||
gradeTags: translateGradeTags(gradeTags),
|
gradeTags: translateGradeTags(gradeTags),
|
||||||
domainTags: translateDomainTags(domainTags),
|
domainTags: translateDomainTags(domainTags),
|
||||||
|
lessonTags: item.lessonTags || [],
|
||||||
duration: item.duration ?? item.durationMinutes ?? 0,
|
duration: item.duration ?? item.durationMinutes ?? 0,
|
||||||
usageCount: item.usageCount ?? 0,
|
usageCount: item.usageCount ?? 0,
|
||||||
avgRating: item.avgRating ?? 0,
|
avgRating: item.avgRating ?? 0,
|
||||||
|
|||||||
@ -237,7 +237,7 @@ const loadCourseData = async () => {
|
|||||||
pictureBookName: data.sourceCourse?.name || '',
|
pictureBookName: data.sourceCourse?.name || '',
|
||||||
theme: data.themeId ? { id: data.themeId, name: '' } : null,
|
theme: data.themeId ? { id: data.themeId, name: '' } : null,
|
||||||
coverImagePath: data.coverImagePath,
|
coverImagePath: data.coverImagePath,
|
||||||
gradeTags: data.gradeTags ? JSON.parse(data.gradeTags) : [],
|
gradeTags: Array.isArray(data.gradeTags) ? data.gradeTags : (data.gradeTags ? JSON.parse(data.gradeTags) : []),
|
||||||
domainTags: data.domainTags ? JSON.parse(data.domainTags) : [],
|
domainTags: data.domainTags ? JSON.parse(data.domainTags) : [],
|
||||||
duration: data.duration || 25,
|
duration: data.duration || 25,
|
||||||
coreContent: data.coreContent || '',
|
coreContent: data.coreContent || '',
|
||||||
|
|||||||
@ -265,7 +265,7 @@ const fetchDetail = async () => {
|
|||||||
// 基本信息
|
// 基本信息
|
||||||
formData.basic.name = data.name || '';
|
formData.basic.name = data.name || '';
|
||||||
formData.basic.themeId = data.themeId;
|
formData.basic.themeId = data.themeId;
|
||||||
formData.basic.grades = data.gradeTags ? JSON.parse(data.gradeTags) : [];
|
formData.basic.grades = Array.isArray(data.gradeTags) ? data.gradeTags : (data.gradeTags ? JSON.parse(data.gradeTags) : []);
|
||||||
formData.basic.pictureBookName = '';
|
formData.basic.pictureBookName = '';
|
||||||
formData.basic.coreContent = data.coreContent || data.core_content || '';
|
formData.basic.coreContent = data.coreContent || data.core_content || '';
|
||||||
formData.basic.duration = data.duration || 25;
|
formData.basic.duration = data.duration || 25;
|
||||||
|
|||||||
@ -3,12 +3,14 @@ package com.reading.platform.common.mapper;
|
|||||||
import com.reading.platform.dto.response.CourseResponse;
|
import com.reading.platform.dto.response.CourseResponse;
|
||||||
import com.reading.platform.entity.CoursePackage;
|
import com.reading.platform.entity.CoursePackage;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
import org.mapstruct.factory.Mappers;
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Course Entity Mapper
|
* Course Entity Mapper
|
||||||
|
* gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐
|
||||||
*/
|
*/
|
||||||
@Mapper(componentModel = "spring")
|
@Mapper(componentModel = "spring")
|
||||||
public interface CoursePackageMapper {
|
public interface CoursePackageMapper {
|
||||||
@ -18,6 +20,8 @@ public interface CoursePackageMapper {
|
|||||||
/**
|
/**
|
||||||
* Entity 转 Response
|
* Entity 转 Response
|
||||||
*/
|
*/
|
||||||
|
@Mapping(target = "gradeTags", expression = "java(com.reading.platform.common.util.JsonUtils.parseStringArray(entity.getGradeTags()))")
|
||||||
|
@Mapping(target = "domainTags", expression = "java(com.reading.platform.common.util.JsonUtils.parseStringArray(entity.getDomainTags()))")
|
||||||
CourseResponse toVO(CoursePackage entity);
|
CourseResponse toVO(CoursePackage entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,5 +32,7 @@ public interface CoursePackageMapper {
|
|||||||
/**
|
/**
|
||||||
* Response 转 Entity(用于创建/更新时)
|
* Response 转 Entity(用于创建/更新时)
|
||||||
*/
|
*/
|
||||||
|
@Mapping(target = "gradeTags", expression = "java(vo.getGradeTags() != null ? com.reading.platform.common.util.JsonUtils.toJson(vo.getGradeTags()) : null)")
|
||||||
|
@Mapping(target = "domainTags", expression = "java(vo.getDomainTags() != null ? com.reading.platform.common.util.JsonUtils.toJson(vo.getDomainTags()) : null)")
|
||||||
CoursePackage toEntity(CourseResponse vo);
|
CoursePackage toEntity(CourseResponse vo);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package com.reading.platform.common.util;
|
package com.reading.platform.common.util;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.alibaba.fastjson2.TypeReference;
|
import com.alibaba.fastjson2.TypeReference;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -223,6 +225,25 @@ public class JsonUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 JSON 数组为 String[](与套餐管理 gradeLevels 对齐)
|
||||||
|
* 支持 ["小班","中班"] 或 小班,中班 格式
|
||||||
|
*/
|
||||||
|
public static String[] parseStringArray(String json) {
|
||||||
|
if (!StringUtils.hasText(json)) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (json.trim().startsWith("[")) {
|
||||||
|
return JSON.parseArray(json, String.class).toArray(new String[0]);
|
||||||
|
}
|
||||||
|
return json.split(",");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析 JSON 数组失败: {}", json, e);
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建空的 JSONObject
|
* 创建空的 JSONObject
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
package com.reading.platform.controller.school;
|
package com.reading.platform.controller.school;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.reading.platform.common.response.Result;
|
import com.reading.platform.common.response.Result;
|
||||||
import com.reading.platform.common.security.SecurityUtils;
|
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.entity.CoursePackage;
|
||||||
|
import com.reading.platform.service.CourseLessonService;
|
||||||
import com.reading.platform.service.CoursePackageService;
|
import com.reading.platform.service.CoursePackageService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -15,6 +21,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 课程管理控制器(学校端)
|
* 课程管理控制器(学校端)
|
||||||
|
* gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@ -24,24 +31,119 @@ import java.util.stream.Collectors;
|
|||||||
public class SchoolCourseController {
|
public class SchoolCourseController {
|
||||||
|
|
||||||
private final CoursePackageService courseService;
|
private final CoursePackageService courseService;
|
||||||
|
private final CourseLessonService courseLessonService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "获取学校课程包列表")
|
@Operation(summary = "获取学校课程包列表")
|
||||||
public Result<List<CoursePackage>> getSchoolCourses(
|
public Result<List<SchoolCourseResponse>> getSchoolCourses(
|
||||||
@RequestParam(required = false) String keyword,
|
@RequestParam(required = false) String keyword,
|
||||||
@RequestParam(required = false) String grade) {
|
@RequestParam(required = false) String grade,
|
||||||
log.info("获取学校课程包列表,keyword={}, grade={}", keyword, 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();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
List<CoursePackage> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade);
|
List<CoursePackage> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, lessonType);
|
||||||
return Result.success(courses);
|
List<SchoolCourseResponse> list = courses.stream()
|
||||||
|
.map(pkg -> toSchoolCourseResponse(pkg))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
// 填充 lessonTags
|
||||||
|
for (SchoolCourseResponse vo : list) {
|
||||||
|
List<CourseLesson> 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "获取课程详情")
|
@Operation(summary = "获取课程详情")
|
||||||
public Result<CoursePackage> getSchoolCourse(@PathVariable Long id) {
|
public Result<SchoolCourseResponse> getSchoolCourse(@PathVariable Long id) {
|
||||||
log.info("获取课程详情,id={}", id);
|
log.info("获取课程详情,id={}", id);
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
CoursePackage course = courseService.getCourseByIdWithTenantCheck(id, tenantId);
|
CoursePackage course = courseService.getCourseByIdWithTenantCheck(id, tenantId);
|
||||||
return Result.success(course);
|
return Result.success(toSchoolCourseResponse(course));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为学校端课程响应(gradeTags/domainTags 规范为 String[])
|
||||||
|
*/
|
||||||
|
private SchoolCourseResponse toSchoolCourseResponse(CoursePackage pkg) {
|
||||||
|
return SchoolCourseResponse.builder()
|
||||||
|
.id(pkg.getId())
|
||||||
|
.tenantId(pkg.getTenantId())
|
||||||
|
.name(pkg.getName())
|
||||||
|
.code(pkg.getCode())
|
||||||
|
.description(pkg.getDescription())
|
||||||
|
.pictureBookName(pkg.getPictureBookName())
|
||||||
|
.coverImagePath(pkg.getCoverImagePath())
|
||||||
|
.coverUrl(pkg.getCoverUrl())
|
||||||
|
.gradeTags(parseJsonArray(pkg.getGradeTags()))
|
||||||
|
.domainTags(parseJsonArray(pkg.getDomainTags()))
|
||||||
|
.duration(pkg.getDurationMinutes())
|
||||||
|
.usageCount(pkg.getUsageCount())
|
||||||
|
.teacherCount(pkg.getTeacherCount())
|
||||||
|
.avgRating(pkg.getAvgRating())
|
||||||
|
.status(pkg.getStatus())
|
||||||
|
.createdAt(pkg.getCreatedAt())
|
||||||
|
.updatedAt(pkg.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 JSON 数组为 String[],兼容多种格式:
|
||||||
|
* - 标准 JSON: ["小班","中班"]
|
||||||
|
* - 逗号分隔: 小班,中班
|
||||||
|
* - 错误格式(split 导致): ["[\"小班\"", " \"中班\""] -> 提取有效值
|
||||||
|
*/
|
||||||
|
private String[] parseJsonArray(String json) {
|
||||||
|
if (!StringUtils.hasText(json)) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
String s = json.trim();
|
||||||
|
try {
|
||||||
|
if (s.startsWith("[")) {
|
||||||
|
var list = JSON.parseArray(s, String.class);
|
||||||
|
if (list != null && !list.isEmpty()) {
|
||||||
|
// 检查是否为被错误 split 的格式,如 ["[\"小班\"", " \"中班\""]
|
||||||
|
String first = list.get(0);
|
||||||
|
if (first != null && first.startsWith("[\"") && !first.contains(",")) {
|
||||||
|
return list.stream()
|
||||||
|
.map(String::trim)
|
||||||
|
.map(this::extractQuotedValue)
|
||||||
|
.filter(v -> v != null && !v.isEmpty())
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
return list.stream()
|
||||||
|
.map(v -> v != null ? v.trim() : "")
|
||||||
|
.filter(v -> !v.isEmpty())
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
return java.util.Arrays.stream(s.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(v -> !v.isEmpty())
|
||||||
|
.toArray(String[]::new);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析 JSON 数组失败: {}", json, e);
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractQuotedValue(String s) {
|
||||||
|
if (s == null) return null;
|
||||||
|
s = s.trim();
|
||||||
|
int start = s.indexOf('"');
|
||||||
|
if (start >= 0) {
|
||||||
|
int end = s.indexOf('"', start + 1);
|
||||||
|
if (end > start) {
|
||||||
|
return s.substring(start + 1, end).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.replaceAll("^\\[\"|\"\\]?$", "").trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package com.reading.platform.controller.school;
|
package com.reading.platform.controller.school;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.reading.platform.common.mapper.TeacherMapper;
|
|
||||||
import com.reading.platform.common.response.PageResult;
|
import com.reading.platform.common.response.PageResult;
|
||||||
import com.reading.platform.common.response.Result;
|
import com.reading.platform.common.response.Result;
|
||||||
import com.reading.platform.common.security.SecurityUtils;
|
import com.reading.platform.common.security.SecurityUtils;
|
||||||
@ -25,14 +24,13 @@ import java.util.List;
|
|||||||
public class SchoolTeacherController {
|
public class SchoolTeacherController {
|
||||||
|
|
||||||
private final TeacherService teacherService;
|
private final TeacherService teacherService;
|
||||||
private final TeacherMapper teacherMapper;
|
|
||||||
|
|
||||||
@Operation(summary = "Create teacher")
|
@Operation(summary = "Create teacher")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public Result<TeacherResponse> createTeacher(@Valid @RequestBody TeacherCreateRequest request) {
|
public Result<TeacherResponse> createTeacher(@Valid @RequestBody TeacherCreateRequest request) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
Teacher teacher = teacherService.createTeacher(tenantId, request);
|
Teacher teacher = teacherService.createTeacher(tenantId, request);
|
||||||
return Result.success(teacherMapper.toVO(teacher));
|
return Result.success(teacherService.toTeacherResponse(teacher));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Update teacher")
|
@Operation(summary = "Update teacher")
|
||||||
@ -40,7 +38,7 @@ public class SchoolTeacherController {
|
|||||||
public Result<TeacherResponse> updateTeacher(@PathVariable Long id, @RequestBody TeacherUpdateRequest request) {
|
public Result<TeacherResponse> updateTeacher(@PathVariable Long id, @RequestBody TeacherUpdateRequest request) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
Teacher teacher = teacherService.updateTeacherWithTenantCheck(id, tenantId, request);
|
Teacher teacher = teacherService.updateTeacherWithTenantCheck(id, tenantId, request);
|
||||||
return Result.success(teacherMapper.toVO(teacher));
|
return Result.success(teacherService.toTeacherResponse(teacher));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Get teacher by ID")
|
@Operation(summary = "Get teacher by ID")
|
||||||
@ -48,7 +46,7 @@ public class SchoolTeacherController {
|
|||||||
public Result<TeacherResponse> getTeacher(@PathVariable Long id) {
|
public Result<TeacherResponse> getTeacher(@PathVariable Long id) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
Teacher teacher = teacherService.getTeacherByIdWithTenantCheck(id, tenantId);
|
Teacher teacher = teacherService.getTeacherByIdWithTenantCheck(id, tenantId);
|
||||||
return Result.success(teacherMapper.toVO(teacher));
|
return Result.success(teacherService.toTeacherResponse(teacher));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "Get teacher page")
|
@Operation(summary = "Get teacher page")
|
||||||
@ -60,7 +58,7 @@ public class SchoolTeacherController {
|
|||||||
@RequestParam(required = false) String status) {
|
@RequestParam(required = false) String status) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
Page<Teacher> page = teacherService.getTeacherPage(tenantId, pageNum, pageSize, keyword, status);
|
Page<Teacher> page = teacherService.getTeacherPage(tenantId, pageNum, pageSize, keyword, status);
|
||||||
List<TeacherResponse> voList = teacherMapper.toVO(page.getRecords());
|
List<TeacherResponse> voList = teacherService.toTeacherResponseList(page.getRecords());
|
||||||
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,10 +72,10 @@ public class SchoolTeacherController {
|
|||||||
|
|
||||||
@Operation(summary = "Reset teacher password")
|
@Operation(summary = "Reset teacher password")
|
||||||
@PostMapping("/{id}/reset-password")
|
@PostMapping("/{id}/reset-password")
|
||||||
public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
|
public Result<java.util.Map<String, String>> resetPassword(@PathVariable Long id) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
teacherService.resetPasswordWithTenantCheck(id, tenantId, newPassword);
|
String tempPassword = teacherService.resetPasswordAndReturnTemp(id, tenantId);
|
||||||
return Result.success();
|
return Result.success(java.util.Map.of("tempPassword", tempPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import com.reading.platform.common.response.Result;
|
|||||||
import com.reading.platform.common.security.SecurityUtils;
|
import com.reading.platform.common.security.SecurityUtils;
|
||||||
import com.reading.platform.dto.response.ClassResponse;
|
import com.reading.platform.dto.response.ClassResponse;
|
||||||
import com.reading.platform.dto.response.CourseResponse;
|
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.StudentResponse;
|
||||||
import com.reading.platform.dto.response.TeacherResponse;
|
import com.reading.platform.dto.response.TeacherResponse;
|
||||||
import com.reading.platform.entity.ClassTeacher;
|
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.CoursePackage;
|
||||||
import com.reading.platform.entity.Student;
|
import com.reading.platform.entity.Student;
|
||||||
import com.reading.platform.entity.Teacher;
|
import com.reading.platform.entity.Teacher;
|
||||||
|
import com.reading.platform.entity.CourseLesson;
|
||||||
import com.reading.platform.service.ClassService;
|
import com.reading.platform.service.ClassService;
|
||||||
|
import com.reading.platform.service.CourseLessonService;
|
||||||
import com.reading.platform.service.CoursePackageService;
|
import com.reading.platform.service.CoursePackageService;
|
||||||
import com.reading.platform.service.StudentService;
|
import com.reading.platform.service.StudentService;
|
||||||
import com.reading.platform.service.TeacherService;
|
import com.reading.platform.service.TeacherService;
|
||||||
@ -39,6 +42,7 @@ import java.util.stream.Collectors;
|
|||||||
public class TeacherCourseController {
|
public class TeacherCourseController {
|
||||||
|
|
||||||
private final CoursePackageService courseService;
|
private final CoursePackageService courseService;
|
||||||
|
private final CourseLessonService courseLessonService;
|
||||||
private final ClassService classService;
|
private final ClassService classService;
|
||||||
private final StudentService studentService;
|
private final StudentService studentService;
|
||||||
private final TeacherService teacherService;
|
private final TeacherService teacherService;
|
||||||
@ -67,18 +71,36 @@ public class TeacherCourseController {
|
|||||||
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||||
@RequestParam(required = false) String keyword,
|
@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();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
Page<CoursePackage> page = courseService.getCoursePage(tenantId, pageNum, pageSize, keyword, category, CourseStatus.PUBLISHED.getCode());
|
// 按 学校 -> 套餐 -> 课程包 层级查询,支持 grade/domain/lessonType 筛选
|
||||||
|
Page<CoursePackage> page = courseService.getTenantPackageCoursePage(
|
||||||
|
tenantId, pageNum, pageSize, keyword, grade, domain, lessonType, CourseStatus.PUBLISHED.getCode());
|
||||||
List<CourseResponse> voList = courseMapper.toVO(page.getRecords());
|
List<CourseResponse> voList = courseMapper.toVO(page.getRecords());
|
||||||
|
// 填充 lessonTags(仅 name、lessonType)供列表 tag 展示
|
||||||
|
for (CourseResponse vo : voList) {
|
||||||
|
List<CourseLesson> 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()));
|
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取所有课程")
|
@Operation(summary = "获取所有课程")
|
||||||
@GetMapping("/courses/all")
|
@GetMapping("/courses/all")
|
||||||
public Result<List<CourseResponse>> getAllCourses() {
|
public Result<List<CourseResponse>> getAllCourses(
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String grade,
|
||||||
|
@RequestParam(required = false) String domain) {
|
||||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||||
List<CoursePackage> courses = courseService.getCoursesByTenantId(tenantId);
|
// 按 学校 -> 套餐 -> 课程包 层级查询
|
||||||
|
List<CoursePackage> courses = courseService.getTenantPackageCourses(tenantId, keyword, grade, domain, null);
|
||||||
return Result.success(courseMapper.toVO(courses));
|
return Result.success(courseMapper.toVO(courses));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
package com.reading.platform.dto.request;
|
package com.reading.platform.dto.request;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "教师创建请求")
|
@Schema(description = "教师创建请求")
|
||||||
public class TeacherCreateRequest {
|
public class TeacherCreateRequest {
|
||||||
|
|
||||||
@NotBlank(message = "用户名不能为空")
|
@NotBlank(message = "用户名不能为空")
|
||||||
@Schema(description = "用户名")
|
@JsonAlias("loginAccount")
|
||||||
|
@Schema(description = "用户名/登录账号")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@NotBlank(message = "密码不能为空")
|
@NotBlank(message = "密码不能为空")
|
||||||
@ -20,6 +24,7 @@ public class TeacherCreateRequest {
|
|||||||
@Schema(description = "姓名")
|
@Schema(description = "姓名")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
@Schema(description = "电话")
|
@Schema(description = "电话")
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
@ -32,4 +37,7 @@ public class TeacherCreateRequest {
|
|||||||
@Schema(description = "简介")
|
@Schema(description = "简介")
|
||||||
private String bio;
|
private String bio;
|
||||||
|
|
||||||
|
@Schema(description = "负责班级ID列表")
|
||||||
|
private List<Long> classIds;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package com.reading.platform.dto.request;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "教师更新请求")
|
@Schema(description = "教师更新请求")
|
||||||
public class TeacherUpdateRequest {
|
public class TeacherUpdateRequest {
|
||||||
@ -28,4 +30,7 @@ public class TeacherUpdateRequest {
|
|||||||
@Schema(description = "状态")
|
@Schema(description = "状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "负责班级ID列表")
|
||||||
|
private List<Long> classIds;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,11 +138,11 @@ public class CourseResponse {
|
|||||||
@Schema(description = "评估数据")
|
@Schema(description = "评估数据")
|
||||||
private String assessmentData;
|
private String assessmentData;
|
||||||
|
|
||||||
@Schema(description = "年级标签")
|
@Schema(description = "年级标签(规范为数组,与套餐管理适用年级对齐)")
|
||||||
private String gradeTags;
|
private String[] gradeTags;
|
||||||
|
|
||||||
@Schema(description = "领域标签")
|
@Schema(description = "领域标签(规范为数组)")
|
||||||
private String domainTags;
|
private String[] domainTags;
|
||||||
|
|
||||||
@Schema(description = "是否有集体课")
|
@Schema(description = "是否有集体课")
|
||||||
private Integer hasCollectiveLesson;
|
private Integer hasCollectiveLesson;
|
||||||
@ -197,4 +197,7 @@ public class CourseResponse {
|
|||||||
|
|
||||||
@Schema(description = "关联的课程环节")
|
@Schema(description = "关联的课程环节")
|
||||||
private List<CourseLessonResponse> courseLessons;
|
private List<CourseLessonResponse> courseLessons;
|
||||||
|
|
||||||
|
@Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType)")
|
||||||
|
private List<LessonTagResponse> lessonTags;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.reading.platform.dto.response;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学校端课程响应(gradeTags/domainTags 规范为 String[],与套餐管理适用年级对齐)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "学校端课程响应")
|
||||||
|
public class SchoolCourseResponse {
|
||||||
|
|
||||||
|
@Schema(description = "ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "租户 ID")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "课程名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "课程编码")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "描述")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "绘本名称")
|
||||||
|
private String pictureBookName;
|
||||||
|
|
||||||
|
@Schema(description = "封面图片路径")
|
||||||
|
private String coverImagePath;
|
||||||
|
|
||||||
|
@Schema(description = "封面 URL")
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
@Schema(description = "年级标签(规范为数组)")
|
||||||
|
private String[] gradeTags;
|
||||||
|
|
||||||
|
@Schema(description = "领域标签(规范为数组)")
|
||||||
|
private String[] domainTags;
|
||||||
|
|
||||||
|
@Schema(description = "课程时长(分钟)")
|
||||||
|
private Integer duration;
|
||||||
|
|
||||||
|
@Schema(description = "使用次数")
|
||||||
|
private Integer usageCount;
|
||||||
|
|
||||||
|
@Schema(description = "教师数量")
|
||||||
|
private Integer teacherCount;
|
||||||
|
|
||||||
|
@Schema(description = "平均评分")
|
||||||
|
private BigDecimal avgRating;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Schema(description = "课程环节标签(列表展示用,仅 name 和 lessonType)")
|
||||||
|
private List<LessonTagResponse> lessonTags;
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
package com.reading.platform.dto.response;
|
package com.reading.platform.dto.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 教师响应
|
* 教师响应
|
||||||
@ -21,7 +23,8 @@ public class TeacherResponse {
|
|||||||
@Schema(description = "租户 ID")
|
@Schema(description = "租户 ID")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
|
|
||||||
@Schema(description = "用户名")
|
@JsonProperty("loginAccount")
|
||||||
|
@Schema(description = "登录账号")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
@Schema(description = "姓名")
|
@Schema(description = "姓名")
|
||||||
@ -45,6 +48,15 @@ public class TeacherResponse {
|
|||||||
@Schema(description = "状态")
|
@Schema(description = "状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "负责班级ID列表")
|
||||||
|
private List<Long> classIds;
|
||||||
|
|
||||||
|
@Schema(description = "负责班级名称")
|
||||||
|
private Object classNames;
|
||||||
|
|
||||||
|
@Schema(description = "授课次数")
|
||||||
|
private Integer lessonCount;
|
||||||
|
|
||||||
@Schema(description = "最后登录时间")
|
@Schema(description = "最后登录时间")
|
||||||
private LocalDateTime lastLoginAt;
|
private LocalDateTime lastLoginAt;
|
||||||
|
|
||||||
|
|||||||
@ -61,8 +61,26 @@ public interface CoursePackageService extends com.baomidou.mybatisplus.extension
|
|||||||
*
|
*
|
||||||
* @param tenantId 租户 ID
|
* @param tenantId 租户 ID
|
||||||
* @param keyword 关键词(课程名称、绘本名称,可选)
|
* @param keyword 关键词(课程名称、绘本名称,可选)
|
||||||
* @param grade 年级筛选(小班/中班/大班 或 small/middle/big,可选)
|
* @param grade 年级筛选(小班/中班/大班,可选)
|
||||||
|
* @param domain 领域筛选(健康/语言/社会/科学/艺术 或对应英文码,可选)
|
||||||
|
* @param lessonType 课程环节类型筛选(可选)
|
||||||
*/
|
*/
|
||||||
List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade);
|
List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 学校 -> 套餐 -> 课程包 层级分页查询教师可用课程
|
||||||
|
* 教师端课程中心应使用此方法,通过租户已购买的套餐获取课程包
|
||||||
|
*
|
||||||
|
* @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<CoursePackage> getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize,
|
||||||
|
String keyword, String grade, String domain, String lessonType, String status);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.reading.platform.dto.request.TeacherCreateRequest;
|
import com.reading.platform.dto.request.TeacherCreateRequest;
|
||||||
import com.reading.platform.dto.request.TeacherUpdateRequest;
|
import com.reading.platform.dto.request.TeacherUpdateRequest;
|
||||||
|
import com.reading.platform.dto.response.TeacherResponse;
|
||||||
import com.reading.platform.entity.Teacher;
|
import com.reading.platform.entity.Teacher;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -63,9 +64,24 @@ public interface TeacherService extends IService<Teacher> {
|
|||||||
*/
|
*/
|
||||||
void resetPasswordWithTenantCheck(Long id, Long tenantId, String newPassword);
|
void resetPasswordWithTenantCheck(Long id, Long tenantId, String newPassword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码并返回临时密码(带租户验证)
|
||||||
|
*/
|
||||||
|
String resetPasswordAndReturnTemp(Long id, Long tenantId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 ID 列表查询教师
|
* 根据 ID 列表查询教师
|
||||||
*/
|
*/
|
||||||
List<Teacher> getTeachersByIds(List<Long> teacherIds);
|
List<Teacher> getTeachersByIds(List<Long> teacherIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为教师响应(含班级、授课数等扩展信息)
|
||||||
|
*/
|
||||||
|
TeacherResponse toTeacherResponse(Teacher teacher);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量转换为教师响应
|
||||||
|
*/
|
||||||
|
List<TeacherResponse> toTeacherResponseList(List<Teacher> teachers);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -226,7 +227,7 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade) {
|
public List<CoursePackage> getTenantPackageCourses(Long tenantId, String keyword, String grade, String domain, String lessonType) {
|
||||||
List<Long> collectionIds = tenantPackageMapper.selectList(
|
List<Long> collectionIds = tenantPackageMapper.selectList(
|
||||||
new LambdaQueryWrapper<TenantPackage>()
|
new LambdaQueryWrapper<TenantPackage>()
|
||||||
.eq(TenantPackage::getTenantId, tenantId)
|
.eq(TenantPackage::getTenantId, tenantId)
|
||||||
@ -253,6 +254,16 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
|
|||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lessonType 筛选:仅保留包含该类型环节的课程包
|
||||||
|
if (StringUtils.hasText(lessonType)) {
|
||||||
|
Set<Long> courseIdsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType).stream()
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
packageIds = packageIds.stream().filter(courseIdsWithLesson::contains).collect(Collectors.toList());
|
||||||
|
if (packageIds.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.in(CoursePackage::getId, packageIds)
|
wrapper.in(CoursePackage::getId, packageIds)
|
||||||
.eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode());
|
.eq(CoursePackage::getStatus, CourseStatus.PUBLISHED.getCode());
|
||||||
@ -271,10 +282,96 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
|
|||||||
.or().like(CoursePackage::getGradeTags, "\"" + grade + "\"")
|
.or().like(CoursePackage::getGradeTags, "\"" + grade + "\"")
|
||||||
.or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\""));
|
.or().like(CoursePackage::getGradeTags, "\"" + gradeLower + "\""));
|
||||||
}
|
}
|
||||||
|
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);
|
wrapper.orderByDesc(CoursePackage::getUsageCount);
|
||||||
return coursePackageMapper.selectList(wrapper);
|
return coursePackageMapper.selectList(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<CoursePackage> getTenantPackageCoursePage(Long tenantId, Integer pageNum, Integer pageSize,
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 1. 学校 -> 套餐:查询租户已购买的套餐(collection)
|
||||||
|
List<Long> collectionIds = tenantPackageMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<TenantPackage>()
|
||||||
|
.eq(TenantPackage::getTenantId, tenantId)
|
||||||
|
.eq(TenantPackage::getStatus, TenantPackageStatus.ACTIVE.getCode())
|
||||||
|
.isNotNull(TenantPackage::getCollectionId))
|
||||||
|
.stream()
|
||||||
|
.map(TenantPackage::getCollectionId)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (collectionIds.isEmpty()) {
|
||||||
|
return new Page<>(current, size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 套餐 -> 课程包:查询套餐下的课程包 ID
|
||||||
|
List<Long> packageIds = collectionPackageMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<CourseCollectionPackage>()
|
||||||
|
.in(CourseCollectionPackage::getCollectionId, collectionIds))
|
||||||
|
.stream()
|
||||||
|
.map(CourseCollectionPackage::getPackageId)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (packageIds.isEmpty()) {
|
||||||
|
return new Page<>(current, size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.5 lessonType 筛选:仅保留包含该类型环节的课程包
|
||||||
|
if (StringUtils.hasText(lessonType)) {
|
||||||
|
Set<Long> 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<CoursePackage> page = new Page<>(current, size);
|
||||||
|
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.in(CoursePackage::getId, packageIds)
|
||||||
|
.eq(CoursePackage::getStatus, status != null ? status : CourseStatus.PUBLISHED.getCode());
|
||||||
|
|
||||||
|
if (StringUtils.hasText(keyword)) {
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.like(CoursePackage::getName, keyword)
|
||||||
|
.or().like(CoursePackage::getPictureBookName, keyword)
|
||||||
|
.or().like(CoursePackage::getCode, keyword));
|
||||||
|
}
|
||||||
|
// 年级筛选:匹配 gradeTags(支持中文小班/中班/大班 或英文 SMALL/MIDDLE/BIG)
|
||||||
|
if (StringUtils.hasText(grade)) {
|
||||||
|
String gradeLower = grade.toLowerCase();
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.like(CoursePackage::getGradeTags, grade)
|
||||||
|
.or().like(CoursePackage::getGradeTags, gradeLower)
|
||||||
|
.or().like(CoursePackage::getGradeTags, "\"" + grade + "\"")
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
private CoursePackage buildEntityFromRequest(CourseCreateRequest request) {
|
private CoursePackage buildEntityFromRequest(CourseCreateRequest request) {
|
||||||
CoursePackage entity = new CoursePackage();
|
CoursePackage entity = new CoursePackage();
|
||||||
entity.setName(request.getName());
|
entity.setName(request.getName());
|
||||||
|
|||||||
@ -6,17 +6,28 @@ import com.reading.platform.common.enums.ErrorCode;
|
|||||||
import com.reading.platform.common.exception.BusinessException;
|
import com.reading.platform.common.exception.BusinessException;
|
||||||
import com.reading.platform.dto.request.TeacherCreateRequest;
|
import com.reading.platform.dto.request.TeacherCreateRequest;
|
||||||
import com.reading.platform.dto.request.TeacherUpdateRequest;
|
import com.reading.platform.dto.request.TeacherUpdateRequest;
|
||||||
|
import com.reading.platform.dto.response.TeacherResponse;
|
||||||
|
import com.reading.platform.entity.ClassTeacher;
|
||||||
|
import com.reading.platform.entity.Clazz;
|
||||||
|
import com.reading.platform.entity.Lesson;
|
||||||
import com.reading.platform.entity.Teacher;
|
import com.reading.platform.entity.Teacher;
|
||||||
|
import com.reading.platform.mapper.ClassTeacherMapper;
|
||||||
|
import com.reading.platform.mapper.ClazzMapper;
|
||||||
|
import com.reading.platform.mapper.LessonMapper;
|
||||||
import com.reading.platform.mapper.TeacherMapper;
|
import com.reading.platform.mapper.TeacherMapper;
|
||||||
|
import com.reading.platform.service.ClassService;
|
||||||
import com.reading.platform.service.TeacherService;
|
import com.reading.platform.service.TeacherService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 教师服务实现类
|
* 教师服务实现类
|
||||||
@ -28,6 +39,11 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
|
|||||||
implements TeacherService {
|
implements TeacherService {
|
||||||
|
|
||||||
private final TeacherMapper teacherMapper;
|
private final TeacherMapper teacherMapper;
|
||||||
|
private final com.reading.platform.common.mapper.TeacherMapper teacherVoMapper;
|
||||||
|
private final ClassTeacherMapper classTeacherMapper;
|
||||||
|
private final ClazzMapper clazzMapper;
|
||||||
|
private final LessonMapper lessonMapper;
|
||||||
|
private final ClassService classService;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -57,6 +73,18 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
|
|||||||
|
|
||||||
teacherMapper.insert(teacher);
|
teacherMapper.insert(teacher);
|
||||||
|
|
||||||
|
// 分配教师到班级
|
||||||
|
if (!CollectionUtils.isEmpty(request.getClassIds())) {
|
||||||
|
for (Long classId : request.getClassIds()) {
|
||||||
|
classService.getClassByIdWithTenantCheck(classId, tenantId);
|
||||||
|
ClassTeacher classTeacher = new ClassTeacher();
|
||||||
|
classTeacher.setClassId(classId);
|
||||||
|
classTeacher.setTeacherId(teacher.getId());
|
||||||
|
classTeacher.setRole("MAIN");
|
||||||
|
classTeacherMapper.insert(classTeacher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.info("教师创建成功,ID: {}", teacher.getId());
|
log.info("教师创建成功,ID: {}", teacher.getId());
|
||||||
return teacher;
|
return teacher;
|
||||||
}
|
}
|
||||||
@ -92,6 +120,21 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
|
|||||||
|
|
||||||
teacherMapper.updateById(teacher);
|
teacherMapper.updateById(teacher);
|
||||||
|
|
||||||
|
// 更新教师班级分配
|
||||||
|
if (request.getClassIds() != null) {
|
||||||
|
classTeacherMapper.delete(
|
||||||
|
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getTeacherId, id)
|
||||||
|
);
|
||||||
|
for (Long classId : request.getClassIds()) {
|
||||||
|
classService.getClassByIdWithTenantCheck(classId, teacher.getTenantId());
|
||||||
|
ClassTeacher classTeacher = new ClassTeacher();
|
||||||
|
classTeacher.setClassId(classId);
|
||||||
|
classTeacher.setTeacherId(id);
|
||||||
|
classTeacher.setRole("MAIN");
|
||||||
|
classTeacherMapper.insert(classTeacher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.info("教师更新成功,ID: {}", id);
|
log.info("教师更新成功,ID: {}", id);
|
||||||
return teacher;
|
return teacher;
|
||||||
}
|
}
|
||||||
@ -200,6 +243,18 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
|
|||||||
resetPassword(id, newPassword);
|
resetPassword(id, newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public String resetPasswordAndReturnTemp(Long id, Long tenantId) {
|
||||||
|
log.info("开始重置密码并返回临时密码,ID: {}, tenantId: {}", id, tenantId);
|
||||||
|
Teacher teacher = getTeacherByIdWithTenantCheck(id, tenantId);
|
||||||
|
String tempPassword = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||||
|
teacher.setPassword(passwordEncoder.encode(tempPassword));
|
||||||
|
teacherMapper.updateById(teacher);
|
||||||
|
log.info("密码重置成功,ID: {}", id);
|
||||||
|
return tempPassword;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Teacher> getTeachersByIds(List<Long> teacherIds) {
|
public List<Teacher> getTeachersByIds(List<Long> teacherIds) {
|
||||||
log.debug("根据 ID 列表查询教师,ID 列表:{}", teacherIds);
|
log.debug("根据 ID 列表查询教师,ID 列表:{}", teacherIds);
|
||||||
@ -215,4 +270,53 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
|
|||||||
return teacherMapper.selectList(wrapper);
|
return teacherMapper.selectList(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TeacherResponse toTeacherResponse(Teacher teacher) {
|
||||||
|
if (teacher == null) return null;
|
||||||
|
TeacherResponse response = teacherVoMapper.toVO(teacher);
|
||||||
|
enrichTeacherResponse(response, teacher.getId());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TeacherResponse> toTeacherResponseList(List<Teacher> teachers) {
|
||||||
|
if (teachers == null) return null;
|
||||||
|
List<TeacherResponse> list = new ArrayList<>(teachers.size());
|
||||||
|
for (Teacher teacher : teachers) {
|
||||||
|
list.add(toTeacherResponse(teacher));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enrichTeacherResponse(TeacherResponse response, Long teacherId) {
|
||||||
|
List<ClassTeacher> classTeachers = classTeacherMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getTeacherId, teacherId)
|
||||||
|
);
|
||||||
|
List<Long> classIds = new ArrayList<>();
|
||||||
|
List<String> classNames = new ArrayList<>();
|
||||||
|
for (ClassTeacher ct : classTeachers) {
|
||||||
|
classIds.add(ct.getClassId());
|
||||||
|
Clazz clazz = clazzMapper.selectById(ct.getClassId());
|
||||||
|
if (clazz != null) {
|
||||||
|
classNames.add(clazz.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.setClassIds(classIds);
|
||||||
|
response.setClassNames(classNames.isEmpty() ? null : classNames);
|
||||||
|
|
||||||
|
if ("active".equals(response.getStatus())) {
|
||||||
|
response.setStatus("ACTIVE");
|
||||||
|
}
|
||||||
|
|
||||||
|
long lessonCount = 0;
|
||||||
|
try {
|
||||||
|
lessonCount = lessonMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<Lesson>().eq(Lesson::getTeacherId, teacherId)
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Query lesson count failed: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
response.setLessonCount((int) lessonCount);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user