fix:修复章节预览弹窗部分报错;修改评论页面布局;隐藏资源菜单
This commit is contained in:
parent
82ae528785
commit
03f9d1b423
@ -7,7 +7,7 @@
|
|||||||
<div v-else class="document-content">
|
<div v-else class="document-content">
|
||||||
<!-- 文档信息 -->
|
<!-- 文档信息 -->
|
||||||
<div class="document-info">
|
<div class="document-info">
|
||||||
<h4>{{ detailData.name || '文档标题' }}</h4>
|
<h4>{{ detailData.name}}.{{ detailData.fileUrl.split('.').pop() }}</h4>
|
||||||
<div class="document-meta">
|
<div class="document-meta">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-tag type="success" size="small">
|
<n-tag type="success" size="small">
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</n-icon>
|
</n-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="document-type-info">
|
<div class="document-type-info">
|
||||||
<h4>{{ detailData.name }}</h4>
|
<h4>{{ detailData.name }}.{{ detailData.fileUrl.split('.').pop() }}</h4>
|
||||||
<p class="file-description">{{ detailData.description }}</p>
|
<p class="file-description">{{ detailData.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -73,7 +73,7 @@
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="document-operations">
|
<div class="document-operations">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button type="primary" size="large" @click="downloadDocument">
|
<n-button type="primary" @click="downloadDocument">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon><DownloadOutline /></n-icon>
|
<n-icon><DownloadOutline /></n-icon>
|
||||||
</template>
|
</template>
|
||||||
|
@ -59,8 +59,7 @@
|
|||||||
<!-- 作业描述 -->
|
<!-- 作业描述 -->
|
||||||
<div class="homework-description">
|
<div class="homework-description">
|
||||||
<h5>作业描述</h5>
|
<h5>作业描述</h5>
|
||||||
<div class="description-content">
|
<div class="description-content" v-html="detailData.description">
|
||||||
{{ detailData.description || '暂无作业要求说明' }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
<n-space>
|
<n-space>
|
||||||
<n-tag type="info" size="small">
|
<n-tag type="info" size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon><PlayCircleOutline /></n-icon>
|
<n-icon>
|
||||||
|
<PlayCircleOutline />
|
||||||
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
视频
|
视频
|
||||||
</n-tag>
|
</n-tag>
|
||||||
@ -40,24 +42,6 @@
|
|||||||
<template #description>视频加载中...</template>
|
<template #description>视频加载中...</template>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div v-else class="no-video">
|
<div v-else class="no-video">
|
||||||
@ -99,11 +83,9 @@ import {
|
|||||||
NIcon,
|
NIcon,
|
||||||
NSpin,
|
NSpin,
|
||||||
NResult,
|
NResult,
|
||||||
NButton,
|
|
||||||
NDivider,
|
NDivider,
|
||||||
NDescriptions,
|
NDescriptions,
|
||||||
NDescriptionsItem,
|
NDescriptionsItem,
|
||||||
NButtonGroup,
|
|
||||||
useMessage
|
useMessage
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { PlayCircleOutline } from '@vicons/ionicons5'
|
import { PlayCircleOutline } from '@vicons/ionicons5'
|
||||||
@ -178,10 +160,10 @@ const getQualityNameFromUrl = (url: string, index: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取画质名称 - 根据链接内容判断画质
|
// 获取画质名称 - 根据链接内容判断画质
|
||||||
const getQualityName = (index: number) => {
|
// const getQualityName = (index: number) => {
|
||||||
const quality = videoQualities.value[index]
|
// const quality = videoQualities.value[index]
|
||||||
return quality?.name || `画质${index + 1}`
|
// return quality?.name || `画质${index + 1}`
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 获取当前播放的视频源
|
// 获取当前播放的视频源
|
||||||
const currentVideoSource = computed(() => {
|
const currentVideoSource = computed(() => {
|
||||||
@ -219,6 +201,7 @@ const initDPlayer = async () => {
|
|||||||
defaultQuality: currentQualityIndex.value
|
defaultQuality: currentQualityIndex.value
|
||||||
} : {}
|
} : {}
|
||||||
|
|
||||||
|
|
||||||
// 创建新的DPlayer实例
|
// 创建新的DPlayer实例
|
||||||
dplayer = new DPlayer({
|
dplayer = new DPlayer({
|
||||||
container: dplayerContainer.value,
|
container: dplayerContainer.value,
|
||||||
@ -255,25 +238,8 @@ const initDPlayer = async () => {
|
|||||||
airplay: false,
|
airplay: false,
|
||||||
chromecast: false
|
chromecast: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听播放器事件
|
|
||||||
dplayer.on('loadstart', () => {
|
|
||||||
videoLoading.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
dplayer.on('loadedmetadata', () => {
|
|
||||||
videoLoading.value = false
|
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) {
|
} catch (error) {
|
||||||
videoLoading.value = false
|
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) => {
|
const formatFileSize = (bytes: number | string) => {
|
||||||
@ -450,57 +362,6 @@ onUnmounted(() => {
|
|||||||
z-index: 10;
|
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 样式覆盖 */
|
/* DPlayer 样式覆盖 */
|
||||||
:deep(.dplayer) {
|
:deep(.dplayer) {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -534,17 +395,5 @@ onUnmounted(() => {
|
|||||||
.dplayer-container {
|
.dplayer-container {
|
||||||
height: 250px;
|
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>
|
</style>
|
@ -81,12 +81,12 @@
|
|||||||
</div>
|
</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 default-icon" referrerpolicy="no-referrer" src="/images/profile/download.png" />
|
||||||
<img class="thumbnail_41 hover-icon" referrerpolicy="no-referrer"
|
<img class="thumbnail_41 hover-icon" referrerpolicy="no-referrer"
|
||||||
src="/images/profile/download-active.png" />
|
src="/images/profile/download-active.png" />
|
||||||
<span class="text-group_26">我的下载</span>
|
<span class="text-group_26">我的下载</span>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ const flattenedChapters = computed(() => {
|
|||||||
if (a.sortOrder === null && b.sortOrder === null) return 0
|
if (a.sortOrder === null && b.sortOrder === null) return 0
|
||||||
if (a.sortOrder === null) return 1
|
if (a.sortOrder === null) return 1
|
||||||
if (b.sortOrder === null) return -1
|
if (b.sortOrder === null) return -1
|
||||||
return (a.sortOrder || 0) - (b.sortOrder || 0)
|
return (b.sortOrder || 0) - (a.sortOrder || 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 为每个章节添加其子节
|
// 为每个章节添加其子节
|
||||||
|
@ -85,36 +85,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, h, onMounted } from 'vue'
|
import { ref, computed, h, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
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'
|
import { DiscussionApi } from '@/api/modules/teachCourse'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@ -123,12 +100,11 @@ const route = useRoute()
|
|||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const showAddDiscussionModal = ref(false)
|
// const newDiscussion = ref({
|
||||||
const newDiscussion = ref({
|
// title: '',
|
||||||
title: '',
|
// chapterId: '',
|
||||||
chapterId: '',
|
// content: ''
|
||||||
content: ''
|
// })
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:排序后的讨论列表(置顶的在前)
|
// 计算属性:排序后的讨论列表(置顶的在前)
|
||||||
const sortedDiscussions = computed(() => {
|
const sortedDiscussions = computed(() => {
|
||||||
@ -215,12 +191,6 @@ const formatTime = (timeStr: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 章节选项
|
|
||||||
const chapterOptions = [
|
|
||||||
{ label: '第一章:基础概念', value: 'chapter1' },
|
|
||||||
{ label: '第二章:进阶应用', value: 'chapter2' },
|
|
||||||
{ label: '第三章:实战项目', value: 'chapter3' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 在组件挂载时加载讨论列表
|
// 在组件挂载时加载讨论列表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -290,15 +260,6 @@ const deleteDiscussion = () => {
|
|||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
const addDiscussion = () => {
|
|
||||||
message.success('添加成功')
|
|
||||||
showAddDiscussionModal.value = false
|
|
||||||
newDiscussion.value = {
|
|
||||||
title: '',
|
|
||||||
chapterId: '',
|
|
||||||
content: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewComments = (discussion: any) => {
|
const viewComments = (discussion: any) => {
|
||||||
router.push({
|
router.push({
|
||||||
|
@ -14,14 +14,8 @@
|
|||||||
</template>
|
</template>
|
||||||
返回
|
返回
|
||||||
</n-button>
|
</n-button>
|
||||||
<div class="page-title-section">
|
|
||||||
<h1 class="page-title">{{ discussionInfo.title || '加载中...' }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<n-button type="primary" @click="goToAddReply">
|
|
||||||
添加回复
|
|
||||||
</n-button>
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px;" clearable
|
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px;" clearable
|
||||||
@clear="clearSearch" @keyup.enter="handleSearch" />
|
@clear="clearSearch" @keyup.enter="handleSearch" />
|
||||||
@ -32,8 +26,12 @@
|
|||||||
|
|
||||||
<!-- 讨论详情卡片 -->
|
<!-- 讨论详情卡片 -->
|
||||||
<div v-if="discussionInfo.title" class="discussion-detail-card">
|
<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="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>
|
||||||
<div class="discussion-meta">
|
<div class="discussion-meta">
|
||||||
<span class="timestamp">{{ discussionInfo.timestamp }}</span>
|
<span class="timestamp">{{ discussionInfo.timestamp }}</span>
|
||||||
@ -42,6 +40,31 @@
|
|||||||
|
|
||||||
<!-- 回复列表 -->
|
<!-- 回复列表 -->
|
||||||
<div class="replies-list">
|
<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">
|
<div v-if="searchKeyword.trim()" class="search-result-tip">
|
||||||
<span v-if="sortedReplies.length === 0" class="no-result">
|
<span v-if="sortedReplies.length === 0" class="no-result">
|
||||||
@ -57,6 +80,8 @@
|
|||||||
<p>暂无回复数据</p>
|
<p>暂无回复数据</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入框 -->
|
||||||
|
|
||||||
<!-- 回复项列表 -->
|
<!-- 回复项列表 -->
|
||||||
<div v-for="reply in sortedReplies" :key="reply.id" class="reply-item">
|
<div v-for="reply in sortedReplies" :key="reply.id" class="reply-item">
|
||||||
<!-- 用户信息 -->
|
<!-- 用户信息 -->
|
||||||
@ -174,23 +199,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<n-modal v-model:show="showReplyToReplyModal" preset="card" title="回复评论" style="width: 600px">
|
||||||
<div class="reply-to-reply-form">
|
<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 { NButton, NInput, NIcon, NModal, NForm, NFormItem, useMessage } from 'naive-ui'
|
||||||
import { DiscussionApi } from '@/api/modules/teachCourse'
|
import { DiscussionApi } from '@/api/modules/teachCourse'
|
||||||
import type { CreateCommentRequest } from '@/api/modules/teachCourse'
|
import type { CreateCommentRequest } from '@/api/modules/teachCourse'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -589,10 +600,6 @@ const submitReplyToReply = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到添加回复页面
|
|
||||||
const goToAddReply = () => {
|
|
||||||
showAddReplyModal.value = true
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -607,7 +614,6 @@ const goToAddReply = () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
border-bottom: 1.5px solid #f6f6f6;
|
border-bottom: 1.5px solid #f6f6f6;
|
||||||
}
|
}
|
||||||
@ -631,10 +637,15 @@ const goToAddReply = () => {
|
|||||||
.page-title {
|
.page-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #0C99DA;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
color: #333333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.discussion-title {
|
.discussion-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@ -683,11 +694,9 @@ const goToAddReply = () => {
|
|||||||
/* 讨论详情卡片 */
|
/* 讨论详情卡片 */
|
||||||
.discussion-detail-card {
|
.discussion-detail-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e8e8e8;
|
border-bottom: 1px solid #e8e8e8;
|
||||||
border-radius: 8px;
|
|
||||||
margin: 0 20px 24px 20px;
|
margin: 0 20px 24px 20px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.discussion-header {
|
.discussion-header {
|
||||||
@ -751,15 +760,14 @@ const goToAddReply = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.discussion-content-preview {
|
.discussion-content-preview {
|
||||||
margin-top: 12px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-text {
|
.content-text {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
padding: 16px;
|
padding: 9px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -796,6 +804,40 @@ const goToAddReply = () => {
|
|||||||
font-size: 16px;
|
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 {
|
.reply-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 20px 20px 20px;
|
padding: 0 20px 20px 20px;
|
||||||
@ -834,7 +876,6 @@ const goToAddReply = () => {
|
|||||||
|
|
||||||
/* 回复内容 */
|
/* 回复内容 */
|
||||||
.reply-content {
|
.reply-content {
|
||||||
padding-top: 6px;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
@ -859,7 +900,6 @@ const goToAddReply = () => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-meta {
|
.reply-meta {
|
||||||
@ -918,14 +958,7 @@ const goToAddReply = () => {
|
|||||||
|
|
||||||
/* 嵌套回复样式 */
|
/* 嵌套回复样式 */
|
||||||
.nested-replies {
|
.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-top: 12px;
|
||||||
padding-bottom: 8px;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nested-reply-item {
|
.nested-reply-item {
|
||||||
|
@ -111,6 +111,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||||
import { downloadFileFromUrl, getFileExtension } from '@/utils/download'
|
import { downloadFileFromUrl, getFileExtension } from '@/utils/download'
|
||||||
|
import DPlayer from 'dplayer'
|
||||||
import {
|
import {
|
||||||
FolderOutline,
|
FolderOutline,
|
||||||
VideocamOutline,
|
VideocamOutline,
|
||||||
@ -121,13 +122,6 @@ import {
|
|||||||
AttachOutline
|
AttachOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
|
|
||||||
// 声明全局 DPlayer 类型
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
DPlayer: any
|
|
||||||
Hls: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@ -155,7 +149,7 @@ const breadcrumbs = ref<FileItem[]>([])
|
|||||||
const dplayerRef = ref<HTMLElement | null>(null)
|
const dplayerRef = ref<HTMLElement | null>(null)
|
||||||
const dplayer = ref<any>(null)
|
const dplayer = ref<any>(null)
|
||||||
const videoQualities = ref<VideoQuality[]>([])
|
const videoQualities = ref<VideoQuality[]>([])
|
||||||
const currentQuality = ref<string>('')
|
// const currentQuality = ref<string>('')
|
||||||
|
|
||||||
// 视频画质类型定义
|
// 视频画质类型定义
|
||||||
interface VideoQuality {
|
interface VideoQuality {
|
||||||
@ -255,78 +249,50 @@ const parseVideoQualities = (fileUrl: string): VideoQuality[] => {
|
|||||||
return [{
|
return [{
|
||||||
name: '原画',
|
name: '原画',
|
||||||
url: fileUrl,
|
url: fileUrl,
|
||||||
type: fileUrl.includes('.m3u8') ? 'hls' : 'normal'
|
type: 'auto'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
const qualities: VideoQuality[] = []
|
// 按画质排序:480p -> 720p -> 1080p
|
||||||
|
return urls.sort((a: string, b: string) => {
|
||||||
for (const url of urls) {
|
const getQualityOrder = (url: string): number => {
|
||||||
const quality = extractQualityFromUrl(url)
|
if (url.includes('1080p') || url.includes('1080')) return 3
|
||||||
qualities.push({
|
if (url.includes('720p') || url.includes('720')) return 2
|
||||||
name: quality,
|
if (url.includes('480p') || url.includes('480')) return 1
|
||||||
url: url,
|
return 0
|
||||||
type: url.includes('.m3u8') ? 'hls' : 'normal'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return getQualityOrder(a) - getQualityOrder(b)
|
||||||
// 按画质排序(从高到低)
|
}).map((url: string, index: number) => ({
|
||||||
return qualities.sort((a, b) => {
|
name: getQualityNameFromUrl(url, index),
|
||||||
const aRes = getResolutionValue(a.name)
|
url,
|
||||||
const bRes = getResolutionValue(b.name)
|
type: 'auto'
|
||||||
return bRes - aRes
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从URL中提取画质信息
|
// 根据URL获取画质名称
|
||||||
const extractQualityFromUrl = (url: string): string => {
|
const getQualityNameFromUrl = (url: string, index: number): string => {
|
||||||
const patterns = [
|
if (url.includes('1080p') || url.includes('1080')) return '1080p'
|
||||||
{ pattern: /\/(\d+p)\//i, extract: (match: RegExpMatchArray) => match[1] },
|
if (url.includes('720p') || url.includes('720')) return '720p'
|
||||||
{ pattern: /\/(\d+)\//i, extract: (match: RegExpMatchArray) => `${match[1]}p` }
|
if (url.includes('480p') || url.includes('480')) return '480p'
|
||||||
]
|
|
||||||
|
|
||||||
for (const { pattern, extract } of patterns) {
|
const defaultQualities = ['480p', '720p', '1080p']
|
||||||
const match = url.match(pattern)
|
return defaultQualities[index] || `画质${index + 1}`
|
||||||
if (match) {
|
|
||||||
return extract(match).toUpperCase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试从路径推断
|
|
||||||
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 '原画'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取分辨率数值(用于排序)
|
// 获取默认画质索引
|
||||||
const getResolutionValue = (quality: string): number => {
|
const getDefaultQualityIndex = (qualities: VideoQuality[]): number => {
|
||||||
const resolutionMap: { [key: string]: number } = {
|
if (qualities.length === 0) return 0
|
||||||
'1080P': 1080,
|
|
||||||
'720P': 720,
|
|
||||||
'480P': 480,
|
|
||||||
'360P': 360,
|
|
||||||
'原画': 9999
|
|
||||||
}
|
|
||||||
return resolutionMap[quality] || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取默认画质
|
// 优先选择720p
|
||||||
const getDefaultQuality = (qualities: VideoQuality[]): VideoQuality | null => {
|
const preferred = qualities.findIndex(q => q.name === '720p')
|
||||||
if (qualities.length === 0) return null
|
if (preferred !== -1) return preferred
|
||||||
|
|
||||||
// 优先选择720P
|
// 其次选择480p
|
||||||
const preferred = qualities.find(q => q.name === '720P')
|
const fallback = qualities.findIndex(q => q.name === '480p')
|
||||||
if (preferred) return preferred
|
if (fallback !== -1) return fallback
|
||||||
|
|
||||||
// 其次选择1080P
|
// 最后返回第一个(最低画质)
|
||||||
const fallback = qualities.find(q => q.name === '1080P')
|
return 0
|
||||||
if (fallback) return fallback
|
|
||||||
|
|
||||||
// 最后返回第一个
|
|
||||||
return qualities[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取视频画质选项
|
// 获取视频画质选项
|
||||||
@ -339,6 +305,18 @@ const getVideoQualities = (file: FileItem): VideoQuality[] => {
|
|||||||
return parseVideoQualities(fileUrl)
|
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) => {
|
const initVideoPlayer = async (file: FileItem) => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@ -367,75 +345,62 @@ const initVideoPlayer = async (file: FileItem) => {
|
|||||||
const qualities = getVideoQualities(file)
|
const qualities = getVideoQualities(file)
|
||||||
videoQualities.value = qualities
|
videoQualities.value = qualities
|
||||||
|
|
||||||
// 设置默认画质
|
// 获取默认画质索引
|
||||||
const defaultQuality = getDefaultQuality(qualities)
|
const defaultQualityIndex = getDefaultQualityIndex(qualities)
|
||||||
let videoUrl = fileUrl
|
const currentVideoSource = getCurrentVideoSource(qualities, defaultQualityIndex)
|
||||||
|
|
||||||
if (defaultQuality) {
|
if (!currentVideoSource) {
|
||||||
currentQuality.value = defaultQuality.name
|
console.error('无法获取视频源')
|
||||||
videoUrl = defaultQuality.url
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要加载HLS.js
|
|
||||||
const isHLS = videoUrl.includes('.m3u8')
|
|
||||||
if (isHLS && !window.Hls && !(window as any).Hls) {
|
|
||||||
await loadHLSScript()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查DPlayer是否存在
|
// 清空容器内容
|
||||||
if (!window.DPlayer) {
|
dplayerRef.value.innerHTML = ''
|
||||||
await loadDPlayerScript()
|
|
||||||
}
|
// 准备画质配置
|
||||||
|
const qualityConfig = qualities.length > 1 ? {
|
||||||
|
quality: qualities,
|
||||||
|
defaultQuality: defaultQualityIndex
|
||||||
|
} : {}
|
||||||
|
|
||||||
const options: any = {
|
const options: any = {
|
||||||
container: dplayerRef.value,
|
container: dplayerRef.value,
|
||||||
autoplay: false,
|
video: {
|
||||||
theme: '#1890ff',
|
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,
|
loop: false,
|
||||||
lang: 'zh-cn',
|
lang: 'zh-cn',
|
||||||
screenshot: true,
|
screenshot: false,
|
||||||
hotkey: true,
|
hotkey: true,
|
||||||
preload: 'auto',
|
preload: 'metadata',
|
||||||
volume: 0.8,
|
volume: 0.7,
|
||||||
video: {
|
mutex: true,
|
||||||
url: videoUrl,
|
pictureInPicture: false,
|
||||||
type: isHLS ? 'hls' : 'normal'
|
airplay: false,
|
||||||
}
|
chromecast: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有多个画质,添加画质选择
|
dplayer.value = new DPlayer(options)
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('✅ DPlayer 初始化成功')
|
console.log('✅ DPlayer 初始化成功')
|
||||||
} catch (error) {
|
} 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 buildBreadcrumbs = (file: FileItem, folderData?: FileItem) => {
|
||||||
const crumbs: FileItem[] = []
|
const crumbs: FileItem[] = []
|
||||||
|
@ -25,15 +25,9 @@
|
|||||||
<!-- 空状态占位 -->
|
<!-- 空状态占位 -->
|
||||||
<div v-if="filteredHomeworks.length === 0 && !loading" class="empty-state">
|
<div v-if="filteredHomeworks.length === 0 && !loading" class="empty-state">
|
||||||
<n-empty
|
<n-empty
|
||||||
description="还没有作业"
|
description="暂无数据"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<template #icon>
|
|
||||||
<div class="empty-icon">📝</div>
|
|
||||||
</template>
|
|
||||||
<template #extra>
|
|
||||||
<span class="empty-extra">请先去创建作业吧~</span>
|
|
||||||
</template>
|
|
||||||
</n-empty>
|
</n-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -48,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content-left">
|
<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="homework-info">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<img class="icon" src="/images/teacher/发布人.png" alt="发布人" />
|
<img class="icon" src="/images/teacher/发布人.png" alt="发布人" />
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<!-- 右侧按钮和搜索 -->
|
<!-- 右侧按钮和搜索 -->
|
||||||
<div class="actions-container">
|
<div class="actions-container">
|
||||||
<n-button class="action-btn export-btn" ghost type="primary" size="small">导出</n-button>
|
<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">
|
<div class="search-container">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="searchText" placeholder="请输入学生姓名" :style="{ width: '200px' }" size="medium" />
|
<n-input v-model:value="searchText" placeholder="请输入学生姓名" :style="{ width: '200px' }" size="medium" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user