import CryptoJS from 'crypto-js';
import * as xlsx from 'xlsx';
import { StorageKeysEnum } from '@/constants';
export const isObject = <T extends object>(value: unknown): value is T => {
return value !== null && typeof value === 'object';
};
export const isNumber = (value: unknown): value is number => {
return typeof value === 'number';
};
/**
*
* @param {unknown} value
*/
export const isFormData = (value: unknown) => {
return Object.prototype.toString.call(value) === '[object FormData]';
};
/**
* 从 localStorage 中获取 UserInfo
* @returns UserInfo | null
*/
export const getUserInfoFromStorage = <T extends Record<string, string>>() => {
const json = window.localStorage.getItem(StorageKeysEnum.USER_INFO);
if (!json) {
return null;
}
return JSON.parse(json) as T;
};
export const dateToString = (data?: any) => {
if (!(data instanceof Object) || data instanceof FormData) {
return data;
}
const keys = Object.keys(data);
return keys.reduce<any>((previousValue, currentValue) => {
const value = data[currentValue];
previousValue[currentValue] = value instanceof Date ? formatDateTime(value) : value;
return previousValue;
}, {});
};
export interface BuildHeadersOptions {
baseURL?: string;
url?: string;
params?: any;
method?: string;
data?: any;
}
const paramsSerializer = (params: any) => {
const result: string[] = [];
const _params = Object.keys(params)
// 过滤掉对象上 undefined, function 等等不能序列化的参数, 和 url 上面的保持一致
// PS: 不能使用 JSON.parse(JSON.stringify(params)) 过滤, 因为 params 也支持数组, 但是这个方法会把数组也过滤
.reduce<Record<string, unknown>>((previousValue, currentValue) => {
const valueType = typeof params[currentValue];
if (!['undefined', 'function'].includes(valueType)) {
previousValue[currentValue] = params[currentValue];
}
return previousValue;
}, {});
const paramsKeys = Object.keys(_params)
// 获取 params 所有的 key
// 用 params 的 key 和 value 拼成的 urlParams 字符串, 后端会自动按 a-z 的顺序排序拼接到 url 上, 虽然参数都一样, 但是因为参数顺序不一样, 生成的 MD5 字符串也会不一样, 这就会导致签名不一致
// 所以需要跟后端同步拼接顺序 (sort 方法会自动按照 a-z 的顺序排序)
.sort();
// 如果有 key, 说明不是空对象 (空对象虽然不影响 url, 拼接不上去, 但是添加 ? 时需要知道, 不带 params 时不需要带 ?)
const hasParams = paramsKeys.length !== 0;
for (const key of paramsKeys) {
const value = params[key];
if (value == null) {
continue;
}
let elements: string;
if (Array.isArray(value)) {
if (!value.length) {
continue;
}
// PS: params 的数组, (标准) 需要手动给 key 添加后缀 [${index}]
elements = value.map((value, index) => `${encodeURIComponent(`${key}[${index}]`)}=${encodeURIComponent(value)}`).join('&');
} else {
elements = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}
result.push(elements);
}
return hasParams ? '?' + result.join('&') : '';
};
/**
* 根据配置生成对应的 headers 对象
*/
export const buildHeaders = (options?: BuildHeadersOptions) => {
const { baseURL = '', url = '', params = {}, method, data = null } = options ?? {};
const token = window.localStorage.getItem(StorageKeysEnum.TOKEN) ?? '';
const userId = getUserInfoFromStorage()?.id ?? '';
const originTimestamp = new Date().getTime();
const timestamp = originTimestamp.toString().substring(0, 10);
// 如果是 GET 请求, 那么就没有 request body, 那么 content 就不是 json 格式
const isGet = method?.toUpperCase() === 'GET';
// 一般情况下 data 都是对象和 FormData, 所以只要 data 不是 FormData, 那么 content 就是 json 格式
const isJson = !isFormData(data);
const searchParams = paramsSerializer(params);
const uri = baseURL + url + searchParams;
let sign;
// 逻辑: 不是 json 请求和 json 请求的 headers 不一样
if (isGet) {
// GET 请求
sign = userId + timestamp + timestamp + uri;
} else if (isJson) {
// 不是 GET 但是 data 是 json
const content = data ? JSON.stringify(data) : '';
sign = userId + timestamp + content + timestamp + uri;
} else {
// 不是 GET 并且 data 也不是 json (例如: POST 并且传输 FormData)
sign = userId + timestamp + timestamp + uri;
}
// if (url.includes('upload')) {
// console.log('origin decodeURIComponent(sign)', decodeURIComponent(sign));
// console.log('origin sign', sign);
// console.log('timestamp', formatDateTime(Number(originTimestamp)));
// console.log('timestamp', originTimestamp);
// }
sign = CryptoJS.MD5(sign).toString();
return {
// 登陆时返回的 token
'Ds-Token': token,
// 用户 ID
'Ds-User-Id': userId,
// md5 (userId + time + content + time + uri) 只有发送 json 时需要 content
'Ds-Sign': sign,
// 当前时间戳
'Ds-Time': timestamp,
};
};
export const download = (source: string | Blob) => {
let link;
if (typeof source === 'string') {
link = source;
downloadByLink(link);
} else if (source instanceof Blob) {
const blob = new Blob([source]);
link = URL.createObjectURL(blob);
downloadByLink(link);
URL.revokeObjectURL(link);
}
};
const downloadByLink = (source: string) => {
const anchor = document.createElement('a');
anchor.download = source;
anchor.href = source;
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
};
export const openWindow = (url?: string | URL, target?: string) => {
const newWindow = window.open(url, target);
if (!newWindow) {
const anchor = document.createElement('a');
url && (anchor.href = url.toString());
target && (anchor.target = target);
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
};
export const formatNumberByCN = (number: number) => {
const tenThousand = 10000;
const aHundredMillion = tenThousand * tenThousand;
const tenThousandUnit = '亿';
const aHundredMillionUnit = '万';
const reservedBit = 2;
let result;
if (((result = number / aHundredMillion), result >= 1)) {
return result.toFixed(reservedBit) + tenThousandUnit;
} else if (((result = number / tenThousand), result >= 1)) {
return result.toFixed(reservedBit) + aHundredMillionUnit;
} else {
return number;
}
};
export const withNullAsUndefined = <T>(value: T | null) => {
return value === null ? undefined : value;
};
export const withUndefinedAsNull = <T>(value: T | null) => {
return value === undefined ? null : value;
};
export const isAnyMatch = <T>(value: T, ...values: T[]) => {
return values.includes(value);
};
const fillZero = (value: number) => {
return value >= 10 ? value : `0${value}`;
};
export function formatDate(value: Date): string;
export function formatDate(value: string | number | null | undefined): string | undefined;
export function formatDate(value: string | number | null | undefined | Date) {
if (!value) {
return undefined;
}
if (isNumber(value)) {
const date = value.toString();
const year = date.substring(0, 4);
const month = date.substring(4, 6);
const day = date.substring(6, 8);
return `${year}-${month}-${day}`;
}
const date = value instanceof Date ? value : new Date(value);
const year = date.getFullYear();
const month = fillZero(date.getMonth() + 1);
const day = fillZero(date.getDate());
return `${year}-${month}-${day}`;
}
export const formatDateTime = (value: any) => {
const date = value instanceof Date ? value : new Date(value);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
/**
* 当 value 为 0, 或者 空字符串 这种时, 其实就不需要传过去了, 返回 undefined 即可
*/
export const optional = <T>(value: T | null | undefined) => {
return (([null, undefined, ''] as any[]).includes(value) ? undefined : value) as T | undefined;
};
export const createIdWorker = () => {
let id = 0;
return {
nextId: () => {
return id++;
},
};
};
/**
*
* 一键复制
*/
export const copyText = async (value: string) => {
if (window.navigator.clipboard) {
return window.navigator.clipboard.writeText(value);
}
return new Promise<void>((resolve, reject) => {
try {
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = value;
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
resolve();
} catch (error) {
reject(error);
}
});
};
export interface JsonToExcelOptions {
headers?: Record<string, string>;
data: any[];
filename: string;
bookType?: xlsx.BookType;
}
export const jsonToExcel = ({ headers, data, filename, bookType = 'xlsx' }: JsonToExcelOptions) => {
return new Promise<string>((resolve, reject) => {
if (headers) {
data = data.map((row) => {
const object: Record<string, string | number> = {};
for (const key in row) {
if (headers[key]) {
object[headers[key]] = row[key];
} else {
object[key] = row[key];
}
}
return object;
});
}
// 1. 创建一个工作簿 workbook
const workBook = xlsx.utils.book_new();
// 2. 创建工作表 worksheet
const workSheet = xlsx.utils.json_to_sheet(data);
// 3. 将工作表放入工作簿中
xlsx.utils.book_append_sheet(workBook, workSheet);
// 4. 生成数据保存
const blobUrl = xlsx.writeFile(workBook, filename, { bookType });
resolve(blobUrl);
});
};
const MONTH_DAYS: Record<number, number[]> = {
1: [31],
2: [28, 29],
3: [31],
4: [30],
5: [31],
6: [30],
7: [31],
8: [31],
9: [30],
10: [31],
11: [30],
12: [31],
};
/**
* 判断是闰年还是平年
* @param year 年份
* @returns
*/
export const isLeapYear = (year: number) => {
return (year % 4 === 0 && !(year % 100 === 0)) || year % 400 === 0;
};
export const getMonthDays = (date = new Date()) => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const monthDays = MONTH_DAYS[month];
return isLeapYear(year) && month === 2 ? monthDays[1] : monthDays[0];
};
export class TimeRanges {
static today() {
const now = new Date();
return [now, now];
}
static yesterday() {
const now = new Date();
now.setDate(now.getDate() - 1);
return [now, now];
}
static lastSevenDays() {
const now = new Date();
const start = new Date();
start.setDate(now.getDate() - 6);
return [start, now];
}
static currentMonth() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const start = new Date(year, month, 1);
const end = new Date(year, month, getMonthDays(now));
return [start, end];
}
static lastMonth() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const start = new Date(year, month - 1, 1);
const end = new Date(year, month - 1, getMonthDays(start));
return [start, end];
}
}
export const throttle = <T>(func: (...args: any[]) => void, delay: number) => {
let timer = -1;
let isFirstCall = true;
return function (this: T, ...args: any[]) {
if (isFirstCall) {
func.apply(this, args);
isFirstCall = false;
return;
}
if (timer === -1) {
return;
}
timer = window.setTimeout(() => {
func.apply(this, args);
timer = -1;
}, delay);
};
};