feat: 新增 S3预签名上传
This commit is contained in:
@@ -4,7 +4,8 @@
|
|||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'bg-gray-800 bg-opacity-50 border-gray-600 hover:border-indigo-500'
|
? '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"
|
@click="triggerFileUpload"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@@ -16,18 +17,22 @@
|
|||||||
class="hidden"
|
class="hidden"
|
||||||
@change="handleFileUpload"
|
@change="handleFileUpload"
|
||||||
:accept="acceptedTypes"
|
:accept="acceptedTypes"
|
||||||
|
:disabled="isUploading"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 w-full h-full" v-if="progress > 0">
|
<div class="absolute inset-0 w-full h-full" v-if="progress > 0">
|
||||||
<BorderProgressBar :progress="progress" />
|
<BorderProgressBar :progress="progress" />
|
||||||
</div>
|
</div>
|
||||||
<UploadCloudIcon
|
|
||||||
|
<!-- 上传状态图标 -->
|
||||||
|
<component
|
||||||
|
:is="statusIcon"
|
||||||
:class="[
|
:class="[
|
||||||
'w-16 h-16 transition-colors duration-300',
|
'w-16 h-16 transition-colors duration-300',
|
||||||
isDarkMode
|
statusIconClass
|
||||||
? 'text-gray-400 group-hover:text-indigo-400'
|
|
||||||
: 'text-gray-600 group-hover:text-indigo-600'
|
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 文件名或占位文本 -->
|
||||||
<p
|
<p
|
||||||
:class="[
|
:class="[
|
||||||
'mt-4 text-sm transition-colors duration-300 w-full text-center',
|
'mt-4 text-sm transition-colors duration-300 w-full text-center',
|
||||||
@@ -37,33 +42,75 @@
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span class="block truncate">
|
<span class="block truncate">
|
||||||
{{ selectedFile ? selectedFile.name : placeholderText }}
|
{{ displayText }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p :class="['mt-2 text-xs', isDarkMode ? 'text-gray-500' : 'text-gray-400']">
|
|
||||||
{{ descriptionText }}
|
<!-- 状态描述或默认描述 -->
|
||||||
|
<p :class="['mt-2 text-xs', statusDescriptionClass]">
|
||||||
|
{{ statusDescription }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- 进度详情(上传中显示) -->
|
||||||
|
<div v-if="isUploading && showProgressDetails" class="mt-3 w-full">
|
||||||
|
<div class="flex justify-between text-xs mb-1" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||||
|
<span>{{ formatBytes(uploadedBytes) }} / {{ formatBytes(totalBytes) }}</span>
|
||||||
|
<span>{{ progress }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误重试按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="hasError && allowRetry"
|
||||||
|
@click.stop="handleRetry"
|
||||||
|
class="mt-3 px-4 py-2 text-sm rounded-lg transition-colors duration-200"
|
||||||
|
:class="[
|
||||||
|
isDarkMode
|
||||||
|
? 'bg-indigo-600 hover:bg-indigo-500 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ retryText }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject, computed } from 'vue'
|
import { ref, inject, computed } from 'vue'
|
||||||
import { UploadCloudIcon } from 'lucide-vue-next'
|
import { UploadCloudIcon, CheckCircleIcon, XCircleIcon, LoaderIcon } from 'lucide-vue-next'
|
||||||
import BorderProgressBar from './BorderProgressBar.vue'
|
import BorderProgressBar from './BorderProgressBar.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
type UploadStatusType = 'idle' | 'uploading' | 'success' | 'error' | 'initializing' | 'confirming'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedFile?: File | null
|
selectedFile?: File | null
|
||||||
progress?: number
|
progress?: number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
description?: string
|
description?: string
|
||||||
acceptedTypes?: string
|
acceptedTypes?: string
|
||||||
|
/** 上传状态 */
|
||||||
|
uploadStatus?: UploadStatusType
|
||||||
|
/** 已上传字节数 */
|
||||||
|
uploadedBytes?: number
|
||||||
|
/** 总字节数 */
|
||||||
|
totalBytes?: number
|
||||||
|
/** 错误消息 */
|
||||||
|
errorMessage?: string
|
||||||
|
/** 是否允许重试 */
|
||||||
|
allowRetry?: boolean
|
||||||
|
/** 重试按钮文本 */
|
||||||
|
retryText?: string
|
||||||
|
/** 是否显示进度详情 */
|
||||||
|
showProgressDetails?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
fileSelected: [file: File]
|
fileSelected: [file: File]
|
||||||
fileDrop: [event: DragEvent]
|
fileDrop: [event: DragEvent]
|
||||||
|
retry: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -71,7 +118,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
description: '',
|
description: '',
|
||||||
acceptedTypes: '*'
|
acceptedTypes: '*',
|
||||||
|
uploadStatus: 'idle',
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalBytes: 0,
|
||||||
|
errorMessage: '',
|
||||||
|
allowRetry: true,
|
||||||
|
retryText: '重试',
|
||||||
|
showProgressDetails: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
@@ -83,7 +137,103 @@ const placeholderText = computed(() => props.placeholder || t('send.uploadArea.p
|
|||||||
const descriptionText = computed(() => props.description || t('send.uploadArea.description'))
|
const descriptionText = computed(() => props.description || t('send.uploadArea.description'))
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
const isUploading = computed(() => {
|
||||||
|
return ['uploading', 'initializing', 'confirming'].includes(props.uploadStatus)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasError = computed(() => props.uploadStatus === 'error')
|
||||||
|
|
||||||
|
const isSuccess = computed(() => props.uploadStatus === 'success')
|
||||||
|
|
||||||
|
const displayText = computed(() => {
|
||||||
|
if (props.selectedFile) {
|
||||||
|
return props.selectedFile.name
|
||||||
|
}
|
||||||
|
return placeholderText.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusIcon = computed(() => {
|
||||||
|
if (isUploading.value) return LoaderIcon
|
||||||
|
if (isSuccess.value) return CheckCircleIcon
|
||||||
|
if (hasError.value) return XCircleIcon
|
||||||
|
return UploadCloudIcon
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusIconClass = computed(() => {
|
||||||
|
if (isUploading.value) {
|
||||||
|
return isDarkMode
|
||||||
|
? 'text-indigo-400 animate-spin'
|
||||||
|
: 'text-indigo-600 animate-spin'
|
||||||
|
}
|
||||||
|
if (isSuccess.value) {
|
||||||
|
return isDarkMode
|
||||||
|
? 'text-green-400'
|
||||||
|
: 'text-green-600'
|
||||||
|
}
|
||||||
|
if (hasError.value) {
|
||||||
|
return isDarkMode
|
||||||
|
? 'text-red-400'
|
||||||
|
: 'text-red-600'
|
||||||
|
}
|
||||||
|
return isDarkMode
|
||||||
|
? 'text-gray-400 group-hover:text-indigo-400'
|
||||||
|
: 'text-gray-600 group-hover:text-indigo-600'
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
if (hasError.value) {
|
||||||
|
return isDarkMode
|
||||||
|
? 'border-red-500/50'
|
||||||
|
: 'border-red-300'
|
||||||
|
}
|
||||||
|
if (isSuccess.value) {
|
||||||
|
return isDarkMode
|
||||||
|
? 'border-green-500/50'
|
||||||
|
: 'border-green-300'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusDescription = computed(() => {
|
||||||
|
if (hasError.value && props.errorMessage) {
|
||||||
|
return props.errorMessage
|
||||||
|
}
|
||||||
|
if (props.uploadStatus === 'initializing') {
|
||||||
|
return '正在初始化上传...'
|
||||||
|
}
|
||||||
|
if (props.uploadStatus === 'uploading') {
|
||||||
|
return '正在上传文件...'
|
||||||
|
}
|
||||||
|
if (props.uploadStatus === 'confirming') {
|
||||||
|
return '正在确认上传...'
|
||||||
|
}
|
||||||
|
if (isSuccess.value) {
|
||||||
|
return '上传成功!'
|
||||||
|
}
|
||||||
|
return descriptionText.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusDescriptionClass = computed(() => {
|
||||||
|
if (hasError.value) {
|
||||||
|
return isDarkMode ? 'text-red-400' : 'text-red-500'
|
||||||
|
}
|
||||||
|
if (isSuccess.value) {
|
||||||
|
return isDarkMode ? 'text-green-400' : 'text-green-500'
|
||||||
|
}
|
||||||
|
return isDarkMode ? 'text-gray-500' : 'text-gray-400'
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
const triggerFileUpload = () => {
|
const triggerFileUpload = () => {
|
||||||
|
// 上传中或成功状态下不允许重新选择文件
|
||||||
|
if (isUploading.value) return
|
||||||
fileInput.value?.click()
|
fileInput.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +246,12 @@ const handleFileUpload = (event: Event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFileDrop = (event: DragEvent) => {
|
const handleFileDrop = (event: DragEvent) => {
|
||||||
|
// 上传中不允许拖放
|
||||||
|
if (isUploading.value) return
|
||||||
emit('fileDrop', event)
|
emit('fileDrop', event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
emit('retry')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,12 +1,30 @@
|
|||||||
import { ref, computed, readonly } from 'vue'
|
import { ref, computed, readonly } from 'vue'
|
||||||
import { FileService } from '@/services'
|
import { FileService } from '@/services'
|
||||||
import { useAlertStore } from '@/stores/alertStore'
|
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'
|
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()
|
const alertStore = useAlertStore()
|
||||||
|
|
||||||
|
// 预签名上传 composable
|
||||||
|
const presignedUpload = usePresignedUpload()
|
||||||
|
|
||||||
|
// 是否默认使用预签名上传
|
||||||
|
const defaultUsePresigned = options?.defaultUsePresigned ?? false
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const uploadStatus = ref<UploadStatus>(UPLOAD_STATUS.IDLE)
|
const uploadStatus = ref<UploadStatus>(UPLOAD_STATUS.IDLE)
|
||||||
const uploadProgress = ref<UploadProgress>({
|
const uploadProgress = ref<UploadProgress>({
|
||||||
@@ -17,11 +35,34 @@ export function useFileUpload() {
|
|||||||
const uploadedCode = ref<string>('')
|
const uploadedCode = ref<string>('')
|
||||||
const currentFile = ref<File | null>(null)
|
const currentFile = ref<File | null>(null)
|
||||||
|
|
||||||
|
// 当前是否使用预签名上传
|
||||||
|
const isUsingPresigned = ref<boolean>(false)
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isUploading = computed(() => uploadStatus.value === UPLOAD_STATUS.UPLOADING)
|
const isUploading = computed(() => {
|
||||||
const isSuccess = computed(() => uploadStatus.value === UPLOAD_STATUS.SUCCESS)
|
if (isUsingPresigned.value) {
|
||||||
const isError = computed(() => uploadStatus.value === UPLOAD_STATUS.ERROR)
|
return presignedUpload.isUploading.value || presignedUpload.isInitializing.value || presignedUpload.isConfirming.value
|
||||||
const isIdle = computed(() => uploadStatus.value === UPLOAD_STATUS.IDLE)
|
}
|
||||||
|
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 => {
|
const validateFile = (file: File): boolean => {
|
||||||
@@ -35,19 +76,43 @@ export function useFileUpload() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传文件
|
/**
|
||||||
const uploadFile = async (file: File): Promise<string | null> => {
|
* 上传文件(支持预签名上传和传统上传)
|
||||||
|
*/
|
||||||
|
const uploadFile = async (file: File, uploadOptions?: FileUploadOptions): Promise<string | null> => {
|
||||||
|
const shouldUsePresigned = uploadOptions?.usePresigned ?? defaultUsePresigned
|
||||||
|
|
||||||
if (!validateFile(file)) {
|
if (!validateFile(file)) {
|
||||||
return null
|
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<string | null> => {
|
||||||
try {
|
try {
|
||||||
uploadStatus.value = UPLOAD_STATUS.UPLOADING
|
uploadStatus.value = UPLOAD_STATUS.UPLOADING
|
||||||
currentFile.value = file
|
|
||||||
uploadedCode.value = ''
|
uploadedCode.value = ''
|
||||||
|
|
||||||
const response = await FileService.uploadFile(file, (progress) => {
|
const response = await FileService.uploadFile(file, (progress) => {
|
||||||
uploadProgress.value = progress
|
uploadProgress.value = progress
|
||||||
|
onProgress?.(progress)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.code === 200 && response.detail?.code) {
|
if (response.code === 200 && response.detail?.code) {
|
||||||
@@ -66,6 +131,37 @@ export function useFileUpload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预签名上传方式
|
||||||
|
*/
|
||||||
|
const uploadFileWithPresigned = async (
|
||||||
|
file: File,
|
||||||
|
uploadOptions?: FileUploadOptions
|
||||||
|
): Promise<string | null> => {
|
||||||
|
// 构建预签名上传选项
|
||||||
|
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<string | null> => {
|
const uploadText = async (text: string): Promise<string | null> => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
@@ -105,6 +201,22 @@ export function useFileUpload() {
|
|||||||
}
|
}
|
||||||
uploadedCode.value = ''
|
uploadedCode.value = ''
|
||||||
currentFile.value = null
|
currentFile.value = null
|
||||||
|
|
||||||
|
// 如果使用预签名上传,也重置预签名状态
|
||||||
|
if (isUsingPresigned.value) {
|
||||||
|
presignedUpload.reset()
|
||||||
|
}
|
||||||
|
isUsingPresigned.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消上传
|
||||||
|
*/
|
||||||
|
const cancelUpload = async (): Promise<void> => {
|
||||||
|
if (isUsingPresigned.value) {
|
||||||
|
await presignedUpload.cancelUpload()
|
||||||
|
}
|
||||||
|
resetUpload()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化文件大小
|
// 格式化文件大小
|
||||||
@@ -122,6 +234,13 @@ export function useFileUpload() {
|
|||||||
uploadProgress: readonly(uploadProgress),
|
uploadProgress: readonly(uploadProgress),
|
||||||
uploadedCode: readonly(uploadedCode),
|
uploadedCode: readonly(uploadedCode),
|
||||||
currentFile: readonly(currentFile),
|
currentFile: readonly(currentFile),
|
||||||
|
isUsingPresigned: readonly(isUsingPresigned),
|
||||||
|
|
||||||
|
// 预签名上传相关状态(透传)
|
||||||
|
presignStatus: presignedUpload.presignStatus,
|
||||||
|
uploadSession: presignedUpload.uploadSession,
|
||||||
|
currentMode: presignedUpload.currentMode,
|
||||||
|
presignErrorMessage: presignedUpload.errorMessage,
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
isUploading,
|
isUploading,
|
||||||
@@ -129,11 +248,19 @@ export function useFileUpload() {
|
|||||||
isError,
|
isError,
|
||||||
isIdle,
|
isIdle,
|
||||||
|
|
||||||
|
// 预签名上传计算属性(透传)
|
||||||
|
isInitializing: presignedUpload.isInitializing,
|
||||||
|
isConfirming: presignedUpload.isConfirming,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
uploadFile,
|
uploadFile,
|
||||||
uploadText,
|
uploadText,
|
||||||
resetUpload,
|
resetUpload,
|
||||||
|
cancelUpload,
|
||||||
validateFile,
|
validateFile,
|
||||||
formatFileSize
|
formatFileSize,
|
||||||
|
|
||||||
|
// 预签名上传方法(透传)
|
||||||
|
getPresignStatus: presignedUpload.getStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<PresignUploadStatus>(PRESIGN_UPLOAD_STATUS.IDLE)
|
||||||
|
const uploadSession = ref<PresignInitResponse | null>(null)
|
||||||
|
const uploadProgress = ref<UploadProgress>({
|
||||||
|
loaded: 0,
|
||||||
|
total: 0,
|
||||||
|
percentage: 0
|
||||||
|
})
|
||||||
|
const uploadedCode = ref<string>('')
|
||||||
|
const errorMessage = ref<string>('')
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
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<PresignUploadMode | null>(() => 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<PresignInitResponse | null> => {
|
||||||
|
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<string | null> => {
|
||||||
|
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<string | null> => {
|
||||||
|
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<string | null> => {
|
||||||
|
// 重置状态
|
||||||
|
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<void> => {
|
||||||
|
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<PresignStatusResponse | null> => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
-2
@@ -1,7 +1,8 @@
|
|||||||
// API 服务层
|
// API 服务层
|
||||||
import api from '@/utils/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 { 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 {
|
export class ConfigService {
|
||||||
@@ -128,12 +129,105 @@ export class StatsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预签名上传服务
|
||||||
|
export class PresignUploadService {
|
||||||
|
/**
|
||||||
|
* 初始化上传会话
|
||||||
|
*/
|
||||||
|
static async initUpload(request: PresignInitRequest): Promise<ApiResponse<PresignInitResponse>> {
|
||||||
|
return api.post('/presign/upload/init', request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理模式上传
|
||||||
|
*/
|
||||||
|
static async proxyUpload(
|
||||||
|
uploadId: string,
|
||||||
|
file: File,
|
||||||
|
onProgress?: (progress: UploadProgress) => void
|
||||||
|
): Promise<ApiResponse<PresignUploadResult>> {
|
||||||
|
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<ApiResponse<PresignUploadResult>> {
|
||||||
|
return api.post(`/presign/upload/confirm/${uploadId}`, request || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询上传状态
|
||||||
|
*/
|
||||||
|
static async getUploadStatus(uploadId: string): Promise<ApiResponse<PresignStatusResponse>> {
|
||||||
|
return api.get(`/presign/upload/status/${uploadId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消上传
|
||||||
|
*/
|
||||||
|
static async cancelUpload(uploadId: string): Promise<ApiResponse<{ message: string }>> {
|
||||||
|
return api.delete(`/presign/upload/${uploadId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3 直传(不经过后端)
|
||||||
|
*/
|
||||||
|
static async directUploadToS3(
|
||||||
|
uploadUrl: string,
|
||||||
|
file: File,
|
||||||
|
onProgress?: (progress: UploadProgress) => void
|
||||||
|
): Promise<boolean> {
|
||||||
|
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 = {
|
export const services = {
|
||||||
config: ConfigService,
|
config: ConfigService,
|
||||||
file: FileService,
|
file: FileService,
|
||||||
auth: AuthService,
|
auth: AuthService,
|
||||||
stats: StatsService
|
stats: StatsService,
|
||||||
|
presignUpload: PresignUploadService
|
||||||
}
|
}
|
||||||
|
|
||||||
export default services
|
export default services
|
||||||
@@ -178,3 +178,67 @@ export interface RouteConfig {
|
|||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== 预签名上传相关类型 ====================
|
||||||
|
|
||||||
|
// 预签名上传模式
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -511,6 +511,8 @@ import PageHeader from '@/components/common/PageHeader.vue'
|
|||||||
import SendTypeSelector from '@/components/common/SendTypeSelector.vue'
|
import SendTypeSelector from '@/components/common/SendTypeSelector.vue'
|
||||||
import FileUploadArea from '@/components/common/FileUploadArea.vue'
|
import FileUploadArea from '@/components/common/FileUploadArea.vue'
|
||||||
import TextInputArea from '@/components/common/TextInputArea.vue'
|
import TextInputArea from '@/components/common/TextInputArea.vue'
|
||||||
|
import { usePresignedUpload } from '@/composables/usePresignedUpload'
|
||||||
|
import type { ExpireStyle } from '@/types'
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
name: string
|
name: string
|
||||||
@@ -552,6 +554,14 @@ const { t } = useI18n()
|
|||||||
const alertStore = useAlertStore()
|
const alertStore = useAlertStore()
|
||||||
const sendRecords = computed(() => fileDataStore.shareData)
|
const sendRecords = computed(() => fileDataStore.shareData)
|
||||||
|
|
||||||
|
// 预签名上传
|
||||||
|
const {
|
||||||
|
presignStatus,
|
||||||
|
uploadProgress: presignProgress,
|
||||||
|
uploadFile: presignUploadFile,
|
||||||
|
reset: resetPresignUpload
|
||||||
|
} = usePresignedUpload()
|
||||||
|
|
||||||
const fileHash = ref('')
|
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)
|
const response: ApiResponse<{ code?: string; name?: string }> = await api.post('share/file/', formData, config)
|
||||||
return response
|
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 = () => {
|
const checkOpenUpload = () => {
|
||||||
if (config.openUpload === 0 && localStorage.getItem('token') === null) {
|
if (config.openUpload === 0 && localStorage.getItem('token') === null) {
|
||||||
alertStore.showAlert(t('send.messages.guestUploadDisabled'), 'error')
|
alertStore.showAlert(t('send.messages.guestUploadDisabled'), 'error')
|
||||||
@@ -963,12 +995,17 @@ const handleSubmit = async () => {
|
|||||||
let response: ApiResponse
|
let response: ApiResponse
|
||||||
|
|
||||||
if (sendType.value === 'file') {
|
if (sendType.value === 'file') {
|
||||||
// 使用切片上传替代原来的直接上传
|
// 优先使用预签名上传,如果失败则回退到其他方式
|
||||||
|
try {
|
||||||
|
response = await handlePresignedUpload(selectedFile.value!)
|
||||||
|
} catch {
|
||||||
|
// 预签名上传失败,回退到切片上传或默认上传
|
||||||
if (config.enableChunk) {
|
if (config.enableChunk) {
|
||||||
response = await handleChunkUpload(selectedFile.value!)
|
response = await handleChunkUpload(selectedFile.value!)
|
||||||
} else {
|
} else {
|
||||||
response = await handleDefaultFileUpload(selectedFile.value!)
|
response = await handleDefaultFileUpload(selectedFile.value!)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 文本上传保持不变
|
// 文本上传保持不变
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@@ -1008,6 +1045,7 @@ const handleSubmit = async () => {
|
|||||||
selectedFile.value = null
|
selectedFile.value = null
|
||||||
textContent.value = ''
|
textContent.value = ''
|
||||||
uploadProgress.value = 0
|
uploadProgress.value = 0
|
||||||
|
resetPresignUpload()
|
||||||
// 显示详情
|
// 显示详情
|
||||||
selectedRecord.value = newRecord
|
selectedRecord.value = newRecord
|
||||||
// 自动复制取件码链接
|
// 自动复制取件码链接
|
||||||
|
|||||||
Reference in New Issue
Block a user