Merge remote-tracking branch 'origin/dev' into dev
# Conflicts: # package-lock.json # src/views/CourseDetailEnrolled.vue
@ -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 提供了更好的用户体验和更丰富的功能,是升级视频播放器的理想选择。
|
Before Width: | Height: | Size: 724 B After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/auth/revise.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 808 B After Width: | Height: | Size: 1.7 KiB |
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.3 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 |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 78 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 |
BIN
public/nav-icons/切换_switch备份 2.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/nav-icons/切换_switch备份.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/nav-icons/学习中心-选中.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 2.4 KiB |
BIN
public/nav-icons/提醒,感叹号_jurassic.png备份.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/nav-icons/用户_user备份 2.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
public/nav-icons/用户_user备份.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
public/nav-icons/矩形-选中.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 4.9 KiB |
BIN
public/nav-icons/管理端-选中.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 3.3 KiB |
BIN
public/nav-icons/路径备份 2.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/nav-icons/退出_logout备份 2.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/nav-icons/退出_logout备份 3.png
Normal file
After Width: | Height: | Size: 2.0 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
@ -19,6 +19,6 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
function open() {
|
||||
window.location.href = 'http://110.42.96.65:55613/demo/'
|
||||
window.location.href = 'http://103.40.14.23:25538/demo/'
|
||||
}
|
||||
</script>
|
@ -19,6 +19,6 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
function open() {
|
||||
window.location.href = 'http://110.42.96.65:55611/'
|
||||
window.location.href = 'http://103.40.14.23:25539/'
|
||||
}
|
||||
</script>
|
@ -22,6 +22,6 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
function open() {
|
||||
window.location.href = 'http://110.42.96.65:55612/'
|
||||
window.location.href = 'http://103.40.14.23:25537/'
|
||||
}
|
||||
</script>
|
@ -22,6 +22,6 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
function open() {
|
||||
window.location.href = 'http://110.42.96.65:55610/'
|
||||
window.location.href = 'http://103.40.14.23:25540/llm'
|
||||
}
|
||||
</script>
|
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>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
@ -64,7 +64,8 @@
|
||||
<div class="header-actions">
|
||||
<!-- 切换语言 -->
|
||||
<div class="action-item language-switcher" @click="toggleLanguageDropdown" ref="languageSwitcherRef">
|
||||
<img src="/nav-icons/矩形.png" alt="" class="action-icon" />
|
||||
<img src="/nav-icons/矩形.png" alt="" class="action-icon default-icon" />
|
||||
<img src="/nav-icons/矩形-选中.png" alt="" class="action-icon hover-icon" />
|
||||
<span>{{ t('header.languageSwitch') }}</span>
|
||||
<div v-if="showLanguageDropdown" class="language-dropdown">
|
||||
<div class="language-option" @click.stop="switchLanguage('zh')">
|
||||
@ -78,13 +79,15 @@
|
||||
|
||||
<!-- 学习中心 -->
|
||||
<div class="action-item" @click="handleLearningCenter">
|
||||
<img src="/nav-icons/学习中心.png" alt="" class="action-icon" />
|
||||
<img src="/nav-icons/学习中心.png" alt="" class="action-icon default-icon" />
|
||||
<img src="/nav-icons/学习中心-选中.png" alt="" class="action-icon hover-icon" />
|
||||
<span>{{ t('header.learningCenter') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 管理端 -->
|
||||
<div class="action-item">
|
||||
<img src="/nav-icons/管理端.png" alt="" class="action-icon" />
|
||||
<img src="/nav-icons/管理端.png" alt="" class="action-icon default-icon" />
|
||||
<img src="/nav-icons/管理端-选中.png" alt="" class="action-icon hover-icon" />
|
||||
<span>{{ t('header.management') }}</span>
|
||||
</div>
|
||||
|
||||
@ -101,8 +104,11 @@
|
||||
<div v-else class="user-menu">
|
||||
<n-dropdown :options="userMenuOptions" @select="handleUserMenuSelect">
|
||||
<div class="user-info">
|
||||
<SafeAvatar :src="userStore.user?.avatar" :name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username" :size="32" />
|
||||
<span class="username">{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username }}</span>
|
||||
<SafeAvatar :src="userStore.user?.avatar"
|
||||
:name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username"
|
||||
:size="32" />
|
||||
<span class="username">{{ userStore.user?.profile?.realName || userStore.user?.nickname ||
|
||||
userStore.user?.username }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
@ -126,8 +132,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import {
|
||||
PersonOutline,
|
||||
LogOutOutline,
|
||||
MenuOutline,
|
||||
CloseOutline
|
||||
} from '@vicons/ionicons5'
|
||||
@ -203,7 +207,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 +220,7 @@ const userMenuOptions = computed(() => [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: () => h(LogOutOutline)
|
||||
icon: () => h('div', { class: 'custom-icon' }, '🚪')
|
||||
}
|
||||
])
|
||||
|
||||
@ -258,6 +267,12 @@ const handleUserMenuSelect = (key: string) => {
|
||||
window.location.reload();
|
||||
})
|
||||
break
|
||||
case 'teacher':
|
||||
// 切换教师端逻辑
|
||||
console.log('切换到教师端')
|
||||
// 这里可以添加切换教师端的逻辑,比如跳转到教师端页面
|
||||
router.push('/teacher')
|
||||
break
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
router.push('/')
|
||||
@ -319,9 +334,12 @@ onUnmounted(() => {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0 30px;
|
||||
height: 100%;
|
||||
height: 64px;
|
||||
background: white;
|
||||
position: relative;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
@ -372,18 +390,31 @@ onUnmounted(() => {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 图标悬停效果 */
|
||||
.action-item .hover-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-item:hover .default-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-item:hover .hover-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
|
||||
/* 导航菜单 */
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
flex: 1;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -397,72 +428,72 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 两个字的导航项:首页 */
|
||||
.nav-item:nth-child(1) {
|
||||
/* 两个字的导航项:首页 */
|
||||
.nav-item:nth-child(1) {
|
||||
width: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
|
||||
.nav-item:nth-child(2),
|
||||
.nav-item:nth-child(3),
|
||||
.nav-item:nth-child(4),
|
||||
.nav-item:nth-child(5) {
|
||||
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
|
||||
.nav-item:nth-child(2),
|
||||
.nav-item:nth-child(3),
|
||||
.nav-item:nth-child(4),
|
||||
.nav-item:nth-child(5) {
|
||||
width: 72px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 两个字的导航项:活动 */
|
||||
.nav-item:nth-child(6) {
|
||||
/* 两个字的导航项:活动 */
|
||||
.nav-item:nth-child(6) {
|
||||
width: 36px;
|
||||
padding-right: 16px;
|
||||
/* 为HOT标签留出空间 */
|
||||
}
|
||||
}
|
||||
|
||||
/* AI导航项 */
|
||||
.nav-item:nth-child(7) {
|
||||
/* AI导航项 */
|
||||
.nav-item:nth-child(7) {
|
||||
/* width: 50px;
|
||||
height: 40px; */
|
||||
background-image: url('/images/ai/ai-bg.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:nth-child(8) {
|
||||
.nav-item:nth-child(8) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.active .nav-item-ai {
|
||||
.nav-item.active .nav-item-ai {
|
||||
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item-ai:hover {
|
||||
.nav-item-ai:hover {
|
||||
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
.nav-item:hover {
|
||||
color: #0084CD;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
.nav-item.active {
|
||||
color: #0084CD;
|
||||
font-weight: 400;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item.active::after {
|
||||
.nav-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
@ -472,9 +503,9 @@ onUnmounted(() => {
|
||||
height: 2px;
|
||||
background-color: #0084CD;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
.new-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -22px;
|
||||
@ -484,55 +515,55 @@ onUnmounted(() => {
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
/* 搜索区域 */
|
||||
.search-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
border-left: 1px solid #ececec;
|
||||
border-right: 1px solid #ececec;
|
||||
.search-box {
|
||||
/* border-left: 1px solid #ececec;
|
||||
border-right: 1px solid #ececec; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22px 16px;
|
||||
width: 280px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
/* .search-box:hover {
|
||||
/* .search-box:hover {
|
||||
background: #eeeeee;
|
||||
} */
|
||||
} */
|
||||
|
||||
.search-icon {
|
||||
.search-icon {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
.search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端汉堡菜单按钮 */
|
||||
.mobile-menu-toggle {
|
||||
/* 移动端汉堡菜单按钮 */
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -541,21 +572,21 @@ onUnmounted(() => {
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-toggle:hover {
|
||||
.mobile-menu-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧操作区域 */
|
||||
.header-actions {
|
||||
/* 右侧操作区域 */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item {
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
@ -566,20 +597,20 @@ onUnmounted(() => {
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item:hover {
|
||||
.action-item:hover {
|
||||
color: #1890ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 语言切换器 */
|
||||
.language-switcher {
|
||||
/* 语言切换器 */
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
.language-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
@ -591,38 +622,38 @@ onUnmounted(() => {
|
||||
z-index: 1000;
|
||||
margin-top: 4px;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option {
|
||||
.language-option {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option:last-child {
|
||||
.language-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
.language-option:hover {
|
||||
background: #f0f8ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.language-text {
|
||||
.language-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 认证按钮 */
|
||||
.auth-buttons {
|
||||
/* 认证按钮 */
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-combined-btn {
|
||||
.auth-combined-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0088D1;
|
||||
@ -633,37 +664,37 @@ onUnmounted(() => {
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-combined-btn:hover {
|
||||
.auth-combined-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-login,
|
||||
.auth-register {
|
||||
.auth-login,
|
||||
.auth-register {
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-login:hover,
|
||||
.auth-register:hover {
|
||||
.auth-login:hover,
|
||||
.auth-register:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
.auth-divider {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 0 4px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@ -671,18 +702,63 @@ onUnmounted(() => {
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
.user-info:hover {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 美化用户菜单样式 */
|
||||
:deep(.n-dropdown-menu) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option) {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option:hover) {
|
||||
background: linear-gradient(135deg, #f0f8ff, #e6f4ff);
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.n-dropdown-option .n-dropdown-option-icon) {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* 分割线样式 */
|
||||
:deep(.n-dropdown-divider) {
|
||||
margin: 8px 0;
|
||||
border-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* 大屏幕 */
|
||||
@media (min-width: 1200px) {
|
||||
|
@ -40,7 +40,7 @@ import AppFooter from './AppFooter.vue'
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
|
||||
position: sticky;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
flex-shrink: 0;
|
||||
@ -66,12 +66,19 @@ import AppFooter from './AppFooter.vue'
|
||||
.header {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding-top: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (max-width: 480px) {
|
||||
.header {
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏模式样式现在在App.vue中统一管理 */
|
||||
|
@ -7,7 +7,7 @@
|
||||
"resources": "精选资源",
|
||||
"about": "活动",
|
||||
"languageSwitch": "切换语言",
|
||||
"learningCenter": "学习中心",
|
||||
"learningCenter": "积分中心",
|
||||
"management": "管理端",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
|
@ -12,6 +12,7 @@ import '@/assets/fonts/AlimamaShuHeiTi-Bold.ttf'
|
||||
import '@/assets/fonts/文道潮黑.ttf'
|
||||
import '@/assets/fonts/庞门正道标题体3.0.ttf'
|
||||
import '@/assets/fonts/DouyinSansBold.otf'
|
||||
import '@/assets/fonts/Alibaba_PuHuiTi_2.0_55_Regular_85_Bold.ttf'
|
||||
|
||||
// Naive UI
|
||||
import {
|
||||
|
@ -23,6 +23,7 @@ import ExamNotice from '@/views/ExamNotice.vue'
|
||||
import ExamSubmitted from '@/views/ExamSubmitted.vue'
|
||||
import TestSections from '@/views/TestSections.vue'
|
||||
import LocalVideoDemo from '@/views/LocalVideoDemo.vue'
|
||||
import DPlayerTest from '@/views/DPlayerTest.vue'
|
||||
import SpecialTraining from '@/views/SpecialTraining.vue'
|
||||
import SpecialTrainingDetail from '@/views/SpecialTrainingDetail.vue'
|
||||
import HelpCenter from '@/views/HelpCenter.vue'
|
||||
@ -250,6 +251,14 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '本地视频播放演示'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dplayer-test',
|
||||
name: 'DPlayerTest',
|
||||
component: DPlayerTest,
|
||||
meta: {
|
||||
title: 'DPlayer 测试页面'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/course/:courseId/practice/:sectionId',
|
||||
name: 'Practice',
|
||||
|
@ -289,7 +289,7 @@ onMounted(() => {
|
||||
margin: auto;
|
||||
width: 1420px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
@ -420,6 +420,11 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
src: url('/fonts/AlimamaShuHeiTiBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
body * {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
@ -852,7 +857,7 @@ button:active {
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
font-size: 32px;
|
||||
font-family: AlimamaShuHeiTi-Bold;
|
||||
font-family: 'AlimamaShuHeiTiBold';
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
@ -132,9 +132,9 @@
|
||||
|
||||
<!-- 课程描述 -->
|
||||
<div class="course-description">
|
||||
<p>{{ course.description ||
|
||||
'本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书,技能大赛紧密结合,课程设置紧密对应实际全面共享,可为职业工作人员、在校学生、创行教师提供服务与学习支持。'
|
||||
}}</p>
|
||||
<p
|
||||
v-html="course.description || '本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书,技能大赛紧密结合,课程设置紧密对应实际全面共享,可为职业工作人员、在校学生、创行教师提供服务与学习支持。'">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 讲师信息 -->
|
||||
@ -2059,7 +2059,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.instructor-info {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.instructor-name {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -12,9 +12,11 @@
|
||||
<!-- 左侧侧边栏 -->
|
||||
<div class="block_14">
|
||||
<!-- 用户头像和姓名 -->
|
||||
<SafeAvatar class="image_7" :src="userStore.user?.avatar" :name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户'" :size="96"
|
||||
alt="用户头像" />
|
||||
<span class="text_72">{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户名' }}</span>
|
||||
<SafeAvatar class="image_7" :src="userStore.user?.avatar"
|
||||
:name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户'"
|
||||
:size="96" alt="用户头像" />
|
||||
<span class="text_72">{{ userStore.user?.profile?.realName || userStore.user?.nickname ||
|
||||
userStore.user?.username || '用户名' }}</span>
|
||||
|
||||
<!-- 菜单项容器 -->
|
||||
<div class="box_22">
|
||||
@ -23,73 +25,66 @@
|
||||
|
||||
<!-- 我的课程 -->
|
||||
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')">
|
||||
<img class="image_8" referrerpolicy="no-referrer" :src="activeTab === 'courses'
|
||||
? '/images/profile/course-active.png'
|
||||
: '/images/profile/course.png'" />
|
||||
<img class="image_8 default-icon" referrerpolicy="no-referrer" src="/images/profile/course.png" />
|
||||
<img class="image_8 hover-icon" referrerpolicy="no-referrer" src="/images/profile/course-active.png" />
|
||||
<span class="text-group_19">我的课程</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的作业 -->
|
||||
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')">
|
||||
<img class="label_4" referrerpolicy="no-referrer" :src="activeTab === 'homework'
|
||||
? '/images/profile/grade-active.png'
|
||||
: '/images/profile/grade.png'" />
|
||||
<img class="label_4 default-icon" referrerpolicy="no-referrer" src="/images/profile/grade.png" />
|
||||
<img class="label_4 hover-icon" referrerpolicy="no-referrer" src="/images/profile/grade-active.png" />
|
||||
<span class="text-group_20">我的作业</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的考试 -->
|
||||
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')">
|
||||
<img class="label_5" referrerpolicy="no-referrer" :src="activeTab === 'exam'
|
||||
? '/images/profile/checklist-active.png'
|
||||
: '/images/profile/checklist.png'" />
|
||||
<img class="label_5 default-icon" referrerpolicy="no-referrer" src="/images/profile/checklist.png" />
|
||||
<img class="label_5 hover-icon" referrerpolicy="no-referrer" src="/images/profile/checklist-active.png" />
|
||||
<span class="text-group_21">我的考试</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的练习 -->
|
||||
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')">
|
||||
<img class="label_6" referrerpolicy="no-referrer" :src="activeTab === 'practice'
|
||||
? '/images/profile/bookmark-active.png'
|
||||
: '/images/profile/bookmark.png'" />
|
||||
<img class="label_6 default-icon" referrerpolicy="no-referrer" src="/images/profile/bookmark.png" />
|
||||
<img class="label_6 hover-icon" referrerpolicy="no-referrer" src="/images/profile/bookmark-active.png" />
|
||||
<span class="text-group_22">我的练习</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的活动 -->
|
||||
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')">
|
||||
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="activeTab === 'activity'
|
||||
? '/images/profile/gift-active.png'
|
||||
: '/images/profile/gift.png'" />
|
||||
<img class="thumbnail_40 default-icon" referrerpolicy="no-referrer" src="/images/profile/gift.png" />
|
||||
<img class="thumbnail_40 hover-icon" referrerpolicy="no-referrer" src="/images/profile/gift-active.png" />
|
||||
<span class="text-group_23">我的活动</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的关注 -->
|
||||
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')">
|
||||
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="activeTab === 'follows'
|
||||
? '/images/profile/concern-active.png'
|
||||
: '/images/profile/concern.png'" />
|
||||
<img class="thumbnail_42 default-icon" referrerpolicy="no-referrer" src="/images/profile/concern.png" />
|
||||
<img class="thumbnail_42 hover-icon" referrerpolicy="no-referrer"
|
||||
src="/images/profile/concern-active.png" />
|
||||
<span class="text-group_27">我的关注</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的消息 -->
|
||||
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')">
|
||||
<img class="label_7" referrerpolicy="no-referrer" :src="activeTab === 'message'
|
||||
? '/images/profile/message-active.png'
|
||||
: '/images/profile/message.png'" />
|
||||
<img class="label_7 default-icon" referrerpolicy="no-referrer" src="/images/profile/message.png" />
|
||||
<img class="label_7 hover-icon" referrerpolicy="no-referrer" src="/images/profile/message-active.png" />
|
||||
<span class="text-group_24">我的消息</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的资料 -->
|
||||
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')">
|
||||
<img class="image_9" referrerpolicy="no-referrer" :src="activeTab === 'materials'
|
||||
? '/images/profile/profile-active.png'
|
||||
: '/images/profile/profile.png'" />
|
||||
<img class="image_9 default-icon" referrerpolicy="no-referrer" src="/images/profile/profile.png" />
|
||||
<img class="image_9 hover-icon" referrerpolicy="no-referrer" src="/images/profile/profile-active.png" />
|
||||
<span class="text-group_25">我的资料</span>
|
||||
</div>
|
||||
|
||||
<!-- 我的下载 -->
|
||||
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')">
|
||||
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="activeTab === 'download'
|
||||
? '/images/profile/download-active.png'
|
||||
: '/images/profile/download.png'" />
|
||||
<img class="thumbnail_41 default-icon" referrerpolicy="no-referrer" src="/images/profile/download.png" />
|
||||
<img class="thumbnail_41 hover-icon" referrerpolicy="no-referrer"
|
||||
src="/images/profile/download-active.png" />
|
||||
<span class="text-group_26">我的下载</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,7 +182,7 @@
|
||||
<!-- 作业内容 -->
|
||||
<div v-else-if="isHomeworkTab" class="homework-content">
|
||||
<!-- 作业筛选标签 -->
|
||||
<div class="text-wrapper_1 flex-row">
|
||||
<div v-if="!showDraftBoxView" class="text-wrapper_1 flex-row">
|
||||
<span class="text_12" :class="{ active: activeHomeworkTab === 'all' }"
|
||||
@click="handleHomeworkTabChange('all')">全部作业</span>
|
||||
<span class="text_13" :class="{ active: activeHomeworkTab === 'pending' }"
|
||||
@ -200,7 +195,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="course-divider"></div>
|
||||
<div v-if="!showDraftBoxView" class="course-divider"></div>
|
||||
|
||||
<!-- 面包屑 -->
|
||||
<!-- <div class="breadcrumb-wrapper flex-row">
|
||||
@ -211,13 +206,13 @@
|
||||
|
||||
<!-- 作业详情视图 -->
|
||||
<div v-if="showDetailView && detailAssignment">
|
||||
<div class="detail-header">
|
||||
<!-- <div class="detail-header">
|
||||
<div class="breadcrumb-nav">
|
||||
<span class="breadcrumb-item" @click="backToAssignmentList">全部作业</span>
|
||||
<span class="breadcrumb-separator">></span>
|
||||
<span class="breadcrumb-current">作业名称</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="group_11">
|
||||
<!-- 头部信息 -->
|
||||
@ -245,6 +240,7 @@
|
||||
|
||||
<!-- 作业内容 -->
|
||||
<div class="text-group_4">
|
||||
<div class="course-divider"></div>
|
||||
<span class="text_33">{{ detailAssignment.title }}</span>
|
||||
<div class="description-container">
|
||||
<span class="text_34 description-full-view">
|
||||
@ -854,12 +850,10 @@
|
||||
<div class="avatar-section">
|
||||
<div class="avatar-container">
|
||||
<SafeAvatar :src="userStore.user?.avatar"
|
||||
:name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户'" :size="68" alt="用户头像"
|
||||
class="user-avatar-large" />
|
||||
:name="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username || '用户'"
|
||||
:size="68" alt="用户头像" class="user-avatar-large" />
|
||||
<div class="avatar-edit-btn">
|
||||
<img
|
||||
src="https://lanhu-oss-2537-2.lanhuapp.com/SketchPngf31d99a65996c8c8ed0d6b2446b4176a30b16838933c10b800a735d092717c57"
|
||||
alt="编辑头像" class="edit-icon" />
|
||||
<img src="/images/auth/revise.png" alt="编辑头像" class="edit-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -957,8 +951,8 @@
|
||||
<!-- 面包屑导航或筛选和操作区域 -->
|
||||
<div v-if="isInSubDirectory" class="breadcrumb-controls">
|
||||
<div class="breadcrumb-nav">
|
||||
<span class="breadcrumb-text" @click="goBack">课件>图片></span>
|
||||
<span class="breadcrumb-current">风景图片</span>
|
||||
<span class="breadcrumb-text" @click="goBack">{{ currentPath.join(' > ') }} ></span>
|
||||
<span class="breadcrumb-current">{{ currentPath[currentPath.length - 1] || '文件夹' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -979,7 +973,7 @@
|
||||
<input v-model="downloadFilter.keyword" type="text" class="search-input" placeholder="请输入文件名称" />
|
||||
<button class="search-btn">
|
||||
<img
|
||||
src="https://lanhu-oss-2537-2.lanhuapp.com/SketchPng870a86da8af58a60f35fcb27ef4822e645d2ad5aaabe6416e4179342a53a5a60"
|
||||
src="/images/profile/search.png"
|
||||
alt="搜索图标" class="search-icon" />
|
||||
</button>
|
||||
</div>
|
||||
@ -997,7 +991,8 @@
|
||||
<div class="files-grid"
|
||||
:class="{ 'subdirectory-grid': isInSubDirectory, 'certificate-grid': activeDownloadTab === 'certificate', 'homework-grid': activeDownloadTab === 'homework' }">
|
||||
<div v-for="file in filteredDownloadFiles" :key="file.id" class="file-item"
|
||||
:class="{ 'subdirectory-item': isInSubDirectory }" @click="handleFileClick(file)">
|
||||
:class="{ 'subdirectory-item': isInSubDirectory }" @dblclick="handleFolderDoubleClick(file)"
|
||||
@click="handleFileClick(file)">
|
||||
<div class="file-menu">
|
||||
<button class="file-menu-btn" @click.stop="toggleFileMenu(file.id)">
|
||||
<img src="/images/profile/more.png" alt="更多操作" class="more-icon" />
|
||||
@ -2486,6 +2481,11 @@ const handleDownloadTabChange = (tab: string) => {
|
||||
|
||||
// 筛选后的下载文件
|
||||
const filteredDownloadFiles = computed(() => {
|
||||
// 如果在子目录中,返回空数组显示空页面
|
||||
if (isInSubDirectory.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
let files = downloadFiles.filter(file => file.category === activeDownloadTab.value)
|
||||
|
||||
if (downloadFilter.type !== 'all') {
|
||||
@ -2507,10 +2507,16 @@ const toggleFileMenu = (fileId: number) => {
|
||||
}
|
||||
|
||||
const handleFileClick = (file: any) => {
|
||||
if (file.type === 'folder' && file.name === '图片') {
|
||||
// 点击图片文件夹,进入子目录
|
||||
// 单击文件时的处理逻辑(如选中文件等)
|
||||
console.log('单击文件:', file.name)
|
||||
}
|
||||
|
||||
const handleFolderDoubleClick = (file: any) => {
|
||||
if (file.type === 'folder') {
|
||||
// 双击文件夹,进入子目录(显示空页面)
|
||||
isInSubDirectory.value = true
|
||||
currentPath.value = ['课件', '图片']
|
||||
currentPath.value = ['课件', file.name]
|
||||
console.log('进入文件夹:', file.name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2541,7 +2547,7 @@ const getFileIcon = (fileId?: number) => {
|
||||
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPngf45333052202c303acc2c06223c26b820d330459ce2d452a21a3132fbbeab442'
|
||||
} else {
|
||||
// 默认文件夹图标
|
||||
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPng5548891b00234027dbe6dadafbd83596d616261421c0587a85652dc194b2d5ef'
|
||||
return '/images/profile/folder.png'
|
||||
}
|
||||
}
|
||||
|
||||
@ -2689,10 +2695,10 @@ const viewAssignmentDetail = (assignment: Assignment) => {
|
||||
}
|
||||
|
||||
// 返回作业列表
|
||||
const backToAssignmentList = () => {
|
||||
showDetailView.value = false
|
||||
detailAssignment.value = null
|
||||
}
|
||||
// const backToAssignmentList = () => {
|
||||
// showDetailView.value = false
|
||||
// detailAssignment.value = null
|
||||
// }
|
||||
|
||||
// 从详情页面跳转到上传作业
|
||||
const showUploadFromDetail = () => {
|
||||
@ -2967,7 +2973,7 @@ onActivated(() => {
|
||||
/* 自适应高度 */
|
||||
min-height: 3vh;
|
||||
/* 设置最小高度,让盒子更大 */
|
||||
margin: 1.5vh;
|
||||
margin: .5vh;
|
||||
/* 减小间距:从2.34vh减少到1.5vh */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -3076,6 +3082,68 @@ onActivated(() => {
|
||||
/* 悬停时蓝色 */
|
||||
}
|
||||
|
||||
/* 菜单项图标悬停效果 */
|
||||
.image-text_19 .hover-icon,
|
||||
.image-text_20 .hover-icon,
|
||||
.image-text_21 .hover-icon,
|
||||
.image-text_22 .hover-icon,
|
||||
.image-text_23 .hover-icon,
|
||||
.image-text_24 .hover-icon,
|
||||
.image-text_25 .hover-icon,
|
||||
.image-text_26 .hover-icon,
|
||||
.image-text_27 .hover-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-text_19:hover .default-icon,
|
||||
.image-text_20:hover .default-icon,
|
||||
.image-text_21:hover .default-icon,
|
||||
.image-text_22:hover .default-icon,
|
||||
.image-text_23:hover .default-icon,
|
||||
.image-text_24:hover .default-icon,
|
||||
.image-text_25:hover .default-icon,
|
||||
.image-text_26:hover .default-icon,
|
||||
.image-text_27:hover .default-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-text_19:hover .hover-icon,
|
||||
.image-text_20:hover .hover-icon,
|
||||
.image-text_21:hover .hover-icon,
|
||||
.image-text_22:hover .hover-icon,
|
||||
.image-text_23:hover .hover-icon,
|
||||
.image-text_24:hover .hover-icon,
|
||||
.image-text_25:hover .hover-icon,
|
||||
.image-text_26:hover .hover-icon,
|
||||
.image-text_27:hover .hover-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 激活状态的图标显示 */
|
||||
.image-text_19.active .default-icon,
|
||||
.image-text_20.active .default-icon,
|
||||
.image-text_21.active .default-icon,
|
||||
.image-text_22.active .default-icon,
|
||||
.image-text_23.active .default-icon,
|
||||
.image-text_24.active .default-icon,
|
||||
.image-text_25.active .default-icon,
|
||||
.image-text_26.active .default-icon,
|
||||
.image-text_27.active .default-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-text_19.active .hover-icon,
|
||||
.image-text_20.active .hover-icon,
|
||||
.image-text_21.active .hover-icon,
|
||||
.image-text_22.active .hover-icon,
|
||||
.image-text_23.active .hover-icon,
|
||||
.image-text_24.active .hover-icon,
|
||||
.image-text_25.active .hover-icon,
|
||||
.image-text_26.active .hover-icon,
|
||||
.image-text_27.active .hover-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 右侧课程列表区域 */
|
||||
.group_5 {
|
||||
width: 65vw;
|
||||
@ -4186,9 +4254,8 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.course-name {
|
||||
margin-top: 10px;
|
||||
margin-left: 83px;
|
||||
color: #999999;
|
||||
margin-left: 0px;
|
||||
color: #497087;
|
||||
}
|
||||
|
||||
.course-name span {
|
||||
@ -5745,6 +5812,7 @@ onActivated(() => {
|
||||
line-height: 1.4;
|
||||
padding: 0 1.04vw;
|
||||
/* 添加左右内边距 */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
@ -6172,7 +6240,7 @@ onActivated(() => {
|
||||
/* height: 5.21vh; */
|
||||
padding: 0.52vh 0.57vw;
|
||||
/* 100px转换为vh,进一步增加高度 */
|
||||
background: url('https://lanhu-oss-2537-2.lanhuapp.com/SketchPng9491a7fe5bdac8e8a88de63907163bd6b8a259824f56a3c76784ba6cdc7bc32b') 100% no-repeat;
|
||||
background: #F5F8FB;
|
||||
background-size: 100% 100%;
|
||||
margin-top: 0.26vh;
|
||||
/* 5px转换为vh */
|
||||
@ -7043,6 +7111,7 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.account-display {
|
||||
padding-left: 15px;
|
||||
width: 100%;
|
||||
height: 41px;
|
||||
background: #F5F8FB;
|
||||
@ -7609,7 +7678,7 @@ onActivated(() => {
|
||||
/* 4px转换为vw,减小图标和文字间距 */
|
||||
padding: 0.52vh 0.73vw;
|
||||
/* 10px 14px转换 */
|
||||
font-size: 10px;
|
||||
font-size: 14px;
|
||||
/* 14px转换为vw */
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
color: #000;
|
||||
@ -7620,9 +7689,9 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 12px;
|
||||
/* 24px转换为vw,增加一倍 */
|
||||
height: 12px;
|
||||
width: 18px;
|
||||
/* 84px转换为vw,增加一倍 */
|
||||
height: 18px;
|
||||
/* 24px转换为vh,增加一倍 */
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
@ -7643,13 +7712,13 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 5.21vw;
|
||||
width: 131px;
|
||||
/* 100px转换为vw */
|
||||
height: 5.21vw;
|
||||
height: 131px;
|
||||
/* 100px转换为vw */
|
||||
margin-top: 4.56vh;
|
||||
margin-left: 0.5vw;
|
||||
margin-top: 1.5vh;
|
||||
/* 30px转换为vh,增加顶部间距,图片下移 */
|
||||
margin-left: 1.26vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|