diff --git a/src/components/upload/index.vue b/src/components/upload/index.vue new file mode 100644 index 0000000..0b23c6d --- /dev/null +++ b/src/components/upload/index.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 2d2dc9b..5585c7b 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -26,6 +26,7 @@ declare module 'vue' { IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default'] IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] + IconIcRoundUpload: typeof import('~icons/ic/round-upload')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default'] IconLocalLogo: typeof import('~icons/local/logo')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] @@ -97,6 +98,7 @@ declare module 'vue' { TableFilter: typeof import('./../components/table/table-filter.vue')['default'] TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] + Upload: typeof import('./../components/upload/index.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] } } @@ -117,6 +119,7 @@ declare global { const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] const IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default'] const IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] + const IconIcRoundUpload: typeof import('~icons/ic/round-upload')['default'] const IconLocalBanner: typeof import('~icons/local/banner')['default'] const IconLocalLogo: typeof import('~icons/local/logo')['default'] const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] @@ -188,5 +191,6 @@ declare global { const TableFilter: typeof import('./../components/table/table-filter.vue')['default'] const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] + const Upload: typeof import('./../components/upload/index.vue')['default'] const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] } \ No newline at end of file diff --git a/src/utils/aws/s3.ts b/src/utils/aws/s3.ts new file mode 100644 index 0000000..6413fe1 --- /dev/null +++ b/src/utils/aws/s3.ts @@ -0,0 +1,78 @@ +import { toRefs } from 'vue'; +import { client, safeClient } from '@/service/api'; + +interface UploadOptions { + fetchOptions: UploadFetchOptions; + onProgress?: (progress: number) => void; + signal?: AbortSignal; +} + +export type UploadFetchOptions = CommonType.TreatyBody; + +export async function uploadToS3(file: File, options: UploadOptions) { + const { onProgress, signal, fetchOptions } = options; + + // 1. 获取预签名 URL + const { data, error } = await safeClient( + client.api.file_storage.upload_url.post({ + ...fetchOptions + }) + ); + + if (error.value || !data.value) { + throw new Error('获取上传 URL 失败'); + } + const { fileId, uploadUrl, method, headers } = toRefs(data.value); + + // 2. 上传文件到 S3 + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + // 监听上传进度 + xhr.upload.addEventListener('progress', (e: ProgressEvent) => { + if (e.lengthComputable && onProgress) { + const progress = Math.round((e.loaded / e.total) * 100); + onProgress(progress); + } + }); + + // 上传成功 + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(fileId.value); + } else { + reject(new Error(`上传失败: ${xhr.status} ${xhr.statusText}`)); + } + }); + + // 网络错误 + xhr.addEventListener('error', () => { + reject(new Error('网络错误,上传失败')); + }); + + // 上传取消 + xhr.addEventListener('abort', () => { + reject(new Error('上传已取消')); + }); + + // 超时 + xhr.addEventListener('timeout', () => { + reject(new Error('上传超时')); + }); + + // 支持外部取消 + if (signal) { + signal.addEventListener('abort', () => { + xhr.abort(); + }); + } + + // 开始上传 + xhr.open(method.value, uploadUrl.value, true); + for (const [key, value] of Object.entries(headers.value)) { + xhr.setRequestHeader(key, value); + } + xhr.timeout = 300000; // 5分钟超时 + xhr.send(file); + }); +} diff --git a/src/views/rwa/product/components/edit.vue b/src/views/rwa/product/components/edit.vue index 4a7a973..37a3f1a 100644 --- a/src/views/rwa/product/components/edit.vue +++ b/src/views/rwa/product/components/edit.vue @@ -111,15 +111,14 @@ function handleSubmit() { - - 上传文件 - + accept="application/pdf,image/*,.doc,.docx" + @update:model-value="val => (form.proofDocuments = val.join(','))" + /> 取 消