From 45f8b191f527d90ddd2ef546e2a30105b9717b18 Mon Sep 17 00:00:00 2001 From: D8D Developer Date: Fri, 27 Jun 2025 02:24:35 +0000 Subject: [PATCH] u --- src/client/utils/MinIOXHRUploader.ts | 343 ---------------- src/share/types.ts | 576 +-------------------------- 2 files changed, 2 insertions(+), 917 deletions(-) delete mode 100644 src/client/utils/MinIOXHRUploader.ts diff --git a/src/client/utils/MinIOXHRUploader.ts b/src/client/utils/MinIOXHRUploader.ts deleted file mode 100644 index cdb0c19..0000000 --- a/src/client/utils/MinIOXHRUploader.ts +++ /dev/null @@ -1,343 +0,0 @@ -import type { InferResponseType, InferRequestType } from 'hono/client'; -import { MinioProgressCallbacks, UploadResult } from "@/share/types"; -import { filesClient } from "../api"; - - - -interface UploadPart { - ETag: string; - PartNumber: number; -} - -interface UploadProgressDetails { - partNumber: number; - totalParts: number; - partSize: number; - totalSize: number; - partProgress?: number; -} - -type MinioMultipartUploadPolicy = InferResponseType -type MinioUploadPolicy = InferResponseType - - -const PART_SIZE = 5 * 1024 * 1024; // 每部分5MB - - -export class MinIOXHRMultipartUploader { - /** - * 使用XHR分段上传文件到MinIO - */ - static async upload( - policy: MinioMultipartUploadPolicy, - file: File | Blob, - key: string, - callbacks?: MinioProgressCallbacks - ): Promise { - const partSize = PART_SIZE; - const totalSize = file.size; - const totalParts = Math.ceil(totalSize / partSize); - const uploadedParts: UploadPart[] = []; - - callbacks?.onProgress?.({ - stage: 'uploading', - message: '准备上传文件...', - progress: 0, - details: { - loaded: 0, - total: totalSize - }, - timestamp: Date.now() - }); - - // 分段上传 - for (let i = 0; i < totalParts; i++) { - const start = i * partSize; - const end = Math.min(start + partSize, totalSize); - const partBlob = file.slice(start, end); - const partNumber = i + 1; - - try { - const etag = await this.uploadPart( - policy.partUrls[i], - partBlob, - callbacks, - { - partNumber, - totalParts, - partSize: partBlob.size, - totalSize - } - ); - - uploadedParts.push({ - ETag: etag, - PartNumber: partNumber - }); - - // 更新进度 - const progress = Math.round((end / totalSize) * 100); - callbacks?.onProgress?.({ - stage: 'uploading', - message: `上传文件片段 ${partNumber}/${totalParts}`, - progress, - details: { - loaded: end, - total: totalSize, - }, - timestamp: Date.now() - }); - } catch (error) { - callbacks?.onError?.(error instanceof Error ? error : new Error(String(error))); - throw error; - } - } - - // 完成上传 - try { - await this.completeMultipartUpload(policy, key, uploadedParts); - - callbacks?.onProgress?.({ - stage: 'complete', - message: '文件上传完成', - progress: 100, - timestamp: Date.now() - }); - - callbacks?.onComplete?.(); - return { - fileUrl: `${policy.host}/${key}`, - fileKey: key, - bucketName: policy.bucket - }; - } catch (error) { - callbacks?.onError?.(error instanceof Error ? error : new Error(String(error))); - throw error; - } - } - - // 上传单个片段 - private static uploadPart( - uploadUrl: string, - partBlob: Blob, - callbacks?: MinioProgressCallbacks, - progressDetails?: UploadProgressDetails - ): Promise { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - xhr.upload.onprogress = (event) => { - if (event.lengthComputable && callbacks?.onProgress) { - const partProgress = Math.round((event.loaded / event.total) * 100); - callbacks.onProgress({ - stage: 'uploading', - message: `上传文件片段 ${progressDetails?.partNumber}/${progressDetails?.totalParts} (${partProgress}%)`, - progress: Math.round(( - (progressDetails?.partNumber ? (progressDetails.partNumber - 1) * (progressDetails.partSize || 0) : 0) + event.loaded - ) / (progressDetails?.totalSize || 1) * 100), - details: { - ...progressDetails, - loaded: event.loaded, - total: event.total - }, - timestamp: Date.now() - }); - } - }; - - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - // 获取ETag(MinIO返回的标识) - const etag = xhr.getResponseHeader('ETag')?.replace(/"/g, '') || ''; - resolve(etag); - } else { - reject(new Error(`上传片段失败: ${xhr.status} ${xhr.statusText}`)); - } - }; - - xhr.onerror = () => reject(new Error('上传片段失败')); - - xhr.open('PUT', uploadUrl); - xhr.send(partBlob); - - if (callbacks?.signal) { - callbacks.signal.addEventListener('abort', () => { - xhr.abort(); - reject(new Error('上传已取消')); - }); - } - }); - } - - // 完成分段上传 - private static async completeMultipartUpload( - policy: MinioMultipartUploadPolicy, - key: string, - uploadedParts: UploadPart[] - ): Promise { - const response = await filesClient["complete-multipart-upload"].$post({ - json:{ - bucket: policy.bucket, - key, - uploadId: policy.uploadId, - parts: uploadedParts - } - }); - - if (!response.ok) { - throw new Error(`完成分段上传失败: ${response.status} ${response.statusText}`); - } - } -} - -export class MinIOXHRUploader { - /** - * 使用XHR上传文件到MinIO - */ - static upload( - policy: MinioUploadPolicy, - file: File | Blob, - key: string, - callbacks?: MinioProgressCallbacks - ): Promise { - const formData = new FormData(); - - // 添加 MinIO 需要的字段 - Object.entries(policy).forEach(([k, value]) => { - // 排除 policy 中的 key、host、prefix、ossType 字段 - if (k !== 'key' && k !== 'host' && k !== 'prefix' && k !== 'ossType' && typeof value === 'string') { - formData.append(k, value); - } - }); - // 添加 自定义 key 字段 - formData.append('key', key); - formData.append('file', file); - - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - // 上传进度处理 - if (callbacks?.onProgress) { - xhr.upload.onprogress = (event) => { - if (event.lengthComputable) { - callbacks.onProgress?.({ - stage: 'uploading', - message: '正在上传文件...', - progress: Math.round((event.loaded * 100) / event.total), - details: { - loaded: event.loaded, - total: event.total - }, - timestamp: Date.now() - }); - } - }; - } - - // 完成处理 - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - if (callbacks?.onProgress) { - callbacks.onProgress({ - stage: 'complete', - message: '文件上传完成', - progress: 100, - timestamp: Date.now() - }); - } - callbacks?.onComplete?.(); - resolve({ - fileUrl:`${policy.host}/${key}`, - fileKey: key, - bucketName: policy.bucket - }); - } else { - const error = new Error(`上传失败: ${xhr.status} ${xhr.statusText}`); - callbacks?.onError?.(error); - reject(error); - } - }; - - // 错误处理 - xhr.onerror = () => { - const error = new Error('上传失败'); - if (callbacks?.onProgress) { - callbacks.onProgress({ - stage: 'error', - message: '文件上传失败', - progress: 0, - timestamp: Date.now() - }); - } - callbacks?.onError?.(error); - reject(error); - }; - - // 根据当前页面协议和 host 配置决定最终的上传地址 - const currentProtocol = typeof window !== 'undefined' ? window.location.protocol : 'https:'; - const host = policy.host?.startsWith('http') - ? policy.host - : `${currentProtocol}//${policy.host}`; - // 开始上传 - xhr.open('POST', host); - xhr.send(formData); - - // 处理取消 - if (callbacks?.signal) { - callbacks.signal.addEventListener('abort', () => { - xhr.abort(); - reject(new Error('上传已取消')); - }); - } - }); - } -} - -export async function getUploadPolicy(key: string): Promise { - const policyResponse = await filesClient.policy.$post({ - json: { key } - }); - if (!policyResponse.ok) { - throw new Error('获取上传策略失败'); - } - return policyResponse.json(); -} - -export async function getMultipartUploadPolicy(totalSize: number,fileKey: string) { - const policyResponse = await filesClient["multipart-policy"].$post({ - json: { totalSize, partSize: PART_SIZE, fileKey} - }); - if (!policyResponse.ok) { - throw new Error('获取分段上传策略失败'); - } - return await policyResponse.json(); -} - -export async function uploadMinIOWithPolicy( - uploadPath: string, - file: File | Blob, - fileKey: string, - callbacks?: MinioProgressCallbacks -): Promise { - if(uploadPath === '/') uploadPath = ''; - else{ - if(!uploadPath.endsWith('/')) uploadPath = `${uploadPath}/` - // 去掉开头的 / - if(uploadPath.startsWith('/')) uploadPath = uploadPath.replace(/^\//, ''); - } - - - const key = `${uploadPath}${fileKey}` - if( file.size > PART_SIZE ){ - const policy = await getMultipartUploadPolicy(file.size, key) - return MinIOXHRMultipartUploader.upload( - policy, - file, - key, - callbacks - ); - }else{ - const policy = await getUploadPolicy(key); - return MinIOXHRUploader.upload(policy, file, key, callbacks); - } -} \ No newline at end of file diff --git a/src/share/types.ts b/src/share/types.ts index e6c5e17..25d2949 100644 --- a/src/share/types.ts +++ b/src/share/types.ts @@ -1,587 +1,15 @@ -export enum OssType { - ALIYUN = 'aliyun', - MINIO = 'minio', -} - // 全局配置常量 export interface GlobalConfig { OSS_BASE_URL: string; - OSS_TYPE: OssType; - API_BASE_URL: string; APP_NAME: string; - ENV: string; - DEFAULT_THEME: string; - MAP_CONFIG: { - KEY: string; - VERSION: string; - PLUGINS: string[]; - MAP_MODE: MapMode; - }; - CHART_THEME: string; - ENABLE_THEME_CONFIG: boolean; - THEME: ThemeSettings | null; -} - - -export interface Role { - id: number, - name: string, - description?: string | null, - permissions: string[]; -} - - -// 定义类型 -export interface User { - id: number; - username: string; - nickname: string | null; - email: string | null; - phone: string | null; - roles?: Role[]; - avatar?: string | null; -} - -export interface MenuItem { - id: number; - name: string; - path: string; - icon: string; - component: string; - parent_id?: number; - children?: MenuItem[]; } // 认证上下文类型 -export interface AuthContextType { - user: User | null; +export interface AuthContextType { + user: T | null; token: string | null; login: (username: string, password: string, latitude?: number, longitude?: number) => Promise; logout: () => Promise; isAuthenticated: boolean; isLoading: boolean; } - -// 主题上下文类型 -export interface ThemeContextType { - isDark: boolean; - currentTheme: ThemeSettings; - updateTheme: (theme: Partial) => void; // 实时预览 - saveTheme: (theme: Partial) => Promise; // 保存到后端 - resetTheme: () => Promise; - toggleTheme: () => void; // 切换主题模式 -} - -// 主题模式枚举 -export enum ThemeMode { - LIGHT = 'light', - DARK = 'dark' -} - -// 字体大小枚举 -export enum FontSize { - SMALL = 'small', - MEDIUM = 'medium', - LARGE = 'large' -} - -// 紧凑模式枚举 -export enum CompactMode { - NORMAL = 0, // 正常模式 - COMPACT = 1 // 紧凑模式 -} - -// 颜色方案类型 -export interface ColorScheme { - name: string; - primary: string; - background: string; - text: string; -} - -// 主题设置类型 -export interface ThemeSettings { - /** 主键ID */ - // id?: number; - - /** 用户ID */ - // user_id: number; - - /** 主题模式(light/dark) */ - theme_mode: ThemeMode; - - /** 主题方案名称 */ - scheme_name?: string; - - /** 主题主色 */ - primary_color: string; - - /** 背景色 */ - background_color?: string; - - /** 文字颜色 */ - text_color?: string; - - /** 边框圆角 */ - border_radius?: number; - - /** 字体大小(small/medium/large) */ - font_size: FontSize; - - /** 是否紧凑模式(0否 1是) */ - is_compact: CompactMode; - - /** 创建时间 */ - // created_at?: string; - - // /** 更新时间 */ - // updated_at?: string; -} - -// 启用/禁用状态枚举 -export enum EnableStatus { - DISABLED = 0, // 禁用 - ENABLED = 1 // 启用 -} - -// 启用/禁用状态中文映射 -export const EnableStatusNameMap: Record = { - [EnableStatus.DISABLED]: '禁用', - [EnableStatus.ENABLED]: '启用' -}; - -// 删除状态枚举 -export enum DeleteStatus { - NOT_DELETED = 0, // 未删除 - DELETED = 1 // 已删除 -} - -// 删除状态中文映射 -export const DeleteStatusNameMap: Record = { - [DeleteStatus.NOT_DELETED]: '未删除', - [DeleteStatus.DELETED]: '已删除' -}; - -// 地图类型 -export enum MapMode { - ONLINE = 'online', - OFFLINE = 'offline' -} - - -// 地图标记数据接口 - 基础定义 -export interface MarkerData { - /** 标记点经度 */ - longitude: number; - - /** 标记点纬度 */ - latitude: number; - - /** 标记点ID */ - id?: string | number; - - /** 标记点标题 */ - title?: string; - - /** 标记点描述 */ - description?: string; - - /** 标记点图标URL */ - iconUrl?: string; - - /** 标记点状态 */ - status?: string; - - /** 标记点类型 */ - type?: string; - - /** 标记点额外数据 */ - extraData?: Record; -} - -// 审核状态枚举 -export enum AuditStatus { - PENDING = 0, // 待审核 - APPROVED = 1, // 已通过 - REJECTED = 2 // 已拒绝 -} - -// 审核状态中文映射 -export const AuditStatusNameMap: Record = { - [AuditStatus.PENDING]: '待审核', - [AuditStatus.APPROVED]: '已通过', - [AuditStatus.REJECTED]: '已拒绝' -}; - -// 图标类型映射 -type IconType = 'dashboard' | 'user' | 'setting' | 'team' | 'book' | 'calendar' | 'pie-chart' | 'database'; - -// 图标类型中文映射 -export const IconTypeNameMap: Record = { - 'dashboard': '仪表盘', - 'user': '用户', - 'setting': '设置', - 'team': '团队', - 'book': '文档', - 'calendar': '日历', - 'pie-chart': '图表', - 'database': '数据库' -}; - -// 附件类型定义 -export interface Attachment { - /** 附件ID */ - id: string; - - /** 附件名称 */ - name: string; - - /** 附件访问地址 */ - url: string; - - /** 附件类型(如image/jpeg, application/pdf等) */ - type: string; - - /** 附件大小(字节) */ - size: number; - - /** 上传时间 */ - upload_time: string; -} - -// 操作日志表 -export interface OperationLog { - /** 主键ID */ - id: number; - - /** 操作人ID */ - operator_id: number; - - /** 操作类型 */ - operation_type: string; - - /** 操作内容 */ - operation_content?: string; - - /** 操作结果 */ - operation_result?: string; - - /** 操作IP */ - ip_address?: string; - - /** 是否删除 (0否 1是) */ - is_deleted?: number; - - /** 创建时间 */ - created_at: Date; - - /** 更新时间 */ - updated_at: Date; -} - -// 系统设置分组 -export enum SystemSettingGroup { - BASIC = 'basic', // 基础设置 - FEATURE = 'feature', // 功能设置 - UPLOAD = 'upload', // 上传设置 - NOTIFICATION = 'notify' // 通知设置 -} - -// 系统设置键 -export enum SystemSettingKey { - // 基础设置 - SITE_NAME = 'SITE_NAME', // 站点名称 - SITE_DESCRIPTION = 'SITE_DESCRIPTION', // 站点描述 - SITE_KEYWORDS = 'SITE_KEYWORDS', // 站点关键词 - SITE_LOGO = 'SITE_LOGO', // 站点LOGO - SITE_FAVICON = 'SITE_FAVICON', // 站点图标 - - // 功能设置 - ENABLE_REGISTER = 'ENABLE_REGISTER', // 是否开启注册 - ENABLE_CAPTCHA = 'ENABLE_CAPTCHA', // 是否开启验证码 - LOGIN_ATTEMPTS = 'LOGIN_ATTEMPTS', // 登录尝试次数 - SESSION_TIMEOUT = 'SESSION_TIMEOUT', // 会话超时时间(分钟) - - // 上传设置 - UPLOAD_MAX_SIZE = 'UPLOAD_MAX_SIZE', // 最大上传大小(MB) - ALLOWED_FILE_TYPES = 'ALLOWED_FILE_TYPES', // 允许的文件类型 - IMAGE_COMPRESS = 'IMAGE_COMPRESS', // 是否压缩图片 - IMAGE_MAX_WIDTH = 'IMAGE_MAX_WIDTH', // 图片最大宽度 - - // 通知设置 - NOTIFY_ON_LOGIN = 'NOTIFY_ON_LOGIN', // 登录通知 - NOTIFY_ON_UPLOAD = 'NOTIFY_ON_UPLOAD', // 上传通知 - NOTIFY_ON_ERROR = 'NOTIFY_ON_ERROR', // 错误通知 - SMS_API_URL = 'SMS_API_URL', // SMS接口URL - SMS_API_AUTH = 'SMS_API_AUTH', // SMS接口认证头 - SMS_API_TIMEOUT = 'SMS_API_TIMEOUT', // SMS接口超时时间(毫秒) - SMS_API_RETRY = 'SMS_API_RETRY', // SMS接口重试次数 - - // 主题设置 - ENABLE_THEME_CONFIG = 'ENABLE_THEME_CONFIG' // 是否开启主题配置 -} - -export type SystemSettingGroupType = SystemSettingGroup; -export type SystemSettingKeyType = SystemSettingKey; - -// 系统设置值类型 -export type SystemSettingValue = string | number | boolean; - -// 系统设置项接口 -export interface SystemSetting { - id: number; - key: SystemSettingKeyType; // 设置键 - value: SystemSettingValue; // 设置值 - description?: string; // 设置描述 - group: SystemSettingGroupType; // 设置分组 - created_at?: string; - updated_at?: string; -} - -// 系统设置分组类型 -export interface SystemSettingGroupData { - name: string; - description: string; - settings: SystemSetting[]; -} - -// 系统设置记录类型 -export type SystemSettingRecord = Record; - -// 允许的文件类型枚举 -export enum AllowedFileType { - JPG = 'jpg', - JPEG = 'jpeg', - PNG = 'png', - GIF = 'gif', - DOC = 'doc', - DOCX = 'docx', - XLS = 'xls', - XLSX = 'xlsx', - PDF = 'pdf' -} - -// 允许的文件类型列表(用于系统设置) -export const ALLOWED_FILE_TYPES = Object.values(AllowedFileType).join(','); - -// 文件库接口 -export interface FileLibrary { - /** 主键ID */ - id: number; - - /** 文件名称 */ - file_name: string; - - /** 原始文件名 */ - original_filename?: string; - - /** 文件路径 */ - file_path: string; - - /** 文件类型 */ - file_type: string; - - /** 文件大小(字节) */ - file_size: number; - - /** 上传用户ID */ - uploader_id?: number; - - /** 上传者名称 */ - uploader_name?: string; - - /** 文件分类 */ - category_id?: number; - - /** 文件标签 */ - tags?: string; - - /** 文件描述 */ - description?: string; - - /** 下载次数 */ - download_count: number; - - /** 是否禁用 (0否 1是) */ - is_disabled?: EnableStatus; - - /** 是否被删除 (0否 1是) */ - is_deleted?: DeleteStatus; - - /** 创建时间 */ - created_at: string; - - /** 更新时间 */ - updated_at: string; -} - -// 文件分类接口 -export interface FileCategory { - id: number; - name: string; - code: string; - description?: string; - is_deleted?: DeleteStatus; - created_at: string; - updated_at: string; -} - - -// 知识库表 -export interface KnowInfo { - /** 主键ID */ - id: number; - - /** 标题 */ - title: string; - - /** 内容 */ - content?: string; - - /** 作者 */ - author?: string; - - /** 分类 */ - category: string; - - /** 标签 */ - tags?: string; - - /** 封面图片URL */ - cover_url?: string; - - /** 审核状态 */ - audit_status?: number; - - /** 排序权重 */ - sort_order?: number; - - /** 是否删除 (0否 1是) */ - is_deleted?: number; - - /** 创建时间 */ - created_at: string; - - /** 更新时间 */ - updated_at: string; -} - -// 登录位置相关类型定义 -export interface LoginLocation { - id: number; - loginTime: string; - ipAddress: string; - longitude: number; - latitude: number; - location_name?: string; - user: { - id: number; - username: string; - nickname: string; - }; -} - -export interface LoginLocationDetail { - id: number; - user_id: number; - login_time: string; - ip_address: string; - longitude: number; - latitude: number; - location_name: string; - user_agent: string; - user: { - id: number; - username: string; - nickname: string; - }; -} - -// 消息类型枚举 -export enum MessageType { - SYSTEM = 'system', // 系统通知 - PRIVATE = 'private', // 私信 - ANNOUNCE = 'announce' // 公告 -} - -// 消息状态枚举 -export enum MessageStatus { - UNREAD = 0, // 未读 - READ = 1, // 已读 - DELETED = 2 // 已删除 -} - -// 消息状态中文映射 -export const MessageStatusNameMap: Record = { - [MessageStatus.UNREAD]: '未读', - [MessageStatus.READ]: '已读', - [MessageStatus.DELETED]: '已删除' -}; - -// 消息实体接口 -export interface Message { - id: number; - title: string; - content: string; - type: MessageType; - sender_id?: number; // 发送者ID(系统消息可为空) - sender_name?: string; // 发送者名称 - created_at: string; - updated_at: string; -} - -// 用户消息关联接口 -export interface UserMessage { - id: number; - user_id: number; - message_id: number; - title: string; - content: string; - user_status: MessageStatus; - user_message_id: number; - is_deleted?: DeleteStatus; - read_at?: string; - created_at: string; - updated_at: string; - - // 关联信息 - message?: Message; - sender?: User; -} - -export interface MinioUploadPolicy { - 'x-amz-algorithm': string; - 'x-amz-credential': string; - 'x-amz-date': string; - 'x-amz-security-token'?: string; - policy: string; - 'x-amz-signature': string; - host: string; - prefix: string; - bucket: string; - // key: string; -} - -export interface MinioProgressEvent { - stage: 'uploading' | 'complete' | 'error'; - message: string; - progress: number; - details?: { - loaded: number; - total: number; - }; - timestamp: number; -} - -export interface MinioProgressCallbacks { - onProgress?: (event: MinioProgressEvent) => void; - onComplete?: () => void; - onError?: (error: Error) => void; - signal?: AbortSignal; -} - -export interface UploadResult { - fileUrl:string; - fileKey:string; - bucketName:string; -} \ No newline at end of file