Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	package-lock.json
#	src/views/CourseDetailEnrolled.vue
This commit is contained in:
小张 2025-08-20 11:16:39 +08:00
commit a4832dc8fc
63 changed files with 3049 additions and 520 deletions

@ -1 +0,0 @@
Subproject commit 96c6f6254ac8ada76c63f2b88e30a143b6d115b8

View File

@ -0,0 +1,320 @@
# DPlayer 集成指南
## 什么是 DPlayer
**DPlayer** 是由 [DIYGod](https://github.com/DIYGod) 开发的一个开源的 HTML5 视频播放器,具有以下特点:
- 🎨 **界面美观**:现代化的设计风格
- 🎯 **轻量级**:体积小,加载快
- 🌏 **中文友好**:由中国开发者开发,中文文档完善
- 🎮 **功能丰富**:支持弹幕、快捷键、倍速播放等
- 📱 **移动端适配**:响应式设计,支持移动设备
## 主要功能特性
### 基础功能
- ✅ 播放/暂停控制
- ✅ 音量控制
- ✅ 进度条拖拽
- ✅ 全屏切换
- ✅ 倍速播放 (0.5x - 2x)
### 高级功能
- 🎯 键盘快捷键支持
- 🎨 自定义主题色
- 📝 右键菜单自定义
- 🎵 音频可视化
- 📱 移动端手势支持
### 格式支持
- MP4
- WebM
- Ogg
- HLS (.m3u8)
- FLV
- 更多格式通过插件支持
## 安装和集成
### 方法1CDN 引入(推荐用于快速测试)
```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>
```
### 方法2NPM 安装(推荐用于生产环境)
```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 是一个优秀的视频播放器选择**,特别适合:
- 🎯 需要美观界面的项目
- 🌏 中文用户群体
- 📱 重视移动端体验
- 🎨 需要自定义主题的项目
- ⚡ 追求轻量级解决方案
相比当前的 CKPlayerDPlayer 提供了更好的用户体验和更丰富的功能,是升级视频播放器的理想选择。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/logo/云师大.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View 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
使

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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;
}

View File

@ -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;
}

View 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>

View File

@ -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) {

View File

@ -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中统一管理 */

View File

@ -7,7 +7,7 @@
"resources": "精选资源",
"about": "活动",
"languageSwitch": "切换语言",
"learningCenter": "学习中心",
"learningCenter": "积分中心",
"management": "管理端",
"login": "登录",
"register": "注册",

View File

@ -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 {

View File

@ -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',

View File

@ -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;
}

View File

@ -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;

View File

@ -132,9 +132,9 @@
<!-- 课程描述 -->
<div class="course-description">
<p>{{ course.description ||
'本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员、在校学生、创行教师提供服务与学习支持。'
}}</p>
<p
v-html="course.description || '本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、&quot;1+X&quot;WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员、在校学生、创行教师提供服务与学习支持。'">
</p>
</div>
<!-- 讲师信息 -->
@ -2059,7 +2059,7 @@ onMounted(() => {
}
.instructor-info {
text-align: center;
text-align: left;
}
.instructor-name {

View File

@ -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
View 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
View 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>

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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">课件&gt;图片&gt;</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;

View File

@ -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>

View File

@ -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);
}