feat: 完成“我的资源”页面,功能

This commit is contained in:
QDKF 2025-09-08 19:15:49 +08:00
parent 7de929ac18
commit 2c1fb5ab7f
8 changed files with 739 additions and 179 deletions

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

View File

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

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

View File

@ -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',

View File

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

View File

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

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

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