PageSourceSearch

https://lingyuzhao.top/js/utils.js

js lingyuzhao.top collected 2026-10-06 13:15:38 UTC 61,648 bytes, 1,746 lines download raw bytes

1/**
2 * 生成一个指定区间 指定步长数值的等差list
3 * @param start list的左区间
4 * @param end list的右区间
5 * @param step 步长数值
6 * @returns {*[]} 等差数列
7 */
8function range(start, end, step) {
9    const res = [];
10    let index = start;
11    while (index < end) {
12        res.push(index);
13        index += step;
14    }
15    return res;
16}
17
18// 准备url路径提取所需要 正则对象
19const urlPathReg1 = new RegExp("\\?");
20const urlPathReg2 = new RegExp("/+");
21
22/**
23 * 将当前的 url 进行拆分
24 * @returns {{pram: string, url: string[]}}  {"url" :[url协议,url主机,路径1,路径2,.... ], pram :"参数字符串"}
25 */
26function parseUrl() {
27    // 首先获取到url地址
28    const url = window.location.href;
29    // 获取到 请求数据部分 [url和路径,参数1,参数2]
30    const strings1 = url.split(urlPathReg1);
31    // 然后按照规则获取到{"url" :[url请求部分,路径1,路径2,.... ], pram :"参数字符串"}
32    return {"url": strings1[0].split(urlPathReg2), "pram": strings1[1]};
33}
34
35/**
36 * 将当前的 url 部分中的servlet请求部分进行替换
37 * @param servlet_str 服务类字符串数据
38 * @param prams 额外追加的参数字符串
39 * @returns {string} 转换之后的新字符串
40 */
41function setUrlServlet(servlet_str, prams) {
42    const d = parseUrl();
43    let res = d['url'][0] + '/' + d['url'][1] + '/' + servlet_str + '?';
44    const p = d['pram'];
45    if (!(p == null || p.length === 0)) {
46        res += p;
47        res += '&';
48    }
49    if (prams != null) {
50        for (let pram of prams) {
51            res += prams;
52        }
53    }
54    return res;
55}
56
57/**
58 * 判断一个对象是否可以进行迭代
59 * @param obj 需要被判断的对象
60 * @returns {boolean} 如果可迭代则返回 true
61 */
62function isIterable(obj) {
63    return typeof obj === 'object' && obj !== null && !Array.isArray(obj) && typeof obj[Symbol.iterator] === 'function';
64}
65
66function isJson(obj) {
67    return obj !== null && obj instanceof Object && typeof obj === 'object';
68}
69
70/**
71 * 将一个 json 字符串中的数据按照指定的 keys 获取
72 * @param jsonStr json字符串
73 * @param showKeys 需要被提取的值对应的路径
74 * @returns {any}
75 */
76function jsonShow(jsonStr, showKeys) {
77    // 首先将字符串转换为 json 然后获取 key 对应的value
78    let res = JSON.parse(jsonStr);
79    for (let name in showKeys) {
80        res = res[name];
81    }
82    return res;
83}
84
85/**
86 *
87 * @param jsonString 需要被格式化的 json 字符串
88 * @returns {string} 格式化好的json字符串
89 */
90function formatJson(jsonString) {
91    if (isJson(jsonString)) {
92        return JSON.stringify(jsonString, null, 2);
93    }
94    const jsonObj = JSON.parse(jsonString);
95    return JSON.stringify(jsonObj, null, 2);
96}
97
98/**
99 * 将url中的 params 中的字符串重新设置
100 * @param params 参数
101 */
102function setParamToHref(params) {
103    // 获取当前地址栏的URL
104    let currentUrl = window.location.href;
105    // 按照 ? 拆分之后 重新设置参数
106    window.location.href = currentUrl.split('?')[0] + '?' + params;
107}
108
109/**
110 * 搜索URL中的指定的参数
111 * @return 搜索到的同名参数组合的 list
112 */
113function search_Params(paramName) {
114    // 创建一个 URLSearchParams 对象
115    const params = new URLSearchParams(window.location.search);
116    // 获取名为 "paramName" 的参数值
117    const paramValues = params.getAll(paramName);
118    // 将参数值封装为列表
119    return Array.from(paramValues);
120}
121
122/**
123 * 获取当前页面的路径
124 * @return {string[]} 返回当前页面的路径
125 */
126function search_pathFromUrl() {
127    const search = window.location.search;
128    if (search.length === 0) {
129        return [];
130    }
131    return search.split('?')[0].split('/');
132}
133
134/**
135 * 检查一个字符串是否是一个数值
136 * @param str 需要被检查的字符串
137 * @return {boolean} 如果字符串中为数值形式,则返回 true
138 */
139function isNumericString(str) {
140    // 使用 parseFloat 尝试将字符串转换为数值
141    const num = parseFloat(str);
142
143    // 检查是否为 NaN,如果为 NaN,则说明不是数值
144    return !isNaN(num);
145}
146
147/**
148 * 判断两个 json 对象是否一致
149 * @param j1 被判断的第一个 json 对象
150 * @param j2 被判断的第二个 json 对象
151 * @return {boolean} 如果 j1 中的 所有 key 存在于 j2 中 同时 值一致 则返回 true
152 */
153function eq_json(j1, j2) {
154    if (isJson(j1) && isJson(j2)) {
155        let res = j1 !== undefined && j2 !== undefined;
156
157        for (let k in j1) {
158            if (k === 'ip' || k === 'address') {
159                continue;
160            }
161            if (res) {
162                // 判断 j1 中的 key 是否在 j2 中 并且 值一致
163                res = k in j2 && j1[k] === j2[k];
164            } else {
165                break;
166            }
167        }
168        return res;
169    } else {
170        return false;
171    }
172}
173
174/**
175 * 检查当前页面是否属于自己的检查级别函数存储目录
176 * @type {*[]}
177 */
178const checkPl_List = [
179    () => eq_json(my, my_temp),
180    () => eq_json(USER_CONF, USER_CONF_TEMP),
181    () => TRENDS_DATA_TEMP.sendId === parseInt(my.id),
182    () => MALI_DATA_TEMP.sendId === parseInt(my.id),
183    () =>
183 MALI_DATA_TEMP.recId === parseInt(my.id),
184    () => ARTICLE_DATA_TEMP.sendId === parseInt(my.id)
185];
186
187/**
188 * 判断当前页面是否属于自己
189 * 如果属于自己 user_other 类的控件将会被隐藏
190 * 如果不属于自己 user_self 类的控件将会被隐藏
191 * @param checkPl 检查的级别 越高检查的越多 如果是负数代表指定检查的数据的索引
192 * @param checkStart 链式检查级别的起始索引 例如 checkPL 为 2 checkStart=0 代表检查 0 1 2  也就是 checkStart 到 checkPL
193 * @param fun 额外的检查函数 如果不需要进行自定义检查 直接至为空
194 * @param isOr 判断规则是否为 or 如果是 则所有检查中有一个通过检查则代表是自己
195 * @return boolean 当前登录的用户是否为自己
196 */
197function checkIsSelf(checkPl = 2, fun = undefined, checkStart = 0, isOr = false) {
198    if (isNotLogin()) {
199        const es = document.getElementsByClassName("user_self");
200        for (let i = 0; i < es.length; i++) {
201            es[i].style.display = 'none';
202        }
203        return false;
204    }
205    let isLogin = false;
206    if (checkPl > 0) {
207        const max = checkPl_List.length < checkPl ? checkPl_List.length : checkPl;
208        // 代表的是级别
209        if (isOr) {
210            for (let i = checkStart; i < max; i++) {
211                const fun = checkPl_List[i];
212                if (fun === undefined) {
213                    break;
214                }
215                if (fun()) {
216                    isLogin = true;
217                    break;
218                }
219            }
220        } else {
221            for (let i = checkStart; i < max; i++) {
222                const fun = checkPl_List[i];
223                if (fun === undefined) {
224                    break;
225                }
226                isLogin = fun();
227            }
228        }
229    } else {
230        // 代表的是索引的负数
231        isLogin = checkPl_List[-checkPl]();
232    }
233    if (isLogin) {
234        // 代表是本人访问 隐藏所有的其它用户类的按钮
235        const es = document.getElementsByClassName("user_other");
236        for (let i = 0; i < es.length; i++) {
237            es[i].style.display = 'none';
238        }
239    } else {
240        // 代表是别人访问 隐藏所有的自己用户类的按钮
241        const es = document.getElementsByClassName("user_self");
242        for (let i = 0; i < es.length; i++) {
243            es[i].style.display = 'none';
244        }
245    }
246    return isLogin;
247}
248
249/**
250 * 清除掉一个元素中的所有子元素
251 * @param e 需要被清理的元素
252 */
253function clearChildren(e) {
254    const delete_list = [];
255    e.childNodes.forEach(child => {
256        const childNodes = child.childNodes;
257        if (childNodes.length > 0) {
258            childNodes.forEach(node => clearChildren(node));
259        }
260        delete_list.push(child);
261    });
262    for (let i = 0; i < delete_list.length; i++) {
263        delete_list[i].remove();
264    }
265}
266
267/**
268 * 禁用按钮 并且更新按钮的显示的文本
269 * @param button 需要被禁用的按钮
270 */
271function hiddenButton(button) {
272    button.hidden = true;
273}
274
275/**
276 * 禁用按钮 并且更新按钮的显示的文本
277 * @param button 需要被禁用的按钮
278 */
279function showHiddenButton(button) {
280    button.hidden = false;
281}
282
283/**
284 * 禁用按钮 并且更新按钮的显示的文本
285 * @param button 需要被禁用的按钮
286 * @param newString 按钮中显示的新的文本
287 */
288function disableAndUpdate(button, newString) {
289    button.disabled = true;
290    button.innerText = newString;
291}
292
293/**
294 * 启用按钮 并且更新按钮的显示的文本
295 * @param button 需要被禁用的按钮
296 * @param newString 按钮中显示的新的文本
297 */
298function openAndUpdate(button, newString) {
299    button.disabled = false;
300    button.innerText = newString;
301}
302
303/**
304 * 获取当前客户端的时区
305 * @return {number} 当前客户端的时区 正代表东,负代表西 单位是分钟!
306 */
307function getTimeZone() {
308    return -new Date().getTimezoneOffset();
309}
310
311/**
312 * 时间转换工具类
313 * @param aOffset 源时区相对于UTC的偏移量(秒,东正西负)
314 * @param timeInMsForA 需转换的源时区时间(毫秒)
315 * @param bOffset 目标时区相对于UTC的偏移量(秒,默认为客户端时区)
316 * @return {number} 转换后的时间毫秒值
317 */
318function convertTimezone(aOffset, timeInMsForA, bOffset = getTimeZone() * 60) {
319    if (timeInMsForA instanceof String) {
320        timeInMsForA = parseInt(timeInMsForA);
321    }
322
323    // 将时区偏移量从秒转为毫秒
324    aOffset *= 1000;
325    bOffset *= 1000;
326
327    // 调整时间到UTC,然后转为目标时区
328    // 注意:从aOffset到UTC,再从UTC到bOffset,直接应用偏移量的差即可
329    return timeInMsForA - aOffset + bOffset;
330}
331
332/**
333 * 格式化日期和时间。
334 * @param {Date} date - 要格式化的 Date 对象。
335 * @returns {string} 格式化后的日期时间字符串,格式为 YYYY-MM-DD HH:MM:SS。
336 */
337function formatDateTime(date) {
338    let year = date.getFullYear(),
339        month = date.getMonth() + 1, // 月份从0开始计数,所以需要加1
340        day = date.getDate(),
341        hour = date.getHours(),
342        minute = date.getMinutes(),
343        second = date.getSeconds();
344
345    // 使用三元运算符来格式化小于10的数字,前面加零
346    return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day} `
347        + `${hour}:${minute < 10 ? '0' + minute : minute}:${second < 10 ? '0' + second : second}`;
348}
349
350/**
351 * 计算时间差。
352 * @param {number} timestamp1 - 第一个时间戳(毫秒)。如果是 -1 则返回空字符串!
353 * @param {number} ifTime1IsUndef - 第一个时间戳是否为undefined,如果是则会使用此数值 如果是 -1 则返回空字符串!
354 * @param {number} timestamp2 - 第二个时间戳(毫秒)。
355 * @returns {string} 时间差字符串,格式为 "X day(s)" 或 "X hour(s)" 或 "X minute(s)" 或 "X second(s)"。
356 */
357function timeDifference(timestamp1, ifTime1IsUndef = 0, timestamp2 = Date.now()) {
358    if (timestamp1 === undefined) {
359        timestamp1 = ifTime1IsUndef;
360    }
361    if (timestamp1 === -1) {
362        return "";
363    }
364    // Calculate the difference in milliseconds
365    const diffMilliseconds = Math.abs(timestamp1 - timestamp2);
366
367    // Convert to seconds, minutes, hours, days, months, and years
368    let diffSeconds = Math.floor(diffMilliseconds / 1000);
369    let diffMinutes = Math.floor(diffSeconds / 60);
370    let diffHours = Math.floor(diffMinutes / 60);
371    let diffDays = Math.floor(diffHours / 24);
372    let diffMonths = Math.floor(diffDays / 30.44);
373    let diffYears = Math.floor(diffDays / 365.25);
374
375    // Format the output
376    if (diffYears > 0) {
377        return `${diffYears}å¹´`;
378    } else if (diffMonths > 0) {
379        return `${diffMonths}月`;
380    } else if (diffDays > 0) {
381        return `${diffDays}天`;
382    } else if (diffHours > 0) {
383        return `${diffHours}小时`;
384    } else if (diffMinutes > 0) {
385        return `${diffMinutes}分钟`;
386    } else {
387        return `${diffSeconds}秒`;
388    }
389}
390
391/**
392 * 获取格式化的日期时间字符串。
393 * @param {Date|string} n - 要格式化的日期时间,可以是 Date 对象或能被 Date 构造器解析的字符串。
394 * @returns {string} 格式化后的日期时间字符串。
395 * @throws {Error} 如果提供的参数无法被解析为有效的日期,则抛出错误。
396 */
397function getDate(n) {
398    let date;
399    if (n === undefined) {
400        return getDate(new Date());
401    }
402    // 如果参数已经是 Date 类型,直接使用
403    if (n instanceof Date) {
404        date = n;
405    }
406    // 否则尝试将字符串转换为 Date 对象
407    else {
408        date = new Date(n);
409        // 检查转换后的 Date 对象是否有效
410        if (isNaN(date.getTime())) {
411            throw new Error('Invalid date');
412        }
413    }
414
415    // 调用 formatDateTime 函数来格式化日期时间
416    return formatDateTime(date);
417}
418
419/**
420 * 获取到一天的时间
421 * @param n 需要被转换的时间的毫秒值
422 * @return {string} 转换之后的时间戳数值
423 */
424function getDateByDay(n) {
425    if (n instanceof Date) {
426        return n.toTimeString().substring(0, 8)
427    }
428    return new Date(n).toTimeString().substring(0, 8);
429}
430
431/**
432 * 将时间字符串转换为Date
433 * @param dateStr 时间字符串 日期字符串格式为YYYY-MM-DD HH:mm:ss.SSS
434 * @return {Date} date 对象!
435 */
436function dateStringToMilliseconds(dateStr) {
437    let parts = dateStr.split(/[- :.]/);
438    // parts[0]: 年, parts[1]: 月, parts[2]: 日, parts[3]: 时, parts[4]: 分, parts[5]: 秒, parts[6]: 毫秒
439    return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5], parts[6]);
440}
441
442/**
443 * IP 与地址的映射存储池 key 是ip value 是IP 对应的地址
444 * @type {{string:string}}
445 */
446let ipAddress_SAVE = {};
447
448/**
449 * IP 解析器的实现类 key 是要试用的 IP 解析的模式 value 就是解析器的实现函数
450 * @type {{}}
451 */
452const getIPLocation_parser = {
453    "AI_WEN": (ipAddress) => {
454        return axios(
455            {
456                url: `https://api.ipplus360.com/ip/geo/v1/city/`,
457                params: {
458                    lang: 'zh-CN',
459                    key: getWebConfByKey("IP_SK"),
460                    ip: ipAddress,
461                    coordsys: 'WGS84'
462                }
463            }
464        ).then(
465            (response) => {
466                let data = response.data;
467                if (data && data.code === 'Success') {
468                    data = data.data;
469                    return {
470                        country: data.country,
471                        region: data.city,
472                        city: data.city,
473                        lat: data.lat,
474                        lon: data.lng,
475                        district: data.district === undefined ? "" : data.district,
476                    }
477                } else {
478                    // 如果 埃文 出错了 就不用埃文
479                    console.error("getIPLocation is 埃文 出错了:", data.code);
480                    return getIPLocation(ipAddress, false);
481                }
482            }
483        )
484    },
485    "ipDataCloud": (ipAddress) => {
486        return axios(
487            {
488                url: `https://api.ipdatacloud.com/v2/query`,
489                method: 'post',
490                params: {
491                    lang: 'zh-CN',
492                    key: getWebConfByKey("IP_SK"),
493                    ip: ipAddress,
494                }
495            }
496        ).then(
497            (response) => {
498                let data = response.data;
499                if (data && data.code === 200 && data.data) {
500                    data = data.data.location;
501                    return {
502                        country: data.country,
503                        region: data.city,
504                        city: data.city,
505                        lat: data.latitude,
506                        lon: data.longitude,
507                        district: data.district === undefined ? "" : data.district,
508                    }
509                } else {
510                    // 如果 IP 云 出错了 就不用 IP 云
511                    console.error("getIPLocation is IP数据云 出错了:", data.msg);
512                    return getIPLocation(ipAddress, false);
513                }
514            }
515        )
516    },
517    "apiSpace.com": (ipAddress) => {
518        return axios(
519            {
520                "url": "https://eolink.o.apispace.com/ipguishu/ip/geo/v1/district?ip=" + ipAddress + "&coordsys=WGS84",
521                "method": "GET",
522                "headers": {
523                    "X-APISpace-Token": getWebConfByKey("IP_SK")
524                },
525                "data": {},
526            }
527        ).then(
528            (response) => {
529                let data = response.data;
530                if (data && data.code === 'Success') {
531                    data = data.data;
532                    return {
533                        country: data.country,
534                        region: data.city,
535                        city: data.city,
536                        lat: data.lat,
537                        lon: data.lng,
538                        district: data.district === undefined ? "" : data.district,
539                    }
540                } else {
541                    // 如果 apiSpace 出错了 就不用 apiSpace
542                    console.error("getIPLocation is apiSpace 出错了:", data.code);
543                    return getIPLocation(ipAddress, false);
544                }
545            }
546        )
547    },
548    "codeBookShop": (ipAddress) => {
549        return axios(
550            {
551                url: `https://shop.lingyuzhao.top/api/ipAddress/${ipAddress}`,
552                params: {
553                    id: 17,
554                    sk: "-2462274026238529984",
555                }
556            }
557        ).then(
558            (response) => {
559                const data = response.data;
560                if (data && data.status === 'success') {
561                    return {
562                        country: data.country,
563                        region: data.region,
564                        city: data.city,
565                        lat: data.lat,
566                        lon: data.lon,
567                        district: data.district === undefined ? "" : data.district,
568                    };
569                }
570            }
571        )
572    }
573}
574
575/**
576 * getMyIPAddress 函数对应的缓存的 IP
577 */
578let getMyIPAddress_cacheIP = null;
579
580/**
581 * 获取我的IP地址`
582 */
583async function getMyIPAddress() {
584    const orgFormatJson = ip_server_href() + "/API/ipAddress";
585    if (getMyIPAddress_cacheIP === null) {
586        // 请求成功之后 会直接缓存一个 cookie 其中存储IP信息 这个 IP 只能被后端解析
587        return axios.get(orgFormatJson)
588            .then(
589                (response) => {
590                    const d = response.data;
591                    if (d.ip !== undefined) {
592                        getMyIPAddress_cacheIP = d.ip;
593                        return Promise.resolve({ip: getMyIPAddress_cacheIP});
594                    } else {
595                        console.error("getMyIPAddress: ip error~~ networkError " + orgFormatJson);
596                    }
597                }
598            );
599    } else {
600        return Promise.resolve({ip: getMyIPAddress_cacheIP});
601    }
602}
603
604/**
605 * 将 ip 地址转换为 文字的地址
606 * @param ipAddress ip地址
607 * @param useParser 如果要使用 实现好的模式 则设置为 true
608 * @param useIpParseName 要试用的IP解析器的名字,如果 useParser
609 * @return {Promise<null|{zip, country, city, lon, region: *, lat}>}
610 */
611async function getIPLocation(ipAddress, useParser = true, useIpParseName = 'AI_WEN') {
612    axios.defaults.withCredentials = false;
613    try {
614        console.info(`${ipAddress} parsing~`);
615        const parserName = useParser ? useIpParseName : "codeBookShop";
616        console.info(`getIPLocation is using parser: ${parserName}`);
617        return getIPLocation_parser[parserName](ipAddress);
618    } catch (error) {
619        console.error('Error getting IP location:', error);
620        return null;
621    } finally {
622        axios.defaults.withCredentials = true;
623    }
624}
625
626/**
627 * 将 ip 地址转换为 文字的地址 如果映射池中存储了地址就使用映射地址
628 * @param ipAddress ip地址
629 * @param useParser 如果要使用 内置解析器 则设置为 true
630 * @return {Promise<null|{res, address, lon, lat}>}
631 */
632async function getIPLocationAndCache(ipAddress, useParser = true) {
633    if (useParser) {
634        useParser = getWebConfByKey("IP_SK") !== undefined;
635    }
636    if (ipAddress === undefined) {
637        const res = {};
638        res[WEB_CONF.RES_RETURN] = WEB_CONF.RES_RETURN_OK_VALUE;
639        res['address'] = "银河系";
640        res['lon'] = NaN;
641        res['lat'] = NaN;
642        console.error("ipAddress is undefined");
643        return Promise.resolve(res);
644    }
645    const ipAddressSAVEElement = ipAddress_SAVE[ipAddress];
646    if (ipAddressSAVEElement !== undefined) {
647        // 开始进行结果构建和返回
648        const res = {};
649        res[WEB_CONF.RES_RETURN] = WEB_CONF.RES_RETURN_OK_VALUE;
650        res['address'] = ipAddressSAVEElement.address;
651        res['lon'] = ipAddressSAVEElement.lon;
652        res['lat'] = ipAddressSAVEElement.lat;
653        return Promise.resolve(res);
654    } else {
655        // 如果没有就调用 API
656        return getIPLocation(ipAddress, useParser, "ipDataCloud").then(
657            (res) => {
658                if (res !== null) {
659                    // 首先进行缓存
660                    ipAddress_SAVE[ipAddress] = {
661                        'address': res.country + ' ' + res.city + ' ' + res.district,
662                        'lon': res.lon,
663                        'lat': res.lat,
664                    };
665                    const ipAddressSAVEElement1 = ipAddress_SAVE[ipAddress];
666                    // 然后进行构造和返回
667                    res[WEB_CONF.RES_RETURN] = WEB_CONF.RES_RETURN_OK_VALUE;
668                    res['address'] = ipAddressSAVEElement1.address;
669                    res['lon'] = ipAddressSAVEElement1.lon;
670                    res['lat'] = ipAddressSAVEElement1.lat;
671                    return res;
672                }
673            }
674        ).catch(
675            (error) => {
676                if (useParser) {
677                    // 如果 出错了 就不用解析器了
678                    console.error("getIPLocation 出错了,尝试启动备用方案:", error);
679                    return getIPLocationAndCache(ipAddress, false);
680                } else {
681                    // 备用方案也出错了
682                    jokerBoxPopUp.show("无法正常使用 IP 服务器:" + JSON.stringify(error));
683                    const temp = {};
684                    Ring.playRing('dengDeng')
685                    temp[WEB_CONF.RES_RETURN] = "警告:无法使用 IP 服务器:" + JSON.stringify(error);
686                    return Promise.resolve(temp)
687                }
688            }
689        );
690    }
691}
692
693/**
694 * 处理剪贴板中的数据列表,仅仅能够处理粘贴板中的数据列表,不能自动回调
695 *
696 * @param items 粘贴板中的数据列表。
697 * @param callbackImage 图片回调函数。
698 * @param callbackText 文本回调函数。
699 * @param callbackVideo 视频回调函数。
700 * @param callbackFile 文件回调函数。
701 * @param useLastData 是否只获取到最新复制数据 如果选择是 则不会获取到所有的数据
702 */
703function handleClipboardData(items, callbackImage, callbackText, callbackVideo, callbackFile, useLastData = true) {
704    console.info("handleClipboardData run!!!")
705    if (!items || items.length === 0) {
706        return;
707    }
708    let blob;
709
710    function run(item) {
711        if (item.type.indexOf('image') !== -1) {
712            // 图片数据
713            convertImageToWebP(item.getAsFile(), (blob) =>  callbackImage(blob), (blob) =>  {
714                console.warn("图片转 webp 失败了~");
715                callbackImage(blob);
716            });
717        } else if (item.type.indexOf('video/') === 0) {
718            // 视频数据
719            blob = item.getAsFile();
720            callbackVideo(blob);
721        } else if (item.kind === 'string') {
722            // 文本数据
723            item.getAsString(callbackText)
724        } else if (item.kind === 'file') {
725            // 其它的文件数据
726            blob = item.getAsFile();
727            callbackFile(blob);
728        }
729    }
730
731    if (useLastData) {
732        run(items[items.length - 1]);
733        return;
734    }
735    for (let i = 0; i < items.length; i++) {
736        run(items[i]);
737    }
738}
739
740
741/**
742 * 读取剪贴板中的最新一条数据,并根据数据类型调用不同的回调函数。
743 *
744 * @param e 需要被设置粘贴事件的元素列表。
745 * @param {Function} callbackImage 处理图片数据的回调函数。
746 * @param {Function} callbackText 处理文本数据的回调函数。除了这个函数的参数是元素和字符串 其它的都是 元素和file 对象
747 * @param {Function} callbackVideo 处理视频数据的回调函数。
748 * @param {Function} callbackFile 处理任意类型文件数据的回调函数。
749 * @param {boolean} preventDefault 是否阻止默认事件。默认值为 false。
750 * @param eventName 对应的事件对象的名称。
751 * @param preventDefault 是否阻止默认事件。默认值为 false。
752 * @param useLastData 是否只获取到最新复制数据 如果选择是 则不会获取到所有的数据
753 */
754function readClipboard(e, callbackImage, callbackText, callbackVideo, callbackFile, eventName = 'paste', preventDefault = false, useLastData = true) {
755    function hf(event) {
756        if (preventDefault) {
757            event.preventDefault();
758        }
759        const items = (event.clipboardData || window.clipboardData).items;
760        handleClipboardData(items, callbackImage, callbackText, callbackVideo, callbackFile, useLastData);
761    }
762
763    e.forEach(function (element) {
764        element.addEventListener(eventName, hf);
765    });
766}
767
768/**
769 * 将一个文本复制到剪贴板 并且返回复制操作是否成功
770 * @param textToCopy 需要被复制的文字
771 * @return boolean 如果复制操作成功 返回 true
772 */
773async function copyText(textToCopy) {
774    try {
775        await window.navigator.clipboard.writeText(textToCopy);
776        return true;
777    } catch (err) {
778        return false;
779    }
780}
781
782/**
783 * 将一个文本复制到剪贴板 并且返回复制操作是否成功 备用的复制操作
784 * @param text 需要被复制的文字
785 * @return boolean 如果复制操作成功 返回 true
786 */
787function copyTextToClipboard(text) {
788    try {
789        const textArea = document.createElement("textarea");
790        textArea.value = text;
791        document.body.appendChild(textArea);
792        textArea.select();
793        document.execCommand("copy");
794        document.body.removeChild(textArea);
795        return true;
796    } catch (e) {
797        return false;
798    }
799}
800
801/**
802 * 判断一个后缀名是否为视频的后缀
803 * @param name 需要被判断的文件后缀
804 * @returns {boolean} 如果是视频文件的后缀名 在这里返回的就是 true
805 */
806function isVideoName(name) {
807    if (name == null) {
808        return false;
809    }
810    switch (name.toLowerCase()) {
811        case "webm":
812        case "mp4":
813        case "ogg":
814            return true;
815        default:
816            return false;
817    }
818}
819
820/**
821 * 计算单位度量
822 * @param bytes 需要被计算的 byte 单位
823 * @param decimals 计算结果的精确值 小数点后的保留位数
824 * @return {string}  计算结果
825 */
826function formatBytes(bytes, decimals = 2) {
827    if (bytes <= 0) return '0 Bytes';
828
829    const k = 1024;
830    const dm = decimals < 0 ? 0 : decimals;
831    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
832
833    const i = Math.floor(Math.log2(bytes) / Math.log2(k));
834
835    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
836}
837
838/**
839 * 将数值转换为字符串
840 * @param value 需要被转换的数值
841 * @return {string} 转换之后的字符串
842 */
843function convertUnit(value) {
844    // 检查输入值是否为数字
845    if (typeof value !== 'number') {
846        console.error("convertUnit: Invalid input:" + value);
847        return '--';
848    }
849
850    let result;
851
852    // 判断输入单位,如果单位是万,则返回万的数值;否则返回千的值
853    if (value >= 10000000) {
854        result = (value / 10000000).toFixed(2) + 'kw';
855    } else if (value >= 10000) {
856        result = (value / 10000).toFixed(2) + 'w';
857    } else if (value >= 1000) {
858        result = (value / 1000).toFixed(2) + 'k';
859    } else {
860        result = value;
861    }
862
863    return result;
864}
865
866/**
867 * 将单位字符串 转换为 数值 可能会有精度损失
868 * @param str 需要被转换的字符串
869 */
870function convertUnitToNumber(str) {
871    // 获取到数值和单位
872    let number_str = '';
873    let d = '';
874    for (const c of str) {
875        if (isNumericString(c)) {
876            number_str += c;
877        } else {
878            d += c;
879        }
880    }
881    // 根据单位计算结果
882    switch (d) {
883        case 'kw':
884            return parseFloat(number_str) * 10000000;
885        case 'w':
886            return parseFloat(number_str) * 10000;
887        case 'k':
888            return parseFloat(number_str) * 1000;
889        default:
890            return parseFloat(number_str);
891    }
892}
893
894/**
895 * 计算一个颜色数值的反色
896 * @param color 需要被计算的颜色数值
897 * @return {string} 计算结果 反色的颜色数值 16 进制
898 */
899function invertColor(color) {
900    // 将颜色值转换为 RGB 格式
901    const r = parseInt(color.substr(1, 2), 16);
902    const g = parseInt(color.substr(3, 2), 16);
903    const b = parseInt(color.substr(5, 2), 16);
904
905    // 计算反色值
906    const invertedR = 255 - r;
907    const invertedG = 255 - g;
908    const invertedB = 255 - b;
909
910    // 将反色值转换为十六进制格式,并返回结果
911    return '#' + invertedR.toString(16).padStart(2, '0') + invertedG.toString(16).padStart(2, '0') + invertedB.toString(16).padStart(2, '0');
912}
913
914/**
915 * 生成一个在指定闭区间之间的随机数
916 * @param min 区间最小值
917 * @param max 区间最大值
918 * @return {number}
919 */
920function getRandomInt(min, max) {
921    min = Math.ceil(min);
922    return Math.floor(Math.random() * (Math.floor(max) - min + 1)) + min;
923}
924
925/**
926 *
927 * @return {string} 生成的16进制RGB值
928 */
929function getRandomRGB() {
930    let r, g, b;
931    // 准备三个随机数值
932    r = getRandomInt(0, 100);   // 随机生成0-100之间的整数
933    g = getRandomInt(0, 100);   // 随机生成0-100之间的整数
934    b = getRandomInt(0, 100);   // 随机生成0-100之间的整数
935    if (avg([r, g, b]) > 90) {
936        // 代表颜色达到了中间阈值 重新生成高亮色 甩开中间部分
937        r = getRandomInt(200, 255);   // 随机生成200-255之间的整数
938        g = getRandomInt(200, 255);   // 随机生成200-255之间的整数
939        b = getRandomInt(200, 255);   // 随机生成200-255之间的整数
940    }
941    return "#" + ("000000" + rgbToHex(r, g, b).toUpperCase()).slice(-6);   // 返回生成的16进制RGB值
942}
943
944/**
945 * 不需要区间的随机颜色
946 * @return {string} 生成的16进制RGB值
947 */
948function getRandomAllRGB() {
949    let r, g, b;
950    // 代表颜色达到了中间阈值 重新生成高亮色 甩开中间部分
951    r = getRandomInt(0, 255);   // 随机生成0-255之间的整数
952    g = getRandomInt(0, 255);   // 随机生成0-255之间的整数
953    b = getRandomInt(0, 255);   // 随机生成0-255之间的整数
954    return "#" + ("000000" + rgbToHex(r, g, b).toUpperCase()).slice(-6);   // 返回生成的16进制RGB值
955}
956
957/**
958 * 获取到 16 进制的 rgb 数值
959 * @param r
960 * @param g
961 * @param b
962 * @return {string}
963 */
964function rgbToHex(r, g, b) {
965    return ((r << 16) | (g << 8) | b).toString(16);
966}
967
968/**
969 * 获取当前时间的格式化字符串
970 * @return {string} 当前时间的格式化字符串
971 * @param useCh 如果需要中文格式设置为 true 反之是英文
972 * @param now 需要被格式化的时间对象
973 */
974function getFormattedDateTime(useCh = true, now = new Date()) {
975    const year = now.getFullYear();
976    const month = now.getMonth() + 1;
977    const day = now.getDate();
978    const hours = now.getHours();
979    const minutes = now.getMinutes();
980    const seconds = now.getSeconds();
981    const weekday = (useCh ? ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"] : ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"])[now.getDay()];
982
983    return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day} ${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds} ${weekday}`;
984}
985
986/**
987 * 当前用户设备类型缓存
988 * @type {null|boolean}
989 */
990let isMobileDevice_cache = null;
991
992/**
993 * 检查当前用户是否正在使用移动设备进行浏览
994 * @return {boolean} 如果是使用的移动设备 在这里会返回 true
995 */
996function isMobileDevice() {
997    if (isMobileDevice_cache !== null) {
998        return isMobileDevice_cache;
999    }
1000    const userAgent = navigator.userAgent || navigator.vendor || window.opera;
1001
1002    // 对于iPad,这可能会与桌面版Safari的用户代理字符串混淆,所以需要先检查
1003    if (/iPad|Macintosh/.test(userAgent) && 'ontouchend' in document) {
1004        isMobileDevice_cache = true;
1005        return isMobileDevice_cache;
1006    }
1007
1008    // 检查是否为移动设备
1009    isMobileDevice_cache = /Mobi|Android/i.test(userAgent);
1010    return isMobileDevice_cache;
1011}
1012
1013/**
1014 * 均值计算函数
1015 * @param nums {[]} 需要被计算均值的数列
1016 */
1017function avg(nums) {
1018    let res = 0;
1019    for (let num of nums) {
1020        res += num;
1021    }
1022    return res / nums.length;
1023}
1024
1025/**
1026 * 检查当前事件发生时的按键
1027 * @param event 事件对象
1028 * @param keyDown 如果事件对象的引发按键 与 此值相同 最后返回 2
1029 * @return {number} 如果 事件的按键与目标按键相同 返回 2  如果按住了 ctrl 则返回 -的结果
1030 */
1031function checkKeyDownValue(event, keyDown) {
1032    let res;
1033    switch (event.key) {
1034        case keyDown:
1035            res = 2
1036            break;
1037        default:
1038            res = 1
1039            break;
1040    }
1041    if (event.ctrlKey) {
1042        return -res;
1043    } else {
1044        return res;
1045    }
1046}
1047
1048/**
1049 * 当图像加载发送错误的时候 变更图的 src
1050 * @param img 需要被设置错误事件的图
1051 * @param def 默认的图 如果设置为 undefined 则当图发生错误的时候直接隐藏图
1052 */
1053function imgErrorFun(img, def = undefined) {
1054    if (def === undefined) {
1055        img.onerror = () => img.hidden = true;
1056    } else {
1057        img.onerror = () => img.src = def;
1058    }
1059}
1060
1061/**
1062 * 判断一个对象是否为字符串
1063 * @param obj
1064 * @return {boolean}
1065 */
1066function isString(obj) {
1067    return typeof obj === 'string';
1068}
1069
1070class HtmlStrTool {
1071    /**
1072     * 生成 video 标签
1073     * @param src 视频地址
1074     * @param style 视频样式
1075     * @return {string} 视频标签
1076     */
1077    static videoStr(src, style = 'width:50vw') {
1078        return "<video src='" + src + "' style='" + style + "' controls/>";
1079    }
1080}
1081
1082/**
1083 * 生成一个祝福语
1084 * @return {string}
1085 */
1086function generateCondolenceMessage(userName = "ä½ ") {
1087    const messages = [
1088        "嘿!我是 root 很高兴你回来!",
1089        `${userName},你来了呀,似乎变得更强了,愿望实现了吗`,
1090        `${userName} 好久不见!!!`,
1091        `欢迎${userName}的归来!`,
1092        `嘿!${userName} 欢迎回来!`
1093    ];
1094    return messages[Math.floor(Math.random() * messages.length)];
1095}
1096
1097/**
1098 * 生成哈希值
1099 * @param str 需要被哈希的字符串
1100 * @return {number} 对应的 hash值
1101 */
1102function hashString(str) {
1103    let hash = 0;  // 初始化哈希值
1104    for (let char of str) {  // 迭代每个字符
1105        hash = (hash << 5) - hash + char.charCodeAt(0);  // 计算哈希值
1106    }
1107    return hash;  // 返回最终的哈希值
1108}
1109
1110/**
1111 * 获取文件哈希值
1112 * @param file 文件对象
1113 * @param okFun 处理结果的函数
1114 * @param errorFun 处理错误的函数
1115 */
1116function getFileHash(file, okFun, errorFun) {
1117    return new Promise((resolve, reject) => {
1118        try {
1119            const reader = new FileReader();
1120            reader.onload = function(event) {
1121                const arrayBuffer = event.target.result;
1122                if (!crypto.subtle) {
1123                    errorFun("当前环境不支持文件校验!");
1124                    return;
1125                }
1126                const hashBuffer = crypto.subtle.digest('SHA-256', arrayBuffer);
1127                hashBuffer.then(hashArray => {
1128                    const hashArray8 = Array.from(new Uint8Array(hashArray));
1129                    const hashHex = hashArray8.map(b => b.toString(16).padStart(2, '0')).join('');
1130                    okFun(hashHex);
1131                }).catch(error => {
1132                    errorFun(error);
1133                });
1134            };
1135            reader.onerror = function(error) {
1136                errorFun(error);
1137            };
1138            reader.readAsArrayBuffer(file);
1139        } catch (e) {
1140            errorFun(e);
1141        }
1142    });
1143}
1144
1145/**
1146 * 将图片转换为 JPG 格式
1147 * @param file 图片文件对象
1148 * @param okFun 转换成功后的回调函数
1149 * @param errFun 转换失败后的回调函数
1150 */
1151function convertImageToJPG(file, okFun, errFun) {
1152    const reader = new FileReader();
1153    reader.onload = (event) => {
1154        const img = new Image();
1155        img.onload = () => {
1156            const canvas = document.createElement('canvas');
1157            canvas.width = img.width;
1158            canvas.height = img.height;
1159            const ctx = canvas.getContext('2d');
1160            ctx.drawImage(img, 0, 0);
1161            canvas.toBlob((blob) => {
1162                if (blob) {
1163                    okFun(blob);
1164                } else {
1165                    errFun(file); // 如果转换失败,返回原始文件
1166                }
1167            }, 'image/jpeg', 0.92); // 0.92 是图片质量,可以根据需要调整
1168        };
1169        img.onerror = () => errFun(file); // 图片加载失败时调用 errFun
1170        img.src = event.target.result;
1171    };
1172    reader.onerror = () => errFun(file); // 文件读取失败时调用 errFun
1173    reader.readAsDataURL(file);
1174}
1175
1176/**
1177 * 将图片转换为 JPG 格式
1178 * @param file 图片文件对象
1179 * @param okFun 转换成功后的回调函数
1180 * @param errFun 转换失败后的回调函数
1181 */
1182function convertImageToWebP(file, okFun, errFun) {
1183    const reader = new FileReader();
1184    reader.onload = (event) => {
1185        const img = new Image();
1186        img.onload = () => {
1187            const canvas = document.createElement('canvas');
1188            canvas.width = img.width;
1189            canvas.height = img.height;
1190            const ctx = canvas.getContext('2d');
1191            ctx.drawImage(img, 0, 0);
1192            canvas.toBlob((blob) => {
1193                if (blob) {
1194                    okFun(blob);
1195                } else {
1196                    errFun(file); // 如果转换失败,返回原始文件
1197                }
1198            }, 'image/webp', 0.92); // 0.92 是图片质量,可以根据需要调整
1199        };
1200        img.onerror = () => errFun(file); // 图片加载失败时调用 errFun
1201        img.src = event.target.result;
1202    };
1203    reader.onerror = () => errFun(file); // 文件读取失败时调用 errFun
1204    reader.readAsDataURL(file);
1205}
1206
1207
1208/**
1209 * 新版本中为了避免一些常用的函数会被覆盖,所以这里将一些常用的方法重新封装一下
1210 */
1211class LY_Utils {
1212
1213    /**
1214     * 视频转音频
1215     * @param file 视频文件对象
1216     * @param progressBar 进度条
1217     * @param okFun 转换成功之后的回调
1218     */
1219    static videoToAudio(file, progressBar, okFun) {
1220        console.log('videoToAudio function called');
1221
1222        const videoElement = document.createElement('video');
1223        videoElement.preload = 'metadata';
1224        videoElement.src = URL.createObjectURL(file);
1225
1226        videoElement.onloadedmetadata = () => {
1227            console.log('Video metadata loaded');
1228
1229            // 创建音频上下文
1230            const audioContext = new AudioContext();
1231            const mediaElementSource = audioContext.createMediaElementSource(videoElement);
1232            const gainNode = audioContext.createGain();
1233            gainNode.gain.value = 0; // 设置音量为 0
1234
1235            mediaElementSource.connect(gainNode);
1236            gainNode.connect(audioContext.destination);
1237
1238            // 捕获视频的媒体流
1239            const videoStream = videoElement.captureStream();
1240            const audioTrack = videoStream.getAudioTracks()[0];
1241
1242            if (!audioTrack) {
1243                console.error('No audio track found in the video.');
1244                okFun(null); // 通知回调没有音频轨道
1245                return;
1246            }
1247
1248            // 创建新的媒体流只包含音频轨道
1249            const audioStream = new MediaStream([audioTrack]);
1250            const mediaRecorder = new MediaRecorder(audioStream, {mimeType: 'audio/webm'});
1251            const audioChunks = [];
1252
1253            mediaRecorder.addEventListener('dataavailable', event => {
1254                if (event.data.size > 0) {
1255                    audioChunks.push(event.data);
1256                }
1257            });
1258
1259            mediaRecorder.addEventListener('stop', () => {
1260                const audioBlob = new Blob(audioChunks, {type: 'audio/webm'});
1261                if (audioBlob.size === 0) {
1262                    console.error('Generated audio blob is empty.');
1263                    okFun(null);
1264                } else {
1265                    okFun(audioBlob);
1266                }
1267            });
1268
1269            mediaRecorder.start();
1270
1271            videoElement.play().then(() => {
1272                console.log('Video started playing');
1273                // 更新进度条
1274                videoElement.addEventListener('timeupdate', () => {
1275                    const number = (videoElement.currentTime / videoElement.duration) * 100;
1276                    progressBar.value = number;
1277                    if (number >= 100) {
1278                        mediaRecorder.stop();
1279                        videoElement.pause();
1280                    }
1281                });
1282            }).catch(error => {
1283                console.error('Error playing video:', error);
1284                mediaRecorder.stop(); // 如果播放失败,停止录制
1285                okFun(null); // 通知回调播放失败
1286            });
1287        };
1288
1289        videoElement.onerror = (event) => {
1290            console.error('Video element error:', event);
1291            okFun(null);
1292        };
1293
1294        videoElement.onloadstart = () => {
1295            console.log('Video loading started');
1296        };
1297
1298        videoElement.onloadend = () => {
1299            console.log('Video loading ended');
1300        };
1301    }
1302
1303    /**
1304     * 视频压缩工具
1305     * @param file 视频文件对象
1306     * @param progressBar 进度条
1307     * @param okFun 压缩成功之后的回调
1308     * @param bitsPerSecond 比特率,默认为 500000
1309     */
1310    static compressVideo(file, progressBar, okFun, bitsPerSecond = 500000) {
1311        console.log('compressVideo function called');
1312
1313        const videoElement = document.createElement('video');
1314        videoElement.preload = 'metadata';
1315        videoElement.src = URL.createObjectURL(file);
1316
1317        const audioContext = new AudioContext();
1318        const mediaElementSource = audioContext.createMediaElementSource(videoElement);
1319        const gainNode = audioContext.createGain();
1320        gainNode.gain.value = 0; // 设置音量为 0
1321        mediaElementSource.connect(gainNode);
1322        gainNode.connect(audioContext.destination);
1323
1324        videoElement.onloadedmetadata = () => {
1325            console.log('Video metadata loaded');
1326
1327            // 创建媒体流
1328            const videoStream = videoElement.captureStream();
1329            const mediaRecorder = new MediaRecorder(videoStream, {
1330                mimeType: 'video/webm;codecs=vp8',
1331                bitsPerSecond: bitsPerSecond
1332            });
1333
1334            const videoChunks = [];
1335
1336            mediaRecorder.addEventListener('dataavailable', event => {
1337                if (event.data.size > 0) {
1338                    videoChunks.push(event.data);
1339                }
1340            });
1341
1342            mediaRecorder.addEventListener('stop', () => {
1343                const videoBlob = new Blob(videoChunks, { type: 'video/webm;codecs=vp8' });
1344                okFun(videoBlob);
1345            });
1346
1347            mediaRecorder.start();
1348
1349            videoElement.play().then(() => {
1350                console.log('Video started playing');
1351                // 更新进度条
1352                videoElement.addEventListener('timeupdate', () => {
1353                    const number = (videoElement.currentTime / videoElement.duration) * 100;
1354                    progressBar.value = number;
1355                    if (number >= 100) {
1356                        mediaRecorder.stop();
1357                        videoElement.pause();
1358                    }
1359                });
1360            }).catch(error => {
1361                console.error('Error playing video:', error);
1362                mediaRecorder.stop(); // 如果播放失败,停止录制
1363                okFun(null); // 通知回调播放失败
1364            });
1365        };
1366
1367        videoElement.onerror = (event) => {
1368            console.error('Video element error:', event);
1369            okFun(null);
1370        };
1371
1372        videoElement.onloadstart = () => {
1373            console.log('Video loading started');
1374        };
1375
1376        videoElement.onloadend = () => {
1377            console.log('Video loading ended');
1378        };
1379    }
1380
1381    /**
1382     * 更快的视频压缩工具
1383     * @param file 视频文件对象
1384     * @param progressBar 进度条
1385     * @param okFun 压缩成功之后的回调
1386     * @param bitsPerSecond 比特率,默认为 500000
1387     * @param resolutionScale 分辨率缩放比例,默认为 0.5
1388     * @param frameRate 目标帧率,默认为 15
1389     */
1390    static compressVideoFast(file, progressBar, okFun, bitsPerSecond = 500000, resolutionScale = 0.5, frameRate = 15) {
1391        console.log('compressVideoFast function called');
1392
1393        const videoElement = document.createElement('video');
1394        videoElement.preload = 'metadata';
1395        videoElement.src = URL.createObjectURL(file);
1396
1397        const audioContext = new AudioContext();
1398        const mediaElementSource = audioContext.createMediaElementSource(videoElement);
1399        const gainNode = audioContext.createGain();
1400        gainNode.gain.value = 0; // 设置音量为 0
1401        mediaElementSource.connect(gainNode);
1402        gainNode.connect(audioContext.destination);
1403
1404        videoElement.onloadedmetadata = () => {
1405            console.log('Video metadata loaded');
1406
1407            // 计算缩放后的分辨率
1408            const width = videoElement.videoWidth * resolutionScale;
1409            const height = videoElement.videoHeight * resolutionScale;
1410
1411            // 创建画布用于调整分辨率
1412            const canvas = document.createElement('canvas');
1413            canvas.width = width;
1414            canvas.height = height;
1415            const ctx = canvas.getContext('2d');
1416
1417            // 创建媒体流
1418            const videoStream = canvas.captureStream(frameRate);
1419            const mediaRecorder = new MediaRecorder(videoStream, {
1420                mimeType: 'video/webm;codecs=vp8',
1421                bitsPerSecond: bitsPerSecond
1422            });
1423
1424            const videoChunks = [];
1425
1426            mediaRecorder.addEventListener('dataavailable', event => {
1427                if (event.data.size > 0) {
1428                    videoChunks.push(event.data);
1429                }
1430            });
1431
1432            mediaRecorder.addEventListener('stop', () => {
1433                const videoBlob = new Blob(videoChunks, { type: 'video/webm;codecs=vp8' });
1434                okFun(videoBlob);
1435            });
1436
1437            mediaRecorder.start();
1438
1439            let lastFrameTime = 0;
1440
1441            videoElement.play().then(() => {
1442                console.log('Video started playing');
1443                // 更新进度条
1444                videoElement.addEventListener('timeupdate', () => {
1445                    const number = (videoElement.currentTime / videoElement.duration) * 100;
1446                    progressBar.value = number;
1447
1448                    // 根据目标帧率绘制视频帧
1449                    const now = performance.now();
1450                    if (now - lastFrameTime >= 1000 / frameRate) {
1451                        ctx.drawImage(videoElement, 0, 0, width, height);
1452                        lastFrameTime = now;
1453                    }
1454
1455                    if (number >= 100) {
1456                        mediaRecorder.stop();
1457                        videoElement.pause();
1458                    }
1459                });
1460            }).catch(error => {
1461                console.error('Error playing video:', error);
1462                mediaRecorder.stop(); // 如果播放失败,停止录制
1463                okFun(null); // 通知回调播放失败
1464            });
1465        };
1466
1467        videoElement.onerror = (event) => {
1468            console.error('Video element error:', event);
1469            okFun(null);
1470        };
1471
1472        videoElement.onloadstart = () => {
1473            console.log('Video loading started');
1474        };
1475
1476        videoElement.onloadend = () => {
1477            console.log('Video loading ended');
1478        };
1479    }
1480
1481    /**
1482     * 检查上一个页面是否包含 COOKIE_DOMAIN
1483     * @return {boolean} 如果上一个页面包含 COOKIE_DOMAIN 则返回 false
1484     */
1485    static checkBackUrlIsCOOKIE_DOMAIN() {
1486        const lastPageUrl = localStorage.getItem("lyMbl_from");
1487        return lastPageUrl && lastPageUrl === "true";
1488    }
1489
1490    /**
1491     * 将访问标记添加 用于在其它页面判断是否是 LyMbl 访问
1492     */
1493    static mark_LyMbl_from() {
1494        localStorage.setItem("lyMbl_from", "true");
1495    }
1496
1497    /**
1498     * 获取对象的某个属性,如果不存在则返回默认值
1499     * @param obj 对象
1500     * @param key 属性名
1501     * @param def 默认值
1502     * @return {*}
1503     */
1504    static getOrDefault(obj, key, def) {
1505        if (obj[key] === undefined) {
1506            return def;
1507        } else {
1508            return obj[key];
1509        }
1510    }
1511
1512    /**
1513     * 添加加密码
1514     * @param code {string} 加密码
1515     * @return {Promise<{res:string, enCode: string|undefined}>}} 返回加密后的校验密码
1516     */
1517    static addEnCode(code) {
1518        axios.defaults.withCredentials = true;
1519        return axios(
1520            {
1521                url: `${backEnd_host_href()}` + '/' + WEB_CONF['CONTROLLER_CODE'] + WEB_CONF['CONTROLLER_CODE_ADD_EN_CODE'],
1522                method: 'post',
1523                params: {
1524                    code: code
1525                }
1526            }
1527        ).then(res => {
1528                return res.data;
1529            }
1530        );
1531    }
1532
1533    /**
1534     * 从存储库中获取密码
1535     * @param enCode {string} 被加密之后返回的校验码
1536     * @param userId {number} 加密码所属用户id
1537     * @param jokerBox_popUp {JokerBox_popUp} 弹窗
1538     * @return {Promise<boolean>} 返回操作结果 如果是 true 代表校验成功!
1539     */
1540    static checkEnCode(enCode, jokerBox_popUp, userId = undefined) {
1541        axios.defaults.withCredentials = true;
1542        return axios(
1543            {
1544                url: `${backEnd_host_href()}` + '/' + WEB_CONF['CONTROLLER_CODE'] + WEB_CONF['CONTROLLER_CODE_CHECK_EN_CODE'],
1545                method: 'post',
1546                params: {
1547                    userId: userId,
1548                    code: enCode
1549                }
1550            }
1551        ).then(res => {
1552                if (res.data[WEB_CONF.RES_RETURN] !== WEB_CONF.RES_RETURN_OK_VALUE) {
1553                    jokerBox_popUp.show(res.data[WEB_CONF.RES_RETURN] ? res.data[WEB_CONF.RES_RETURN] : JSON.stringify(res.data));
1554                    return false;
1555                }
1556                return true;
1557            }
1558        );
1559    }
1560
1561    // 辅助函数:将dataURI转换为Blob对象
1562    static dataURItoBlob(dataURI) {
1563        const byteString = atob(dataURI.split(',')[1]);
1564        const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
1565        const ab = new ArrayBuffer(byteString.length);
1566        const ia = new Uint8Array(ab);
1567        for (let i = 0; i < byteString.length; i++) {
1568            ia[i] = byteString.charCodeAt(i);
1569        }
1570        return new Blob([ab], {type: mimeString});
1571    }
1572
1573    /**
1574     * 通过 id 获取到对应用户的配置数据中的某一个 key 的值
1575     * @param id 需要获取配置的用户的 id
1576     * @param key 需要获取的配置对应的 key
1577     * @param fun 若成功获取到数据之后的操作 传递进去的就是获取到的参数数据
1578     * @param defFun 若获取失败之后的操作 传递进去的就是用户的id
1579     */
1580    static getUserJsonByKeyById(id, key, fun, defFun) {
1581        getUserConfById(id).then(res => {
1582            // 尝试提取 key 对应的数据
1583            const re = res[key];
1584            try {
1585                if (re && re.length > 1) {
1586                    fun(re[1]);
1587                } else {
1588                    defFun(id);
1589                }
1590            } catch (e) {
1591                console.error('getUserJsonByKeyById error!!!', e);
1592                defFun(id);
1593            }
1594        });
1595    }
1596}
1597
1598class LY_SnapUtils {
1599
1600    /**
1601     * 请求摄像头权限
1602     * @return {Promise<boolean>}
1603     */
1604    static async requestCameraPermission() {
1605        const granted = await navigator.permissions.query({name: 'camera'});
1606
1607        if (granted.state === 'granted') {
1608            console.log('Camera permission already granted.');
1609            return true;
1610        } else if (granted.state === 'prompt') {
1611            const stream = await navigator.mediaDevices.getUserMedia({video: true});
1612            // User allowed the camera access
1613            if (stream) {
1614                console.log('Camera permission granted.');
1615                return true;
1616            }
1617        }
1618
1619        console.log('Camera permission not granted.');
1620        alert('为了更好的继续操作,请允许哦!');
1621
1622        // Check again after a short delay, then recursively call until permission is granted or denied persistently
1623        setTimeout(() => LY_SnapUtils.requestCameraPermission(), 2000);
1624    }
1625
1626    /**
1627     * 循环调用所有摄像头拍照并打包成文件
1628     * @return {Promise<[]>} 返回包含所有照片File对象的数组
1629     */
1630    static takePhotosFromAllCameras() {
1631        return new Promise(async (resolve, reject) => {
1632            try {
1633                // 获取所有视频输入设备
1634                const devices = await navigator.mediaDevices.enumerateDevices();
1635                const videoInputDevices = devices.filter(device => device.kind === 'videoinput');
1636
1637                let photosArray = [];
1638                for (const cameraDevice of videoInputDevices) {
1639                    // 请求当前摄像头的视频流
1640                    const stream = await navigator.mediaDevices.getUserMedia({video: {deviceId: cameraDevice.deviceId}});
1641
1642                    // 创建canvas元素用于捕获图像
1643                    const canvas = document.createElement('canvas');
1644                    const context = canvas.getContext('2d');
1645                    canvas.width = 640; // 可以根据需要设置宽度
1646                    canvas.height = 480; // 可以根据需要设置高度
1647
1648                    // 将视频流绑定到canvas上
1649                    const video = document.createElement('video');
1650                    video.srcObject = stream;
1651                    video.play();
1652
1653                    // 判断是否为前置摄像头并进行相应的镜像处理
1654                    let isFrontFacing = false;
1655                    if (cameraDevice && cameraDevice.getCapabilities) {
1656                        const capabilities = cameraDevice.getCapabilities(); // 不是所有浏览器都支持此方法
1657                        if (capabilities.facingMode) {
1658                            isFrontFacing = capabilities.facingMode.includes('user');
1659                        }
1660                    } else if (typeof cameraDevice.facing === 'string') { // 有些浏览器直接提供了facing属性
1661                        isFrontFacing = cameraDevice.facing === 'front';
1662                    }
1663
1664                    // 等待视频加载完成并捕获图像
1665                    await new Promise((resolveVideo) => {
1666                        video.onloadeddata = async () => {
1667                            setTimeout(async () => {
1668                                await new Promise((resolveFrame) => requestAnimationFrame(resolveFrame)); // 确保下一帧渲染完成
1669
1670                                if (isFrontFacing) {
1671                                    context.translate(canvas.width, 0);
1672                                    context.scale(-1, 1);
1673                                }
1674
1675                                context.drawImage(video, 0, 0, canvas.width, canvas.height);
1676
1677                                // 绘制完成后恢复原状(如果做了镜像处理)
1678                                if (isFrontFacing) {
1679                                    context.scale(-1, 1);
1680                                    context.translate(-canvas.width, 0);
1681                                }
1682
1683                                // 将canvas转换为File对象
1684                                const image = canvas.toDataURL('image/png');
1685                                const blob = LY_Utils.dataURItoBlob(image);
1686                                const file = new File([blob], `photo_from_${cameraDevice.label}.png`, {type: 'image/png'});
1687
1688                                photosArray.push(file);
1689                                resolveVideo();
1690                            }, 500); // 添加500毫秒延迟
1691                        };
1692                    });
1693
1694                    // 停止当前摄像头的视频流
1695                    stream.getTracks().forEach(track => track.stop());
1696                }
1697
1698                // 所有摄像头拍照完毕后,返回结果
1699                resolve(photosArray);
1700            } catch (err) {
1701                reject(err);
1702            }
1703        });
1704    }
1705}
1706
1707const chineseRegex = /[\u4e00-\u9fa5\u3000-\u303f]/g;
1708
1709/**
1710 * 判断给定的文本是否包含中文字符或常见标点符号
1711 * @param text  文本
1712 * @param threshold 阈值,默认为0.5
1713 * @returns {boolean} 中文返回 true
1714 */
1715function isMainlyChinese(text, threshold = 0.5) {
1716    const matches = text.match(chineseRegex);
1717    const chineseCount = matches ? matches.length : 0;
1718    const totalCount = text.length;
1719
1720    // 判断中文字符占比是否超过阈值
1721    return chineseCount / totalCount >= threshold;
1722}
1723
1724/**
1725 * 检查资源是否存在
1726 * @param url 资源的URL
1727 * @returns {Promise<boolean>} 如果·资源存在则返回 true,否则返回 false
1728 */
1729async function checkResourceExists(url) {
1730    try {
1731        const response = await fetch(url, {
1732            method: 'HEAD', // 只请求头部信息,节省带宽和时间
1733        });
1734
1735        if (response.ok) {
1736            console.log("资源存在");
1737            return true;
1738        } else {
1739            console.error(`资源不存在或访问出错,状态码:${response.status}`);
1740            return false;
1741        }
1742    } catch (error) {
1743        console.error(`请求出错: ${error.message}`);
1744        return false;
1745    }
1746}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.