style: 调整样式
@ -1 +0,0 @@
|
||||
Subproject commit 96c6f6254ac8ada76c63f2b88e30a143b6d115b8
|
320
docs/DPlayer-Integration-Guide.md
Normal file
@ -0,0 +1,320 @@
|
||||
# DPlayer 集成指南
|
||||
|
||||
## 什么是 DPlayer?
|
||||
|
||||
**DPlayer** 是由 [DIYGod](https://github.com/DIYGod) 开发的一个开源的 HTML5 视频播放器,具有以下特点:
|
||||
|
||||
- 🎨 **界面美观**:现代化的设计风格
|
||||
- 🎯 **轻量级**:体积小,加载快
|
||||
- 🌏 **中文友好**:由中国开发者开发,中文文档完善
|
||||
- 🎮 **功能丰富**:支持弹幕、快捷键、倍速播放等
|
||||
- 📱 **移动端适配**:响应式设计,支持移动设备
|
||||
|
||||
## 主要功能特性
|
||||
|
||||
### 基础功能
|
||||
- ✅ 播放/暂停控制
|
||||
- ✅ 音量控制
|
||||
- ✅ 进度条拖拽
|
||||
- ✅ 全屏切换
|
||||
- ✅ 倍速播放 (0.5x - 2x)
|
||||
|
||||
### 高级功能
|
||||
- 🎯 键盘快捷键支持
|
||||
- 🎨 自定义主题色
|
||||
- 📝 右键菜单自定义
|
||||
- 🎵 音频可视化
|
||||
- 📱 移动端手势支持
|
||||
|
||||
### 格式支持
|
||||
- MP4
|
||||
- WebM
|
||||
- Ogg
|
||||
- HLS (.m3u8)
|
||||
- FLV
|
||||
- 更多格式通过插件支持
|
||||
|
||||
## 安装和集成
|
||||
|
||||
### 方法1:CDN 引入(推荐用于快速测试)
|
||||
|
||||
```html
|
||||
<!-- 在 index.html 中引入 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js"></script>
|
||||
```
|
||||
|
||||
### 方法2:NPM 安装(推荐用于生产环境)
|
||||
|
||||
```bash
|
||||
npm install dplayer
|
||||
```
|
||||
|
||||
然后在组件中导入:
|
||||
|
||||
```javascript
|
||||
import DPlayer from 'dplayer'
|
||||
import 'dplayer/dist/DPlayer.min.css'
|
||||
```
|
||||
|
||||
## 基础使用
|
||||
|
||||
### 创建播放器
|
||||
|
||||
```javascript
|
||||
const player = new DPlayer({
|
||||
container: document.getElementById('dplayer'),
|
||||
video: {
|
||||
url: 'video.mp4',
|
||||
type: 'auto'
|
||||
},
|
||||
autoplay: false,
|
||||
theme: '#007bff',
|
||||
lang: 'zh-cn'
|
||||
})
|
||||
```
|
||||
|
||||
### 事件监听
|
||||
|
||||
```javascript
|
||||
player.on('play', () => {
|
||||
console.log('视频开始播放')
|
||||
})
|
||||
|
||||
player.on('pause', () => {
|
||||
console.log('视频暂停')
|
||||
})
|
||||
|
||||
player.on('ended', () => {
|
||||
console.log('视频播放结束')
|
||||
})
|
||||
|
||||
player.on('error', () => {
|
||||
console.log('播放出错')
|
||||
})
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
### 基础配置
|
||||
|
||||
```javascript
|
||||
const options = {
|
||||
container: document.getElementById('dplayer'), // 容器元素
|
||||
video: {
|
||||
url: 'video.mp4', // 视频地址
|
||||
type: 'auto', // 视频类型:auto, normal, hls, flv
|
||||
defaultQuality: 0, // 默认画质
|
||||
pic: 'poster.jpg', // 封面图
|
||||
thumbnails: 'thumbnails.jpg' // 缩略图
|
||||
},
|
||||
autoplay: false, // 自动播放
|
||||
theme: '#007bff', // 主题色
|
||||
lang: 'zh-cn', // 语言:zh-cn, en
|
||||
hotkey: true, // 启用快捷键
|
||||
preload: 'auto', // 预加载:auto, metadata, none
|
||||
volume: 0.8, // 默认音量
|
||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2], // 倍速选项
|
||||
contextmenu: [ // 右键菜单
|
||||
{
|
||||
text: '关于 DPlayer',
|
||||
link: 'https://github.com/DIYGod/DPlayer'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 高级配置
|
||||
|
||||
```javascript
|
||||
const advancedOptions = {
|
||||
// 弹幕配置
|
||||
danmaku: {
|
||||
id: 'dplayer-danmaku',
|
||||
api: 'https://api.prprpr.me/dplayer/',
|
||||
token: 'token',
|
||||
maximum: 1000,
|
||||
addition: ['https://api.prprpr.me/dplayer/bilibili?aid=4157142'],
|
||||
user: 'DIYGod',
|
||||
bottom: '15%',
|
||||
unlimited: true
|
||||
},
|
||||
|
||||
// 字幕配置
|
||||
subtitle: {
|
||||
url: 'subtitle.vtt',
|
||||
type: 'webvtt',
|
||||
fontSize: '20px',
|
||||
bottom: '10%',
|
||||
color: '#fff'
|
||||
},
|
||||
|
||||
// 画质切换
|
||||
video: {
|
||||
url: [
|
||||
{
|
||||
name: '1080P',
|
||||
url: 'video-1080p.mp4'
|
||||
},
|
||||
{
|
||||
name: '720P',
|
||||
url: 'video-720p.mp4'
|
||||
}
|
||||
],
|
||||
defaultQuality: 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 在 Vue 项目中使用
|
||||
|
||||
### 创建 DPlayer 组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="video-player-wrapper">
|
||||
<div ref="dplayerContainer" class="dplayer-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const dplayerContainer = ref()
|
||||
let player = null
|
||||
|
||||
onMounted(() => {
|
||||
// 确保 DPlayer 已加载
|
||||
if (window.DPlayer) {
|
||||
initPlayer()
|
||||
} else {
|
||||
loadDPlayer().then(() => {
|
||||
initPlayer()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const loadDPlayer = () => {
|
||||
return new Promise((resolve) => {
|
||||
const cssLink = document.createElement('link')
|
||||
cssLink.rel = 'stylesheet'
|
||||
cssLink.href = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css'
|
||||
document.head.appendChild(cssLink)
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js'
|
||||
script.onload = resolve
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
const initPlayer = () => {
|
||||
player = new window.DPlayer({
|
||||
container: dplayerContainer.value,
|
||||
video: {
|
||||
url: '/video/first.mp4',
|
||||
type: 'auto'
|
||||
},
|
||||
autoplay: false,
|
||||
theme: '#007bff',
|
||||
lang: 'zh-cn',
|
||||
hotkey: true
|
||||
})
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (player) {
|
||||
player.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-player-wrapper {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dplayer-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 样式定制
|
||||
|
||||
### 自定义主题色
|
||||
|
||||
```css
|
||||
/* 修改播放器主题色 */
|
||||
.dplayer {
|
||||
--dplayer-theme: #007bff;
|
||||
}
|
||||
|
||||
/* 自定义进度条颜色 */
|
||||
.dplayer .dplayer-bar-wrap .dplayer-bar .dplayer-played {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
/* 自定义控制按钮颜色 */
|
||||
.dplayer .dplayer-icons .dplayer-icon {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dplayer .dplayer-icons .dplayer-icon:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
```
|
||||
|
||||
### 响应式设计
|
||||
|
||||
```css
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.dplayer {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dplayer .dplayer-icons .dplayer-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 快捷键支持
|
||||
|
||||
DPlayer 默认支持以下快捷键:
|
||||
|
||||
- `空格键` - 播放/暂停
|
||||
- `←` - 后退 10 秒
|
||||
- `→` - 前进 10 秒
|
||||
- `↑` - 音量 +10%
|
||||
- `↓` - 音量 -10%
|
||||
- `F` - 全屏切换
|
||||
- `M` - 静音切换
|
||||
|
||||
## 与 CKPlayer 对比
|
||||
|
||||
| 特性 | CKPlayer | DPlayer |
|
||||
|------|----------|---------|
|
||||
| 界面美观度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 功能丰富度 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| 移动端支持 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| 中文支持 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 社区活跃度 | ⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| 文档质量 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| 学习成本 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
|
||||
|
||||
## 总结
|
||||
|
||||
**DPlayer 是一个优秀的视频播放器选择**,特别适合:
|
||||
|
||||
- 🎯 需要美观界面的项目
|
||||
- 🌏 中文用户群体
|
||||
- 📱 重视移动端体验
|
||||
- 🎨 需要自定义主题的项目
|
||||
- ⚡ 追求轻量级解决方案
|
||||
|
||||
相比当前的 CKPlayer,DPlayer 提供了更好的用户体验和更丰富的功能,是升级视频播放器的理想选择。
|
35
package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.11.0",
|
||||
"ckplayer": "^3.1.2",
|
||||
"dplayer": "^1.27.1",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pinia": "^3.0.3",
|
||||
"quill": "^2.0.3",
|
||||
@ -1777,6 +1778,12 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balloon-css": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/balloon-css/-/balloon-css-1.2.0.tgz",
|
||||
"integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
|
||||
@ -2139,6 +2146,28 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dplayer": {
|
||||
"version": "1.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/dplayer/-/dplayer-1.27.1.tgz",
|
||||
"integrity": "sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "1.2.3",
|
||||
"balloon-css": "^1.0.3",
|
||||
"promise-polyfill": "8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dplayer/node_modules/axios": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.2.3.tgz",
|
||||
"integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -3207,6 +3236,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-polyfill": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
|
||||
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"axios": "^1.11.0",
|
||||
"ckplayer": "^3.1.2",
|
||||
"dplayer": "^1.27.1",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pinia": "^3.0.3",
|
||||
"quill": "^2.0.3",
|
||||
|
BIN
public/images/profile/folder.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
public/images/profile/search.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 242 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 251 KiB |
Before Width: | Height: | Size: 40 KiB |
BIN
public/images/studys/切片 42@2x.png
Normal file
After Width: | Height: | Size: 250 KiB |
BIN
public/logo/云师大.jpg
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
public/logo/德宏师范.jpg
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
public/logo/曲靖师范.jpg
Normal file
After Width: | Height: | Size: 94 KiB |
46
public/subtitle/sample.vtt
Normal file
@ -0,0 +1,46 @@
|
||||
WEBVTT
|
||||
|
||||
00:00:01.000 --> 00:00:04.000
|
||||
欢迎使用 DPlayer 视频播放器
|
||||
|
||||
00:00:05.000 --> 00:00:08.000
|
||||
这是一个功能强大的 HTML5 视频播放器
|
||||
|
||||
00:00:09.000 --> 00:00:12.000
|
||||
支持多种格式和丰富的功能
|
||||
|
||||
00:00:13.000 --> 00:00:16.000
|
||||
包括倍速播放、清晰度切换等
|
||||
|
||||
00:00:17.000 --> 00:00:20.000
|
||||
感谢使用 DPlayer!
|
||||
|
||||
00:00:21.000 --> 00:00:24.000
|
||||
由 DIYGod 开发维护
|
||||
|
||||
00:00:25.000 --> 00:00:28.000
|
||||
开源免费,功能强大
|
||||
|
||||
00:00:29.000 --> 00:00:32.000
|
||||
支持中文字幕显示
|
||||
|
||||
00:00:33.000 --> 00:00:36.000
|
||||
界面美观,用户体验优秀
|
||||
|
||||
00:00:37.000 --> 00:00:40.000
|
||||
移动端适配良好
|
||||
|
||||
00:00:41.000 --> 00:00:44.000
|
||||
支持键盘快捷键操作
|
||||
|
||||
00:00:45.000 --> 00:00:48.000
|
||||
可以自定义主题和样式
|
||||
|
||||
00:00:49.000 --> 00:00:52.000
|
||||
支持弹幕功能
|
||||
|
||||
00:00:53.000 --> 00:00:56.000
|
||||
社区活跃,文档完善
|
||||
|
||||
00:00:57.000 --> 00:01:00.000
|
||||
感谢您的使用!
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 7.1 KiB |
BIN
src/assets/fonts/Alibaba_PuHuiTi_2.0_55_Regular_85_Bold.ttf
Normal file
238
src/components/VideoPlayerUpgraded.vue
Normal file
@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="video-player-wrapper">
|
||||
<div class="video-container" ref="videoContainer">
|
||||
<!-- DPlayer 播放器容器 -->
|
||||
<div v-if="videoUrl" ref="dplayerContainer" class="dplayer-container"></div>
|
||||
|
||||
<!-- 视频占位符 -->
|
||||
<div v-else class="video-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<div class="play-icon">▶</div>
|
||||
<p>{{ placeholder || '请选择要播放的视频' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
videoUrl?: string
|
||||
title?: string
|
||||
poster?: string
|
||||
autoplay?: boolean
|
||||
placeholder?: string
|
||||
useLocalVideo?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoplay: false
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
play: []
|
||||
pause: []
|
||||
ended: []
|
||||
error: [error: Event]
|
||||
}>()
|
||||
|
||||
// Refs
|
||||
const videoContainer = ref<HTMLDivElement>()
|
||||
const dplayerContainer = ref<HTMLDivElement>()
|
||||
let player: any = null
|
||||
|
||||
// 获取视频URL
|
||||
const videoUrl = computed(() => {
|
||||
if (props.useLocalVideo) {
|
||||
return '/video/first.mp4'
|
||||
}
|
||||
return props.videoUrl || ''
|
||||
})
|
||||
|
||||
// 监听视频URL变化
|
||||
watch(() => videoUrl.value, (newUrl) => {
|
||||
if (newUrl) {
|
||||
nextTick(() => {
|
||||
initDPlayer(newUrl)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 加载 DPlayer
|
||||
const loadDPlayer = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if ((window as any).DPlayer) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 加载 CSS
|
||||
const cssLink = document.createElement('link')
|
||||
cssLink.rel = 'stylesheet'
|
||||
cssLink.href = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css'
|
||||
document.head.appendChild(cssLink)
|
||||
|
||||
// 加载 JS
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('Failed to load DPlayer'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化 DPlayer
|
||||
const initDPlayer = async (url: string) => {
|
||||
if (!url || !dplayerContainer.value) return
|
||||
|
||||
try {
|
||||
// 动态加载 DPlayer
|
||||
await loadDPlayer()
|
||||
|
||||
// 销毁之前的播放器实例
|
||||
if (player) {
|
||||
player.destroy()
|
||||
player = null
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// 创建新的播放器
|
||||
const DPlayer = (window as any).DPlayer
|
||||
player = new DPlayer({
|
||||
container: dplayerContainer.value,
|
||||
video: {
|
||||
url: url,
|
||||
type: 'auto'
|
||||
},
|
||||
autoplay: props.autoplay,
|
||||
theme: '#007bff',
|
||||
lang: 'zh-cn',
|
||||
hotkey: true,
|
||||
preload: 'auto',
|
||||
volume: 0.8,
|
||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||
contextmenu: [
|
||||
{
|
||||
text: '关于 DPlayer',
|
||||
link: 'https://github.com/DIYGod/DPlayer'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 设置事件监听器
|
||||
setupEventListeners()
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize DPlayer:', err)
|
||||
emit('error', new Event('error'))
|
||||
}
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
const setupEventListeners = () => {
|
||||
if (!player) return
|
||||
|
||||
player.on('play', () => emit('play'))
|
||||
player.on('pause', () => emit('pause'))
|
||||
player.on('ended', () => emit('ended'))
|
||||
player.on('error', () => emit('error', new Event('error')))
|
||||
}
|
||||
|
||||
// 控制方法
|
||||
const play = () => player?.play()
|
||||
const pause = () => player?.pause()
|
||||
const seek = (time: number) => player?.seek(time)
|
||||
const setVolume = (vol: number) => player?.volume(vol / 100)
|
||||
const setPlaybackRate = (rate: number) => player?.speed(rate)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
if (videoUrl.value) {
|
||||
nextTick(() => {
|
||||
initDPlayer(videoUrl.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (player) {
|
||||
player.destroy()
|
||||
player = null
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ play, pause, seek, setVolume, setPlaybackRate })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-player-wrapper {
|
||||
width: 100%;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.dplayer-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* DPlayer 样式定制 */
|
||||
:deep(.dplayer) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.dplayer .dplayer-bezel) {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
:deep(.dplayer .dplayer-controller .dplayer-bar-wrap .dplayer-bar .dplayer-played) {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
:deep(.dplayer .dplayer-controller .dplayer-icons .dplayer-icon) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.dplayer .dplayer-controller .dplayer-icons .dplayer-icon:hover) {
|
||||
color: #007bff;
|
||||
}
|
||||
</style>
|
@ -38,7 +38,7 @@
|
||||
</form>
|
||||
|
||||
<div class="form-footer">
|
||||
<p>登录即代表同意我们的 <a href="#" class="link">《服务协议和隐私政策》</a></p>
|
||||
<p>登录即代表同意我们的 <a href="#" class="link">《服务协议隐私政策》</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -249,7 +249,7 @@ export default {
|
||||
border: 1px solid #D8D8D8;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #D9D9D9;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@
|
||||
</form>
|
||||
|
||||
<div class="form-footer">
|
||||
<p>注册即代表同意我们的 <a href="#" class="link">《服务协议和隐私政策》</a></p>
|
||||
<p>注册即代表同意我们的 <a href="#" class="link">《服务协议隐私政策》</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -287,12 +287,13 @@ export default {
|
||||
|
||||
.form-input {
|
||||
min-width: 278px;
|
||||
width: 278px;
|
||||
height: 41px;
|
||||
padding: 0 16px 0 30px;
|
||||
border: 1px solid #D8D8D8;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #D9D9D9;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
418
src/components/course/DPlayerVideo.vue
Normal file
@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="dplayer-video">
|
||||
<div class="video-container">
|
||||
<div ref="dplayerContainer" class="dplayer-wrapper"></div>
|
||||
<div v-if="!playerInitialized" class="video-placeholder"
|
||||
:style="{ backgroundImage: placeholderImage ? `url(${placeholderImage})` : '' }"
|
||||
@click="initializePlayer">
|
||||
<div class="placeholder-content">
|
||||
<div class="play-icon">
|
||||
<svg width="60" height="60" viewBox="0 0 60 60">
|
||||
<circle cx="30" cy="30" r="30" fill="rgba(255,255,255,0.9)" />
|
||||
<path d="M23 18l20 12-20 12V18z" fill="#1890ff" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>{{ placeholderText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 清晰度选择器 -->
|
||||
<div v-if="videoQualities.length > 1" class="video-quality-selector">
|
||||
<div class="quality-dropdown">
|
||||
<button class="quality-btn" @click="showQualityMenu = !showQualityMenu">
|
||||
{{ currentQuality }}p
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" class="dropdown-icon">
|
||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showQualityMenu" class="quality-menu">
|
||||
<div v-for="quality in videoQualities" :key="quality.value" class="quality-option"
|
||||
:class="{ active: quality.value === currentQuality }"
|
||||
@click="changeVideoQuality(quality.value); showQualityMenu = false">
|
||||
{{ quality.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
|
||||
interface VideoQuality {
|
||||
value: string
|
||||
label: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
videoUrl?: string
|
||||
placeholderImage?: string
|
||||
placeholderText?: string
|
||||
title?: string
|
||||
autoplay?: boolean
|
||||
videoQualities?: VideoQuality[]
|
||||
currentQuality?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
videoUrl: '',
|
||||
placeholderText: '请选择要播放的视频课程',
|
||||
title: '课程视频',
|
||||
autoplay: false,
|
||||
videoQualities: () => [],
|
||||
currentQuality: '360'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
play: []
|
||||
pause: []
|
||||
ended: []
|
||||
error: [error: any]
|
||||
qualityChange: [quality: string]
|
||||
}>()
|
||||
|
||||
const dplayerContainer = ref<HTMLDivElement>()
|
||||
let player: any = null
|
||||
const playerInitialized = ref(false)
|
||||
const isPlaying = ref(false)
|
||||
const showQualityMenu = ref(false)
|
||||
|
||||
// 加载 DPlayer
|
||||
const loadDPlayer = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if ((window as any).DPlayer) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 加载 CSS
|
||||
const cssLink = document.createElement('link')
|
||||
cssLink.rel = 'stylesheet'
|
||||
cssLink.href = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css'
|
||||
document.head.appendChild(cssLink)
|
||||
|
||||
// 加载 JS
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('Failed to load DPlayer'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化播放器
|
||||
const initializePlayer = async (videoUrl?: string) => {
|
||||
if (!videoUrl && !props.videoUrl) return
|
||||
|
||||
try {
|
||||
await loadDPlayer()
|
||||
await nextTick()
|
||||
|
||||
if (!dplayerContainer.value) return
|
||||
|
||||
const DPlayer = (window as any).DPlayer
|
||||
const url = videoUrl || props.videoUrl
|
||||
|
||||
// 清理之前的播放器实例
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy()
|
||||
} catch (e) {
|
||||
console.log('清理播放器实例时出错:', e)
|
||||
}
|
||||
player = null
|
||||
}
|
||||
|
||||
player = new DPlayer({
|
||||
container: dplayerContainer.value,
|
||||
video: {
|
||||
url: url,
|
||||
type: 'auto'
|
||||
},
|
||||
autoplay: props.autoplay,
|
||||
theme: '#007bff',
|
||||
lang: 'zh-cn',
|
||||
hotkey: true,
|
||||
preload: 'metadata',
|
||||
volume: 0.8,
|
||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||
loop: false,
|
||||
contextmenu: [
|
||||
{
|
||||
text: '关于 DPlayer',
|
||||
link: 'https://github.com/DIYGod/DPlayer'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 事件监听
|
||||
player.on('play', () => {
|
||||
isPlaying.value = true
|
||||
emit('play')
|
||||
})
|
||||
|
||||
player.on('pause', () => {
|
||||
isPlaying.value = false
|
||||
emit('pause')
|
||||
})
|
||||
|
||||
player.on('ended', () => {
|
||||
isPlaying.value = false
|
||||
emit('ended')
|
||||
})
|
||||
|
||||
player.on('error', (error: any) => {
|
||||
console.error('DPlayer error:', error)
|
||||
emit('error', error)
|
||||
})
|
||||
|
||||
playerInitialized.value = true
|
||||
console.log('DPlayer 初始化成功:', url)
|
||||
} catch (err) {
|
||||
console.error('初始化 DPlayer 失败:', err)
|
||||
emit('error', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换视频清晰度
|
||||
const changeVideoQuality = (quality: string) => {
|
||||
const qualityVideo = props.videoQualities.find(q => q.value === quality)
|
||||
if (qualityVideo && player) {
|
||||
try {
|
||||
// 尝试使用 DPlayer 的 switchVideo 方法
|
||||
if (typeof player.switchVideo === 'function') {
|
||||
player.switchVideo({
|
||||
url: qualityVideo.url,
|
||||
type: 'auto'
|
||||
})
|
||||
} else {
|
||||
// 如果没有 switchVideo 方法,重新初始化播放器
|
||||
initializePlayer(qualityVideo.url)
|
||||
}
|
||||
emit('qualityChange', quality)
|
||||
console.log('切换清晰度到:', quality)
|
||||
} catch (error) {
|
||||
console.error('切换清晰度失败:', error)
|
||||
// 重新初始化播放器作为后备方案
|
||||
initializePlayer(qualityVideo.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放控制方法
|
||||
const play = () => {
|
||||
if (player) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (player) {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
|
||||
const seek = (time: number) => {
|
||||
if (player) {
|
||||
player.seek(time)
|
||||
}
|
||||
}
|
||||
|
||||
const setVolume = (volume: number) => {
|
||||
if (player) {
|
||||
player.volume(volume / 100)
|
||||
}
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy()
|
||||
} catch (e) {
|
||||
console.log('销毁播放器时出错:', e)
|
||||
}
|
||||
player = null
|
||||
playerInitialized.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听视频URL变化
|
||||
watch(() => props.videoUrl, (newUrl) => {
|
||||
if (newUrl && playerInitialized.value) {
|
||||
initializePlayer(newUrl)
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
play,
|
||||
pause,
|
||||
seek,
|
||||
setVolume,
|
||||
destroy,
|
||||
initializePlayer
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 如果有视频URL,自动初始化播放器
|
||||
if (props.videoUrl) {
|
||||
initializePlayer()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dplayer-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 578px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.dplayer-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-placeholder::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.play-icon:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 清晰度选择器 */
|
||||
.video-quality-selector {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.quality-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quality-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.quality-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.quality-btn:hover .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.quality-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
min-width: 80px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.quality-option {
|
||||
padding: 8px 12px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.quality-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.quality-option.active {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.video-container {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.video-container {
|
||||
height: 350px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -39,9 +39,9 @@
|
||||
<span class="nav-item-ai">AI体验</span>
|
||||
</div>
|
||||
|
||||
<div class="nav-item" :class="{ active: activeKey === 'practice' }" @click="handleMenuSelect('practice')">
|
||||
<!-- <div class="nav-item" :class="{ active: activeKey === 'practice' }" @click="handleMenuSelect('practice')">
|
||||
<span class="nav-item-practice">AI伴学</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
@ -126,8 +126,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import {
|
||||
PersonOutline,
|
||||
LogOutOutline,
|
||||
MenuOutline,
|
||||
CloseOutline
|
||||
} from '@vicons/ionicons5'
|
||||
@ -189,7 +187,12 @@ const userMenuOptions = computed(() => [
|
||||
{
|
||||
label: '个人中心',
|
||||
key: 'profile',
|
||||
icon: () => h(PersonOutline)
|
||||
icon: () => h('div', { class: 'custom-icon' }, '👤')
|
||||
},
|
||||
{
|
||||
label: '切换教师端',
|
||||
key: 'teacher',
|
||||
icon: () => h('div', { class: 'custom-icon' }, '👨🏫')
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
@ -197,7 +200,7 @@ const userMenuOptions = computed(() => [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: () => h(LogOutOutline)
|
||||
icon: () => h('div', { class: 'custom-icon' }, '🚪')
|
||||
}
|
||||
])
|
||||
|
||||
@ -244,6 +247,12 @@ const handleUserMenuSelect = (key: string) => {
|
||||
window.location.reload();
|
||||
})
|
||||
break
|
||||
case 'teacher':
|
||||
// 切换教师端逻辑
|
||||
console.log('切换到教师端')
|
||||
// 这里可以添加切换教师端的逻辑,比如跳转到教师端页面
|
||||
router.push('/teacher')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
router.push('/')
|
||||
@ -305,71 +314,74 @@ onUnmounted(() => {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0 30px;
|
||||
height: 100%;
|
||||
height: 64px;
|
||||
background: white;
|
||||
position: relative;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-section {
|
||||
/* Logo区域 */
|
||||
.logo-section {
|
||||
flex-shrink: 0;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
.logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
.logo-image {
|
||||
width: 72px;
|
||||
height: 61px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
.nav-icon {
|
||||
max-width: 12px;
|
||||
max-height: 12px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/* AI图标样式 */
|
||||
.ai-icon {
|
||||
/* AI图标样式 */
|
||||
.ai-icon {
|
||||
max-width: 34px;
|
||||
max-height: 34px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
.action-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
flex: 1;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -383,72 +395,72 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 两个字的导航项:首页 */
|
||||
.nav-item:nth-child(1) {
|
||||
/* 两个字的导航项:首页 */
|
||||
.nav-item:nth-child(1) {
|
||||
width: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
|
||||
.nav-item:nth-child(2),
|
||||
.nav-item:nth-child(3),
|
||||
.nav-item:nth-child(4),
|
||||
.nav-item:nth-child(5) {
|
||||
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
|
||||
.nav-item:nth-child(2),
|
||||
.nav-item:nth-child(3),
|
||||
.nav-item:nth-child(4),
|
||||
.nav-item:nth-child(5) {
|
||||
width: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 两个字的导航项:活动 */
|
||||
.nav-item:nth-child(6) {
|
||||
/* 两个字的导航项:活动 */
|
||||
.nav-item:nth-child(6) {
|
||||
width: 36px;
|
||||
padding-right: 16px;
|
||||
/* 为HOT标签留出空间 */
|
||||
}
|
||||
}
|
||||
|
||||
/* AI导航项 */
|
||||
.nav-item:nth-child(7) {
|
||||
/* AI导航项 */
|
||||
.nav-item:nth-child(7) {
|
||||
/* width: 50px;
|
||||
height: 40px; */
|
||||
background-image: url('/images/ai/ai-bg.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:nth-child(8) {
|
||||
.nav-item:nth-child(8) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.active .nav-item-ai {
|
||||
.nav-item.active .nav-item-ai {
|
||||
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item-ai:hover {
|
||||
.nav-item-ai:hover {
|
||||
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
.nav-item:hover {
|
||||
color: #0084CD;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
.nav-item.active {
|
||||
color: #0084CD;
|
||||
font-weight: 400;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.active::after {
|
||||
.nav-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
@ -458,9 +470,9 @@ onUnmounted(() => {
|
||||
height: 2px;
|
||||
background-color: #0084CD;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
.new-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -22px;
|
||||
@ -470,55 +482,55 @@ onUnmounted(() => {
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
border-left: 1px solid #ececec;
|
||||
border-right: 1px solid #ececec;
|
||||
.search-box {
|
||||
/* border-left: 1px solid #ececec;
|
||||
border-right: 1px solid #ececec; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22px 16px;
|
||||
width: 280px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
/* .search-box:hover {
|
||||
/* .search-box:hover {
|
||||
background: #eeeeee;
|
||||
} */
|
||||
} */
|
||||
|
||||
.search-icon {
|
||||
.search-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端汉堡菜单按钮 */
|
||||
.mobile-menu-toggle {
|
||||
/* 移动端汉堡菜单按钮 */
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -527,21 +539,21 @@ onUnmounted(() => {
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-toggle:hover {
|
||||
.mobile-menu-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧操作区域 */
|
||||
.header-actions {
|
||||
/* 右侧操作区域 */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item {
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@ -552,20 +564,20 @@ onUnmounted(() => {
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
.action-item:hover {
|
||||
color: #1890ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 语言切换器 */
|
||||
.language-switcher {
|
||||
/* 语言切换器 */
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
.language-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
@ -577,38 +589,38 @@ onUnmounted(() => {
|
||||
z-index: 1000;
|
||||
margin-top: 4px;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option {
|
||||
.language-option {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option:last-child {
|
||||
.language-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
.language-option:hover {
|
||||
background: #f0f8ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.language-text {
|
||||
.language-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 认证按钮 */
|
||||
.auth-buttons {
|
||||
/* 认证按钮 */
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-combined-btn {
|
||||
.auth-combined-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0088D1;
|
||||
@ -619,37 +631,37 @@ onUnmounted(() => {
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-combined-btn:hover {
|
||||
.auth-combined-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-login,
|
||||
.auth-register {
|
||||
.auth-login,
|
||||
.auth-register {
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-login:hover,
|
||||
.auth-register:hover {
|
||||
.auth-login:hover,
|
||||
.auth-register:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
.auth-divider {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 4px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@ -657,18 +669,63 @@ onUnmounted(() => {
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
.user-info:hover {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 美化用户菜单样式 */
|
||||
:deep(.n-dropdown-menu) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option) {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option:hover) {
|
||||
background: linear-gradient(135deg, #f0f8ff, #e6f4ff);
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option .n-dropdown-option-icon) {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* 分割线样式 */
|
||||
:deep(.n-dropdown-divider) {
|
||||
margin: 8px 0;
|
||||
border-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* 大屏幕 */
|
||||
@media (min-width: 1200px) {
|
||||
|
@ -40,7 +40,7 @@ import AppFooter from './AppFooter.vue'
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
|
||||
position: sticky;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
flex-shrink: 0;
|
||||
@ -66,12 +66,19 @@ import AppFooter from './AppFooter.vue'
|
||||
.header {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding-top: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (max-width: 480px) {
|
||||
.header {
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏模式样式现在在App.vue中统一管理 */
|
||||
|
@ -7,7 +7,7 @@
|
||||
"resources": "精选资源",
|
||||
"about": "活动",
|
||||
"languageSwitch": "切换语言",
|
||||
"learningCenter": "学习中心",
|
||||
"learningCenter": "积分中心",
|
||||
"management": "管理端",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
|
@ -12,6 +12,7 @@ import '@/assets/fonts/AlimamaShuHeiTi-Bold.ttf'
|
||||
import '@/assets/fonts/文道潮黑.ttf'
|
||||
import '@/assets/fonts/庞门正道标题体3.0.ttf'
|
||||
import '@/assets/fonts/DouyinSansBold.otf'
|
||||
import '@/assets/fonts/Alibaba_PuHuiTi_2.0_55_Regular_85_Bold.ttf'
|
||||
|
||||
// Naive UI
|
||||
import {
|
||||
|
@ -23,6 +23,7 @@ import ExamNotice from '@/views/ExamNotice.vue'
|
||||
import ExamSubmitted from '@/views/ExamSubmitted.vue'
|
||||
import TestSections from '@/views/TestSections.vue'
|
||||
import LocalVideoDemo from '@/views/LocalVideoDemo.vue'
|
||||
import DPlayerTest from '@/views/DPlayerTest.vue'
|
||||
import SpecialTraining from '@/views/SpecialTraining.vue'
|
||||
import SpecialTrainingDetail from '@/views/SpecialTrainingDetail.vue'
|
||||
import HelpCenter from '@/views/HelpCenter.vue'
|
||||
@ -250,6 +251,14 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '本地视频播放演示'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dplayer-test',
|
||||
name: 'DPlayerTest',
|
||||
component: DPlayerTest,
|
||||
meta: {
|
||||
title: 'DPlayer 测试页面'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/course/:courseId/practice/:sectionId',
|
||||
name: 'Practice',
|
||||
|
@ -289,7 +289,7 @@ onMounted(() => {
|
||||
margin: auto;
|
||||
width: 1420px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
@ -420,6 +420,11 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
src: url('/fonts/AlimamaShuHeiTiBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
body * {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
@ -852,7 +857,7 @@ button:active {
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
font-size: 32px;
|
||||
font-family: AlimamaShuHeiTi-Bold;
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
@ -132,9 +132,9 @@
|
||||
|
||||
<!-- 课程描述 -->
|
||||
<div class="course-description">
|
||||
<p>{{ course.description ||
|
||||
'本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书,技能大赛紧密结合,课程设置紧密对应实际全面共享,可为职业工作人员、在校学生、创行教师提供服务与学习支持。'
|
||||
}}</p>
|
||||
<p
|
||||
v-html="course.description || '本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书,技能大赛紧密结合,课程设置紧密对应实际全面共享,可为职业工作人员、在校学生、创行教师提供服务与学习支持。'">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 讲师信息 -->
|
||||
@ -2078,7 +2078,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.instructor-info {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.instructor-name {
|
||||
|
@ -33,42 +33,13 @@
|
||||
<!-- 视频播放器区域 - 已报名状态 -->
|
||||
<div class="video-player-section">
|
||||
<div class="video-player enrolled">
|
||||
<div class="video-container">
|
||||
<!-- CKPlayer 容器 -->
|
||||
<div v-if="currentVideoUrl" id="ckplayer_container" class="ckplayer-container">
|
||||
</div>
|
||||
<div v-else class="video-placeholder"
|
||||
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
||||
<div class="placeholder-content">
|
||||
<div class="play-icon">
|
||||
<svg width="60" height="60" viewBox="0 0 60 60">
|
||||
<circle cx="30" cy="30" r="30" fill="rgba(255,255,255,0.9)" />
|
||||
<path d="M23 18l20 12-20 12V18z" fill="#1890ff" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>请选择要播放的视频课程</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 清晰度选择器 -->
|
||||
<div v-if="videoQualities.length > 1" class="video-quality-selector">
|
||||
<div class="quality-dropdown">
|
||||
<button class="quality-btn" @click="showQualityMenu = !showQualityMenu">
|
||||
{{ currentQuality }}p
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" class="dropdown-icon">
|
||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showQualityMenu" class="quality-menu">
|
||||
<div v-for="quality in videoQualities" :key="quality.value" class="quality-option"
|
||||
:class="{ active: quality.value === currentQuality }"
|
||||
@click="changeVideoQuality(quality.value); showQualityMenu = false">
|
||||
{{ quality.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- DPlayer 视频播放器 -->
|
||||
<DPlayerVideo ref="videoPlayerRef" :video-url="currentVideoUrl"
|
||||
:placeholder-image="course?.coverImage || course?.thumbnail"
|
||||
:placeholder-text="currentVideoUrl ? '' : '请选择要播放的视频课程'" :title="currentSection?.name || '课程视频'"
|
||||
:video-qualities="videoQualities" :current-quality="currentQuality" @play="handleVideoPlay"
|
||||
@pause="handleVideoPause" @ended="handleVideoEnded" @error="handleVideoError"
|
||||
@quality-change="handleQualityChange" />
|
||||
|
||||
<!-- 底部交互区域 -->
|
||||
<div class="video-interaction-bar">
|
||||
@ -136,9 +107,9 @@
|
||||
|
||||
<!-- 课程描述 -->
|
||||
<div class="course-description">
|
||||
<p>{{ course.description ||
|
||||
'本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书,技能大赛紧密结合,课程设置紧密对应实际全面共享,可为职业工作人员、在校学生、创行教师提供服务与学习支持。'
|
||||
}}</p>
|
||||
<p
|
||||
v-html="course.description || '本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书,技能大赛紧密结合,课程设置紧密对应实际全面共享,可为职业工作人员、在校学生、创行教师提供服务与学习支持。'">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 讲师信息 -->
|
||||
@ -444,7 +415,7 @@
|
||||
<div class="lesson-actions">
|
||||
<!-- 视频播放图标 - 可点击 -->
|
||||
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
|
||||
@click.stop="handleVideoPlay(section)">
|
||||
@click.stop="handleVideoPlaySection(section)">
|
||||
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
|
||||
</button>
|
||||
<!-- 下载图标 - 可点击 -->
|
||||
@ -560,16 +531,10 @@ import type { Course, CourseSection, SectionVideo, VideoQuality } from '@/api/ty
|
||||
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
||||
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
||||
import NotesModal from '@/components/common/NotesModal.vue'
|
||||
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
|
||||
|
||||
// 声明全局CKPlayer类型
|
||||
declare global {
|
||||
interface Window {
|
||||
ckplayer: any
|
||||
loadedHandler: () => void
|
||||
endedHandler: () => void
|
||||
errorHandler: (error: any) => void
|
||||
}
|
||||
}
|
||||
// DPlayer 视频播放器引用
|
||||
const videoPlayerRef = ref<InstanceType<typeof DPlayerVideo>>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@ -582,7 +547,6 @@ const FORCE_LOCAL_VIDEO = true
|
||||
// 当前选中的章节和视频
|
||||
const currentSection = ref<CourseSection | null>(null)
|
||||
const currentVideoUrl = ref<string>('')
|
||||
const ckplayer = ref<any>(null)
|
||||
|
||||
// 视频相关状态
|
||||
const currentVideo = ref<SectionVideo | null>(null)
|
||||
@ -611,6 +575,39 @@ const getVideoUrl = (section?: CourseSection) => {
|
||||
// return VIDEO_CONFIG.HLS
|
||||
}
|
||||
|
||||
// DPlayer 事件处理方法
|
||||
const handleVideoPlay = () => {
|
||||
console.log('视频开始播放')
|
||||
}
|
||||
|
||||
const handleVideoPause = () => {
|
||||
console.log('视频暂停')
|
||||
}
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
console.log('视频播放结束')
|
||||
// 可以在这里处理播放完成后的逻辑,比如标记为已完成
|
||||
if (currentSection.value && !currentSection.value.completed) {
|
||||
currentSection.value.completed = true
|
||||
const completed = courseSections.value.filter((s: any) => s.completed).length
|
||||
completedLessons.value = completed
|
||||
progress.value = Math.round((completed / courseSections.value.length) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
const handleVideoError = (error: any) => {
|
||||
console.error('视频播放错误:', error)
|
||||
// 可以在这里处理错误,比如自动切换到本地视频
|
||||
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
|
||||
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
|
||||
}
|
||||
}
|
||||
|
||||
const handleQualityChange = (quality: string) => {
|
||||
currentQuality.value = quality
|
||||
console.log('清晰度切换到:', quality)
|
||||
}
|
||||
|
||||
// 课程数据相关状态
|
||||
const course = ref<Course | null>(null)
|
||||
const loading = ref(false)
|
||||
@ -922,8 +919,6 @@ const loadCourseSections = async () => {
|
||||
if (firstVideo) {
|
||||
currentSection.value = firstVideo
|
||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -966,7 +961,6 @@ const loadMockData = () => {
|
||||
if (firstVideo) {
|
||||
currentSection.value = firstVideo
|
||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||
setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1038,39 +1032,14 @@ const updateVideoPlayer = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔍 更新播放器视频源:', currentVideoUrl.value)
|
||||
console.log('🔍 更新 DPlayer 视频源:', currentVideoUrl.value)
|
||||
|
||||
if (ckplayer.value) {
|
||||
// 尝试不同的CKPlayer API方法
|
||||
if (typeof ckplayer.value.newVideo === 'function') {
|
||||
console.log('✅ 使用newVideo方法更新视频源')
|
||||
ckplayer.value.newVideo(currentVideoUrl.value)
|
||||
} else if (typeof ckplayer.value.changeVideo === 'function') {
|
||||
console.log('✅ 使用changeVideo方法更新视频源')
|
||||
ckplayer.value.changeVideo(currentVideoUrl.value)
|
||||
} else if (typeof ckplayer.value.videoSrc === 'function') {
|
||||
console.log('✅ 使用videoSrc方法更新视频源')
|
||||
ckplayer.value.videoSrc(currentVideoUrl.value)
|
||||
} else {
|
||||
console.log('⚠️ 未找到合适的更新方法,重新初始化播放器')
|
||||
// 如果没有找到合适的方法,重新初始化播放器
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
} else {
|
||||
console.log('🔍 播放器未初始化,开始初始化')
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
if (videoPlayerRef.value) {
|
||||
// 使用 DPlayer 组件的 initializePlayer 方法
|
||||
await videoPlayerRef.value.initializePlayer(currentVideoUrl.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 更新播放器失败:', error)
|
||||
// 如果更新失败,尝试重新初始化
|
||||
try {
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
} catch (initError) {
|
||||
console.error('❌ 重新初始化播放器也失败:', initError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1161,10 +1130,10 @@ const handleSectionClick = (section: CourseSection) => {
|
||||
type: section.type
|
||||
})
|
||||
|
||||
// 如果是视频课程,加载视频数据
|
||||
// 如果是视频课程,播放视频
|
||||
if (isVideo) {
|
||||
console.log('✅ 识别为视频课程,开始加载视频数据')
|
||||
loadSectionVideo(section)
|
||||
console.log('✅ 识别为视频课程,开始播放视频')
|
||||
handleVideoPlaySection(section)
|
||||
} else if (isResource) {
|
||||
console.log('✅ 识别为资料课程')
|
||||
handleDownload(section)
|
||||
@ -1176,12 +1145,12 @@ const handleSectionClick = (section: CourseSection) => {
|
||||
handleExam(section)
|
||||
} else {
|
||||
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
|
||||
loadSectionVideo(section)
|
||||
handleVideoPlaySection(section)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理视频播放 - 已报名状态,可以正常播放
|
||||
const handleVideoPlay = async (section: CourseSection) => {
|
||||
const handleVideoPlaySection = async (section: CourseSection) => {
|
||||
console.log('播放视频:', section.name)
|
||||
|
||||
// 获取视频URL
|
||||
@ -1191,11 +1160,11 @@ const handleVideoPlay = async (section: CourseSection) => {
|
||||
|
||||
console.log('使用视频源:', videoUrl)
|
||||
|
||||
// 等待DOM更新
|
||||
// 等待DOM更新后初始化播放器
|
||||
await nextTick()
|
||||
|
||||
// 初始化CKPlayer播放器
|
||||
initCKPlayer(videoUrl)
|
||||
if (videoPlayerRef.value) {
|
||||
await videoPlayerRef.value.initializePlayer(videoUrl)
|
||||
}
|
||||
|
||||
// 标记为已完成
|
||||
if (!section.completed) {
|
||||
@ -1207,95 +1176,7 @@ const handleVideoPlay = async (section: CourseSection) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化CKPlayer播放器
|
||||
const initCKPlayer = (url: string) => {
|
||||
// 清理之前的播放器实例
|
||||
if (ckplayer.value) {
|
||||
try {
|
||||
ckplayer.value.remove()
|
||||
} catch (e) {
|
||||
console.log('清理播放器实例时出错:', e)
|
||||
}
|
||||
ckplayer.value = null
|
||||
}
|
||||
|
||||
// 检查CKPlayer是否已加载
|
||||
if (typeof window.ckplayer === 'undefined') {
|
||||
console.error('CKPlayer not loaded')
|
||||
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 isHLS = url.endsWith('.m3u8')
|
||||
|
||||
// CKPlayer配置
|
||||
const videoObject = {
|
||||
container: '#ckplayer_container', // 容器ID
|
||||
autoplay: false, // 自动播放
|
||||
video: url, // 视频地址
|
||||
volume: 0.8, // 音量
|
||||
poster: course.value?.coverImage || course.value?.thumbnail || '', // 封面图
|
||||
live: false, // 是否直播
|
||||
// 根据视频格式选择插件
|
||||
plug: isHLS ? 'hls.js' : '', // HLS使用hls.js插件,MP4不需要插件
|
||||
playbackrateOpen: true, // 开启倍速选项
|
||||
playbackrateList: [0.5, 0.75, 1, 1.25, 1.5, 2], // 播放速度选项
|
||||
seek: 0, // 默认跳转秒数
|
||||
loaded: 'loadedHandler', // 播放器加载完成回调
|
||||
ended: 'endedHandler', // 播放结束回调
|
||||
error: 'errorHandler', // 播放错误回调
|
||||
title: currentSection.value?.name || '课程视频', // 视频标题
|
||||
controls: true, // 显示控制栏
|
||||
webFull: true, // 启用页面全屏
|
||||
screenshot: true, // 启用截图功能
|
||||
timeScheduleAdjust: 1, // 允许调节播放进度
|
||||
// MP4视频的额外配置
|
||||
...(isMP4 && {
|
||||
type: 'mp4', // 明确指定视频类型
|
||||
crossOrigin: 'anonymous' // 跨域设置
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建播放器实例
|
||||
ckplayer.value = new window.ckplayer(videoObject)
|
||||
console.log('CKPlayer initialized successfully for:', isMP4 ? 'MP4' : 'HLS')
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize CKPlayer:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// CKPlayer回调函数
|
||||
window.loadedHandler = () => {
|
||||
console.log('CKPlayer loaded successfully')
|
||||
}
|
||||
|
||||
window.endedHandler = () => {
|
||||
console.log('Video playback ended')
|
||||
}
|
||||
|
||||
window.errorHandler = (error: any) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理资源下载
|
||||
const handleDownload = (section: CourseSection) => {
|
||||
@ -1399,26 +1280,19 @@ const saveNote = (content: string) => {
|
||||
onMounted(async () => {
|
||||
console.log('已报名课程详情页加载完成,课程ID:', courseId.value)
|
||||
initializeEnrolledState() // 初始化已报名状态
|
||||
// 若强制播放本地视频,优先初始化本地源
|
||||
// 若强制播放本地视频,优先设置本地源
|
||||
if (FORCE_LOCAL_VIDEO) {
|
||||
currentSection.value = null
|
||||
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
loadCourseDetail()
|
||||
loadCourseSections()
|
||||
})
|
||||
|
||||
// 组件卸载时清理CKPlayer实例
|
||||
// 组件卸载时清理播放器实例
|
||||
onUnmounted(() => {
|
||||
if (ckplayer.value) {
|
||||
try {
|
||||
ckplayer.value.remove()
|
||||
} catch (e) {
|
||||
console.log('清理播放器实例时出错:', e)
|
||||
}
|
||||
ckplayer.value = null
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -1496,127 +1370,7 @@ onUnmounted(() => {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.ckplayer-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 背景图片设置 */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/* 如果没有背景图片,使用默认渐变背景 */
|
||||
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-placeholder::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
margin-bottom: 16px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.play-icon:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 清晰度选择器 */
|
||||
.video-quality-selector {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.quality-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quality-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.quality-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.quality-btn:hover .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.quality-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
min-width: 80px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.quality-option {
|
||||
padding: 8px 12px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.quality-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.quality-option.active {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 课程信息区域 */
|
||||
.course-info-section {
|
||||
|
@ -17,15 +17,9 @@
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">类型:</span>
|
||||
<div class="filter-tags">
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }"
|
||||
@click="selectMajor('全部')">全部</span>
|
||||
<span
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: selectedMajor === category.name }"
|
||||
@click="selectMajor(category.name)"
|
||||
>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }" @click="selectMajor('全部')">全部</span>
|
||||
<span v-for="category in categories" :key="category.id" class="filter-tag"
|
||||
:class="{ active: selectedMajor === category.name }" @click="selectMajor(category.name)">
|
||||
{{ category.name }}
|
||||
</span>
|
||||
<!-- 加载状态 -->
|
||||
@ -37,14 +31,10 @@
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">专题:</span>
|
||||
<div class="filter-tags">
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }" @click="selectSubject('全部')">全部</span>
|
||||
<span
|
||||
v-for="subject in subjects"
|
||||
:key="subject.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: selectedSubject === subject.name }"
|
||||
@click="selectSubject(subject.name)"
|
||||
>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }"
|
||||
@click="selectSubject('全部')">全部</span>
|
||||
<span v-for="subject in subjects" :key="subject.id" class="filter-tag"
|
||||
:class="{ active: selectedSubject === subject.name }" @click="selectSubject(subject.name)">
|
||||
{{ subject.name }}
|
||||
</span>
|
||||
<!-- 加载状态 -->
|
||||
@ -59,13 +49,8 @@
|
||||
<div class="filter-tags">
|
||||
<span class="filter-tag" :class="{ active: selectedDifficulty === '全部' }"
|
||||
@click="selectDifficulty('全部')">全部</span>
|
||||
<span
|
||||
v-for="difficulty in difficulties"
|
||||
:key="difficulty.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: selectedDifficulty === difficulty.name }"
|
||||
@click="selectDifficulty(difficulty.name)"
|
||||
>
|
||||
<span v-for="difficulty in difficulties" :key="difficulty.id" class="filter-tag"
|
||||
:class="{ active: selectedDifficulty === difficulty.name }" @click="selectDifficulty(difficulty.name)">
|
||||
{{ difficulty.name }}
|
||||
</span>
|
||||
<!-- 加载状态 -->
|
||||
@ -89,9 +74,10 @@
|
||||
|
||||
<!-- 排序标签 -->
|
||||
<div class="sort-tabs">
|
||||
<span class="sort-tab">最新</span>
|
||||
<span class="sort-tab">最热</span>
|
||||
<span class="sort-tab active">推荐</span>
|
||||
<span class="sort-tab" :class="{ active: selectedSort === 'latest' }" @click="selectSort('latest')">最新</span>
|
||||
<span class="sort-tab" :class="{ active: selectedSort === 'hot' }" @click="selectSort('hot')">最热</span>
|
||||
<span class="sort-tab" :class="{ active: selectedSort === 'recommended' }"
|
||||
@click="selectSort('recommended')">推荐</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
@ -201,6 +187,9 @@ const selectedSubject = ref('全部')
|
||||
const selectedMajor = ref('全部')
|
||||
const selectedDifficulty = ref('全部')
|
||||
|
||||
// 排序状态
|
||||
const selectedSort = ref('recommended')
|
||||
|
||||
// 分页相关状态
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 20
|
||||
@ -292,6 +281,12 @@ const loadCourses = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据选择的排序方式添加sort参数
|
||||
if (selectedSort.value) {
|
||||
queryParams.sort = selectedSort.value
|
||||
console.log('📊 选择的排序方式:', selectedSort.value)
|
||||
}
|
||||
|
||||
console.log('🔍 查询参数:', queryParams)
|
||||
|
||||
// 调用API
|
||||
@ -356,6 +351,7 @@ const clearAllFilters = () => {
|
||||
selectedSubject.value = '全部'
|
||||
selectedMajor.value = '全部'
|
||||
selectedDifficulty.value = '全部'
|
||||
selectedSort.value = 'recommended' // 重置排序为推荐
|
||||
currentPage.value = 1
|
||||
loadCourses()
|
||||
}
|
||||
@ -397,6 +393,13 @@ const goToCourseDetail = (course: Course) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 选择排序方式
|
||||
const selectSort = (sortType: string) => {
|
||||
selectedSort.value = sortType
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
loadCourses() // 重新加载课程数据
|
||||
}
|
||||
|
||||
// 加载课程分类数据
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
@ -473,6 +476,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@font-face {
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
src: url('/fonts/AlimamaShuHeiTiBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
|
||||
.courses-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
@ -510,6 +519,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.page-title {
|
||||
/* 数黑体 */
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
font-size: 28px;
|
||||
margin: 35px 0 5px 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
357
src/views/DPlayerDemo.vue
Normal file
@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<div class="dplayer-demo">
|
||||
<div class="container">
|
||||
<h1>DPlayer 视频播放器演示</h1>
|
||||
<p class="description">
|
||||
这个页面演示了如何使用 DPlayer 播放器替代 CKPlayer,提供更好的用户体验。
|
||||
</p>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>DPlayer 播放器</h2>
|
||||
<div class="video-wrapper">
|
||||
<VideoPlayerUpgraded
|
||||
:use-local-video="true"
|
||||
title="DPlayer 演示"
|
||||
description="使用 DPlayer 播放器播放本地视频文件"
|
||||
:autoplay="false"
|
||||
@play="onPlay"
|
||||
@pause="onPause"
|
||||
@ended="onEnded"
|
||||
@error="onError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>DPlayer 特性</h3>
|
||||
<ul>
|
||||
<li>🎨 界面美观,现代化设计</li>
|
||||
<li>🎯 轻量级,加载速度快</li>
|
||||
<li>🌏 中文友好,文档完善</li>
|
||||
<li>⌨️ 支持键盘快捷键</li>
|
||||
<li>📱 移动端适配优秀</li>
|
||||
<li>🎮 支持倍速播放</li>
|
||||
<li>🎨 可自定义主题色</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="controls-section">
|
||||
<h3>播放控制</h3>
|
||||
<div class="control-buttons">
|
||||
<button @click="playVideo" class="control-btn">播放</button>
|
||||
<button @click="pauseVideo" class="control-btn">暂停</button>
|
||||
<button @click="seekVideo(30)" class="control-btn">跳转到30秒</button>
|
||||
<button @click="setVideoVolume(50)" class="control-btn">音量50%</button>
|
||||
<button @click="setPlaybackRate(1.5)" class="control-btn">1.5倍速</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<h3>播放状态</h3>
|
||||
<div class="status-info">
|
||||
<div class="status-item">
|
||||
<strong>播放状态:</strong> {{ isPlaying ? '播放中' : '已暂停' }}
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<strong>错误信息:</strong> {{ errorMessage || '无' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-section">
|
||||
<h3>与 CKPlayer 对比</h3>
|
||||
<div class="comparison-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>特性</th>
|
||||
<th>CKPlayer</th>
|
||||
<th>DPlayer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>界面美观度</td>
|
||||
<td>⭐⭐</td>
|
||||
<td>⭐⭐⭐⭐⭐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>功能丰富度</td>
|
||||
<td>⭐⭐⭐</td>
|
||||
<td>⭐⭐⭐⭐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>移动端支持</td>
|
||||
<td>⭐⭐⭐</td>
|
||||
<td>⭐⭐⭐⭐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>中文支持</td>
|
||||
<td>⭐⭐⭐⭐</td>
|
||||
<td>⭐⭐⭐⭐⭐</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>社区活跃度</td>
|
||||
<td>⭐⭐</td>
|
||||
<td>⭐⭐⭐⭐</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import VideoPlayerUpgraded from '@/components/VideoPlayerUpgraded.vue'
|
||||
|
||||
// 播放状态
|
||||
const isPlaying = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 视频播放器引用
|
||||
const videoPlayerRef = ref<InstanceType<typeof VideoPlayerUpgraded>>()
|
||||
|
||||
// 事件处理
|
||||
const onPlay = () => {
|
||||
isPlaying.value = true
|
||||
console.log('DPlayer 视频开始播放')
|
||||
}
|
||||
|
||||
const onPause = () => {
|
||||
isPlaying.value = false
|
||||
console.log('DPlayer 视频暂停')
|
||||
}
|
||||
|
||||
const onEnded = () => {
|
||||
isPlaying.value = false
|
||||
console.log('DPlayer 视频播放结束')
|
||||
}
|
||||
|
||||
const onError = (error: Event) => {
|
||||
errorMessage.value = 'DPlayer 视频播放出错'
|
||||
console.error('DPlayer 视频播放错误:', error)
|
||||
}
|
||||
|
||||
// 控制方法
|
||||
const playVideo = () => {
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.play()
|
||||
}
|
||||
}
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.pause()
|
||||
}
|
||||
}
|
||||
|
||||
const seekVideo = (time: number) => {
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.seek(time)
|
||||
}
|
||||
}
|
||||
|
||||
const setVideoVolume = (volume: number) => {
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.setVolume(volume)
|
||||
}
|
||||
}
|
||||
|
||||
const setPlaybackRate = (rate: number) => {
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.setPlaybackRate(rate)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dplayer-demo {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-section li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-section li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controls-section h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 10px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-section h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comparison-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.comparison-section h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.comparison-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comparison-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
502
src/views/DPlayerTest.vue
Normal file
@ -0,0 +1,502 @@
|
||||
<template>
|
||||
<div class="dplayer-test">
|
||||
<div class="container">
|
||||
<h1>DPlayer 测试页面</h1>
|
||||
<p>测试 DPlayer 视频播放器功能</p>
|
||||
|
||||
<div class="video-section">
|
||||
<div ref="dplayerContainer" class="dplayer-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>基础控制</h3>
|
||||
<button @click="play">播放</button>
|
||||
<button @click="pause">暂停</button>
|
||||
<button @click="seek(30)">跳转30秒</button>
|
||||
<button @click="setVolume(50)">音量50%</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>快进控制</h3>
|
||||
<button @click="seekBackward(10)">后退10秒</button>
|
||||
<button @click="seekForward(10)">前进10秒</button>
|
||||
<button @click="seekBackward(30)">后退30秒</button>
|
||||
<button @click="seekForward(30)">前进30秒</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>倍速控制</h3>
|
||||
<button @click="setPlaybackRate(0.5)">0.5x</button>
|
||||
<button @click="setPlaybackRate(0.75)">0.75x</button>
|
||||
<button @click="setPlaybackRate(1)">1x</button>
|
||||
<button @click="setPlaybackRate(1.25)">1.25x</button>
|
||||
<button @click="setPlaybackRate(1.5)">1.5x</button>
|
||||
<button @click="setPlaybackRate(2)">2x</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>清晰度切换</h3>
|
||||
<button @click="switchQuality(0)">1080P</button>
|
||||
<button @click="switchQuality(1)">720P</button>
|
||||
<button @click="switchQuality(2)">480P</button>
|
||||
<button @click="switchQuality(3)">360P</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>字幕控制</h3>
|
||||
<button @click="toggleSubtitle">切换字幕</button>
|
||||
<button @click="showSubtitle">显示字幕</button>
|
||||
<button @click="hideSubtitle">隐藏字幕</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>弹幕控制 (模拟)</h3>
|
||||
<button @click="toggleDanmaku">切换弹幕</button>
|
||||
<button @click="showDanmaku">显示弹幕</button>
|
||||
<button @click="hideDanmaku">隐藏弹幕</button>
|
||||
<button @click="sendDanmaku('测试弹幕', '#fff', 0)">发送白色弹幕</button>
|
||||
<button @click="sendDanmaku('红色弹幕', '#e54256', 0)">发送红色弹幕</button>
|
||||
<button @click="sendDanmaku('顶部弹幕', '#ffe133', 1)">发送顶部弹幕</button>
|
||||
<button @click="sendDanmaku('底部弹幕', '#64DD17', 2)">发送底部弹幕</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>弹幕设置 (模拟)</h3>
|
||||
<button @click="setDanmakuOpacity(0.5)">透明度50%</button>
|
||||
<button @click="setDanmakuOpacity(0.8)">透明度80%</button>
|
||||
<button @click="setDanmakuOpacity(1)">透明度100%</button>
|
||||
<button @click="setDanmakuFontSize(20)">字体20px</button>
|
||||
<button @click="setDanmakuFontSize(24)">字体24px</button>
|
||||
<button @click="setDanmakuFontSize(28)">字体28px</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h3>播放状态</h3>
|
||||
<p>播放状态: {{ isPlaying ? '播放中' : '已暂停' }}</p>
|
||||
<p>当前倍速: {{ currentPlaybackRate }}x</p>
|
||||
<p>当前清晰度: {{ currentQuality }}</p>
|
||||
<p>字幕状态: {{ subtitleVisible ? '显示' : '隐藏' }}</p>
|
||||
<p>弹幕状态: {{ danmakuVisible ? '显示' : '隐藏' }} (模拟)</p>
|
||||
<p>错误信息: {{ errorMessage || '无' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const dplayerContainer = ref<HTMLDivElement>()
|
||||
let player: any = null
|
||||
const isPlaying = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const currentPlaybackRate = ref(1)
|
||||
const currentQuality = ref('1080P')
|
||||
const subtitleVisible = ref(false)
|
||||
const danmakuVisible = ref(true)
|
||||
|
||||
// 视频质量配置
|
||||
const videoQualities = [
|
||||
{ name: '1080P', url: '/video/first.mp4' },
|
||||
{ name: '720P', url: '/video/first.mp4' }, // 实际项目中应该是不同的视频文件
|
||||
{ name: '480P', url: '/video/first.mp4' },
|
||||
{ name: '360P', url: '/video/first.mp4' }
|
||||
]
|
||||
|
||||
// 字幕配置
|
||||
const subtitleConfig = {
|
||||
url: '/subtitle/sample.vtt', // 示例字幕文件
|
||||
type: 'webvtt',
|
||||
fontSize: '20px',
|
||||
bottom: '10%',
|
||||
color: '#fff'
|
||||
}
|
||||
|
||||
// 加载 DPlayer
|
||||
const loadDPlayer = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if ((window as any).DPlayer) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 加载 CSS
|
||||
const cssLink = document.createElement('link')
|
||||
cssLink.rel = 'stylesheet'
|
||||
cssLink.href = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css'
|
||||
document.head.appendChild(cssLink)
|
||||
|
||||
// 加载 JS
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('Failed to load DPlayer'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化播放器
|
||||
const initPlayer = async () => {
|
||||
try {
|
||||
await loadDPlayer()
|
||||
|
||||
if (!dplayerContainer.value) return
|
||||
|
||||
const DPlayer = (window as any).DPlayer
|
||||
player = new DPlayer({
|
||||
container: dplayerContainer.value,
|
||||
video: {
|
||||
url: '/video/first.mp4',
|
||||
type: 'auto'
|
||||
},
|
||||
subtitle: subtitleConfig,
|
||||
// 暂时禁用弹幕 API,避免网络错误
|
||||
// danmaku: {
|
||||
// id: 'dplayer-danmaku',
|
||||
// api: 'https://dplayer-mate.vercel.app/api/dplayer',
|
||||
// token: 'tokendemo',
|
||||
// maximum: 1000,
|
||||
// user: 'DIYGod',
|
||||
// bottom: '15%',
|
||||
// unlimited: true
|
||||
// },
|
||||
autoplay: false,
|
||||
theme: '#007bff',
|
||||
lang: 'zh-cn',
|
||||
hotkey: true,
|
||||
preload: 'auto',
|
||||
volume: 0.8,
|
||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||
loop: false,
|
||||
showDanmaku: true,
|
||||
danmakuUnlimited: true,
|
||||
contextmenu: [
|
||||
{
|
||||
text: '关于 DPlayer',
|
||||
link: 'https://github.com/DIYGod/DPlayer'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 事件监听
|
||||
player.on('play', () => {
|
||||
isPlaying.value = true
|
||||
console.log('播放开始')
|
||||
})
|
||||
|
||||
player.on('pause', () => {
|
||||
isPlaying.value = false
|
||||
console.log('播放暂停')
|
||||
})
|
||||
|
||||
player.on('ended', () => {
|
||||
isPlaying.value = false
|
||||
console.log('播放结束')
|
||||
})
|
||||
|
||||
player.on('error', () => {
|
||||
errorMessage.value = '播放出错'
|
||||
console.error('播放错误')
|
||||
})
|
||||
|
||||
player.on('ratechange', () => {
|
||||
currentPlaybackRate.value = player.speed()
|
||||
console.log('倍速改变:', currentPlaybackRate.value)
|
||||
})
|
||||
|
||||
// 移除可能导致错误的弹幕相关事件监听
|
||||
// player.on('quality_start', () => {
|
||||
// console.log('清晰度切换开始')
|
||||
// })
|
||||
|
||||
// player.on('quality_end', () => {
|
||||
// const quality = player.video.currentQuality
|
||||
// currentQuality.value = videoQualities[quality].name
|
||||
// console.log('清晰度切换完成:', currentQuality.value)
|
||||
// })
|
||||
|
||||
} catch (err) {
|
||||
console.error('初始化 DPlayer 失败:', err)
|
||||
errorMessage.value = '初始化失败'
|
||||
}
|
||||
}
|
||||
|
||||
// 基础控制方法
|
||||
const play = () => {
|
||||
if (player) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (player) {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
|
||||
const seek = (time: number) => {
|
||||
if (player) {
|
||||
player.seek(time)
|
||||
}
|
||||
}
|
||||
|
||||
const setVolume = (volume: number) => {
|
||||
if (player) {
|
||||
player.volume(volume / 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 快进控制方法
|
||||
const seekBackward = (seconds: number) => {
|
||||
if (player) {
|
||||
const currentTime = player.video.currentTime
|
||||
const newTime = Math.max(0, currentTime - seconds)
|
||||
player.seek(newTime)
|
||||
}
|
||||
}
|
||||
|
||||
const seekForward = (seconds: number) => {
|
||||
if (player) {
|
||||
const currentTime = player.video.currentTime
|
||||
const duration = player.video.duration
|
||||
const newTime = Math.min(duration, currentTime + seconds)
|
||||
player.seek(newTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 倍速控制方法
|
||||
const setPlaybackRate = (rate: number) => {
|
||||
if (player) {
|
||||
player.speed(rate)
|
||||
currentPlaybackRate.value = rate
|
||||
}
|
||||
}
|
||||
|
||||
// 清晰度切换方法
|
||||
const switchQuality = (qualityIndex: number) => {
|
||||
if (player && player.video) {
|
||||
// 由于现在使用单一视频源,这里只是模拟切换
|
||||
const quality = videoQualities[qualityIndex]
|
||||
currentQuality.value = quality.name
|
||||
console.log(`切换到清晰度: ${quality.name}`)
|
||||
alert(`模拟切换到清晰度: ${quality.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 字幕控制方法
|
||||
const toggleSubtitle = () => {
|
||||
if (player) {
|
||||
if (subtitleVisible.value) {
|
||||
player.hideSubtitle()
|
||||
subtitleVisible.value = false
|
||||
} else {
|
||||
player.showSubtitle()
|
||||
subtitleVisible.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showSubtitle = () => {
|
||||
if (player) {
|
||||
player.showSubtitle()
|
||||
subtitleVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const hideSubtitle = () => {
|
||||
if (player) {
|
||||
player.hideSubtitle()
|
||||
subtitleVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 弹幕控制方法
|
||||
const sendDanmaku = (text: string, color: string = '#fff', type: number = 0) => {
|
||||
if (player && player.danmaku) {
|
||||
player.danmaku.send({
|
||||
text: text,
|
||||
color: color,
|
||||
type: type
|
||||
})
|
||||
} else {
|
||||
console.log('弹幕功能暂不可用,请检查网络连接')
|
||||
alert(`模拟发送弹幕: ${text} (颜色: ${color}, 类型: ${type})`)
|
||||
}
|
||||
}
|
||||
|
||||
const showDanmaku = () => {
|
||||
if (player && player.danmaku) {
|
||||
player.danmaku.show()
|
||||
danmakuVisible.value = true
|
||||
} else {
|
||||
danmakuVisible.value = true
|
||||
console.log('弹幕显示')
|
||||
}
|
||||
}
|
||||
|
||||
const hideDanmaku = () => {
|
||||
if (player && player.danmaku) {
|
||||
player.danmaku.hide()
|
||||
danmakuVisible.value = false
|
||||
} else {
|
||||
danmakuVisible.value = false
|
||||
console.log('弹幕隐藏')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDanmaku = () => {
|
||||
if (player && player.danmaku) {
|
||||
if (player.danmaku.visible) {
|
||||
player.danmaku.hide()
|
||||
danmakuVisible.value = false
|
||||
} else {
|
||||
player.danmaku.show()
|
||||
danmakuVisible.value = true
|
||||
}
|
||||
} else {
|
||||
danmakuVisible.value = !danmakuVisible.value
|
||||
console.log(`弹幕${danmakuVisible.value ? '显示' : '隐藏'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const setDanmakuOpacity = (opacity: number) => {
|
||||
if (player && player.danmaku) {
|
||||
player.danmaku.opacity(opacity)
|
||||
} else {
|
||||
console.log(`设置弹幕透明度: ${opacity}`)
|
||||
}
|
||||
}
|
||||
|
||||
const setDanmakuFontSize = (size: number) => {
|
||||
if (player && player.danmaku) {
|
||||
player.danmaku.fontSize(size)
|
||||
} else {
|
||||
console.log(`设置弹幕字体大小: ${size}px`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPlayer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (player) {
|
||||
player.destroy()
|
||||
player = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dplayer-test {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.video-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dplayer-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.control-group h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-group button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 15px;
|
||||
margin: 5px 0;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.control-group button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status p {
|
||||
margin: 10px 0;
|
||||
text-align: left;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.control-group button {
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -225,6 +225,13 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@font-face {
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
src: url('@/assets/fonts/AlimamaShuHeiTi-Bold.ttf') format('truetype');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 刷新遮罩层样式 */
|
||||
.refresh-mask {
|
||||
position: fixed;
|
||||
@ -299,7 +306,7 @@ onActivated(() => {
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
font-size: 32px;
|
||||
font-family: AlimamaShuHeiTi-Bold;
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
@ -210,7 +210,7 @@
|
||||
<span class="section-title-text">精选活动</span>
|
||||
<div class="section-subtitle">SELECTED EVENTS</div>
|
||||
</div>
|
||||
<span class="view-all-btn">查看全部 > </span>
|
||||
<span class="view-all-btn">查看更多 > </span>
|
||||
</div>
|
||||
<div class="activities-grid">
|
||||
<div class="activity-left">
|
||||
@ -230,11 +230,11 @@
|
||||
<img src="/images/activity/activity1.png" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-right">
|
||||
<!-- <div class="activity-right">
|
||||
<img src="/images/activity/activity2.png" alt="">
|
||||
<h3 class="activity-title-img">算法挑战</h3>
|
||||
<img src="/images/activity/right.png" class="activity-right-img" alt="">
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -247,6 +247,7 @@
|
||||
<span class="section-title-text">AI智能体验</span>
|
||||
<div class="section-subtitle">FEATURED REVIEWS</div>
|
||||
</div>
|
||||
<div class="view-all-btn">查看更多 > </div>
|
||||
</div>
|
||||
<div class="ai-cards-grid">
|
||||
<div class="ai-card" v-for="aiCard in aiCards" :key="aiCard.id">
|
||||
@ -657,32 +658,32 @@ const partners = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
name: '云南师范大学',
|
||||
logo: '/logo/logo4.png'
|
||||
logo: '/logo/云师大.jpg'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '曲靖师范学院',
|
||||
logo: '/logo/logo4.png'
|
||||
logo: '/logo/曲靖师范.jpg'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '德宏师范学院',
|
||||
logo: '/logo/logo4.png'
|
||||
logo: '/logo/德宏师范.jpg'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '云南师范大学',
|
||||
logo: '/logo/logo4.png'
|
||||
logo: '/logo/云师大.jpg'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '曲靖师范学院',
|
||||
logo: '/logo/logo4.png'
|
||||
logo: '/logo/曲靖师范.jpg'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '德宏师范学院',
|
||||
logo: '/logo/logo4.png'
|
||||
logo: '/logo/德宏师范.jpg'
|
||||
}
|
||||
])
|
||||
|
||||
@ -781,6 +782,12 @@ onMounted(async () => {
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'AlibabaPuHuiTiBold';
|
||||
src: url('@/assets/fonts/Alibaba_PuHuiTi_2.0_55_Regular_85_Bold.ttf') format('truetype');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.home {
|
||||
@ -881,16 +888,14 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.logo-circle img {
|
||||
@ -904,6 +909,7 @@ onMounted(async () => {
|
||||
color: #292C2E;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@ -1159,8 +1165,9 @@ onMounted(async () => {
|
||||
color: #3BA4EB;
|
||||
top: 48%;
|
||||
transform: translateY(-50%);
|
||||
left: 24%;
|
||||
font-size: 22px;
|
||||
left: 20%;
|
||||
font-size: 26px;
|
||||
font-family: 'AlibabaPuHuiTiBold';
|
||||
}
|
||||
|
||||
.ad-container-title1 {
|
||||
@ -2101,7 +2108,7 @@ onMounted(async () => {
|
||||
|
||||
.activity-left {
|
||||
padding-left: 30px;
|
||||
width: 1182px;
|
||||
width: 100%;
|
||||
height: 404px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
|
@ -240,6 +240,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<div v-if="currentView === 'rules' || currentView === 'details'" class="breadcrumb-nav">
|
||||
<span class="breadcrumb-item" @click="switchToHome">积分中心</span>
|
||||
<span class="breadcrumb-separator"> > </span>
|
||||
<span class="breadcrumb-current">{{ currentView === 'rules' ? '积分规则' : '积分明细' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 规则说明 -->
|
||||
<div v-if="currentView === 'rules'" class="rules-container">
|
||||
<div class="rules-header">
|
||||
@ -2278,7 +2285,7 @@ button:active {
|
||||
.rules-container,
|
||||
.details-container {
|
||||
width: 1420px;
|
||||
margin: 30px auto;
|
||||
margin: 0 auto;
|
||||
background: #F9F9F9;
|
||||
border: 1.5px solid #fff;
|
||||
padding: 30px 20px;
|
||||
@ -2576,4 +2583,35 @@ button:active {
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
/* 面包屑导航样式 */
|
||||
.breadcrumb-nav {
|
||||
margin: 0 auto;
|
||||
width: 1420px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 0 15px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #0088d1;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #0066a3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 8px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
@ -14,7 +14,7 @@
|
||||
<!-- 用户头像和姓名 -->
|
||||
<SafeAvatar class="image_7" :src="userStore.user?.avatar" :name="userStore.user?.username || '用户'" :size="96"
|
||||
alt="用户头像" />
|
||||
<span class="text_72">{{ userStore.user?.username || '用户名' }}</span>
|
||||
<span class="text_72">{{ userStore.user?.nickname || userStore.user?.username || '用户名' }}</span>
|
||||
|
||||
<!-- 菜单项容器 -->
|
||||
<div class="box_22">
|
||||
@ -22,72 +22,81 @@
|
||||
<div class="menu-divider"></div>
|
||||
|
||||
<!-- 我的课程 -->
|
||||
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')">
|
||||
<img class="image_8" referrerpolicy="no-referrer" :src="activeTab === 'courses'
|
||||
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')"
|
||||
@mouseenter="hoveredTab = 'courses'" @mouseleave="hoveredTab = null">
|
||||
<img class="image_8" referrerpolicy="no-referrer" :src="(activeTab === 'courses' || hoveredTab === 'courses')
|
||||
? '/images/profile/course-active.png'
|
||||
: '/images/profile/course.png'" />
|
||||
<span class="text-group_19">我的课程</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的作业 -->
|
||||
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')">
|
||||
<img class="label_4" referrerpolicy="no-referrer" :src="activeTab === 'homework'
|
||||
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')"
|
||||
@mouseenter="hoveredTab = 'homework'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_4" referrerpolicy="no-referrer" :src="(activeTab === 'homework' || hoveredTab === 'homework')
|
||||
? '/images/profile/grade-active.png'
|
||||
: '/images/profile/grade.png'" />
|
||||
<span class="text-group_20">我的作业</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的考试 -->
|
||||
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')">
|
||||
<img class="label_5" referrerpolicy="no-referrer" :src="activeTab === 'exam'
|
||||
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')"
|
||||
@mouseenter="hoveredTab = 'exam'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_5" referrerpolicy="no-referrer" :src="(activeTab === 'exam' || hoveredTab === 'exam')
|
||||
? '/images/profile/checklist-active.png'
|
||||
: '/images/profile/checklist.png'" />
|
||||
<span class="text-group_21">我的考试</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的练习 -->
|
||||
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')">
|
||||
<img class="label_6" referrerpolicy="no-referrer" :src="activeTab === 'practice'
|
||||
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')"
|
||||
@mouseenter="hoveredTab = 'practice'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_6" referrerpolicy="no-referrer" :src="(activeTab === 'practice' || hoveredTab === 'practice')
|
||||
? '/images/profile/bookmark-active.png'
|
||||
: '/images/profile/bookmark.png'" />
|
||||
<span class="text-group_22">我的练习</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的活动 -->
|
||||
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')">
|
||||
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="activeTab === 'activity'
|
||||
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')"
|
||||
@mouseenter="hoveredTab = 'activity'" @mouseleave="hoveredTab = null">
|
||||
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="(activeTab === 'activity' || hoveredTab === 'activity')
|
||||
? '/images/profile/gift-active.png'
|
||||
: '/images/profile/gift.png'" />
|
||||
<span class="text-group_23">我的活动</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的关注 -->
|
||||
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')">
|
||||
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="activeTab === 'follows'
|
||||
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')"
|
||||
@mouseenter="hoveredTab = 'follows'" @mouseleave="hoveredTab = null">
|
||||
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="(activeTab === 'follows' || hoveredTab === 'follows')
|
||||
? '/images/profile/concern-active.png'
|
||||
: '/images/profile/concern.png'" />
|
||||
<span class="text-group_27">我的关注</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的消息 -->
|
||||
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')">
|
||||
<img class="label_7" referrerpolicy="no-referrer" :src="activeTab === 'message'
|
||||
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')"
|
||||
@mouseenter="hoveredTab = 'message'" @mouseleave="hoveredTab = null">
|
||||
<img class="label_7" referrerpolicy="no-referrer" :src="(activeTab === 'message' || hoveredTab === 'message')
|
||||
? '/images/profile/message-active.png'
|
||||
: '/images/profile/message.png'" />
|
||||
<span class="text-group_24">我的消息</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的资料 -->
|
||||
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')">
|
||||
<img class="image_9" referrerpolicy="no-referrer" :src="activeTab === 'materials'
|
||||
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')"
|
||||
@mouseenter="hoveredTab = 'materials'" @mouseleave="hoveredTab = null">
|
||||
<img class="image_9" referrerpolicy="no-referrer" :src="(activeTab === 'materials' || hoveredTab === 'materials')
|
||||
? '/images/profile/profile-active.png'
|
||||
: '/images/profile/profile.png'" />
|
||||
<span class="text-group_25">我的资料</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的下载 -->
|
||||
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')">
|
||||
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="activeTab === 'download'
|
||||
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')"
|
||||
@mouseenter="hoveredTab = 'download'" @mouseleave="hoveredTab = null">
|
||||
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="(activeTab === 'download' || hoveredTab === 'download')
|
||||
? '/images/profile/download-active.png'
|
||||
: '/images/profile/download.png'" />
|
||||
<span class="text-group_26">我的下载</span>
|
||||
@ -361,7 +370,7 @@
|
||||
<span class="text_36">上传作业</span>
|
||||
</div>
|
||||
<div class="text-wrapper_8 anew-button" @click="reEditDraft">
|
||||
<span class="text_36">重新编辑</span>
|
||||
<span class="">重新编辑</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -957,8 +966,7 @@
|
||||
<!-- 面包屑导航或筛选和操作区域 -->
|
||||
<div v-if="isInSubDirectory" class="breadcrumb-controls">
|
||||
<div class="breadcrumb-nav">
|
||||
<span class="breadcrumb-text" @click="goBack">课件>图片></span>
|
||||
<span class="breadcrumb-current">风景图片</span>
|
||||
<span class="breadcrumb-text" @click="goBack">{{ currentPath.join(' > ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -978,9 +986,7 @@
|
||||
<div class="search-input-container">
|
||||
<input v-model="downloadFilter.keyword" type="text" class="search-input" placeholder="请输入文件名称" />
|
||||
<button class="search-btn">
|
||||
<img
|
||||
src="https://lanhu-oss-2537-2.lanhuapp.com/SketchPng870a86da8af58a60f35fcb27ef4822e645d2ad5aaabe6416e4179342a53a5a60"
|
||||
alt="搜索图标" class="search-icon" />
|
||||
<img src="/images/profile/search.png" alt="搜索图标" class="search-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1244,6 +1250,7 @@ const userStore = useUserStore()
|
||||
type TabType = 'courses' | 'homework' | 'exam' | 'practice' | 'activity' | 'follows' | 'message' | 'materials' | 'download'
|
||||
|
||||
const activeTab = ref<TabType>('courses')
|
||||
const hoveredTab = ref<TabType | null>(null)
|
||||
const activeCourseTab = ref('all')
|
||||
|
||||
// 作业筛选状态
|
||||
@ -2486,6 +2493,11 @@ const handleDownloadTabChange = (tab: string) => {
|
||||
|
||||
// 筛选后的下载文件
|
||||
const filteredDownloadFiles = computed(() => {
|
||||
// 如果在子目录中,显示空内容(因为目前没有子目录的文件数据)
|
||||
if (isInSubDirectory.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
let files = downloadFiles.filter(file => file.category === activeDownloadTab.value)
|
||||
|
||||
if (downloadFilter.type !== 'all') {
|
||||
@ -2507,10 +2519,14 @@ const toggleFileMenu = (fileId: number) => {
|
||||
}
|
||||
|
||||
const handleFileClick = (file: any) => {
|
||||
if (file.type === 'folder' && file.name === '图片') {
|
||||
// 点击图片文件夹,进入子目录
|
||||
if (file.type === 'folder') {
|
||||
// 点击任何文件夹,进入子目录
|
||||
isInSubDirectory.value = true
|
||||
currentPath.value = ['课件', '图片']
|
||||
currentPath.value = ['课件', file.name]
|
||||
message.info(`进入文件夹:${file.name}`)
|
||||
} else {
|
||||
// 点击文件,显示文件详情或下载
|
||||
message.info(`打开文件:${file.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2536,12 +2552,9 @@ const getFileIcon = (fileId?: number) => {
|
||||
]
|
||||
const index = (fileId || 0) % homeworkImages.length
|
||||
return homeworkImages[index]
|
||||
} else if (isInSubDirectory.value) {
|
||||
// 子目录使用子目录图标
|
||||
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPngf45333052202c303acc2c06223c26b820d330459ce2d452a21a3132fbbeab442'
|
||||
} else {
|
||||
// 默认文件夹图标
|
||||
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPng5548891b00234027dbe6dadafbd83596d616261421c0587a85652dc194b2d5ef'
|
||||
return '/images/profile/folder.png'
|
||||
}
|
||||
}
|
||||
|
||||
@ -2930,9 +2943,8 @@ onActivated(() => {
|
||||
/* 去掉背景色 */
|
||||
border-radius: 0.6vw;
|
||||
/* 12px转换为vw */
|
||||
margin: 2.55vh 0;
|
||||
margin: 2.55vh 0 0 0;
|
||||
/* 去掉左右边距,因为父容器已经居中 */
|
||||
padding: 1.04vh 0;
|
||||
/* 20px 0转换 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -2967,7 +2979,7 @@ onActivated(() => {
|
||||
/* 自适应高度 */
|
||||
min-height: 3vh;
|
||||
/* 设置最小高度,让盒子更大 */
|
||||
margin: 1.5vh;
|
||||
margin: 1.5vh 0 0 0;
|
||||
/* 减小间距:从2.34vh减少到1.5vh */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -4186,9 +4198,9 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.course-name {
|
||||
margin-top: 10px;
|
||||
margin-left: 83px;
|
||||
color: #999999;
|
||||
/* margin-top: 10px; */
|
||||
margin-left: 5px;
|
||||
color: #497087;
|
||||
}
|
||||
|
||||
.course-name span {
|
||||
@ -5744,7 +5756,7 @@ onActivated(() => {
|
||||
/* 0 0 16px 0转换 */
|
||||
line-height: 1.4;
|
||||
padding: 0 1.04vw;
|
||||
/* 添加左右内边距 */
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
@ -6172,7 +6184,7 @@ onActivated(() => {
|
||||
/* height: 5.21vh; */
|
||||
padding: 0.52vh 0.57vw;
|
||||
/* 100px转换为vh,进一步增加高度 */
|
||||
background: url('https://lanhu-oss-2537-2.lanhuapp.com/SketchPng9491a7fe5bdac8e8a88de63907163bd6b8a259824f56a3c76784ba6cdc7bc32b') 100% no-repeat;
|
||||
background: #F5F8FB;
|
||||
background-size: 100% 100%;
|
||||
margin-top: 0.26vh;
|
||||
/* 5px转换为vh */
|
||||
@ -7058,6 +7070,7 @@ onActivated(() => {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.password-form-input {
|
||||
@ -7609,7 +7622,7 @@ onActivated(() => {
|
||||
/* 4px转换为vw,减小图标和文字间距 */
|
||||
padding: 0.52vh 0.73vw;
|
||||
/* 10px 14px转换 */
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
/* 14px转换为vw */
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #000;
|
||||
@ -8066,7 +8079,7 @@ onActivated(() => {
|
||||
width: 80px;
|
||||
height: 23px;
|
||||
border: none;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
|
@ -201,7 +201,200 @@ const loadCourses = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 筛选逻辑
|
||||
let filteredCourses: Course[] = [] // 暂时使用空数组,后续可以从API获取
|
||||
let filteredCourses: Course[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: '教育心理学基础课程',
|
||||
description: '本课程深入讲解教育心理学的基本理论和实践应用,帮助学生理解学习过程中的心理机制。',
|
||||
thumbnail: '/images/courses/course1.png',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
rating: 4.8,
|
||||
ratingCount: 125,
|
||||
studentsCount: 1250,
|
||||
duration: '12小时43分钟',
|
||||
totalLessons: 54,
|
||||
level: 'beginner',
|
||||
language: 'zh-CN',
|
||||
category: { id: 1, name: '教育培训', slug: 'education-training' },
|
||||
tags: ['心理学', '教育', '基础'],
|
||||
skills: ['心理分析', '教育理论'],
|
||||
requirements: ['无特殊要求'],
|
||||
objectives: ['掌握教育心理学基础理论'],
|
||||
instructor: {
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
avatar: '/images/Teachers/师资力量1.png',
|
||||
title: '云南师范大学教授',
|
||||
bio: '资深教育心理学专家',
|
||||
rating: 4.8,
|
||||
studentsCount: 1250,
|
||||
coursesCount: 6,
|
||||
experience: '15年',
|
||||
education: ['云南师范大学', '教育心理学博士'],
|
||||
certifications: ['高级心理咨询师', '教育技术专家']
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
updatedAt: '2024-01-15T10:00:00Z'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '现代教育技术应用',
|
||||
description: '探索现代教育技术在课堂教学中的应用,包括多媒体教学、在线教育平台等。',
|
||||
thumbnail: '/images/courses/course2.png',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
rating: 4.6,
|
||||
ratingCount: 89,
|
||||
studentsCount: 890,
|
||||
duration: '10小时20分钟',
|
||||
totalLessons: 42,
|
||||
level: 'intermediate',
|
||||
language: 'zh-CN',
|
||||
category: { id: 2, name: '技术应用', slug: 'tech-application' },
|
||||
tags: ['教育技术', '多媒体', '在线教育'],
|
||||
skills: ['多媒体制作', '在线教学'],
|
||||
requirements: ['基础计算机操作'],
|
||||
objectives: ['掌握现代教育技术应用'],
|
||||
instructor: {
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
avatar: '/images/Teachers/师资力量1.png',
|
||||
title: '云南师范大学教授',
|
||||
bio: '资深教育技术专家',
|
||||
rating: 4.6,
|
||||
studentsCount: 890,
|
||||
coursesCount: 6,
|
||||
experience: '15年',
|
||||
education: ['云南师范大学', '教育技术博士'],
|
||||
certifications: ['高级教育技术专家', '多媒体制作师']
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-02-20T14:30:00Z',
|
||||
updatedAt: '2024-02-20T14:30:00Z'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '课程设计与开发',
|
||||
description: '学习如何设计和开发高质量的课程内容,包括教学目标制定、教学内容组织等。',
|
||||
thumbnail: '/images/courses/course3.png',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
rating: 4.7,
|
||||
ratingCount: 67,
|
||||
studentsCount: 567,
|
||||
duration: '15小时30分钟',
|
||||
totalLessons: 68,
|
||||
level: 'advanced',
|
||||
language: 'zh-CN',
|
||||
category: { id: 3, name: '课程设计', slug: 'course-design' },
|
||||
tags: ['课程设计', '教学开发', '教育'],
|
||||
skills: ['课程规划', '教学设计'],
|
||||
requirements: ['教育理论基础'],
|
||||
objectives: ['掌握课程设计方法'],
|
||||
instructor: {
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
avatar: '/images/Teachers/师资力量1.png',
|
||||
title: '云南师范大学教授',
|
||||
bio: '课程设计专家'
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-03-10T09:15:00Z',
|
||||
updatedAt: '2024-03-10T09:15:00Z'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: '教育研究方法论',
|
||||
description: '系统介绍教育研究的基本方法和技巧,培养学生进行教育研究的能力。',
|
||||
thumbnail: '/images/courses/course4.png',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
rating: 4.5,
|
||||
ratingCount: 43,
|
||||
studentsCount: 432,
|
||||
duration: '8小时15分钟',
|
||||
totalLessons: 36,
|
||||
level: 'intermediate',
|
||||
language: 'zh-CN',
|
||||
category: { id: 4, name: '研究方法', slug: 'research-methods' },
|
||||
tags: ['研究方法', '教育', '学术'],
|
||||
skills: ['研究设计', '数据分析'],
|
||||
requirements: ['统计学基础'],
|
||||
objectives: ['掌握教育研究方法'],
|
||||
instructor: {
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
avatar: '/images/Teachers/师资力量1.png',
|
||||
title: '云南师范大学教授',
|
||||
bio: '教育研究专家'
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-01-25T16:45:00Z',
|
||||
updatedAt: '2024-01-25T16:45:00Z'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: '学生心理辅导技巧',
|
||||
description: '学习如何对学生进行心理辅导,掌握基本的心理咨询和辅导技巧。',
|
||||
thumbnail: '/images/courses/course5.png',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
rating: 4.9,
|
||||
ratingCount: 78,
|
||||
studentsCount: 678,
|
||||
duration: '6小时45分钟',
|
||||
totalLessons: 28,
|
||||
level: 'beginner',
|
||||
language: 'zh-CN',
|
||||
category: { id: 5, name: '心理辅导', slug: 'psychological-counseling' },
|
||||
tags: ['心理辅导', '学生', '技巧'],
|
||||
skills: ['心理咨询', '沟通技巧'],
|
||||
requirements: ['心理学基础'],
|
||||
objectives: ['掌握心理辅导技巧'],
|
||||
instructor: {
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
avatar: '/images/Teachers/师资力量1.png',
|
||||
title: '云南师范大学教授',
|
||||
bio: '心理咨询专家'
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-02-05T11:20:00Z',
|
||||
updatedAt: '2024-02-05T11:20:00Z'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: '教育评估与测量',
|
||||
description: '学习教育评估的基本理论和方法,掌握教育测量的技术和工具。',
|
||||
thumbnail: '/images/courses/course5.png',
|
||||
price: 0,
|
||||
currency: 'CNY',
|
||||
rating: 4.4,
|
||||
ratingCount: 34,
|
||||
studentsCount: 345,
|
||||
duration: '9小时20分钟',
|
||||
totalLessons: 40,
|
||||
level: 'advanced',
|
||||
language: 'zh-CN',
|
||||
category: { id: 6, name: '教育评估', slug: 'education-assessment' },
|
||||
tags: ['教育评估', '测量', '技术'],
|
||||
skills: ['评估设计', '测量技术'],
|
||||
requirements: ['教育统计学'],
|
||||
objectives: ['掌握教育评估方法'],
|
||||
instructor: {
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
avatar: '/images/Teachers/师资力量1.png',
|
||||
title: '云南师范大学教授',
|
||||
bio: '教育评估专家'
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-03-15T13:10:00Z',
|
||||
updatedAt: '2024-03-15T13:10:00Z'
|
||||
}
|
||||
] // 使用模拟数据,后续可以从API获取
|
||||
|
||||
// 按学科筛选
|
||||
if (selectedSubject.value !== '全部') {
|
||||
@ -502,7 +695,7 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
min-height: 350px;
|
||||
/* min-height: 350px; */
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
|