2025-10-15 21:11:45 +08:00

929 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- 课程筛选标签 -->
<div class="text-wrapper_1 flex-row">
<span
class="text_12"
:class="{ active: activeCourseTab === 'all' }"
@click="handleCourseTabChange('all')"
>全部课程</span
>
<span
class="text_13"
:class="{ active: activeCourseTab === 'learning' }"
@click="handleCourseTabChange('learning')"
>学习中</span
>
<span
class="text_14"
:class="{ active: activeCourseTab === 'completed' }"
@click="handleCourseTabChange('completed')"
>已完结</span
>
</div>
<!-- 分割线 -->
<div class="course-divider"></div>
<!-- 课程列表 -->
<div class="course-list">
<!-- 加载状态 -->
<div v-if="loading" class="loading-wrapper">
<n-spin size="medium" />
</div>
<!-- 课程卡片 -->
<div
v-else-if="filteredCourses.length > 0"
v-for="course in filteredCourses"
:key="course.id"
class="box_2 flex-row justify-between"
>
<div class="block_4 flex-col">
<div class="box_3 flex-row justify-between">
<div class="status-image-container">
<img
class="status-image"
referrerpolicy="no-referrer"
:src="
getCourseStatusClass(course) === 'learning'
? '/images/icon/learning.png'
: '/images/icon/finish.png'
"
:alt="getStatusText(course)"
/>
</div>
<img
class="thumbnail_4"
referrerpolicy="no-referrer"
:src="course.thumbnail"
:alt="course.name"
/>
<span :class="['status-text', getCourseStatusClass(course)]">{{
getStatusText(course)
}}</span>
</div>
</div>
<div class="block_5 flex-col">
<div class="group_6 flex-row">
<span class="text_16">{{ course.title }}</span>
<n-icon size="1.04vw" class="thumbnail_5">
<PersonOutline />
</n-icon>
<span class="text_17">{{ course.enrollmentCount || 0 }}</span>
</div>
<span class="text_18"
>讲师:{{
getInstructorNames(course.teacherList) || "暂无讲师"
}}</span
>
<span class="text_19">{{
formatDescription(course.description)
}}</span>
<div class="group_7 flex-row">
<img
class="thumbnail_6"
referrerpolicy="no-referrer"
src="/images/profile/11.png"
/>
<span class="text_20"
>共{{ course.chapterCount || 0 }}章{{
course.sectionCount || 0
}}节</span
>
<img
class="thumbnail_7"
referrerpolicy="no-referrer"
src="/images/profile/22.png"
/>
<span class="text_21"
>{{
course.startDate
? new Date(course.startDate).toLocaleDateString()
: "时间待定"
}}
-
{{
course.endDate
? new Date(course.endDate).toLocaleDateString()
: "时间待定"
}}</span
>
<!-- <img
class="thumbnail_8"
referrerpolicy="no-referrer"
src="/images/profile/33.png"
/>
<span class="text_22">{{ course.enrollmentCount }}</span> -->
<div
class="text-wrapper_2 flex-col"
@click="goToCourseDetail(course)"
>
<span class="text_23">{{
getCourseStatusClass(course) === "learning"
? "去学习"
: getCourseStatusClass(course) === "completed"
? "去复习"
: "去报名"
}}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<n-empty description="暂无课程数据"> </n-empty>
</div>
</div>
<!-- 分页器 -->
<div class="pagination-wrapper" v-if="totalPages > 1">
<div class="pagination">
<span
class="pagination-item nav-button"
:class="{ disabled: currentPage === 1 }"
@click="goToPage('first')"
>
首页
</span>
<span
class="pagination-item nav-button"
:class="{ disabled: currentPage === 1 }"
@click="goToPage('prev')"
>
上一页
</span>
<span
v-for="page in totalPages"
:key="page"
class="pagination-item page-number"
:class="{ active: page === currentPage }"
@click="goToPage(page)"
>
{{ page }}
</span>
<span
class="pagination-item nav-button"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage('next')"
>
下一页
</span>
<span
class="pagination-item nav-button"
:class="{ disabled: currentPage === totalPages }"
@click="goToPage('last')"
>
尾页
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
// import { useI18n } from "vue-i18n";
import { useMessage } from "naive-ui";
import { useRouter } from "vue-router";
import { PersonOutline } from "@vicons/ionicons5";
import { CourseApi } from "@/api/modules/course";
import { TeachCourseApi } from "@/api/modules/teachCourse";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const router = useRouter();
// const { locale } = useI18n();
const message = useMessage();
// 定义章节接口
interface Section {
id: string;
name: string;
level: number; // 1为章2为节
parentId?: string;
courseId: string;
sortOrder?: number;
}
// 定义教师接口
interface Teacher {
id: string;
name: string;
avatar: string;
title: string;
tag: string;
sortOrder: number;
}
// 定义课程接口
interface Course {
id: string;
name: string;
cover: string;
video: string;
school: string;
description: string;
type: number;
target: string;
difficulty: number;
subject: string;
outline: string;
prerequisite: string;
reference: string;
arrangement: string;
startTime: string;
endTime: string;
enrollCount: number;
maxEnroll: number;
status: number;
question: string;
izAi: number;
pauseExit: number;
allowSpeed: number;
showSubtitle: number;
publishStatus: number;
semester: string | null;
allowDownload: number;
createBy: string;
createTime: string;
updateBy: string;
updateTime: string;
teacherList: Teacher[];
isEnrolled: boolean;
// 新增章节信息
chapterCount?: number; // 章数
sectionCount?: number; // 节数
sections?: Section[]; // 章节列表
}
// 课程筛选状态
const activeCourseTab = ref("all");
// 分页相关状态
const currentPage = ref(1);
const pageSize = ref(10); // 每页显示5个课程
// 课程数据状态
const courses = ref<any[]>([]);
const loading = ref(false);
const total = ref(0);
// banner图片
// const bannerImage = computed(() => {
// return locale.value === "zh"
// ? "/banners/banner8.png"
// : "/banners/banner1-en.png";
// });
// const bannerAlt = computed(() => {
// return t('home.banner.alt')
// })
// 获取课程章节信息
const getCourseSections = async (courseId: string) => {
try {
const response = await TeachCourseApi.getCourseSections(courseId);
return response.data.result || [];
} catch (error) {
console.error(`获取课程${courseId}章节信息失败:`, error);
return [];
}
};
// 计算章节数量
const calculateChapterAndSectionCount = (sections: any) => {
const chapterCount = sections.filter(
(section: any) => section.level === 1
).length;
const sectionCount = sections.filter(
(section: any) => section.level === 2
).length;
return { chapterCount, sectionCount };
};
// 获取课程数据(一次性获取所有数据)
const fetchCourses = async () => {
try {
loading.value = true;
// 移除分页参数,一次性获取所有课程
const response = await CourseApi.getCourses({}, true);
console.log("Fetched courses:", response.data);
if (response.data && Array.isArray(response.data)) {
// 为每个课程获取章节信息
const coursesWithSections = await Promise.all(
response.data.map(async (course: any) => {
// 获取章节信息
const sections = await getCourseSections(course.id);
const { chapterCount, sectionCount } =
calculateChapterAndSectionCount(sections);
return {
...course,
sections,
chapterCount,
sectionCount,
};
})
);
courses.value = coursesWithSections;
total.value = coursesWithSections.length;
console.log("Courses with sections:", coursesWithSections);
} else {
courses.value = [];
total.value = 0;
}
} catch (error) {
message.error("获取课程数据失败请稍后重试");
console.error("Error fetching courses:", error);
courses.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 获取筛选后的所有课程
const allFilteredCourses = computed(() => {
if (activeCourseTab.value === "learning") {
// 筛选学习中的课程
return courses.value.filter(
(course) => getCourseStatusClass(course) === "learning"
);
} else if (activeCourseTab.value === "completed") {
// 筛选已完结的课程(时间区间已结束)
return courses.value.filter(
(course) => getCourseStatusClass(course) === "completed"
);
}
return courses.value;
});
// 计算总页数(基于筛选后的数据)
const totalPages = computed(() => {
return Math.ceil(allFilteredCourses.value.length / pageSize.value);
});
// 当前页显示的课程(前端分页)
const filteredCourses = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return allFilteredCourses.value.slice(start, end);
});
// 获取状态文本
const getStatusText = (course: Course) => {
const now = new Date();
const endTime = new Date(course.endTime);
if (course.isEnrolled) {
if (endTime < now) {
return "已完结";
} else {
return "学习中";
}
}
return "未报名";
};
// 获取课程状态类名
const getCourseStatusClass = (course: Course) => {
const now = new Date();
const endTime = new Date(course.endTime);
if (course.isEnrolled) {
if (endTime < now) {
return "completed";
} else {
return "learning";
}
}
return "not-enrolled";
};
// 获取教师名称
const getInstructorNames = (teacherList: Teacher[]) => {
return teacherList.map((teacher) => teacher.name).join(" ");
};
// 格式化课程描述移除HTML标签
const formatDescription = (description: string) => {
return description.replace(/<[^>]*>/g, "").substring(0, 200) + "...";
};
// 跳转到课程详情页
const goToCourseDetail = async (course: Course) => {
try {
// 检查用户是否已登录
if (!userStore.isLoggedIn) {
console.log("用户未登录跳转到AI伴学页面");
router.push(`/ai-companion?courseId=${course.id}`);
return;
}
console.log("检查课程报名状态课程ID:", course.id);
// 调用报名状态检查接口
const response = await CourseApi.checkEnrollmentStatus(String(course.id));
if ((response.code === 0 || response.code === 200) && response.data) {
const isEnrolled = response.data.result;
if (isEnrolled) {
// 已报名,跳转到已兑换页面
console.log("用户已报名跳转到已兑换页面");
router.push(`/course/${course.id}/exchanged`);
} else {
// 未报名跳转到AI伴学页面
console.log("用户未报名跳转到AI伴学页面");
router.push(`/ai-companion?courseId=${course.id}`);
}
} else {
// 查询失败默认跳转到AI伴学页面
console.warn("查询报名状态失败跳转到AI伴学页面");
router.push(`/ai-companion?courseId=${course.id}`);
}
} catch (error) {
console.error("检查报名状态时发生错误:", error);
// 发生错误时默认跳转到AI伴学页面
router.push(`/ai-companion?courseId=${course.id}`);
}
};
// 监听筛选变化,重置到第一页(不重新调用接口)
const handleCourseTabChange = (tab: string) => {
activeCourseTab.value = tab;
currentPage.value = 1; // 切换tab时重置到第一页
};
// 分页器方法前端分页不调用API
const goToPage = (page: number | string) => {
let newPage = currentPage.value;
if (typeof page === "number") {
if (page >= 1 && page <= totalPages.value) {
newPage = page;
}
} else {
switch (page) {
case "first":
if (currentPage.value > 1) {
newPage = 1;
}
break;
case "prev":
if (currentPage.value > 1) {
newPage = currentPage.value - 1;
}
break;
case "next":
if (currentPage.value < totalPages.value) {
newPage = currentPage.value + 1;
}
break;
case "last":
if (currentPage.value < totalPages.value) {
newPage = totalPages.value;
}
break;
}
}
if (newPage !== currentPage.value) {
currentPage.value = newPage;
// 不再调用fetchCourses(),纯前端分页
}
};
onMounted(async () => {
await fetchCourses();
});
</script>
<style scoped>
.text-wrapper_1 {
display: flex;
gap: 2.08vw;
padding-bottom: 1.04vh;
}
.text_12,
.text_13,
.text_14,
.text_15 {
font-size: 0.94vw;
color: #000;
font-family: "Microsoft YaHei", Arial, sans-serif;
font-weight: normal;
cursor: pointer;
padding: 0.42vh 0;
transition: color 0.3s ease;
border-radius: 0.31vw;
}
.text_12.active,
.text_13.active,
.text_14.active,
.text_15.active {
color: rgba(2, 134, 206, 1);
}
.text_12:hover,
.text_13:hover,
.text_14:hover,
.text_15:hover {
color: rgba(2, 134, 206, 1);
}
.course-divider {
width: 100%;
height: 1.5px;
background: #e6e6e6;
margin-bottom: 1.67vh;
}
.course-list {
display: flex;
flex-direction: column;
gap: 1.25vh;
}
.box_2 {
width: 100%;
min-height: 10.42vh;
background: rgba(255, 255, 255, 1);
border: none;
border-radius: 0.6vw;
padding: 1.04vh 1.04vw;
transition: all 0.3s ease;
}
.box_2:hover {
box-shadow: 0 0.21vh 1.04vh rgba(0, 0, 0, 0.1);
transform: translateY(-0.1vh);
}
.block_4 {
margin-right: 1.04vw;
}
.box_3 {
width: 202px;
height: 156px;
position: relative;
border-radius: 5px;
overflow: hidden;
background: rgba(243, 243, 243, 1);
box-shadow: 0 0.1vh 0.31vh rgba(0, 0, 0, 0.1);
}
.thumbnail_4 {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 5px;
}
.status-image-container {
position: absolute;
top: 0;
left: 0;
z-index: 10;
}
.status-image {
width: 66px;
height: 22px;
}
.status-text {
position: absolute;
top: 26vh;
right: 0.63vw;
color: #fff;
font-size: 0.6vw;
font-family: "Microsoft YaHei", Arial, sans-serif;
font-weight: normal;
z-index: 12;
padding: 0.21vh 0.52vw;
border-radius: 0.21vw;
background: rgba(0, 0, 0, 0.5);
}
.status-text.learning {
background: rgba(2, 134, 206, 0.8);
}
.status-text.completed {
background: rgba(76, 175, 80, 0.8);
}
.status-text.not-enrolled {
background: rgba(158, 158, 158, 0.8);
}
.block_5 {
flex: 1;
display: flex;
flex-direction: column;
}
.group_6 {
align-items: center;
margin-bottom: 0.42vh;
}
.text_16 {
font-size: 1.04vw;
font-weight: bold;
color: #333;
font-family: "Microsoft YaHei", Arial, sans-serif;
flex: 1;
margin-right: 0.52vw;
}
.thumbnail_5 {
width: 1.04vw;
height: 1.04vw;
margin-right: 0.26vw;
}
.text_17 {
font-size: 0.78vw;
color: #666;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
.text_18 {
font-size: 0.78vw;
color: #999;
font-family: "Microsoft YaHei", Arial, sans-serif;
margin-bottom: 0.42vh;
}
.text_19 {
font-size: 0.73vw;
color: #666;
font-family: "Microsoft YaHei", Arial, sans-serif;
margin-bottom: 0.63vh;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.group_7 {
align-items: center;
gap: 0.52vw;
}
.thumbnail_6,
.thumbnail_7,
.thumbnail_8 {
width: 0.83vw;
height: 0.83vw;
}
.text_20,
.text_21,
.text_22 {
font-size: 0.68vw;
color: #999;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
.text-wrapper_2 {
margin-left: auto;
background: rgba(2, 134, 206, 1);
border-radius: 0.31vw;
padding: 0.31vh 0.78vw;
cursor: pointer;
transition: all 0.3s ease;
}
.text-wrapper_2:hover {
background: rgba(1, 120, 185, 1);
}
.text_23 {
font-size: 0.73vw;
color: #fff;
font-family: "Microsoft YaHei", Arial, sans-serif;
font-weight: normal;
}
.pagination-wrapper {
width: 100%;
display: flex;
justify-content: center;
margin-top: 4.17vh;
padding: 1.04vh 0;
}
.pagination {
display: flex;
align-items: center;
gap: 0.52vw;
}
.pagination-item {
padding: 0.52vh 0.78vw;
font-size: 0.73vw;
color: #333;
cursor: pointer;
border-radius: 0.26vw;
transition: all 0.3s ease;
border: 1px solid #ddd;
background: #fff;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
.pagination-item:hover {
color: rgba(2, 134, 206, 1);
border-color: rgba(2, 134, 206, 1);
}
.pagination-item.active {
background: rgba(2, 134, 206, 1);
color: #fff;
border-color: rgba(2, 134, 206, 1);
}
.pagination-item.disabled {
color: #ccc;
cursor: not-allowed;
border-color: #f0f0f0;
}
.pagination-item.disabled:hover {
color: #ccc;
border-color: #f0f0f0;
}
.nav-button {
padding: 0.52vh 1.04vw;
}
.page-number {
min-width: 2.08vw;
text-align: center;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.box_2 {
flex-direction: column;
}
.block_4 {
width: 100%;
margin-right: 0;
margin-bottom: 0.83vh;
}
.box_3 {
width: 100%;
height: 18vh;
}
.text_16 {
font-size: 1.25vw;
}
.text_17,
.text_18,
.text_19 {
font-size: 1vw;
}
.text_20,
.text_21,
.text_22,
.text_23 {
font-size: 0.9vw;
}
.thumbnail_5,
.thumbnail_6,
.thumbnail_7,
.thumbnail_8 {
width: 1.25vw;
height: 1.25vw;
}
}
@media (max-width: 768px) {
.text_12,
.text_13,
.text_14 {
font-size: 2.16px;
padding: 10px 0;
}
.course-divider {
width: 100%;
}
.pagination-wrapper {
margin-top: 6vh;
}
.pagination {
gap: 2vw;
}
.pagination-item {
font-size: 3vw;
padding: 1vh 2vw;
}
.nav-button {
padding: 1vh 3vw;
}
.page-number {
min-width: 8vw;
}
.box_2 {
padding: 0.83vh 0.83vw;
}
.block_4 {
margin-bottom: 0.63vh;
}
.box_3 {
height: 20vh;
}
}
@media (max-width: 480px) {
.text_12,
.text_13,
.text_14 {
font-size: 0.83vw;
padding: 0.31vh 0.63vw;
}
.box_2 {
padding: 0.63vh 0.63vw;
}
.text_16 {
font-size: 0.83vw;
}
.text_19 {
font-size: 0.73vw;
}
}
/* 通用样式类 */
.flex-row {
display: flex;
flex-direction: row;
}
.flex-col {
display: flex;
flex-direction: column;
}
.justify-between {
justify-content: space-between;
}
/* 加载状态样式 */
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 2vh 0;
font-size: 0.83vw;
color: #666;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
/* 空状态样式 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: 4vh 0;
font-size: 0.94vw;
color: #999;
font-family: "Microsoft YaHei", Arial, sans-serif;
}
</style>