fix: 课程管理下的页面样式和逻辑补全优化并添加了文件预览页面
This commit is contained in:
parent
590af0951f
commit
02dfa15e75
@ -2,23 +2,21 @@
|
||||
<div class="course-category">
|
||||
<!-- 顶部 -->
|
||||
<div class="top">
|
||||
<div class="nav-links">
|
||||
<a href="" class="active">进行中</a>
|
||||
<a href="">已结束</a>
|
||||
<a href="">草稿箱</a>
|
||||
</div>
|
||||
<n-tabs v-model:value="activeTab" size="large">
|
||||
<n-tab-pane name="ongoing" tab="进行中" />
|
||||
<n-tab-pane name="finished" tab="已结束" />
|
||||
<n-tab-pane name="draft" tab="草稿箱" />
|
||||
</n-tabs>
|
||||
|
||||
<div class="actions">
|
||||
<button class="create-btn" @click="navigateToCreateCourse">创建课程</button>
|
||||
<n-button type="primary" @click="navigateToCreateCourse">创建课程</n-button>
|
||||
<div class="search-container">
|
||||
<input type="text" placeholder="请输入想要搜索的内容">
|
||||
<button class="search-btn">搜索</button>
|
||||
<n-input v-model:value="searchValue" type="text" placeholder="请输入想要搜索的内容" />
|
||||
<n-button type="primary" class="search-btn">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="course-container">
|
||||
<div class="course-grid">
|
||||
@ -26,23 +24,21 @@
|
||||
<div class="course-image-container">
|
||||
<div class="section-title" :class="{ 'offline': course.status === '下架中' }">{{ course.status }}
|
||||
</div>
|
||||
<div class="more-options">
|
||||
<span class="more-icon">⋮</span>
|
||||
<div class="options-menu">
|
||||
<template v-if="course.status === '发布中'">
|
||||
<a href="#" class="option-item"><img src="/images/teacher/下架.png" alt="">下架</a>
|
||||
<a href="javascript:void(0)" class="option-item"><img src="/images/teacher/小编辑.png" alt="">编辑</a>
|
||||
<a href="#" class="option-item"><img src="/images/teacher/移动.png" alt="">移动</a>
|
||||
<a href="#" class="option-item"><img src="/images/teacher/删除.png" alt="">删除</a>
|
||||
</template>
|
||||
<template v-else-if="course.status === '下架中'">
|
||||
<a href="#" class="option-item"><img src="/images/teacher/加号.png" alt="">发布</a>
|
||||
<a href="javascript:void(0)" class="option-item"><img src="/images/teacher/小编辑.png" alt="">编辑</a>
|
||||
<a href="#" class="option-item"><img src="/images/teacher/移动.png" alt="">移动</a>
|
||||
<a href="#" class="option-item"><img src="/images/teacher/删除.png" alt="">删除</a>
|
||||
</template>
|
||||
<n-popselect
|
||||
:options="getOptionsForCourse(course)"
|
||||
trigger="hover"
|
||||
placement="bottom-end"
|
||||
:render-label="renderOptionLabel"
|
||||
@update:value="(value: string) => handleOptionSelect(value, course)"
|
||||
>
|
||||
<div class="more-options">
|
||||
<span class="more-icon">
|
||||
<n-icon size="20">
|
||||
<EllipsisVerticalSharp />
|
||||
</n-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-popselect>
|
||||
</div>
|
||||
<div class="course-info" @click="router.push(`/teacher/course-editor/${course.id}`)" style="cursor: pointer;">
|
||||
<img :src="course.image" alt="">
|
||||
@ -76,32 +72,196 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, h } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { EllipsisVerticalSharp } from '@vicons/ionicons5';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
|
||||
const router = useRouter();
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
|
||||
// 模拟课程数据
|
||||
const courseList = ref([
|
||||
{ id: 1, name: '前端开发基础课程', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 2, name: 'Vue.js 实战教程', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 3, name: 'React 入门到精通', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 4, name: 'Node.js 后端开发', status: '下架中', image: '/images/teacher/fj.png' },
|
||||
{ id: 5, name: 'TypeScript 高级教程', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 6, name: 'JavaScript 设计模式', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 7, name: 'CSS 动画与特效', status: '下架中', image: '/images/teacher/fj.png' },
|
||||
{ id: 8, name: 'HTML5 新特性详解', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 9, name: 'Web 性能优化指南', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 10, name: '移动端适配实战', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 11, name: '微信小程序开发', status: '下架中', image: '/images/teacher/fj.png' },
|
||||
{ id: 12, name: 'Flutter 跨平台开发', status: '发布中', image: '/images/teacher/fj.png' },
|
||||
{ id: 1, name: '前端开发基础课程', status: '发布中', image: '/images/teacher/fj.png', students: 120 },
|
||||
{ id: 2, name: 'Vue.js 实战教程', status: '发布中', image: '/images/teacher/fj.png', students: 95 },
|
||||
{ id: 3, name: 'React 入门到精通', status: '发布中', image: '/images/teacher/fj.png', students: 87 },
|
||||
{ id: 4, name: 'Node.js 后端开发', status: '下架中', image: '/images/teacher/fj.png', students: 65 },
|
||||
{ id: 5, name: 'TypeScript 高级教程', status: '发布中', image: '/images/teacher/fj.png', students: 73 },
|
||||
{ id: 6, name: 'JavaScript 设计模式', status: '发布中', image: '/images/teacher/fj.png', students: 56 },
|
||||
{ id: 7, name: 'CSS 动画与特效', status: '下架中', image: '/images/teacher/fj.png', students: 42 },
|
||||
{ id: 8, name: 'HTML5 新特性详解', status: '发布中', image: '/images/teacher/fj.png', students: 89 },
|
||||
{ id: 9, name: 'Web 性能优化指南', status: '发布中', image: '/images/teacher/fj.png', students: 67 },
|
||||
{ id: 10, name: '移动端适配实战', status: '发布中', image: '/images/teacher/fj.png', students: 54 },
|
||||
{ id: 11, name: '微信小程序开发', status: '下架中', image: '/images/teacher/fj.png', students: 38 },
|
||||
{ id: 12, name: 'Flutter 跨平台开发', status: '发布中', image: '/images/teacher/fj.png', students: 29 },
|
||||
]);
|
||||
|
||||
const searchValue = ref<string>('')
|
||||
const activeTab = ref<string>('ongoing')
|
||||
|
||||
// 跳转到创建课程页面
|
||||
const navigateToCreateCourse = () => {
|
||||
router.push('/teacher/course-create');
|
||||
};
|
||||
|
||||
// 获取课程对应的选项列表
|
||||
const getOptionsForCourse = (course: any) => {
|
||||
if (course.status === '发布中') {
|
||||
return [
|
||||
{ label: '下架', value: 'offline', icon: '/images/teacher/下架.png' },
|
||||
{ label: '编辑', value: 'edit', icon: '/images/teacher/小编辑.png' },
|
||||
{ label: '移动', value: 'move', icon: '/images/teacher/移动.png' },
|
||||
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
|
||||
];
|
||||
} else if (course.status === '下架中') {
|
||||
return [
|
||||
{ label: '发布', value: 'publish', icon: '/images/teacher/加号.png' },
|
||||
{ label: '编辑', value: 'edit', icon: '/images/teacher/小编辑.png' },
|
||||
{ label: '移动', value: 'move', icon: '/images/teacher/移动.png' },
|
||||
{ label: '删除', value: 'delete', icon: '/images/teacher/删除.png' }
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 渲染选项标签(包含图标)
|
||||
const renderOptionLabel = (option: any) => {
|
||||
return h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
}
|
||||
}, [
|
||||
h('img', {
|
||||
src: option.icon,
|
||||
alt: '',
|
||||
style: {
|
||||
width: '13px',
|
||||
height: '13px'
|
||||
}
|
||||
}),
|
||||
option.label
|
||||
]);
|
||||
};
|
||||
|
||||
// 处理选项选择
|
||||
const handleOptionSelect = (value: string, course: any) => {
|
||||
console.log('选择了操作:', value, '课程:', course);
|
||||
// 根据不同的 value 执行对应的操作
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
// 编辑逻辑 - 跳转到课程编辑页面
|
||||
router.push(`/teacher/course-create/${course.id}`);
|
||||
break;
|
||||
case 'delete':
|
||||
// 删除逻辑
|
||||
handleDeleteCourse(course);
|
||||
break;
|
||||
case 'offline':
|
||||
// 下架逻辑
|
||||
handleOfflineCourse(course);
|
||||
break;
|
||||
case 'publish':
|
||||
// 发布逻辑
|
||||
handlePublishCourse(course);
|
||||
break;
|
||||
case 'move':
|
||||
// 移动逻辑
|
||||
handleMoveCourse(course);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除课程
|
||||
const handleDeleteCourse = (course: any) => {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: `确定要删除课程"${course.name}"吗?此操作不可撤销。`,
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const index = courseList.value.findIndex(c => c.id === course.id);
|
||||
if (index > -1) {
|
||||
courseList.value.splice(index, 1);
|
||||
message.success(`课程"${course.name}"已删除`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 下架课程
|
||||
const handleOfflineCourse = (course: any) => {
|
||||
const targetCourse = courseList.value.find(c => c.id === course.id);
|
||||
if (targetCourse) {
|
||||
targetCourse.status = '下架中';
|
||||
message.success(`课程"${course.name}"已下架`);
|
||||
}
|
||||
};
|
||||
|
||||
// 发布课程
|
||||
const handlePublishCourse = (course: any) => {
|
||||
const targetCourse = courseList.value.find(c => c.id === course.id);
|
||||
if (targetCourse) {
|
||||
targetCourse.status = '发布中';
|
||||
message.success(`课程"${course.name}"已发布`);
|
||||
}
|
||||
};
|
||||
|
||||
// 移动课程
|
||||
const handleMoveCourse = (course: any) => {
|
||||
const currentIndex = courseList.value.findIndex(c => c.id === course.id);
|
||||
const totalCourses = courseList.value.length;
|
||||
|
||||
dialog.create({
|
||||
title: '移动课程位置',
|
||||
content: () => h('div', [
|
||||
h('p', `课程"${course.name}"当前位置:第 ${currentIndex + 1} 位`),
|
||||
h('p', { style: 'margin-top: 10px; margin-bottom: 10px;' }, '移动到位置:'),
|
||||
h('input', {
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: totalCourses,
|
||||
value: currentIndex + 1,
|
||||
style: 'width: 100%; padding: 8px; border: 1px solid #d9d9d9; border-radius: 4px;',
|
||||
placeholder: `请输入位置 (1-${totalCourses})`,
|
||||
id: 'movePositionInput'
|
||||
}),
|
||||
h('p', {
|
||||
style: 'margin-top: 8px; font-size: 12px; color: #666;'
|
||||
}, `提示:输入 1-${totalCourses} 之间的数字`)
|
||||
]),
|
||||
positiveText: '确定移动',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const input = document.getElementById('movePositionInput') as HTMLInputElement;
|
||||
const newPosition = parseInt(input.value);
|
||||
|
||||
if (isNaN(newPosition) || newPosition < 1 || newPosition > totalCourses) {
|
||||
message.error(`请输入有效的位置 (1-${totalCourses})`);
|
||||
return false; // 阻止对话框关闭
|
||||
}
|
||||
|
||||
// 执行移动操作
|
||||
const targetIndex = newPosition - 1;
|
||||
if (targetIndex !== currentIndex) {
|
||||
// 移除课程从当前位置
|
||||
const [movedCourse] = courseList.value.splice(currentIndex, 1);
|
||||
// 插入到新位置
|
||||
courseList.value.splice(targetIndex, 0, movedCourse);
|
||||
|
||||
message.success(`课程"${course.name}"已移动到第 ${newPosition} 位`);
|
||||
} else {
|
||||
message.info('位置未发生变化');
|
||||
}
|
||||
|
||||
return true; // 允许对话框关闭
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -125,10 +285,9 @@ const navigateToCreateCourse = () => {
|
||||
}
|
||||
|
||||
.top {
|
||||
padding: 20px;
|
||||
padding: 10px 20px;
|
||||
white-space: nowrap;
|
||||
/* 进一步减少左右padding */
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@ -139,94 +298,21 @@ const navigateToCreateCourse = () => {
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.top a {
|
||||
margin-right: 74px;
|
||||
display: inline-block;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top a.active {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.top a.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background-color: #0288D1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 7px 16px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-right: 24px;
|
||||
/* 增加按钮与搜索框间距 */
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
border: none;
|
||||
padding: 7px 12px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
width: 240px;
|
||||
/* 增加搜索框宽度 */
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background-color: #0288D1;
|
||||
border: none;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
transition: background-color 0.3s;
|
||||
line-height: 18px;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.course-container {
|
||||
@ -266,7 +352,7 @@ const navigateToCreateCourse = () => {
|
||||
|
||||
.course-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-5px);
|
||||
/* transform: translateY(-5px); */
|
||||
}
|
||||
|
||||
.course-image-container {
|
||||
@ -307,10 +393,12 @@ const navigateToCreateCourse = () => {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
line-height: 1;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.course-image-container img {
|
||||
@ -318,83 +406,6 @@ const navigateToCreateCourse = () => {
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
/* 添加更多选项按钮样式 */
|
||||
.more-options {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.options-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
display: none;
|
||||
z-index: 10;
|
||||
width: auto;
|
||||
min-width: 59px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.more-options:hover .options-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 8px;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
height: 24px;
|
||||
line-height: 14px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.icon-edit,
|
||||
.icon-view,
|
||||
.icon-delete,
|
||||
.icon-more {
|
||||
margin-right: 4px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.icon-view {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.icon-delete {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.icon-more {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.option-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -523,12 +534,15 @@ const navigateToCreateCourse = () => {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
:deep(.n-tabs.n-tabs--top .n-tab-pane){
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 让卡片图片和文字响应式放大 */
|
||||
|
||||
/* 超大屏幕 (≥1920px) - 图片和文字放大 */
|
||||
@media (min-width: 1920px) {
|
||||
.course-category {
|
||||
max-width: 1800px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
@ -575,7 +589,6 @@ const navigateToCreateCourse = () => {
|
||||
/* 超大屏幕 (1600px+) */
|
||||
@media (min-width: 1600px) {
|
||||
.course-category {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,17 @@
|
||||
<div class="form-container">
|
||||
<!-- 表单内容 -->
|
||||
<div class="form-content">
|
||||
<div class="header-left">
|
||||
<n-button quaternary circle size="large" @click="goBack">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<h2 class="page-title">{{ pageTitle }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 上半部分:两列布局 -->
|
||||
<div class="form-row">
|
||||
<!-- 左列 -->
|
||||
@ -163,9 +174,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, shallowRef, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { reactive, ref, shallowRef, onBeforeUnmount, onMounted, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5';
|
||||
import {
|
||||
NInput,
|
||||
NDatePicker,
|
||||
@ -179,8 +191,16 @@ import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditMode = computed(() => !!route.params.id)
|
||||
const courseId = computed(() => route.params.id as string)
|
||||
|
||||
// 页面标题
|
||||
const pageTitle = computed(() => isEditMode.value ? '编辑课程' : '创建课程')
|
||||
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef()
|
||||
|
||||
@ -225,6 +245,80 @@ const formData = reactive({
|
||||
requiredPoints: 60
|
||||
})
|
||||
|
||||
// 模拟课程数据
|
||||
const mockCourseData = {
|
||||
1: {
|
||||
courseName: '前端开发基础课程',
|
||||
courseCategory: 'frontend',
|
||||
instructors: ['李清林', '张老师'],
|
||||
sort: '1',
|
||||
startTime: new Date('2024-01-15 09:00:00').getTime(),
|
||||
endTime: new Date('2024-03-15 18:00:00').getTime(),
|
||||
studentType: 'partial',
|
||||
selectedClasses: ['frontend-class', 'fullstack-class'],
|
||||
courseDescription: '<p>这是一门全面的前端开发基础课程,涵盖HTML、CSS、JavaScript等核心技术。</p><p>课程特色:</p><ul><li>理论与实践相结合</li><li>项目驱动学习</li><li>一对一指导</li></ul>',
|
||||
stopOnLeave: true,
|
||||
videoSpeedControl: true,
|
||||
showVideoText: false,
|
||||
pointsEnabled: true,
|
||||
earnPoints: 80,
|
||||
requiredPoints: 50,
|
||||
courseCover: '/images/teacher/fj.png'
|
||||
},
|
||||
2: {
|
||||
courseName: 'Vue.js 实战教程',
|
||||
courseCategory: 'frontend',
|
||||
instructors: ['刘树光'],
|
||||
sort: '2',
|
||||
startTime: new Date('2024-02-01 10:00:00').getTime(),
|
||||
endTime: new Date('2024-04-01 17:00:00').getTime(),
|
||||
studentType: 'all',
|
||||
selectedClasses: [],
|
||||
courseDescription: '<p>深入学习Vue.js框架,掌握现代前端开发技能。</p>',
|
||||
stopOnLeave: false,
|
||||
videoSpeedControl: true,
|
||||
showVideoText: true,
|
||||
pointsEnabled: true,
|
||||
earnPoints: 100,
|
||||
requiredPoints: 60,
|
||||
courseCover: '/images/teacher/fj.png'
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程数据
|
||||
const loadCourseData = async (id: string) => {
|
||||
try {
|
||||
// 模拟API调用延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const courseData = mockCourseData[id as unknown as keyof typeof mockCourseData]
|
||||
if (courseData) {
|
||||
// 填充表单数据
|
||||
Object.assign(formData, courseData)
|
||||
|
||||
// 如果有课程封面,设置预览URL
|
||||
if (courseData.courseCover) {
|
||||
previewUrl.value = courseData.courseCover
|
||||
}
|
||||
|
||||
message.success('课程数据加载成功')
|
||||
} else {
|
||||
message.error('课程不存在')
|
||||
router.back()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载课程数据失败')
|
||||
console.error('Load course data error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时处理
|
||||
onMounted(() => {
|
||||
if (isEditMode.value && courseId.value) {
|
||||
loadCourseData(courseId.value)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 课程分类选项
|
||||
@ -340,19 +434,37 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.courseCover || !previewUrl.value) {
|
||||
if (!formData.courseCover && !previewUrl.value) {
|
||||
message.error('请上传课程封面')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('表单数据:', formData)
|
||||
message.success('课程创建成功!')
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式
|
||||
message.success('课程更新成功!')
|
||||
console.log('更新课程ID:', courseId.value)
|
||||
} else {
|
||||
// 创建模式
|
||||
message.success('课程创建成功!')
|
||||
}
|
||||
|
||||
// 返回到课程管理页面
|
||||
router.push('/teacher/course-management')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
message.error('提交失败,请重试')
|
||||
const errorMessage = isEditMode.value ? '更新失败,请重试' : '创建失败,请重试'
|
||||
message.error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -370,6 +482,32 @@ const handleSubmit = async () => {
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 100px;
|
||||
|
@ -6,30 +6,21 @@
|
||||
选择文件:
|
||||
</label>
|
||||
<div class="select-wrapper">
|
||||
<select
|
||||
v-model="selectedFolder"
|
||||
class="form-select"
|
||||
@change="handleFolderChange"
|
||||
>
|
||||
<option value="" disabled>请选择文件夹</option>
|
||||
<option
|
||||
v-for="folder in availableFolders"
|
||||
:key="folder.id"
|
||||
:value="folder.id"
|
||||
>
|
||||
{{ folder.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="select-arrow">
|
||||
<img src="/images/teacher/箭头-灰.png" alt="下拉箭头" class="arrow-icon">
|
||||
</div>
|
||||
<n-select
|
||||
v-model:value="selectedFolder"
|
||||
:options="folderOptions"
|
||||
placeholder="请选择文件夹"
|
||||
clearable
|
||||
@update:value="handleFolderChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { NSelect } from 'naive-ui'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@ -51,19 +42,27 @@ const props = defineProps({
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
// 响应式数据
|
||||
const selectedFolder = ref('')
|
||||
const selectedFolder = ref<number | null>(null)
|
||||
|
||||
// 转换文件夹数据为 n-select 组件需要的格式
|
||||
const folderOptions = computed(() => {
|
||||
return props.availableFolders.map(folder => ({
|
||||
label: folder.name,
|
||||
value: folder.id
|
||||
}))
|
||||
})
|
||||
|
||||
// 监听模态框显示状态,重置选择
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
selectedFolder.value = ''
|
||||
selectedFolder.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 监听选中的文件夹变化,实时传递给父组件
|
||||
watch(selectedFolder, (newVal) => {
|
||||
if (props.getValue && newVal) {
|
||||
const folder = props.availableFolders.find((f: { id: number }) => f.id === newVal as unknown as number)
|
||||
const folder = props.availableFolders.find((f: { id: number }) => f.id === newVal)
|
||||
if (folder) {
|
||||
props.getValue(folder)
|
||||
}
|
||||
@ -71,9 +70,9 @@ watch(selectedFolder, (newVal) => {
|
||||
})
|
||||
|
||||
// 处理文件夹选择变化
|
||||
const handleFolderChange = () => {
|
||||
if (selectedFolder.value) {
|
||||
const folder = props.availableFolders.find((f: { id: number }) => f.id === selectedFolder.value as unknown as number)
|
||||
const handleFolderChange = (value: number | null) => {
|
||||
if (value) {
|
||||
const folder = props.availableFolders.find((f: { id: number }) => f.id === value)
|
||||
if (folder) {
|
||||
emit('confirm', folder)
|
||||
}
|
||||
@ -84,7 +83,7 @@ const handleFolderChange = () => {
|
||||
defineExpose({
|
||||
getSelectedFolder: () => selectedFolder.value,
|
||||
clearSelection: () => {
|
||||
selectedFolder.value = ''
|
||||
selectedFolder.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -121,96 +120,37 @@ defineExpose({
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
/* Naive UI Select 组件样式定制 */
|
||||
:deep(.select-wrapper .n-base-selection) {
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
padding-right: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
:deep(.select-wrapper .n-base-selection:hover) {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
:deep(.select-wrapper .n-base-selection.n-base-selection--focus) {
|
||||
border-color: #0288D1;
|
||||
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
|
||||
}
|
||||
|
||||
.form-select:disabled {
|
||||
background-color: #f5f5f5;
|
||||
color: #bfbfbf;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 下拉框选项样式 */
|
||||
.form-select option {
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
:deep(.select-wrapper .n-base-selection-input) {
|
||||
color: #333;
|
||||
background: white;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-select option:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.form-select option:checked {
|
||||
background-color: #e6f7ff;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.form-select option:disabled {
|
||||
:deep(.select-wrapper .n-base-selection-placeholder) {
|
||||
color: #bfbfbf;
|
||||
background-color: #f5f5f5;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 默认提示选项样式 */
|
||||
.form-select option[value=""] {
|
||||
color: #bfbfbf;
|
||||
background-color: #fafafa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 下拉框悬停效果 */
|
||||
.form-select:hover:not(:disabled) {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 确保下拉框在不同浏览器中显示一致 */
|
||||
.form-select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
object-fit: contain;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.form-select:focus + .select-arrow .arrow-icon {
|
||||
opacity: 0.8;
|
||||
:deep(.select-wrapper .n-base-selection-label) {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
@ -48,6 +48,8 @@ import CourseEditor from '@/views/teacher/course/CourseEditor.vue'
|
||||
import CoursewareManagement from '@/views/teacher/course/CoursewareManagement.vue'
|
||||
import ChapterManagement from '@/views/teacher/course/ChapterManagement.vue'
|
||||
import QuestionBankManagement from '@/views/teacher/course/QuestionBankManagement.vue'
|
||||
import FileViewer from '@/views/teacher/course/FileViewer.vue'
|
||||
import FolderBrowser from '@/views/teacher/course/FolderBrowser.vue'
|
||||
import CertificateManagement from '@/views/teacher/certificate/CertificateManagement.vue'
|
||||
import DiscussionManagement from '@/views/teacher/course/DiscussionManagement.vue'
|
||||
import StatisticsManagement from '@/views/teacher/course/StatisticsManagement.vue'
|
||||
@ -123,7 +125,7 @@ const routes: RouteRecordRaw[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'course-create',
|
||||
path: 'course-create/:id?',
|
||||
name: 'CourseCreate',
|
||||
component: CourseCreate,
|
||||
meta: { title: '创建课程' }
|
||||
@ -265,7 +267,19 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'GeneralManagement',
|
||||
component: GeneralManagement,
|
||||
meta: { title: '综合管理' }
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'file-viewer/:fileId',
|
||||
name: 'FileViewer',
|
||||
component: FileViewer,
|
||||
meta: { title: '文件查看' }
|
||||
},
|
||||
{
|
||||
path: 'folder-browser/:folderId',
|
||||
name: 'FolderBrowser',
|
||||
component: FolderBrowser,
|
||||
meta: { title: '文件夹浏览' }
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -234,13 +234,13 @@ const columns = [
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
class: 'revoke-btn',
|
||||
ghost: true,
|
||||
onClick: () => handleRevoke(row)
|
||||
}, { default: () => '撤回' }),
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
class: 'delete-btn',
|
||||
ghost: true,
|
||||
onClick: () => handleDelete(row)
|
||||
}, { default: () => '删除' })
|
||||
])
|
||||
|
@ -1,52 +1,53 @@
|
||||
<template>
|
||||
<div class="modal-container flex-col">
|
||||
<span class="modal-title">添加课件</span>
|
||||
<h2 class="modal-title">添加课件</h2>
|
||||
<n-divider />
|
||||
<div class="upload-section flex-row">
|
||||
<div class="label-group flex-col justify-between">
|
||||
<span class="upload-path-label">上传路径:</span>
|
||||
<span class="upload-file-label">上传文件:</span>
|
||||
</div>
|
||||
<div class="button-group flex-col justify-between">
|
||||
<div class="select-path-button flex-col">
|
||||
<input
|
||||
type="file"
|
||||
id="pathUpload"
|
||||
@change="handleLocalFileUpload"
|
||||
style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
|
||||
/>
|
||||
<label for="pathUpload" class="button-text file-input-label">选择路径</label>
|
||||
</div>
|
||||
<div class="select-file-button flex-col" style="position: relative;">
|
||||
<div class="button-text file-input-label" @click="toggleResourceDropdown">选择文件</div>
|
||||
<div v-show="showResourceDropdown" class="upload-type flex-col">
|
||||
<label class="upload-type-text">
|
||||
<input
|
||||
type="file"
|
||||
@change="handleDropdownLocalUpload"
|
||||
style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
|
||||
/>
|
||||
本地上传
|
||||
</label>
|
||||
<label class="upload-type-text">
|
||||
<input
|
||||
type="file"
|
||||
@change="handleDropdownResourceUpload"
|
||||
style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
|
||||
/>
|
||||
资源上传
|
||||
</label>
|
||||
<n-popselect
|
||||
:options="folderOptions"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
v-model:value="selectedFolder"
|
||||
@update:value="handleFolderSelect"
|
||||
>
|
||||
<div class="select-path-button flex-col">
|
||||
<span class="button-text file-input-label">选择路径</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-popselect>
|
||||
<n-popselect
|
||||
:options="uploadOptions"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
@update:value="handleUploadOptionSelect"
|
||||
:render-label="renderUploadOption"
|
||||
>
|
||||
<div class="select-file-button flex-col">
|
||||
<span class="button-text file-input-label">选择文件</span>
|
||||
</div>
|
||||
</n-popselect>
|
||||
<!-- 隐藏的文件输入框 -->
|
||||
<input
|
||||
ref="localFileInput"
|
||||
type="file"
|
||||
@change="handleLocalFileUpload"
|
||||
style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
|
||||
/>
|
||||
<input
|
||||
ref="resourceFileInput"
|
||||
type="file"
|
||||
@change="handleResourceFileUpload"
|
||||
style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4"
|
||||
/>
|
||||
</div>
|
||||
<div class="folder-display flex-col justify-between">
|
||||
<span class="folder-name">文件夹一</span>
|
||||
<div class="existing-folders flex-col">
|
||||
<span class="folder-item">已由文件夹名称一</span>
|
||||
<span class="folder-item">已由文件夹名称二</span>
|
||||
</div>
|
||||
<span class="folder-name">{{ getSelectedFolderName() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="supported-formats flex-row">
|
||||
@ -74,10 +75,71 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, h } from 'vue'
|
||||
|
||||
// 控制资源上传下拉菜单的显示
|
||||
const showResourceDropdown = ref(false)
|
||||
// 选中的文件夹
|
||||
const selectedFolder = ref<string>('')
|
||||
|
||||
// 上传选项数据
|
||||
const uploadOptions = ref([
|
||||
{ label: '本地上传', value: 'local' },
|
||||
{ label: '资源上传', value: 'resource' }
|
||||
])
|
||||
|
||||
// 文件输入框引用
|
||||
const localFileInput = ref<HTMLInputElement>()
|
||||
const resourceFileInput = ref<HTMLInputElement>()
|
||||
|
||||
// 文件夹选项模拟数据
|
||||
const folderOptions = ref([
|
||||
{ label: '文件夹一', value: 'folder1' },
|
||||
{ label: '文件夹二', value: 'folder2' },
|
||||
{ label: '文件夹三', value: 'folder3' },
|
||||
{ label: '文档资料', value: 'documents' },
|
||||
{ label: '视频课件', value: 'videos' },
|
||||
{ label: '音频文件', value: 'audios' },
|
||||
{ label: '演示文稿', value: 'presentations' },
|
||||
{ label: '其他资源', value: 'others' }
|
||||
])
|
||||
|
||||
// 渲染上传选项标签
|
||||
const renderUploadOption = (option: any) => {
|
||||
return h('span', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '5px 0'
|
||||
}
|
||||
}, option.label)
|
||||
}
|
||||
|
||||
// 处理上传选项选择
|
||||
const handleUploadOptionSelect = (value: string) => {
|
||||
console.log('选中的上传方式:', value)
|
||||
if (value === 'local') {
|
||||
// 触发本地上传文件选择
|
||||
localFileInput.value?.click()
|
||||
} else if (value === 'resource') {
|
||||
// 触发资源上传文件选择
|
||||
resourceFileInput.value?.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中文件夹的名称
|
||||
const getSelectedFolderName = () => {
|
||||
if (!selectedFolder.value) {
|
||||
return '请选择文件夹'
|
||||
}
|
||||
const folder = folderOptions.value.find(option => option.value === selectedFolder.value)
|
||||
return folder ? folder.label : '未知文件夹'
|
||||
}
|
||||
|
||||
// 处理文件夹选择
|
||||
const handleFolderSelect = (value: string) => {
|
||||
console.log('选中的文件夹:', value)
|
||||
// 这里可以添加文件夹选择后的逻辑
|
||||
}
|
||||
|
||||
// 处理本地上传文件选择
|
||||
const handleLocalFileUpload = (event: Event) => {
|
||||
@ -99,23 +161,6 @@ const handleResourceFileUpload = (event: Event) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换资源上传下拉菜单
|
||||
const toggleResourceDropdown = () => {
|
||||
showResourceDropdown.value = !showResourceDropdown.value
|
||||
}
|
||||
|
||||
// 处理下拉选项中的本地上传
|
||||
const handleDropdownLocalUpload = (event: Event) => {
|
||||
handleLocalFileUpload(event)
|
||||
showResourceDropdown.value = false
|
||||
}
|
||||
|
||||
// 处理下拉选项中的资源上传
|
||||
const handleDropdownResourceUpload = (event: Event) => {
|
||||
handleResourceFileUpload(event)
|
||||
showResourceDropdown.value = false
|
||||
}
|
||||
|
||||
// 定义事件发射器
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
@ -163,7 +208,8 @@ const handleConfirm = () => {
|
||||
height: 160px;
|
||||
background: #FCFCFC ;
|
||||
background-size: 100% 100%;
|
||||
margin: 46px 0 0 22px;
|
||||
border: 1px solid rgb(233, 233, 233);
|
||||
margin: 0 0 0 22px;
|
||||
}
|
||||
|
||||
.label-group {
|
||||
@ -453,41 +499,7 @@ const handleConfirm = () => {
|
||||
background: #0277BD;
|
||||
}
|
||||
|
||||
.upload-type {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
width: 123px;
|
||||
height: 76px;
|
||||
background: #FFFFFF;
|
||||
background-size: 241px 194px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
.upload-type-text {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
font-size: 14px;
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
line-height: 30px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.upload-type-text:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 文件输入框标签样式 */
|
||||
.file-input-label {
|
||||
|
@ -4,6 +4,13 @@
|
||||
<!-- 表单内容 -->
|
||||
<div class="form-content">
|
||||
<!-- 上半部分:两列布局 -->
|
||||
<n-button quaternary circle size="large" @click="handleCancel" class="back-button">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="form-row">
|
||||
<!-- 左列 -->
|
||||
<div class="form-column">
|
||||
@ -187,6 +194,7 @@ import {
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
// @ts-ignore
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -307,8 +315,6 @@ const creatorOptions = [
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
router.back()
|
||||
|
@ -5,6 +5,7 @@
|
||||
'tablet-layout': isTablet,
|
||||
'sidebar-collapsed': sidebarCollapsed
|
||||
}">
|
||||
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<n-button v-if="isMobile" class="mobile-menu-toggle" quaternary @click="toggleSidebar">
|
||||
<span class="menu-icon">☰</span>
|
||||
@ -19,6 +20,14 @@
|
||||
<span class="close-icon">×</span>
|
||||
</n-button>
|
||||
|
||||
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
|
||||
<n-button class="header-section flex-row justify-between" type="primary">
|
||||
<template #icon>
|
||||
<img class="chapter-icon" referrerpolicy="no-referrer" src="/images/teacher/加号_4.png" />
|
||||
@ -216,6 +225,10 @@ import CustomDropdown from '@/components/CustomDropdown.vue';
|
||||
import HomeworkDropdown from '@/components/HomeworkDropdown.vue';
|
||||
import ExamPaperLibraryModal from '@/components/ExamPaperLibraryModal.vue';
|
||||
import HomeworkLibraryModal from '@/components/HomeworkLibraryModal.vue';
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 控制菜单显示状态的响应式变量
|
||||
const isMenuVisible = ref(false);
|
||||
@ -544,6 +557,10 @@ const removeLessonSection = (sectionId: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
}
|
||||
|
||||
// 删除未使用的方法
|
||||
</script>
|
||||
|
||||
@ -565,6 +582,10 @@ const removeLessonSection = (sectionId: number) => {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.back-button{
|
||||
margin: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
:deep(.n-button.mobile-close) {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
@ -579,8 +600,6 @@ const removeLessonSection = (sectionId: number) => {
|
||||
}
|
||||
|
||||
:deep(.n-button.header-section) {
|
||||
width: 194px !important;
|
||||
height: 48px !important;
|
||||
background-color: #0288D1 !important;
|
||||
border-radius: 4px !important;
|
||||
margin: 31px 0 0 40px !important;
|
||||
|
@ -4,24 +4,29 @@
|
||||
<div class="toolbar">
|
||||
<h2>全部章节</h2>
|
||||
<div class="toolbar-actions">
|
||||
<button class="btn btn-primary" @click="addChapter">添加章节</button>
|
||||
<button class="btn btn-new" @click="importChapters">导入</button>
|
||||
<button class="btn btn-new" @click="exportChapters">导出</button>
|
||||
<button class="btn btn-danger" @click="deleteSelected" :disabled="selectedChapters.length === 0">删除</button>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入想要搜索的内容" v-model="searchKeyword" />
|
||||
<button class="btn btn-search" @click="searchChapters">搜索</button>
|
||||
</div>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="addChapter">添加章节</n-button>
|
||||
<n-button @click="importChapters">导入</n-button>
|
||||
<n-button @click="exportChapters">导出</n-button>
|
||||
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
|
||||
<div class="search-container">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="请输入想要搜索的内容"
|
||||
style="width: 200px;"
|
||||
>
|
||||
</n-input>
|
||||
<n-button type="primary" @click="searchChapters">搜索</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 章节列表表格 -->
|
||||
<div class="table-box">
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey"
|
||||
:checked-row-keys="selectedChapters" @update:checked-row-keys="handleCheck" :bordered="false"
|
||||
:single-line="false" size="medium" class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" />
|
||||
</n-config-provider>
|
||||
|
||||
<!-- 自定义分页器 -->
|
||||
<div class="custom-pagination">
|
||||
@ -51,7 +56,7 @@
|
||||
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
|
||||
@click="goToPage('last')">
|
||||
尾页
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,7 +66,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { NButton, useMessage, NDataTable, NConfigProvider, zhCN, dateZhCN } from 'naive-ui'
|
||||
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
@ -503,10 +508,9 @@ const columns: DataTableColumns<Chapter> = [
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chapter-management {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
.chapter-management {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -574,9 +578,6 @@ const columns: DataTableColumns<Chapter> = [
|
||||
border: 1px solid #FF4D4F;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #ff7875;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
@ -779,7 +780,7 @@ const columns: DataTableColumns<Chapter> = [
|
||||
}
|
||||
|
||||
.page-number:hover:not(.disabled) {
|
||||
color: #0088D1;
|
||||
/* color: #0088D1; */
|
||||
border-color: #0088D1;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@
|
||||
<div class="menu-group">
|
||||
<div class="menu-header" @click="togglePractice">
|
||||
<img src="/images/teacher/练考通.png" alt="练考通" />
|
||||
<span>练考通</span>
|
||||
<span>考试管理</span>
|
||||
<i class="n-base-icon" :class="{ expanded: practiceExpanded }">
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -55,7 +55,7 @@
|
||||
<div class="submenu" v-show="practiceExpanded">
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/practice/exam`" class="submenu-item"
|
||||
:class="{ active: $route.path.includes('/practice/exam') }">
|
||||
<span>试卷</span>
|
||||
<span>试卷管理</span>
|
||||
</router-link>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/practice/review`" class="submenu-item"
|
||||
:class="{ active: $route.path.includes('/practice/review') }">
|
||||
@ -63,12 +63,12 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="menu-item"
|
||||
<!-- <router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('question-bank') }">
|
||||
<img :src="$route.path.includes('question-bank') ? '/images/teacher/题库-选中.png' : '/images/teacher/题库.png'"
|
||||
alt="题库" />
|
||||
<span>题库</span>
|
||||
</router-link>
|
||||
</router-link> -->
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/certificate`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('certificate') }">
|
||||
<img :src="$route.path.includes('certificate') ? '/images/teacher/证书-选中.png' : '/images/teacher/证书.png'"
|
||||
@ -87,12 +87,12 @@
|
||||
alt="统计" />
|
||||
<span>统计</span>
|
||||
</router-link>
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/notification`" class="menu-item"
|
||||
<!-- <router-link :to="`/teacher/course-editor/${courseId}/notification`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('notification') }">
|
||||
<img :src="$route.path.includes('notification') ? '/images/teacher/通知-选中.png' : '/images/teacher/通知.png'"
|
||||
alt="通知" />
|
||||
<span>通知</span>
|
||||
</router-link>
|
||||
</router-link> -->
|
||||
<router-link :to="`/teacher/course-editor/${courseId}/management`" class="menu-item"
|
||||
:class="{ active: $route.path.includes('management') }">
|
||||
<img :src="$route.path.includes('management') ? '/images/teacher/管理-选中.png' : '/images/teacher/管理.png'"
|
||||
@ -285,9 +285,6 @@ const hideSidebar = computed(() => {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.submenu {
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
display: block;
|
||||
padding: 12px 15px;
|
||||
|
@ -19,11 +19,11 @@
|
||||
|
||||
<!-- 文件列表表格 -->
|
||||
<div class="table-box">
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-data-table :columns="columns" :data="paginatedFiles" :row-key="rowKey" :checked-row-keys="selectedFiles"
|
||||
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
|
||||
class="file-data-table" :row-class-name="rowClassName" style="width: 100%" />
|
||||
</n-config-provider>
|
||||
<div style="flex: 1;">
|
||||
<n-data-table :columns="columns" :data="paginatedFiles" :row-key="rowKey" :checked-row-keys="selectedFiles"
|
||||
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
|
||||
class="file-data-table" :row-class-name="rowClassName" style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- 自定义分页器 -->
|
||||
<div class="custom-pagination">
|
||||
@ -60,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 添加课件模态框 -->
|
||||
<div v-if="showAddCoursewareModal" class="modal-overlay" @click="closeAddCoursewareModal">
|
||||
<div v-if="showAddCoursewareModal" class="modal-overlay">
|
||||
<AddCoursewareModal @close="closeAddCoursewareModal" />
|
||||
</div>
|
||||
|
||||
@ -88,12 +88,28 @@
|
||||
:get-value="getValue" />
|
||||
</template>
|
||||
</CommonModal>
|
||||
|
||||
<!-- 重命名模态框 -->
|
||||
<CommonModal :visible="showRenameModal" title="重命名" @close="closeRenameModal" @confirm="handleRename">
|
||||
<template #content>
|
||||
<div class="rename-content">
|
||||
<n-input
|
||||
v-model:value="renameInputValue"
|
||||
placeholder="请输入新的文件名"
|
||||
style="width: 100%"
|
||||
ref="renameInputRef"
|
||||
@keyup.enter="handleRename"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</CommonModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h, onMounted, onUnmounted } from 'vue'
|
||||
import { NButton, NDropdown, NTag, useMessage, NDataTable, NConfigProvider, zhCN, dateZhCN } from 'naive-ui'
|
||||
import { ref, computed, h, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { NButton, NDropdown, NTag, NInput, useMessage, NDataTable } from 'naive-ui'
|
||||
import type { DataTableColumns, DropdownOption } from 'naive-ui'
|
||||
import AddCoursewareModal from './AddCoursewareModal.vue'
|
||||
import UploadFileModal from './UploadFileModal.vue'
|
||||
@ -103,6 +119,8 @@ import CreateFolderContent from '@/components/common/CreateFolderContent.vue'
|
||||
import MoveFileContent from '@/components/common/MoveFileContent.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 文件类型定义
|
||||
interface FileItem {
|
||||
@ -129,10 +147,19 @@ const showUploadFileModal = ref(false)
|
||||
const showDeleteConfirmModal = ref(false)
|
||||
const showCreateFolderModal = ref(false)
|
||||
const showMoveFileModal = ref(false)
|
||||
const showRenameModal = ref(false)
|
||||
|
||||
// 存储待删除的项目信息
|
||||
const itemsToDelete = ref<{ type: 'single' | 'multiple', data: any }>({ type: 'single', data: null })
|
||||
|
||||
// 存储当前要重命名的文件
|
||||
const currentRenameFile = ref<FileItem | null>(null)
|
||||
// 重命名输入值
|
||||
const renameInputValue = ref('')
|
||||
|
||||
// 重命名输入框引用
|
||||
const renameInputRef = ref<InstanceType<typeof NInput> | null>(null)
|
||||
|
||||
// 存储当前要上传文件的目标文件夹
|
||||
const currentUploadTarget = ref<FileItem | null>(null)
|
||||
|
||||
@ -700,7 +727,25 @@ const closeUploadFileModal = () => {
|
||||
}
|
||||
|
||||
const viewFile = (file: FileItem) => {
|
||||
message.info('查看文件: ' + file.name)
|
||||
if (file.type === 'folder') {
|
||||
// 跳转到文件夹浏览页面
|
||||
router.push({
|
||||
name: 'FolderBrowser',
|
||||
params: {
|
||||
id: route.params.id, // 课程ID
|
||||
folderId: file.id.toString()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 跳转到单个文件查看页面
|
||||
router.push({
|
||||
name: 'FileViewer',
|
||||
params: {
|
||||
id: route.params.id, // 课程ID
|
||||
fileId: file.id.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFile = (file: FileItem) => {
|
||||
@ -745,11 +790,66 @@ const cancelDelete = () => {
|
||||
}
|
||||
|
||||
const renameFile = (file: FileItem) => {
|
||||
const newName = prompt('请输入新的文件名:', file.name)
|
||||
if (newName && newName !== file.name) {
|
||||
file.name = newName
|
||||
message.success('重命名成功')
|
||||
// 存储当前要重命名的文件
|
||||
currentRenameFile.value = file
|
||||
// 设置输入框的初始值
|
||||
renameInputValue.value = file.name
|
||||
showRenameModal.value = true
|
||||
|
||||
// 使用 nextTick 确保模态框渲染后再聚焦输入框
|
||||
nextTick(() => {
|
||||
renameInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭重命名模态框
|
||||
const closeRenameModal = () => {
|
||||
showRenameModal.value = false
|
||||
currentRenameFile.value = null
|
||||
renameInputValue.value = ''
|
||||
}
|
||||
|
||||
// 处理重命名
|
||||
const handleRename = () => {
|
||||
if (!currentRenameFile.value || !renameInputValue.value.trim()) {
|
||||
message.error('文件名不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
const newName = renameInputValue.value.trim()
|
||||
const oldName = currentRenameFile.value.name
|
||||
|
||||
// 检查名称是否有变化
|
||||
if (newName === oldName) {
|
||||
message.info('文件名未发生变化')
|
||||
closeRenameModal()
|
||||
return
|
||||
}
|
||||
|
||||
// 查找原文件并更新名称
|
||||
const originalFile = findFileById(fileList.value, currentRenameFile.value.id)
|
||||
if (originalFile) {
|
||||
originalFile.name = newName
|
||||
message.success(`文件重命名成功:"${oldName}" → "${newName}"`)
|
||||
} else {
|
||||
message.error('未找到要重命名的文件')
|
||||
}
|
||||
|
||||
closeRenameModal()
|
||||
}
|
||||
|
||||
// 递归查找文件
|
||||
const findFileById = (files: FileItem[], id: number): FileItem | null => {
|
||||
for (const file of files) {
|
||||
if (file.id === id) {
|
||||
return file
|
||||
}
|
||||
if (file.children) {
|
||||
const found = findFileById(file.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 递归查找文件在树形结构中的位置
|
||||
@ -829,8 +929,10 @@ const toggleFolder = (folder: FileItem) => {
|
||||
.courseware-management {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
@ -907,8 +1009,8 @@ const toggleFolder = (folder: FileItem) => {
|
||||
|
||||
.btn-default:disabled,
|
||||
.btn-danger:disabled {
|
||||
/* opacity: 0.5; */
|
||||
/* cursor: not-allowed; */
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 模态框遮罩层样式 */
|
||||
@ -925,6 +1027,11 @@ const toggleFolder = (folder: FileItem) => {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 重命名模态框内容样式 */
|
||||
.rename-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -959,6 +1066,7 @@ const toggleFolder = (folder: FileItem) => {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Naive UI 表格样式定制 */
|
||||
@ -1297,7 +1405,6 @@ const toggleFolder = (folder: FileItem) => {
|
||||
}
|
||||
|
||||
.page-number:hover:not(.disabled) {
|
||||
color: #0088D1;
|
||||
border-color: #0088D1;
|
||||
}
|
||||
|
||||
|
800
src/views/teacher/course/FileViewer.vue
Normal file
800
src/views/teacher/course/FileViewer.vue
Normal file
@ -0,0 +1,800 @@
|
||||
<template>
|
||||
<div class="file-viewer">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="header">
|
||||
<div class="nav-left">
|
||||
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="breadcrumb">
|
||||
<span
|
||||
v-for="(crumb, index) in breadcrumbs"
|
||||
:key="crumb.id"
|
||||
class="breadcrumb-item"
|
||||
@click="navigateTo(crumb)"
|
||||
>
|
||||
{{ crumb.name }}
|
||||
<span v-if="index < breadcrumbs.length - 1" class="separator">></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="btn btn-primary" @click="downloadFile" v-if="currentFile && currentFile.type !== 'folder'">
|
||||
下载文件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="content">
|
||||
<!-- 文件夹视图 -->
|
||||
<div v-if="currentFile && currentFile.type === 'folder'" class="folder-view">
|
||||
<div class="folder-header">
|
||||
<h3>{{ currentFile.name }}</h3>
|
||||
<div class="folder-info">
|
||||
<span>创建时间:{{ currentFile.createTime }}</span>
|
||||
<span>创建人:{{ currentFile.creator }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件夹内容列表 -->
|
||||
<div class="folder-content">
|
||||
<div class="file-grid">
|
||||
<div
|
||||
v-for="item in folderItems"
|
||||
:key="item.id"
|
||||
class="file-item"
|
||||
@click="viewItem(item)"
|
||||
@dblclick="openItem(item)"
|
||||
>
|
||||
<div class="file-icon">
|
||||
<img :src="getFileIcon(item.type)" :alt="item.type" />
|
||||
<div v-if="item.isTop" class="top-badge">置顶</div>
|
||||
</div>
|
||||
<div class="file-name" :title="item.name">{{ item.name }}</div>
|
||||
<div class="file-meta">
|
||||
<span class="file-size">{{ item.size }}</span>
|
||||
<span class="file-time">{{ formatTime(item.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="folderItems.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📁</div>
|
||||
<p>该文件夹暂无内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件预览视图 -->
|
||||
<div v-else-if="currentFile" class="file-preview">
|
||||
<div class="file-header">
|
||||
<div class="file-info">
|
||||
<img :src="getFileIcon(currentFile.type)" :alt="currentFile.type" class="file-type-icon" />
|
||||
<div class="file-details">
|
||||
<h3>{{ currentFile.name }}</h3>
|
||||
<div class="file-meta">
|
||||
<span>文件大小:{{ currentFile.size }}</span>
|
||||
<span>创建时间:{{ currentFile.createTime }}</span>
|
||||
<span>创建人:{{ currentFile.creator }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件预览内容 -->
|
||||
<div class="preview-content">
|
||||
<!-- 图片预览 -->
|
||||
<div v-if="isImage(currentFile.type)" class="image-preview">
|
||||
<img :src="getPreviewUrl(currentFile)" :alt="currentFile.name" />
|
||||
</div>
|
||||
|
||||
<!-- 视频预览 -->
|
||||
<div v-else-if="isVideo(currentFile.type)" class="video-preview">
|
||||
<video controls :src="getPreviewUrl(currentFile)">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- PDF预览 -->
|
||||
<div v-else-if="isPdf(currentFile.type)" class="pdf-preview">
|
||||
<iframe :src="getPreviewUrl(currentFile)" frameborder="0"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- 文档预览 -->
|
||||
<div v-else-if="isDocument(currentFile.type)" class="document-preview">
|
||||
<div class="document-placeholder">
|
||||
<div class="file-preview-icon">
|
||||
<img :src="getFileIcon(currentFile.type)" :alt="currentFile.type" />
|
||||
</div>
|
||||
<div class="file-preview-info">
|
||||
<h4>{{ currentFile.name }}</h4>
|
||||
<p class="file-description">该文件类型暂不支持在线预览</p>
|
||||
<n-button type="primary" @click="downloadFile">下载查看</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他文件类型 -->
|
||||
<div v-else class="unsupported-preview">
|
||||
<div class="unsupported-placeholder">
|
||||
<div class="file-preview-icon">
|
||||
<img :src="getFileIcon(currentFile.type)" :alt="currentFile.type" />
|
||||
</div>
|
||||
<div class="file-preview-info">
|
||||
<h4>{{ currentFile.name }}</h4>
|
||||
<p class="file-description">该文件类型暂不支持预览</p>
|
||||
<n-button type="primary" @click="downloadFile">下载查看</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-else class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
// 文件类型定义
|
||||
interface FileItem {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
size: string
|
||||
creator: string
|
||||
createTime: string
|
||||
isTop: boolean
|
||||
children?: FileItem[]
|
||||
parentId?: number
|
||||
}
|
||||
|
||||
// 当前查看的文件/文件夹
|
||||
const currentFile = ref<FileItem | null>(null)
|
||||
// 面包屑导航
|
||||
const breadcrumbs = ref<FileItem[]>([])
|
||||
// 文件夹内容
|
||||
const folderItems = ref<FileItem[]>([])
|
||||
|
||||
// 模拟文件数据(实际项目中应该从API获取)
|
||||
const mockFileData: FileItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '教学资料文件夹',
|
||||
type: 'folder',
|
||||
size: '1MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: true,
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: '课程大纲.xlsx',
|
||||
type: 'excel',
|
||||
size: '1MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '教学计划.docx',
|
||||
type: 'word',
|
||||
size: '2MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '教学PPT.pptx',
|
||||
type: 'ppt',
|
||||
size: '5MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '教学视频.mp4',
|
||||
type: 'video',
|
||||
size: '100MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '学习指南.pdf',
|
||||
type: 'pdf',
|
||||
size: '3MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (type: string) => {
|
||||
const iconMap: { [key: string]: string } = {
|
||||
folder: '/images/teacher/folder.jpg',
|
||||
excel: '/images/activity/xls.png',
|
||||
word: '/images/activity/wrod.png',
|
||||
pdf: '/images/activity/pdf.png',
|
||||
ppt: '/images/activity/ppt.png',
|
||||
video: '/images/activity/file.png',
|
||||
image: '/images/activity/image.png'
|
||||
}
|
||||
return iconMap[type] || '/images/activity/file.png'
|
||||
}
|
||||
|
||||
// 判断文件类型
|
||||
const isImage = (type: string) => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'image'].includes(type)
|
||||
const isVideo = (type: string) => ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'video'].includes(type)
|
||||
const isPdf = (type: string) => type === 'pdf'
|
||||
const isDocument = (type: string) => ['word', 'excel', 'ppt', 'txt'].includes(type)
|
||||
|
||||
// 获取预览URL(实际项目中应该是真实的文件URL)
|
||||
const getPreviewUrl = (file: FileItem) => {
|
||||
// 这里应该返回实际的文件预览URL
|
||||
return `/api/files/preview/${file.id}`
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string) => {
|
||||
return timeStr.replace(/\./g, '-')
|
||||
}
|
||||
|
||||
// 根据ID查找文件
|
||||
const findFileById = (files: FileItem[], id: number): FileItem | null => {
|
||||
for (const file of files) {
|
||||
if (file.id === id) {
|
||||
return file
|
||||
}
|
||||
if (file.children) {
|
||||
const found = findFileById(file.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 构建面包屑导航
|
||||
const buildBreadcrumbs = (file: FileItem) => {
|
||||
const crumbs: FileItem[] = []
|
||||
let current = file
|
||||
|
||||
while (current) {
|
||||
crumbs.unshift(current)
|
||||
if (current.parentId) {
|
||||
current = findFileById(mockFileData, current.parentId) as FileItem
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbs.value = crumbs
|
||||
}
|
||||
|
||||
// 加载文件数据
|
||||
const loadFile = (fileId: number) => {
|
||||
const file = findFileById(mockFileData, fileId)
|
||||
if (file) {
|
||||
currentFile.value = file
|
||||
buildBreadcrumbs(file)
|
||||
|
||||
if (file.type === 'folder' && file.children) {
|
||||
// 对文件夹内容进行排序:置顶文件在前
|
||||
folderItems.value = [...file.children].sort((a, b) => {
|
||||
if (a.isTop && !b.isTop) return -1
|
||||
if (!a.isTop && b.isTop) return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message.error('文件不存在')
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
|
||||
// 查看文件/文件夹项目
|
||||
const viewItem = (_item: FileItem) => {
|
||||
// 选中效果等功能待实现
|
||||
|
||||
}
|
||||
|
||||
// 打开文件/文件夹
|
||||
const openItem = (item: FileItem) => {
|
||||
router.push({
|
||||
name: 'FileViewer',
|
||||
params: { fileId: item.id.toString() }
|
||||
})
|
||||
}
|
||||
|
||||
// 导航到面包屑位置
|
||||
const navigateTo = (crumb: FileItem) => {
|
||||
if (crumb.id !== currentFile.value?.id) {
|
||||
router.push({
|
||||
name: 'FileViewer',
|
||||
params: { fileId: crumb.id.toString() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = () => {
|
||||
if (currentFile.value) {
|
||||
// 这里实现文件下载逻辑
|
||||
message.success(`开始下载:${currentFile.value.name}`)
|
||||
|
||||
// 模拟下载
|
||||
const link = document.createElement('a')
|
||||
link.href = getPreviewUrl(currentFile.value)
|
||||
link.download = currentFile.value.name
|
||||
link.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
const fileId = parseInt(route.params.fileId as string)
|
||||
if (fileId) {
|
||||
loadFile(fileId)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听路由参数变化
|
||||
const unwatchRoute = router.afterEach(() => {
|
||||
const fileId = parseInt(route.params.fileId as string)
|
||||
if (fileId) {
|
||||
loadFile(fileId)
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理监听器
|
||||
onUnmounted(() => {
|
||||
unwatchRoute()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #e6f7ff;
|
||||
border-color: #0288d1;
|
||||
color: #0288d1;
|
||||
}
|
||||
|
||||
.back-btn img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #0288d1;
|
||||
}
|
||||
|
||||
.breadcrumb-item:last-child {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 8px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.nav-right .btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0277bd;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 文件夹视图 */
|
||||
.folder-view {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.folder-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.folder-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.folder-info {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.folder-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: #0288d1;
|
||||
box-shadow: 0 2px 8px rgba(2, 136, 209, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.file-icon img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.top-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 文件预览视图 */
|
||||
.file-preview {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.file-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-type-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.file-details h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-details .file-meta {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
background: #f5f6fa;
|
||||
border-radius: 8px;
|
||||
padding: 32px 24px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 图片预览 */
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 600px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 视频预览 */
|
||||
.video-preview video {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* PDF预览 */
|
||||
.pdf-preview iframe {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 文档预览占位 */
|
||||
.document-preview,
|
||||
.unsupported-preview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.document-placeholder,
|
||||
.unsupported-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
padding: 40px 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.file-preview-icon {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-preview-icon img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.file-preview-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-preview-info h4 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.file-description {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #0288d1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.folder-info {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-details .file-meta {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.document-placeholder,
|
||||
.unsupported-placeholder {
|
||||
max-width: 100%;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.file-preview-icon {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-preview-icon img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.file-preview-info h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
630
src/views/teacher/course/FolderBrowser.vue
Normal file
630
src/views/teacher/course/FolderBrowser.vue
Normal file
@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div class="folder-browser">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="header">
|
||||
<div class="nav-left">
|
||||
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="breadcrumb">
|
||||
<span
|
||||
v-for="(crumb, index) in breadcrumbs"
|
||||
:key="crumb.id"
|
||||
class="breadcrumb-item"
|
||||
@click="navigateTo(crumb)"
|
||||
>
|
||||
{{ crumb.name }}
|
||||
<span v-if="index < breadcrumbs.length - 1" class="separator">></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<n-button type="primary" @click="addFile">
|
||||
添加文件
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="content">
|
||||
<div class="folder-view">
|
||||
<div class="folder-header">
|
||||
<h3>{{ currentFolder?.name || '文件夹' }}</h3>
|
||||
<div class="folder-info" v-if="currentFolder">
|
||||
<span>创建时间:{{ currentFolder.createTime }}</span>
|
||||
<span>创建人:{{ currentFolder.creator }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件夹内容列表 -->
|
||||
<div class="folder-content">
|
||||
<div class="file-grid">
|
||||
<div
|
||||
v-for="item in folderItems"
|
||||
:key="item.id"
|
||||
class="file-item"
|
||||
@click="selectItem(item)"
|
||||
@dblclick="openItem(item)"
|
||||
:class="{ 'selected': selectedItem?.id === item.id }"
|
||||
>
|
||||
<div class="file-icon">
|
||||
<img :src="getFileIcon(item.type)" :alt="item.type" />
|
||||
<div v-if="item.isTop" class="top-badge">置顶</div>
|
||||
</div>
|
||||
<div class="file-name" :title="item.name">{{ item.name }}</div>
|
||||
<div class="file-meta">
|
||||
<span class="file-size">{{ item.size }}</span>
|
||||
<span class="file-time">{{ formatTime(item.createTime) }}</span>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<n-button @click.stop="viewItem(item)">查看</n-button>
|
||||
<n-button @click.stop="downloadItem(item)" v-if="item.type !== 'folder'">下载</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="folderItems.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📁</div>
|
||||
<p>该文件夹暂无内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
// 文件类型定义
|
||||
interface FileItem {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
size: string
|
||||
creator: string
|
||||
createTime: string
|
||||
isTop: boolean
|
||||
children?: FileItem[]
|
||||
parentId?: number
|
||||
}
|
||||
|
||||
// 当前文件夹
|
||||
const currentFolder = ref<FileItem | null>(null)
|
||||
// 面包屑导航
|
||||
const breadcrumbs = ref<FileItem[]>([])
|
||||
// 文件夹内容
|
||||
const folderItems = ref<FileItem[]>([])
|
||||
// 选中的项目
|
||||
const selectedItem = ref<FileItem | null>(null)
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 模拟文件数据
|
||||
const mockFileData: FileItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '教学资料文件夹',
|
||||
type: 'folder',
|
||||
size: '1MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: true,
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: '课程大纲.xlsx',
|
||||
type: 'excel',
|
||||
size: '1MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '教学计划.docx',
|
||||
type: 'word',
|
||||
size: '2MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 09:20',
|
||||
isTop: false,
|
||||
parentId: 1
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '子文件夹',
|
||||
type: 'folder',
|
||||
size: '0B',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 10:30',
|
||||
isTop: false,
|
||||
parentId: 1,
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: '深层文件.pdf',
|
||||
type: 'pdf',
|
||||
size: '3MB',
|
||||
creator: '王建国',
|
||||
createTime: '2025.07.25 11:00',
|
||||
isTop: false,
|
||||
parentId: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (type: string) => {
|
||||
const iconMap: { [key: string]: string } = {
|
||||
folder: '/images/teacher/folder.jpg',
|
||||
excel: '/images/activity/xls.png',
|
||||
word: '/images/activity/wrod.png',
|
||||
pdf: '/images/activity/pdf.png',
|
||||
ppt: '/images/activity/ppt.png',
|
||||
video: '/images/activity/file.png',
|
||||
image: '/images/activity/image.png'
|
||||
}
|
||||
return iconMap[type] || '/images/activity/file.png'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string) => {
|
||||
return timeStr.replace(/\./g, '-')
|
||||
}
|
||||
|
||||
// 根据ID查找文件
|
||||
const findFileById = (files: FileItem[], id: number): FileItem | null => {
|
||||
for (const file of files) {
|
||||
if (file.id === id) {
|
||||
return file
|
||||
}
|
||||
if (file.children) {
|
||||
const found = findFileById(file.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 构建面包屑导航
|
||||
const buildBreadcrumbs = (folder: FileItem) => {
|
||||
const crumbs: FileItem[] = []
|
||||
let current = folder
|
||||
|
||||
while (current) {
|
||||
crumbs.unshift(current)
|
||||
if (current.parentId) {
|
||||
current = findFileById(mockFileData, current.parentId) as FileItem
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbs.value = crumbs
|
||||
}
|
||||
|
||||
// 加载文件夹数据
|
||||
const loadFolder = (folderId: number) => {
|
||||
loading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
const folder = findFileById(mockFileData, folderId)
|
||||
if (folder && folder.type === 'folder') {
|
||||
currentFolder.value = folder
|
||||
buildBreadcrumbs(folder)
|
||||
|
||||
if (folder.children) {
|
||||
// 对文件夹内容进行排序:置顶文件在前
|
||||
folderItems.value = [...folder.children].sort((a, b) => {
|
||||
if (a.isTop && !b.isTop) return -1
|
||||
if (!a.isTop && b.isTop) return 1
|
||||
return 0
|
||||
})
|
||||
} else {
|
||||
folderItems.value = []
|
||||
}
|
||||
} else {
|
||||
message.error('文件夹不存在或不是有效的文件夹')
|
||||
router.back()
|
||||
}
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 选中项目
|
||||
const selectItem = (item: FileItem) => {
|
||||
selectedItem.value = item
|
||||
}
|
||||
|
||||
// 打开项目(双击)
|
||||
const openItem = (item: FileItem) => {
|
||||
if (item.type === 'folder') {
|
||||
// 跳转到子文件夹
|
||||
router.push({
|
||||
name: 'FolderBrowser',
|
||||
params: {
|
||||
id: route.params.id,
|
||||
folderId: item.id.toString()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 跳转到文件查看页面
|
||||
router.push({
|
||||
name: 'FileViewer',
|
||||
params: {
|
||||
id: route.params.id,
|
||||
fileId: item.id.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 查看项目
|
||||
const viewItem = (item: FileItem) => {
|
||||
if (item.type === 'folder') {
|
||||
openItem(item)
|
||||
} else {
|
||||
// 跳转到文件查看页面
|
||||
router.push({
|
||||
name: 'FileViewer',
|
||||
params: {
|
||||
id: route.params.id,
|
||||
fileId: item.id.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 下载项目
|
||||
const downloadItem = (item: FileItem) => {
|
||||
message.success(`开始下载:${item.name}`)
|
||||
// 这里实现下载逻辑
|
||||
}
|
||||
|
||||
// 导航到面包屑位置
|
||||
const navigateTo = (crumb: FileItem) => {
|
||||
if (crumb.id !== currentFolder.value?.id) {
|
||||
router.push({
|
||||
name: 'FolderBrowser',
|
||||
params: {
|
||||
id: route.params.id,
|
||||
folderId: crumb.id.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 添加文件
|
||||
const addFile = () => {
|
||||
message.info('添加文件功能')
|
||||
// 这里可以打开文件上传模态框
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
const folderId = parseInt(route.params.folderId as string)
|
||||
if (folderId) {
|
||||
loadFolder(folderId)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听路由参数变化
|
||||
router.afterEach(() => {
|
||||
const folderId = parseInt(route.params.folderId as string)
|
||||
if (folderId && folderId !== currentFolder.value?.id) {
|
||||
loadFolder(folderId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.folder-browser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #e6f7ff;
|
||||
border-color: #0288d1;
|
||||
color: #0288d1;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #0288d1;
|
||||
}
|
||||
|
||||
.breadcrumb-item:last-child {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 8px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.nav-right .btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0277bd;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.folder-view {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.folder-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.folder-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.folder-info {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.folder-content {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
border-color: #0288d1;
|
||||
box-shadow: 0 2px 8px rgba(2, 136, 209, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
border-color: #0288d1;
|
||||
box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.2);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.file-icon img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.top-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.file-item:hover .file-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #0288d1;
|
||||
background: white;
|
||||
color: #0288d1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #0288d1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #0288d1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.folder-info {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -3,24 +3,22 @@
|
||||
<div class="toolbar">
|
||||
<h2>作业库</h2>
|
||||
<div class="toolbar-actions">
|
||||
<button class="btn btn-primary" @click="handleAddHomework">添加作业</button>
|
||||
<button class="btn btn-new" @click="handleImport">导入</button>
|
||||
<button class="btn btn-new">导出</button>
|
||||
<button class="btn btn-danger">删除</button>
|
||||
<n-button type="primary" @click="handleAddHomework">添加作业</n-button>
|
||||
<n-button @click="handleImport">导入</n-button>
|
||||
<n-button>导出</n-button>
|
||||
<n-button type="error">删除</n-button>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入想要搜索的内容" />
|
||||
<button class="btn btn-search">搜索</button>
|
||||
<n-input v-model:value="searchValue" placeholder="请输入想要搜索的内容" clearable />
|
||||
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="table-container">
|
||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
|
||||
<n-data-table :columns="columns" :data="sortedHomeworkList" :row-key="rowKey"
|
||||
:checked-row-keys="selectedHomework" @update:checked-row-keys="handleCheck" :bordered="false"
|
||||
:single-line="false" size="medium" class="homework-data-table" scroll-x="true" />
|
||||
</n-config-provider>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
@ -64,7 +62,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, h, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { NButton, NDropdown, NDataTable, NConfigProvider, zhCN, dateZhCN } from 'naive-ui'
|
||||
import { NButton, NDropdown, NDataTable, NInput } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
|
||||
// 作业类型定义
|
||||
@ -85,6 +83,9 @@ const route = useRoute()
|
||||
// 选中的作业行
|
||||
const selectedHomework = ref<number[]>([])
|
||||
|
||||
// 搜索相关
|
||||
const searchValue = ref('')
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
@ -462,6 +463,11 @@ const handleImport = () => {
|
||||
// 跳转到作业库模板导入页面
|
||||
router.push(`/teacher/course-editor/${route.params.id}/homework/template-import`)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
console.log('搜索内容:', searchValue.value)
|
||||
// 这里可以添加搜索逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -507,118 +513,14 @@ const handleImport = () => {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 7px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: #40a9ff;
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #0288D1;
|
||||
border-color: #0288D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
background: #fff;
|
||||
border-color: #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.btn-new:hover {
|
||||
background: #0288D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: #fff;
|
||||
border-color: #0288D1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-default:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border-color: #FF4D4F;
|
||||
color: #FF4D4F;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #FF4D4F;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
border-color: #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.btn-more {
|
||||
border-color: #0288D1;
|
||||
color: #0288D1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #F1F3F4;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
outline: none;
|
||||
.search-box :deep(.n-input) {
|
||||
width: 200px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: #0288D1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: #0288D1;
|
||||
}
|
||||
|
||||
/* Naive UI 表格样式定制 */
|
||||
@ -888,10 +790,17 @@ const handleImport = () => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
.search-box :deep(.n-input) {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
/* 响应式按钮样式调整 */
|
||||
:deep(.toolbar-actions .n-button) {
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* 表格在小屏幕下的优化 */
|
||||
:deep(.homework-data-table) {
|
||||
padding: 20px;
|
||||
@ -914,14 +823,14 @@ const handleImport = () => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
:deep(.toolbar-actions .n-button) {
|
||||
font-size: 11px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
.search-box :deep(.n-input) {
|
||||
width: 120px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 表格在移动端的优化 */
|
||||
@ -971,7 +880,7 @@ const handleImport = () => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
.search-box :deep(.n-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,16 @@
|
||||
<!-- 顶部区域 -->
|
||||
<div class="top-section">
|
||||
<!-- 左侧标签页 - 使用 Naive UI Tabs -->
|
||||
<div class="back-button">
|
||||
<n-button quaternary circle size="large" @click="goBack">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="tabs-container">
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<n-tab-pane name="all" tab="全部" />
|
||||
@ -13,35 +23,23 @@
|
||||
|
||||
<!-- 右侧按钮和搜索 -->
|
||||
<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>
|
||||
<div class="search-container">
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入学生姓名"
|
||||
:style="{ width: '200px' }"
|
||||
size="medium"
|
||||
/>
|
||||
<n-button type="primary" size="medium">
|
||||
搜索
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
<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>
|
||||
<div class="search-container">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="searchText" placeholder="请输入学生姓名" :style="{ width: '200px' }" size="medium" />
|
||||
<n-button type="primary" size="medium">
|
||||
搜索
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 - 使用 Naive UI Table -->
|
||||
<div class="table-container">
|
||||
<n-data-table
|
||||
:columns="activeTab === 'unsubmitted' ? unsubmittedColumns : columns"
|
||||
:data="filteredData"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
size="medium"
|
||||
:row-key="(row) => row.id"
|
||||
/>
|
||||
<n-data-table :columns="activeTab === 'unsubmitted' ? unsubmittedColumns : columns" :data="filteredData"
|
||||
:pagination="false" :bordered="false" :single-line="false" size="medium" :row-key="(row) => row.id" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -49,14 +47,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NDataTable,
|
||||
NButton,
|
||||
NInput,
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
import {
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NDataTable,
|
||||
NButton,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
type DataTableColumns
|
||||
type DataTableColumns
|
||||
} from 'naive-ui'
|
||||
|
||||
interface HomeworkSubmission {
|
||||
@ -178,7 +177,7 @@ const filteredData = computed(() => {
|
||||
|
||||
// 根据搜索文本过滤
|
||||
if (searchText.value.trim()) {
|
||||
filtered = filtered.filter((item: HomeworkSubmission) =>
|
||||
filtered = filtered.filter((item: HomeworkSubmission) =>
|
||||
item.name.includes(searchText.value.trim())
|
||||
)
|
||||
}
|
||||
@ -361,9 +360,11 @@ const unsubmittedColumns: DataTableColumns<HomeworkSubmission> = [
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
]
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -381,6 +382,11 @@ const unsubmittedColumns: DataTableColumns<HomeworkSubmission> = [
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
border-bottom: 1.5px solid #E6E6E6;
|
||||
padding: 11px;
|
||||
}
|
||||
|
||||
/* 左侧标签页 */
|
||||
.tabs-container {
|
||||
flex: 1;
|
||||
@ -531,7 +537,7 @@ const unsubmittedColumns: DataTableColumns<HomeworkSubmission> = [
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
.actions-container {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
@ -539,26 +545,26 @@ const unsubmittedColumns: DataTableColumns<HomeworkSubmission> = [
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
|
||||
.actions-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.operations-container {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
|
||||
.operation-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
@ -3,8 +3,13 @@
|
||||
|
||||
<!-- 消息提示组件 -->
|
||||
<MessageComponent ref="messageRef" />
|
||||
|
||||
|
||||
<n-button quaternary circle size="large" @click="goBack" class="back-button">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackOutline />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" @click="startImport">
|
||||
@ -35,7 +40,6 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 隐藏的文件上传输入 -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
@ -49,7 +53,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ArrowBackOutline} from '@vicons/ionicons5'
|
||||
import MessageComponent from '@/components/MessageComponent.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const messageRef = ref<InstanceType<typeof MessageComponent>>()
|
||||
@ -128,7 +136,9 @@ const startImport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -31,7 +31,7 @@
|
||||
<img :src="file.thumbnail" :alt="file.name" />
|
||||
</div>
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-options">
|
||||
<!-- <div class="file-options">
|
||||
<n-dropdown :options="fileMenuOptions" @select="(key: string) => handleFileMenuSelect(key, file)">
|
||||
<n-button quaternary size="small" class="file-menu-btn">
|
||||
<template #icon>
|
||||
@ -43,7 +43,7 @@
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -135,10 +135,10 @@ const filteredFiles = computed(() => {
|
||||
const selectedFile = ref<any>(null)
|
||||
|
||||
// 文件菜单选项
|
||||
const fileMenuOptions = [
|
||||
{ label: '重命名', key: 'rename' },
|
||||
{ label: '删除', key: 'delete' }
|
||||
]
|
||||
// const fileMenuOptions = [
|
||||
// { label: '重命名', key: 'rename' },
|
||||
// { label: '删除', key: 'delete' }
|
||||
// ]
|
||||
|
||||
// 搜索文件
|
||||
const searchFiles = () => {
|
||||
@ -173,17 +173,17 @@ const selectFile = (file: any, event?: Event) => {
|
||||
}
|
||||
|
||||
// 处理文件菜单选择
|
||||
const handleFileMenuSelect = (key: string, file: any) => {
|
||||
if (key === 'delete') {
|
||||
console.log('删除文件:', file)
|
||||
files.value = files.value.filter(f => f.id !== file.id)
|
||||
if (selectedFile.value && selectedFile.value.id === file.id) {
|
||||
selectedFile.value = null
|
||||
}
|
||||
} else if (key === 'rename') {
|
||||
console.log('重命名文件:', file)
|
||||
}
|
||||
}
|
||||
// const handleFileMenuSelect = (key: string, file: any) => {
|
||||
// if (key === 'delete') {
|
||||
// console.log('删除文件:', file)
|
||||
// files.value = files.value.filter(f => f.id !== file.id)
|
||||
// if (selectedFile.value && selectedFile.value.id === file.id) {
|
||||
// selectedFile.value = null
|
||||
// }
|
||||
// } else if (key === 'rename') {
|
||||
// console.log('重命名文件:', file)
|
||||
// }
|
||||
// }
|
||||
|
||||
// 处理取消按钮
|
||||
const handleCancel = () => {
|
||||
|
@ -1,44 +1,48 @@
|
||||
<template>
|
||||
<div class="modal-container flex-col" @click.stop>
|
||||
<span class="modal-title">上传文件</span>
|
||||
<h2 class="modal-title">上传文件</h2>
|
||||
<n-divider />
|
||||
<div class="upload-area flex-row">
|
||||
<span class="upload-label">上传文件:</span>
|
||||
<div class="select-file-btn flex-col" @click="toggleDropdown" style="position: relative;">
|
||||
<span class="btn-text">选择文件</span>
|
||||
<!-- 下拉选项 -->
|
||||
<div v-show="showDropdown" class="upload-methods flex-col">
|
||||
<label class="local-upload" @click="openLocalUpload">
|
||||
<!-- <input type="file" @change="handleLocalUpload" style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4" /> -->
|
||||
本地上传
|
||||
</label>
|
||||
<label class="resource-upload" @click="openResourceModal">
|
||||
<!-- <input type="file" @change="handleResourceUpload" style="display: none;"
|
||||
accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4" /> -->
|
||||
资源上传
|
||||
</label>
|
||||
<n-popselect
|
||||
:options="uploadOptions"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
@update:value="handleUploadOptionSelect"
|
||||
:render-label="renderUploadOption"
|
||||
>
|
||||
<div class="select-file-btn flex-col">
|
||||
<span class="btn-text">选择文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-popselect>
|
||||
</div>
|
||||
<div class="supported-formats flex-row">
|
||||
<span class="formats-label">支持格式:</span>
|
||||
<span class="document-formats">文本文:.doc、.docx、.pdf表格文件:.xls、.xlsx<br />演示文稿:.ppt、.pptx</span>
|
||||
<span class="media-formats">音频文件:.mp3<br />视频文件:.mp4<br /></span>
|
||||
</div>
|
||||
<div class="upload-limits flex-row justify-between">
|
||||
<span class="limits-label">上传限制:</span>
|
||||
<span class="limits-description">word、excel、ppt文件需小于100MB,其他文件需小于2GB.mp4格式编码<br />说明(上传mp4格式文件请查看编码说明)</span>
|
||||
</div>
|
||||
<div class="action-buttons flex-row justify-between">
|
||||
<div class="cancel-btn-container flex-col">
|
||||
<div class="cancel-btn flex-col" @click="handleCancel">
|
||||
<span class="cancel-btn-text">取消</span>
|
||||
<div class="formats-content flex-col">
|
||||
<div class="format-row flex-row">
|
||||
<span class="document-formats">文本文:.doc、.docx、.pdf</span>
|
||||
<span class="media-formats">音频文件:.mp3</span>
|
||||
</div>
|
||||
<div class="format-row flex-row">
|
||||
<span class="document-formats">表格文件:.xls、.xlsx</span>
|
||||
<span class="media-formats">视频文件:.mp4</span>
|
||||
</div>
|
||||
<div class="format-row">
|
||||
<span class="document-formats">演示文稿:.ppt、.pptx</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="confirm-btn flex-col" @click="handleConfirm">
|
||||
<span class="confirm-btn-text">确定</span>
|
||||
</div>
|
||||
<div class="upload-limits flex-row">
|
||||
<span class="limits-label">上传限制:</span>
|
||||
<div class="limits-content flex-col">
|
||||
<span class="limits-description">word、excel、ppt文件需小于100MB,其他文件需小于2GB.mp4格式编码</span>
|
||||
<span class="limits-description">说明(上传mp4格式文件请查看编码说明)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons flex-row justify-between">
|
||||
<n-button @click="handleCancel">取消</n-button>
|
||||
<n-button type="primary" @click="handleConfirm">确定</n-button>
|
||||
</div>
|
||||
|
||||
<!-- 资源选择模态框 -->
|
||||
<ResourceSelectionModal v-model:show="showResourceModal" @select="handleResourceSelection" />
|
||||
@ -49,21 +53,43 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, h } from 'vue'
|
||||
import ResourceSelectionModal from './ResourceSelectionModal.vue'
|
||||
import LocalUploadModal from './LocalUploadModal.vue'
|
||||
|
||||
// 控制下拉菜单显示
|
||||
const showDropdown = ref(false)
|
||||
// 上传选项数据
|
||||
const uploadOptions = ref([
|
||||
{ label: '本地上传', value: 'local' },
|
||||
{ label: '资源上传', value: 'resource' }
|
||||
])
|
||||
|
||||
// 渲染上传选项标签
|
||||
const renderUploadOption = (option: any) => {
|
||||
return h('span', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '5px 0'
|
||||
}
|
||||
}, option.label)
|
||||
}
|
||||
|
||||
// 处理上传选项选择
|
||||
const handleUploadOptionSelect = (value: string) => {
|
||||
console.log('选中的上传方式:', value)
|
||||
if (value === 'local') {
|
||||
// 触发本地上传
|
||||
openLocalUpload()
|
||||
} else if (value === 'resource') {
|
||||
// 触发资源上传
|
||||
openResourceModal()
|
||||
}
|
||||
}
|
||||
|
||||
// 控制本地上传模态框显示
|
||||
const showLocalUploadModal = ref(false)
|
||||
|
||||
// 切换下拉菜单
|
||||
const toggleDropdown = () => {
|
||||
showDropdown.value = !showDropdown.value
|
||||
}
|
||||
|
||||
// 处理本地上传
|
||||
// const handleLocalUpload = (event: Event) => {
|
||||
// const target = event.target as HTMLInputElement
|
||||
@ -89,7 +115,6 @@ const toggleDropdown = () => {
|
||||
// 打开本地资源模态框
|
||||
const openLocalUpload = () => {
|
||||
showLocalUploadModal.value = true
|
||||
showDropdown.value = false
|
||||
}
|
||||
|
||||
// 关闭本地上传模态框
|
||||
@ -108,7 +133,6 @@ const handleUploadMore = () => {
|
||||
const openResourceModal = () => {
|
||||
console.log('打开资源选择模态框')
|
||||
showResourceModal.value = true // 打开资源选择模态框
|
||||
showDropdown.value = false // 关闭下拉菜单
|
||||
}
|
||||
|
||||
// 控制资源选择模态框的显示
|
||||
@ -140,12 +164,11 @@ const handleConfirm = () => {
|
||||
<style scoped>
|
||||
.modal-container {
|
||||
width: 1076px;
|
||||
height: 623px;
|
||||
/* height: 623px; */
|
||||
background: #FFFFFF;
|
||||
background-size: 100% 100%;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
padding-top: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@ -169,7 +192,7 @@ const handleConfirm = () => {
|
||||
background: #FCFCFC;
|
||||
background-size: 100% 100%;
|
||||
margin: 46px 0 0 22px;
|
||||
border: 1px solid rgba(204, 204, 204, 1);
|
||||
border: 1px solid rgb(233, 233, 233);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@ -212,72 +235,24 @@ const handleConfirm = () => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-methods {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 123px;
|
||||
height: 76px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.local-upload {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
font-size: 14px;
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
line-height: 30px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.local-upload:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.resource-upload {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
font-size: 14px;
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
line-height: 30px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.resource-upload:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.supported-formats {
|
||||
width: 532px;
|
||||
height: 93px;
|
||||
margin: 27px 0 0 57px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.formats-content {
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.format-row {
|
||||
gap: 80px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formats-label {
|
||||
@ -292,38 +267,42 @@ const handleConfirm = () => {
|
||||
white-space: nowrap;
|
||||
line-height: 22px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.document-formats {
|
||||
width: 230px;
|
||||
height: 90px;
|
||||
min-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(102, 102, 102, 1);
|
||||
font-size: 18px;
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
line-height: 30px;
|
||||
margin-left: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.media-formats {
|
||||
width: 131px;
|
||||
height: 90px;
|
||||
min-width: 120px;
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(102, 102, 102, 1);
|
||||
font-size: 18px;
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
line-height: 30px;
|
||||
margin: 3px 0 0 63px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.upload-limits {
|
||||
width: 698px;
|
||||
height: 60px;
|
||||
margin: 26px 0 0 57px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.limits-content {
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.limits-label {
|
||||
@ -338,25 +317,25 @@ const handleConfirm = () => {
|
||||
white-space: nowrap;
|
||||
line-height: 22px;
|
||||
margin-top: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.limits-description {
|
||||
width: 590px;
|
||||
height: 60px;
|
||||
max-width: 590px;
|
||||
overflow-wrap: break-word;
|
||||
color: rgba(102, 102, 102, 1);
|
||||
font-size: 18px;
|
||||
font-family: Helvetica, 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
line-height: 30px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 147px;
|
||||
height: 32px;
|
||||
margin: 103px 0 0 905px;
|
||||
/* margin: 103px 0 0 905px; */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@ -429,15 +408,4 @@ const handleConfirm = () => {
|
||||
.select-file-btn:hover {
|
||||
background: #0277BD;
|
||||
}
|
||||
|
||||
/* 确保下拉菜单在按钮上方显示 */
|
||||
.select-file-btn .upload-methods {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 隐藏文件输入框 */
|
||||
.local-upload input[type="file"],
|
||||
.resource-upload input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user