feat: 完成“我的资源”页面,功能
This commit is contained in:
parent
7de929ac18
commit
2c1fb5ab7f
69
src/components/admin/FileInfoCard.vue
Normal file
69
src/components/admin/FileInfoCard.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="file-info-card">
|
||||
<div class="title">{{ name }}</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta">{{ size }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta">{{ modified }}</span>
|
||||
</div>
|
||||
<span class="arrow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
name: string
|
||||
size: string
|
||||
modified: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { name, size, modified } = props
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-info-card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
||||
padding: 12px 16px;
|
||||
min-width: 140px;
|
||||
max-width: 220px;
|
||||
border: 1px solid #ECECEC;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 18px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-right: 6px solid #fff;
|
||||
filter: drop-shadow(-1px 0 0 #ECECEC);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
新建文件夹
|
||||
</button>
|
||||
<button class="recycle-bin-btn" @click="handleRecycleBin">
|
||||
<img src="/images/teacher/delete2.png" alt="回收站" class="action-icon">
|
||||
回收站
|
||||
</button>
|
||||
<div class="search-container">
|
||||
@ -29,9 +30,8 @@
|
||||
<!-- 文件网格 -->
|
||||
<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">
|
||||
@mouseleave="handleItemMouseLeave">
|
||||
<!-- 文件操作菜单 -->
|
||||
<div class="file-menu">
|
||||
<button class="file-menu-btn" @click.stop="toggleMenu(file.id)">
|
||||
@ -39,78 +39,83 @@
|
||||
</button>
|
||||
<div class="file-menu-dropdown" v-if="showMenuFor === file.id">
|
||||
<div class="menu-item" @click="handleEdit(file)">
|
||||
<span class="edit-icon">✏️</span>
|
||||
编辑
|
||||
<img class="menu-icon" src="/images/teacher/edit.png" alt="编辑">
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="handleMove(file)">
|
||||
<span class="move-icon">📁</span>
|
||||
+ 移动
|
||||
<img class="menu-icon" src="/images/teacher/移动.png" alt="移动">
|
||||
<span>移动</span>
|
||||
</div>
|
||||
<div class="menu-item delete" @click="handleDelete(file)">
|
||||
<span class="delete-icon">🗑️</span>
|
||||
删除
|
||||
<img class="menu-icon" src="/images/teacher/删除.png.png" alt="删除">
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件图标 -->
|
||||
<div class="file-icon">
|
||||
<img :src="getFileIcon(file)" :alt="file.type === 'folder' ? '文件夹图标' : '文件图标'" class="folder-icon">
|
||||
<div class="file-icon" @mouseenter.stop="showInfoCard(file, $event)" @mouseleave.stop="hideInfoCard">
|
||||
<img src="/images/profile/folder.png" alt="文件夹图标" 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 v-if="editingId !== file.id" class="file-name" :title="file.name" @click.stop="startEdit(file)">
|
||||
{{ file.name }}
|
||||
</div>
|
||||
<input v-else class="file-name-input" type="text" v-model="editName" @keyup.enter="saveEdit(file)"
|
||||
@keyup.esc="cancelEdit" @blur="saveEdit(file)" :maxlength="50" autofocus />
|
||||
|
||||
<!-- 文件详情由全局定位卡片显示 -->
|
||||
</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>
|
||||
<!-- 悬停信息卡片(固定定位,显示在文件图标右侧) -->
|
||||
<FileInfoCard v-if="infoCardVisible && currentHoverFile" :name="currentHoverFile.name"
|
||||
:size="formatFileSize(currentHoverFile.size)" :modified="formatDate(currentHoverFile.modifiedAt)"
|
||||
:style="{ position: 'fixed', top: infoCardPosition.top + 'px', left: infoCardPosition.left + 'px', zIndex: 3000 }" />
|
||||
|
||||
<!-- 文件上传对话框 -->
|
||||
<!-- 新建文件夹对话框(使用 RecycleConfirmModal) -->
|
||||
<RecycleConfirmModal :visible="showNewFolderModal" title="新建文件夹" message="" @cancel="closeNewFolderModal"
|
||||
@confirm="confirmNewFolder">
|
||||
<template #body>
|
||||
<div style="display:flex; align-items:center; gap:12px;">
|
||||
<span style="width:90px; text-align:right; color:#333; font-size:14px;">文件夹名称:</span>
|
||||
<input type="text" v-model="newFolderName" placeholder="请输入文件夹名称"
|
||||
style="flex:1; height:34px; border:1px solid #D9D9D9; border-radius:4px; padding:0 10px; font-size:14px; outline:none;">
|
||||
</div>
|
||||
</template>
|
||||
</RecycleConfirmModal>
|
||||
|
||||
<!-- 文件上传对话框(复用资源库样式组件) -->
|
||||
<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 @click.stop>
|
||||
<UploadFileModal @close="closeUploadModal" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动文件(使用 RecycleConfirmModal) -->
|
||||
<RecycleConfirmModal :visible="showMoveModal" title="移动文件" message="" @cancel="() => showMoveModal = false"
|
||||
@confirm="() => showMoveModal = false">
|
||||
<template #body>
|
||||
<div style="display:flex; align-items:center; gap:12px;">
|
||||
<span style="width:90px; text-align:right; color:#333; font-size:14px;">选择文件夹:</span>
|
||||
<select v-model="moveTargetFolder"
|
||||
style="flex:1; height:34px; border:1px solid #D9D9D9; border-radius:4px; padding:0 10px; font-size:14px; outline:none;">
|
||||
<option value="">请选择文件夹</option>
|
||||
<option v-for="f in folderFiles" :key="f.id" :value="f.name">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
</RecycleConfirmModal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import FileInfoCard from '@/components/admin/FileInfoCard.vue'
|
||||
import UploadFileModal from '@/views/teacher/course/UploadFileModal.vue'
|
||||
import RecycleConfirmModal from '@/views/teacher/resource/RecycleConfirmModal.vue'
|
||||
|
||||
// 文件类型定义
|
||||
interface FileItem {
|
||||
@ -124,14 +129,21 @@ interface FileItem {
|
||||
|
||||
// 响应式数据
|
||||
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 editingId = ref<string | null>(null)
|
||||
const editName = ref('')
|
||||
const infoCardVisible = ref(false)
|
||||
const currentHoverFile = ref<FileItem | null>(null)
|
||||
const infoCardPosition = ref<{ top: number; left: number }>({ top: 0, left: 0 })
|
||||
const showMoveModal = ref(false)
|
||||
const moveTargetFolder = ref('')
|
||||
|
||||
const folderFiles = computed(() => files.value.filter((f: FileItem) => f.type === 'folder'))
|
||||
|
||||
// 模拟文件数据
|
||||
const files = ref<FileItem[]>([
|
||||
@ -168,41 +180,6 @@ const filteredFiles = computed(() => {
|
||||
})
|
||||
|
||||
// 方法
|
||||
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'
|
||||
@ -216,20 +193,7 @@ 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()
|
||||
@ -248,6 +212,7 @@ const handleEdit = (file: FileItem) => {
|
||||
const handleMove = (file: FileItem) => {
|
||||
console.log('移动文件:', file.name)
|
||||
showMenuFor.value = null
|
||||
showMoveModal.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (file: FileItem) => {
|
||||
@ -273,7 +238,7 @@ const handleNewFolder = () => {
|
||||
}
|
||||
|
||||
const handleRecycleBin = () => {
|
||||
console.log('打开回收站')
|
||||
window.location.href = '/teacher/recycle-bin'
|
||||
}
|
||||
|
||||
const closeNewFolderModal = () => {
|
||||
@ -306,44 +271,7 @@ const closeUploadModal = () => {
|
||||
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()
|
||||
}
|
||||
}
|
||||
// 以下上传细节交由 UploadFileModal 处理,无需本地函数
|
||||
|
||||
// 点击外部关闭菜单
|
||||
onMounted(() => {
|
||||
@ -351,12 +279,53 @@ onMounted(() => {
|
||||
showMenuFor.value = null
|
||||
})
|
||||
})
|
||||
|
||||
// 内联重命名
|
||||
const startEdit = (file: FileItem) => {
|
||||
editingId.value = file.id
|
||||
editName.value = file.name
|
||||
}
|
||||
|
||||
const saveEdit = (file: FileItem) => {
|
||||
const value = editName.value.trim()
|
||||
if (value) {
|
||||
const target = files.value.find((f: FileItem) => f.id === file.id)
|
||||
if (target) target.name = value
|
||||
}
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
// 悬浮信息卡片定位到图标右侧
|
||||
const showInfoCard = (file: FileItem, event: MouseEvent) => {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const iconRect = target.getBoundingClientRect()
|
||||
currentHoverFile.value = file
|
||||
infoCardPosition.value = {
|
||||
top: iconRect.top + window.scrollY,
|
||||
left: iconRect.right + window.scrollX + 12
|
||||
}
|
||||
infoCardVisible.value = true
|
||||
}
|
||||
|
||||
const hideInfoCard = () => {
|
||||
infoCardVisible.value = false
|
||||
currentHoverFile.value = null
|
||||
}
|
||||
|
||||
const handleItemMouseLeave = () => {
|
||||
hideInfoCard()
|
||||
hoveredFile.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resources-content {
|
||||
padding: 0;
|
||||
background: #F5F7FA;
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
@ -387,17 +356,30 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.upload-btn,
|
||||
.new-folder-btn,
|
||||
.recycle-bin-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #999999;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
color: #999999;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.new-folder-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid #0288D1;
|
||||
border-radius: 4px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
color: #0288D1;
|
||||
min-width: 70px;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
}
|
||||
@ -411,12 +393,29 @@ onMounted(() => {
|
||||
background: #0277BD;
|
||||
}
|
||||
|
||||
.new-folder-btn:hover,
|
||||
|
||||
.recycle-bin-btn:hover {
|
||||
background-color: #f5f8fb;
|
||||
}
|
||||
|
||||
.new-folder-btn:hover {
|
||||
background: #F5F8FB;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
/* 回收站按钮图标与间距 */
|
||||
.recycle-bin-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.recycle-bin-btn .action-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -444,7 +443,7 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
font-size: 14px;
|
||||
height: 30px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
@ -467,27 +466,24 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 15px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
background: white;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #F5F8FB;
|
||||
border-color: #0288D1;
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: #E3F2FD;
|
||||
border-color: #0288D1;
|
||||
}
|
||||
|
||||
/* 移除选中态样式 */
|
||||
.file-menu {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
@ -504,35 +500,32 @@ onMounted(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-menu-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.file-menu-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
right: -20px;
|
||||
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;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 2px 60px 0px rgba(220, 220, 220, 0.74);
|
||||
width: 60px;
|
||||
height: 81px;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
padding: 6px 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
@ -541,24 +534,25 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.menu-item.delete {
|
||||
color: #FF4D4F;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.menu-item.delete:hover {
|
||||
background: #FFF2F0;
|
||||
}
|
||||
.file-icon {}
|
||||
|
||||
.file-icon {
|
||||
margin-bottom: 8px;
|
||||
.menu-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
/* margin-left: 10px; */
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
margin-top: -20px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
@ -568,6 +562,24 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-name-input {
|
||||
margin-top: -20px;
|
||||
width: 100%;
|
||||
max-width: 140px;
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 12px;
|
||||
border: 1.5px solid #D8D8D8;
|
||||
background: #F5F8FB;
|
||||
color: #0288D1;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name-input::placeholder {
|
||||
color: #0288D1;
|
||||
}
|
||||
.file-details {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
|
87
src/components/common/ConfirmModal.vue
Normal file
87
src/components/common/ConfirmModal.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div v-if="visible" class="modal-overlay" @click.self="onCancel">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">{{ title }}</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="modal-message" v-if="message">{{ message }}</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn ghost" @click="onCancel">取消</button>
|
||||
<button class="btn primary" @click="onConfirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Props
|
||||
defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定'
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['close', 'confirm'])
|
||||
|
||||
const onCancel = () => emit('close')
|
||||
const onConfirm = () => emit('confirm')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 4000;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: 520px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header { padding: 16px 20px; border-bottom: 1px solid #F0F0F5; }
|
||||
.modal-title { font-size: 16px; color: #333; font-weight: 600; }
|
||||
.modal-body { padding: 24px 20px; }
|
||||
.modal-message { margin: 0; font-size: 14px; color: #062333; }
|
||||
.modal-footer { padding: 12px 20px 20px; display: flex; justify-content: flex-end; gap: 12px; }
|
||||
|
||||
.btn { height: 32px; padding: 0 16px; border-radius: 3px; font-size: 14px; cursor: pointer; transition: .2s; }
|
||||
.btn.ghost { background: #fff; border: 1px solid #D9D9D9; color: #333; }
|
||||
.btn.ghost:hover { background: #F5F7FA; }
|
||||
.btn.primary { background: #0288D1; border: 1px solid #0288D1; color: #fff; }
|
||||
.btn.primary:hover { background: #0277BD; border-color: #0277BD; }
|
||||
</style>
|
||||
|
||||
|
@ -58,6 +58,7 @@ import StatisticsManagement from '@/views/teacher/statistics/StatisticsManagemen
|
||||
import NotificationManagement from '@/views/teacher/course/NotificationManagement.vue'
|
||||
import GeneralManagement from '@/views/teacher/course/GeneralManagement.vue'
|
||||
import UserAgreement from '@/views/UserAgreement.vue'
|
||||
import RecycleBin from '@/views/teacher/resource/RecycleBin.vue'
|
||||
|
||||
// 作业子组件
|
||||
import HomeworkLibrary from '@/views/teacher/course/HomeworkLibrary.vue'
|
||||
@ -310,6 +311,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: MyResources,
|
||||
meta: { title: '我的资源' }
|
||||
},
|
||||
{
|
||||
path: 'recycle-bin',
|
||||
name: 'RecycleBin',
|
||||
component: RecycleBin,
|
||||
meta: { title: '回收站' }
|
||||
},
|
||||
{
|
||||
path: 'student-management',
|
||||
name: 'StudentManagement',
|
||||
|
@ -589,6 +589,21 @@ const breadcrumbPathItems = computed(() => {
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
// 回收站页面的面包屑逻辑
|
||||
if (currentPath.includes('recycle-bin')) {
|
||||
const breadcrumbs = [
|
||||
{
|
||||
title: '我的资源',
|
||||
path: '/teacher/my-resources'
|
||||
},
|
||||
{
|
||||
title: '回收站',
|
||||
path: currentPath
|
||||
}
|
||||
]
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
|
||||
// 其他页面的面包屑逻辑
|
||||
const matchedRoutes = route.matched;
|
||||
|
@ -168,8 +168,10 @@ const handleConfirm = () => {
|
||||
background: #FFFFFF;
|
||||
background-size: 100% 100%;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
|
251
src/views/teacher/resource/RecycleBin.vue
Normal file
251
src/views/teacher/resource/RecycleBin.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="recycle-bin-page">
|
||||
<div class="page-header">
|
||||
<div class="page-title">回收站</div>
|
||||
<div class="header-actions">
|
||||
<button class="clear-btn" @click="clearBin">清空回收站</button>
|
||||
<button class="recover-selected-btn" @click="recoverSelected">恢复</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table">
|
||||
<div class="table-header">
|
||||
<div class="cell checkbox"><input type="checkbox" v-model="selectAll" @change="toggleAll"></div>
|
||||
<div class="cell name">文件名</div>
|
||||
<div class="cell size">大小</div>
|
||||
<div class="cell ops"></div>
|
||||
<div class="cell deleted-at">删除时间</div>
|
||||
<div class="cell ttl">有效期</div>
|
||||
</div>
|
||||
<div class="table-row" v-for="item in items" :key="item.id">
|
||||
<div class="cell checkbox"><input type="checkbox" v-model="selectedIds" :value="item.id"></div>
|
||||
<div class="cell name">
|
||||
<img class="folder" src="/images/profile/folder.png" alt="folder">
|
||||
<span class="filename">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="cell size">{{ item.size }}</div>
|
||||
<div class="cell ops">
|
||||
<button class="link" @click="recover(item)">恢复</button>
|
||||
<button class="link danger" @click="remove(item)">删除</button>
|
||||
</div>
|
||||
<div class="cell deleted-at">{{ item.deletedAt }}</div>
|
||||
<div class="cell ttl">{{ item.ttl }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RecycleConfirmModal :visible="modalVisible" :title="modalTitle" :message="modalMessage" @cancel="cancelModal" @confirm="confirmModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import RecycleConfirmModal from '@/views/teacher/resource/RecycleConfirmModal.vue'
|
||||
|
||||
interface BinItem {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
deletedAt: string
|
||||
ttl: string
|
||||
}
|
||||
|
||||
const items = ref<BinItem[]>([
|
||||
{ id: '1', name: '文件名称名称文件名称名称', size: '12kb', deletedAt: '2025.08.20 09:15', ttl: '10天' }
|
||||
])
|
||||
|
||||
const selectedIds = ref<string[]>([])
|
||||
const selectAll = computed({
|
||||
get: () => selectedIds.value.length > 0 && selectedIds.value.length === items.value.length,
|
||||
set: (val: boolean) => {
|
||||
if (val) selectedIds.value = items.value.map(i => i.id)
|
||||
else selectedIds.value = []
|
||||
}
|
||||
})
|
||||
|
||||
const toggleAll = () => {
|
||||
selectAll.value = !selectAll.value
|
||||
}
|
||||
|
||||
// modal state
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const modalMessage = ref('')
|
||||
let modalAction: null | (() => void) = null
|
||||
|
||||
const openModal = (title: string, message: string, onConfirm: () => void) => {
|
||||
modalTitle.value = title
|
||||
modalMessage.value = message
|
||||
modalAction = onConfirm
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const confirmModal = () => {
|
||||
if (modalAction) modalAction()
|
||||
modalVisible.value = false
|
||||
modalAction = null
|
||||
}
|
||||
|
||||
const cancelModal = () => {
|
||||
modalVisible.value = false
|
||||
modalAction = null
|
||||
}
|
||||
|
||||
const recover = (item: BinItem) => {
|
||||
openModal('恢复', '确认还原选中的文件?', () => {
|
||||
console.log('recover', item)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = (item: BinItem) => {
|
||||
openModal('彻底删除', '文件删除后将无法恢复,您确认要彻底删除所选文件吗?', () => {
|
||||
console.log('remove', item)
|
||||
})
|
||||
}
|
||||
|
||||
const recoverSelected = () => {
|
||||
openModal('恢复', '确认还原选中的文件?', () => {
|
||||
console.log('recover selected', selectedIds.value)
|
||||
})
|
||||
}
|
||||
|
||||
const clearBin = () => {
|
||||
openModal('清空回收站', '确认清空回收站?', () => {
|
||||
console.log('clear bin')
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recycle-bin-page {
|
||||
padding: 20px 30px;
|
||||
background: #fff;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.clear-btn,
|
||||
.recover-selected-btn {
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: #0288D1;
|
||||
color: #fff;
|
||||
border: 1px solid #0288D1;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
background: #0277BD;
|
||||
border-color: #0277BD;
|
||||
}
|
||||
|
||||
.recover-selected-btn {
|
||||
background: transparent;
|
||||
color: #0288D1;
|
||||
border: 1px solid #0288D1;
|
||||
}
|
||||
|
||||
.recover-selected-btn:hover {
|
||||
background: #F0F9FF;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr 160px 220px 220px 120px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.table-header {
|
||||
background: #FCFCFC;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
height: 42px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
height: 46px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: #F5F8FB;
|
||||
}
|
||||
|
||||
.cell.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #062333;
|
||||
}
|
||||
|
||||
.cell.size,
|
||||
.cell.deleted-at,
|
||||
.cell.ttl {
|
||||
color: #062333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cell.ops {
|
||||
display: flex;
|
||||
gap: 62px;
|
||||
}
|
||||
|
||||
.link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #0288D1;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.link.danger {
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.folder {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.filename {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
117
src/views/teacher/resource/RecycleConfirmModal.vue
Normal file
117
src/views/teacher/resource/RecycleConfirmModal.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div v-if="visible" class="recycle-modal-mask" @click.self="emit('cancel')">
|
||||
<div class="recycle-modal">
|
||||
<div class="recycle-modal__header">
|
||||
<div class="recycle-modal__title">{{ title }}</div>
|
||||
</div>
|
||||
<div class="recycle-modal__body">
|
||||
<slot name="body">
|
||||
<p class="recycle-modal__message">{{ message }}</p>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="recycle-modal__footer">
|
||||
<button class="btn btn-ghost" @click="emit('cancel')">取消</button>
|
||||
<button class="btn btn-primary" @click="emit('confirm')">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title: string
|
||||
message: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits(['confirm', 'cancel'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recycle-modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 4000;
|
||||
}
|
||||
|
||||
.recycle-modal {
|
||||
width: 560px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
min-height: 315px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recycle-modal__header {
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1.5px solid #EFF0F5;
|
||||
}
|
||||
|
||||
.recycle-modal__title {
|
||||
font-size: 16px;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.recycle-modal__body {
|
||||
padding: 28px 24px;
|
||||
}
|
||||
|
||||
.recycle-modal__message {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #062333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recycle-modal__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
.btn.btn-ghost {
|
||||
width: 66px;
|
||||
height: 32px;
|
||||
background: #E2F5FF;
|
||||
border: 1px solid #0288D1;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
.btn.btn-ghost:hover {
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
background: #0288D1;
|
||||
border: 1px solid #0288D1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.btn-primary:hover {
|
||||
background: #0277BD;
|
||||
border-color: #0277BD;
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user