'use client'; import { useState, useRef, useCallback } from 'react'; import * as tus from 'tus-js-client'; import { useAuth } from '@/components/providers/auth-provider'; /** * 上传状态 */ export interface TusUploadState { /** 当前上传的文件 */ file: File | null; /** 上传进度 (0-100) */ progress: number; /** 上传状态 */ status: 'idle' | 'uploading' | 'success' | 'error' | 'paused'; /** 错误信息 */ error?: string; /** 成功后的文件URL */ uploadUrl?: string; /** 已上传的字节数 */ bytesUploaded?: number; /** 文件总字节数 */ bytesTotal?: number; } /** * 上传元数据接口 */ export interface UploadMetadata { /** 用户ID */ authorId?: string; /** 父目录ID */ parentId?: string; /** 组织ID */ organizationId?: string; /** 文件描述 */ description?: string; /** 自定义标签 */ tags?: string[]; /** 自定义元数据 */ [key: string]: any; } /** * TUS 上传钩子 */ export function useTusUpload(defaultMetadata: UploadMetadata = {}) { const { user } = useAuth(); const [state, setState] = useState({ file: null, progress: 0, status: 'idle', }); const uploadRef = useRef(null); const retryDelays = [1000, 3000, 5000]; // 重试延迟 /** * 构建TUS元数据 */ const buildTusMetadata = useCallback((file: File, customMetadata: UploadMetadata = {}) => { const metadata: Record = { filename: file.name, filetype: file.type || 'application/octet-stream', filesize: file.size.toString(), }; // 添加用户信息 if (user?.id) { metadata.authorId = user.id; } if (user?.organizationId) { metadata.organizationId = user.organizationId; } // 添加默认元数据 Object.entries(defaultMetadata).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (typeof value === 'object') { metadata[key] = JSON.stringify(value); } else { metadata[key] = String(value); } } }); // 添加自定义元数据 Object.entries(customMetadata).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (typeof value === 'object') { metadata[key] = JSON.stringify(value); } else { metadata[key] = String(value); } } }); return metadata; }, [user, defaultMetadata]); /** * 开始上传文件 */ const uploadFile = useCallback((file: File, metadata: UploadMetadata = {}) => { // 构建元数据 const tusMetadata = buildTusMetadata(file, metadata); // 创建上传实例 const upload = new tus.Upload(file, { endpoint: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/files/`, retryDelays, metadata: tusMetadata, chunkSize: 1024 * 1024, // 1MB 分块大小 onError: (error: Error) => { console.error('上传失败:', error); console.error('错误详情:', error); setState((prev) => ({ ...prev, status: 'error', error: error.message, })); }, onProgress: (bytesUploaded: number, bytesTotal: number) => { const progress = Math.round((bytesUploaded / bytesTotal) * 100); setState((prev) => ({ ...prev, progress, bytesUploaded, bytesTotal, })); }, onSuccess: () => { setState((prev) => ({ ...prev, status: 'success', uploadUrl: upload.url || undefined, progress: 100, })); }, onShouldRetry: (err: Error, retryAttempt: number) => { // TUS错误对象可能包含额外属性 const tusError = err as any; const status = tusError.originalResponse ? tusError.originalResponse.getStatus() : 0; // 不重试认证错误、权限错误等 if (status === 401 || status === 403 || status === 413) { return false; } // 如果超过最大重试次数,不再重试 if (retryAttempt >= retryDelays.length) { return false; } return true; }, }); // 保存上传实例引用 uploadRef.current = upload; // 设置初始状态 setState({ file, progress: 0, status: 'uploading', error: undefined, uploadUrl: undefined, bytesUploaded: 0, bytesTotal: file.size, }); // 开始上传 upload.start(); }, [buildTusMetadata, retryDelays]); /** * 批量上传文件 */ const uploadFiles = useCallback(async (files: File[], metadata: UploadMetadata = {}) => { const results: Array<{ file: File; success: boolean; error?: string; uploadUrl?: string }> = []; for (const file of files) { try { await new Promise((resolve, reject) => { const tusMetadata = buildTusMetadata(file, metadata); const upload = new tus.Upload(file, { endpoint: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/files/`, retryDelays, metadata: tusMetadata, onError: reject, onSuccess: () => { results.push({ file, success: true, uploadUrl: upload.url || undefined, }); resolve(); }, onShouldRetry: (err: Error, retryAttempt: number) => { const tusError = err as any; const status = tusError.originalResponse ? tusError.originalResponse.getStatus() : 0; if (status === 401 || status === 403 || status === 413) { return false; } if (retryAttempt >= retryDelays.length) { return false; } return true; }, }); upload.start(); }); } catch (error) { results.push({ file, success: false, error: error instanceof Error ? error.message : '上传失败', }); } } return results; }, [buildTusMetadata, retryDelays]); /** * 暂停上传 */ const pauseUpload = useCallback(() => { if (uploadRef.current) { uploadRef.current.abort(); setState((prev) => ({ ...prev, status: 'paused', })); } }, []); /** * 恢复上传 */ const resumeUpload = useCallback(() => { if (uploadRef.current) { uploadRef.current.start(); setState((prev) => ({ ...prev, status: 'uploading', error: undefined, })); } }, []); /** * 取消上传 */ const cancelUpload = useCallback(() => { if (uploadRef.current) { uploadRef.current.abort(); uploadRef.current = null; } setState({ file: null, progress: 0, status: 'idle', }); }, []); /** * 重置状态 */ const reset = useCallback(() => { setState({ file: null, progress: 0, status: 'idle', }); }, []); return { state, uploadFile, uploadFiles, pauseUpload, resumeUpload, cancelUpload, reset, // 便利方法 isUploading: state.status === 'uploading', isSuccess: state.status === 'success', isError: state.status === 'error', isPaused: state.status === 'paused', canResume: state.status === 'paused', canPause: state.status === 'uploading', }; }