From 662de358adba373beaa4c8fc0b81bb64469fd233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B3=BD=E5=A8=81?= <958142070@qq.com> Date: Tue, 3 Mar 2026 11:06:05 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=88=86?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=E9=AB=98=E7=BA=A7=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加高级配置切换按钮,支持展开和收起高级选项 - 新增分片大小(字节)和分片临时地址两个配置字段 - 分片临时地址仅在类型为1时显示 - 重置表单时自动关闭高级配置面板 - 提交时对分片大小和分片临时地址进行有效性处理,空值使用默认配置 - 接口请求参数中新增分片上传相关字段 - 添加相关样式以优化高级配置项的展示效果 --- src/apis/system/type.ts | 2 + src/views/system/config/storage/AddModal.vue | 87 +++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/apis/system/type.ts b/src/apis/system/type.ts index 3aecca2..18be878 100644 --- a/src/apis/system/type.ts +++ b/src/apis/system/type.ts @@ -269,6 +269,8 @@ export interface StorageResp { endpoint: string bucketName: string domain: string + multipartUploadPartSize?: number | null + multipartTempDir?: string | null recycleBinEnabled: boolean recycleBinPath: string description: string diff --git a/src/views/system/config/storage/AddModal.vue b/src/views/system/config/storage/AddModal.vue index 1cf97ab..6c6da3d 100644 --- a/src/views/system/config/storage/AddModal.vue +++ b/src/views/system/config/storage/AddModal.vue @@ -16,6 +16,13 @@ /> +
+ + 高级配置 + + +
+ @@ -40,10 +47,13 @@ const isUpdate = computed(() => !!dataId.value) const storageType = ref('') const title = computed(() => (isUpdate.value ? `修改${storageType.value}` : `新增${storageType.value}`)) const formRef = ref>() +const advancedVisible = ref(false) const { storage_type_enum } = useDict('storage_type_enum') const [form, resetForm] = useResetReactive({ type: 2, + multipartUploadPartSize: undefined, + multipartTempDir: '', recycleBinEnabled: true, recycleBinPath: '.RECYCLE.BIN/', isDefault: false, @@ -183,27 +193,68 @@ const columns: ColumnItem[] = reactive([ }, ]) +const advancedColumns: ColumnItem[] = reactive([ + { + label: '分片大小(字节)', + field: 'multipartUploadPartSize', + type: 'input-number', + span: 24, + props: { + min: 1, + precision: 0, + placeholder: '为空则使用默认配置', + mode: 'button', + }, + }, + { + label: '分片临时地址', + field: 'multipartTempDir', + type: 'input', + span: 24, + props: { + maxLength: 255, + placeholder: '为空则使用默认配置', + }, + show: () => form.type === 1, + }, +]) + // 重置 const reset = () => { formRef.value?.formRef?.resetFields() + advancedVisible.value = false resetForm() } +const buildPayload = () => { + const multipartUploadPartSize = Number(form.multipartUploadPartSize) + return { + ...form, + multipartUploadPartSize: Number.isFinite(multipartUploadPartSize) && multipartUploadPartSize > 0 + ? multipartUploadPartSize + : null, + multipartTempDir: form.type === 1 && form.multipartTempDir?.trim() + ? form.multipartTempDir.trim() + : null, + } +} + // 保存 const save = async () => { try { const isInvalid = await formRef.value?.formRef?.validate() if (isInvalid) return false + const payload = buildPayload() if (isUpdate.value) { await updateStorage({ - ...form, - secretKey: form.type === 2 && form.secretKey ? encryptByRsa(form.secretKey) || '' : null, + ...payload, + secretKey: payload.type === 2 && payload.secretKey ? encryptByRsa(payload.secretKey) || '' : null, }, dataId.value) Message.success('修改成功') } else { await addStorage({ - ...form, - secretKey: form.type === 2 ? encryptByRsa(form.secretKey) || '' : form.secretKey, + ...payload, + secretKey: payload.type === 2 ? encryptByRsa(payload.secretKey) || '' : payload.secretKey, }) Message.success('新增成功') } @@ -235,3 +286,31 @@ const onUpdate = async (id: string) => { defineExpose({ onAdd, onUpdate }) + + -- Gitee From 0012feee255bc954210ea17cf4d2d36a673acdb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B3=BD=E5=A8=81?= <958142070@qq.com> Date: Tue, 3 Mar 2026 11:26:20 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(storage):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=88=86=E7=89=87=E9=98=88=E5=80=BC=E9=85=8D=E7=BD=AE=E5=8F=8A?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 multipartUploadThreshold 字段支持分片阈值(MB)配置 - 调整 multipartUploadPartSize 精度支持小数点后两位 - 新增字节与兆字节转换工具函数 toMb 和 toBytes - 添加分片阈值和分片大小的业务校验逻辑,保证配置合理性 - 修改数据回显时进行分片阈值与分片大小的单位转换 - 发送接口时将分片阈值和分片大小统一转换为字节数 - 类型定义中新增 multipartUploadThreshold 可选字段支持 --- src/apis/system/type.ts | 1 + src/views/system/config/storage/AddModal.vue | 64 +++++++++++++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/apis/system/type.ts b/src/apis/system/type.ts index 18be878..2f4ae78 100644 --- a/src/apis/system/type.ts +++ b/src/apis/system/type.ts @@ -269,6 +269,7 @@ export interface StorageResp { endpoint: string bucketName: string domain: string + multipartUploadThreshold?: number | null multipartUploadPartSize?: number | null multipartTempDir?: string | null recycleBinEnabled: boolean diff --git a/src/views/system/config/storage/AddModal.vue b/src/views/system/config/storage/AddModal.vue index 6c6da3d..6d296b1 100644 --- a/src/views/system/config/storage/AddModal.vue +++ b/src/views/system/config/storage/AddModal.vue @@ -49,9 +49,11 @@ const title = computed(() => (isUpdate.value ? `修改${storageType.value}` : ` const formRef = ref>() const advancedVisible = ref(false) const { storage_type_enum } = useDict('storage_type_enum') +const MB = 1024 * 1024 const [form, resetForm] = useResetReactive({ type: 2, + multipartUploadThreshold: undefined, multipartUploadPartSize: undefined, multipartTempDir: '', recycleBinEnabled: true, @@ -195,13 +197,25 @@ const columns: ColumnItem[] = reactive([ const advancedColumns: ColumnItem[] = reactive([ { - label: '分片大小(字节)', + label: '分片阈值(MB)', + field: 'multipartUploadThreshold', + type: 'input-number', + span: 24, + props: { + min: 1, + precision: 2, + placeholder: '为空则使用默认配置', + mode: 'button', + }, + }, + { + label: '分片大小(MB)', field: 'multipartUploadPartSize', type: 'input-number', span: 24, props: { min: 1, - precision: 0, + precision: 2, placeholder: '为空则使用默认配置', mode: 'button', }, @@ -226,13 +240,44 @@ const reset = () => { resetForm() } +const toMb = (bytes?: number | null) => { + if (typeof bytes !== 'number' || bytes <= 0) return undefined + return Number((bytes / MB).toFixed(2)) +} + +const toBytes = (mb?: number | null) => { + const value = Number(mb) + if (!Number.isFinite(value) || value <= 0) return null + return Math.round(value * MB) +} + +const validateAdvancedConfig = () => { + const thresholdMb = Number(form.multipartUploadThreshold) + const partSizeMb = Number(form.multipartUploadPartSize) + const hasThreshold = Number.isFinite(thresholdMb) && thresholdMb > 0 + const hasPartSize = Number.isFinite(partSizeMb) && partSizeMb > 0 + + if (hasPartSize) { + const minPartSizeMb = form.type === 2 ? 5 : 1 + if (partSizeMb < minPartSizeMb) { + Message.error(`分片大小不能小于 ${minPartSizeMb}MB`) + return false + } + } + + if (hasThreshold && hasPartSize && thresholdMb < partSizeMb) { + Message.error('分片阈值不能小于分片大小') + return false + } + + return true +} + const buildPayload = () => { - const multipartUploadPartSize = Number(form.multipartUploadPartSize) return { ...form, - multipartUploadPartSize: Number.isFinite(multipartUploadPartSize) && multipartUploadPartSize > 0 - ? multipartUploadPartSize - : null, + multipartUploadThreshold: toBytes(form.multipartUploadThreshold), + multipartUploadPartSize: toBytes(form.multipartUploadPartSize), multipartTempDir: form.type === 1 && form.multipartTempDir?.trim() ? form.multipartTempDir.trim() : null, @@ -244,6 +289,7 @@ const save = async () => { try { const isInvalid = await formRef.value?.formRef?.validate() if (isInvalid) return false + if (!validateAdvancedConfig()) return false const payload = buildPayload() if (isUpdate.value) { await updateStorage({ @@ -279,7 +325,11 @@ const onUpdate = async (id: string) => { reset() dataId.value = id const { data } = await getStorage(id) - Object.assign(form, data) + Object.assign(form, { + ...data, + multipartUploadThreshold: toMb(data.multipartUploadThreshold), + multipartUploadPartSize: toMb(data.multipartUploadPartSize), + }) storageType.value = storage_type_enum.value.find((item) => item.value === form.type)?.label || '本地存储' visible.value = true } -- Gitee From e7117b980fb60ae9b4e9d25f3e6b6ac99049d930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B3=BD=E5=A8=81?= <958142070@qq.com> Date: Tue, 3 Mar 2026 15:48:12 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8D=95=E6=96=87=E4=BB=B6=E6=88=96=E5=88=86=E7=89=87?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增默认存储上传配置接口及接口调用逻辑 - 实现自动判定单文件上传或分片上传的模式选择 - 增加单文件上传任务管理与进度轮询功能 - 优化分片上传暂停、恢复、取消控制方法 - 添加上传配置展示组件及界面优化 - 文件统计饼图新增图例交互及样式调整 - 上传页面移除普通上传下拉菜单,简化操作按钮 - 增强上传任务删除时的中断和状态清理功能 - 调整上传列表状态列样式支持错误详情换行显示 --- src/apis/system/file.ts | 15 +- src/apis/system/type.ts | 26 ++ src/components/FilePreview/index.vue | 2 +- src/components/MultipartUpload/index.vue | 118 ++++++- src/hooks/modules/useMultipartUploader.ts | 308 ++++++++++++------ src/layout/components/Tabs/index.vue | 1 - src/views/system/config/storage/AddModal.vue | 2 +- .../system/file/main/FileAsideStatistics.vue | 131 ++++++-- src/views/system/file/main/FileMain/index.vue | 74 +---- 9 files changed, 475 insertions(+), 202 deletions(-) diff --git a/src/apis/system/file.ts b/src/apis/system/file.ts index 56d7012..635c0fd 100644 --- a/src/apis/system/file.ts +++ b/src/apis/system/file.ts @@ -1,3 +1,4 @@ +import type { AxiosRequestConfig } from 'axios' import type * as T from './type' import http from '@/utils/http' @@ -7,8 +8,18 @@ const BASE_URL = '/system/file' const RECYCLE_URL = `${BASE_URL}/recycle` /** @desc 上传文件 */ -export function uploadFile(data: FormData) { - return http.post(`${BASE_URL}/upload`, data) +export function uploadFile(data: FormData, config?: AxiosRequestConfig) { + return http.post(`${BASE_URL}/upload`, data, config) +} + +/** @desc 查询默认存储上传配置 */ +export function getDefaultUploadConfig() { + return http.get(`${BASE_URL}/upload/config/default`) +} + +/** @desc 查询单文件上传进度 */ +export function getUploadProgress(uploadTaskId: string) { + return http.get(`${BASE_URL}/upload/progress/${uploadTaskId}`) } /** @desc 查询文件列表 */ diff --git a/src/apis/system/type.ts b/src/apis/system/type.ts index 2f4ae78..b988a60 100644 --- a/src/apis/system/type.ts +++ b/src/apis/system/type.ts @@ -249,6 +249,32 @@ export interface FileStatisticsResp { export interface FileDirCalcSizeResp { size: number } +export interface FileUploadResp { + id: string + url: string + thUrl: string + metadata: Record +} +export type StorageType = 1 | 2 +export interface FileUploadConfigResp { + storageId: string | number + storageName: string + storageCode: string + storageType: StorageType + multipartUploadThreshold: number + multipartUploadPartSize: number + multipartTempDir?: string | null +} +export interface FileUploadProgressResp { + uploadTaskId: string + status: 'INIT' | 'UPLOADING' | 'FINALIZING' | 'COMPLETED' | 'FAILED' | 'NOT_FOUND' + percentage: number + bytesRead: number + totalBytes: number + fileId?: string + url?: string + message?: string +} export interface FileQuery { originalName?: string type?: string diff --git a/src/components/FilePreview/index.vue b/src/components/FilePreview/index.vue index 8de9da9..f73bb1a 100644 --- a/src/components/FilePreview/index.vue +++ b/src/components/FilePreview/index.vue @@ -69,7 +69,7 @@ const filePreview = reactive({ }) // 弹框标题 const modalTitle = computed(() => { - const { fileName, fileType } = filePreview.fileInfo || {} + const { fileName } = filePreview.fileInfo || {} // fileName 已经包含扩展名,直接显示 return fileName || '文件预览' }) diff --git a/src/components/MultipartUpload/index.vue b/src/components/MultipartUpload/index.vue index 55d6a84..e0d6e82 100644 --- a/src/components/MultipartUpload/index.vue +++ b/src/components/MultipartUpload/index.vue @@ -21,10 +21,21 @@ 清空 +
+
默认存储上传策略
+ +
+
类型{{ storageTypeLabel }}
+
分片阈值{{ thresholdMb }} MB
+
分片大小{{ partSizeMb }} MB
+
本地分片临时目录{{ defaultUploadConfig.multipartTempDir || '-' }}
+
+
+
支持拖拽文件到此区域上传(文件夹请使用"选择文件夹"按钮)
- 提示:拖拽上传时,所有文件将上传到根目录 + 系统会根据默认存储分片阈值自动选择单文件上传或分片上传
@@ -33,6 +44,7 @@ :data="fileTasks" :columns="columns" row-key="uid" + :scroll="{ y: '100%' }" :pagination="pagination" style="height: 100%; background: transparent;" > @@ -47,7 +59,7 @@