fix:修复章节预览弹窗部分报错;修改评论页面布局;隐藏资源菜单

This commit is contained in:
yuk255 2025-09-25 10:27:51 +08:00
parent 82ae528785
commit 03f9d1b423
10 changed files with 203 additions and 445 deletions

View File

@ -7,7 +7,7 @@
<div v-else class="document-content">
<!-- 文档信息 -->
<div class="document-info">
<h4>{{ detailData.name || '文档标题' }}</h4>
<h4>{{ detailData.name}}.{{ detailData.fileUrl.split('.').pop() }}</h4>
<div class="document-meta">
<n-space>
<n-tag type="success" size="small">
@ -65,7 +65,7 @@
</n-icon>
</div>
<div class="document-type-info">
<h4>{{ detailData.name }}</h4>
<h4>{{ detailData.name }}.{{ detailData.fileUrl.split('.').pop() }}</h4>
<p class="file-description">{{ detailData.description }}</p>
</div>
</div>
@ -73,7 +73,7 @@
<!-- 操作按钮 -->
<div class="document-operations">
<n-space>
<n-button type="primary" size="large" @click="downloadDocument">
<n-button type="primary" @click="downloadDocument">
<template #icon>
<n-icon><DownloadOutline /></n-icon>
</template>

View File

@ -59,8 +59,7 @@
<!-- 作业描述 -->
<div class="homework-description">
<h5>作业描述</h5>
<div class="description-content">
{{ detailData.description || '暂无作业要求说明' }}
<div class="description-content" v-html="detailData.description">
</div>
</div>

View File

