Compare commits
3 Commits
b37cdd3ccc
...
f06aef9913
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f06aef9913 | ||
![]() |
d6e76b7c73 | ||
![]() |
0fe429cd79 |
@ -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 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>
|
||||
@ -276,7 +276,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'
|
||||
@ -203,7 +201,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'
|
||||
@ -211,7 +214,7 @@ const userMenuOptions = computed(() => [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: () => h(LogOutOutline)
|
||||
icon: () => h('div', { class: 'custom-icon' }, '🚪')
|
||||
}
|
||||
])
|
||||
|
||||
@ -258,6 +261,12 @@ const handleUserMenuSelect = (key: string) => {
|
||||
window.location.reload();
|
||||
})
|
||||
break
|
||||
case 'teacher':
|
||||
// 切换教师端逻辑
|
||||
console.log('切换到教师端')
|
||||
// 这里可以添加切换教师端的逻辑,比如跳转到教师端页面
|
||||
router.push('/teacher')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
router.push('/')
|
||||
@ -319,370 +328,418 @@ onUnmounted(() => {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0 30px;
|
||||
height: 100%;
|
||||
height: 64px;
|
||||
background: white;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-section {
|
||||
flex-shrink: 0;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 72px;
|
||||
height: 61px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
max-width: 12px;
|
||||
max-height: 12px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* AI图标样式 */
|
||||
.ai-icon {
|
||||
max-width: 34px;
|
||||
max-height: 34px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
flex: 1;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* 两个字的导航项:首页 */
|
||||
.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) {
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
/* 两个字的导航项:活动 */
|
||||
.nav-item:nth-child(6) {
|
||||
width: 36px;
|
||||
padding-right: 16px;
|
||||
/* 为HOT标签留出空间 */
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
color: #0084CD;
|
||||
}
|
||||
|
||||
.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 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
background-color: #0084CD;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -22px;
|
||||
max-width: 28px;
|
||||
max-height: 28px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #eeeeee;
|
||||
} */
|
||||
|
||||
.search-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 移动端汉堡菜单按钮 */
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 右侧操作区域 */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
color: #1890ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
/* 语言切换器 */
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
margin-top: 4px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
background: #f0f8ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.language-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 认证按钮 */
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.auth-combined-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0088D1;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.auth-combined-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.auth-login,
|
||||
.auth-register {
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.auth-login:hover,
|
||||
.auth-register:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 4px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-section {
|
||||
flex-shrink: 0;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 72px;
|
||||
height: 61px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
max-width: 12px;
|
||||
max-height: 12px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* AI图标样式 */
|
||||
.ai-icon {
|
||||
max-width: 34px;
|
||||
max-height: 34px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
flex: 1;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* 两个字的导航项:首页 */
|
||||
.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) {
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
/* 两个字的导航项:活动 */
|
||||
.nav-item:nth-child(6) {
|
||||
width: 36px;
|
||||
padding-right: 16px;
|
||||
/* 为HOT标签留出空间 */
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
color: #0084CD;
|
||||
}
|
||||
|
||||
.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 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
background-color: #0084CD;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -22px;
|
||||
max-width: 28px;
|
||||
max-height: 28px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: #eeeeee;
|
||||
} */
|
||||
|
||||
.search-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 移动端汉堡菜单按钮 */
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 右侧操作区域 */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
color: #1890ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
/* 语言切换器 */
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
margin-top: 4px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
background: #f0f8ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.language-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 认证按钮 */
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.auth-combined-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0088D1;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.auth-combined-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
.auth-login,
|
||||
.auth-register {
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.auth-login:hover,
|
||||
.auth-register:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 4px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header {
|
||||
height: 52px;
|
||||
}
|
||||
.content {
|
||||
padding-top: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
<!-- 讲师信息 -->
|
||||
@ -2059,7 +2059,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.instructor-info {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.instructor-name {
|
||||
|
@ -33,46 +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">
|
||||
@ -140,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>
|
||||
|
||||
<!-- 讲师信息 -->
|
||||
@ -225,7 +192,7 @@
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="commentsError" class="comments-error">
|
||||
<p>{{ commentsError }}</p>
|
||||
<button @click="loadCourseComments" class="retry-btn">重试</button>
|
||||
<button @click="loadComments" class="retry-btn">重试</button>
|
||||
</div>
|
||||
|
||||
<!-- 评论列表 -->
|
||||
@ -244,18 +211,20 @@
|
||||
|
||||
<!-- 评论图片 -->
|
||||
<div v-if="comment.images.length > 0" class="comment-images">
|
||||
<img v-for="(image, index) in comment.images" :key="index"
|
||||
:src="image" :alt="`评论图片${index + 1}`" />
|
||||
<img v-for="(image, index) in comment.images" :key="index" :src="image"
|
||||
:alt="`评论图片${index + 1}`" />
|
||||
</div>
|
||||
|
||||
<div class="comment-actions">
|
||||
<div class="comment-actions">
|
||||
<button v-if="comment.isTop" class="action-btn top-comment">
|
||||
<span class="top">置顶评论</span>
|
||||
<span>{{ comment.createTime }}</span>
|
||||
</button>
|
||||
<button class="action-btn like-btn">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" class="like-icon">
|
||||
<path d="M7 12.5L6.125 11.75C3.5 9.375 1.75 7.75 1.75 5.75C1.75 4.25 2.875 3.125 4.375 3.125C5.25 3.125 6.125 3.5 7 4.25C7.875 3.5 8.75 3.125 9.625 3.125C11.125 3.125 12.25 4.25 12.25 5.75C12.25 7.75 10.5 9.375 7.875 11.75L7 12.5Z" fill="currentColor"/>
|
||||
<path
|
||||
d="M7 12.5L6.125 11.75C3.5 9.375 1.75 7.75 1.75 5.75C1.75 4.25 2.875 3.125 4.375 3.125C5.25 3.125 6.125 3.5 7 4.25C7.875 3.5 8.75 3.125 9.625 3.125C11.125 3.125 12.25 4.25 12.25 5.75C12.25 7.75 10.5 9.375 7.875 11.75L7 12.5Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
{{ comment.likeCount }}
|
||||
</button>
|
||||
@ -338,7 +307,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>
|
||||
<!-- 下载图标 - 可点击 -->
|
||||
@ -450,20 +419,14 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { CourseApi } from '@/api/modules/course'
|
||||
import type { Course, CourseSection, SectionVideo, VideoQuality, CourseComment, Instructor } from '@/api/types'
|
||||
import type { Course, CourseSection, VideoQuality, CourseComment, Instructor } from '@/api/types'
|
||||
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()
|
||||
@ -476,14 +439,10 @@ 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)
|
||||
const videoQualities = ref<VideoQuality[]>([])
|
||||
const currentQuality = ref<string>('360') // 默认360p
|
||||
const videoLoading = ref(false)
|
||||
const showQualityMenu = ref(false)
|
||||
|
||||
// 评论相关状态
|
||||
const comments = ref<CourseComment[]>([])
|
||||
@ -491,9 +450,34 @@ const commentsLoading = ref(false)
|
||||
const commentsError = ref('')
|
||||
|
||||
// 讲师相关状态
|
||||
const instructors = ref<Instructor[]>([])
|
||||
const instructorsLoading = ref(false)
|
||||
const instructorsError = ref('')
|
||||
const instructors = ref<Instructor[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '汪波',
|
||||
title: '教授',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80',
|
||||
rating: 4.8,
|
||||
studentsCount: 1200,
|
||||
coursesCount: 15,
|
||||
experience: '10年教学经验',
|
||||
education: ['博士学位'],
|
||||
certifications: ['AI专家认证'],
|
||||
bio: '人工智能领域专家,拥有丰富的教学经验'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李老师',
|
||||
title: '副教授',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=60&q=80',
|
||||
rating: 4.6,
|
||||
studentsCount: 800,
|
||||
coursesCount: 12,
|
||||
experience: '8年教学经验',
|
||||
education: ['硕士学位'],
|
||||
certifications: ['深度学习认证'],
|
||||
bio: '深度学习专家,专注于实用技术教学'
|
||||
}
|
||||
])
|
||||
|
||||
// 评论输入相关状态
|
||||
const newComment = ref('')
|
||||
@ -518,6 +502,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)
|
||||
@ -564,7 +581,7 @@ const totalSections = computed(() => {
|
||||
const formatTotalDuration = () => {
|
||||
// 计算总时长
|
||||
let totalMinutes = 0
|
||||
courseSections.value.forEach(section => {
|
||||
courseSections.value.forEach((section: any) => {
|
||||
if (section.duration) {
|
||||
const parts = section.duration.split(':')
|
||||
if (parts.length === 3) {
|
||||
@ -687,12 +704,10 @@ const loadCourseSections = async () => {
|
||||
console.log('✅ 分组数据:', groupedSections.value)
|
||||
// 默认播放右侧第一个视频章节(当未强制使用本地视频时)
|
||||
if (!FORCE_LOCAL_VIDEO) {
|
||||
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
||||
const firstVideo = courseSections.value.find((s: any) => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
||||
if (firstVideo) {
|
||||
currentSection.value = firstVideo
|
||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -735,198 +750,63 @@ const loadMockData = () => {
|
||||
if (firstVideo) {
|
||||
currentSection.value = firstVideo
|
||||
currentVideoUrl.value = getVideoUrl(firstVideo)
|
||||
setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程评论列表
|
||||
const loadCourseComments = async () => {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
commentsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
commentsLoading.value = true
|
||||
commentsError.value = ''
|
||||
|
||||
console.log('调用API获取课程评论...')
|
||||
const response = await CourseApi.getCourseComments(courseId.value)
|
||||
console.log('评论API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// 按置顶状态和时间排序:置顶评论在前,然后按时间倒序
|
||||
const sortedComments = response.data.sort((a, b) => {
|
||||
// 先按置顶状态排序
|
||||
if (a.isTop !== b.isTop) {
|
||||
return a.isTop ? -1 : 1 // 置顶的在前
|
||||
}
|
||||
// 再按创建时间倒序排序
|
||||
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||
})
|
||||
|
||||
comments.value = sortedComments
|
||||
console.log('✅ 评论数据设置成功:', comments.value)
|
||||
} else {
|
||||
console.log('⚠️ API返回的评论数据为空')
|
||||
comments.value = []
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ API返回错误')
|
||||
commentsError.value = response.message || '获取评论失败'
|
||||
comments.value = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载课程评论失败:', err)
|
||||
commentsError.value = '获取评论失败'
|
||||
comments.value = []
|
||||
} finally {
|
||||
commentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程讲师列表
|
||||
const loadCourseInstructors = async () => {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
instructorsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
instructorsLoading.value = true
|
||||
instructorsError.value = ''
|
||||
|
||||
console.log('调用API获取课程讲师...')
|
||||
const response = await CourseApi.getCourseInstructors(courseId.value)
|
||||
console.log('讲师API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
instructors.value = response.data
|
||||
console.log('✅ 讲师数据设置成功:', instructors.value)
|
||||
} else {
|
||||
console.log('⚠️ API返回的讲师数据为空,使用默认数据')
|
||||
// 保持默认的mock数据
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ API返回错误,使用默认数据')
|
||||
instructorsError.value = response.message || '获取讲师信息失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载课程讲师失败:', err)
|
||||
instructorsError.value = '获取讲师信息失败'
|
||||
// 保持默认的mock数据
|
||||
} finally {
|
||||
instructorsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换章节展开/收起
|
||||
const toggleChapter = (chapterIndex: number) => {
|
||||
console.log('切换章节展开/收起:', chapterIndex)
|
||||
groupedSections.value[chapterIndex].expanded = !groupedSections.value[chapterIndex].expanded
|
||||
}
|
||||
|
||||
// 加载章节视频
|
||||
const loadSectionVideo = async (section: CourseSection) => {
|
||||
// 加载评论
|
||||
const loadComments = async () => {
|
||||
try {
|
||||
videoLoading.value = true
|
||||
console.log('🔍 加载章节视频,章节ID:', section.id)
|
||||
commentsLoading.value = true
|
||||
commentsError.value = ''
|
||||
|
||||
const response = await CourseApi.getSectionVideos(courseId.value, section.id)
|
||||
console.log('🔍 视频API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && response.data.length > 0) {
|
||||
const video = response.data[0] // 取第一个视频
|
||||
currentVideo.value = video
|
||||
videoQualities.value = video.qualities
|
||||
currentQuality.value = video.defaultQuality
|
||||
|
||||
// 获取默认清晰度的URL
|
||||
const defaultQualityVideo = video.qualities.find(q => q.value === video.defaultQuality)
|
||||
if (defaultQualityVideo) {
|
||||
currentVideoUrl.value = defaultQualityVideo.url
|
||||
console.log('✅ 设置视频URL:', currentVideoUrl.value)
|
||||
|
||||
// 更新播放器
|
||||
await updateVideoPlayer()
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 没有找到视频数据')
|
||||
// 模拟加载评论数据
|
||||
const mockComments: CourseComment[] = [
|
||||
{
|
||||
id: '1',
|
||||
content: '这个课程非常有用,老师讲解得很清楚!',
|
||||
userId: '1',
|
||||
userName: '学习者小王',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
userTag: '学员',
|
||||
images: [],
|
||||
isTop: false,
|
||||
likeCount: 23,
|
||||
createTime: '2024-01-15T10:30:00Z',
|
||||
timeAgo: '2天前'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: '通过这个课程学到了很多实用的AI知识,推荐!',
|
||||
userId: '2',
|
||||
userName: 'AI爱好者',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
||||
userTag: '学员',
|
||||
images: [],
|
||||
isTop: false,
|
||||
likeCount: 18,
|
||||
createTime: '2024-01-12T14:20:00Z',
|
||||
timeAgo: '5天前'
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 获取视频失败:', response.message)
|
||||
}
|
||||
]
|
||||
|
||||
comments.value = mockComments
|
||||
console.log('评论加载成功')
|
||||
} catch (error) {
|
||||
console.error('❌ 加载章节视频失败:', error)
|
||||
console.error('加载评论失败:', error)
|
||||
commentsError.value = '加载评论失败,请重试'
|
||||
} finally {
|
||||
videoLoading.value = false
|
||||
commentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换视频清晰度
|
||||
const changeVideoQuality = async (quality: string) => {
|
||||
if (!currentVideo.value) return
|
||||
|
||||
const qualityVideo = currentVideo.value.qualities.find(q => q.value === quality)
|
||||
if (qualityVideo) {
|
||||
currentQuality.value = quality
|
||||
currentVideoUrl.value = qualityVideo.url
|
||||
console.log('🔍 切换清晰度到:', quality, 'URL:', qualityVideo.url)
|
||||
|
||||
// 更新播放器
|
||||
await updateVideoPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新视频播放器
|
||||
const updateVideoPlayer = async () => {
|
||||
if (!currentVideoUrl.value) {
|
||||
console.warn('⚠️ 视频URL为空,无法更新播放器')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔍 更新播放器视频源:', 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)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 更新播放器失败:', error)
|
||||
// 如果更新失败,尝试重新初始化
|
||||
try {
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
} catch (initError) {
|
||||
console.error('❌ 重新初始化播放器也失败:', initError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取章节编号
|
||||
const getChapterNumber = (num: number) => {
|
||||
@ -1015,10 +895,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)
|
||||
@ -1030,12 +910,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
|
||||
@ -1045,111 +925,23 @@ 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) {
|
||||
section.completed = true
|
||||
// 重新计算进度
|
||||
const completed = courseSections.value.filter(s => s.completed).length
|
||||
const completed = courseSections.value.filter((s: any) => s.completed).length
|
||||
completedLessons.value = completed
|
||||
progress.value = Math.round((completed / courseSections.value.length) * 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化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) => {
|
||||
@ -1160,7 +952,7 @@ const handleDownload = (section: CourseSection) => {
|
||||
// 标记为已完成
|
||||
if (!section.completed) {
|
||||
section.completed = true
|
||||
const completed = courseSections.value.filter(s => s.completed).length
|
||||
const completed = courseSections.value.filter((s: any) => s.completed).length
|
||||
completedLessons.value = completed
|
||||
progress.value = Math.round((completed / courseSections.value.length) * 100)
|
||||
}
|
||||
@ -1274,28 +1066,20 @@ const submitComment = () => {
|
||||
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()
|
||||
loadCourseComments() // 启用评论接口调用
|
||||
loadCourseInstructors() // 启用讲师接口调用
|
||||
loadComments() // 启用评论接口调用
|
||||
})
|
||||
|
||||
// 组件卸载时清理CKPlayer实例
|
||||
// 组件卸载时清理播放器实例
|
||||
onUnmounted(() => {
|
||||
if (ckplayer.value) {
|
||||
try {
|
||||
ckplayer.value.remove()
|
||||
} catch (e) {
|
||||
console.log('清理播放器实例时出错:', e)
|
||||
}
|
||||
ckplayer.value = null
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -1374,131 +1158,10 @@ 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 {
|
||||
/* padding: 24px 0; */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.course-header {
|
||||
@ -2244,7 +1907,7 @@ onUnmounted(() => {
|
||||
|
||||
/* 课程标签页 */
|
||||
.course-tabs {
|
||||
/* margin-top: 32px; */
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
|
@ -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()
|
||||
}
|
||||
@ -430,6 +426,13 @@ const goToCourseDetail = (course: Course) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 选择排序方式
|
||||
const selectSort = (sortType: string) => {
|
||||
selectedSort.value = sortType
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
loadCourses() // 重新加载课程数据
|
||||
}
|
||||
|
||||
// 加载课程分类数据
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
@ -506,6 +509,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@font-face {
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
src: url('/fonts/AlimamaShuHeiTiBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
|
||||
.courses-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
@ -543,6 +552,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;
|
||||
|
@ -89,9 +89,9 @@
|
||||
<h3 class="course-title">{{ course.title }}</h3>
|
||||
<div class="course-meta">
|
||||
<span class="course-students">{{ course.studentsCount }}{{ t('home.popularCourses.studentsEnrolled')
|
||||
}}</span>
|
||||
}}</span>
|
||||
<button class="enroll-btn" @click="handleEnrollCourse(course.id)">{{ t('home.popularCourses.enroll')
|
||||
}}</button>
|
||||
}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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>
|
@ -3,11 +3,7 @@
|
||||
<!-- 横幅图区域 -->
|
||||
<div class="banner-section">
|
||||
<div class="banner-container">
|
||||
<img
|
||||
src="/images/Featured_resources/精选资源轮播.png"
|
||||
alt="精选资源横幅"
|
||||
class="banner-image"
|
||||
/>
|
||||
<img src="/images/Featured_resources/精选资源轮播.png" alt="精选资源横幅" class="banner-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,26 +14,18 @@
|
||||
<section class="featured-videos">
|
||||
<h2 class="section-title">精选视频</h2>
|
||||
<div class="featured-grid">
|
||||
<div
|
||||
v-for="video in featuredVideos"
|
||||
:key="video.id"
|
||||
class="featured-card"
|
||||
>
|
||||
<div v-for="video in featuredVideos" :key="video.id" class="featured-card" @click="handleVideoClick(video)">
|
||||
<div class="card-image">
|
||||
<img
|
||||
:src="video.image"
|
||||
:alt="video.title"
|
||||
class="video-thumbnail"
|
||||
/>
|
||||
<img :src="video.image" :alt="video.title" class="video-thumbnail" />
|
||||
<div class="duration-badge">
|
||||
<img src="/images/Featured_resources/duration.png" alt="时长" class="duration-icon">
|
||||
42:52
|
||||
</div>
|
||||
<!-- <div class="play-button">
|
||||
<div class="play-button">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
|
||||
<path d="M8 5V19L19 12L8 5Z" fill="white" />
|
||||
</svg>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{{ video.title }}</h3>
|
||||
@ -51,37 +39,25 @@
|
||||
<h2 class="section-title">全部视频</h2>
|
||||
<!-- 筛选标签 -->
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
v-for="tab in videoTabs"
|
||||
:key="tab.id"
|
||||
:class="['filter-tab', { active: activeVideoTab === tab.id }]"
|
||||
@click="activeVideoTab = tab.id"
|
||||
>
|
||||
<button v-for="tab in videoTabs" :key="tab.id"
|
||||
:class="['filter-tab', { active: activeVideoTab === tab.id }]" @click="activeVideoTab = tab.id">
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 视频网格 -->
|
||||
<div class="video-grid">
|
||||
<div
|
||||
v-for="video in allVideos"
|
||||
:key="video.id"
|
||||
class="video-card"
|
||||
>
|
||||
<div v-for="video in allVideos" :key="video.id" class="video-card" @click="handleVideoClick(video)">
|
||||
<div class="card-image">
|
||||
<img
|
||||
:src="video.image"
|
||||
:alt="video.title"
|
||||
class="video-thumbnail"
|
||||
/>
|
||||
<img :src="video.image" :alt="video.title" class="video-thumbnail" />
|
||||
<div class="duration-badge">
|
||||
<img src="/images/Featured_resources/duration.png" alt="时长" class="duration-icon">
|
||||
42:52
|
||||
</div>
|
||||
<!-- <div class="play-button">
|
||||
<div class="play-button">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 5V19L19 12L8 5Z" fill="white"/>
|
||||
<path d="M8 5V19L19 12L8 5Z" fill="white" />
|
||||
</svg>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{{ video.title }}</h3>
|
||||
@ -98,28 +74,16 @@
|
||||
<h2 class="section-title">全部图片</h2>
|
||||
<!-- 筛选标签 -->
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
v-for="tab in imageTabs"
|
||||
:key="tab.id"
|
||||
:class="['filter-tab', { active: activeImageTab === tab.id }]"
|
||||
@click="activeImageTab = tab.id"
|
||||
>
|
||||
<button v-for="tab in imageTabs" :key="tab.id"
|
||||
:class="['filter-tab', { active: activeImageTab === tab.id }]" @click="activeImageTab = tab.id">
|
||||
{{ tab.name }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 图片网格 -->
|
||||
<div class="image-grid">
|
||||
<div
|
||||
v-for="image in allImages"
|
||||
:key="image.id"
|
||||
class="image-card"
|
||||
>
|
||||
<div v-for="image in allImages" :key="image.id" class="image-card">
|
||||
<div class="card-image">
|
||||
<img
|
||||
:src="image.image"
|
||||
:alt="image.title"
|
||||
class="image-thumbnail"
|
||||
/>
|
||||
<img :src="image.image" :alt="image.title" class="image-thumbnail" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{{ image.title }}</h3>
|
||||
@ -132,28 +96,65 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放弹窗 -->
|
||||
<div v-if="showVideoModal" class="video-modal-overlay" @click="closeVideoModal">
|
||||
<div class="video-modal" @click.stop>
|
||||
<div class="video-modal-header">
|
||||
<h3 class="video-modal-title">{{ currentVideo?.title || '视频播放' }}</h3>
|
||||
<button class="close-btn" @click="closeVideoModal">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="video-modal-body">
|
||||
<DPlayerVideo ref="videoPlayerRef" :video-url="currentVideoUrl" :placeholder-image="currentVideo?.image"
|
||||
:placeholder-text="'点击播放视频'" :title="currentVideo?.title || '视频播放'" @play="handleVideoPlay"
|
||||
@pause="handleVideoPause" @ended="handleVideoEnded" @error="handleVideoError" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
|
||||
|
||||
// 视频播放弹窗相关状态
|
||||
const showVideoModal = ref(false)
|
||||
const currentVideo = ref<any>(null)
|
||||
const currentVideoUrl = ref('')
|
||||
const videoPlayerRef = ref<InstanceType<typeof DPlayerVideo>>()
|
||||
|
||||
// 视频源配置
|
||||
const VIDEO_CONFIG = {
|
||||
// 本地视频(当前使用)
|
||||
LOCAL: '/video/first.mp4',
|
||||
// HLS流(服务器准备好后使用)
|
||||
HLS: 'http://110.42.96.65:55513/learn/index.m3u8'
|
||||
}
|
||||
|
||||
// 精选视频数据
|
||||
const featuredVideos = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '西安工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/精选视频1.png'
|
||||
image: '/images/Featured_resources/精选视频1.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL // 添加视频URL
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '华南工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/精选视频2.png'
|
||||
image: '/images/Featured_resources/精选视频2.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '西安工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/精品视频3.png'
|
||||
image: '/images/Featured_resources/精品视频3.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
}
|
||||
])
|
||||
|
||||
@ -173,42 +174,50 @@ const allVideos = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '北京工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频1.png'
|
||||
image: '/images/Featured_resources/全部视频1.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '北京工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频2.png'
|
||||
image: '/images/Featured_resources/全部视频2.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '西安工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频3.png'
|
||||
image: '/images/Featured_resources/全部视频3.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '北京工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频4.png'
|
||||
image: '/images/Featured_resources/全部视频4.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '中国工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频5.png'
|
||||
image: '/images/Featured_resources/全部视频5.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '西安工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频6.png'
|
||||
image: '/images/Featured_resources/全部视频6.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '西安工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频7.png'
|
||||
image: '/images/Featured_resources/全部视频7.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: '内蒙古工业大学内部资源之一',
|
||||
image: '/images/Featured_resources/全部视频8.png'
|
||||
image: '/images/Featured_resources/全部视频8.png',
|
||||
videoUrl: VIDEO_CONFIG.LOCAL
|
||||
}
|
||||
])
|
||||
|
||||
@ -266,6 +275,52 @@ const allImages = ref([
|
||||
image: '/images/Featured_resources/全部图片8.png'
|
||||
}
|
||||
])
|
||||
|
||||
// 视频播放相关方法
|
||||
const handleVideoClick = async (video: any) => {
|
||||
console.log('点击视频:', video.title)
|
||||
currentVideo.value = video
|
||||
currentVideoUrl.value = video.videoUrl || VIDEO_CONFIG.LOCAL
|
||||
showVideoModal.value = true
|
||||
|
||||
// 等待弹窗显示后初始化播放器
|
||||
await nextTick()
|
||||
if (videoPlayerRef.value) {
|
||||
await videoPlayerRef.value.initializePlayer(currentVideoUrl.value)
|
||||
}
|
||||
}
|
||||
|
||||
const closeVideoModal = () => {
|
||||
showVideoModal.value = false
|
||||
currentVideo.value = null
|
||||
currentVideoUrl.value = ''
|
||||
|
||||
// 销毁播放器实例
|
||||
if (videoPlayerRef.value) {
|
||||
videoPlayerRef.value.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// DPlayer 事件处理方法
|
||||
const handleVideoPlay = () => {
|
||||
console.log('视频开始播放')
|
||||
}
|
||||
|
||||
const handleVideoPause = () => {
|
||||
console.log('视频暂停')
|
||||
}
|
||||
|
||||
const handleVideoEnded = () => {
|
||||
console.log('视频播放结束')
|
||||
}
|
||||
|
||||
const handleVideoError = (error: any) => {
|
||||
console.error('视频播放错误:', error)
|
||||
// 可以在这里处理错误,比如自动切换到本地视频
|
||||
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
|
||||
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -663,4 +718,150 @@ const allImages = ref([
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 视频播放弹窗样式 */
|
||||
.video-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.video-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
width: 1000px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: modalFadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.video-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.video-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #e5e7eb;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.video-modal-body {
|
||||
padding: 0;
|
||||
background: #000;
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* 播放按钮显示样式 */
|
||||
.play-button {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.featured-card:hover .play-button,
|
||||
.video-card:hover .play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.featured-card .play-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.video-card .play-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 弹窗 */
|
||||
@media (max-width: 768px) {
|
||||
.video-modal {
|
||||
width: 95vw;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.video-modal-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.video-modal-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.video-modal-body {
|
||||
height: 50vh;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.video-modal-overlay {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.video-modal {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-modal-header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.video-modal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.video-modal-body {
|
||||
height: 40vh;
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -201,7 +201,224 @@ 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: '课程设计专家',
|
||||
rating: 4.7,
|
||||
studentsCount: 567,
|
||||
coursesCount: 6,
|
||||
experience: '15年',
|
||||
education: ['云南师范大学', '课程设计博士'],
|
||||
certifications: ['高级课程设计师', '教育专家']
|
||||
},
|
||||
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: '教育研究专家',
|
||||
rating: 4.5,
|
||||
studentsCount: 432,
|
||||
coursesCount: 6,
|
||||
experience: '15年',
|
||||
education: ['云南师范大学', '教育研究博士'],
|
||||
certifications: ['高级教育研究专家', '学术顾问']
|
||||
},
|
||||
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: '心理咨询专家',
|
||||
rating: 4.9,
|
||||
studentsCount: 678,
|
||||
coursesCount: 6,
|
||||
experience: '15年',
|
||||
education: ['云南师范大学', '心理学博士'],
|
||||
certifications: ['高级心理咨询师', '心理治疗师']
|
||||
},
|
||||
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: '教育评估专家',
|
||||
rating: 4.4,
|
||||
studentsCount: 345,
|
||||
coursesCount: 6,
|
||||
experience: '15年',
|
||||
education: ['云南师范大学', '教育评估博士'],
|
||||
certifications: ['高级教育评估师', '测量技术专家']
|
||||
},
|
||||
status: 'published',
|
||||
createdAt: '2024-03-15T13:10:00Z',
|
||||
updatedAt: '2024-03-15T13:10:00Z'
|
||||
}
|
||||
] // 使用模拟数据,后续可以从API获取
|
||||
|
||||
// 按学科筛选
|
||||
if (selectedSubject.value !== '全部') {
|
||||
@ -502,7 +719,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);
|
||||
}
|
||||
|
||||
|