kindergarten_java/reading-platform-frontend/src/views/teacher/courses-new/CourseCenterView.vue
zhonghua 2d9856edac feat: 课程中心增加课程包主题筛选,过滤导入课/集体课
- 后端: 套餐筛选元数据新增 themes,getPackages 支持 themeId
- 前端: 学校端/教师端课程中心增加课程包主题下拉筛选
- 课程配置筛选和卡片展示均过滤导入课、集体课

Made-with: Cursor
2026-03-23 18:28:54 +08:00

572 lines
14 KiB
Vue

<template>
<div class="course-center-page">
<!-- 左侧套餐列表 -->
<aside class="collection-sidebar">
<div class="sidebar-header">
<h3>课程套餐</h3>
</div>
<a-spin :spinning="loadingCollections">
<div class="collection-list">
<div v-for="collection in collections" :key="collection.id"
:class="['collection-item', { active: selectedCollectionId === collection.id }]"
@click="selectCollection(collection)">
<div class="collection-name">{{ collection.name }}</div>
<div class="collection-count">{{ collection.packageCount || 0 }}个课程包</div>
</div>
<div v-if="!loadingCollections && collections.length === 0" class="empty-collections">
<InboxOutlined />
<p>暂无可用套餐</p>
</div>
</div>
</a-spin>
</aside>
<!-- 右侧主内容区 -->
<main class="main-content">
<template v-if="selectedCollection">
<!-- 套餐信息区 -->
<section class="collection-info">
<h2 class="collection-title">{{ selectedCollection.name }}</h2>
<div v-if="selectedCollection.description" class="collection-description">
<div ref="descRef" :class="['desc-text', { expanded: descExpanded }]">
{{ selectedCollection.description }}
</div>
<button v-if="showExpandBtn" class="expand-btn" @click="descExpanded = !descExpanded">
{{ descExpanded ? '收起' : '展开更多' }}
<DownOutlined :class="{ rotated: descExpanded }" />
</button>
</div>
</section>
<!-- 筛选区 -->
<section class="filter-section">
<div class="filter-row">
<!-- 年级筛选(标签形式) -->
<div class="filter-group">
<span class="filter-label">年级:</span>
<div class="grade-tags">
<span :class="['grade-tag', { active: !selectedGrade }]" @click="selectedGrade = ''">
全部
</span>
<span v-for="grade in filterMeta.grades" :key="grade.label"
:class="['grade-tag', { active: selectedGrade === grade.label }]"
@click="selectedGrade = grade.label">
{{ grade.label }}
<span class="count">({{ grade.count }})</span>
</span>
</div>
</div>
</div>
<div class="filter-row"><!-- 课程包主题筛选 -->
<div class="filter-group">
<span class="filter-label">课程包主题:</span>
<a-select v-model:value="selectedThemeId" placeholder="全部课程包主题" style="width: 180px" allowClear
@change="loadPackages">
<a-select-option :value="undefined">全部课程包主题</a-select-option>
<a-select-option v-for="opt in (filterMeta.themes || [])" :key="opt.themeId" :value="opt.themeId">
{{ opt.name }} ({{ opt.count }})
</a-select-option>
</a-select>
</div>
<!-- 课程配置筛选 -->
<div class="filter-group">
<span class="filter-label">课程配置:</span>
<a-select v-model:value="selectedLessonType" placeholder="全部课程配置" style="width: 180px" allowClear
@change="loadPackages">
<a-select-option :value="undefined">全部课程配置</a-select-option>
<a-select-option v-for="opt in filteredLessonTypes" :key="opt.lessonType"
:value="opt.lessonType">
{{ opt.name }} ({{ opt.count }})
</a-select-option>
</a-select>
</div>
<!-- 搜索 -->
<div class="filter-group search-group">
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程包..." style="width: 220px" allowClear
@search="loadPackages" />
</div>
</div>
</section>
<!-- 课程包网格 -->
<section class="packages-section">
<a-spin :spinning="loadingPackages">
<div v-if="packages.length > 0" class="packages-grid">
<CoursePackageCard v-for="pkg in packages" :key="pkg.id" :pkg="pkg" @click="handlePackageClick"
@prepare="handlePrepare" />
</div>
<div v-else class="empty-packages">
<InboxOutlined class="empty-icon" />
<p class="empty-text">暂无符合条件的课程包</p>
<p class="empty-hint">试试调整筛选条件</p>
</div>
</a-spin>
</section>
</template>
<!-- 未选择套餐时的占位 -->
<div v-else class="no-selection">
<BookOutlined class="no-selection-icon" />
<p>请在左侧选择一个课程套餐</p>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import {
InboxOutlined,
DownOutlined,
BookOutlined,
} from '@ant-design/icons-vue';
import {
getCollections,
getPackages,
getFilterMeta,
type CourseCollection,
type CoursePackage,
type FilterMetaResponse,
} from '@/api/course-center';
import CoursePackageCard from './components/CoursePackageCard.vue';
const router = useRouter();
// 套餐列表
const collections = ref<CourseCollection[]>([]);
const loadingCollections = ref(false);
const selectedCollectionId = ref<number | null>(null);
const selectedCollection = computed(() =>
collections.value.find(c => c.id === selectedCollectionId.value)
);
// 筛选元数据
const filterMeta = ref<FilterMetaResponse>({ grades: [], lessonTypes: [], themes: [] });
// 课程配置筛选选项(过滤导入课、集体课)
const EXCLUDED_LESSON_TYPES = new Set(['INTRODUCTION', 'INTRO', 'COLLECTIVE']);
const filteredLessonTypes = computed(() => {
const list = filterMeta.value.lessonTypes || [];
return list.filter(opt => !EXCLUDED_LESSON_TYPES.has((opt.lessonType || '').toUpperCase()));
});
// 课程包列表
const packages = ref<CoursePackage[]>([]);
const loadingPackages = ref(false);
// 筛选条件
const selectedGrade = ref('');
const selectedLessonType = ref<string | undefined>(undefined);
const selectedThemeId = ref<number | undefined>(undefined);
const searchKeyword = ref('');
// 描述展开
const descRef = ref<HTMLElement | null>(null);
const descExpanded = ref(false);
const showExpandBtn = ref(false);
// 加载套餐列表
const loadCollections = async () => {
loadingCollections.value = true;
try {
const data = await getCollections();
collections.value = data || [];
// 默认选中第一个
if (collections.value.length > 0 && !selectedCollectionId.value) {
selectCollection(collections.value[0]);
}
} catch (error: any) {
message.error(error.message || '获取套餐列表失败');
} finally {
loadingCollections.value = false;
}
};
// 选择套餐
const selectCollection = async (collection: CourseCollection) => {
selectedCollectionId.value = collection.id;
// 重置筛选条件
selectedGrade.value = '';
selectedLessonType.value = undefined;
selectedThemeId.value = undefined;
searchKeyword.value = '';
descExpanded.value = false;
// 加载筛选元数据和课程包
await Promise.all([
loadFilterMeta(),
loadPackages(),
]);
// 检查描述是否需要展开按钮
nextTick(() => {
checkDescHeight();
});
};
// 检查描述高度
const checkDescHeight = () => {
if (descRef.value) {
showExpandBtn.value = descRef.value.scrollHeight > descRef.value.clientHeight;
}
};
// 加载筛选元数据
const loadFilterMeta = async () => {
if (!selectedCollectionId.value) return;
try {
filterMeta.value = await getFilterMeta(selectedCollectionId.value);
} catch (error) {
console.error('获取筛选元数据失败', error);
filterMeta.value = { grades: [], lessonTypes: [], themes: [] };
}
};
// 加载课程包列表
const loadPackages = async () => {
if (!selectedCollectionId.value) return;
loadingPackages.value = true;
try {
packages.value = await getPackages(selectedCollectionId.value, {
grade: selectedGrade.value || undefined,
lessonType: selectedLessonType.value,
themeId: selectedThemeId.value,
keyword: searchKeyword.value || undefined,
});
} catch (error: any) {
message.error(error.message || '获取课程包列表失败');
packages.value = [];
} finally {
loadingPackages.value = false;
}
};
// 点击课程包
const handlePackageClick = (pkg: CoursePackage) => {
router.push(`/teacher/courses/${pkg.id}`);
};
// 开始备课
const handlePrepare = (pkg: CoursePackage) => {
router.push(`/teacher/courses/${pkg.id}/prepare`);
};
// 监听年级、课程配置、主题变化
watch([selectedGrade, selectedLessonType, selectedThemeId], () => {
loadPackages();
});
onMounted(() => {
loadCollections();
});
</script>
<style scoped>
.course-center-page {
display: flex;
min-height: calc(100vh - 120px);
background: #F5F7FA;
gap: 20px;
padding: 20px;
}
/* 左侧套餐列表 */
.collection-sidebar {
width: 220px;
flex-shrink: 0;
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid #F0F0F0;
}
.sidebar-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.collection-list {
padding: 8px;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.collection-item {
padding: 12px 14px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
margin-bottom: 4px;
}
.collection-item:hover {
background: #FFF7E6;
}
.collection-item.active {
background: #FFF7E6;
border-left-color: #FF8C42;
}
.collection-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.collection-count {
font-size: 12px;
color: #999;
}
.empty-collections {
text-align: center;
padding: 40px 20px;
color: #BFBFBF;
}
.empty-collections .anticon {
font-size: 32px;
margin-bottom: 8px;
}
/* 右侧主内容 */
.main-content {
flex: 1;
min-width: 0;
}
/* 套餐信息区 */
.collection-info {
background: #fff;
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.collection-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0 0 12px;
}
.collection-description {
background: #FAFAFA;
border-radius: 8px;
padding: 12px 16px;
}
.desc-text {
font-size: 14px;
color: #666;
line-height: 1.6;
max-height: 44px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.desc-text.expanded {
max-height: 500px;
}
.expand-btn {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 8px;
padding: 0;
border: none;
background: none;
color: #FF8C42;
font-size: 13px;
cursor: pointer;
}
.expand-btn .anticon {
transition: transform 0.3s ease;
}
.expand-btn .anticon.rotated {
transform: rotate(180deg);
}
/* 筛选区 */
.filter-section {
background: #fff;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filter-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.filter-row+.filter-row {
margin-top: 12px;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 14px;
color: #666;
flex-shrink: 0;
}
.grade-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.grade-tag {
display: inline-flex;
align-items: center;
padding: 4px 14px;
border-radius: 16px;
font-size: 13px;
color: #666;
background: #F5F5F5;
cursor: pointer;
transition: all 0.2s ease;
}
.grade-tag:hover {
background: #FFF7E6;
color: #FF8C42;
}
.grade-tag.active {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
color: #fff;
}
.grade-tag .count {
font-size: 12px;
margin-left: 2px;
opacity: 0.8;
}
.search-group {
margin-left: auto;
}
/* 课程包网格 */
.packages-section {
background: #fff;
border-radius: 12px;
padding: 20px;
min-height: 400px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.packages-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
/* 空状态 */
.empty-packages {
text-align: center;
padding: 80px 20px;
}
.empty-icon {
font-size: 64px;
color: #D9D9D9;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #666;
margin: 0 0 8px;
}
.empty-hint {
font-size: 14px;
color: #999;
margin: 0;
}
/* 未选择套餐 */
.no-selection {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 500px;
background: #fff;
border-radius: 12px;
color: #BFBFBF;
}
.no-selection-icon {
font-size: 64px;
margin-bottom: 16px;
}
/* 响应式 */
@media (max-width: 768px) {
.course-center-page {
flex-direction: column;
padding: 12px;
}
.collection-sidebar {
width: 100%;
order: 1;
}
.main-content {
order: 2;
}
.packages-grid {
grid-template-columns: 1fr;
}
.filter-row {
flex-direction: column;
align-items: flex-start;
}
.search-group {
margin-left: 0;
width: 100%;
}
.search-group :deep(.ant-input-search) {
width: 100% !important;
}
}
</style>