631 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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