项目中遇到的工具类装

100 阅读3分钟
   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);
    };
};