diff --git a/src/utils/tool.js b/src/utils/tool.js index 889abb9..f2e9f01 100644 --- a/src/utils/tool.js +++ b/src/utils/tool.js @@ -1,180 +1,499 @@ -/** - * 工具类 +/* + * @Descripttion: 工具集 + * @version: 2.0 + * @LastEditors: sakuya + * @LastEditTime: 2026年1月15日 */ -const tool = { - /** - * 本地存储操作 - */ - data: { - /** - * 设置本地存储 - * @param {string} key - 键名 - * @param {*} value - 值 - */ - set(key, value) { - if (typeof value === 'object') { - localStorage.setItem(key, JSON.stringify(value)) - } else { - localStorage.setItem(key, value) + +import CryptoJS from "crypto-js"; +import sysConfig from "@/config"; + +const tool = {}; + +/** + * 检查是否为有效的值(非null、非undefined、非空字符串、非空数组、非空对象) + * @param {*} value - 要检查的值 + * @returns {boolean} + */ +tool.isValid = function (value) { + if (value === null || value === undefined) { + return false; + } + if (typeof value === "string" && value.trim() === "") { + return false; + } + if (Array.isArray(value) && value.length === 0) { + return false; + } + if (typeof value === "object" && Object.keys(value).length === 0) { + return false; + } + return true; +}; + +/** + * 防抖函数 + * @param {Function} func - 要执行的函数 + * @param {number} wait - 等待时间(毫秒) + * @param {boolean} immediate - 是否立即执行 + * @returns {Function} + */ +tool.debounce = function (func, wait = 300, immediate = false) { + let timeout; + return function (...args) { + const context = this; + clearTimeout(timeout); + if (immediate && !timeout) { + func.apply(context, args); + } + timeout = setTimeout(() => { + timeout = null; + if (!immediate) { + func.apply(context, args); } - }, + }, wait); + }; +}; - /** - * 获取本地存储 - * @param {string} key - 键名 - * @param {*} defaultValue - 默认值 - * @returns {*} - */ - get(key, defaultValue = null) { - const value = localStorage.getItem(key) - if (!value) { - return defaultValue +/** + * 节流函数 + * @param {Function} func - 要执行的函数 + * @param {number} wait - 等待时间(毫秒) + * @param {Object} options - 配置选项 { leading: boolean, trailing: boolean } + * @returns {Function} + */ +tool.throttle = function (func, wait = 300, options = {}) { + let timeout; + let previous = 0; + const { leading = true, trailing = true } = options; + + return function (...args) { + const context = this; + const now = Date.now(); + + if (!previous && !leading) { + previous = now; + } + + const remaining = wait - (now - previous); + + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; } - try { - return JSON.parse(value) - } catch (e) { - return value - } - }, - - /** - * 删除本地存储 - * @param {string} key - 键名 - */ - remove(key) { - localStorage.removeItem(key) - }, - - /** - * 清空本地存储 - */ - clear() { - localStorage.clear() - } - }, - - /** - * 树形结构转列表 - * @param {Array} tree - 树形结构数据 - * @param {string} childrenKey - 子节点键名 - * @returns {Array} 扁平化后的数组 - */ - tree_to_list(tree, childrenKey = 'children') { - if (!tree || !Array.isArray(tree)) { - return [] - } - - const result = [] - - const traverse = (nodes) => { - if (!nodes || !Array.isArray(nodes)) { - return - } - nodes.forEach((node) => { - result.push(node) - if (node[childrenKey] && node[childrenKey].length > 0) { - traverse(node[childrenKey]) - } - }) - } - - traverse(tree) - return result - }, - - /** - * 列表转树形结构 - * @param {Array} list - 列表数据 - * @param {string} idKey - ID键名 - * @param {string} parentIdKey - 父ID键名 - * @param {string} childrenKey - 子节点键名 - * @returns {Array} 树形结构数据 - */ - list_to_tree(list, idKey = 'id', parentIdKey = 'parentId', childrenKey = 'children') { - if (!list || !Array.isArray(list)) { - return [] - } - - const map = {} - const roots = [] - - // 创建映射表 - list.forEach((item) => { - map[item[idKey]] = { ...item, [childrenKey]: [] } - }) - - // 构建树形结构 - list.forEach((item) => { - const node = map[item[idKey]] - const parentId = item[parentIdKey] - - if (parentId && map[parentId]) { - map[parentId][childrenKey].push(node) - } else { - roots.push(node) - } - }) - - return roots - }, - - /** - * 深拷贝 - * @param {*} obj - 要拷贝的对象 - * @returns {*} 拷贝后的对象 - */ - deepClone(obj) { - if (obj === null || typeof obj !== 'object') { - return obj - } - - if (Array.isArray(obj)) { - return obj.map((item) => this.deepClone(item)) - } - - const cloned = {} - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - cloned[key] = this.deepClone(obj[key]) - } - } - - return cloned - }, - - /** - * 防抖函数 - * @param {Function} func - 要防抖的函数 - * @param {number} wait - 等待时间 - * @returns {Function} - */ - debounce(func, wait = 300) { - let timeout - return function (...args) { - clearTimeout(timeout) + previous = now; + func.apply(context, args); + } else if (!timeout && trailing) { timeout = setTimeout(() => { - func.apply(this, args) - }, wait) + previous = leading ? Date.now() : 0; + timeout = null; + func.apply(context, args); + }, remaining); } - }, + }; +}; - /** - * 节流函数 - * @param {Function} func - 要节流的函数 - * @param {number} wait - 等待时间 - * @returns {Function} - */ - throttle(func, wait = 300) { - let timeout - return function (...args) { - if (!timeout) { - timeout = setTimeout(() => { - func.apply(this, args) - timeout = null - }, wait) - } +/** + * 深拷贝对象(支持循环引用) + * @param {*} obj - 要拷贝的对象 + * @param {WeakMap} hash - 用于检测循环引用 + * @returns {*} + */ +tool.deepClone = function (obj, hash = new WeakMap()) { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (hash.has(obj)) { + return hash.get(obj); + } + + const clone = Array.isArray(obj) ? [] : {}; + hash.set(obj, clone); + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + clone[key] = tool.deepClone(obj[key], hash); } } -} -export default tool + return clone; +}; + +/* localStorage */ +tool.data = { + set(key, data, datetime = 0) { + //加密 + if (sysConfig.LS_ENCRYPTION == "AES") { + data = tool.crypto.AES.encrypt( + JSON.stringify(data), + sysConfig.LS_ENCRYPTION_key, + ); + } + let cacheValue = { + content: data, + datetime: + parseInt(datetime) === 0 + ? 0 + : new Date().getTime() + parseInt(datetime) * 1000, + }; + return localStorage.setItem(key, JSON.stringify(cacheValue)); + }, + get(key) { + try { + const value = JSON.parse(localStorage.getItem(key)); + if (value) { + let nowTime = new Date().getTime(); + if (nowTime > value.datetime && value.datetime != 0) { + localStorage.removeItem(key); + return null; + } + //解密 + if (sysConfig.LS_ENCRYPTION == "AES") { + value.content = JSON.parse( + tool.crypto.AES.decrypt( + value.content, + sysConfig.LS_ENCRYPTION_key, + ), + ); + } + return value.content; + } + return null; + } catch (err) { + return null; + } + }, + remove(key) { + return localStorage.removeItem(key); + }, + clear() { + return localStorage.clear(); + }, +}; + +/*sessionStorage*/ +tool.session = { + set(table, settings) { + const _set = JSON.stringify(settings); + return sessionStorage.setItem(table, _set); + }, + get(table) { + const data = sessionStorage.getItem(table); + try { + return JSON.parse(data); + } catch (err) { + return null; + } + }, + remove(table) { + return sessionStorage.removeItem(table); + }, + clear() { + return sessionStorage.clear(); + }, +}; + +/*cookie*/ +tool.cookie = { + /** + * 设置cookie + * @param {string} name - cookie名称 + * @param {string} value - cookie值 + * @param {Object} config - 配置选项 + */ + set(name, value, config = {}) { + const cfg = { + expires: null, + path: null, + domain: null, + secure: false, + httpOnly: false, + sameSite: "Lax", + ...config, + }; + let cookieStr = `${name}=${encodeURIComponent(value)}`; + if (cfg.expires) { + const exp = new Date(); + exp.setTime(exp.getTime() + parseInt(cfg.expires) * 1000); + cookieStr += `;expires=${exp.toUTCString()}`; + } + if (cfg.path) { + cookieStr += `;path=${cfg.path}`; + } + if (cfg.domain) { + cookieStr += `;domain=${cfg.domain}`; + } + if (cfg.secure) { + cookieStr += `;secure`; + } + if (cfg.sameSite) { + cookieStr += `;SameSite=${cfg.sameSite}`; + } + document.cookie = cookieStr; + }, + /** + * 获取cookie + * @param {string} name - cookie名称 + * @returns {string|null} + */ + get(name) { + const arr = document.cookie.match( + new RegExp("(^| )" + name + "=([^;]*)(;|$)"), + ); + if (arr != null) { + return decodeURIComponent(arr[2]); + } + return null; + }, + /** + * 删除cookie + * @param {string} name - cookie名称 + */ + remove(name) { + const exp = new Date(); + exp.setTime(exp.getTime() - 1); + document.cookie = `${name}=;expires=${exp.toUTCString()}`; + }, +}; + +/* Fullscreen */ +/** + * 切换全屏状态 + * @param {HTMLElement} element - 要全屏的元素 + */ +tool.screen = function (element) { + const isFull = !!( + document.webkitIsFullScreen || + document.mozFullScreen || + document.msFullscreenElement || + document.fullscreenElement + ); + if (isFull) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } else { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(); + } + } +}; + +/* 复制对象(浅拷贝) */ +/** + * 浅拷贝对象 + * @param {*} obj - 要拷贝的对象 + * @returns {*} - 拷贝后的对象 + */ +tool.objCopy = function (obj) { + if (obj === null || typeof obj !== "object") { + return obj; + } + return JSON.parse(JSON.stringify(obj)); +}; + +/* 日期格式化 */ +/** + * 格式化日期 + * @param {Date|string|number} date - 日期对象、时间戳或日期字符串 + * @param {string} fmt - 格式化字符串,默认 "yyyy-MM-dd hh:mm:ss" + * @returns {string} - 格式化后的日期字符串 + */ +tool.dateFormat = function (date, fmt = "yyyy-MM-dd hh:mm:ss") { + if (!date) return ""; + const dateObj = new Date(date); + if (isNaN(dateObj.getTime())) return ""; + + const o = { + "M+": dateObj.getMonth() + 1, // 月份 + "d+": dateObj.getDate(), // 日 + "h+": dateObj.getHours(), // 小时 + "m+": dateObj.getMinutes(), // 分 + "s+": dateObj.getSeconds(), // 秒 + "q+": Math.floor((dateObj.getMonth() + 3) / 3), // 季度 + S: dateObj.getMilliseconds(), // 毫秒 + }; + if (/(y+)/.test(fmt)) { + fmt = fmt.replace( + RegExp.$1, + (dateObj.getFullYear() + "").substr(4 - RegExp.$1.length), + ); + } + for (const k in o) { + if (new RegExp("(" + k + ")").test(fmt)) { + fmt = fmt.replace( + RegExp.$1, + RegExp.$1.length == 1 + ? o[k] + : ("00" + o[k]).substr(("" + o[k]).length), + ); + } + } + return fmt; +}; + +/* 千分符 */ +/** + * 格式化数字,添加千分位分隔符 + * @param {number|string} num - 要格式化的数字 + * @param {number} decimals - 保留小数位数,默认为0 + * @returns {string} - 格式化后的字符串 + */ +tool.groupSeparator = function (num, decimals = 0) { + if (num === null || num === undefined || num === "") return ""; + const numStr = Number(num).toFixed(decimals); + const parts = numStr.split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); +}; + +/* 常用加解密 */ +tool.crypto = { + //MD5加密 + MD5(data) { + return CryptoJS.MD5(data).toString(); + }, + //BASE64加解密 + BASE64: { + encrypt(data) { + return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)); + }, + decrypt(cipher) { + return CryptoJS.enc.Base64.parse(cipher).toString( + CryptoJS.enc.Utf8, + ); + }, + }, + //AES加解密 + AES: { + encrypt(data, secretKey, config = {}) { + if (secretKey.length % 8 != 0) { + console.warn( + "[SCUI error]: 秘钥长度需为8的倍数,否则解密将会失败。", + ); + } + const result = CryptoJS.AES.encrypt( + data, + CryptoJS.enc.Utf8.parse(secretKey), + { + iv: CryptoJS.enc.Utf8.parse(config.iv || ""), + mode: CryptoJS.mode[config.mode || "ECB"], + padding: CryptoJS.pad[config.padding || "Pkcs7"], + }, + ); + return result.toString(); + }, + decrypt(cipher, secretKey, config = {}) { + const result = CryptoJS.AES.decrypt( + cipher, + CryptoJS.enc.Utf8.parse(secretKey), + { + iv: CryptoJS.enc.Utf8.parse(config.iv || ""), + mode: CryptoJS.mode[config.mode || "ECB"], + padding: CryptoJS.pad[config.padding || "Pkcs7"], + }, + ); + return CryptoJS.enc.Utf8.stringify(result); + }, + }, +}; + +/* 树形数据转扁平数组 */ +/** + * 将树形结构转换为扁平数组 + * @param {Array} tree - 树形数组 + * @param {Object} config - 配置项 { children: "children" } + * @returns {Array} - 扁平化后的数组 + */ +tool.treeToList = function (tree, config = { children: "children" }) { + const result = []; + tree.forEach((item) => { + const tmp = { ...item }; + const childrenKey = config.children || "children"; + + if (tmp[childrenKey] && tmp[childrenKey].length > 0) { + result.push({ ...item }); + const childrenRoutes = tool.treeToList(tmp[childrenKey], config); + result.push(...childrenRoutes); + } else { + result.push(tmp); + } + }); + return result; +}; + +/* 获取父节点数据(保留原有函数名) */ +/** + * 根据ID获取父节点数据 + * @param {Array} list - 数据列表 + * @param {number|string} targetId - 目标ID + * @param {Object} config - 配置项 { pid: "parent_id", idField: "id", field: [] } + * @returns {*} - 父节点数据或指定字段 + */ +tool.get_parents = function ( + list, + targetId = 0, + config = { pid: "parent_id", idField: "id", field: [] }, +) { + let res = null; + list.forEach((item) => { + if (item[config.idField || "id"] === targetId) { + if (config.field && config.field.length > 1) { + res = {}; + config.field.forEach((field) => { + res[field] = item[field]; + }); + } else if (config.field && config.field.length === 1) { + res = item[config.field[0]]; + } else { + res = item; + } + } + }); + return res; +}; + +/* 获取数据字段 */ +/** + * 从数据对象中提取指定字段 + * @param {Object} data - 数据对象 + * @param {Array} fields - 字段名数组 + * @returns {*} - 提取的字段数据 + */ +tool.getDataField = function (data, fields = []) { + if (!data || typeof data !== "object") { + return data; + } + if (fields.length === 0) { + return data; + } + if (fields.length === 1) { + return data[fields[0]]; + } else { + const result = {}; + fields.forEach((field) => { + result[field] = data[field]; + }); + return result; + } +}; + +// 兼容旧函数名 +tool.tree_to_list = tool.treeToList; +tool.get_data_field = tool.getDataField; + +export default tool;