738 lines
22 KiB
TypeScript
738 lines
22 KiB
TypeScript
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,
|
||
});
|
||
}
|