2025-09-06 17:20:05 +08:00

736 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="resources-content">
<!-- 头部操作区域 -->
<div class="resources-header">
<div class="resources-title">
<h1>我的资源</h1>
</div>
<div class="resources-actions">
<button class="upload-btn" @click="handleUpload">
上传文件
</button>
<button class="new-folder-btn" @click="handleNewFolder">
新建文件夹
</button>
<button class="recycle-bin-btn" @click="handleRecycleBin">
回收站
</button>
<div class="search-container">
<input type="text" class="search-input" placeholder="请输入关键字" v-model="searchKeyword"
@keyup.enter="handleSearch">
<button class="search-btn" @click="handleSearch">
搜索
</button>
</div>
</div>
</div>
<!-- 文件网格 -->
<div class="files-grid">
<div v-for="file in filteredFiles" :key="file.id" class="file-item"
:class="{ 'selected': selectedFiles.includes(file.id) }" @click="handleFileClick(file)"
@contextmenu.prevent="handleRightClick(file, $event)" @mouseenter="hoveredFile = file.id"
@mouseleave="hoveredFile = null">
<!-- 文件操作菜单 -->
<div class="file-menu">
<button class="file-menu-btn" @click.stop="toggleMenu(file.id)">
<img src="/images/profile/more.png" alt="更多操作" class="more-icon">
</button>
<div class="file-menu-dropdown" v-if="showMenuFor === file.id">
<div class="menu-item" @click="handleEdit(file)">
<span class="edit-icon"></span>
编辑
</div>
<div class="menu-item" @click="handleMove(file)">
<span class="move-icon">📁</span>
+ 移动
</div>
<div class="menu-item delete" @click="handleDelete(file)">
<span class="delete-icon">🗑</span>
删除
</div>
</div>
</div>
<!-- 文件图标 -->
<div class="file-icon">
<img :src="getFileIcon(file)" :alt="file.type === 'folder' ? '文件夹图标' : '文件图标'" class="folder-icon">
</div>
<!-- 文件名称 -->
<div class="file-name" :title="file.name">{{ file.name }}</div>
<!-- 文件详情悬浮框 -->
<div class="file-details" v-if="hoveredFile === file.id">
<div class="detail-item">
<span class="detail-label">文件名称</span>
<span class="detail-value">{{ file.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">大小</span>
<span class="detail-value">{{ formatFileSize(file.size) }}</span>
</div>
<div class="detail-item">
<span class="detail-label">修改时间</span>
<span class="detail-value">{{ formatDate(file.modifiedAt) }}</span>
</div>
</div>
</div>
</div>
<!-- 新建文件夹对话框 -->
<div class="modal-overlay" v-if="showNewFolderModal" @click="closeNewFolderModal">
<div class="modal-content" @click.stop>
<h3>新建文件夹</h3>
<input type="text" v-model="newFolderName" placeholder="请输入文件夹名称" @keyup.enter="confirmNewFolder">
<div class="modal-actions">
<button class="cancel-btn" @click="closeNewFolderModal">取消</button>
<button class="confirm-btn" @click="confirmNewFolder">确定</button>
</div>
</div>
</div>
<!-- 文件上传对话框 -->
<div class="modal-overlay" v-if="showUploadModal" @click="closeUploadModal">
<div class="modal-content" @click.stop>
<h3>上传文件</h3>
<div class="upload-area" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleDrop">
<input ref="fileInput" type="file" multiple @change="handleFileSelect" style="display: none">
<div class="upload-icon">📁</div>
<p>点击选择文件或拖拽文件到此处</p>
</div>
<div class="modal-actions">
<button class="cancel-btn" @click="closeUploadModal">取消</button>
<button class="confirm-btn" @click="confirmUpload">上传</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
// 文件类型定义
interface FileItem {
id: string
name: string
type: 'file' | 'folder'
size: number
modifiedAt: string
parentId?: string
}
// 响应式数据
const searchKeyword = ref('')
const selectedFiles = ref<string[]>([])
const showMenuFor = ref<string | null>(null)
const hoveredFile = ref<string | null>(null)
const showNewFolderModal = ref(false)
const showUploadModal = ref(false)
const newFolderName = ref('')
const fileInput = ref<HTMLInputElement>()
const selectedFilesToUpload = ref<File[]>([])
// 模拟文件数据
const files = ref<FileItem[]>([
{ id: '1', name: '图片', type: 'folder', size: 0, modifiedAt: '2024-01-15 09:25' },
{ id: '2', name: '文档', type: 'folder', size: 0, modifiedAt: '2024-01-14 14:30' },
{ id: '3', name: '视频', type: 'folder', size: 0, modifiedAt: '2024-01-13 16:45' },
{ id: '4', name: '种子', type: 'file', size: 14912, modifiedAt: '2024-01-12 08:25' },
{ id: '5', name: '音频', type: 'folder', size: 0, modifiedAt: '2024-01-11 11:20' },
{ id: '6', name: '文件名称', type: 'file', size: 2048, modifiedAt: '2024-01-10 15:30' },
{ id: '7', name: '文件名称', type: 'file', size: 1024, modifiedAt: '2024-01-09 10:15' },
{ id: '8', name: '文件名称', type: 'file', size: 512, modifiedAt: '2024-01-08 13:45' },
{ id: '9', name: '文件名称', type: 'file', size: 4096, modifiedAt: '2024-01-07 09:30' },
{ id: '10', name: '文件名称', type: 'file', size: 8192, modifiedAt: '2024-01-06 16:20' },
{ id: '11', name: '文件名称', type: 'file', size: 256, modifiedAt: '2024-01-05 12:10' },
{ id: '12', name: '文件名称', type: 'file', size: 1536, modifiedAt: '2024-01-04 14:55' },
{ id: '13', name: '文件名称', type: 'file', size: 3072, modifiedAt: '2024-01-03 11:40' },
{ id: '14', name: '文件名称', type: 'file', size: 6144, modifiedAt: '2024-01-02 08:25' },
{ id: '15', name: '文件名称', type: 'file', size: 128, modifiedAt: '2024-01-01 17:15' },
{ id: '16', name: '文件名称', type: 'file', size: 768, modifiedAt: '2023-12-31 20:30' }
])
// 计算属性
const filteredFiles = computed(() => {
let result = files.value
// 按关键词搜索
if (searchKeyword.value) {
result = result.filter((file: FileItem) =>
file.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
return result
})
// 方法
const getFileIcon = (file: FileItem) => {
if (file.type === 'folder') {
return '/images/profile/folder.png'
}
// 根据文件扩展名返回不同图标
const ext = file.name.split('.').pop()?.toLowerCase()
switch (ext) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return '/images/profile/image.png'
case 'mp4':
case 'avi':
case 'mov':
return '/images/profile/video.png'
case 'mp3':
case 'wav':
case 'flac':
return '/images/profile/audio.png'
case 'pdf':
return '/images/profile/pdf.png'
case 'doc':
case 'docx':
return '/images/profile/word.png'
case 'xls':
case 'xlsx':
return '/images/profile/excel.png'
case 'ppt':
case 'pptx':
return '/images/profile/powerpoint.png'
default:
return '/images/profile/file.png'
}
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const formatDate = (dateString: string) => {
return dateString
}
const handleFileClick = (file: FileItem) => {
if (file.type === 'folder') {
// 进入文件夹
console.log('进入文件夹:', file.name)
} else {
// 选择文件
const index = selectedFiles.value.indexOf(file.id)
if (index > -1) {
selectedFiles.value.splice(index, 1)
} else {
selectedFiles.value.push(file.id)
}
}
}
const handleRightClick = (file: FileItem, event: MouseEvent) => {
event.preventDefault()
showMenuFor.value = file.id
}
const toggleMenu = (fileId: string) => {
showMenuFor.value = showMenuFor.value === fileId ? null : fileId
}
const handleEdit = (file: FileItem) => {
console.log('编辑文件:', file.name)
showMenuFor.value = null
}
const handleMove = (file: FileItem) => {
console.log('移动文件:', file.name)
showMenuFor.value = null
}
const handleDelete = (file: FileItem) => {
if (confirm(`确定要删除 "${file.name}" 吗?`)) {
const index = files.value.findIndex((f: FileItem) => f.id === file.id)
if (index > -1) {
files.value.splice(index, 1)
}
}
showMenuFor.value = null
}
const handleSearch = () => {
console.log('搜索:', searchKeyword.value)
}
const handleUpload = () => {
showUploadModal.value = true
}
const handleNewFolder = () => {
showNewFolderModal.value = true
}
const handleRecycleBin = () => {
console.log('打开回收站')
}
const closeNewFolderModal = () => {
showNewFolderModal.value = false
newFolderName.value = ''
}
const confirmNewFolder = () => {
if (newFolderName.value.trim()) {
const newFolder: FileItem = {
id: Date.now().toString(),
name: newFolderName.value.trim(),
type: 'folder',
size: 0,
modifiedAt: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '-')
}
files.value.push(newFolder)
closeNewFolderModal()
}
}
const closeUploadModal = () => {
showUploadModal.value = false
selectedFilesToUpload.value = []
}
const triggerFileInput = () => {
fileInput.value?.click()
}
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files) {
selectedFilesToUpload.value = Array.from(target.files)
}
}
const handleDrop = (event: DragEvent) => {
if (event.dataTransfer?.files) {
selectedFilesToUpload.value = Array.from(event.dataTransfer.files)
}
}
const confirmUpload = () => {
if (selectedFilesToUpload.value.length > 0) {
selectedFilesToUpload.value.forEach((file: File) => {
const newFile: FileItem = {
id: Date.now().toString() + Math.random(),
name: file.name,
type: 'file',
size: file.size,
modifiedAt: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '-')
}
files.value.push(newFile)
})
closeUploadModal()
}
}
// 点击外部关闭菜单
onMounted(() => {
document.addEventListener('click', () => {
showMenuFor.value = null
})
})
</script>
<style scoped>
.resources-content {
padding: 0;
background: #F5F7FA;
min-height: 100vh;
width: 100%;
}
/* 头部操作区域 */
.resources-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
background: white;
padding: 20px 30px;
border-bottom: 1px solid #E8E8E8;
width: 100%;
}
.resources-title h1 {
margin: 0;
color: #333;
font-size: 20px;
font-weight: 500;
}
.resources-actions {
display: flex;
align-items: center;
gap: 12px;
}
.upload-btn,
.new-folder-btn,
.recycle-bin-btn {
padding: 6px 16px;
border: 1px solid #0288D1;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
background: white;
color: #0288D1;
min-width: 70px;
text-align: center;
height: 32px;
}
.upload-btn {
background: #0288D1;
color: white;
}
.upload-btn:hover {
background: #0277BD;
}
.new-folder-btn:hover,
.recycle-bin-btn:hover {
background: #F5F8FB;
color: #0288D1;
}
.search-container {
display: flex;
align-items: center;
background: white;
border: 1px solid #D9D9D9;
border-radius: 4px;
overflow: hidden;
height: 32px;
}
.search-input {
padding: 6px 12px;
border: none;
outline: none;
width: 180px;
font-size: 14px;
height: 30px;
}
.search-btn {
padding: 6px 16px;
background: #0288D1;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s ease;
font-size: 14px;
height: 30px;
}
.search-btn:hover {
background: #0277BD;
}
/* 文件网格 */
.files-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 20px;
background: white;
padding: 30px;
margin: 0;
}
.file-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 15px;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s ease;
border: 1.5px solid #D8D8D8;
background: white;
}
.file-item:hover {
background: #F5F8FB;
border-color: #0288D1;
}
.file-item.selected {
background: #E3F2FD;
border-color: #0288D1;
}
.file-menu {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
}
.file-menu-btn {
width: 6px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.file-menu-btn:hover {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.more-icon {
width: 16px;
height: 16px;
}
.file-menu-dropdown {
position: absolute;
top: 100%;
right: 0;
background: white;
border: 1px solid #D9D9D9;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 120px;
z-index: 1000;
}
.menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s ease;
}
.menu-item:hover {
background: #F5F5F5;
}
.menu-item.delete {
color: #FF4D4F;
}
.menu-item.delete:hover {
background: #FFF2F0;
}
.file-icon {
margin-bottom: 8px;
}
.folder-icon {
width: 48px;
height: 48px;
}
.file-name {
font-size: 12px;
color: #333;
text-align: center;
word-break: break-all;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-details {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
background: white;
border: 1px solid #D9D9D9;
border-radius: 6px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 200px;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
}
.detail-label {
color: #666;
}
.detail-value {
color: #333;
font-weight: 500;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 24px;
min-width: 400px;
max-width: 500px;
}
.modal-content h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 18px;
}
.modal-content input {
width: 100%;
padding: 10px 12px;
border: 1px solid #D9D9D9;
border-radius: 6px;
font-size: 14px;
outline: none;
margin-bottom: 20px;
}
.upload-area {
border: 2px dashed #D9D9D9;
border-radius: 6px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s ease;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #0288D1;
}
.upload-icon {
font-size: 48px;
margin-bottom: 12px;
display: block;
}
.upload-area p {
margin: 0;
color: #666;
font-size: 14px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.cancel-btn,
.confirm-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.cancel-btn {
background: #F5F5F5;
color: #666;
}
.cancel-btn:hover {
background: #E6E6E6;
}
.confirm-btn {
background: #0288D1;
color: white;
}
.confirm-btn:hover {
background: #0277BD;
}
/* 响应式设计 */
@media (max-width: 768px) {
.resources-header {
flex-direction: column;
gap: 15px;
padding: 20px;
}
.resources-actions {
flex-wrap: wrap;
justify-content: center;
}
.search-input {
width: 150px;
}
.files-grid {
grid-template-columns: repeat(4, 1fr);
gap: 15px;
padding: 20px;
}
.modal-content {
min-width: 300px;
margin: 20px;
}
}
/* 更小屏幕的响应式设计 */
@media (max-width: 480px) {
.files-grid {
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 15px;
}
}
</style>