function copy(obj) { return JSON.parse(JSON.stringify(obj)); } function formatDate(date) { if (isEmpty(date)) { return 'NaN'; } const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const monthStr = ('' + month).padStart(2, 0); const dayStr = ('' + day).padStart(2, 0); const formatted = year + '-' + monthStr + '-' + dayStr; return formatted; } function formatTime(date) { const hours = date.getHours(); const minutes = date.getMinutes(); const hoursStr = ('' + hours).padStart(2, 0); const minutesStr = ('' + minutes).padStart(2, 0); const formatted = hoursStr + ':' + minutesStr; return formatted; } function formatDatetime(date) { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const monthStr = ('' + month).padStart(2, 0); const dayStr = ('' + day).padStart(2, 0); const hours = date.getHours(); const minutes = date.getMinutes(); const hoursStr = ('' + hours).padStart(2, 0); const minutesStr = ('' + minutes).padStart(2, 0); const formatted = year + '-' + monthStr + '-' + dayStr + ' ' + hoursStr + ':' + minutesStr; return formatted; } function camelCase(name) { return name.substr(0, 1).toLowerCase() + name.substr(1); } class UrlBuilder { constructor(url) { this.url = url; this.paramList = []; } append(name, value) { if (isNotEmpty(value)) { this.paramList.push({ name: name, value: value }); } return this; } build() { this.paramList.forEach((e, i, a) => { if (i === 0) { this.url += '?'; } else { this.url += '&'; } this.url += e.name + '=' + e.value; }); return this.url; } } function isNotEmpty(val) { return val !== undefined && val !== null && val !== ''; } function prepareUrl(url) { return new UrlBuilder(url); } function throttle(fn, delay, atleast) { let timeout = null, startTime = new Date(); return function () { let curTime = new Date(); clearTimeout(timeout); if (curTime - startTime >= atleast) { fn(); startTime = curTime; } else { timeout = setTimeout(fn, delay); } }; } function clickUrl(url) { const httpLink = document.createElement('a'); httpLink.href = url; httpLink.target = '_blank'; httpLink.click(); } function escape2Html(str) { var temp = document.createElement('div'); temp.innerHTML = str; var output = temp.innerText || temp.textContent; temp = null; return output; } function formatPrice(price) { return Math.ceil(price).toLocaleString(); } function formatPercent(number) { return Math.round(number * 100) + '%'; } /** * ! 不支持计算 Set 或 Map * @param {*} val * @example * true if: 0, [], {}, null, '', undefined * false if: 'false', 'undefined' */ function isEmpty(val) { // return val === undefined || val === null || val === ""; return [Object, Array].includes((val || {}).constructor) && !Object.entries(val || {}).length; } /** * 数组排序 */ const sortBy = (key) => { return (a, b) => (getNestedValue(a, key) > getNestedValue(b, key) ? 1 : getNestedValue(b, key) > getNestedValue(a, key) ? -1 : 0); }; /** * Object排序keys */ const sortKeys = (obj) => Object.keys(obj) .sort() .reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {}); /** * 数组排序, 给定排序数组 * @param {array} items 需要排序的数组 * @param {array} keyName 排序的key * @param {array} keyOrder 给定排序 * @returns */ const sortArrayByOrder = (items, keyName, keyOrder) => { return items.sort((a, b) => { return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]); }); }; /** * 合并Object, 递归地 */ function merge(...objects) { const isDeep = objects.some((obj) => obj !== null && typeof obj === 'object'); const result = objects[0] || (isDeep ? {} : objects[0]); for (let i = 1; i < objects.length; i++) { const obj = objects[i]; if (!obj) continue; Object.keys(obj).forEach((key) => { const val = obj[key]; if (isDeep) { if (Array.isArray(val)) { result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val); } else if (typeof val === 'object') { result[key] = merge(result[key], val); } else { result[key] = val; } } else { result[key] = typeof val === 'boolean' ? val : result[key]; } }); } return result; } /** * 数组分组 * - 相当于 lodash 的 _.groupBy * @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity */ function groupBy(array = [], callback) { return array.reduce((groups, item) => { const key = typeof callback === 'function' ? callback(item) : item[callback]; if (!groups[key]) { groups[key] = []; } groups[key].push(item); return groups; }, {}); } /** * 创建一个从 object 中选中的属性的对象。 * @param {*} object * @param {array} keys */ function pick(object, keys) { return keys.reduce((obj, key) => { if (object && Object.prototype.hasOwnProperty.call(object, key)) { obj[key] = object[key]; } return obj; }, {}); } /** * 返回对象的副本,经过筛选以省略指定的键。 * @param {*} object * @param {string[]} keysToOmit * @returns */ function omit(object, keysToOmit) { return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key))); } /** * 深拷贝 */ function cloneDeep(value, visited = new WeakMap()) { // 处理循环引用 if (visited.has(value)) { return visited.get(value); } // 特殊对象和基本类型处理 if (value instanceof Date) { return new Date(value); } if (value instanceof RegExp) { return new RegExp(value.source, value.flags); } if (value === null || typeof value !== 'object') { return value; } // 创建一个新的WeakMap项以避免内存泄漏 let result; if (Array.isArray(value)) { result = []; visited.set(value, result); } else { result = {}; visited.set(value, result); } for (const key of Object.getOwnPropertySymbols(value)) { // 处理Symbol属性 result[key] = cloneDeep(value[key], visited); } for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { // 处理普通属性 result[key] = cloneDeep(value[key], visited); } } return result; } /** * 向零四舍五入, 固定精度设置 */ function curriedFix(precision = 0) { return function (number) { // Shift number by precision places const shift = Math.pow(10, precision); const shiftedNumber = number * shift; // Round to nearest integer const roundedNumber = Math.round(shiftedNumber); // Shift back decimal place return roundedNumber / shift; }; } /** * 向零四舍五入, 保留2位小数 */ const fixTo2Decimals = curriedFix(2); /** * 向零四舍五入, 保留4位小数 */ const fixTo4Decimals = curriedFix(4); const fixTo1Decimals = curriedFix(1); const fixToInt = curriedFix(0); /** * 创建一个按大小分组的元素数组 */ const chunk = (input = [], size = 0) => { return input.reduce((arr, item, idx) => { return idx % size === 0 ? [...arr, [item]] : [...arr.slice(0, -1), [...arr.slice(-1)[0], item]]; }, []); }; /** * 映射 * @example * const keyMap = { a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}], b: {key: 'b1'} }; const result = objectMapper({a: 1, b: 3}, keyMap); // result = {a1: 1, a2: 2, b1: 3} * */ function objectMapper(input, keyMap, keep = true) { // Loop through array mapping if (Array.isArray(input)) { return input.map((obj) => objectMapper(obj, keyMap)); } if (typeof input === 'object') { const mappedObj = {}; Object.keys(input).forEach((key) => { // Keep original keys not in keyMap if (!keyMap[key] && keep) { mappedObj[key] = input[key]; } // Handle array of maps if (Array.isArray(keyMap[key])) { keyMap[key].forEach((map) => { let value = input[key]; if (map.transform) value = map.transform(value); mappedObj[map.key] = value; }); // Handle single map } else { const map = keyMap[key]; if (map) { let value = input[key]; if (map.transform) value = map.transform(value); mappedObj[map.key || map] = value; } } }); return mappedObj; } return input; } /** * 创建一个对应于对象路径的值数组 */ function at(obj, path) { let result; if (Array.isArray(obj)) { // array case const indexes = path.split('.').map((i) => parseInt(i)); result = []; for (let i = 0; i < indexes.length; i++) { result.push(obj[indexes[i]]); } } else { // object case const indexes = path.split('.').map((i) => i); result = [obj]; for (let i = 0; i < indexes.length; i++) { result = [result[0]?.[indexes[i]] || undefined]; } } return result; } /** * 删除 null/undefined */ function flush(collection) { let result, len, i; if (!collection) { return undefined; } if (Array.isArray(collection)) { result = []; len = collection.length; for (i = 0; i < len; i++) { const elem = collection[i]; if (elem != null) { result.push(elem); } } return result; } if (typeof collection === 'object') { result = {}; const keys = Object.keys(collection); len = keys.length; for (i = 0; i < len; i++) { const key = keys[i]; const value = collection[key]; if (value != null) { result[key] = value; } } return result; } return undefined; } /** * 千分位 格式化数字 */ const numberFormatter = (number) => { return new Intl.NumberFormat().format(number); }; /** * @example * const obj = { a: { b: 'c' } }; * const keyArr = ['a', 'b']; * getNestedValue(obj, keyArr); // Returns: 'c' */ const getNestedValue = (obj, keyArr) => { return keyArr.reduce((acc, curr) => { return acc && Object.prototype.hasOwnProperty.call(acc, curr) ? acc[curr] : undefined; // return acc && acc[curr]; }, obj); }; /** * 计算笛卡尔积 */ const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') => { let result = []; if (index === arr.length) { return [prefix]; } arr[index].forEach((item) => { result = result.concat(cartesianProductArray(arr, sep, index + 1, prefix ? `${prefix}${sep}${item}` : `${item}`)); }); return result; }; const stringToColour = (str) => { var hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } var colour = '#'; for (let i = 0; i < 3; i++) { var value = (hash >> (i * 8)) & 0xff; value = (value % 150) + 50; colour += ('00' + value.toString(16)).substr(-2); } return colour; }; const debounce = (func, wait, immediate) => { var timeout; return function () { var context = this, args = arguments; clearTimeout(timeout); if (immediate && !timeout) func.apply(context, args); timeout = setTimeout(function () { timeout = null; if (!immediate) func.apply(context, args); }, wait); }; }; const removeFormattingChars = (str) => { const regex = /[\r\n\t\v\f]/g; str = str.replace(regex, ' '); // Replace more than four consecutive spaces with a single space str = str.replace(/\s{4,}/g, ' '); return str; }; const olog = (text, ...args) => { console.log(`%c ${text} `, 'background:#fb923c ; padding: 1px; border-radius: 3px; color: #fff', ...args); }; const sanitizeFilename = (str) => { // Remove whitespace and replace with hyphens str = str.replace(/\s+/g, '-'); // Remove invalid characters and replace with hyphens str = str.replace(/[^a-zA-Z0-9.-]/g, '-'); // Replace consecutive hyphens with a single hyphen str = str.replace(/-+/g, '-'); // Trim leading and trailing hyphens str = str.replace(/^-+|-+$/g, ''); return str; }; const formatBytes = (bytes, decimals = 2) => { if (bytes === 0) return ''; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }; const calcCacheSizes = async () => { try { let swCacheSize = 0; let diskCacheSize = 0; let indexedDBSize = 0; // 1. Get the service worker cache size if ('caches' in window) { const cacheNames = await caches.keys(); for (const name of cacheNames) { const cache = await caches.open(name); const requests = await cache.keys(); for (const request of requests) { const response = await cache.match(request); swCacheSize += Number(response.headers.get('Content-Length')) || 0; } } } // 2. Get the disk cache size // const diskCacheName = 'disk-cache'; // const diskCache = await caches.open(diskCacheName); // const diskCacheKeys = await diskCache.keys(); // for (const request of diskCacheKeys) { // const response = await diskCache.match(request); // diskCacheSize += Number(response.headers.get('Content-Length')) || 0; // } // 3. Get the IndexedDB cache size // const indexedDBNames = await window.indexedDB.databases(); // for (const dbName of indexedDBNames) { // const db = await window.indexedDB.open(dbName.name); // const objectStoreNames = db.objectStoreNames; // if (objectStoreNames !== undefined) { // const objectStores = Array.from(objectStoreNames).map((storeName) => db.transaction([storeName], 'readonly').objectStore(storeName)); // for (const objectStore of objectStores) { // const request = objectStore.count(); // request.onsuccess = () => { // indexedDBSize += request.result; // }; // } // } // } return { swCacheSize, diskCacheSize, indexedDBSize, totalSize: Number(swCacheSize) + Number(diskCacheSize) + indexedDBSize }; } catch (error) { console.error('Error getting cache sizes:', error); } }; const clearAllCaches = async (cb) => { try { // 1. Clear the service worker cache if ('caches' in window) { // if (navigator.serviceWorker) { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map((name) => caches.delete(name))); } // 2. Clear the disk cache (HTTP cache) // const diskCacheName = 'disk-cache'; // await window.caches.delete(diskCacheName); // const diskCache = await window.caches.open(diskCacheName); // const diskCacheKeys = await diskCache.keys(); // await Promise.all(diskCacheKeys.map((request) => diskCache.delete(request))); // 3. Clear the IndexedDB cache const indexedDBNames = await window.indexedDB.databases(); await Promise.all(indexedDBNames.map((dbName) => window.indexedDB.deleteDatabase(dbName.name))); // Unregister the service worker const registration = await navigator.serviceWorker.getRegistration(); if (registration) { await registration.unregister(); console.log('Service worker unregistered'); } else { console.log('No service worker registered'); } if (typeof cb === 'function') { cb(); } } catch (error) { console.error('Error clearing caches or unregistering service worker:', error); } }; const loadScript = (src) => { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.type = 'text/javascript'; script.onload = resolve; script.onerror = reject; script.crossOrigin = 'anonymous'; script.src = src; if (document.head.append) { document.head.append(script); } else { document.getElementsByTagName('head')[0].appendChild(script); } }); }; //格式化为冒号时间,2010转为20:10 const formatColonTime = (text) => { const hours = text.substring(0, 2); const minutes = text.substring(2); return `${hours}:${minutes}`; }; // 生成唯一 36 位数字,用于新增记录 ID 赋值,React key 属性等 const generateId = () => new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 9); module.exports = { copy, formatDate, formatTime, formatDatetime, camelCase, prepareUrl, throttle, clickUrl, escape2Html, formatPrice, formatPercent, isEmpty, isNotEmpty, sortBy, sortKeys, sortArrayByOrder, merge, groupBy, pick, omit, cloneDeep, curriedFix, fixTo2Decimals, fixTo4Decimals, fixTo1Decimals, fixToInt, chunk, objectMapper, at, flush, numberFormatter, getNestedValue, cartesianProductArray, stringToColour, debounce, removeFormattingChars, olog, sanitizeFilename, formatBytes, calcCacheSizes, clearAllCaches, loadScript, formatColonTime, generateId, };