style: 调整样式

This commit is contained in:
Wxp 2025-08-19 19:04:11 +08:00
parent eff83cfdc3
commit 0fe429cd79
41 changed files with 2810 additions and 791 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 提供了更好的用户体验和更丰富的功能,是升级视频播放器的理想选择。

35
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@vicons/ionicons5": "^0.13.0",
"axios": "^1.11.0",
"ckplayer": "^3.1.2",
"dplayer": "^1.27.1",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"quill": "^2.0.3",
@ -1777,6 +1778,12 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balloon-css": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/balloon-css/-/balloon-css-1.2.0.tgz",
"integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==",
"license": "MIT"
},
"node_modules/birpc": {
"version": "2.5.0",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz",
@ -2139,6 +2146,28 @@
"node": ">=0.4.0"
}
},
"node_modules/dplayer": {
"version": "1.27.1",
"resolved": "https://registry.npmmirror.com/dplayer/-/dplayer-1.27.1.tgz",
"integrity": "sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==",
"license": "MIT",
"dependencies": {
"axios": "1.2.3",
"balloon-css": "^1.0.3",
"promise-polyfill": "8.3.0"
}
},
"node_modules/dplayer/node_modules/axios": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.2.3.tgz",
"integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -3207,6 +3236,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/promise-polyfill": {
"version": "8.3.0",
"resolved": "https://registry.npmmirror.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",

View File

@ -16,6 +16,7 @@
"@vicons/ionicons5": "^0.13.0",
"axios": "^1.11.0",
"ckplayer": "^3.1.2",
"dplayer": "^1.27.1",
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"quill": "^2.0.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 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

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

@ -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>
@ -249,7 +249,7 @@ export default {
border: 1px solid #D8D8D8;
border-radius: 6px;
font-size: 12px;
color: #D9D9D9;
color: #000;
background: #fff;
transition: all 0.2s;
}

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>
<!-- 搜索框 -->
@ -126,8 +126,6 @@ import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import {
PersonOutline,
LogOutOutline,
MenuOutline,
CloseOutline
} from '@vicons/ionicons5'
@ -189,7 +187,12 @@ const userMenuOptions = computed(() => [
{
label: '个人中心',
key: 'profile',
icon: () => h(PersonOutline)
icon: () => h('div', { class: 'custom-icon' }, '👤')
},
{
label: '切换教师端',
key: 'teacher',
icon: () => h('div', { class: 'custom-icon' }, '👨‍🏫')
},
{
type: 'divider'
@ -197,7 +200,7 @@ const userMenuOptions = computed(() => [
{
label: '退出登录',
key: 'logout',
icon: () => h(LogOutOutline)
icon: () => h('div', { class: 'custom-icon' }, '🚪')
}
])
@ -244,6 +247,12 @@ const handleUserMenuSelect = (key: string) => {
window.location.reload();
})
break
case 'teacher':
//
console.log('切换到教师端')
//
router.push('/teacher')
break
case 'logout':
userStore.logout()
router.push('/')
@ -305,370 +314,418 @@ onUnmounted(() => {
max-width: none;
margin: 0;
padding: 0 30px;
height: 100%;
height: 64px;
background: white;
position: relative;
z-index: 1001;
}
/* Logo区域 */
.logo-section {
flex-shrink: 0;
margin-right: 40px;
}
.logo {
display: flex;
align-items: center;
cursor: pointer;
gap: 8px;
}
.logo:hover {
opacity: 0.8;
}
.logo-image {
width: 72px;
height: 61px;
object-fit: contain;
}
.nav-icon {
max-width: 12px;
max-height: 12px;
width: auto;
height: auto;
margin-right: 4px;
object-fit: contain;
}
/* AI图标样式 */
.ai-icon {
max-width: 34px;
max-height: 34px;
margin-right: 0;
}
.action-icon {
max-width: 18px;
max-height: 18px;
width: auto;
height: auto;
object-fit: contain;
}
/* 导航菜单 */
.nav-menu {
display: flex;
align-items: center;
gap: 30px;
flex: 1;
margin-right: 40px;
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
font-size: 14px;
font-weight: 400;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #000;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
position: relative;
height: 24px;
}
/* 两个字的导航项:首页 */
.nav-item:nth-child(1) {
width: 36px;
}
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
.nav-item:nth-child(2),
.nav-item:nth-child(3),
.nav-item:nth-child(4),
.nav-item:nth-child(5) {
width: 72px;
}
/* 两个字的导航项:活动 */
.nav-item:nth-child(6) {
width: 36px;
padding-right: 16px;
/* 为HOT标签留出空间 */
}
/* AI导航项 */
.nav-item:nth-child(7) {
/* width: 50px;
height: 40px; */
background-image: url('/images/ai/ai-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.nav-item:nth-child(8) {
padding: 0;
}
.nav-item.active .nav-item-ai {
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.nav-item-ai:hover {
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.nav-item:hover {
color: #0084CD;
}
.nav-item.active {
color: #0084CD;
font-weight: 400;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
position: relative;
}
.nav-item.active::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 2px;
background-color: #0084CD;
border-radius: 1px;
}
.new-badge {
position: absolute;
top: -10px;
right: -22px;
max-width: 28px;
max-height: 28px;
width: auto;
height: auto;
object-fit: contain;
z-index: 10;
}
/* 搜索区域 */
.search-section {
display: flex;
align-items: center;
margin-right: 40px;
}
.search-box {
border-left: 1px solid #ececec;
border-right: 1px solid #ececec;
display: flex;
align-items: center;
padding: 22px 16px;
width: 280px;
transition: all 0.2s;
}
/* .search-box:hover {
background: #eeeeee;
} */
.search-icon {
max-width: 18px;
max-height: 18px;
width: auto;
height: auto;
margin-right: 8px;
object-fit: contain;
}
.search-input {
flex: 1;
border: none;
background: transparent;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 移动端汉堡菜单按钮 */
.mobile-menu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.2s;
}
.mobile-menu-toggle:hover {
background: rgba(0, 0, 0, 0.05);
}
/* 右侧操作区域 */
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.action-item {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
font-size: 13px;
color: #000;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
}
.action-item:hover {
color: #1890ff;
background: #f0f8ff;
}
/* 语言切换器 */
.language-switcher {
position: relative;
cursor: pointer;
}
.language-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e8e8e8;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 4px;
min-width: 120px;
}
.language-option {
padding: 8px 12px;
font-size: 13px;
color: #666;
cursor: pointer;
transition: all 0.2s;
border-bottom: 1px solid #f5f5f5;
}
.language-option:last-child {
border-bottom: none;
}
.language-option:hover {
background: #f0f8ff;
color: #1890ff;
}
.language-text {
white-space: nowrap;
}
/* 认证按钮 */
.auth-buttons {
display: flex;
align-items: center;
gap: 12px;
}
.auth-combined-btn {
display: flex;
align-items: center;
background: #0088D1;
color: white;
border-radius: 5px;
padding: 8px 10px;
font-size: 12px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
}
.auth-combined-btn:hover {
background: #40a9ff;
}
.auth-login,
.auth-register {
padding: 0 8px;
cursor: pointer;
transition: all 0.2s;
}
.auth-login:hover,
.auth-register:hover {
opacity: 0.8;
}
.auth-divider {
color: rgba(255, 255, 255, 0.6);
margin: 0 4px;
font-weight: 300;
}
/* 用户菜单 */
.user-menu {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.user-info:hover {
background: #f0f8ff;
}
.username {
font-size: 14px;
color: #333;
white-space: nowrap;
}
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1001;
}
/* Logo区域 */
.logo-section {
flex-shrink: 0;
margin-right: 40px;
}
.logo {
display: flex;
align-items: center;
cursor: pointer;
gap: 8px;
}
.logo:hover {
opacity: 0.8;
}
.logo-image {
width: 72px;
height: 61px;
object-fit: contain;
}
.nav-icon {
max-width: 12px;
max-height: 12px;
width: auto;
height: auto;
margin-right: 4px;
object-fit: contain;
}
/* AI图标样式 */
.ai-icon {
max-width: 34px;
max-height: 34px;
margin-right: 0;
}
.action-icon {
max-width: 18px;
max-height: 18px;
width: auto;
height: auto;
object-fit: contain;
}
/* 导航菜单 */
.nav-menu {
display: flex;
align-items: center;
gap: 30px;
flex: 1;
margin-right: 40px;
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
font-size: 14px;
font-weight: 400;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #000;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
position: relative;
height: 24px;
}
/* 两个字的导航项:首页 */
.nav-item:nth-child(1) {
width: 36px;
}
/* 四个字的导航项:热门好课、专题训练、师资力量、精选资源 */
.nav-item:nth-child(2),
.nav-item:nth-child(3),
.nav-item:nth-child(4),
.nav-item:nth-child(5) {
width: 72px;
}
/* 两个字的导航项:活动 */
.nav-item:nth-child(6) {
width: 36px;
padding-right: 16px;
/* 为HOT标签留出空间 */
}
/* AI导航项 */
.nav-item:nth-child(7) {
/* width: 50px;
height: 40px; */
background-image: url('/images/ai/ai-bg.png');
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
.nav-item:nth-child(8) {
padding: 0;
}
.nav-item.active .nav-item-ai {
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.nav-item-ai:hover {
background: linear-gradient(90deg, #0FAAFF, #79DEFF);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.nav-item:hover {
color: #0084CD;
}
.nav-item.active {
color: #0084CD;
font-weight: 400;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
position: relative;
}
.nav-item.active::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 2px;
background-color: #0084CD;
border-radius: 1px;
}
.new-badge {
position: absolute;
top: -10px;
right: -22px;
max-width: 28px;
max-height: 28px;
width: auto;
height: auto;
object-fit: contain;
z-index: 10;
}
/* 搜索区域 */
.search-section {
display: flex;
align-items: center;
margin-right: 40px;
}
.search-box {
/* border-left: 1px solid #ececec;
border-right: 1px solid #ececec; */
display: flex;
align-items: center;
padding: 22px 16px;
width: 280px;
transition: all 0.2s;
}
/* .search-box:hover {
background: #eeeeee;
} */
.search-icon {
max-width: 18px;
max-height: 18px;
width: auto;
height: auto;
margin-right: 8px;
object-fit: contain;
}
.search-input {
flex: 1;
border: none;
background: transparent;
outline: none;
font-size: 14px;
color: #333;
}
.search-input::placeholder {
color: #999;
}
/* 移动端汉堡菜单按钮 */
.mobile-menu-toggle {
display: none;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
cursor: pointer;
border-radius: 6px;
transition: background-color 0.2s;
}
.mobile-menu-toggle:hover {
background: rgba(0, 0, 0, 0.05);
}
/* 右侧操作区域 */
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.action-item {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
font-size: 13px;
color: #000;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
}
.action-item:hover {
color: #1890ff;
background: #f0f8ff;
}
/* 语言切换器 */
.language-switcher {
position: relative;
cursor: pointer;
}
.language-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e8e8e8;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 4px;
min-width: 120px;
}
.language-option {
padding: 8px 12px;
font-size: 13px;
color: #666;
cursor: pointer;
transition: all 0.2s;
border-bottom: 1px solid #f5f5f5;
}
.language-option:last-child {
border-bottom: none;
}
.language-option:hover {
background: #f0f8ff;
color: #1890ff;
}
.language-text {
white-space: nowrap;
}
/* 认证按钮 */
.auth-buttons {
display: flex;
align-items: center;
gap: 12px;
}
.auth-combined-btn {
display: flex;
align-items: center;
background: #0088D1;
color: white;
border-radius: 5px;
padding: 8px 10px;
font-size: 12px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
}
.auth-combined-btn:hover {
background: #40a9ff;
}
.auth-login,
.auth-register {
padding: 0 8px;
cursor: pointer;
transition: all 0.2s;
}
.auth-login:hover,
.auth-register:hover {
opacity: 0.8;
}
.auth-divider {
color: rgba(255, 255, 255, 0.6);
margin: 0 4px;
font-weight: 300;
}
/* 用户菜单 */
.user-menu {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.user-info:hover {
background: #f0f8ff;
}
.username {
font-size: 14px;
color: #333;
white-space: nowrap;
}
/* 美化用户菜单样式 */
:deep(.n-dropdown-menu) {
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid #f0f0f0;
padding: 8px 0;
}
:deep(.n-dropdown-option) {
padding: 12px 16px;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
border-radius: 0;
}
:deep(.n-dropdown-option:hover) {
background: linear-gradient(135deg, #f0f8ff, #e6f4ff);
color: #1890ff;
}
:deep(.n-dropdown-option .n-dropdown-option-icon) {
margin-right: 12px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
.custom-icon {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
/* 分割线样式 */
:deep(.n-dropdown-divider) {
margin: 8px 0;
border-color: #f0f0f0;
}
/* 大屏幕 */
@media (min-width: 1200px) {

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;
}
}
@media (max-width: 480px) {
.header {
height: 52px;
}
.content {
padding-top: 56px;
}
}
@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>
<!-- 讲师信息 -->
@ -2078,7 +2078,7 @@ onMounted(() => {
}
.instructor-info {
text-align: center;
text-align: left;
}
.instructor-name {

View File

@ -33,42 +33,13 @@
<!-- 视频播放器区域 - 已报名状态 -->
<div class="video-player-section">
<div class="video-player enrolled">
<div class="video-container">
<!-- CKPlayer 容器 -->
<div v-if="currentVideoUrl" id="ckplayer_container" class="ckplayer-container">
</div>
<div v-else class="video-placeholder"
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
<div class="placeholder-content">
<div class="play-icon">
<svg width="60" height="60" viewBox="0 0 60 60">
<circle cx="30" cy="30" r="30" fill="rgba(255,255,255,0.9)" />
<path d="M23 18l20 12-20 12V18z" fill="#1890ff" />
</svg>
</div>
<p>请选择要播放的视频课程</p>
</div>
</div>
<!-- 清晰度选择器 -->
<div v-if="videoQualities.length > 1" class="video-quality-selector">
<div class="quality-dropdown">
<button class="quality-btn" @click="showQualityMenu = !showQualityMenu">
{{ currentQuality }}p
<svg width="12" height="12" viewBox="0 0 12 12" class="dropdown-icon">
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
</svg>
</button>
<div v-if="showQualityMenu" class="quality-menu">
<div v-for="quality in videoQualities" :key="quality.value" class="quality-option"
:class="{ active: quality.value === currentQuality }"
@click="changeVideoQuality(quality.value); showQualityMenu = false">
{{ quality.label }}
</div>
</div>
</div>
</div>
</div>
<!-- DPlayer 视频播放器 -->
<DPlayerVideo ref="videoPlayerRef" :video-url="currentVideoUrl"
:placeholder-image="course?.coverImage || course?.thumbnail"
:placeholder-text="currentVideoUrl ? '' : '请选择要播放的视频课程'" :title="currentSection?.name || '课程视频'"
:video-qualities="videoQualities" :current-quality="currentQuality" @play="handleVideoPlay"
@pause="handleVideoPause" @ended="handleVideoEnded" @error="handleVideoError"
@quality-change="handleQualityChange" />
<!-- 底部交互区域 -->
<div class="video-interaction-bar">
@ -136,9 +107,9 @@
<!-- 课程描述 -->
<div class="course-description">
<p>{{ course.description ||
'本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、"1+X"WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员、在校学生、创行教师提供服务与学习支持。'
}}</p>
<p
v-html="course.description || '本课程深度聚焦问题让每一位教师了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。课程内容与全国计算机等级考试、&quot;1+X&quot;WPS办公应用职业技能等级证书技能大赛紧密结合课程设置紧密对应实际全面共享可为职业工作人员、在校学生、创行教师提供服务与学习支持。'">
</p>
</div>
<!-- 讲师信息 -->
@ -444,7 +415,7 @@
<div class="lesson-actions">
<!-- 视频播放图标 - 可点击 -->
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
@click.stop="handleVideoPlay(section)">
@click.stop="handleVideoPlaySection(section)">
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
</button>
<!-- 下载图标 - 可点击 -->
@ -560,16 +531,10 @@ import type { Course, CourseSection, SectionVideo, VideoQuality } from '@/api/ty
import SafeAvatar from '@/components/common/SafeAvatar.vue'
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
import NotesModal from '@/components/common/NotesModal.vue'
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
// CKPlayer
declare global {
interface Window {
ckplayer: any
loadedHandler: () => void
endedHandler: () => void
errorHandler: (error: any) => void
}
}
// DPlayer
const videoPlayerRef = ref<InstanceType<typeof DPlayerVideo>>()
const route = useRoute()
const router = useRouter()
@ -582,7 +547,6 @@ const FORCE_LOCAL_VIDEO = true
//
const currentSection = ref<CourseSection | null>(null)
const currentVideoUrl = ref<string>('')
const ckplayer = ref<any>(null)
//
const currentVideo = ref<SectionVideo | null>(null)
@ -611,6 +575,39 @@ const getVideoUrl = (section?: CourseSection) => {
// return VIDEO_CONFIG.HLS
}
// DPlayer
const handleVideoPlay = () => {
console.log('视频开始播放')
}
const handleVideoPause = () => {
console.log('视频暂停')
}
const handleVideoEnded = () => {
console.log('视频播放结束')
//
if (currentSection.value && !currentSection.value.completed) {
currentSection.value.completed = true
const completed = courseSections.value.filter((s: any) => s.completed).length
completedLessons.value = completed
progress.value = Math.round((completed / courseSections.value.length) * 100)
}
}
const handleVideoError = (error: any) => {
console.error('视频播放错误:', error)
//
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
}
}
const handleQualityChange = (quality: string) => {
currentQuality.value = quality
console.log('清晰度切换到:', quality)
}
//
const course = ref<Course | null>(null)
const loading = ref(false)
@ -922,8 +919,6 @@ const loadCourseSections = async () => {
if (firstVideo) {
currentSection.value = firstVideo
currentVideoUrl.value = getVideoUrl(firstVideo)
await nextTick()
initCKPlayer(currentVideoUrl.value)
}
}
} else {
@ -966,7 +961,6 @@ const loadMockData = () => {
if (firstVideo) {
currentSection.value = firstVideo
currentVideoUrl.value = getVideoUrl(firstVideo)
setTimeout(() => initCKPlayer(currentVideoUrl.value), 0)
}
}
}
@ -1038,39 +1032,14 @@ const updateVideoPlayer = async () => {
}
try {
console.log('🔍 更新播放器视频源:', currentVideoUrl.value)
console.log('🔍 更新 DPlayer 视频源:', currentVideoUrl.value)
if (ckplayer.value) {
// CKPlayer API
if (typeof ckplayer.value.newVideo === 'function') {
console.log('✅ 使用newVideo方法更新视频源')
ckplayer.value.newVideo(currentVideoUrl.value)
} else if (typeof ckplayer.value.changeVideo === 'function') {
console.log('✅ 使用changeVideo方法更新视频源')
ckplayer.value.changeVideo(currentVideoUrl.value)
} else if (typeof ckplayer.value.videoSrc === 'function') {
console.log('✅ 使用videoSrc方法更新视频源')
ckplayer.value.videoSrc(currentVideoUrl.value)
} else {
console.log('⚠️ 未找到合适的更新方法,重新初始化播放器')
//
await nextTick()
initCKPlayer(currentVideoUrl.value)
}
} else {
console.log('🔍 播放器未初始化,开始初始化')
await nextTick()
initCKPlayer(currentVideoUrl.value)
if (videoPlayerRef.value) {
// 使 DPlayer initializePlayer
await videoPlayerRef.value.initializePlayer(currentVideoUrl.value)
}
} catch (error) {
console.error('❌ 更新播放器失败:', error)
//
try {
await nextTick()
initCKPlayer(currentVideoUrl.value)
} catch (initError) {
console.error('❌ 重新初始化播放器也失败:', initError)
}
}
}
@ -1161,10 +1130,10 @@ const handleSectionClick = (section: CourseSection) => {
type: section.type
})
//
//
if (isVideo) {
console.log('✅ 识别为视频课程,开始加载视频数据')
loadSectionVideo(section)
console.log('✅ 识别为视频课程,开始播放视频')
handleVideoPlaySection(section)
} else if (isResource) {
console.log('✅ 识别为资料课程')
handleDownload(section)
@ -1176,12 +1145,12 @@ const handleSectionClick = (section: CourseSection) => {
handleExam(section)
} else {
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
loadSectionVideo(section)
handleVideoPlaySection(section)
}
}
// -
const handleVideoPlay = async (section: CourseSection) => {
const handleVideoPlaySection = async (section: CourseSection) => {
console.log('播放视频:', section.name)
// URL
@ -1191,11 +1160,11 @@ const handleVideoPlay = async (section: CourseSection) => {
console.log('使用视频源:', videoUrl)
// DOM
// DOM
await nextTick()
// CKPlayer
initCKPlayer(videoUrl)
if (videoPlayerRef.value) {
await videoPlayerRef.value.initializePlayer(videoUrl)
}
//
if (!section.completed) {
@ -1207,95 +1176,7 @@ const handleVideoPlay = async (section: CourseSection) => {
}
}
// CKPlayer
const initCKPlayer = (url: string) => {
//
if (ckplayer.value) {
try {
ckplayer.value.remove()
} catch (e) {
console.log('清理播放器实例时出错:', e)
}
ckplayer.value = null
}
// CKPlayer
if (typeof window.ckplayer === 'undefined') {
console.error('CKPlayer not loaded')
return
}
// ""
const containerEl = document.querySelector('#ckplayer_container') as HTMLElement | null
if (!containerEl) {
console.warn('Player container not found, retrying init...')
setTimeout(() => initCKPlayer(url), 50)
return
}
//
const isMP4 = url.endsWith('.mp4')
const isHLS = url.endsWith('.m3u8')
// CKPlayer
const videoObject = {
container: '#ckplayer_container', // ID
autoplay: false, //
video: url, //
volume: 0.8, //
poster: course.value?.coverImage || course.value?.thumbnail || '', //
live: false, //
//
plug: isHLS ? 'hls.js' : '', // HLS使hls.jsMP4
playbackrateOpen: true, //
playbackrateList: [0.5, 0.75, 1, 1.25, 1.5, 2], //
seek: 0, //
loaded: 'loadedHandler', //
ended: 'endedHandler', //
error: 'errorHandler', //
title: currentSection.value?.name || '课程视频', //
controls: true, //
webFull: true, //
screenshot: true, //
timeScheduleAdjust: 1, //
// MP4
...(isMP4 && {
type: 'mp4', //
crossOrigin: 'anonymous' //
})
}
try {
//
ckplayer.value = new window.ckplayer(videoObject)
console.log('CKPlayer initialized successfully for:', isMP4 ? 'MP4' : 'HLS')
} catch (error) {
console.error('Failed to initialize CKPlayer:', error)
}
}
// CKPlayer
window.loadedHandler = () => {
console.log('CKPlayer loaded successfully')
}
window.endedHandler = () => {
console.log('Video playback ended')
}
window.errorHandler = (error: any) => {
console.error('CKPlayer error:', error)
// 退
if (currentVideoUrl.value !== VIDEO_CONFIG.LOCAL) {
try {
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
//
initCKPlayer(VIDEO_CONFIG.LOCAL)
} catch (e) {
console.error('Fallback to local video failed:', e)
}
}
}
//
const handleDownload = (section: CourseSection) => {
@ -1399,26 +1280,19 @@ const saveNote = (content: string) => {
onMounted(async () => {
console.log('已报名课程详情页加载完成课程ID:', courseId.value)
initializeEnrolledState() //
//
//
if (FORCE_LOCAL_VIDEO) {
currentSection.value = null
currentVideoUrl.value = VIDEO_CONFIG.LOCAL
await nextTick()
initCKPlayer(currentVideoUrl.value)
}
loadCourseDetail()
loadCourseSections()
})
// CKPlayer
//
onUnmounted(() => {
if (ckplayer.value) {
try {
ckplayer.value.remove()
} catch (e) {
console.log('清理播放器实例时出错:', e)
}
ckplayer.value = null
if (videoPlayerRef.value) {
videoPlayerRef.value.destroy()
}
})
</script>
@ -1496,127 +1370,7 @@ onUnmounted(() => {
object-fit: cover;
}
.ckplayer-container {
width: 100%;
height: 100%;
background: #000;
}
.video-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
/* 背景图片设置 */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
/* 如果没有背景图片,使用默认渐变背景 */
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
position: relative;
}
.video-placeholder::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.placeholder-content {
text-align: center;
position: relative;
z-index: 2;
}
.play-icon {
margin-bottom: 16px;
cursor: pointer;
transition: transform 0.3s;
}
.play-icon:hover {
transform: scale(1.1);
}
.placeholder-content p {
margin: 0;
font-size: 16px;
opacity: 0.9;
}
/* 清晰度选择器 */
.video-quality-selector {
position: absolute;
top: 15px;
right: 15px;
z-index: 10;
}
.quality-dropdown {
position: relative;
}
.quality-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.quality-btn:hover {
background: rgba(0, 0, 0, 0.8);
}
.dropdown-icon {
transition: transform 0.2s;
}
.quality-btn:hover .dropdown-icon {
transform: rotate(180deg);
}
.quality-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: rgba(0, 0, 0, 0.9);
border-radius: 4px;
overflow: hidden;
min-width: 80px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.quality-option {
padding: 8px 12px;
color: white;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
}
.quality-option:hover {
background: rgba(255, 255, 255, 0.1);
}
.quality-option.active {
background: #1890ff;
color: white;
}
/* 课程信息区域 */
.course-info-section {

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()
}
@ -397,6 +393,13 @@ const goToCourseDetail = (course: Course) => {
})
}
//
const selectSort = (sortType: string) => {
selectedSort.value = sortType
currentPage.value = 1 //
loadCourses() //
}
//
const loadCategories = async () => {
try {
@ -473,6 +476,12 @@ onMounted(() => {
</script>
<style scoped>
@font-face {
font-family: 'AlimamaShuHeiTiBold';
src: url('/fonts/AlimamaShuHeiTiBold.ttf') format('truetype');
}
.courses-page {
min-height: 100vh;
background: #fff;
@ -510,6 +519,8 @@ onMounted(() => {
}
.page-title {
/* 数黑体 */
font-family: 'AlimamaShuHeiTiBold';
font-size: 28px;
margin: 35px 0 5px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

357
src/views/DPlayerDemo.vue Normal file
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

@ -89,9 +89,9 @@
<h3 class="course-title">{{ course.title }}</h3>
<div class="course-meta">
<span class="course-students">{{ course.studentsCount }}{{ t('home.popularCourses.studentsEnrolled')
}}</span>
}}</span>
<button class="enroll-btn" @click="handleEnrollCourse(course.id)">{{ t('home.popularCourses.enroll')
}}</button>
}}</button>
</div>
</div>
</div>
@ -210,7 +210,7 @@
<span class="section-title-text">精选活动</span>
<div class="section-subtitle">SELECTED EVENTS</div>
</div>
<span class="view-all-btn">查看全部 > </span>
<span class="view-all-btn">查看更多 > </span>
</div>
<div class="activities-grid">
<div class="activity-left">
@ -230,11 +230,11 @@
<img src="/images/activity/activity1.png" alt="">
</div>
</div>
<div class="activity-right">
<!-- <div class="activity-right">
<img src="/images/activity/activity2.png" alt="">
<h3 class="activity-title-img">算法挑战</h3>
<img src="/images/activity/right.png" class="activity-right-img" alt="">
</div>
</div> -->
</div>
</div>
</section>
@ -247,6 +247,7 @@
<span class="section-title-text">AI智能体验</span>
<div class="section-subtitle">FEATURED REVIEWS</div>
</div>
<div class="view-all-btn">查看更多 > </div>
</div>
<div class="ai-cards-grid">
<div class="ai-card" v-for="aiCard in aiCards" :key="aiCard.id">
@ -657,32 +658,32 @@ const partners = computed(() => [
{
id: 1,
name: '云南师范大学',
logo: '/logo/logo4.png'
logo: '/logo/云师大.jpg'
},
{
id: 2,
name: '曲靖师范学院',
logo: '/logo/logo4.png'
logo: '/logo/曲靖师范.jpg'
},
{
id: 3,
name: '德宏师范学院',
logo: '/logo/logo4.png'
logo: '/logo/德宏师范.jpg'
},
{
id: 4,
name: '云南师范大学',
logo: '/logo/logo4.png'
logo: '/logo/云师大.jpg'
},
{
id: 5,
name: '曲靖师范学院',
logo: '/logo/logo4.png'
logo: '/logo/曲靖师范.jpg'
},
{
id: 6,
name: '德宏师范学院',
logo: '/logo/logo4.png'
logo: '/logo/德宏师范.jpg'
}
])
@ -781,6 +782,12 @@ onMounted(async () => {
font-display: swap;
}
@font-face {
font-family: 'AlibabaPuHuiTiBold';
src: url('@/assets/fonts/Alibaba_PuHuiTi_2.0_55_Regular_85_Bold.ttf') format('truetype');
font-display: swap;
}
.home {
@ -881,16 +888,14 @@ onMounted(async () => {
}
.logo-circle {
width: 100px;
height: 100px;
width: 148px;
height: 148px;
border-radius: 50%;
background: white;
display: flex;
justify-content: center;
align-items: center;
padding: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-right: 15px;
}
.logo-circle img {
@ -904,6 +909,7 @@ onMounted(async () => {
color: #292C2E;
font-weight: 500;
text-align: center;
margin-right: 25px;
}
/* 响应式调整 */
@ -1159,8 +1165,9 @@ onMounted(async () => {
color: #3BA4EB;
top: 48%;
transform: translateY(-50%);
left: 24%;
font-size: 22px;
left: 20%;
font-size: 26px;
font-family: 'AlibabaPuHuiTiBold';
}
.ad-container-title1 {
@ -2101,7 +2108,7 @@ onMounted(async () => {
.activity-left {
padding-left: 30px;
width: 1182px;
width: 100%;
height: 404px;
background: white;
border-radius: 5px;

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

@ -14,7 +14,7 @@
<!-- 用户头像和姓名 -->
<SafeAvatar class="image_7" :src="userStore.user?.avatar" :name="userStore.user?.username || '用户'" :size="96"
alt="用户头像" />
<span class="text_72">{{ userStore.user?.username || '用户名' }}</span>
<span class="text_72">{{ userStore.user?.nickname || userStore.user?.username || '用户名' }}</span>
<!-- 菜单项容器 -->
<div class="box_22">
@ -22,72 +22,81 @@
<div class="menu-divider"></div>
<!-- 我的课程 -->
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')">
<img class="image_8" referrerpolicy="no-referrer" :src="activeTab === 'courses'
<div :class="['image-text_19', { active: activeTab === 'courses' }]" @click="handleMenuSelect('courses')"
@mouseenter="hoveredTab = 'courses'" @mouseleave="hoveredTab = null">
<img class="image_8" referrerpolicy="no-referrer" :src="(activeTab === 'courses' || hoveredTab === 'courses')
? '/images/profile/course-active.png'
: '/images/profile/course.png'" />
<span class="text-group_19">我的课程</span>
</div>
<!-- 我的作业 -->
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')">
<img class="label_4" referrerpolicy="no-referrer" :src="activeTab === 'homework'
<div :class="['image-text_20', { active: activeTab === 'homework' }]" @click="handleMenuSelect('homework')"
@mouseenter="hoveredTab = 'homework'" @mouseleave="hoveredTab = null">
<img class="label_4" referrerpolicy="no-referrer" :src="(activeTab === 'homework' || hoveredTab === 'homework')
? '/images/profile/grade-active.png'
: '/images/profile/grade.png'" />
<span class="text-group_20">我的作业</span>
</div>
<!-- 我的考试 -->
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')">
<img class="label_5" referrerpolicy="no-referrer" :src="activeTab === 'exam'
<div :class="['image-text_21', { active: activeTab === 'exam' }]" @click="handleMenuSelect('exam')"
@mouseenter="hoveredTab = 'exam'" @mouseleave="hoveredTab = null">
<img class="label_5" referrerpolicy="no-referrer" :src="(activeTab === 'exam' || hoveredTab === 'exam')
? '/images/profile/checklist-active.png'
: '/images/profile/checklist.png'" />
<span class="text-group_21">我的考试</span>
</div>
<!-- 我的练习 -->
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')">
<img class="label_6" referrerpolicy="no-referrer" :src="activeTab === 'practice'
<div :class="['image-text_22', { active: activeTab === 'practice' }]" @click="handleMenuSelect('practice')"
@mouseenter="hoveredTab = 'practice'" @mouseleave="hoveredTab = null">
<img class="label_6" referrerpolicy="no-referrer" :src="(activeTab === 'practice' || hoveredTab === 'practice')
? '/images/profile/bookmark-active.png'
: '/images/profile/bookmark.png'" />
<span class="text-group_22">我的练习</span>
</div>
<!-- 我的活动 -->
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')">
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="activeTab === 'activity'
<div :class="['image-text_23', { active: activeTab === 'activity' }]" @click="handleMenuSelect('activity')"
@mouseenter="hoveredTab = 'activity'" @mouseleave="hoveredTab = null">
<img class="thumbnail_40" referrerpolicy="no-referrer" :src="(activeTab === 'activity' || hoveredTab === 'activity')
? '/images/profile/gift-active.png'
: '/images/profile/gift.png'" />
<span class="text-group_23">我的活动</span>
</div>
<!-- 我的关注 -->
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')">
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="activeTab === 'follows'
<div :class="['image-text_27', { active: activeTab === 'follows' }]" @click="handleMenuSelect('follows')"
@mouseenter="hoveredTab = 'follows'" @mouseleave="hoveredTab = null">
<img class="thumbnail_42" referrerpolicy="no-referrer" :src="(activeTab === 'follows' || hoveredTab === 'follows')
? '/images/profile/concern-active.png'
: '/images/profile/concern.png'" />
<span class="text-group_27">我的关注</span>
</div>
<!-- 我的消息 -->
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')">
<img class="label_7" referrerpolicy="no-referrer" :src="activeTab === 'message'
<div :class="['image-text_24', { active: activeTab === 'message' }]" @click="handleMenuSelect('message')"
@mouseenter="hoveredTab = 'message'" @mouseleave="hoveredTab = null">
<img class="label_7" referrerpolicy="no-referrer" :src="(activeTab === 'message' || hoveredTab === 'message')
? '/images/profile/message-active.png'
: '/images/profile/message.png'" />
<span class="text-group_24">我的消息</span>
</div>
<!-- 我的资料 -->
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')">
<img class="image_9" referrerpolicy="no-referrer" :src="activeTab === 'materials'
<div :class="['image-text_25', { active: activeTab === 'materials' }]" @click="handleMenuSelect('materials')"
@mouseenter="hoveredTab = 'materials'" @mouseleave="hoveredTab = null">
<img class="image_9" referrerpolicy="no-referrer" :src="(activeTab === 'materials' || hoveredTab === 'materials')
? '/images/profile/profile-active.png'
: '/images/profile/profile.png'" />
<span class="text-group_25">我的资料</span>
</div>
<!-- 我的下载 -->
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')">
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="activeTab === 'download'
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')"
@mouseenter="hoveredTab = 'download'" @mouseleave="hoveredTab = null">
<img class="thumbnail_41" referrerpolicy="no-referrer" :src="(activeTab === 'download' || hoveredTab === 'download')
? '/images/profile/download-active.png'
: '/images/profile/download.png'" />
<span class="text-group_26">我的下载</span>
@ -238,7 +247,7 @@
fontSize: '14px'
}">
{{ detailAssignment.status === '未完成' || detailAssignment.status === '待提交' ? '未完成' :
(detailAssignment.status === '已完成' ? '已完成' : '541人已完成') }}
(detailAssignment.status === '已完成' ? '已完成' : '541人已完成') }}
</span>
</span>
</div>
@ -361,7 +370,7 @@
<span class="text_36">上传作业</span>
</div>
<div class="text-wrapper_8 anew-button" @click="reEditDraft">
<span class="text_36">重新编辑</span>
<span class="">重新编辑</span>
</div>
</div>
</div>
@ -667,7 +676,7 @@
<div class="activity-status-left">
<span :class="['activity-status-text', activity.status]">{{ activity.status === 'ongoing' ? '进行中' :
'已结束'
}}</span>
}}</span>
</div>
</div>
</div>
@ -957,8 +966,7 @@
<!-- 面包屑导航或筛选和操作区域 -->
<div v-if="isInSubDirectory" class="breadcrumb-controls">
<div class="breadcrumb-nav">
<span class="breadcrumb-text" @click="goBack">课件&gt;图片&gt;</span>
<span class="breadcrumb-current">风景图片</span>
<span class="breadcrumb-text" @click="goBack">{{ currentPath.join(' > ') }}</span>
</div>
</div>
@ -978,9 +986,7 @@
<div class="search-input-container">
<input v-model="downloadFilter.keyword" type="text" class="search-input" placeholder="请输入文件名称" />
<button class="search-btn">
<img
src="https://lanhu-oss-2537-2.lanhuapp.com/SketchPng870a86da8af58a60f35fcb27ef4822e645d2ad5aaabe6416e4179342a53a5a60"
alt="搜索图标" class="search-icon" />
<img src="/images/profile/search.png" alt="搜索图标" class="search-icon" />
</button>
</div>
</div>
@ -1244,6 +1250,7 @@ const userStore = useUserStore()
type TabType = 'courses' | 'homework' | 'exam' | 'practice' | 'activity' | 'follows' | 'message' | 'materials' | 'download'
const activeTab = ref<TabType>('courses')
const hoveredTab = ref<TabType | null>(null)
const activeCourseTab = ref('all')
//
@ -2486,6 +2493,11 @@ const handleDownloadTabChange = (tab: string) => {
//
const filteredDownloadFiles = computed(() => {
//
if (isInSubDirectory.value) {
return []
}
let files = downloadFiles.filter(file => file.category === activeDownloadTab.value)
if (downloadFilter.type !== 'all') {
@ -2507,10 +2519,14 @@ const toggleFileMenu = (fileId: number) => {
}
const handleFileClick = (file: any) => {
if (file.type === 'folder' && file.name === '图片') {
//
if (file.type === 'folder') {
//
isInSubDirectory.value = true
currentPath.value = ['课件', '图片']
currentPath.value = ['课件', file.name]
message.info(`进入文件夹:${file.name}`)
} else {
//
message.info(`打开文件:${file.name}`)
}
}
@ -2536,12 +2552,9 @@ const getFileIcon = (fileId?: number) => {
]
const index = (fileId || 0) % homeworkImages.length
return homeworkImages[index]
} else if (isInSubDirectory.value) {
// 使
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPngf45333052202c303acc2c06223c26b820d330459ce2d452a21a3132fbbeab442'
} else {
//
return 'https://lanhu-oss-2537-2.lanhuapp.com/SketchPng5548891b00234027dbe6dadafbd83596d616261421c0587a85652dc194b2d5ef'
return '/images/profile/folder.png'
}
}
@ -2930,9 +2943,8 @@ onActivated(() => {
/* 去掉背景色 */
border-radius: 0.6vw;
/* 12px转换为vw */
margin: 2.55vh 0;
margin: 2.55vh 0 0 0;
/* 去掉左右边距,因为父容器已经居中 */
padding: 1.04vh 0;
/* 20px 0转换 */
display: flex;
flex-direction: column;
@ -2967,7 +2979,7 @@ onActivated(() => {
/* 自适应高度 */
min-height: 3vh;
/* 设置最小高度,让盒子更大 */
margin: 1.5vh;
margin: 1.5vh 0 0 0;
/* 减小间距从2.34vh减少到1.5vh */
display: flex;
align-items: center;
@ -4186,9 +4198,9 @@ onActivated(() => {
}
.course-name {
margin-top: 10px;
margin-left: 83px;
color: #999999;
/* margin-top: 10px; */
margin-left: 5px;
color: #497087;
}
.course-name span {
@ -5744,7 +5756,7 @@ onActivated(() => {
/* 0 0 16px 0转换 */
line-height: 1.4;
padding: 0 1.04vw;
/* 添加左右内边距 */
font-weight: 700;
}
.activity-details {
@ -6172,7 +6184,7 @@ onActivated(() => {
/* height: 5.21vh; */
padding: 0.52vh 0.57vw;
/* 100px转换为vh进一步增加高度 */
background: url('https://lanhu-oss-2537-2.lanhuapp.com/SketchPng9491a7fe5bdac8e8a88de63907163bd6b8a259824f56a3c76784ba6cdc7bc32b') 100% no-repeat;
background: #F5F8FB;
background-size: 100% 100%;
margin-top: 0.26vh;
/* 5px转换为vh */
@ -7058,6 +7070,7 @@ onActivated(() => {
box-sizing: border-box;
display: flex;
align-items: center;
padding-left: 15px;
}
.password-form-input {
@ -7609,7 +7622,7 @@ onActivated(() => {
/* 4px转换为vw减小图标和文字间距 */
padding: 0.52vh 0.73vw;
/* 10px 14px转换 */
font-size: 10px;
font-size: 12px;
/* 14px转换为vw */
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
color: #000;
@ -8066,7 +8079,7 @@ onActivated(() => {
width: 80px;
height: 23px;
border: none;
font-size: 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;

View File

@ -201,7 +201,200 @@ const loadCourses = async () => {
await new Promise(resolve => setTimeout(resolve, 500))
//
let filteredCourses: Course[] = [] // 使API
let filteredCourses: Course[] = [
{
id: '1',
title: '教育心理学基础课程',
description: '本课程深入讲解教育心理学的基本理论和实践应用,帮助学生理解学习过程中的心理机制。',
thumbnail: '/images/courses/course1.png',
price: 0,
currency: 'CNY',
rating: 4.8,
ratingCount: 125,
studentsCount: 1250,
duration: '12小时43分钟',
totalLessons: 54,
level: 'beginner',
language: 'zh-CN',
category: { id: 1, name: '教育培训', slug: 'education-training' },
tags: ['心理学', '教育', '基础'],
skills: ['心理分析', '教育理论'],
requirements: ['无特殊要求'],
objectives: ['掌握教育心理学基础理论'],
instructor: {
id: 1,
name: '汪波',
avatar: '/images/Teachers/师资力量1.png',
title: '云南师范大学教授',
bio: '资深教育心理学专家',
rating: 4.8,
studentsCount: 1250,
coursesCount: 6,
experience: '15年',
education: ['云南师范大学', '教育心理学博士'],
certifications: ['高级心理咨询师', '教育技术专家']
},
status: 'published',
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z'
},
{
id: '2',
title: '现代教育技术应用',
description: '探索现代教育技术在课堂教学中的应用,包括多媒体教学、在线教育平台等。',
thumbnail: '/images/courses/course2.png',
price: 0,
currency: 'CNY',
rating: 4.6,
ratingCount: 89,
studentsCount: 890,
duration: '10小时20分钟',
totalLessons: 42,
level: 'intermediate',
language: 'zh-CN',
category: { id: 2, name: '技术应用', slug: 'tech-application' },
tags: ['教育技术', '多媒体', '在线教育'],
skills: ['多媒体制作', '在线教学'],
requirements: ['基础计算机操作'],
objectives: ['掌握现代教育技术应用'],
instructor: {
id: 1,
name: '汪波',
avatar: '/images/Teachers/师资力量1.png',
title: '云南师范大学教授',
bio: '资深教育技术专家',
rating: 4.6,
studentsCount: 890,
coursesCount: 6,
experience: '15年',
education: ['云南师范大学', '教育技术博士'],
certifications: ['高级教育技术专家', '多媒体制作师']
},
status: 'published',
createdAt: '2024-02-20T14:30:00Z',
updatedAt: '2024-02-20T14:30:00Z'
},
{
id: '3',
title: '课程设计与开发',
description: '学习如何设计和开发高质量的课程内容,包括教学目标制定、教学内容组织等。',
thumbnail: '/images/courses/course3.png',
price: 0,
currency: 'CNY',
rating: 4.7,
ratingCount: 67,
studentsCount: 567,
duration: '15小时30分钟',
totalLessons: 68,
level: 'advanced',
language: 'zh-CN',
category: { id: 3, name: '课程设计', slug: 'course-design' },
tags: ['课程设计', '教学开发', '教育'],
skills: ['课程规划', '教学设计'],
requirements: ['教育理论基础'],
objectives: ['掌握课程设计方法'],
instructor: {
id: 1,
name: '汪波',
avatar: '/images/Teachers/师资力量1.png',
title: '云南师范大学教授',
bio: '课程设计专家'
},
status: 'published',
createdAt: '2024-03-10T09:15:00Z',
updatedAt: '2024-03-10T09:15:00Z'
},
{
id: '4',
title: '教育研究方法论',
description: '系统介绍教育研究的基本方法和技巧,培养学生进行教育研究的能力。',
thumbnail: '/images/courses/course4.png',
price: 0,
currency: 'CNY',
rating: 4.5,
ratingCount: 43,
studentsCount: 432,
duration: '8小时15分钟',
totalLessons: 36,
level: 'intermediate',
language: 'zh-CN',
category: { id: 4, name: '研究方法', slug: 'research-methods' },
tags: ['研究方法', '教育', '学术'],
skills: ['研究设计', '数据分析'],
requirements: ['统计学基础'],
objectives: ['掌握教育研究方法'],
instructor: {
id: 1,
name: '汪波',
avatar: '/images/Teachers/师资力量1.png',
title: '云南师范大学教授',
bio: '教育研究专家'
},
status: 'published',
createdAt: '2024-01-25T16:45:00Z',
updatedAt: '2024-01-25T16:45:00Z'
},
{
id: '5',
title: '学生心理辅导技巧',
description: '学习如何对学生进行心理辅导,掌握基本的心理咨询和辅导技巧。',
thumbnail: '/images/courses/course5.png',
price: 0,
currency: 'CNY',
rating: 4.9,
ratingCount: 78,
studentsCount: 678,
duration: '6小时45分钟',
totalLessons: 28,
level: 'beginner',
language: 'zh-CN',
category: { id: 5, name: '心理辅导', slug: 'psychological-counseling' },
tags: ['心理辅导', '学生', '技巧'],
skills: ['心理咨询', '沟通技巧'],
requirements: ['心理学基础'],
objectives: ['掌握心理辅导技巧'],
instructor: {
id: 1,
name: '汪波',
avatar: '/images/Teachers/师资力量1.png',
title: '云南师范大学教授',
bio: '心理咨询专家'
},
status: 'published',
createdAt: '2024-02-05T11:20:00Z',
updatedAt: '2024-02-05T11:20:00Z'
},
{
id: '6',
title: '教育评估与测量',
description: '学习教育评估的基本理论和方法,掌握教育测量的技术和工具。',
thumbnail: '/images/courses/course5.png',
price: 0,
currency: 'CNY',
rating: 4.4,
ratingCount: 34,
studentsCount: 345,
duration: '9小时20分钟',
totalLessons: 40,
level: 'advanced',
language: 'zh-CN',
category: { id: 6, name: '教育评估', slug: 'education-assessment' },
tags: ['教育评估', '测量', '技术'],
skills: ['评估设计', '测量技术'],
requirements: ['教育统计学'],
objectives: ['掌握教育评估方法'],
instructor: {
id: 1,
name: '汪波',
avatar: '/images/Teachers/师资力量1.png',
title: '云南师范大学教授',
bio: '教育评估专家'
},
status: 'published',
createdAt: '2024-03-15T13:10:00Z',
updatedAt: '2024-03-15T13:10:00Z'
}
] // 使API
//
if (selectedSubject.value !== '全部') {
@ -502,7 +695,7 @@ onMounted(() => {
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
min-height: 350px;
/* min-height: 350px; */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}