chore: initial commit
Some checks failed
Synchronize to Gitee / repo-sync (push) Has been cancelled
Typos Checking / Spell Check with Typos (push) Has been cancelled

This commit is contained in:
2026-06-23 11:56:23 +08:00
commit 72e2110987
2883 changed files with 367388 additions and 0 deletions

View File

@@ -0,0 +1,737 @@
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,
});
}