diff --git a/src/components/common/FileUploadArea.vue b/src/components/common/FileUploadArea.vue
index 3071ec4..5e7acce 100644
--- a/src/components/common/FileUploadArea.vue
+++ b/src/components/common/FileUploadArea.vue
@@ -4,7 +4,8 @@
:class="[
isDarkMode
? 'bg-gray-800 bg-opacity-50 border-gray-600 hover:border-indigo-500'
- : 'bg-gray-100 border-gray-300 hover:border-indigo-500'
+ : 'bg-gray-100 border-gray-300 hover:border-indigo-500',
+ statusClass
]"
@click="triggerFileUpload"
@dragover.prevent
@@ -16,18 +17,22 @@
class="hidden"
@change="handleFileUpload"
:accept="acceptedTypes"
+ :disabled="isUploading"
/>
-
+
+
+
- {{ selectedFile ? selectedFile.name : placeholderText }}
+ {{ displayText }}
-
- {{ descriptionText }}
+
+
+
+ {{ statusDescription }}
+
+
+
+
+ {{ formatBytes(uploadedBytes) }} / {{ formatBytes(totalBytes) }}
+ {{ progress }}%
+
+
+
+
+
\ No newline at end of file
diff --git a/src/composables/useFileUpload.ts b/src/composables/useFileUpload.ts
index 7f1b673..6bb3131 100644
--- a/src/composables/useFileUpload.ts
+++ b/src/composables/useFileUpload.ts
@@ -1,12 +1,30 @@
import { ref, computed, readonly } from 'vue'
import { FileService } from '@/services'
import { useAlertStore } from '@/stores/alertStore'
-import type { UploadProgress, UploadStatus } from '@/types'
+import { usePresignedUpload } from '@/composables/usePresignedUpload'
+import type { UploadProgress, UploadStatus, PresignUploadOptions, ExpireStyle } from '@/types'
import { UPLOAD_STATUS, FILE_SIZE_LIMITS } from '@/constants'
-export function useFileUpload() {
+export interface FileUploadOptions {
+ /** 是否使用预签名上传,默认 false 保持向后兼容 */
+ usePresigned?: boolean
+ /** 过期时间值 */
+ expireValue?: number
+ /** 过期时间类型 */
+ expireStyle?: ExpireStyle
+ /** 进度回调 */
+ onProgress?: (progress: UploadProgress) => void
+}
+
+export function useFileUpload(options?: { defaultUsePresigned?: boolean }) {
const alertStore = useAlertStore()
+ // 预签名上传 composable
+ const presignedUpload = usePresignedUpload()
+
+ // 是否默认使用预签名上传
+ const defaultUsePresigned = options?.defaultUsePresigned ?? false
+
// 状态管理
const uploadStatus = ref(UPLOAD_STATUS.IDLE)
const uploadProgress = ref({
@@ -17,11 +35,34 @@ export function useFileUpload() {
const uploadedCode = ref('')
const currentFile = ref(null)
+ // 当前是否使用预签名上传
+ const isUsingPresigned = ref(false)
+
// 计算属性
- const isUploading = computed(() => uploadStatus.value === UPLOAD_STATUS.UPLOADING)
- const isSuccess = computed(() => uploadStatus.value === UPLOAD_STATUS.SUCCESS)
- const isError = computed(() => uploadStatus.value === UPLOAD_STATUS.ERROR)
- const isIdle = computed(() => uploadStatus.value === UPLOAD_STATUS.IDLE)
+ const isUploading = computed(() => {
+ if (isUsingPresigned.value) {
+ return presignedUpload.isUploading.value || presignedUpload.isInitializing.value || presignedUpload.isConfirming.value
+ }
+ return uploadStatus.value === UPLOAD_STATUS.UPLOADING
+ })
+ const isSuccess = computed(() => {
+ if (isUsingPresigned.value) {
+ return presignedUpload.isSuccess.value
+ }
+ return uploadStatus.value === UPLOAD_STATUS.SUCCESS
+ })
+ const isError = computed(() => {
+ if (isUsingPresigned.value) {
+ return presignedUpload.isError.value
+ }
+ return uploadStatus.value === UPLOAD_STATUS.ERROR
+ })
+ const isIdle = computed(() => {
+ if (isUsingPresigned.value) {
+ return presignedUpload.presignStatus.value === 'idle'
+ }
+ return uploadStatus.value === UPLOAD_STATUS.IDLE
+ })
// 文件验证
const validateFile = (file: File): boolean => {
@@ -35,19 +76,43 @@ export function useFileUpload() {
return true
}
- // 上传文件
- const uploadFile = async (file: File): Promise => {
+ /**
+ * 上传文件(支持预签名上传和传统上传)
+ */
+ const uploadFile = async (file: File, uploadOptions?: FileUploadOptions): Promise => {
+ const shouldUsePresigned = uploadOptions?.usePresigned ?? defaultUsePresigned
+
if (!validateFile(file)) {
return null
}
+ // 记录当前使用的上传方式
+ isUsingPresigned.value = shouldUsePresigned
+ currentFile.value = file
+
+ if (shouldUsePresigned) {
+ // 使用预签名上传
+ return await uploadFileWithPresigned(file, uploadOptions)
+ } else {
+ // 使用传统上传方式
+ return await uploadFileTraditional(file, uploadOptions?.onProgress)
+ }
+ }
+
+ /**
+ * 传统上传方式
+ */
+ const uploadFileTraditional = async (
+ file: File,
+ onProgress?: (progress: UploadProgress) => void
+ ): Promise => {
try {
uploadStatus.value = UPLOAD_STATUS.UPLOADING
- currentFile.value = file
uploadedCode.value = ''
const response = await FileService.uploadFile(file, (progress) => {
uploadProgress.value = progress
+ onProgress?.(progress)
})
if (response.code === 200 && response.detail?.code) {
@@ -66,6 +131,37 @@ export function useFileUpload() {
}
}
+ /**
+ * 预签名上传方式
+ */
+ const uploadFileWithPresigned = async (
+ file: File,
+ uploadOptions?: FileUploadOptions
+ ): Promise => {
+ // 构建预签名上传选项
+ const presignOptions: PresignUploadOptions = {
+ expireValue: uploadOptions?.expireValue,
+ expireStyle: uploadOptions?.expireStyle,
+ onProgress: (progress) => {
+ // 同步进度到本 composable 的状态
+ uploadProgress.value = progress
+ uploadOptions?.onProgress?.(progress)
+ }
+ }
+
+ const result = await presignedUpload.uploadFile(file, presignOptions)
+
+ if (result) {
+ // 同步预签名上传的结果到本 composable 的状态
+ uploadedCode.value = result
+ uploadStatus.value = UPLOAD_STATUS.SUCCESS
+ } else {
+ uploadStatus.value = UPLOAD_STATUS.ERROR
+ }
+
+ return result
+ }
+
// 上传文本
const uploadText = async (text: string): Promise => {
if (!text.trim()) {
@@ -105,6 +201,22 @@ export function useFileUpload() {
}
uploadedCode.value = ''
currentFile.value = null
+
+ // 如果使用预签名上传,也重置预签名状态
+ if (isUsingPresigned.value) {
+ presignedUpload.reset()
+ }
+ isUsingPresigned.value = false
+ }
+
+ /**
+ * 取消上传
+ */
+ const cancelUpload = async (): Promise => {
+ if (isUsingPresigned.value) {
+ await presignedUpload.cancelUpload()
+ }
+ resetUpload()
}
// 格式化文件大小
@@ -122,6 +234,13 @@ export function useFileUpload() {
uploadProgress: readonly(uploadProgress),
uploadedCode: readonly(uploadedCode),
currentFile: readonly(currentFile),
+ isUsingPresigned: readonly(isUsingPresigned),
+
+ // 预签名上传相关状态(透传)
+ presignStatus: presignedUpload.presignStatus,
+ uploadSession: presignedUpload.uploadSession,
+ currentMode: presignedUpload.currentMode,
+ presignErrorMessage: presignedUpload.errorMessage,
// 计算属性
isUploading,
@@ -129,11 +248,19 @@ export function useFileUpload() {
isError,
isIdle,
+ // 预签名上传计算属性(透传)
+ isInitializing: presignedUpload.isInitializing,
+ isConfirming: presignedUpload.isConfirming,
+
// 方法
uploadFile,
uploadText,
resetUpload,
+ cancelUpload,
validateFile,
- formatFileSize
+ formatFileSize,
+
+ // 预签名上传方法(透传)
+ getPresignStatus: presignedUpload.getStatus
}
}
\ No newline at end of file
diff --git a/src/composables/usePresignedUpload.ts b/src/composables/usePresignedUpload.ts
new file mode 100644
index 0000000..b121a7e
--- /dev/null
+++ b/src/composables/usePresignedUpload.ts
@@ -0,0 +1,357 @@
+import { ref, computed, readonly } from 'vue'
+import { PresignUploadService } from '@/services'
+import { useAlertStore } from '@/stores/alertStore'
+import { FILE_SIZE_LIMITS } from '@/constants'
+import type {
+ PresignUploadStatus,
+ PresignUploadMode,
+ PresignInitResponse,
+ PresignUploadOptions,
+ PresignStatusResponse,
+ UploadProgress,
+ ExpireStyle
+} from '@/types'
+import axios from 'axios'
+
+// 预签名上传状态常量
+export const PRESIGN_UPLOAD_STATUS = {
+ IDLE: 'idle',
+ INITIALIZING: 'initializing',
+ UPLOADING: 'uploading',
+ CONFIRMING: 'confirming',
+ SUCCESS: 'success',
+ ERROR: 'error'
+} as const
+
+// 默认过期配置
+const DEFAULT_EXPIRE_VALUE = 1
+const DEFAULT_EXPIRE_STYLE: ExpireStyle = 'day'
+
+/**
+ * 预签名上传 Composable
+ * 支持 S3 直传模式和服务器代理模式
+ */
+export function usePresignedUpload() {
+ const alertStore = useAlertStore()
+
+ // 状态管理
+ const presignStatus = ref(PRESIGN_UPLOAD_STATUS.IDLE)
+ const uploadSession = ref(null)
+ const uploadProgress = ref({
+ loaded: 0,
+ total: 0,
+ percentage: 0
+ })
+ const uploadedCode = ref('')
+ const errorMessage = ref('')
+
+ // 计算属性
+ const isInitializing = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.INITIALIZING)
+ const isUploading = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.UPLOADING)
+ const isConfirming = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.CONFIRMING)
+ const isSuccess = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.SUCCESS)
+ const isError = computed(() => presignStatus.value === PRESIGN_UPLOAD_STATUS.ERROR)
+ const currentMode = computed(() => uploadSession.value?.mode ?? null)
+
+
+ /**
+ * 文件大小验证
+ */
+ const validateFileSize = (file: File): boolean => {
+ if (file.size > FILE_SIZE_LIMITS.MAX_FILE_SIZE) {
+ const maxSizeMB = Math.round(FILE_SIZE_LIMITS.MAX_FILE_SIZE / 1024 / 1024)
+ errorMessage.value = `文件大小不能超过 ${maxSizeMB}MB`
+ alertStore.showAlert(errorMessage.value, 'error')
+ return false
+ }
+ return true
+ }
+
+ /**
+ * 进度追踪回调
+ */
+ const createProgressHandler = (
+ externalHandler?: (progress: UploadProgress) => void
+ ): ((progress: UploadProgress) => void) => {
+ return (progress: UploadProgress) => {
+ // 确保进度值在有效范围内
+ uploadProgress.value = {
+ loaded: Math.max(0, progress.loaded),
+ total: Math.max(0, progress.total),
+ percentage: Math.min(100, Math.max(0, progress.percentage))
+ }
+ // 调用外部进度回调
+ if (externalHandler) {
+ externalHandler(uploadProgress.value)
+ }
+ }
+ }
+
+ /**
+ * 错误处理
+ */
+ const handleUploadError = (error: unknown): void => {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.ERROR
+
+ if (axios.isAxiosError(error)) {
+ const status = error.response?.status
+ const detail = error.response?.data?.detail
+
+ switch (status) {
+ case 400:
+ errorMessage.value = detail || '请求参数错误'
+ break
+ case 403:
+ errorMessage.value = detail || '操作被禁止'
+ break
+ case 404:
+ errorMessage.value = '上传会话不存在或已过期'
+ break
+ case 500:
+ errorMessage.value = '服务器错误,请稍后重试'
+ break
+ default:
+ errorMessage.value = detail || '上传失败,请重试'
+ }
+ } else if (error instanceof Error) {
+ errorMessage.value = error.message
+ } else {
+ errorMessage.value = '未知错误'
+ }
+
+ alertStore.showAlert(errorMessage.value, 'error')
+ }
+
+
+ /**
+ * 初始化上传会话
+ */
+ const initUploadSession = async (
+ file: File,
+ options?: PresignUploadOptions
+ ): Promise => {
+ try {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.INITIALIZING
+
+ const response = await PresignUploadService.initUpload({
+ file_name: file.name,
+ file_size: file.size,
+ expire_value: options?.expireValue ?? DEFAULT_EXPIRE_VALUE,
+ expire_style: options?.expireStyle ?? DEFAULT_EXPIRE_STYLE
+ })
+
+ if (response.code === 200 && response.detail) {
+ uploadSession.value = response.detail
+ return response.detail
+ } else {
+ throw new Error(response.message || '初始化上传失败')
+ }
+ } catch (error) {
+ handleUploadError(error)
+ return null
+ }
+ }
+
+ /**
+ * 直传模式上传到 S3
+ */
+ const uploadDirect = async (
+ file: File,
+ session: PresignInitResponse,
+ options?: PresignUploadOptions
+ ): Promise => {
+ try {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.UPLOADING
+
+ // 上传到 S3
+ const progressHandler = createProgressHandler(options?.onProgress)
+ const uploadSuccess = await PresignUploadService.directUploadToS3(
+ session.upload_url,
+ file,
+ progressHandler
+ )
+
+ if (!uploadSuccess) {
+ throw new Error('S3 上传失败')
+ }
+
+ // 确认上传
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.CONFIRMING
+ const confirmResponse = await PresignUploadService.confirmUpload(session.upload_id, {
+ expire_value: options?.expireValue ?? DEFAULT_EXPIRE_VALUE,
+ expire_style: options?.expireStyle ?? DEFAULT_EXPIRE_STYLE
+ })
+
+ if (confirmResponse.code === 200 && confirmResponse.detail?.code) {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.SUCCESS
+ uploadedCode.value = String(confirmResponse.detail.code)
+ // 确保进度为 100%
+ uploadProgress.value = {
+ loaded: file.size,
+ total: file.size,
+ percentage: 100
+ }
+ alertStore.showAlert('文件上传成功!', 'success')
+ return uploadedCode.value
+ } else {
+ throw new Error(confirmResponse.message || '确认上传失败')
+ }
+ } catch (error) {
+ handleUploadError(error)
+ return null
+ }
+ }
+
+
+ /**
+ * 代理模式上传
+ */
+ const uploadProxy = async (
+ file: File,
+ session: PresignInitResponse,
+ options?: PresignUploadOptions
+ ): Promise => {
+ try {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.UPLOADING
+
+ const progressHandler = createProgressHandler(options?.onProgress)
+ const response = await PresignUploadService.proxyUpload(
+ session.upload_id,
+ file,
+ progressHandler
+ )
+
+ if (response.code === 200 && response.detail?.code) {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.SUCCESS
+ uploadedCode.value = String(response.detail.code)
+ // 确保进度为 100%
+ uploadProgress.value = {
+ loaded: file.size,
+ total: file.size,
+ percentage: 100
+ }
+ alertStore.showAlert('文件上传成功!', 'success')
+ return uploadedCode.value
+ } else {
+ throw new Error(response.message || '代理上传失败')
+ }
+ } catch (error) {
+ handleUploadError(error)
+ return null
+ }
+ }
+
+ /**
+ * 上传文件(主入口方法)
+ * 根据 mode 自动选择直传或代理上传
+ */
+ const uploadFile = async (
+ file: File,
+ options?: PresignUploadOptions
+ ): Promise => {
+ // 重置状态
+ reset()
+
+ // 验证文件大小
+ if (!validateFileSize(file)) {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.ERROR
+ return null
+ }
+
+ // 初始化上传会话
+ const session = await initUploadSession(file, options)
+ if (!session) {
+ return null
+ }
+
+ // 根据模式选择上传方式
+ if (session.mode === 'direct') {
+ return await uploadDirect(file, session, options)
+ } else {
+ return await uploadProxy(file, session, options)
+ }
+ }
+
+
+ /**
+ * 取消上传
+ */
+ const cancelUpload = async (): Promise => {
+ if (!uploadSession.value?.upload_id) {
+ return
+ }
+
+ try {
+ await PresignUploadService.cancelUpload(uploadSession.value.upload_id)
+ alertStore.showAlert('上传已取消', 'info')
+ } catch (error) {
+ // 取消失败时静默处理,因为会话可能已过期
+ console.warn('取消上传失败:', error)
+ } finally {
+ reset()
+ }
+ }
+
+ /**
+ * 获取上传状态
+ */
+ const getStatus = async (): Promise => {
+ if (!uploadSession.value?.upload_id) {
+ return null
+ }
+
+ try {
+ const response = await PresignUploadService.getUploadStatus(uploadSession.value.upload_id)
+ if (response.code === 200 && response.detail) {
+ // 检查会话是否过期
+ if (response.detail.is_expired) {
+ errorMessage.value = '上传会话已过期'
+ alertStore.showAlert(errorMessage.value, 'warning')
+ }
+ return response.detail
+ }
+ return null
+ } catch (error) {
+ handleUploadError(error)
+ return null
+ }
+ }
+
+ /**
+ * 重置状态
+ */
+ const reset = (): void => {
+ presignStatus.value = PRESIGN_UPLOAD_STATUS.IDLE
+ uploadSession.value = null
+ uploadProgress.value = {
+ loaded: 0,
+ total: 0,
+ percentage: 0
+ }
+ uploadedCode.value = ''
+ errorMessage.value = ''
+ }
+
+ return {
+ // 状态 (只读)
+ presignStatus: readonly(presignStatus),
+ uploadSession: readonly(uploadSession),
+ uploadProgress: readonly(uploadProgress),
+ uploadedCode: readonly(uploadedCode),
+ errorMessage: readonly(errorMessage),
+
+ // 计算属性
+ isInitializing,
+ isUploading,
+ isConfirming,
+ isSuccess,
+ isError,
+ currentMode,
+
+ // 方法
+ uploadFile,
+ cancelUpload,
+ getStatus,
+ reset
+ }
+}
diff --git a/src/services/index.ts b/src/services/index.ts
index 536296c..774f02a 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,7 +1,8 @@
// API 服务层
import api from '@/utils/api'
+import axios from 'axios'
import type { ApiResponse, FileInfo, ConfigState, AdminUser, FileUploadResponse, TextSendResponse, DashboardData, FileListResponse, FileEditForm } from '@/types'
-import type { UploadProgress } from '@/types'
+import type { UploadProgress, PresignInitRequest, PresignInitResponse, PresignConfirmRequest, PresignUploadResult, PresignStatusResponse } from '@/types'
// 系统配置服务
export class ConfigService {
@@ -128,12 +129,105 @@ export class StatsService {
}
}
+// 预签名上传服务
+export class PresignUploadService {
+ /**
+ * 初始化上传会话
+ */
+ static async initUpload(request: PresignInitRequest): Promise> {
+ return api.post('/presign/upload/init', request)
+ }
+
+ /**
+ * 代理模式上传
+ */
+ static async proxyUpload(
+ uploadId: string,
+ file: File,
+ onProgress?: (progress: UploadProgress) => void
+ ): Promise> {
+ const formData = new FormData()
+ formData.append('file', file)
+
+ return api.put(`/presign/upload/proxy/${uploadId}`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ },
+ onUploadProgress: (progressEvent) => {
+ if (onProgress && progressEvent.total) {
+ const progress: UploadProgress = {
+ loaded: progressEvent.loaded,
+ total: progressEvent.total,
+ percentage: Math.round((progressEvent.loaded * 100) / progressEvent.total)
+ }
+ onProgress(progress)
+ }
+ }
+ })
+ }
+
+ /**
+ * 确认直传上传
+ */
+ static async confirmUpload(
+ uploadId: string,
+ request?: PresignConfirmRequest
+ ): Promise> {
+ return api.post(`/presign/upload/confirm/${uploadId}`, request || {})
+ }
+
+ /**
+ * 查询上传状态
+ */
+ static async getUploadStatus(uploadId: string): Promise> {
+ return api.get(`/presign/upload/status/${uploadId}`)
+ }
+
+ /**
+ * 取消上传
+ */
+ static async cancelUpload(uploadId: string): Promise> {
+ return api.delete(`/presign/upload/${uploadId}`)
+ }
+
+ /**
+ * S3 直传(不经过后端)
+ */
+ static async directUploadToS3(
+ uploadUrl: string,
+ file: File,
+ onProgress?: (progress: UploadProgress) => void
+ ): Promise {
+ try {
+ await axios.put(uploadUrl, file, {
+ headers: {
+ 'Content-Type': 'application/octet-stream'
+ },
+ onUploadProgress: (progressEvent) => {
+ if (onProgress && progressEvent.total) {
+ const progress: UploadProgress = {
+ loaded: progressEvent.loaded,
+ total: progressEvent.total,
+ percentage: Math.round((progressEvent.loaded * 100) / progressEvent.total)
+ }
+ onProgress(progress)
+ }
+ }
+ })
+ return true
+ } catch {
+ return false
+ }
+ }
+}
+
// 导出所有服务
export const services = {
config: ConfigService,
file: FileService,
auth: AuthService,
- stats: StatsService
+ stats: StatsService,
+ presignUpload: PresignUploadService
}
export default services
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
index 812f3fd..6dfd698 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -177,4 +177,68 @@ export interface RouteConfig {
requiresAuth?: boolean
title?: string
}
-}
\ No newline at end of file
+}
+
+
+// ==================== 预签名上传相关类型 ====================
+
+// 预签名上传模式
+export type PresignUploadMode = 'direct' | 'proxy'
+
+// 预签名上传状态
+export type PresignUploadStatus =
+ | 'idle'
+ | 'initializing'
+ | 'uploading'
+ | 'confirming'
+ | 'success'
+ | 'error'
+
+// 过期类型
+export type ExpireStyle = 'day' | 'hour' | 'minute' | 'forever' | 'count'
+
+// 初始化上传请求
+export interface PresignInitRequest {
+ file_name: string
+ file_size: number
+ expire_value?: number
+ expire_style?: ExpireStyle
+}
+
+// 初始化上传响应
+export interface PresignInitResponse {
+ upload_id: string
+ upload_url: string
+ mode: PresignUploadMode
+ expires_in: number
+}
+
+// 确认上传请求
+export interface PresignConfirmRequest {
+ expire_value?: number
+ expire_style?: ExpireStyle
+}
+
+// 上传结果
+export interface PresignUploadResult {
+ code: string
+ name: string
+}
+
+// 上传状态查询响应
+export interface PresignStatusResponse {
+ upload_id: string
+ file_name: string
+ file_size: number
+ mode: PresignUploadMode
+ created_at: string
+ expires_at: string
+ is_expired: boolean
+}
+
+// 上传配置选项
+export interface PresignUploadOptions {
+ expireValue?: number
+ expireStyle?: ExpireStyle
+ onProgress?: (progress: UploadProgress) => void
+}
diff --git a/src/views/SendFileView.vue b/src/views/SendFileView.vue
index b4e00af..1f08d87 100644
--- a/src/views/SendFileView.vue
+++ b/src/views/SendFileView.vue
@@ -511,6 +511,8 @@ import PageHeader from '@/components/common/PageHeader.vue'
import SendTypeSelector from '@/components/common/SendTypeSelector.vue'
import FileUploadArea from '@/components/common/FileUploadArea.vue'
import TextInputArea from '@/components/common/TextInputArea.vue'
+import { usePresignedUpload } from '@/composables/usePresignedUpload'
+import type { ExpireStyle } from '@/types'
interface Config {
name: string
@@ -552,6 +554,14 @@ const { t } = useI18n()
const alertStore = useAlertStore()
const sendRecords = computed(() => fileDataStore.shareData)
+// 预签名上传
+const {
+ presignStatus,
+ uploadProgress: presignProgress,
+ uploadFile: presignUploadFile,
+ reset: resetPresignUpload
+} = usePresignedUpload()
+
const fileHash = ref('')
@@ -891,6 +901,28 @@ const handleDefaultFileUpload = async (file: File) => {
const response: ApiResponse<{ code?: string; name?: string }> = await api.post('share/file/', formData, config)
return response
}
+
+// 预签名上传处理
+const handlePresignedUpload = async (file: File) => {
+ const code = await presignUploadFile(file, {
+ expireValue: expirationValue.value ? parseInt(expirationValue.value) : 1,
+ expireStyle: expirationMethod.value as ExpireStyle,
+ onProgress: (progress) => {
+ uploadProgress.value = progress.percentage
+ }
+ })
+
+ if (code) {
+ return {
+ code: 200,
+ detail: {
+ code: code,
+ name: file.name
+ }
+ } as ApiResponse<{ code?: string; name?: string }>
+ }
+ throw new Error(t('send.messages.uploadFailed'))
+}
const checkOpenUpload = () => {
if (config.openUpload === 0 && localStorage.getItem('token') === null) {
alertStore.showAlert(t('send.messages.guestUploadDisabled'), 'error')
@@ -963,11 +995,16 @@ const handleSubmit = async () => {
let response: ApiResponse
if (sendType.value === 'file') {
- // 使用切片上传替代原来的直接上传
- if (config.enableChunk) {
- response = await handleChunkUpload(selectedFile.value!)
- } else {
- response = await handleDefaultFileUpload(selectedFile.value!)
+ // 优先使用预签名上传,如果失败则回退到其他方式
+ try {
+ response = await handlePresignedUpload(selectedFile.value!)
+ } catch {
+ // 预签名上传失败,回退到切片上传或默认上传
+ if (config.enableChunk) {
+ response = await handleChunkUpload(selectedFile.value!)
+ } else {
+ response = await handleDefaultFileUpload(selectedFile.value!)
+ }
}
} else {
// 文本上传保持不变
@@ -1008,6 +1045,7 @@ const handleSubmit = async () => {
selectedFile.value = null
textContent.value = ''
uploadProgress.value = 0
+ resetPresignUpload()
// 显示详情
selectedRecord.value = newRecord
// 自动复制取件码链接