@ -12,7 +12,9 @@
<n-space>
<n-tag type="info" size="small">
<template #icon>
<n-icon><PlayCircleOutline /></n-icon>
<n-icon>
<PlayCircleOutline />
</n-icon>
</template>
视频
</n-tag>
@ -40,24 +42,6 @@
<template #description>视频加载中...</template>
</n-spin>
</div>
<!-- 画质选择器仅在需要手动切换时显示 -->
<div v-if="videoQualities.length > 1 && !dplayer?.switchQuality" class="quality-selector">
<n-space>
<span class="quality-label">画质:</span>
<n-button-group>
<n-button
v-for="(quality, index) in videoQualities"
:key="index"
:type="currentQualityIndex === index ? 'primary' : 'default'"
size="small"
@click="switchQuality(index)"
>
{{ quality.name }}
</n-button>
</n-button-group>
</n-space>
</div>
</div>
<div v-else class="no-video">
@ -99,11 +83,9 @@ import {
NIcon,
NSpin,
NResult,
NButton,
NDivider,
NDescriptions,
NDescriptionsItem,
NButtonGroup,
useMessage
} from 'naive-ui'
import { PlayCircleOutline } from '@vicons/ionicons5'
@ -178,10 +160,10 @@ const getQualityNameFromUrl = (url: string, index: number): string => {
}
// -
const getQualityName = (index: number) => {
const quality = videoQualities.value[index]
return quality?.name || `画质${index + 1}`
}
// const getQualityName = (index: number) => {
// const quality = videoQualities.value[index]
// return quality?.name || `${index + 1}`
// }
//
const currentVideoSource = computed(() => {
@ -219,6 +201,7 @@ const initDPlayer = async () => {
defaultQuality: currentQualityIndex.value
} : {}
// DPlayer
dplayer = new DPlayer({
container: dplayerContainer.value,
@ -255,25 +238,8 @@ const initDPlayer = async () => {
airplay: false,
chromecast: false
})
videoLoading.value = false
//
dplayer.on('loadstart', () => {
videoLoading.value = true
})
dplayer.on('loadedmetadata', () => {
videoLoading.value = false
})
dplayer.on('canplay', () => {
videoLoading.value = false
})
dplayer.on('error', (info?: any) => {
videoLoading.value = false
console.error('DPlayer播放错误:', info)
emit('error', '视频播放失败,请检查视频链接是否有效')
})
} catch (error) {
videoLoading.value = false
@ -282,60 +248,6 @@ const initDPlayer = async () => {
}
}
//
const switchQuality = async (index: number) => {
if (index === currentQualityIndex.value || !dplayer || videoQualities.value.length <= 1) return
try {
// DPlayer使switchQuality
if (typeof dplayer.switchQuality === 'function') {
dplayer.switchQuality(index)
return
}
//
const currentTime = dplayer.video.currentTime
const isPaused = dplayer.video.paused
const currentVolume = dplayer.volume()
currentQualityIndex.value = index
//
await initDPlayer()
//
if (dplayer && dplayer.video) {
const waitForLoad = () => {
return new Promise<void>((resolve) => {
const checkLoad = () => {
if (dplayer && dplayer.video.readyState >= 2) {
try {
dplayer.video.currentTime = currentTime
dplayer.volume(currentVolume)
if (!isPaused) {
dplayer.play().catch(console.error)
}
} catch (error) {
console.warn('恢复播放状态时出错:', error)
}
resolve()
} else {
setTimeout(checkLoad, 100)
}
}
checkLoad()
})
}
await waitForLoad()
}
message.success(`已切换到${getQualityName(index)}`)
} catch (error) {
console.error('切换画质失败:', error)
message.error('切换画质失败')
}
}
//
const formatFileSize = (bytes: number | string) => {
@ -450,57 +362,6 @@ onUnmounted(() => {
z-index: 10;
}
.quality-selector {
position: absolute;
top: 12px;
right: 12px;
background: rgba(0, 0, 0, 0.7);
padding: 8px 12px;
border-radius: 6px;
z-index: 5;
}
.quality-label {
color: white;
font-size: 12px;
}
.no-video {
padding: 40px 20px;
text-align: center;
}
.video-description {
margin-bottom: 20px;
}
.video-description h5 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
font-weight: 600;
}
.description-content {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: #555;
white-space: pre-wrap;
}
.video-details {
margin-top: 20px;
}
.quality-count,
.duration,
.file-size {
white-space: nowrap;
}
/* DPlayer 样式覆盖 */
:deep(.dplayer) {
border-radius: 8px;
@ -534,17 +395,5 @@ onUnmounted(() => {
.dplayer-container {
height: 250px;
}
.quality-selector {
position: static;
background: rgba(0, 0, 0, 0.9);
margin-top: 8px;
border-radius: 6px;
}
.quality-label {
display: block;
margin-bottom: 4px;
}
}
</style>

View File

@ -81,12 +81,12 @@
</div>
<!-- 我的下载 -->
<div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')">
<!-- <div :class="['image-text_26', { active: activeTab === 'download' }]" @click="handleMenuSelect('download')">
<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> -->
</div>
</div>

View File

@ -149,7 +149,7 @@ const flattenedChapters = computed(() => {
if (a.sortOrder === null && b.sortOrder === null) return 0
if (a.sortOrder === null) return 1
if (b.sortOrder === null) return -1
return (a.sortOrder || 0) - (b.sortOrder || 0)
return (b.sortOrder || 0) - (a.sortOrder || 0)
})
//

View File

@ -85,36 +85,13 @@
</div>
</div>
</div>
<!-- 添加讨论模态框 -->
<n-modal v-model:show="showAddDiscussionModal" preset="card" title="添加讨论" style="width: 600px">
<div class="add-discussion-form">
<n-form :model="newDiscussion" label-placement="top">
<n-form-item label="讨论标题">
<n-input v-model:value="newDiscussion.title" placeholder="请输入讨论标题" />
</n-form-item>
<n-form-item label="所属章节">
<n-select v-model:value="newDiscussion.chapterId" :options="chapterOptions" placeholder="请选择章节" />
</n-form-item>
<n-form-item label="讨论内容">
<n-input v-model:value="newDiscussion.content" type="textarea" placeholder="请输入讨论内容" :rows="6" />
</n-form-item>
</n-form>
</div>
<template #footer>
<div class="modal-footer">
<n-button @click="showAddDiscussionModal = false">取消</n-button>
<n-button type="primary" @click="addDiscussion">确定</n-button>
</div>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, h, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { NButton, NInput, NDropdown, NIcon, NModal, NForm, NFormItem, NSelect, useMessage } from 'naive-ui'
import { NButton, NInput, NDropdown, NIcon, useMessage } from 'naive-ui'
import { DiscussionApi } from '@/api/modules/teachCourse'
const message = useMessage()
@ -123,12 +100,11 @@ const route = useRoute()
//
const searchKeyword = ref('')
const showAddDiscussionModal = ref(false)
const newDiscussion = ref({
title: '',
chapterId: '',
content: ''
})
// const newDiscussion = ref({
// title: '',
// chapterId: '',
// content: ''
// })
//
const sortedDiscussions = computed(() => {
@ -215,12 +191,6 @@ const formatTime = (timeStr: string) => {
}
}
//
const chapterOptions = [
{ label: '第一章:基础概念', value: 'chapter1' },
{ label: '第二章:进阶应用', value: 'chapter2' },
{ label: '第三章:实战项目', value: 'chapter3' }
]
//
onMounted(() => {
@ -290,15 +260,6 @@ const deleteDiscussion = () => {
message.success('删除成功')
}
const addDiscussion = () => {
message.success('添加成功')
showAddDiscussionModal.value = false
newDiscussion.value = {
title: '',
chapterId: '',
content: ''
}
}
const viewComments = (discussion: any) => {
router.push({

View File

@ -14,14 +14,8 @@
</template>
返回
</n-button>
<div class="page-title-section">
<h1 class="page-title">{{ discussionInfo.title || '加载中...' }}</h1>
</div>
</div>
<div class="header-actions">
<n-button type="primary" @click="goToAddReply">
添加回复
</n-button>
<div class="search-box">
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px;" clearable
@clear="clearSearch" @keyup.enter="handleSearch" />
@ -32,8 +26,12 @@
<!-- 讨论详情卡片 -->
<div v-if="discussionInfo.title" class="discussion-detail-card">
<h1 class="page-title">{{ discussionInfo.title || '加载中...' }}</h1>
<div class="discussion-content-preview" v-if="discussionInfo.content">
<div class="content-text" v-html="discussionInfo.content"></div>
<div class="page-content" v-html="discussionInfo.content"></div>
</div>
<div class="discussion-content-preview" v-if="discussionInfo.chapterName">
<div class="content-text" v-html="discussionInfo.chapterName"></div>
</div>
<div class="discussion-meta">
<span class="timestamp">{{ discussionInfo.timestamp }}</span>
@ -42,7 +40,32 @@
<!-- 回复列表 -->
<div class="replies-list">
<!-- 搜索结果提示 -->
<div class="reply-box">
<div class="reply-total">全部评论 {{ sortedReplies.length }}</div>
<div class="message-box">
<div class="avatar">
<img :src="userStore.user?.avatar"
:alt="userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username"
class="avatar">
<div class="avatar-text">
{{ userStore.user?.profile?.realName || userStore.user?.nickname || userStore.user?.username
}}
</div>
</div>
<div class="textarea">
<n-input v-model:value="newReply.content" type="textarea" placeholder="请输入回复内容" :rows="6" />
<div class="submit-box">
<n-button>取消</n-button>
<n-button type="primary" @click="addReply">
发布
</n-button>
</div>
</div>
</div>
</div>
<!-- 搜索结果提示 -->
<div v-if="searchKeyword.trim()" class="search-result-tip">
<span v-if="sortedReplies.length === 0" class="no-result">
未找到包含"{{ searchKeyword.trim() }}"的回复
@ -57,6 +80,8 @@
<p>暂无回复数据</p>
</div>
<!-- 输入框 -->
<!-- 回复项列表 -->
<div v-for="reply in sortedReplies" :key="reply.id" class="reply-item">
<!-- 用户信息 -->
@ -174,23 +199,6 @@
</div>
</div>
<!-- 添加回复模态框 -->
<n-modal v-model:show="showAddReplyModal" preset="card" title="添加回复" style="width: 600px">
<div class="add-reply-form">
<n-form :model="newReply" label-placement="top">
<n-form-item label="回复内容">
<n-input v-model:value="newReply.content" type="textarea" placeholder="请输入回复内容" :rows="6" />
</n-form-item>
</n-form>
</div>
<template #footer>
<div class="modal-footer">
<n-button @click="showAddReplyModal = false">取消</n-button>
<n-button type="primary" @click="addReply">确定</n-button>
</div>
</template>
</n-modal>
<!-- 回复回复模态框 -->
<n-modal v-model:show="showReplyToReplyModal" preset="card" title="回复评论" style="width: 600px">
<div class="reply-to-reply-form">
@ -221,6 +229,9 @@ import { useRouter, useRoute } from 'vue-router'
import { NButton, NInput, NIcon, NModal, NForm, NFormItem, useMessage } from 'naive-ui'
import { DiscussionApi } from '@/api/modules/teachCourse'
import type { CreateCommentRequest } from '@/api/modules/teachCourse'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const message = useMessage()
const router = useRouter()
@ -589,10 +600,6 @@ const submitReplyToReply = async () => {
}
}
//
const goToAddReply = () => {
showAddReplyModal.value = true
}
</script>
<style scoped>
@ -607,7 +614,6 @@ const goToAddReply = () => {
justify-content: space-between;
align-items: center;
padding: 20px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1.5px solid #f6f6f6;
}
@ -631,10 +637,15 @@ const goToAddReply = () => {
.page-title {
font-size: 18px;
font-weight: 500;
color: #333;
color: #0C99DA;
margin: 0;
}
.page-content {
color: #333333;
font-size: 14px;
}
.discussion-title {
font-size: 14px;
color: #666;
@ -683,11 +694,9 @@ const goToAddReply = () => {
/* 讨论详情卡片 */
.discussion-detail-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
border-bottom: 1px solid #e8e8e8;
margin: 0 20px 24px 20px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.discussion-header {
@ -751,15 +760,14 @@ const goToAddReply = () => {
}
.discussion-content-preview {
margin-top: 12px;
margin-top: 6px;
}
.content-text {
font-size: 14px;
line-height: 1.6;
font-size: 12px;
color: #333;
background: #f8f9fa;
padding: 16px;
padding: 9px;
border-radius: 6px;
}
@ -796,6 +804,40 @@ const goToAddReply = () => {
font-size: 16px;
}
.reply-box {
display: flex;
flex-direction: column;
padding: 0 20px 20px 20px;
border-bottom: 1.5px solid #F1F3F4;
background: #fff;
transition: all 0.3s ease;
position: relative;
}
.reply-total {
margin-bottom: 16px;
}
.message-box {
display: flex;
}
.avatar {
margin-right: 10px;
}
.textarea {
width: 100%;
}
.submit-box{
width: 100%;
margin: 10px 0;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.reply-item {
display: flex;
padding: 0 20px 20px 20px;
@ -834,7 +876,6 @@ const goToAddReply = () => {
/* 回复内容 */
.reply-content {
padding-top: 6px;
flex: 1;
margin-right: 16px;
}
@ -859,7 +900,6 @@ const goToAddReply = () => {
font-size: 14px;
color: #333;
line-height: 1.6;
margin-bottom: 12px;
}
.reply-meta {
@ -918,14 +958,7 @@ const goToAddReply = () => {
/* 嵌套回复样式 */
.nested-replies {
margin-top: 16px;
padding-left: 20px;
border-left: 2px solid #f0f0f0;
background: #fafafa;
border-radius: 0 8px 8px 0;
padding-top: 12px;
padding-bottom: 8px;
padding-right: 12px;
}
.nested-reply-item {

View File

@ -111,6 +111,7 @@ import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { ArrowBackOutline } from '@vicons/ionicons5'
import { downloadFileFromUrl, getFileExtension } from '@/utils/download'
import DPlayer from 'dplayer'
import {
FolderOutline,
VideocamOutline,
@ -121,13 +122,6 @@ import {
AttachOutline
} from '@vicons/ionicons5'
// DPlayer
declare global {
interface Window {
DPlayer: any
Hls: any
}
}
const router = useRouter()
const message = useMessage()
@ -155,7 +149,7 @@ const breadcrumbs = ref<FileItem[]>([])
const dplayerRef = ref<HTMLElement | null>(null)
const dplayer = ref<any>(null)
const videoQualities = ref<VideoQuality[]>([])
const currentQuality = ref<string>('')
// const currentQuality = ref<string>('')
//
interface VideoQuality {
@ -255,78 +249,50 @@ const parseVideoQualities = (fileUrl: string): VideoQuality[] => {
return [{
name: '原画',
url: fileUrl,
type: fileUrl.includes('.m3u8') ? 'hls' : 'normal'
type: 'auto'
}]
}
const qualities: VideoQuality[] = []
for (const url of urls) {
const quality = extractQualityFromUrl(url)
qualities.push({
name: quality,
url: url,
type: url.includes('.m3u8') ? 'hls' : 'normal'
})
}
//
return qualities.sort((a, b) => {
const aRes = getResolutionValue(a.name)
const bRes = getResolutionValue(b.name)
return bRes - aRes
})
}
// URL
const extractQualityFromUrl = (url: string): string => {
const patterns = [
{ pattern: /\/(\d+p)\//i, extract: (match: RegExpMatchArray) => match[1] },
{ pattern: /\/(\d+)\//i, extract: (match: RegExpMatchArray) => `${match[1]}p` }
]
for (const { pattern, extract } of patterns) {
const match = url.match(pattern)
if (match) {
return extract(match).toUpperCase()
// 480p -> 720p -> 1080p
return urls.sort((a: string, b: string) => {
const getQualityOrder = (url: string): number => {
if (url.includes('1080p') || url.includes('1080')) return 3
if (url.includes('720p') || url.includes('720')) return 2
if (url.includes('480p') || url.includes('480')) return 1
return 0
}
}
//
if (url.includes('1080')) return '1080P'
if (url.includes('720')) return '720P'
if (url.includes('480')) return '480P'
if (url.includes('360')) return '360P'
return '原画'
return getQualityOrder(a) - getQualityOrder(b)
}).map((url: string, index: number) => ({
name: getQualityNameFromUrl(url, index),
url,
type: 'auto'
}))
}
//
const getResolutionValue = (quality: string): number => {
const resolutionMap: { [key: string]: number } = {
'1080P': 1080,
'720P': 720,
'480P': 480,
'360P': 360,
'原画': 9999
}
return resolutionMap[quality] || 0
// URL
const getQualityNameFromUrl = (url: string, index: number): string => {
if (url.includes('1080p') || url.includes('1080')) return '1080p'
if (url.includes('720p') || url.includes('720')) return '720p'
if (url.includes('480p') || url.includes('480')) return '480p'
const defaultQualities = ['480p', '720p', '1080p']
return defaultQualities[index] || `画质${index + 1}`
}
//
const getDefaultQuality = (qualities: VideoQuality[]): VideoQuality | null => {
if (qualities.length === 0) return null
//
const getDefaultQualityIndex = (qualities: VideoQuality[]): number => {
if (qualities.length === 0) return 0
// 720P
const preferred = qualities.find(q => q.name === '720P')
if (preferred) return preferred
// 720p
const preferred = qualities.findIndex(q => q.name === '720p')
if (preferred !== -1) return preferred
// 1080P
const fallback = qualities.find(q => q.name === '1080P')
if (fallback) return fallback
// 480p
const fallback = qualities.findIndex(q => q.name === '480p')
if (fallback !== -1) return fallback
//
return qualities[0]
//
return 0
}
//
@ -339,6 +305,18 @@ const getVideoQualities = (file: FileItem): VideoQuality[] => {
return parseVideoQualities(fileUrl)
}
//
const getCurrentVideoSource = (qualities: VideoQuality[], qualityIndex: number) => {
if (qualities.length === 0) return null
const currentQuality = qualities[qualityIndex]
return {
url: currentQuality.url,
pic: currentFile.value?.originalData?.thumbnailUrl || '',
type: 'auto'
}
}
//
const initVideoPlayer = async (file: FileItem) => {
await nextTick()
@ -367,75 +345,62 @@ const initVideoPlayer = async (file: FileItem) => {
const qualities = getVideoQualities(file)
videoQualities.value = qualities
//
const defaultQuality = getDefaultQuality(qualities)
let videoUrl = fileUrl
//
const defaultQualityIndex = getDefaultQualityIndex(qualities)
const currentVideoSource = getCurrentVideoSource(qualities, defaultQualityIndex)
if (defaultQuality) {
currentQuality.value = defaultQuality.name
videoUrl = defaultQuality.url
}
// HLS.js
const isHLS = videoUrl.includes('.m3u8')
if (isHLS && !window.Hls && !(window as any).Hls) {
await loadHLSScript()
if (!currentVideoSource) {
console.error('无法获取视频源')
return
}
try {
// DPlayer
if (!window.DPlayer) {
await loadDPlayerScript()
}
//
dplayerRef.value.innerHTML = ''
//
const qualityConfig = qualities.length > 1 ? {
quality: qualities,
defaultQuality: defaultQualityIndex
} : {}
const options: any = {
container: dplayerRef.value,
autoplay: false,
theme: '#1890ff',
video: {
url: currentVideoSource.url,
pic: currentVideoSource.pic,
type: 'auto',
...qualityConfig,
},
subtitle: {
url: '',
type: 'webvtt',
fontSize: '20px',
bottom: '40px',
color: '#fff'
},
contextmenu: [
{
text: '视频信息',
click: () => {
message.info(`视频: ${file.name || '未知'}`)
}
}
],
theme: '#FADFA3',
loop: false,
lang: 'zh-cn',
screenshot: true,
screenshot: false,
hotkey: true,
preload: 'auto',
volume: 0.8,
video: {
url: videoUrl,
type: isHLS ? 'hls' : 'normal'
}
preload: 'metadata',
volume: 0.7,
mutex: true,
pictureInPicture: false,
airplay: false,
chromecast: false
}
//
if (qualities.length > 1) {
options.video.quality = qualities.map(q => ({
name: q.name,
url: q.url,
type: q.type || (q.url.includes('.m3u8') ? 'hls' : 'normal')
}))
options.video.defaultQuality = 0 //
}
// HLS
if (isHLS && (window as any).Hls && (window as any).Hls.isSupported()) {
options.video.customType = {
hls: function(video: HTMLVideoElement, _player: any) {
const hls = new (window as any).Hls({
debug: false,
enableWorker: true,
lowLatencyMode: false
})
hls.loadSource(video.src)
hls.attachMedia(video)
}
}
}
dplayer.value = new window.DPlayer(options)
//
dplayer.value.on('quality_end', (quality: any) => {
currentQuality.value = quality.name
console.log('画质切换到:', quality.name)
})
dplayer.value = new DPlayer(options)
console.log('✅ DPlayer 初始化成功')
} catch (error) {
@ -443,49 +408,6 @@ const initVideoPlayer = async (file: FileItem) => {
}
}
// DPlayer
const loadDPlayerScript = (): Promise<void> => {
return new Promise((resolve, reject) => {
if (window.DPlayer) {
resolve()
return
}
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js'
script.onload = () => {
console.log('✅ DPlayer脚本加载成功')
resolve()
}
script.onerror = (error) => {
console.error('❌ DPlayer脚本加载失败:', error)
reject(new Error('Failed to load DPlayer'))
}
document.head.appendChild(script)
})
}
// HLS.js
const loadHLSScript = (): Promise<void> => {
return new Promise((resolve, reject) => {
if ((window as any).Hls) {
resolve()
return
}
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.15/dist/hls.min.js'
script.onload = () => {
console.log('✅ HLS.js脚本加载成功')
resolve()
}
script.onerror = (error) => {
console.error('❌ HLS.js脚本加载失败:', error)
reject(new Error('Failed to load HLS.js'))
}
document.head.appendChild(script)
})
}
//
const buildBreadcrumbs = (file: FileItem, folderData?: FileItem) => {
const crumbs: FileItem[] = []

View File

@ -25,15 +25,9 @@
<!-- 空状态占位 -->
<div v-if="filteredHomeworks.length === 0 && !loading" class="empty-state">
<n-empty
description="还没有作业"
description="暂无数据"
size="large"
>
<template #icon>
<div class="empty-icon">📝</div>
</template>
<template #extra>
<span class="empty-extra">请先去创建作业吧~</span>
</template>
</n-empty>
</div>
@ -48,7 +42,7 @@
</div>
<div class="card-content">
<div class="content-left">
<p class="homework-desc">{{ homework.content }}</p>
<p class="homework-desc" v-html="homework.content"></p>
<div class="homework-info">
<div class="info-item">
<img class="icon" src="/images/teacher/发布人.png" alt="发布人" />

View File

@ -24,7 +24,7 @@
<!-- 右侧按钮和搜索 -->
<div class="actions-container">
<n-button class="action-btn export-btn" ghost type="primary" size="small">导出</n-button>
<n-button class="action-btn remove-btn" ghost type="error" size="small">移除</n-button>
<!-- <n-button class="action-btn remove-btn" ghost type="error" size="small">移除</n-button> -->
<div class="search-container">
<n-input-group>
<n-input v-model:value="searchText" placeholder="请输入学生姓名" :style="{ width: '200px' }" size="medium" />