You've already forked DataMate
init datamate
This commit is contained in:
79
frontend/src/utils/file.util.ts
Normal file
79
frontend/src/utils/file.util.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
55
frontend/src/utils/loading.ts
Normal file
55
frontend/src/utils/loading.ts
Normal 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;
|
||||
526
frontend/src/utils/request.ts
Normal file
526
frontend/src/utils/request.ts
Normal 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
295
frontend/src/utils/unit.ts
Normal 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"
|
||||
|
||||
// 你可以根据需要添加更多的实用函数
|
||||
|
||||
// 例如:深拷贝对象、合并对象、数组去重、节流、防抖等
|
||||
|
||||
// 这些函数可以根据你的项目需求进行调整和扩展
|
||||
|
||||
// 记得添加适当的类型注解以提高代码的可读性和可维护性
|
||||
|
||||
// 以及编写单元测试以确保函数的正确性
|
||||
Reference in New Issue
Block a user