init datamate

This commit is contained in:
Dallas98
2025-10-21 23:00:48 +08:00
commit 1c97afed7d
692 changed files with 135442 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
import { UploadFile } from "antd";
import jsSHA from "jssha";
const CHUNK_SIZE = 1024 * 1024 * 60;
export function sliceFile(file, chunkSize = CHUNK_SIZE): Blob[] {
const totalSize = file.size;
let start = 0;
let end = start + chunkSize;
const chunks = [];
while (start < totalSize) {
const blob = file.slice(start, end);
chunks.push(blob);
start = end;
end = start + chunkSize;
}
return chunks;
}
export function calculateSHA256(file: Blob): Promise<string> {
let count = 0;
const hash = new jsSHA("SHA-256", "ARRAYBUFFER", { encoding: "UTF8" });
return new Promise((resolve, reject) => {
const reader = new FileReader();
function readChunk(start: number, end: number) {
const slice = file.slice(start, end);
reader.readAsArrayBuffer(slice);
}
const bufferChunkSize = 1024 * 1024 * 20;
function processChunk(offset: number) {
const start = offset;
const end = Math.min(start + bufferChunkSize, file.size);
count = end;
readChunk(start, end);
}
reader.onloadend = function () {
const arraybuffer = reader.result;
hash.update(arraybuffer);
if (count < file.size) {
processChunk(count);
} else {
resolve(hash.getHash("HEX", { outputLen: 256 }));
}
};
processChunk(0);
});
}
export function checkIsFilesExist(
fileList: UploadFile[]
): Promise<UploadFile | null> {
return new Promise((resolve) => {
const loadEndFn = (file: UploadFile, reachEnd: boolean, e) => {
const fileNotExist = !e.target.result;
if (fileNotExist) {
resolve(file);
}
if (reachEnd) {
resolve(null);
}
};
for (let i = 0; i < fileList.length; i++) {
const { originFile: file } = fileList[i];
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onloadend = (e) =>
loadEndFn(fileList[i], i === fileList.length - 1, e);
}
});
}

View File

@@ -0,0 +1,55 @@
class LoadingManager {
constructor() {
this.isShowing = false;
this.queue = 0; // 支持多个并发请求
}
show() {
this.queue++;
this.isShowing = true;
// 触发全局事件
const event = new Event("loading:show");
window.dispatchEvent(event);
}
hide() {
this.queue = Math.max(0, this.queue - 1);
if (this.queue === 0) {
this.isShowing = false;
// 触发全局事件
const event = new Event("loading:hide");
window.dispatchEvent(event);
}
}
// 强制隐藏所有加载
hideAll() {
this.queue = 0;
this.isShowing = false;
const event = new Event("loading:hide");
window.dispatchEvent(event);
}
// 获取当前状态
getStatus() {
return {
isShowing: this.isShowing,
queueCount: this.queue,
};
}
}
// 创建单例实例
const loadingManager = new LoadingManager();
// 导出常用方法
export const Loading = {
show: () => loadingManager.show(),
hide: () => loadingManager.hide(),
hideAll: () => loadingManager.hideAll(),
getStatus: () => loadingManager.getStatus(),
};
export default Loading;

View File

