mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-05-06 20:32:48 +08:00
feat:新增文件列表右键功能(复制链接、共享文件、删除文件)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ public interface IFileService {
|
||||
|
||||
void removeSharedFile(String fileIdentifier);
|
||||
|
||||
void deleteFile(String fileIdentifier);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
103
upload-file-frontend/src/components/ContextMenu/ContextMenu.vue
Normal file
103
upload-file-frontend/src/components/ContextMenu/ContextMenu.vue
Normal 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>
|
||||
@@ -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%;
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
width="30%"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
:before-close="handleDialogClose"
|
||||
custom-class="password-dialog"
|
||||
>
|
||||
<el-input
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user