完成资源案例

This commit is contained in:
2025-11-04 11:53:16 +08:00
parent 0bec4c0129
commit 6001c8d918

View File

@@ -32,7 +32,7 @@
<div class="courses-grid">
<div
class="course-card"
v-for="course in initCourses"
v-for="(course, index) in initCourses"
:key="course.id"
@click="selectCourse(course)"
style="cursor: pointer"
@@ -69,7 +69,7 @@
<div class="courses-grid">
<div
class="course-card"
v-for="course in onlineCourses"
v-for="(course, index) in onlineCourses"
:key="course.id"
@click="selectCourse(course)"
style="cursor: pointer"
@@ -142,10 +142,10 @@
</div>
</div>
<div v-if="chapters[chapter.id]?.expanded && chapter.children?.length" class="sub-chapters-container">
<div v-for="sub in chapter.children.sort((a,b)=>a.sort-b.sort)" :key="sub.id" class="sub-chapter-item">
<div v-for="sub in chapter.children.sort((a: Chapter, b: Chapter) => a.sort - b.sort)" :key="sub.id" class="sub-chapter-item">
<div class="sub-chapter-title">{{ sub.title }}</div>
<div v-if="sub.children?.length" class="grand-children-container">
<div v-for="grand in sub.children.sort((a,b)=>a.sort-b.sort)" :key="grand.id" class="grand-child-title">
<div v-for="grand in sub.children.sort((a: Chapter, b: Chapter) => a.sort - b.sort)" :key="grand.id" class="grand-child-title">
{{ grand.title }}
</div>
</div>
@@ -417,21 +417,105 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import axios from 'axios';
// ==================== 核心类型定义 ====================
/** 线上课程类型 */
interface Course {
id: number;
cover_url: string;
title: string;
subtitle?: string;
intro: string;
[key: string]: any;
}
/** 教学案例类型 */
interface TeachingCase {
id: number;
title: string;
tutor_name: string;
tutor_title: string;
student_names?: string;
content: string;
cover_url: string;
create_time: string;
update_time: string;
[key: string]: any;
}
/** 视频案例类型 */
interface VideoCase {
id: number;
video_url: string;
title: string;
intro: string;
designer_names: string;
tutor_names: string;
create_time: string;
update_time: string;
[key: string]: any;
}
/** 课程章节类型 */
interface Chapter {
id: number;
title: string;
children?: Chapter[];
sort: number;
[key: string]: any;
}
/** 拓展资源类型 */
interface Resource {
id: number;
title: string;
resource_url: string;
[key: string]: any;
}
/** 学生活动类型 */
interface Activity {
id: number;
title: string;
content: string;
start_time: string;
end_time: string;
[key: string]: any;
}
/** 教师类型 */
interface Teacher {
id: number;
avatar: string;
name: string;
title: string;
intro: string;
[key: string]: any;
}
/** 接口响应基础类型 */
interface ApiResponse<T = any> {
code?: number;
message?: string;
data?: T;
list?: T[];
total?: number;
}
// ==================== 全局状态 ====================
const activeTab = ref('online');
const selectedCourse = ref(null);
const selectedCase = ref(null);
const playingVideo = ref(null);
const activeDetailTab = ref('content');
const chapters = ref({});
const activeTab = ref<'online' | 'teaching' | 'video'>('online');
const selectedCourse = ref<Course | null>(null);
const selectedCase = ref<TeachingCase | null>(null);
const playingVideo = ref<VideoCase | null>(null);
const activeDetailTab = ref<'content' | 'resource' | 'activity' | 'team'>('content');
const chapters = ref<Record<number, { expanded: boolean }>>({});
// 线上课程
const onlineCourses = ref([]);
const initCourses = ref([]);
const onlineCourses = ref<Course[]>([]);
const initCourses = ref<Course[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const totalCount = ref(0);
@@ -443,7 +527,7 @@ const showFullCourseList = ref(false);
const searchKeyword = ref('');
// 教学案例
const caseList = ref([]);
const caseList = ref<TeachingCase[]>([]);
const casePage = ref(1);
const caseSize = 20;
const caseTotal = ref(0);
@@ -454,7 +538,7 @@ const caseError = ref('');
const caseKeyword = ref('');
// 视频案例
const videoList = ref([]);
const videoList = ref<VideoCase[]>([]);
const videoPage = ref(1);
const videoSize = 10;
const videoTotal = ref(0);
@@ -465,53 +549,53 @@ const videoError = ref('');
const videoKeyword = ref('');
// 课程详情
const courseChapters = ref([]);
const courseChapters = ref<Chapter[]>([]);
const loadingChapters = ref(false);
const chapterError = ref('');
const resourceList = ref([]);
const resourceList = ref<Resource[]>([]);
const resourcePage = ref(1);
const resourcePageSize = 20;
const hasMoreResources = ref(true);
const loadingResources = ref(false);
const loadingMoreResources = ref(false);
const resourceError = ref('');
const activityList = ref([]);
const activityList = ref<Activity[]>([]);
const activityPage = ref(1);
const activityPageSize = 10;
const hasMoreActivities = ref(true);
const loadingActivities = ref(false);
const loadingMoreActivities = ref(false);
const activityError = ref('');
const teacherList = ref([]);
const teacherList = ref<Teacher[]>([]);
const teacherPage = ref(1);
const teacherPageSize = 10;
const hasMoreTeachers = ref(true);
const loadingTeachers = ref(false);
const loadingMoreTeachers = ref(false);
const teacherError = ref('');
const currentCourseId = ref(null);
const currentCourseId = ref<number | null>(null);
// ==================== 工具函数 ====================
const formatDate = (time) => (time ? time.split(' ')[0] : '');
const formatTime = (timeStr) => (timeStr ? timeStr.split('.')[0].replace(' ', ' ') : '');
const onAvatarError = (e) => { e.target.src = 'https://via.placeholder.com/80?text=头像'; };
const onVideoError = (e) => { console.error('视频加载失败:', e); alert('视频加载失败'); };
const onVideoLoaded = (e) => { e.target.currentTime = 0.1; };
const formatDate = (time: string | undefined) => (time ? time.split(' ')[0] : '');
const formatTime = (timeStr: string | undefined) => (timeStr ? timeStr.split('.')[0]?.replace(' ', ' ') : '');
const onAvatarError = (e: Event) => { (e.target as HTMLImageElement).src = 'https://via.placeholder.com/80?text=头像'; };
const onVideoError = (e: Event) => { console.error('视频加载失败:', e); alert('视频加载失败'); };
const onVideoLoaded = (e: Event) => { (e.target as HTMLVideoElement).currentTime = 0.1; };
const getFileExtension = (url) => {
const getFileExtension = (url: string | undefined) => {
if (!url) return '';
const fileName = url.split('/').pop().split('?')[0];
const fileName = url.split('/').pop()?.split('?')[0] || '';
const parts = fileName.split('.');
return parts.length > 1 ? parts.pop().toLowerCase() : '';
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
};
const getDisplayTitle = (item) => {
const getDisplayTitle = (item: Resource) => {
const ext = getFileExtension(item.resource_url);
return ext ? `${item.title}.${ext}` : item.title;
};
const openResource = (url) => { if (url) window.open(url, '_blank'); };
const openResource = (url: string | undefined) => { if (url) window.open(url, '_blank'); };
// ==================== 统一 tab 切换 ====================
const setActiveTab = (tab) => {
const setActiveTab = (tab: 'content' | 'resource' | 'activity' | 'team') => {
if (activeDetailTab.value === tab) return;
activeDetailTab.value = tab;
if (!selectedCourse.value) return;
@@ -522,7 +606,7 @@ const setActiveTab = (tab) => {
};
// ==================== 导航切换 ====================
const handleTabChange = (tab) => {
const handleTabChange = (tab: 'online' | 'teaching' | 'video') => {
activeTab.value = tab;
selectedCourse.value = null;
selectedCase.value = null;
@@ -538,16 +622,16 @@ const initOnlineCourses = async () => {
loadingInit.value = true;
try {
const res = await fetchCoursePage(1, '');
onlineCourses.value = res.list;
totalCount.value = res.total;
onlineCourses.value = res.list as Course[];
totalCount.value = res.total || 0;
currentPage.value = 1;
hasMore.value = currentPage.value * pageSize.value < totalCount.value;
initCourses.value = onlineCourses.value.slice(0, 3);
} catch { errorMsg.value = '加载失败'; } finally { loadingInit.value = false; }
};
const fetchCoursePage = async (page, keyword) => {
const { data } = await axios.post('http://localhost:8080/api/courses/list', {
const fetchCoursePage = async (page: number, keyword: string): Promise<ApiResponse<Course>> => {
const { data } = await axios.post<ApiResponse<Course>>('http://localhost:8080/api/courses/list', {
page, size: pageSize.value, status: 1, keyword: keyword || ''
});
if (!data?.list) throw new Error();
@@ -558,8 +642,8 @@ const handleCourseSearch = async () => {
loadingInit.value = true;
try {
const res = await fetchCoursePage(1, searchKeyword.value);
onlineCourses.value = res.list;
totalCount.value = res.total;
onlineCourses.value = res.list as Course[];
totalCount.value = res.total || 0;
currentPage.value = 1;
hasMore.value = currentPage.value * pageSize.value < totalCount.value;
} catch { errorMsg.value = '搜索失败'; } finally { loadingInit.value = false; }
@@ -570,9 +654,10 @@ const loadNextPage = async () => {
loadingMore.value = true;
try {
const res = await fetchCoursePage(currentPage.value + 1, searchKeyword.value);
onlineCourses.value.push(...res.list);
const newCourses = res.list as Course[];
onlineCourses.value.push(...newCourses);
currentPage.value++;
hasMore.value = currentPage.value * pageSize.value < totalCount.value;
hasMore.value = currentPage.value * pageSize.value < (res.total || 0);
} catch { errorMsg.value = '加载失败'; } finally { loadingMore.value = false; }
};
@@ -587,15 +672,15 @@ const initCases = async () => {
caseError.value = '';
try {
const res = await fetchCasePage(1, caseKeyword.value);
caseList.value = res.list;
caseTotal.value = res.total;
caseList.value = res.list as TeachingCase[];
caseTotal.value = res.total || 0;
casePage.value = 1;
caseHasMore.value = casePage.value * caseSize < caseTotal.value;
} catch { caseError.value = '加载失败'; } finally { caseLoading.value = false; }
};
const fetchCasePage = async (page, keyword) => {
const { data } = await axios.post('http://localhost:8080/api/teaching-cases/list', {
const fetchCasePage = async (page: number, keyword: string): Promise<ApiResponse<TeachingCase>> => {
const { data } = await axios.post<ApiResponse<TeachingCase>>('http://localhost:8080/api/teaching-cases/list', {
page, size: caseSize, keyword: keyword || '', sort: 0
});
if (!data?.list || typeof data.total !== 'number') throw new Error();
@@ -606,8 +691,8 @@ const searchCases = async () => {
caseLoading.value = true;
try {
const res = await fetchCasePage(1, caseKeyword.value);
caseList.value = res.list;
caseTotal.value = res.total;
caseList.value = res.list as TeachingCase[];
caseTotal.value = res.total || 0;
casePage.value = 1;
caseHasMore.value = casePage.value * caseSize < caseTotal.value;
} catch { caseError.value = '搜索失败'; } finally { caseLoading.value = false; }
@@ -618,13 +703,14 @@ const loadMoreCases = async () => {
caseLoadingMore.value = true;
try {
const res = await fetchCasePage(casePage.value + 1, caseKeyword.value);
caseList.value.push(...res.list);
const newCases = res.list as TeachingCase[];
caseList.value.push(...newCases);
casePage.value++;
caseHasMore.value = casePage.value * caseSize < caseTotal.value;
caseHasMore.value = casePage.value * caseSize < (res.total || 0);
} catch { caseError.value = '加载更多失败'; } finally { caseLoadingMore.value = false; }
};
const selectCase = (item) => { selectedCase.value = item; };
const selectCase = (item: TeachingCase) => { selectedCase.value = item; };
const backToCaseList = () => { selectedCase.value = null; };
// ==================== 视频案例 ====================
@@ -633,15 +719,15 @@ const initVideos = async () => {
videoError.value = '';
try {
const res = await fetchVideoPage(1, videoKeyword.value);
videoList.value = res.list;
videoTotal.value = res.total;
videoList.value = res.list as VideoCase[];
videoTotal.value = res.total || 0;
videoPage.value = 1;
videoHasMore.value = videoPage.value * videoSize < videoTotal.value;
} catch { videoError.value = '加载失败'; } finally { videoLoading.value = false; }
};
const fetchVideoPage = async (page, keyword) => {
const { data } = await axios.post('http://localhost:8080/api/video-cases/list', {
const fetchVideoPage = async (page: number, keyword: string): Promise<ApiResponse<VideoCase>> => {
const { data } = await axios.post<ApiResponse<VideoCase>>('http://localhost:8080/api/video-cases/list', {
page, size: videoSize, keyword: keyword || '', sort: 0
});
if (!data?.list || typeof data.total !== 'number') throw new Error();
@@ -652,8 +738,8 @@ const searchVideos = async () => {
videoLoading.value = true;
try {
const res = await fetchVideoPage(1, videoKeyword.value);
videoList.value = res.list;
videoTotal.value = res.total;
videoList.value = res.list as VideoCase[];
videoTotal.value = res.total || 0;
videoPage.value = 1;
videoHasMore.value = videoPage.value * videoSize < videoTotal.value;
} catch { videoError.value = '搜索失败'; } finally { videoLoading.value = false; }
@@ -664,95 +750,160 @@ const loadMoreVideos = async () => {
videoLoadingMore.value = true;
try {
const res = await fetchVideoPage(videoPage.value + 1, videoKeyword.value);
videoList.value.push(...res.list);
const newVideos = res.list as VideoCase[];
videoList.value.push(...newVideos);
videoPage.value++;
videoHasMore.value = videoPage.value * videoSize < videoTotal.value;
videoHasMore.value = videoPage.value * videoSize < (res.total || 0);
} catch { videoError.value = '加载更多失败'; } finally { videoLoadingMore.value = false; }
};
const playVideo = (item) => { playingVideo.value = item; };
const playVideo = (item: VideoCase) => { playingVideo.value = item; };
const backToVideoList = () => { playingVideo.value = null; };
// ==================== 课程详情 ====================
const fetchCourseContent = async (id) => {
const fetchCourseContent = async (id: number) => {
loadingChapters.value = true;
try {
const { data } = await axios.post('http://localhost:8080/api/course-content/list', { course_id: id, parent_id: 0 });
const { data } = await axios.post<ApiResponse<Chapter>>('http://localhost:8080/api/course-content/list', { course_id: id, parent_id: 0 });
if (data.code === 0 && Array.isArray(data.data)) {
courseChapters.value = data.data.sort((a,b)=>a.sort-b.sort);
courseChapters.value = data.data.sort((a: Chapter, b: Chapter) => a.sort - b.sort);
courseChapters.value.forEach(c => chapters.value[c.id] = { expanded: false });
}
} catch { chapterError.value = '加载失败'; } finally { loadingChapters.value = false; }
};
const toggleChapter = (id) => {
const toggleChapter = (id: number) => {
if (!chapters.value[id]) chapters.value[id] = { expanded: false };
chapters.value[id].expanded = !chapters.value[id].expanded;
};
const fetchResources = async (page = 1, reset = false) => {
if (reset) { resourceList.value = []; resourcePage.value = 1; hasMoreResources.value = true; resourceError.value = ''; loadingResources.value = true; }
else loadingMoreResources.value = true;
if (!selectedCourse.value) return;
if (reset) {
resourceList.value = [];
resourcePage.value = 1;
hasMoreResources.value = true;
resourceError.value = '';
loadingResources.value = true;
} else {
loadingMoreResources.value = true;
}
try {
const { data } = await axios.post('http://localhost:8080/api/course-resource/list', {
course_id: selectedCourse.value.id, page, page_size: resourcePageSize
const { data } = await axios.post<ApiResponse<Resource>>('http://localhost:8080/api/course-resource/list', {
course_id: selectedCourse.value.id,
page,
page_size: resourcePageSize
});
if (!data?.list || typeof data.total !== 'number') throw new Error();
if (reset) resourceList.value = data.list; else resourceList.value.push(...data.list);
const newResources = data.list as Resource[];
if (reset) resourceList.value = newResources;
else resourceList.value.push(...newResources);
resourcePage.value = page;
hasMoreResources.value = page * resourcePageSize < data.total;
currentCourseId.value = selectedCourse.value.id;
} catch { resourceError.value = reset ? '加载失败' : '加载更多失败'; }
finally { loadingResources.value = false; loadingMoreResources.value = false; }
} catch {
resourceError.value = reset ? '加载失败' : '加载更多失败';
} finally {
loadingResources.value = false;
loadingMoreResources.value = false;
}
};
const loadMoreResources = () => { if (loadingMoreResources.value || !hasMoreResources.value) return; fetchResources(resourcePage.value + 1); };
const loadMoreResources = () => {
if (loadingMoreResources.value || !hasMoreResources.value) return;
fetchResources(resourcePage.value + 1);
};
const fetchActivities = async (page = 1, reset = false) => {
if (reset) { activityList.value = []; activityPage.value = 1; hasMoreActivities.value = true; activityError.value = ''; loadingActivities.value = true; }
else loadingMoreActivities.value = true;
if (!selectedCourse.value) return;
if (reset) {
activityList.value = [];
activityPage.value = 1;
hasMoreActivities.value = true;
activityError.value = '';
loadingActivities.value = true;
} else {
loadingMoreActivities.value = true;
}
try {
const { data } = await axios.post('http://localhost:8080/api/course-activity/list', {
course_id: selectedCourse.value.id, page, page_size: activityPageSize
const { data } = await axios.post<ApiResponse<Activity>>('http://localhost:8080/api/course-activity/list', {
course_id: selectedCourse.value.id,
page,
page_size: activityPageSize
});
if (!data || typeof data.total !== 'number' || !Array.isArray(data.list)) throw new Error();
if (reset) activityList.value = data.list; else activityList.value.push(...data.list);
const newActivities = data.list as Activity[];
if (reset) activityList.value = newActivities;
else activityList.value.push(...newActivities);
activityPage.value = page;
hasMoreActivities.value = page * activityPageSize < data.total;
currentCourseId.value = selectedCourse.value.id;
} catch { activityError.value = reset ? '加载失败' : '加载更多失败'; }
finally { loadingActivities.value = false; loadingMoreActivities.value = false; }
} catch {
activityError.value = reset ? '加载失败' : '加载更多失败';
} finally {
loadingActivities.value = false;
loadingMoreActivities.value = false;
}
};
const loadMoreActivities = () => { if (loadingMoreActivities.value || !hasMoreActivities.value) return; fetchActivities(activityPage.value + 1); };
const loadMoreActivities = () => {
if (loadingMoreActivities.value || !hasMoreActivities.value) return;
fetchActivities(activityPage.value + 1);
};
const fetchTeachers = async (page = 1, reset = false) => {
if (reset) { teacherList.value = []; teacherPage.value = 1; hasMoreTeachers.value = true; teacherError.value = ''; loadingTeachers.value = true; }
else loadingMoreTeachers.value = true;
if (!selectedCourse.value) return;
if (reset) {
teacherList.value = [];
teacherPage.value = 1;
hasMoreTeachers.value = true;
teacherError.value = '';
loadingTeachers.value = true;
} else {
loadingMoreTeachers.value = true;
}
try {
const { data } = await axios.post('http://localhost:8080/api/course-teacher/list', {
course_id: selectedCourse.value.id, page, page_size: teacherPageSize
const { data } = await axios.post<ApiResponse<Teacher>>('http://localhost:8080/api/course-teacher/list', {
course_id: selectedCourse.value.id,
page,
page_size: teacherPageSize
});
if (!data || typeof data.total !== 'number' || !Array.isArray(data.list)) throw new Error();
if (reset) teacherList.value = data.list; else teacherList.value.push(...data.list);
const newTeachers = data.list as Teacher[];
if (reset) teacherList.value = newTeachers;
else teacherList.value.push(...newTeachers);
teacherPage.value = page;
hasMoreTeachers.value = page * teacherPageSize < data.total;
currentCourseId.value = selectedCourse.value.id;
} catch { teacherError.value = reset ? '加载失败' : '加载更多失败'; }
finally { loadingTeachers.value = false; loadingMoreTeachers.value = false; }
} catch {
teacherError.value = reset ? '加载失败' : '加载更多失败';
} finally {
loadingTeachers.value = false;
loadingMoreTeachers.value = false;
}
};
const loadMoreTeachers = () => { if (loadingMoreTeachers.value || !hasMoreTeachers.value) return; fetchTeachers(teacherPage.value + 1); };
const loadMoreTeachers = () => {
if (loadingMoreTeachers.value || !hasMoreTeachers.value) return;
fetchTeachers(teacherPage.value + 1);
};
const selectCourse = (course) => {
const selectCourse = (course: Course) => {
selectedCourse.value = course;
currentCourseId.value = course.id;
courseChapters.value = []; resourceList.value = []; activityList.value = []; teacherList.value = []; chapters.value = {};
courseChapters.value = [];
resourceList.value = [];
activityList.value = [];
teacherList.value = [];
chapters.value = {};
fetchCourseContent(course.id);
activeDetailTab.value = 'content';
};
const handleBackToList = () => { selectedCourse.value = null; currentCourseId.value = null; };
const handleBackToList = () => {
selectedCourse.value = null;
currentCourseId.value = null;
};
// ==================== 初始化 ====================
onMounted(() => {