mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-09-05 15:38:25 +08:00
feat:新增文件列表右键功能(复制链接、共享文件、删除文件)
This commit is contained in:
@@ -102,8 +102,17 @@ public class FileController {
|
|||||||
return Result.success(fileService.pageFiles(page, pageSize, storageType, fileName));
|
return Result.success(fileService.pageFiles(page, pageSize, storageType, fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/sharedFile")
|
@PostMapping("/deleteFile")
|
||||||
public Result<?> listSharedFiles(@RequestParam String fileIdentifier) {
|
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);
|
fileService.addSharedFile(fileIdentifier);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ public interface IFileService {
|
|||||||
|
|
||||||
void removeSharedFile(String fileIdentifier);
|
void removeSharedFile(String fileIdentifier);
|
||||||
|
|
||||||
|
void deleteFile(String fileIdentifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package cn.czh.service.impl;
|
package cn.czh.service.impl;
|
||||||
|
|
||||||
|
import cn.czh.base.BusinessException;
|
||||||
import cn.czh.entity.SharedFile;
|
import cn.czh.entity.SharedFile;
|
||||||
import cn.czh.entity.UploadFile;
|
import cn.czh.entity.UploadFile;
|
||||||
import cn.czh.mapper.ShareFileMapper;
|
import cn.czh.mapper.ShareFileMapper;
|
||||||
@@ -13,9 +14,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -35,18 +34,29 @@ public class FileServiceImpl implements IFileService {
|
|||||||
public List<UploadFile> listSharedFiles() {
|
public List<UploadFile> listSharedFiles() {
|
||||||
|
|
||||||
List<SharedFile> sharedFiles = shareFileMapper.selectList(null);
|
List<SharedFile> sharedFiles = shareFileMapper.selectList(null);
|
||||||
Set<String> files = sharedFiles.stream()
|
List<String> files = sharedFiles.stream()
|
||||||
.map(SharedFile::getFileIdentifier)
|
.map(SharedFile::getFileIdentifier)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
List<UploadFile> uploadFiles = uploadFileMapper.selectList(null);
|
List<UploadFile> uploadFiles = uploadFileMapper.selectList(null);
|
||||||
if (uploadFiles != null) {
|
if (uploadFiles == null) {
|
||||||
return uploadFiles.stream()
|
return Collections.emptyList();
|
||||||
.filter(uploadFile -> files.contains(uploadFile.getFileIdentifier()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@Override
|
||||||
@@ -67,4 +77,13 @@ public class FileServiceImpl implements IFileService {
|
|||||||
public void removeSharedFile(String fileIdentifier) {
|
public void removeSharedFile(String fileIdentifier) {
|
||||||
shareFileMapper.delete(Wrappers.lambdaQuery(SharedFile.class).eq(SharedFile::getFileIdentifier, 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",
|
"axios-extra": "^0.0.8",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"element-ui": "^2.15.14",
|
"element-ui": "^2.15.14",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
"pdfjs-dist": "^5.0.375",
|
"pdfjs-dist": "^5.0.375",
|
||||||
"promise-queue-plus": "^1.2.2",
|
"promise-queue-plus": "^1.2.2",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
|
|||||||
@@ -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>
|
<template>
|
||||||
<div class="gallery-container" @scroll="handleScroll">
|
<div class="gallery-container" @scroll="handleScroll" @contextmenu.prevent>
|
||||||
<div class="gallery-header">
|
<div class="gallery-header">
|
||||||
<label for="storage-select">存储类型:</label>
|
<label for="storage-select">存储类型:</label>
|
||||||
<select id="storage-select" v-model="selectedStorageType" @change="fetchFiles(true)">
|
<select id="storage-select" v-model="selectedStorageType" @change="fetchFiles(true)">
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
加载中...
|
加载中...
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="file-grid">
|
<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">
|
<div class="preview-wrapper">
|
||||||
<img v-if="isImage(file)" :src="file.accessUrl" :alt="file.fileName" class="preview-image"
|
<img v-if="isImage(file)" :src="file.accessUrl" :alt="file.fileName" class="preview-image"
|
||||||
@click="openImagePreview(file)" loading="lazy" />
|
@click="openImagePreview(file)" loading="lazy" />
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
<div class="pdf-icon">📜</div>
|
<div class="pdf-icon">📜</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="file-icon">📄</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>
|
||||||
<div class="file-meta">
|
<div class="file-meta">
|
||||||
<div class="filename">{{ file.fileName }}</div>
|
<div class="filename">{{ file.fileName }}</div>
|
||||||
@@ -39,6 +41,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
<div v-if="loading && filePage.length > 0" class="loading-more">加载更多...</div>
|
<div v-if="loading && filePage.length > 0" class="loading-more">加载更多...</div>
|
||||||
|
|
||||||
@@ -69,10 +75,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { pageFiles } from '@/utils/api';
|
import ContextMenu from '../ContextMenu/ContextMenu.vue';
|
||||||
|
import { pageFiles, addSharedFile, deleteFile } from '@/utils/api';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileGallery',
|
name: 'FileGallery',
|
||||||
|
components: {
|
||||||
|
ContextMenu,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
storageOptions: {
|
storageOptions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -98,7 +108,12 @@ export default {
|
|||||||
selectedVideoUrl: '',
|
selectedVideoUrl: '',
|
||||||
showPdfPreview: false,
|
showPdfPreview: false,
|
||||||
selectedPdfUrl: '',
|
selectedPdfUrl: '',
|
||||||
loadingPdf: false
|
loadingPdf: false,
|
||||||
|
contextMenu: {
|
||||||
|
file: null,
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -190,10 +205,45 @@ export default {
|
|||||||
console.error('复制URL失败:', error);
|
console.error('复制URL失败:', error);
|
||||||
this.$message.error('复制URL失败');
|
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() {
|
mounted() {
|
||||||
|
document.addEventListener('click', this.hideContextMenu);
|
||||||
this.fetchFiles(true);
|
this.fetchFiles(true);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
document.removeEventListener('click', this.hideContextMenu);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -263,27 +313,6 @@ export default {
|
|||||||
box-shadow: 0 3px 8px rgba(25, 118, 210, 0.2);
|
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 {
|
.preview-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 100%;
|
padding-top: 100%;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
width="30%"
|
width="30%"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
:before-close="handleDialogClose"
|
|
||||||
custom-class="password-dialog"
|
custom-class="password-dialog"
|
||||||
>
|
>
|
||||||
<el-input
|
<el-input
|
||||||
|
|||||||
@@ -229,6 +229,28 @@ const unShareFile = (fileIdentifier) => {
|
|||||||
return http.post("/file/unShareFile", formData);
|
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
|
* @returns
|
||||||
@@ -265,5 +287,7 @@ export {
|
|||||||
unShareFile,
|
unShareFile,
|
||||||
getPassword,
|
getPassword,
|
||||||
setPassword,
|
setPassword,
|
||||||
|
addSharedFile,
|
||||||
|
deleteFile,
|
||||||
httpExtra
|
httpExtra
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user