feat(upload): 上传组件删除前需二次确认,支持ZIP/RAR/7Z压缩包上传

This commit is contained in:
DXC
2026-05-12 15:34:26 +08:00
parent 1edd471208
commit 6b4ebfa24f
2 changed files with 42 additions and 12 deletions

View File

@ -25,10 +25,10 @@ BASE_DIR = get_project_root()
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads') UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
# 允许上传的文件后缀 # 允许上传的文件后缀
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx'} ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'zip', 'rar', '7z'}
# ★ 文件上传安全加固:限制最大文件大小 (10MB) # ★ 文件上传安全加固:限制最大文件大小 (50MB,支持压缩包)
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
def allowed_file(filename): def allowed_file(filename):
@ -68,7 +68,7 @@ def upload_file():
if file_size > MAX_CONTENT_LENGTH: if file_size > MAX_CONTENT_LENGTH:
return jsonify({ return jsonify({
"code": 400, "code": 400,
"msg": f"文件大小超过限制 ({MAX_CONTENT_LENGTH // (1024*1024)}MB)" "msg": f"文件大小超过限制(最大 50MB"
}), 400 }), 400
if file and allowed_file(file.filename): if file and allowed_file(file.filename):

View File

@ -273,7 +273,8 @@
<!-- 非图片文件 --> <!-- 非图片文件 -->
<div v-for="(link, idx) in row.generalManual.filter(l => !isExternalLink(l) && !isImageFile(l))" :key="'file-' + idx"> <div v-for="(link, idx) in row.generalManual.filter(l => !isExternalLink(l) && !isImageFile(l))" :key="'file-' + idx">
<el-link @click.prevent="handleDownloadConfirm(link)" type="info" :underline="false"> <el-link @click.prevent="handleDownloadConfirm(link)" type="info" :underline="false">
<el-icon><Files /></el-icon> <el-icon v-if="isCompressedFile(link)"><Zipper /></el-icon>
<el-icon v-else><Files /></el-icon>
{{ link.split('/').pop() }} {{ link.split('/').pop() }}
</el-link> </el-link>
</div> </div>
@ -515,7 +516,7 @@
<span class="el-upload-list__item-preview" @click="handlePreviewPicture(file)"> <span class="el-upload-list__item-preview" @click="handlePreviewPicture(file)">
<el-icon><ZoomIn /></el-icon> <el-icon><ZoomIn /></el-icon>
</span> </span>
<span class="el-upload-list__item-delete" @click="() => handleRemoveImage(file, 'generalManual')"> <span class="el-upload-list__item-delete" @click.stop.prevent="() => handleRemoveImage(file, 'generalManual')">
<el-icon><Delete /></el-icon> <el-icon><Delete /></el-icon>
</span> </span>
</span> </span>
@ -1534,6 +1535,7 @@ const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) } const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') } const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url) } const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url) }
const isCompressedFile = (url: string) => { return /\.(zip|rar|7z)$/i.test(url) }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && isImageFile(item)) } const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && isImageFile(item)) }
const getNonImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && !isImageFile(item)) } const getNonImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && !isImageFile(item)) }
const truncateFileName = (name: string, maxLen = 12) => { return name.length > maxLen ? name.slice(0, maxLen - 3) + '...' : name } const truncateFileName = (name: string, maxLen = 12) => { return name.length > maxLen ? name.slice(0, maxLen - 3) + '...' : name }
@ -1550,13 +1552,23 @@ const handleDownloadConfirm = (link: string) => {
} }
const beforeAvatarUpload = (rawFile: any) => { const beforeAvatarUpload = (rawFile: any) => {
const isTypeValid = ['image/jpeg', 'image/png', 'application/pdf'].includes(rawFile.type); const isTypeValid = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp',
'application/pdf',
'application/zip', 'application/x-zip-compressed',
'application/x-rar-compressed',
'application/vnd.rar',
'application/x-7z-compressed',
'application/octet-stream' // 兼容某些浏览器对 .zip/.rar 的错误识别
].includes(rawFile.type);
if (!isTypeValid) { if (!isTypeValid) {
ElMessage.error('仅支持 JPG/PNG/PDF'); ElMessage.error('仅支持 JPG/PNG/GIF/PDF/ZIP/RAR/7Z');
return false; return false;
} }
if (rawFile.size / 1024 / 1024 > 10) { const isCompressed = ['application/zip', 'application/x-zip-compressed', 'application/x-rar-compressed', 'application/vnd.rar', 'application/x-7z-compressed'].includes(rawFile.type);
ElMessage.error('文件不能超过 10MB'); const maxMB = isCompressed ? 50 : 10;
if (rawFile.size / 1024 / 1024 > maxMB) {
ElMessage.error(`文件不能超过 ${maxMB}MB`);
return false; return false;
} }
return true; return true;
@ -1586,6 +1598,21 @@ const customUpload = async (options: any, targetField: 'generalImage' | 'general
} }
const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' | 'generalManual') => { const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' | 'generalManual') => {
const fileName = uploadFile.name || uploadFile.url?.split('/').pop() || '此文件'
try {
await ElMessageBox.confirm(
`确认要删除「${fileName}」吗?删除后不可恢复。`,
'删除确认',
{
confirmButtonText: '确认删除',
cancelButtonText: '取消',
type: 'warning'
}
)
} catch {
return // 用户取消,不删除
}
try { try {
const urlToRemove = form.value[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url const urlToRemove = form.value[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
form.value[targetField] = form.value[targetField].filter(u => u !== urlToRemove) form.value[targetField] = form.value[targetField].filter(u => u !== urlToRemove)
@ -1593,8 +1620,11 @@ const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' |
const filename = urlToRemove.split('/').pop(); const filename = urlToRemove.split('/').pop();
if (filename) await deleteFile(filename) if (filename) await deleteFile(filename)
} }
ElMessage.success('已从列表移除') ElMessage.success('已除')
} catch (e) { console.error(e) } } catch (e) {
console.error(e)
ElMessage.error('删除失败')
}
} }
const handlePreviewPicture = (uploadFile: any) => { const handlePreviewPicture = (uploadFile: any) => {