@@ -0,0 +1,526 @@
import { message } from "antd";
import Loading from "./loading";
/**
* 通用请求工具类
*/
class Request {
constructor(baseURL = "") {
this.baseURL = baseURL;
this.defaultHeaders = {
"Content-Type": "application/json",
Accept: "*/*",
};
// 请求拦截器列表
this.requestInterceptors = [];
// 响应拦截器列表
this.responseInterceptors = [];
}
_count = 0;
$interval;
get count() {
return this._count;
}
set count(value) {
clearTimeout(this.$interval);
if (value > 0) {
Loading.show();
}
if (value <= 0) {
this.$interval = setTimeout(() => {
Loading.hide();
}, 300);
}
this._count = value >= 0 ? value : 0;
}
/**
* 添加请求拦截器
*/
addRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
}
/**
* 添加响应拦截器
*/
addResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
}
/**
* 执行请求拦截器
*/
async executeRequestInterceptors(config) {
let processedConfig = { ...config };
for (const interceptor of this.requestInterceptors) {
processedConfig = (await interceptor(processedConfig)) || processedConfig;
}
return processedConfig;
}
/**
* 执行响应拦截器
*/
async executeResponseInterceptors(response, config) {
let processedResponse = response;
for (const interceptor of this.responseInterceptors) {
processedResponse =
(await interceptor(processedResponse, config)) || processedResponse;
}
return processedResponse;
}
/**
* 创建支持进度监听的XMLHttpRequest
*/
createXHRWithProgress(url, config, onProgress, onDownloadProgress) {
return new Promise((resolve, reject) => {
// 设置请求头
if (config.headers) {
Object.keys(config.headers).forEach((key) => {
xhr.setRequestHeader(key, config.headers[key]);
});
}
const xhr = new XMLHttpRequest();
console.log("upload xhr", url, config);
// 监听上传进度
xhr.upload.addEventListener("progress", function (event) {
if (event.lengthComputable) {
if (onProgress) {
onProgress(event);
}
if (onDownloadProgress) {
onDownloadProgress(event);
}
}
});
// 请求完成
// xhr.addEventListener("load", function () {
// if (xhr.status >= 200 && xhr.status < 300) {
// const response = JSON.parse(xhr.responseText);
// resolve(xhr);
// }
// });
// 请求完成处理
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
let response;
try {
// 尝试解析JSON
const contentType = xhr.getResponseHeader("content-type");
if (contentType && contentType.includes("application/json")) {
response = JSON.parse(xhr.responseText);
} else {
response = xhr.responseText;
}
} catch (e) {
response = xhr.responseText;
}
resolve({
data: response,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
xhr: xhr,
});
} else {
reject(new Error(`HTTP error! status: ${xhr.status}`));
}
});
// 请求错误
xhr.addEventListener("error", function () {
console.error("网络错误");
if (onError) onError(new Error("网络错误"));
});
// 请求中止
xhr.addEventListener("abort", function () {
console.log("上传已取消");
if (onError) onError(new Error("上传已取消"));
});
xhr.open("POST", url);
xhr.send(config.body);
return xhr; // 返回 xhr 对象以便后续控制
});
}
/**
* 构建完整URL
*/
buildURL(url, params) {
const fullURL = this.baseURL + url;
if (!params) return fullURL;
const searchParams = new URLSearchParams();
Object.keys(params).forEach((key) => {
if (params[key] !== undefined && params[key] !== null) {
searchParams.append(key, params[key]);
}
});
const queryString = searchParams.toString();
return queryString ? `${fullURL}?${queryString}` : fullURL;
}
/**
* 处理响应
*/
async handleResponse(response, config) {
// 如果显示了loading,需要隐藏
if (config.showLoading) {
this.count--;
}
// 执行响应拦截器
const processedResponse = await this.executeResponseInterceptors(
response,
config
);
if (!processedResponse.ok) {
const error = new Error(
`HTTP error! status: ${processedResponse.status}`
);
error.status = processedResponse.status;
error.statusText = processedResponse.statusText;
try {
const errorData = await processedResponse.json();
error.data = errorData;
message.error(`请求失败,错误信息: ${processedResponse.statusText}`);
} catch {
// 忽略JSON解析错误
}
throw error;
}
// 检查响应是否为空
const contentType = processedResponse.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return await processedResponse.json();
}
return await processedResponse.text();
}
/**
* 处理XHR响应
*/
async handleXHRResponse(xhrResponse, config) {
// 模拟fetch响应格式用于拦截器
const mockResponse = {
ok: xhrResponse.status >= 200 && xhrResponse.status < 300,
status: xhrResponse.status,
statusText: xhrResponse.statusText,
headers: {
get: (key) => xhrResponse.xhr.getResponseHeader(key),
},
};
// 执行响应拦截器
await this.executeResponseInterceptors(mockResponse, config);
if (!mockResponse.ok) {
const error = new Error(`HTTP error! status: ${xhrResponse.status}`);
error.status = xhrResponse.status;
error.statusText = xhrResponse.statusText;
error.data = xhrResponse.data;
message.error(`请求失败,错误信息: ${xhrResponse.statusText}`);
throw error;
}
return xhrResponse.data;
}
/**
* 通用请求方法
*/
async request(url, config) {
// 处理showLoading参数
if (config.showLoading) {
this.count++;
}
// 执行请求拦截器
const processedConfig = await this.executeRequestInterceptors(config);
// 如果需要进度监听,使用XMLHttpRequest
if (config.onUploadProgress || config.onDownloadProgress) {
const xhrResponse = await this.createXHRWithProgress(
url,
processedConfig,
config.onUploadProgress,
config.onDownloadProgress
);
return await this.handleXHRResponse(xhrResponse, processedConfig);
}
// 否则使用fetch
if (processedConfig.body instanceof FormData) {
}
const response = await fetch(url, processedConfig);
return await this.handleResponse(response, processedConfig);
}
/**
* GET请求
* @param {string} url - 请求URL
* @param {object} params - 查询参数
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
*/
async get(url, params = null, options = {}) {
const fullURL = this.buildURL(url, params);
const config = {
method: "GET",
headers: {
...this.defaultHeaders,
...options.headers,
},
...options,
};
return this.request(fullURL, config);
}
/**
* POST请求
* @param {string} url - 请求URL
* @param {object} data - 请求体数据
* @param {object} options - 额外的fetch选项,包括showLoading, onUploadProgress, onDownloadProgress
*/
async post(url, data = {}, options = {}) {
let config = {
method: "POST",
headers: {
...this.defaultHeaders,
...options.headers,
},
body: data ? JSON.stringify(data) : undefined,
...options,
};
const isFormData = data instanceof FormData;
if (isFormData) {
config = {
method: "POST",
headers: {
...options.headers, // FormData不需要Content-Type
},
body: data,
...options,
};
}
console.log("post", url, config);
return this.request(this.baseURL + url, config);
}
/**
* PUT请求
* @param {string} url - 请求URL
* @param {object} data - 请求体数据
* @param {object} options - 额外的fetch选项,包括showLoading, onUploadProgress, onDownloadProgress
*/
async put(url, data = null, options = {}) {
const config = {
method: "PUT",
headers: {
...this.defaultHeaders,
...options.headers,
},
body: data ? JSON.stringify(data) : undefined,
...options,
};
return this.request(this.baseURL + url, config);
}
/**
* DELETE请求
* @param {string} url - 请求URL
* @param {object} params - 查询参数或请求体数据
* @param {object} options - 额外的fetch选项,包括showLoading
*/
async delete(url, params = null, options = {}) {
let fullURL = this.baseURL + url;
let config = {
method: "DELETE",
redirect: "follow",
headers: {
...this.defaultHeaders,
...options.headers,
"X-Requested-With": "XMLHttpRequest",
},
credentials: "include",
mode: "cors",
...options,
};
// 判断params是否应该作为查询参数或请求体
if (params && typeof params === "object" && !Array.isArray(params)) {
// 如果params是普通对象,检查是否应该作为查询参数
const isQueryParams =
Object.keys(params).length === 1 &&
(Object.prototype.hasOwnProperty.call(params, "id") ||
Object.prototype.hasOwnProperty.call(params, "ids"));
if (isQueryParams) {
fullURL = this.buildURL(url, params);
} else {
// 作为请求体发送
config.body = JSON.stringify(params);
}
} else if (Array.isArray(params)) {
// 数组形式的数据作为请求体发送
config.body = JSON.stringify(params);
} else if (params) {
// 其他情况作为查询参数
fullURL = this.buildURL(url, { id: params });
}
return this.request(fullURL, config);
}
/**
* 下载文件
* @param {string} url - 请求URL
* @param {object} params - 查询参数
* @param {string} filename - 下载文件名
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
*/
async download(url, params = null, filename = "download", options = {}) {
const fullURL = this.buildURL(url, params);
const config = {
method: "GET",
responseType: "blob",
...options,
};
// 执行请求拦截器
const processedConfig = await this.executeRequestInterceptors(config);
let blob;
// 如果需要下载进度监听,使用XMLHttpRequest
if (config.onDownloadProgress) {
const xhrResponse = await this.createXHRWithProgress(
fullURL,
{ ...processedConfig, responseType: "blob" },
null,
config.onDownloadProgress
);
if (xhrResponse.status < 200 || xhrResponse.status >= 300) {
throw new Error(`HTTP error! status: ${xhrResponse.status}`);
}
blob = xhrResponse.xhr.response;
} else {
// 使用fetch
const response = await fetch(fullURL, processedConfig);
// 执行响应拦截器
const processedResponse = await this.executeResponseInterceptors(
response,
processedConfig
);
if (!processedResponse.ok) {
throw new Error(`HTTP error! status: ${processedResponse.status}`);
}
blob = await processedResponse.blob();
}
// 创建下载链接
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = filename;
// 添加到DOM并触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理URL对象
window.URL.revokeObjectURL(downloadUrl);
return blob;
}
/**
* 上传文件(专门的上传方法)
* @param {string} url - 上传URL
* @param {FormData|File} data - 文件数据
* @param {object} options - 选项,包括onUploadProgress回调
*/
async upload(url, data, options = {}) {
let formData = data;
// 如果传入的是File对象,包装成FormData
if (data instanceof File) {
formData = new FormData();
formData.append("file", data);
}
return this.post(url, formData, {
...options,
showLoading: options.showLoading !== false, // 上传默认显示loading
onUploadProgress: options.onUploadProgress, // 上传进度回调
});
}
}
// 创建默认实例
const request = new Request();
// 添加默认请求拦截器 - Token处理
request.addRequestInterceptor((config) => {
const token =
localStorage.getItem("token") || sessionStorage.getItem("token");
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
});
// 添加默认响应拦截器 - 错误处理
request.addResponseInterceptor((response, config) => {
// 可以在这里添加全局错误处理逻辑
// 比如token过期自动跳转登录页等
return response;
});
// 导出方法
export const get = request.get.bind(request);
export const post = request.post.bind(request);
export const put = request.put.bind(request);
export const del = request.delete.bind(request);
export const download = request.download.bind(request);
export const upload = request.upload.bind(request);
// 导出类,允许创建自定义实例
export { Request };
// 默认导出
export default request;

