- 后端: 套餐筛选元数据新增 themes,getPackages 支持 themeId - 前端: 学校端/教师端课程中心增加课程包主题下拉筛选 - 课程配置筛选和卡片展示均过滤导入课、集体课 Made-with: Cursor
590 lines
14 KiB
Vue
590 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"
|
|
@view="handlePackageView" />
|
|
</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 { getCoursePackageDetail } from '@/api/school';
|
|
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 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 selectedLessonType = ref<string | undefined>(undefined);
|
|
// 选择套餐
|
|
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;
|
|
}
|
|
};
|
|
|
|
// 监听年级、课程配置、主题变化
|
|
watch([selectedGrade, selectedLessonType, selectedThemeId], () => {
|
|
loadPackages();
|
|
});
|
|
|
|
// 点击课程包
|
|
const handlePackageClick = (pkg: CoursePackage) => {
|
|
// 跳转到课程详情页
|
|
router.push(`/school/courses/${pkg.id}`);
|
|
};
|
|
|
|
// 查看课程包详情
|
|
const handlePackageView = (pkg: CoursePackage) => {
|
|
router.push(`/school/courses/${pkg.id}`);
|
|
};
|
|
|
|
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: #F5F7FA;
|
|
}
|
|
|
|
.collection-item.active {
|
|
background: #E6F7FF;
|
|
border-left-color: #1890ff;
|
|
}
|
|
|
|
.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 16px;
|
|
color: #BFBFBF;
|
|
}
|
|
|
|
/* 右侧主内容区 */
|
|
.main-content {
|
|
flex: 1;
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
min-height: calc(100vh - 160px);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
/* 套餐信息区 */
|
|
.collection-info {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.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: #1890ff;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.expand-btn .anticon {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.expand-btn .anticon.rotated {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* 筛选区 */
|
|
.filter-section {
|
|
background: #FAFAFA;
|
|
border-radius: 12px;
|
|
padding: 16px 20px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.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: #E6F7FF;
|
|
color: #1890ff;
|
|
}
|
|
|
|
.grade-tag.active {
|
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
|
color: #fff;
|
|
}
|
|
|
|
.grade-tag .count {
|
|
font-size: 12px;
|
|
margin-left: 2px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.search-group {
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* 课程包网格 */
|
|
.packages-section {
|
|
min-height: 400px;
|
|
}
|
|
|
|
.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;
|
|
color: #BFBFBF;
|
|
}
|
|
|
|
.no-selection-icon {
|
|
font-size: 64px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* 详情抽屉样式 */
|
|
.cover-preview {
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.cover-preview h4,
|
|
.lessons-section h4 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.lessons-section {
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.lesson-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.lesson-name {
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
/* 响应式 */
|
|
@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>
|