import { NAME } from '../const/index'; import TUIGlobal from '../TUIGlobal/tuiGlobal'; export const isUndefined = function (input: any) { return typeof input === NAME.UNDEFINED; }; export const isPlainObject = function (input: any) { // 注意不能使用以下方式判断,因为IE9/IE10下,对象的__proto__是 undefined // return isObject(input) && input.__proto__ === Object.prototype; if (typeof input !== NAME.OBJECT || input === null) { return false; } const proto = Object.getPrototypeOf(input); if (proto === null) { // edge case Object.create(null) return true; } let baseProto = proto; while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto); } // 原型链第一个和最后一个比较 return proto === baseProto; }; export const isArray = function (input: any) { if (typeof Array.isArray === NAME.FUNCTION) { return Array.isArray(input); } return (Object as any).prototype.toString.call(input).match(/^\[object (.*)\]$/)[1].toLowerCase() === NAME.ARRAY; }; export const isPrivateKey = function (key: string) { return key.startsWith('_'); }; export const isUrl = function (url: string) { return /^(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+[a-zA-Z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url); }; /** * 检测input类型是否为string * @param {*} input 任意类型的输入 * @returns {Boolean} true->string / false->not a string */ export const isString = function (input: any) { return typeof input === NAME.STRING; }; export const isBoolean = function (input: any) { return typeof input === NAME.BOOLEAN; }; export const isNumber = function (input: any) { return ( // eslint-disable-next-line input !== null && ((typeof input === NAME.NUMBER && !isNaN(input - 0)) || (typeof input === NAME.OBJECT && input.constructor === Number)) ); }; export function formatTime(secondTime: number): string { const hours: number = Math.floor(secondTime / 3600); const minutes: number = Math.floor((secondTime % 3600) / 60); const seconds: number = Math.floor(secondTime % 60); let callDurationStr: string = hours > 9 ? `${hours}` : `0${hours}`; callDurationStr += minutes > 9 ? `:${minutes}` : `:0${minutes}`; callDurationStr += seconds > 9 ? `:${seconds}` : `:0${seconds}`; return callDurationStr; } export function formatTimeInverse(stringTime: string): number { const list = stringTime.split(':'); return parseInt(list[0]) * 3600 + parseInt(list[1]) * 60 + parseInt(list[2]); // eslint-disable-line } // Determine if it is a JSON string export function isJSON(str: string) { if (typeof str === NAME.STRING) { try { const data = JSON.parse(str); if (data) { return true; } return false; } catch (error) { console.debug(error); return false; } } return false; } // Determine if it is a JSON string export const JSONToObject = function (str: string) { if (!str || !isJSON(str)) { return str; } return JSON.parse(str); }; /** * 重试函数, catch 时,重试 * @param {Promise} promise 需重试的函数 * @param {number} num 需要重试的次数 * @param {number} time 间隔时间(s) * @returns {Promise} im 接口的 response 原样返回 */ export const retryPromise = (promise: Promise, num: number = 6, time: number = 0.5) => { let n = num; const func = () => promise; return func() .catch((error: any) => { if (n === 0) { throw error; } const timer = setTimeout(() => { func(); clearTimeout(timer); n = n - 1; }, time * 1000); }); }; // /** // * 节流函数(目前 TUICallKit 增加防重调用装饰器,该方法可删除) // * @param {Function} func 传入的函数 // * @param {wait} time 间隔时间(ms) // */ // export const throttle = (func: Function, wait: number) => { // let previousTime = 0; // return function () { // const now = Date.now(); // const args = [...arguments]; // if (now - previousTime > wait) { // func.apply(this, args); // previousTime = now; // } // }; // } /** * web call engine 重复调用时的错误, 这种错误在 TUICallKit 应该忽略 * @param {any} error 错误信息 * @returns {Boolean} */ export function handleRepeatedCallError(error: any) { if (error?.message?.indexOf('is ongoing, please avoid repeated calls') !== -1) { return true; } return false; } /** * 设备无权限时的错误处理 * @param {any} error 错误信息 * @returns {Boolean} */ export function handleNoDevicePermissionError(error: any) { const { message } = error; if (message?.indexOf('NotAllowedError: Permission denied') !== -1) { return true; } return false; } /* * 获取向下取整的 performance.now() 值 * 在不支持 performance.now 的浏览器中,使用 Date.now(). 例如 ie 9,ie 10,避免加载 sdk 时报错 * @export * @return {Number} */ export function performanceNow() { // if (!performance || !performance.now) { // return Date.now(); // } // return Math.floor(performance.now()); // uni-app 打包小程序没有 performance, 报错 return Date.now(); } /** * 检测input类型是否为function * @param {*} input 任意类型的输入 * @returns {Boolean} true->input is a function */ export const isFunction = function (input: any) { return typeof input === NAME.FUNCTION; }; /* * 获取浏览器语言 * @export * @return {zh-cn | en} */ export const getLanguage = () => { if (TUIGlobal.getInstance().isWeChat) { return 'zh-cn'; } // @ts-ignore const lang = (navigator?.language || navigator?.userLanguage || '').substr(0, 2); let language = 'en'; switch (lang) { case 'zh': language = 'zh-cn'; break; case 'ja': language = 'ja_JP'; break; default: language = 'en'; } return language; }; export function noop(e: any) {} /** * Get the object type string * @param {*} input 任意类型的输入 * @returns {String} the object type string */ export const getType = function(input) { return Object.prototype.toString .call(input) .match(/^\[object (.*)\]$/)[1] .toLowerCase(); }; // 修改对象键名 export function modifyObjectKey(obj, oldKey, newKey) { if (!obj.hasOwnProperty(oldKey)) { return obj; } const newObj = {}; Object.keys(obj).forEach(key => { if (key === oldKey) { newObj[newKey] = obj[key]; } else { newObj[key] = obj[key]; } }); return newObj; } /** * interpolate function * @param {string} str - 'hello {{name}}' * @param {object} data - { name: 'sam' } * @returns {string} 'hello sam' * */ export function interpolate(str, data) { return str.replace(/{{\s*(\w+)(\s*,\s*[^}]+)?\s*}}/g, (match, p1) => { const key = p1.trim(); return data[key] !== undefined ? String(data[key]) : match; }); } /** * Detect current runtime environment */ function detectEnvironment(): 'pc-browser' | 'mobile-browser' | 'webview' | 'miniprogram' { const ua = navigator.userAgent || ''; const uaLower = ua.toLowerCase(); // WeChat MiniProgram WebView if (uaLower.includes('miniprogram') || (window as any).__wxjs_environment === 'miniprogram') { return 'miniprogram'; } // App WebView if (uaLower.includes('webview') || uaLower.includes(' wv') || (window as any).webkit?.messageHandlers) { return 'webview'; } // Mobile browser if (/Android|iPhone|iPad|iPod|Mobile/i.test(ua)) { return 'mobile-browser'; } return 'pc-browser'; } /** * Detect if current device is iOS */ function isIOS(): boolean { return /iPad|iPhone|iPod/.test(navigator.userAgent || ''); } /** * Detect if current device is Android */ function isAndroid(): boolean { return /Android/.test(navigator.userAgent || ''); } /** * Enhanced browser/webview close detection for exception exit handling * Supports: PC browsers (Chrome/Safari/Firefox/Edge), Mobile browsers (Android/iOS), WebView * * Note: Mobile background switching (visibilitychange to hidden) will NOT trigger hangup, * only actual page close/navigation events will trigger the callback. * * @param callback - Function to call when exit is detected */ export function initBrowserCloseDetection(callback: Function) { if (typeof window === 'undefined') return; let isExiting = false; const env = detectEnvironment(); console.log(`[ExitDetection] Environment: ${env}, iOS: ${isIOS()}, Android: ${isAndroid()}`); /** * Trigger exit handler with deduplication */ const triggerExit = (source: string, event?: Event) => { if (isExiting) return; isExiting = true; console.log(`[ExitDetection] Exit triggered by: ${source}`); try { callback(event); } catch (error) { console.error(`[ExitDetection] Callback error:`, error); } // Reset flag after delay (for bfcache scenarios) setTimeout(() => { isExiting = false; }, 1000); }; /** * Handle beforeunload event * Best for: PC browsers, Android browsers */ const handleBeforeUnload = (event: BeforeUnloadEvent) => { // Skip if it's just navigation (not actual page close) const navigationEntries = performance?.getEntriesByType?.('navigation') || []; const navigationEntry = navigationEntries[0] as PerformanceNavigationTiming; if (navigationEntry && navigationEntry.type === 'navigate') { return; } triggerExit('beforeunload', event); // Required for some browsers to trigger the event event.returnValue = ''; return ''; }; /** * Handle pagehide event * Best for: iOS Safari, all modern browsers * More reliable than beforeunload on iOS */ const handlePageHide = (event: PageTransitionEvent) => { // persisted=true means page might be restored from bfcache if (!event.persisted) { triggerExit('pagehide', event); } }; /** * Handle visibilitychange event * Note: We only log this event, do NOT trigger hangup on mobile background switch * Because user may just switch apps temporarily and come back */ const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { // Only log, do NOT trigger exit on visibility change // Mobile users often switch apps temporarily during calls console.log(`[ExitDetection] Visibility changed to hidden (not triggering exit)`); } }; /** * Handle freeze event (Page Lifecycle API, Chrome 68+) * This indicates the page is being frozen, which is a strong signal of exit */ const handleFreeze = () => { triggerExit('freeze'); }; // ============ Register event listeners based on environment ============ // 1. pagehide - Most reliable for iOS, works on all modern browsers if (window.addEventListener) { window.addEventListener('pagehide', handlePageHide, { capture: true }); } // 2. beforeunload - Best for PC browsers and Android if (env === 'pc-browser' || isAndroid()) { window.addEventListener('beforeunload', handleBeforeUnload); } // 3. visibilitychange - Only for logging, not triggering exit document.addEventListener('visibilitychange', handleVisibilityChange); // 4. freeze - Page Lifecycle API (Chrome 68+) if ('onfreeze' in document) { document.addEventListener('freeze', handleFreeze as EventListener); } // 5. unload - Legacy fallback (deprecated but still useful in some cases) window.addEventListener('unload', (event) => { triggerExit('unload', event); }); console.log(`[ExitDetection] Listeners registered for environment: ${env}`); }