295
frontend/src/utils/unit.ts Normal file
View File

@@ -0,0 +1,295 @@
// 字节数转换为更大单位的方法
export const formatBytes = (bytes: number): string => {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
const k = 1024;
const decimals = 3;
const i = Math.floor(Math.log(bytes) / Math.log(k));
const value = bytes / Math.pow(k, i);
// 如果是整数则不显示小数点,否则最多显示3位小数并去除末尾的0
const formattedValue =
value % 1 === 0
? value.toString()
: parseFloat(value.toFixed(decimals)).toString();
return `${formattedValue} ${units[i]}`;
};
export const formatDateTime = (dateString: string): string => {
if (!dateString) return "";
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
export const formatDate = (dateString: string): string => {
if (!dateString) return "";
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
export const formatTime = (dateString: string): string => {
if (!dateString) return "";
const date = new Date(dateString);
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
};
export function formatExecutionDuration(
startTime: string,
endTime: string
): string {
if (!startTime || !endTime) return "--";
const start = new Date(startTime).getTime();
const end = new Date(endTime).getTime();
const durationInSeconds = Math.floor((end - start) / 1000);
return formatDuration(durationInSeconds);
}
export const formatDuration = (seconds: number): string => {
if (seconds < 60) {
return `${seconds}`;
} else if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return secs === 0 ? `${mins} 分钟` : `${mins} 分钟 ${secs}`;
} else {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return mins === 0 ? `${hrs} 小时` : `${hrs} 小时 ${mins} 分钟`;
}
};
export const formatNumber = (num: number): string => {
if (num >= 1e9) {
return (num / 1e9).toFixed(2).replace(/\.?0+$/, "") + "B";
} else if (num >= 1e6) {
return (num / 1e6).toFixed(2).replace(/\.?0+$/, "") + "M";
} else if (num >= 1e3) {
return (num / 1e3).toFixed(2).replace(/\.?0+$/, "") + "K";
} else {
return num.toString();
}
};
export const formatPercentage = (num: number): string => {
return (num * 100).toFixed(2).replace(/\.?0+$/, "") + "%";
};
export const truncateString = (str: string, maxLength: number): string => {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength) + "...";
};
export const capitalizeFirstLetter = (str: string): string => {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const lowercaseFirstLetter = (str: string): string => {
if (!str) return str;
return str.charAt(0).toLowerCase() + str.slice(1);
};
export const slugify = (str: string): string => {
return str
.toLowerCase()
.trim()
.replace(/[\s\W-]+/g, "-")
.replace(/^-+|-+$/g, "");
};
export const unslugify = (str: string): string => {
return str.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
};
export const isValidEmail = (email: string): boolean => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email.toLowerCase());
};
export const isValidURL = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
export const isValidPhoneNumber = (phone: string): boolean => {
const re = /^\+?[1-9]\d{1,14}$/; // E.164 format
return re.test(phone);
};
export const generateRandomString = (length: number): string => {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
export const generateUUID = (): string => {
// 简单的UUID生成方法
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
export const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const debounce = <F extends (...args: any[]) => any>(
func: F,
wait: number
): F => {
let timeout: NodeJS.Timeout;
return function (this: any, ...args: any[]) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
} as F;
};
export const throttle = <F extends (...args: any[]) => any>(
func: F,
limit: number
): F => {
let inThrottle: boolean;
return function (this: any, ...args: any[]) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
} as F;
};
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj));
};
export const mergeObjects = <T, U>(obj1: T, obj2: U): T & U => {
return { ...obj1, ...obj2 };
};
export const pick = <T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> => {
const result = {} as Pick<T, K>;
keys.forEach((key) => {
if (key in obj) {
result[key] = obj[key];
}
});
return result;
};
export const omit = <T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> => {
const result = { ...obj } as T;
keys.forEach((key) => {
if (key in result) {
delete result[key];
}
});
return result;
};
export const groupBy = <T, K extends keyof T>(
array: T[],
key: K
): Record<string, T[]> => {
return array.reduce((result, currentItem) => {
const groupKey = String(currentItem[key]);
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(currentItem);
return result;
}, {} as Record<string, T[]>);
};
export const uniqueBy = <T, K extends keyof T>(array: T[], key: K): T[] => {
const seen = new Set();
return array.filter((item) => {
const k = item[key];
return seen.has(k) ? false : seen.add(k);
});
};
export const sortBy = <T, K extends keyof T>(
array: T[],
key: K,
ascending = true
): T[] => {
return [...array].sort((a, b) => {
if (a[key] < b[key]) return ascending ? -1 : 1;
if (a[key] > b[key]) return ascending ? 1 : -1;
return 0;
});
};
export const chunkArray = <T>(array: T[], size: number): T[][] => {
const result: T[][] = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
};
export const arrayDifference = <T>(arr1: T[], arr2: T[]): T[] => {
const set2 = new Set(arr2);
return arr1.filter((item) => !set2.has(item));
};
export const arrayIntersection = <T>(arr1: T[], arr2: T[]): T[] => {
const set2 = new Set(arr2);
return arr1.filter((item) => set2.has(item));
};
export const arrayUnion = <T>(arr1: T[], arr2: T[]): T[] => {
return Array.from(new Set([...arr1, ...arr2]));
};
export const flattenArray = <T>(array: T[][]): T[] => {
return array.reduce((acc, val) => acc.concat(val), []);
};
export const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
// 这里可以添加提示消息
};
// 示例用法
// console.log(formatBytes(1024)); // "1 KB"
// console.log(formatDateTime("2023-10-01T12:34:56Z")); // "2023-10-01 12:34:56"
// console.log(isValidEmail("test@example.com")); // true
// console.log(generateUUID()); // "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
// 你可以根据需要添加更多的实用函数
// 例如:深拷贝对象、合并对象、数组去重、节流、防抖等
// 这些函数可以根据你的项目需求进行调整和扩展
// 记得添加适当的类型注解以提高代码的可读性和可维护性
// 以及编写单元测试以确保函数的正确性