631 lines
13 KiB
Vue
Raw Normal View History

<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>