Files
cody-crm/frontend/packages/lib-shared/method/index.ts
wangziqi 72e2110987
Some checks failed
Synchronize to Gitee / repo-sync (push) Has been cancelled
Typos Checking / Spell Check with Typos (push) Has been cancelled
chore: initial commit
2026-06-23 11:56:23 +08:00

738 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import JSEncrypt from 'jsencrypt';
import { isObject } from './is';
import { CHINA_PCD, COUNTRIES_TREE } from '@cordys/web/src/components/business/crm-city-select/config';
import type {
FormCreateField,
FormCreateFieldDateType,
} from '@cordys/web/src/components/business/crm-form-create/types';
import { getLocalStorage } from '@lib/shared/method/local-storage';
import industryOptions from '@cordys/web/src/components/pure/crm-industry-select/config';
/**
* 递归深度合并
* @param src 源对象
* @param target 待合并的目标对象
* @returns 合并后的对象
*/
export const deepMerge = <T = any>(src: any = {}, target: any = {}): T => {
Object.keys(target).forEach((key) => {
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
});
return src;
};
/**
* 遍历对象属性并一一添加到 url 地址参数上
* @param baseUrl 需要添加参数的 url
* @param obj 参数对象
* @returns 拼接后的 url
*/
export function setObjToUrlParams(baseUrl: string, obj: any): string {
let parameters = '';
Object.keys(obj).forEach((key) => {
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
});
parameters = parameters.replace(/&$/, '');
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
}
/**
* 加密
* @param input 输入的字符串
* @param publicKey 公钥
* @returns
*/
export function encrypted(input: string) {
const publicKey = getLocalStorage('publicKey') || '';
const encrypt = new JSEncrypt({ default_key_size: '1024' });
encrypt.setPublicKey(publicKey);
return encrypt.encrypt(input);
}
/**
* 休眠
* @param ms 睡眠时长,单位毫秒
* @returns
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
}
export function getQueryVariable(variable: string) {
const urlString = window.location.href;
const queryIndex = urlString.indexOf('?');
if (queryIndex !== -1) {
// 先获取?到#之间的内容,如果没有#则获取到结尾
const hashIndex = urlString.indexOf('#');
const queryEnd = hashIndex !== -1 ? hashIndex : urlString.length;
const query = urlString.substring(queryIndex + 1, queryEnd);
// 分割查询参数
const params = query.split('&');
// 遍历参数,找到 _token 参数的值
let variableValue;
params.forEach((param) => {
const equalIndex = param.indexOf('=');
const variableName = param.substring(0, equalIndex);
if (variableName === variable) {
variableValue = param.substring(equalIndex + 1);
}
});
return variableValue;
}
}
export function getUrlParameterWidthRegExp(name: string) {
const url = window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
/**
* 建立 SSE 连接
* @param url 连接地址
* @param host 连接主机
* @returns EventSource 实例
*/
export const apiSSE = (url: string, host?: string): EventSource => {
let protocol = 'http://';
// 判断是否使用 HTTPS
if (!host?.startsWith('http') && (window.location.protocol === 'https:' || host?.startsWith('https'))) {
protocol = 'https://';
}
// 解析 URL自动适配 host
const uri = protocol + (host?.split('://')[1] || window.location.host) + url;
return new EventSource(uri, {
withCredentials: true,
});
};
/**
* 获取 SSE 连接
* @param sseUrl自定义 SSE 地址
* @param host 自定义主机
* @returns EventSource 实例
*/
export function getSSE(sseUrl: string, params: Record<string, string>, host?: string): EventSource {
const queryString = new URLSearchParams(params).toString();
return apiSSE(`${sseUrl}?${queryString}`, host);
}
export interface TreeNode<T> {
children?: TreeNode<T>[];
[key: string]: any;
}
/**
* 递归遍历树形数组或树
* @param tree 树形数组或树
* @param customNodeFn 自定义节点函数
* @param customChildrenKey 自定义子节点的key
* @param continueCondition 继续递归的条件,某些情况下需要无需递归某些节点的子孙节点,可传入该条件
*/
export function traverseTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
customNodeFn: (node: TreeNode<T>) => void,
continueCondition?: (node: TreeNode<T>) => boolean,
customChildrenKey = 'children'
) {
if (!Array.isArray(tree)) {
tree = [tree];
}
for (let i = 0; i < tree.length; i++) {
const node = (tree as TreeNode<T>[])[i];
if (typeof customNodeFn === 'function') {
customNodeFn(node);
}
if (node[customChildrenKey] && Array.isArray(node[customChildrenKey]) && node[customChildrenKey].length > 0) {
if (typeof continueCondition === 'function' && !continueCondition(node)) {
// 如果有继续递归的条件,则判断是否继续递归
break;
}
traverseTree(node[customChildrenKey], customNodeFn, continueCondition, customChildrenKey);
}
}
}
/**
* 生成 id 序列号
* @returns
*/
let lastTimestamp = 0;
let sequence = 0;
export const getGenerateId = () => {
let timestamp = new Date().getTime();
if (timestamp === lastTimestamp) {
sequence++;
if (sequence >= 100000) {
// 如果超过999则重置为0等待下一秒
sequence = 0;
while (timestamp <= lastTimestamp) {
timestamp = new Date().getTime();
}
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return timestamp.toString() + sequence.toString().padStart(5, '0');
};
/**
* 删除树形数组中的某个节点
* @param treeArr 目标树
* @param targetKey 目标节点唯一值
*/
export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string | number, customKey = 'key'): void {
function deleteNodeInTree(tree: TreeNode<T>[]): void {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node[customKey] === targetKey) {
tree.splice(i, 1); // 直接删除当前节点
// 重新调整剩余子节点的 sort 序号
for (let j = i; j < tree.length; j++) {
tree[j].sort = j + 1;
}
return;
}
if (Array.isArray(node.children)) {
deleteNodeInTree(node.children); // 递归删除子节点
}
}
}
deleteNodeInTree(treeArr);
}
/**
* 递归遍历树形数组或树,返回新的树
* @param tree 树形数组或树
* @param customNodeFn 自定义节点函数
* @param customChildrenKey 自定义子节点的key
* @param parent 父节点
* @param parentPath 父节点路径
* @param level 节点层级
* @returns 遍历后的树形数组
*/
export function mapTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
customNodeFn: (node: TreeNode<T>, path: string, _level: number) => TreeNode<T> | null = (node) => node,
customChildrenKey = 'children',
parentPath = '',
level = 0,
parent: TreeNode<T> | null = null
): T[] {
let cloneTree = cloneDeep(tree);
if (!Array.isArray(cloneTree)) {
cloneTree = [cloneTree];
}
function mapFunc(
_tree: TreeNode<T> | TreeNode<T>[] | T | T[],
_parentPath = '',
_level = 0,
_parent: TreeNode<T> | null = null
): T[] {
if (!Array.isArray(_tree)) {
_tree = [_tree];
}
return _tree
.map((node: TreeNode<T>, i: number) => {
const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
node.sort = i + 1; // sort 从 1 开始
node.parent = _parent || undefined; // 没有父节点说明是树的第一层
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath, _level) : node;
if (newNode) {
newNode.level = _level;
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
newNode[customChildrenKey] = mapFunc(newNode[customChildrenKey], fullPath, _level + 1, newNode);
}
}
return newNode;
})
.filter((node: TreeNode<T> | null) => node !== null);
}
return mapFunc(cloneTree, parentPath, level, parent);
}
/**
* 获取树形数据所有有子节点的父节点
* @param treeData 树形数组
* @param childrenKey 自定义子节点的key
* @returns 遍历后父节点数组
*/
export function getAllParentNodeIds<T>(
treeData: TreeNode<T>[],
childrenKey = 'children',
customKey = 'id'
): Array<string | number> {
const parentIds: Array<string | number> = [];
const traverse = (nodes: TreeNode<T>) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node[childrenKey] && node[childrenKey].length > 0) {
parentIds.push(node[customKey]); // 记录当前节点的 ID
traverse(node[childrenKey]); // 递归遍历子节点
}
}
};
traverse(treeData); // 开始递归
return parentIds;
}
/**
* 过滤树形数组或树
* @param tree 树形数组或树
* @param customNodeFn 自定义节点函数
* @param customChildrenKey 自定义子节点的key
* @returns 遍历后的树形数组
*/
export function filterTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
filterFn: (node: TreeNode<T>, nodeIndex: number, parent?: TreeNode<T> | null) => boolean,
customChildrenKey = 'children',
parentNode: TreeNode<T> | null = null
): TreeNode<T>[] {
if (!Array.isArray(tree)) {
tree = [tree];
}
const filteredTree: TreeNode<T>[] = [];
for (let i = 0; i < tree.length; i++) {
const node = (tree as TreeNode<T>[])[i];
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
if (filterFn(node, i, parentNode)) {
const newNode = cloneDeep({ ...node, [customChildrenKey]: [] });
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey, node);
} else {
newNode[customChildrenKey] = [];
}
filteredTree.push(newNode);
}
}
return filteredTree;
}
/**
*
* 返回文件的大小
* @param fileSize file文件的大小size
* @returns
*/
export function formatFileSize(fileSize: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = fileSize;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
const unit = units[unitIndex];
if (size) {
const formattedSize = size.toFixed(2);
return `${formattedSize} ${unit}`;
}
const formattedSize = 0;
return `${formattedSize} ${unit}`;
}
/**
* 字符串脱敏
* @param str 需要脱敏的字符串
* @returns 脱敏后的字符串
*/
export function desensitize(str: string): string {
if (!str || typeof str !== 'string') {
return '';
}
return str.replace(/./g, '*');
}
/**
* 对话框标题动态内容字符限制
* @param str 标题的动态内容
* @returns 转化后的字符串
*/
export function characterLimit(str?: string, length?: number): string {
if (!str) return '';
const limit = length ?? 20;
if (str.length <= limit) return str;
return `${str.slice(0, limit - 3)}...`;
}
/**
* 根据属性 key 查找树形数组中匹配的某个节点
* @param trees 属性数组
* @param targetKey 需要匹配的属性值
* @param customKey 默认为 key可自定义需要匹配的属性名
* @returns 匹配的节点/null
*/
export function findNodeByKey<T>(
trees: TreeNode<T>[],
targetKey: string | number,
customKey = 'key',
dataKey: string | undefined = undefined
): TreeNode<T> | T | null {
for (let i = 0; i < trees.length; i++) {
const node = trees[i];
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
return node; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
}
if (Array.isArray(node.children) && node.children.length > 0) {
const _node = findNodeByKey(node.children, targetKey, customKey, dataKey); // 递归在子节点中查找
if (_node) {
return _node; // 如果在子节点中找到了匹配的节点,则返回该节点
}
}
}
return null; // 如果在整个树形数组中都没有找到匹配的节点,则返回 null
}
/**
* 根据 key 遍历树,并返回找到的节点路径和节点
*/
export function findNodePathByKey<T>(
tree: TreeNode<T>[],
targetKey: string,
dataKey?: string,
customKey = 'key'
): TreeNode<T> | null {
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
return { ...node, treePath: [dataKey ? node[dataKey] : node] }; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
}
if (Array.isArray(node.children) && node.children.length > 0) {
const result = findNodePathByKey(node.children, targetKey, dataKey, customKey); // 递归在子节点中查找
if (result) {
result.treePath.unshift(dataKey ? node[dataKey] : node);
return result; // 如果在子节点中找到了匹配的节点,则返回该节点
}
}
}
return null;
}
/**
* 根据 cityId 返回城市路径
*/
export function getCityPath(cityId: string | null, scope?: string): string {
if (!cityId) return '';
const nodePathObject = findNodePathByKey(scope === 'CN' ? CHINA_PCD.children : [CHINA_PCD, ...COUNTRIES_TREE], cityId, undefined, 'value');
const nodePathName = (nodePathObject?.treePath || []).map((item: any) => item.label);
return nodePathName.length === 1 ? nodePathName[0] : nodePathName.join('/');
}
/**
* 根据 industryId 返回行业路径
*/
export function getIndustryPath(industryId: string | null): string {
if (!industryId) return '';
const nodePathObject = findNodePathByKey(industryOptions, industryId, undefined, 'value');
const nodePathName = (nodePathObject?.treePath || []).map((item: any) => item.label);
return nodePathName.length === 1 ? nodePathName[0] : nodePathName.join('/');
}
/**
* 返回添加节点下一个有效未命名name
* @param existingNames 已存在名称列表
* @param baseName 基础名称
*/
export function getNextAvailableName(existingNames: string[], baseName: string): string {
const baseNamePattern = new RegExp(`^${baseName}(\\d+)$`);
const existingSuffixes = existingNames.reduce((suffixes: number[], name: string) => {
const match = baseNamePattern.exec(name);
if (match) {
suffixes.push(parseInt(match[1], 10));
}
return suffixes;
}, []);
if (existingSuffixes.length === 0) {
return existingNames.includes(baseName) ? `${baseName}1` : baseName;
}
return `${baseName}${Math.max(...existingSuffixes) + 1}`;
}
/**
* 分步处理分数表达式
* @param str 分数表达式
*/
export function safeFractionConvert(str: string | number) {
if (!str) {
return 1;
}
if (typeof str === 'number') {
return str;
}
const parts = str.split('/').map(Number); // 分割分子分母
if (parts.length !== 2 || parts.some((e) => Number.isNaN(e))) return 1;
return parts[0] / parts[1];
}
/**
* 打开网页链接
* @param url 链接地址
*/
export function openDocumentLink(url: string) {
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.rel = 'noopener noreferrer'; // 防止打开页面控制当前页面
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/**
* 格式化时间
* @param value 时间戳
* @param type 类型
*/
export function formatTimeValue(value: string | number, type?: FormCreateFieldDateType) {
if (value) {
const date = dayjs(Number(value));
switch (type) {
case 'month':
return date.format('YYYY-MM');
case 'date':
return date.format('YYYY-MM-DD');
case 'datetime':
default:
return date.format('YYYY-MM-DD HH:mm:ss');
}
}
return '-';
}
/**
* 下载文件
* @param byte 字节流
* @param fileName 文件名
*/
export const downloadByteFile = (byte: BlobPart, fileName: string) => {
// 创建一个Blob对象
const blob = new Blob([byte], { type: 'application/octet-stream' });
// 创建一个URL对象用于生成下载链接
const url = window.URL.createObjectURL(blob);
// 创建一个虚拟的<a>标签来触发下载
const link = document.createElement('a');
link.href = url;
link.download = fileName; // 设置下载文件的名称
document.body.appendChild(link);
link.click();
// 释放URL对象
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
};
/**
* 获取每三位使用逗号隔开数字格式
* @param number 目标值
*/
export function addCommasToNumber(number: number) {
if (number === 0 || number === undefined) {
return '0';
}
// 将数字转换为字符串
const numberStr = number.toString();
// 分割整数部分和小数部分
const parts = numberStr.split('.');
const integerPart = parts[0];
const decimalPart = parts[1] || ''; // 如果没有小数部分,则设为空字符串
// 对整数部分添加逗号分隔
const integerWithCommas = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// 拼接整数部分和小数部分(如果有)
const result = decimalPart ? `${integerWithCommas}.${decimalPart}` : integerWithCommas;
return result;
}
// 是否是企业微信端打开
export function isWeComBrowser() {
const ua = window.navigator.userAgent.toLowerCase();
return ua.includes('wxwork'); // 企业微信 UA 一定包含 wxwork
}
export function isDingTalkBrowser() {
const ua = window.navigator.userAgent.toLowerCase();
return (
ua.includes('dingtalk') ||
ua.includes('aliapp(dingtalk') ||
(getQueryVariable('authCode') !== '' &&
getQueryVariable('authCode') !== undefined &&
getQueryVariable('authCode') !== null)
);
}
// 飞书
export function isLarkBrowser(): boolean {
const ua = window.navigator.userAgent.toLowerCase();
return ua.includes('lark') || ua.includes('feishu') || getQueryVariable('state') === 'LARK';
}
/**
* 国际单位数字缩写
* @param amount 金额数字
* @param decimals 保留小数位数
* @param currency 货币单位
*/
export function abbreviateNumber(count: number | string, currency: string, decimals = 2) {
if (typeof count !== 'number') {
return { value: '-', unit: '', full: '-' };
}
const locale = localStorage.getItem('CRM-locale') || 'zh-CN';
const truncateNumber = (num: number) => {
const factor = 10 ** decimals;
return Math.round(num * factor) / factor;
};
const full = `${count.toLocaleString('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})} (${currency})`;
let value = '';
let unit = '';
if (locale === 'zh-CN') {
if (count >= 1e8) {
value = truncateNumber(count / 1e8).toString();
unit = '亿';
} else if (count >= 1e4) {
value = truncateNumber(count / 1e4).toString();
unit = '万';
} else {
value = truncateNumber(count).toString();
unit = '';
}
} else if (locale === 'en-US') {
if (count >= 1e9) {
value = truncateNumber(count / 1e9).toString();
unit = 'B';
} else if (count >= 1e6) {
value = truncateNumber(count / 1e6).toString();
unit = 'M';
} else if (count >= 1e3) {
value = truncateNumber(count / 1e3).toString();
unit = 'K';
} else {
value = truncateNumber(count).toString();
unit = '';
}
}
return { value, unit, full };
}
export function getFileIconType(type: string) {
switch (type) {
case 'zip':
return 'icona-icon_file-compressed_colorful';
case 'ppt':
return 'iconicon_file-ppt_colorful';
case 'pdf':
return 'iconicon_file-pdf_colorful';
case 'docx':
return 'iconicon_file-word_colorful';
case 'xlsx':
return 'iconicon_file-excel_colorful';
case 'csv':
return 'iconicon_file-CSV_colorful';
case 'xmind':
return 'iconicon_file-xmind_colorful';
case 'sql':
return 'iconicon_file-sql_colorful';
case 'jar':
return 'icona-icon_file-jar_colorful';
case 'json':
return 'icona-icon_file-json';
case 'jmx':
return 'icona-icon_file-JMX';
case 'har':
return 'iconicon_file_har';
case 'mp4':
case 'mov':
case 'wmv':
return 'iconicon_file_video_colorful';
default:
return /(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(type)
? 'iconicon_file-image_colorful'
: 'iconicon_file-unknown_colorful1';
}
}
/**
* 限制字符串长度并添加后缀(如 copy保证总长度不超过 maxLen
* @param name 原名称
* @param suffix 后缀(默认 "copy"
* @param maxLen 最大长度(默认 255
*/
export function getCopiedName(name: string, suffix = 'copy', maxLen = 255): string {
const baseName = name || '';
if (baseName.length + suffix.length > maxLen) {
return baseName.slice(0, maxLen - suffix.length) + suffix;
}
return baseName + suffix;
}
/**
* 通用数字千分位展示
* @param value 数值或数值字符串
* @param options 小数位配置
* @returns 格式化后的字符串
*/
export function formatThousands(
value: string | number | null | undefined,
options?: {
minimumFractionDigits?: number;
maximumFractionDigits?: number;
}
) {
if (value === null || value === undefined || value === '') {
return '';
}
const numberValue = Number(value);
if (!Number.isFinite(numberValue)) {
return String(value);
}
const decimalLength = String(value).includes('.') ? String(value).split('.')[1]?.length || 0 : 0;
return numberValue.toLocaleString('en-US', {
minimumFractionDigits: options?.minimumFractionDigits,
maximumFractionDigits: options?.maximumFractionDigits ?? decimalLength,
});
}