feat:新增文件列表右键功能(复制链接、共享文件、删除文件)

This commit is contained in:
czhqwer
2025-03-29 22:13:13 +08:00
parent ff03724c71
commit 1782ac2a4c
8 changed files with 224 additions and 39 deletions

View File

@@ -102,8 +102,17 @@ public class FileController {
return Result.success(fileService.pageFiles(page, pageSize, storageType, fileName));
}
@PostMapping("/sharedFile")
public Result<?> listSharedFiles(@RequestParam String fileIdentifier) {
@PostMapping("/deleteFile")
public Result<?> deleteFile(HttpServletRequest request, @RequestParam String fileIdentifier) {
if (!authService.isMainUser(request.getRemoteAddr())) {
return Result.error("权限不足");
}
fileService.deleteFile(fileIdentifier);
return Result.success();
}
@PostMapping("/addSharedFile")
public Result<?> addSharedFile(@RequestParam String fileIdentifier) {
fileService.addSharedFile(fileIdentifier);
return Result.success();
}

View File

@@ -15,4 +15,5 @@ public interface IFileService {
void removeSharedFile(String fileIdentifier);
void deleteFile(String fileIdentifier);
}

View File

@@ -1,5 +1,6 @@
package cn.czh.service.impl;
import cn.czh.base.BusinessException;
import cn.czh.entity.SharedFile;
import cn.czh.entity.UploadFile;
import cn.czh.mapper.ShareFileMapper;
@@ -13,9 +14,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
@Service
@@ -35,18 +34,29 @@ public class FileServiceImpl implements IFileService {
public List<UploadFile> listSharedFiles() {
List<SharedFile> sharedFiles = shareFileMapper.selectList(null);
Set<String> files = sharedFiles.stream()
List<String> files = sharedFiles.stream()
.map(SharedFile::getFileIdentifier)
.collect(Collectors.toSet());
.collect(Collectors.toList());
List<UploadFile> uploadFiles = uploadFileMapper.selectList(null);
if (uploadFiles != null) {
return uploadFiles.stream()
.filter(uploadFile -> files.contains(uploadFile.getFileIdentifier()))
.collect(Collectors.toList());
if (uploadFiles == null) {
return Collections.emptyList();
}
return Collections.emptyList();
Map<String, UploadFile> uploadFileMap = uploadFiles.stream()
.collect(Collectors.toMap(
UploadFile::getFileIdentifier,
uploadFile -> uploadFile,
(oldValue, newValue) -> oldValue,
LinkedHashMap::new
));
return files.stream()
.filter(uploadFileMap::containsKey)
.map(uploadFileMap::get)
.collect(Collectors.toList());
}
@Override
@@ -67,4 +77,13 @@ public class FileServiceImpl implements IFileService {
public void removeSharedFile(String fileIdentifier) {
shareFileMapper.delete(Wrappers.lambdaQuery(SharedFile.class).eq(SharedFile::getFileIdentifier, fileIdentifier));
}
@Override
public void deleteFile(String fileIdentifier) {
SharedFile sharedFile = shareFileMapper.selectOne(Wrappers.lambdaQuery(SharedFile.class).eq(SharedFile::getFileIdentifier, fileIdentifier));
if (sharedFile != null) {
throw new BusinessException("文件正在分享中,无法删除");
}
uploadFileMapper.delete(Wrappers.lambdaQuery(UploadFile.class).eq(UploadFile::getFileIdentifier, fileIdentifier));
}
}

View File

@@ -12,6 +12,7 @@
"axios-extra": "^0.0.8",
"core-js": "^3.8.3",
"element-ui": "^2.15.14",
"os-browserify": "^0.3.0",
"pdfjs-dist": "^5.0.375",
"promise-queue-plus": "^1.2.2",
"spark-md5": "^3.0.2",

View File

@@ -0,0 +1,103 @@
<!-- ContextMenu.vue -->
<template>
<div v-if="visible" class="context-menu" :style="{ top: top + 'px', left: left + 'px' }" @click.stop>
<div class="menu-item" @click="copyLink">复制链接</div>
<div class="menu-item" @click="shareFile">共享文件</div>
<div class="menu-item" @click="deleteFile">删除文件</div>
</div>
</template>
<script>
export default {
props: {
file: {
type: Object,
required: true
},
top: {
type: Number,
required: true
},
left: {
type: Number,
required: true
}
},
data() {
return {
visible: false
};
},
watch: {
file: {
handler(newFile) {
if (newFile) {
this.show();
} else {
this.hide();
}
},
immediate: true
}
},
methods: {
show() {
this.visible = true;
},
hide() {
this.visible = false;
},
copyLink() {
this.$emit('copy-link', this.file.accessUrl);
this.hide();
},
shareFile() {
this.$emit('share-file', this.file);
this.hide();
},
deleteFile() {
this.$emit('delete-file', this.file);
this.hide();
},
}
};
</script>
<style scoped>
.context-menu {
position: fixed;
background: linear-gradient(135deg, #ffffff, #f8faff);
border: 1px solid rgba(64, 158, 255, 0.15);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 50, 150, 0.15);
z-index: 1000;
padding: 8px 0;
min-width: 140px;
animation: fadeIn 0.2s ease-out;
}
.menu-item {
padding: 10px 20px;
font-size: 14px;
color: #2c3e50;
cursor: pointer;
transition: all 0.2s ease;
}
.menu-item:hover {
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
color: #1976d2;
transform: translateX(4px);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>

View File

@@ -1,5 +1,6 @@
<!-- FileGallery.vue -->
<template>
<div class="gallery-container" @scroll="handleScroll">
<div class="gallery-container" @scroll="handleScroll" @contextmenu.prevent>
<div class="gallery-header">
<label for="storage-select">存储类型:</label>
<select id="storage-select" v-model="selectedStorageType" @change="fetchFiles(true)">
@@ -18,7 +19,8 @@
加载中...
</div>
<div v-else class="file-grid">
<div v-for="file in filePage" :key="file.id" class="file-card">
<div v-for="file in filePage" :key="file.id" class="file-card"
@contextmenu.prevent="showContextMenu($event, file)">
<div class="preview-wrapper">
<img v-if="isImage(file)" :src="file.accessUrl" :alt="file.fileName" class="preview-image"
@click="openImagePreview(file)" loading="lazy" />
@@ -29,7 +31,7 @@
<div class="pdf-icon">📜</div>
</div>
<div v-else class="file-icon">📄</div>
<div class="copy-button" @click.stop="copyUrl(file.accessUrl)" title="复制文件URL">📋</div>
<!-- <div class="copy-button" @click.stop="copyUrl(file.accessUrl)" title="复制文件URL">📋</div> -->
</div>
<div class="file-meta">
<div class="filename">{{ file.fileName }}</div>
@@ -39,6 +41,10 @@
</div>
</div>
</div>
<context-menu v-if="contextMenu.file" :file="contextMenu.file" :top="contextMenu.top" :left="contextMenu.left"
@copy-link="copyUrl" @share-file="shareFile" @delete-file="deleteFile" />
</div>
<div v-if="loading && filePage.length > 0" class="loading-more">加载更多...</div>
@@ -69,10 +75,14 @@
</template>
<script>
import { pageFiles } from '@/utils/api';
import ContextMenu from '../ContextMenu/ContextMenu.vue';
import { pageFiles, addSharedFile, deleteFile } from '@/utils/api';
export default {
name: 'FileGallery',
components: {
ContextMenu,
},
props: {
storageOptions: {
type: Array,
@@ -98,7 +108,12 @@ export default {
selectedVideoUrl: '',
showPdfPreview: false,
selectedPdfUrl: '',
loadingPdf: false
loadingPdf: false,
contextMenu: {
file: null,
top: 0,
left: 0
}
};
},
methods: {
@@ -190,10 +205,45 @@ export default {
console.error('复制URL失败:', error);
this.$message.error('复制URL失败');
}
},
showContextMenu(event, file) {
this.contextMenu.file = file;
this.contextMenu.top = event.clientY;
this.contextMenu.left = event.clientX;
},
async shareFile(file) {
const res = await addSharedFile(file.fileIdentifier);
if (res.code === 200) {
this.$message.success('分享成功');
}
},
deleteFile(file) {
this.$confirm('确定要删除此文件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await deleteFile(file.fileIdentifier);
if (res.code === 200) {
this.fetchFiles(true);
this.$message.success('删除成功');
} else {
this.$message.error(res.msg);
}
}).catch(() => {
this.$message.info('已取消删除');
});
},
hideContextMenu() {
this.contextMenu.file = null;
}
},
mounted() {
document.addEventListener('click', this.hideContextMenu);
this.fetchFiles(true);
},
beforeDestroy() {
document.removeEventListener('click', this.hideContextMenu);
}
};
</script>
@@ -263,27 +313,6 @@ export default {
box-shadow: 0 3px 8px rgba(25, 118, 210, 0.2);
}
.copy-button {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 14px;
opacity: 0;
transition: opacity 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.file-card:hover .copy-button {
opacity: 1;
}
.preview-wrapper {
position: relative;
padding-top: 100%;

View File

@@ -35,7 +35,6 @@
width="30%"
:close-on-click-modal="false"
:show-close="false"
:before-close="handleDialogClose"
custom-class="password-dialog"
>
<el-input

View File

@@ -229,6 +229,28 @@ const unShareFile = (fileIdentifier) => {
return http.post("/file/unShareFile", formData);
}
/**
* 添加共享文件
* @param {string} fileIdentifier
* @returns
*/
const addSharedFile = (fileIdentifier) => {
const formData = new FormData();
formData.append('fileIdentifier', fileIdentifier);
return http.post("/file/addSharedFile", formData);
}
/**
* 删除文件
* @param {string} fileIdentifier
* @returns
*/
const deleteFile = (fileIdentifier) => {
const formData = new FormData();
formData.append('fileIdentifier', fileIdentifier);
return http.post("/file/deleteFile", formData);
}
/**
* 获取密码
* @returns
@@ -265,5 +287,7 @@ export {
unShareFile,
getPassword,
setPassword,
addSharedFile,
deleteFile,
httpExtra
};