/** * toTree 方法参数 */ export interface ToTreeOption { /** 数据 */ data?: T[] | null; /** id 字段名称 */ idField?: string | null; /** parentId 字段名称 */ parentIdField?: string | null; /** 生成的 children 字段名称 */ childrenField?: string | null; /** 最顶级的 parentId 值 */ parentId?: number | string | (number | string)[] | null; /** 是否添加包含所有父级 id 的字段 */ addParentIds?: boolean | null; /** 包含所有父级 id 字段的名称 */ parentIdsField?: string | null; /** 所有父级的 id */ parentIds?: (number | string)[] | null; } /** * parentId 形式数据转 children 形式 * @param option ToTreeOption */ export function toTree(option: ToTreeOption): T[] { const data = option.data; const idField = option.idField || 'id'; const parentIdField = option.parentIdField || 'parentId'; const childrenField = option.childrenField || 'children'; const parentIdIsNull = option.parentId == null; const parentId = parentIdIsNull ? [] : option.parentId; const parentIdIsArray = Array.isArray(parentId); const addParentIds = option.addParentIds; const parentIdsField = option.parentIdsField || 'parentIds'; const parentIds = option.parentIds ?? []; if (data == null) { return []; } if (parentIdIsNull) { data.forEach((d) => { if (!data.some((t) => d[parentIdField] == t[idField]) && !(parentId as unknown[]).includes(d[parentIdField])) { (parentId as unknown[]).push(d[parentIdField]); } }); } const result: T[] = []; data.forEach((d) => { if (d[idField] == d[parentIdField]) { const error = { [idField]: d[idField], [parentIdField]: d[parentIdField], data: d, }; console.error('data error:', error); throw new Error('data error'); } if (parentIdIsArray ? parentId.includes(d[parentIdField]) : d[parentIdField] == parentId) { const t = { ...d }; const children = toTree({ data, idField, parentIdField, childrenField, parentId: d[idField], addParentIds, parentIdsField, parentIds: [...parentIds, d[idField]], }); if (children.length > 0) { t[childrenField] = children; } if (addParentIds) { t[parentIdsField] = parentIds; } result.push(t); } }); return result; } /** * 遍历 children 形式数据 * @param data 数据 * @param callback 回调 * @param childrenField children 字段名 * @param parent 当前的父级 */ export function eachTree(data?: T[], callback?: (item: T, index: number, parent?: T) => void | boolean, childrenField = 'children', parent?: T) { if (!data) { return; } for (let i = 0; i < data.length; i++) { const flag = callback ? callback(data[i], i, parent) : void 0; if (flag === false) { return false; } if (data[i][childrenField]?.length) { if (eachTree(data[i][childrenField], callback, childrenField, data[i]) === false) { return false; } } } } /** * 处理每一项数据 * @param item 当前的数据 * @param index 当前的索引 * @param parent 当前的父级 * @param formatter 格式器 * @param childrenField children 字段名 * @param resultChildrenField 返回后的 children 字段名 * @param afterFormatter 后置格式器 */ function formatTreeItem( item: T, index: number, parent: T | K | undefined, formatter: any, childrenField = 'children', resultChildrenField = 'children', afterFormatter?: (item: K) => K | void | undefined ) { const result: K[] = []; const itemResult = formatter(item, index, parent); if (itemResult === 'flatChildren') { const children: T[] | undefined = item[childrenField]; if (children) { children.forEach((c, j) => { const childItems = formatTreeItem(c, j, item, formatter, childrenField, resultChildrenField, afterFormatter); childItems.forEach((childItem) => { const afterItem = afterFormatter ? afterFormatter(childItem) : childItem; if (afterItem) { result.push(afterItem); } }); }); } } else if (itemResult) { if (item[childrenField] != null) { itemResult[resultChildrenField] = mapTree(item[childrenField], formatter, childrenField, resultChildrenField, afterFormatter, itemResult); } const afterItem = afterFormatter ? afterFormatter(itemResult) : itemResult; if (afterItem) { result.push(afterItem); } } return result; } /** * 处理 children 形式数据 * @param data 数据 * @param formatter 格式器 * @param childrenField children 字段名 * @param resultChildrenField 返回后的 children 字段名 * @param afterFormatter 后置格式器 * @param parent 当前的父级 */ export function mapTree( data: T[] | undefined | null, formatter: (item: T, index?: number, parent?: T | K) => K | void | undefined | 'flatChildren', childrenField = 'children', resultChildrenField = 'children', afterFormatter?: (item: K) => K | void | undefined, parent?: T | K ): K[] { const result: K[] = []; if (data) { data.forEach((d, i) => { const items = formatTreeItem(d, i, parent, formatter, childrenField, resultChildrenField, afterFormatter); items.forEach((item) => { result.push(item); }); }); } return result; } /** * 查找 children 形式数据 * @param data 数据 * @param predicate 查找条件 * @param childrenField children 字段名 */ export function findTree(data: T[] | undefined, predicate: (value: T, index: number) => unknown, childrenField?: string): T | undefined { let temp: T | undefined; eachTree( data, (d, i) => { if (predicate(d, i)) { temp = d; return false; } }, childrenField ); return temp; } /** * 检查是否全屏 */ export function checkFullscreen(): boolean { return !!( document.fullscreenElement || (document as any).webkitFullscreenElement || (document as any).mozFullScreenElement || (document as any).msFullscreenElement ); } /** * 退出全屏 */ export function exitFullscreen() { const func = document.exitFullscreen || (document as any).exitFullScreen || (document as any).webkitCancelFullScreen || (document as any).mozCancelFullScreen || (document as any).msExitFullscreen; func && func.call(document); } /** * 全屏 * @param el HTMLElement */ export function requestFullscreen(el?: HTMLElement) { if (el == null) { el = document.documentElement; } const func = el.requestFullscreen || (el as any).requestFullScreen || (el as any).webkitRequestFullScreen || (el as any).mozRequestFullScreen || (el as any).msRequestFullScreen; if (!func) { throw new Error('您的浏览器不支持全屏模式'); } func.call(el); } /** * 经纬度坐标 */ export interface Point { /** 经度 */ lng: number; /** 纬度 */ lat: number; } /** * 百度地图坐标转高德地图坐标 * @param point 坐标 */ export function bd09ToGcj02(point: Point): Point { const x_pi = (3.141592653589793 * 3000.0) / 180.0; const x = point.lng - 0.0065; const y = point.lat - 0.006; const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi); const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi); return { lng: z * Math.cos(theta), lat: z * Math.sin(theta), }; } /** * 高德地图坐标转百度地图坐标 * @param point 坐标 */ export function gcj02ToBd09(point: Point): Point { const x_pi = (3.141592653589793 * 3000.0) / 180.0; const x = point.lng; const y = point.lat; const z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * x_pi); const theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * x_pi); return { lng: z * Math.cos(theta) + 0.0065, lat: z * Math.sin(theta) + 0.006, }; } /** * 生成 m 到 n 的随机数 * @param m 最小值(包含) * @param n 最大值(不包含) */ export function random(m: number, n: number): number { return Math.floor(Math.random() * (m - n) + n); } /** * 生成随机字符串 * @param length 长度 * @param radix 基数 */ export function uuid(length = 32, radix?: number): string { const str = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; let result = ''; for (let i = 0; i < length; i++) { result += str.charAt(Math.floor(Math.random() * (radix || str.length))); } return result; } /** * 数字千分位 * @param num 数字 */ export function formatNumber(num?: number | null): string { return String(num ?? '').replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g, '$1,'); } /** * 赋值不改变原字段 * @param target 目标对象 * @param source 源对象 * @param excludes 排除的字段 */ export function assignObject(target: T, source: K, excludes?: string[]) { Object.keys(target).forEach((key) => { if (!excludes?.includes?.(key)) { target[key] = source[key]; } }); return target; } /** * 复制字符串 * @param text 字符串 */ export async function copyText(text: string) { if (typeof navigator?.clipboard?.writeText === 'function') { await navigator.clipboard.writeText(text); return; } const el = document.createElement('textarea'); el.value = text; el.style.position = 'fixed'; el.style.top = '-200px'; el.style.left = '-200px'; el.style.width = '100px'; el.style.height = '100px'; document.body.appendChild(el); el.focus(); el.select(); if (!document.execCommand('copy')) { el.remove(); return Promise.reject(new Error('浏览器不支持复制')); } el.remove(); } /** * 查找直接子元素 * @param parentEl 父元素 * @param className 根据类名查找 * @param attr 根据属性查找 */ export function queryChild(parentEl?: Element, className?: string, attr?: string[]): Element | undefined { return Array.from(parentEl?.children ?? []).find((el) => { if (className && !el.classList.contains(className)) { return false; } if (attr != null && attr[0] != null) { if (el.getAttribute(attr[0]) != attr[1]) { return false; } } return true; }); } /** * 判断是否是外链 * @param url 地址 */ export function isExternalLink(url?: string | null): boolean { return !!(url && (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//'))); } /** * 遍历children形式数据 * @param data 需要遍历的数据 * @param callback 回调 * @param childrenField children字段名 */ export function eachTreeData(data: any[], callback: Function, childrenField = 'children', parent: any = '') { if (data) { data.forEach((d, i) => { if (callback && callback(d, i, parent) !== false && d[childrenField]?.length) { eachTreeData(d[childrenField], callback, childrenField, d); } }); } } /** * 遍历每一项,并可修改节点 * @param data 数据数组 * @param callback 回调,可修改节点 * @param options 配置选项 * - childrenField: children 字段名,默认 'children' * - mutate: 是否直接修改原始对象,默认 false * @returns 返回处理后的数组(根据 mutate 决定是原对象还是新对象) */ export function eachTreeItemData(data: any[], callback: (node: any) => void, options: { childrenField?: string; mutate?: boolean } = {}) { const { childrenField = 'children', mutate = false } = options; const result: any[] = []; data.forEach((item) => { // 决定是克隆还是直接修改原始对象 const node = mutate ? item : { ...item }; // 执行回调,可修改 node callback(node); // 如果有 children,递归处理 if (node[childrenField]?.length) { node[childrenField] = eachTreeItemData(node[childrenField], callback, options); } result.push(node); }); return result; } /** * 获取屏幕宽度 */ export function screenWidth() { return document.documentElement.clientWidth || document.body.clientWidth; }