631 lines
13 KiB
Vue
631 lines
13 KiB
Vue
<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>
|