This commit is contained in:
D8D Developer
2025-06-27 02:24:35 +00:00
parent e6ca49b039
commit 45f8b191f5
2 changed files with 2 additions and 917 deletions

View File

@@ -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<typeof filesClient["multipart-policy"]['$post'],200>
type MinioUploadPolicy = InferResponseType<typeof filesClient["policy"]['$post'],200>
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<UploadResult> {
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<string> {
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) {
// 获取ETagMinIO返回的标识
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<void> {
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<UploadResult> {
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<MinioUploadPolicy> {
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<UploadResult> {
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);
}
}

View File

@@ -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<T> {
user: T | null;
token: string | null;
login: (username: string, password: string, latitude?: number, longitude?: number) => Promise<void>;
logout: () => Promise<void>;
isAuthenticated: boolean;
isLoading: boolean;
}
// 主题上下文类型
export interface ThemeContextType {
isDark: boolean;
currentTheme: ThemeSettings;
updateTheme: (theme: Partial<ThemeSettings>) => void; // 实时预览
saveTheme: (theme: Partial<ThemeSettings>) => Promise<ThemeSettings>; // 保存到后端
resetTheme: () => Promise<ThemeSettings>;
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, string> = {
[EnableStatus.DISABLED]: '禁用',
[EnableStatus.ENABLED]: '启用'
};
// 删除状态枚举
export enum DeleteStatus {
NOT_DELETED = 0, // 未删除
DELETED = 1 // 已删除
}
// 删除状态中文映射
export const DeleteStatusNameMap: Record<DeleteStatus, string> = {
[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<string, any>;
}
// 审核状态枚举
export enum AuditStatus {
PENDING = 0, // 待审核
APPROVED = 1, // 已通过
REJECTED = 2 // 已拒绝
}
// 审核状态中文映射
export const AuditStatusNameMap: Record<AuditStatus, string> = {
[AuditStatus.PENDING]: '待审核',
[AuditStatus.APPROVED]: '已通过',
[AuditStatus.REJECTED]: '已拒绝'
};
// 图标类型映射
type IconType = 'dashboard' | 'user' | 'setting' | 'team' | 'book' | 'calendar' | 'pie-chart' | 'database';
// 图标类型中文映射
export const IconTypeNameMap: Record<IconType, string> = {
'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<SystemSettingKey, SystemSettingValue>;
// 允许的文件类型枚举
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, string> = {
[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;
}