解决build问题
This commit is contained in:
parent
c0dbb2c97d
commit
a532924668
@ -9,6 +9,8 @@
|
|||||||
<meta name="keywords" content="在线学习,编程课程,技术培训,Vue.js,React,Node.js">
|
<meta name="keywords" content="在线学习,编程课程,技术培训,Vue.js,React,Node.js">
|
||||||
<!-- CKPlayer CSS -->
|
<!-- CKPlayer CSS -->
|
||||||
<link rel="stylesheet" href="/ckplayer/css/ckplayer.css">
|
<link rel="stylesheet" href="/ckplayer/css/ckplayer.css">
|
||||||
|
<!-- HLS.js for m3u8 playback in CKPlayer -->
|
||||||
|
<script src="/ckplayer/hls.js/hls.min.js"></script>
|
||||||
<!-- CKPlayer JS -->
|
<!-- CKPlayer JS -->
|
||||||
<script src="/ckplayer/js/ckplayer.js"></script>
|
<script src="/ckplayer/js/ckplayer.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -12,6 +12,9 @@ const checkNetworkStatus = (): boolean => {
|
|||||||
return true // 默认认为网络可用
|
return true // 默认认为网络可用
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全局开关:强制使用Mock数据(当前需求:全部使用Mock,不访问后端)
|
||||||
|
const FORCE_ENABLE_MOCK = true
|
||||||
|
|
||||||
// 消息提示函数 - 使用window.alert作为fallback,实际项目中应该使用UI库的消息组件
|
// 消息提示函数 - 使用window.alert作为fallback,实际项目中应该使用UI库的消息组件
|
||||||
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
|
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
|
||||||
// 这里可以替换为你使用的UI库的消息组件
|
// 这里可以替换为你使用的UI库的消息组件
|
||||||
@ -26,7 +29,8 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
|
|||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
|
// 统一走 /api,由 Vite 代理到后端,避免CORS;若启用Mock,则只会走本地Mock
|
||||||
|
baseURL: '/api',
|
||||||
timeout: 30000, // 增加到30秒
|
timeout: 30000, // 增加到30秒
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -454,8 +458,9 @@ export class ApiRequest {
|
|||||||
params?: any,
|
params?: any,
|
||||||
config?: AxiosRequestConfig
|
config?: AxiosRequestConfig
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
// 检查是否启用Mock
|
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true')
|
||||||
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
|
// 优先:若显式启用Mock,直接使用Mock
|
||||||
|
if (enableMock) {
|
||||||
return handleMockRequest<T>(url, 'GET', params)
|
return handleMockRequest<T>(url, 'GET', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,7 +468,7 @@ export class ApiRequest {
|
|||||||
return await retryRequest(() => request.get(url, { params, ...config }))
|
return await retryRequest(() => request.get(url, { params, ...config }))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('API请求失败,降级到Mock数据:', error)
|
console.warn('API请求失败,降级到Mock数据:', error)
|
||||||
// 如果真实API失败,降级到Mock数据
|
// 后备:真实API失败时,仍回落到Mock,保障页面可用
|
||||||
return handleMockRequest<T>(url, 'GET', params)
|
return handleMockRequest<T>(url, 'GET', params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -474,8 +479,8 @@ export class ApiRequest {
|
|||||||
data?: any,
|
data?: any,
|
||||||
config?: AxiosRequestConfig
|
config?: AxiosRequestConfig
|
||||||
): Promise<ApiResponse<T>> {
|
): Promise<ApiResponse<T>> {
|
||||||
// 检查是否启用Mock
|
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true')
|
||||||
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
|
if (enableMock) {
|
||||||
return handleMockRequest<T>(url, 'POST', data)
|
return handleMockRequest<T>(url, 'POST', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +488,6 @@ export class ApiRequest {
|
|||||||
return await retryRequest(() => request.post(url, data, config))
|
return await retryRequest(() => request.post(url, data, config))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('API请求失败,降级到Mock数据:', error)
|
console.warn('API请求失败,降级到Mock数据:', error)
|
||||||
// 如果真实API失败,降级到Mock数据
|
|
||||||
return handleMockRequest<T>(url, 'POST', data)
|
return handleMockRequest<T>(url, 'POST', data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,15 +255,7 @@ const selectedGroup = ref(0)
|
|||||||
// activeCategory.value = categoryId
|
// activeCategory.value = categoryId
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 获取状态文本
|
// 已移除未使用的状态文本函数,避免构建时报未使用错误
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
const statusMap = {
|
|
||||||
'upcoming': '即将开始',
|
|
||||||
'ongoing': '报名中',
|
|
||||||
'ended': '已结束'
|
|
||||||
}
|
|
||||||
return statusMap[status as keyof typeof statusMap] || '未知状态'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载活动详情
|
// 加载活动详情
|
||||||
const loadActivityDetail = async () => {
|
const loadActivityDetail = async () => {
|
||||||
|
@ -34,13 +34,13 @@
|
|||||||
<div class="video-player-section">
|
<div class="video-player-section">
|
||||||
<div class="video-player enrolled">
|
<div class="video-player enrolled">
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<!-- CKPlayer 容器 -->
|
<!-- CKPlayer 容器:使用 v-show 确保容器始终挂载,避免"未找到放置视频的容器" -->
|
||||||
<div
|
<div
|
||||||
v-if="currentVideoUrl"
|
v-show="!!currentVideoUrl"
|
||||||
id="ckplayer_container"
|
id="ckplayer_container"
|
||||||
class="ckplayer-container">
|
class="ckplayer-container">
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="video-placeholder" :style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
<div v-show="!currentVideoUrl" class="video-placeholder" :style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
||||||
<div class="placeholder-content">
|
<div class="placeholder-content">
|
||||||
<div class="play-icon">
|
<div class="play-icon">
|
||||||
<svg width="60" height="60" viewBox="0 0 60 60">
|
<svg width="60" height="60" viewBox="0 0 60 60">
|
||||||
@ -97,7 +97,7 @@
|
|||||||
分类:<span class="category-link">{{ course.category?.name || '信息技术' }}</span>
|
分类:<span class="category-link">{{ course.category?.name || '信息技术' }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="meta-right">
|
<div class="meta-right">
|
||||||
<button class="btn-notes">
|
<button class="btn-notes" @click="openNotesModal">
|
||||||
<i class="icon-note"></i>
|
<i class="icon-note"></i>
|
||||||
记笔记
|
记笔记
|
||||||
</button>
|
</button>
|
||||||
@ -419,6 +419,9 @@ const router = useRouter()
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const courseId = ref(Number(route.params.id))
|
const courseId = ref(Number(route.params.id))
|
||||||
|
|
||||||
|
// 强制仅播放本地视频(如需关闭,置为 false)
|
||||||
|
const FORCE_LOCAL_VIDEO = true
|
||||||
|
|
||||||
// 当前选中的章节和视频
|
// 当前选中的章节和视频
|
||||||
const currentSection = ref<CourseSection | null>(null)
|
const currentSection = ref<CourseSection | null>(null)
|
||||||
const currentVideoUrl = ref<string>('')
|
const currentVideoUrl = ref<string>('')
|
||||||
@ -433,7 +436,11 @@ const VIDEO_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取视频URL的函数
|
// 获取视频URL的函数
|
||||||
const getVideoUrl = () => {
|
const getVideoUrl = (section?: CourseSection) => {
|
||||||
|
const outline = section?.outline?.trim()
|
||||||
|
if (outline && (outline.endsWith('.mp4') || outline.endsWith('.m3u8'))) {
|
||||||
|
return outline
|
||||||
|
}
|
||||||
// 当前使用本地视频,将来可以通过环境变量或配置切换
|
// 当前使用本地视频,将来可以通过环境变量或配置切换
|
||||||
return VIDEO_CONFIG.LOCAL
|
return VIDEO_CONFIG.LOCAL
|
||||||
// 服务器准备好后,可以改为:
|
// 服务器准备好后,可以改为:
|
||||||
@ -675,6 +682,16 @@ const loadCourseSections = async () => {
|
|||||||
groupedSections.value = groupSectionsByChapter(response.data)
|
groupedSections.value = groupSectionsByChapter(response.data)
|
||||||
console.log('章节数据设置成功:', courseSections.value)
|
console.log('章节数据设置成功:', courseSections.value)
|
||||||
console.log('分组数据:', groupedSections.value)
|
console.log('分组数据:', groupedSections.value)
|
||||||
|
// 默认播放右侧第一个视频章节(当未强制使用本地视频时)
|
||||||
|
if (!FORCE_LOCAL_VIDEO) {
|
||||||
|
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
||||||
|
if (firstVideo) {
|
||||||
|
currentSection.value = firstVideo
|
||||||
|
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||||
|
await nextTick()
|
||||||
|
initCKPlayer(currentVideoUrl.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('API返回的章节数据为空,使用模拟数据')
|
console.log('API返回的章节数据为空,使用模拟数据')
|
||||||
loadMockData()
|
loadMockData()
|
||||||
@ -709,6 +726,15 @@ const loadMockData = () => {
|
|||||||
completed: completed,
|
completed: completed,
|
||||||
progress: progress.value
|
progress: progress.value
|
||||||
})
|
})
|
||||||
|
// 默认播放右侧第一个视频章节(模拟数据,未强制使用本地视频时)
|
||||||
|
if (!FORCE_LOCAL_VIDEO) {
|
||||||
|
const firstVideo = mockSections.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
||||||
|
if (firstVideo) {
|
||||||
|
currentSection.value = firstVideo
|
||||||
|
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||||
|
setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换章节展开/收起
|
// 切换章节展开/收起
|
||||||
@ -785,7 +811,7 @@ const handleVideoPlay = async (section: CourseSection) => {
|
|||||||
console.log('播放视频:', section.name)
|
console.log('播放视频:', section.name)
|
||||||
|
|
||||||
// 获取视频URL
|
// 获取视频URL
|
||||||
const videoUrl = getVideoUrl()
|
const videoUrl = getVideoUrl(section)
|
||||||
currentVideoUrl.value = videoUrl
|
currentVideoUrl.value = videoUrl
|
||||||
currentSection.value = section
|
currentSection.value = section
|
||||||
|
|
||||||
@ -825,6 +851,14 @@ const initCKPlayer = (url: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若容器暂未挂载,延迟重试一次,避免"未找到放置视频的容器"
|
||||||
|
const containerEl = document.querySelector('#ckplayer_container') as HTMLElement | null
|
||||||
|
if (!containerEl) {
|
||||||
|
console.warn('Player container not found, retrying init...')
|
||||||
|
setTimeout(() => initCKPlayer(url), 50)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 判断视频类型
|
// 判断视频类型
|
||||||
const isMP4 = url.endsWith('.mp4')
|
const isMP4 = url.endsWith('.mp4')
|
||||||
const isHLS = url.endsWith('.m3u8')
|
const isHLS = url.endsWith('.m3u8')
|
||||||
@ -844,6 +878,7 @@ const initCKPlayer = (url: string) => {
|
|||||||
seek: 0, // 默认跳转秒数
|
seek: 0, // 默认跳转秒数
|
||||||
loaded: 'loadedHandler', // 播放器加载完成回调
|
loaded: 'loadedHandler', // 播放器加载完成回调
|
||||||
ended: 'endedHandler', // 播放结束回调
|
ended: 'endedHandler', // 播放结束回调
|
||||||
|
error: 'errorHandler', // 播放错误回调
|
||||||
title: currentSection.value?.name || '课程视频', // 视频标题
|
title: currentSection.value?.name || '课程视频', // 视频标题
|
||||||
controls: true, // 显示控制栏
|
controls: true, // 显示控制栏
|
||||||
webFull: true, // 启用页面全屏
|
webFull: true, // 启用页面全屏
|
||||||
@ -876,6 +911,16 @@ window.endedHandler = () => {
|
|||||||
|
|
||||||
window.errorHandler = (error: any) => {
|
window.errorHandler = (error: any) => {
|
||||||
console.error('CKPlayer error:', error)
|
console.error('CKPlayer error:', error)
|
||||||
|
// 自动回退到本地视频,避免一直缓冲
|
||||||
|
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
|
||||||
|
try {
|
||||||
|
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
|
||||||
|
// 重新初始化播放器播放本地视频
|
||||||
|
initCKPlayer(VIDEO_CONFIG.LOCAL)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Fallback to local video failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理资源下载
|
// 处理资源下载
|
||||||
@ -965,9 +1010,16 @@ const saveNote = (content: string) => {
|
|||||||
// 这里可以添加保存笔记到服务器的逻辑
|
// 这里可以添加保存笔记到服务器的逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
console.log('已报名课程详情页加载完成,课程ID:', courseId.value)
|
console.log('已报名课程详情页加载完成,课程ID:', courseId.value)
|
||||||
initializeEnrolledState() // 初始化已报名状态
|
initializeEnrolledState() // 初始化已报名状态
|
||||||
|
// 若强制播放本地视频,优先初始化本地源
|
||||||
|
if (FORCE_LOCAL_VIDEO) {
|
||||||
|
currentSection.value = null
|
||||||
|
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
|
||||||
|
await nextTick()
|
||||||
|
initCKPlayer(currentVideoUrl.value)
|
||||||
|
}
|
||||||
loadCourseDetail()
|
loadCourseDetail()
|
||||||
loadCourseSections()
|
loadCourseSections()
|
||||||
})
|
})
|
||||||
|
@ -1173,13 +1173,7 @@ const showTimeUpAndSubmit = () => {
|
|||||||
// return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
|
// return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 格式化时间显示为时分秒
|
// 已移除未使用的时间格式化函数,避免构建时报未使用错误
|
||||||
const formatTimeHMS = (seconds: number): string => {
|
|
||||||
const hours = Math.floor(seconds / 3600)
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60)
|
|
||||||
const remainingSeconds = seconds % 60
|
|
||||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时清理计时器
|
// 组件卸载时清理计时器
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
@ -94,6 +94,18 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
// 为教师项添加明确类型,包含可选的 courseCount 字段
|
||||||
|
interface Teacher {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
position: string
|
||||||
|
description: string
|
||||||
|
specialty: string
|
||||||
|
avatar: string
|
||||||
|
featured?: boolean
|
||||||
|
courseCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
// 横幅图片相关
|
// 横幅图片相关
|
||||||
const bannerImageSrc = ref('/images/Teachers/师资力量切图-轮播区.png')
|
const bannerImageSrc = ref('/images/Teachers/师资力量切图-轮播区.png')
|
||||||
const hasBannerImage = computed(() => bannerImageSrc.value.trim() !== '')
|
const hasBannerImage = computed(() => bannerImageSrc.value.trim() !== '')
|
||||||
@ -138,7 +150,7 @@ const filterTabs = ref([
|
|||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
|
|
||||||
// 师资数据
|
// 师资数据
|
||||||
const teachers = ref([
|
const teachers = ref<Teacher[]>([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: '黄天羽',
|
name: '黄天羽',
|
||||||
|
@ -2438,9 +2438,7 @@ const getFileIcon = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewFolder = () => {
|
// 已移除未使用的新建文件夹函数,避免构建时报未使用错误
|
||||||
message.info('新建文件夹功能开发中...')
|
|
||||||
}
|
|
||||||
|
|
||||||
const renameFile = (fileId: number) => {
|
const renameFile = (fileId: number) => {
|
||||||
message.info(`重命名文件 ${fileId}`)
|
message.info(`重命名文件 ${fileId}`)
|
||||||
|
@ -139,13 +139,7 @@ const itemsPerPage = 20
|
|||||||
const totalItems = computed(() => total.value)
|
const totalItems = computed(() => total.value)
|
||||||
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
|
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
|
||||||
|
|
||||||
// 控制广告显示状态
|
// 已移除未使用的广告显隐逻辑,避免构建时报未使用错误
|
||||||
const showAdvertisement = ref(true)
|
|
||||||
|
|
||||||
// 关闭广告函数
|
|
||||||
const closeAdvertisement = () => {
|
|
||||||
showAdvertisement.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数字转中文
|
// 数字转中文
|
||||||
const numberToChinese = (num: number): string => {
|
const numberToChinese = (num: number): string => {
|
||||||
@ -315,24 +309,7 @@ const clearAllFilters = () => {
|
|||||||
loadCourses()
|
loadCourses()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 筛选功能
|
// 已移除未使用的筛选函数,避免构建时报未使用错误
|
||||||
const selectSubject = (subject: string) => {
|
|
||||||
selectedSubject.value = subject
|
|
||||||
currentPage.value = 1 // 重置到第一页
|
|
||||||
loadCourses()
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectMajor = (major: string) => {
|
|
||||||
selectedMajor.value = major
|
|
||||||
currentPage.value = 1 // 重置到第一页
|
|
||||||
loadCourses()
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectDifficulty = (difficulty: string) => {
|
|
||||||
selectedDifficulty.value = difficulty
|
|
||||||
currentPage.value = 1 // 重置到第一页
|
|
||||||
loadCourses()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序功能
|
// 排序功能
|
||||||
const selectSortType = (type: string) => {
|
const selectSortType = (type: string) => {
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
const proxyTarget = env.VITE_PROXY_TARGET || 'http://110.42.96.65:55510'
|
||||||
|
|
||||||
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
@ -17,6 +21,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
open: true
|
open: true,
|
||||||
|
proxy: {
|
||||||
|
// 将以 /api 开头的请求代理到后端,避免浏览器CORS限制
|
||||||
|
'/api': {
|
||||||
|
target: proxyTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
// 如果后端接口不是以 /api 开头,可在这里改写路径
|
||||||
|
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user