PageSourceSearch

https://www.diipoo.net/dist/front/static/base/base.js?v=641

js diipoo.net collected 2026-10-04 20:18:15 UTC 84,118 bytes, 2,390 lines download raw bytes

1// ==================== base.js ====================
2(function (global) {
3    'use strict';
4
5    // 私有路由配置
6    const ROUTES = [
7        {code: "/", text: "home"},
8        {code: "product", text: "product"},
9        {code: "collection", text: "collection"},
10        {code: "page", text: "page"},
11        {code: "blog_collection", text: "blog_collection"},
12        {code: "blog_article", text: "blog_article"},
13        {code: "search", text: "search"}
14    ];
15
16    /**
17     * 根据当前URL路径匹配路由
18     * @returns {String} 匹配到的text值,未匹配到返回undefined
19     */
20    function getCurrentRouteText() {
21        // 获取当前路径并规范化
22        const path = (global.location.pathname || '').replace(/^\/|\/$/g, '').toLowerCase();
23
24        // 特殊处理根路径
25        if (path === '' || path === 'en') {
26            const rootRoute = ROUTES.find(r => r.code === '/');
27            return rootRoute ? rootRoute.text : undefined;
28        }
29
30        // 查找匹配的路由
31        for (const route of ROUTES) {
32            if (route.code === '/') continue; // 跳过根路由
33
34            const routeCode = route.code.toLowerCase();
35            const regex = new RegExp(`(^|/)${routeCode}(/|$)`);
36
37            if (regex.test(path)) {
38                return route.text;
39            }
40        }
41
42        return undefined;
43    }
44
45    // 将方法安全地附加到全局对象
46    if (typeof global.getCurrentRouteText === 'undefined') {
47        global.getCurrentRouteText = getCurrentRouteText;
48    }
49
50})(typeof window !== 'undefined' ? window : this);
51/**
52 * Cookie 操作工具类
53 */
54window.Cookies = {
55    /**
56     * 设置 Cookie
57     * @param {string} name Cookie 名称
58     * @param {string} value Cookie 值
59     * @param {Object} [options] 配置选项
60     * @param {number} [options.expires] 过期天数
61     * @param {string} [options.path] 路径,默认为 '/'
62     * @param {string} [options.domain] 域名
63     * @param {boolean} [options.secure] 是否仅 HTTPS
64     * @param {string} [options.sameSite] SameSite 属性
65     */
66    set: function (name, value, options = {}) {
67        let cookieText = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
68
69        // 处理过期时间
70        if (options.expires) {
71            const date = new Date();
72            date.setTime(date.getTime() + options.expires * 24 * 60 * 60 * 1000);
73            cookieText += `; expires=${date.toUTCString()}`;
74        }
75
76        // 设置路径
77        cookieText += `; path=${options.path || '/'}`;
78
79        // 可选配置
80        if (options.domain) {
81            cookieText += `; domain=${options.domain}`;
82        }
83        if (options.secure) {
84            cookieText += '; secure';
85        }
86        if (options.sameSite) {
87            cookieText += `; samesite=${options.sameSite}`;
88        }
89
90        document.cookie = cookieText;
91    },
92
93    /**
94     * 获取 Cookie
95     * @param {string} name Cookie 名称
96     * @returns {string|null} Cookie 值,不存在时返回 null
97     */
98    get: function (name) {
99        const cookies = document.cookie.split('; ');
100        for (const cookie of cookies) {
101            const [cookieName, cookieValue] = cookie.split('=');
102            if (decodeURIComponent(cookieName) === name) {
103                return decodeURIComponent(cookieValue);
104            }
105        }
106        return null;
107    },
108
109    /**
110     * 删除 Cookie
111     * @param {string} name Cookie 名称
112     * @param {string} [path] 路径,默认为 '/'
113     * @param {string} [domain] 域名
114     */
115    remove: function (name, path = '/', domain) {
116        this.set(name, '', {
117            expires: -1,
118            path,
119            domain
120        });
121    }
122};
123
124
125/**
126 * 根据当前URL路径匹配路由并返回对应的text值
127 * @param {Array} routes 路由配置数组
128 * @returns {String} 匹配到的text值,未匹配到返回undefined
129 */
130function getRouteText(routes) {
131    // 获取当前路径并规范化(移除首尾斜杠)
132    const path = window.location.pathname.replace(/^\/|\/$/g, '').toLowerCase();
133
134    // 匹配路由
135    for (const route of routes) {
136        // 规范化路由代码(移除首尾斜杠)
137        const routeCode = route.code.replace(/^\/|\/$/g, '').toLowerCase();
138
139        // 特殊处理根路由
140        if (routeCode === '' && (path === '' || path === 'en')) {
141            return route.text;
142        }
143
144        // 创建正则表达式,匹配路由代码出现在路径末尾或前面有斜杠
145        const regex = new RegExp(`(^|/)${routeCode}(/|$)`);
146
147        // 检查是否匹配
148        if (regex.test(path)) {
149            return route.text;
150        }
151    }
152
153    // 没有匹配到任何路由
154    return undefined;
155}
156
157// 路由配置
158const routes = [
159    {code: "/", text: "home"},//首页
160    {code: "product", text: "product"},//商品详情
161    {code: "collection", text: "collection"},//商品专辑
162    {code: "page", text: "page"},//自定义页
163    {code: "blog_collection", text: "blog_collection"},//博客专辑
164    {code: "blog_article", text: "blog_article"},//博客文章
165    {code: "search", text: "search"}//搜索
166];
167
168/**
169 * 获取URL参数
170 * @returns
171 */
172function getQueryParams() {
173    const search = window.location.search;
174    const params = new URLSearchParams(search);
175    const result = {};
176
177    // 遍历所有参数
178    for (const [key, value] of params.entries()) {
179        result[key] = value;
180    }
181
182    return result;
183}
184
185/**
186 * 加法
187 * @param arg1
188 * @param arg2
189 * @returns
190 */
191function accAdd(arg1, arg2) {
192    var r1, r2, m;
193    try {
194        r1 = arg1.toString().split(".")[1].length
195    } catch (e) {
196        r1 = 0
197    }
198    ;
199    try {
200        r2 = arg2.toString().split(".")[1].length
201    } catch (e) {
202        r2 = 0
203    }
204    ;
205    m = Math.pow(10, Math.max(r1, r2));
206    return (arg1 * m + arg2 * m) / m;
207}
208
209/**
210 * 减法
211 * @param arg1
212 * @param arg2
213 * @returns
214 */
215function accSubtr(arg1, arg2) {
216    var r1, r2, m, n;
217    try {
218        r1 = arg1.toString().split(".")[1].length;
219    } catch (e) {
220        r1 = 0;
221    }
222    try {
223        r2 = arg2.toString().split(".")[1].length;
224    } catch (e) {
225        r2 = 0;
226    }
227    m = Math.pow(10, Math.max(r1, r2));
228    //动态控制精度长度
229    n = (r1 >= r2) ? r1 : r2;
230    return ((arg1 * m - arg2 * m) / m).toFixed(n);
231}
232
233/***
234 * 乘法,获取精确乘法的结果值
235 * @param arg1
236 * @param arg2
237 * @returns
238 */
239function accMul(arg1, arg2) {
240    var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
241    try {
242        m += s1.split(".")[1].length
243    } catch (e) {
244    }
245    ;
246    try {
247        m += s2.split(".")[1].length
248    } catch (e) {
249    }
250    ;
251    return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
252}
253
254/***
255 * 除法,获取精确乘法的结果值
256 * @param arg1
257 * @param arg2
258 * @returns
259 */
260
261//除
262function floatDiv(arg1, arg2) {
263    var t1 = 0, t2 = 0, r1, r2;
264    try {
265        t1 = arg1.toString().split(".")[1].length
266    } catch (e) {
267    }
268    try {
269        t2 = arg2.toString().split(".")[1].length
270    } catch (e) {
271    }
272
273    r1 = Number(arg1.toString().replace(".", ""));
274
275    r2 = Number(arg2.toString().replace(".", ""));
276    return (r1 / r2) * Math.pow(10, t2 - t1);
277}
278
279
280/**
281 * AJAX请求封装
282 * @param {Object} config 请求配置
283 * @param {string} config.baseURL 基础URL
284 * @param {number} config.timeout 超时时间(毫秒)
285 * @param {Object} config.headers 默认请求头
286 */
287// function createRequest(config = {}) {
288//     // 存储所有请求的控制器
289//     const activeRequests = new Map();
290//     // 默认配置
291//     const defaults = {
292//         baseURL: '',
293//         timeout: 10_000,
294//         headers: {
295//             'Content-Type': 'application/json'
296//         },
297//         responseType: 'json',
298//         credentials: 'include'
299//     };
300//
301//     // 合并配置
302//     const requestConfig = {
303//         ...defaults,
304//         ...config,
305//         headers: {
306//             ...defaults.headers,
307//             ...(config.headers || {})
308//         }
309//     };
310//
311//     // 拦截器容器
312//     const interceptors = {
313//         request: [],
314//         response: []
315//     };
316//
317//     // 添加拦截器
318//     function addInterceptor(type, handler) {
319//         if (interceptors[type]) {
320//             interceptors[type].push(handler);
321//         }
322//     }
323//
324//     /**
325//      * 发起请求主函数
326//      * @param {Object} options 请求配置
327//      * @returns {Promise}
328//      */
329//     async function request(options) {
330//         const controller = new AbortController();
331//         const requestId = Symbol('requestId');
332//         const timeoutId = setTimeout(() => controller.abort(), options.timeout || requestConfig.timeout);
333//
334//         // 存储控制器和定时器
335//         const requestInfo = {
336//             controller,
337//             timeoutId,
338//             promise: null
339//         };
340//         activeRequests.set(requestId, requestInfo);
341//
342//         try {
343//             // 合并配置
344//             const merged = {
345//                 ...requestConfig,
346//                 ...options,
347//                 headers: {
348//                     ...requestConfig.headers,
349//                     ...(options.headers || {})
350//                 }
351//             };
352//
353//             // 处理 baseURL
354//             if (merged.baseURL && !/^https?:\/\//.test(merged.url)) {
355//                 merged.url = merged.baseURL.replace(/\/$/, '') + '/' + merged.url.replace(/^\//, '');
356//             }
357//
358//             // 执行请求拦截器
359//             let chain = Promise.resolve(merged);
360//             interceptors.request.forEach(fn => {
361//                 chain = chain.then(fn);
362//             });
363//
364//             // 发起请求
365//             const fetchPromise = chain.then(processed => {
366//                 let {url, method = 'GET', data, headers, responseType} = processed;
367//
368//                 // 处理 GET 请求参数
369//                 if (method.toUpperCase() === 'GET' && data && typeof data === 'object') {
370//                     const qs = new URLSearchParams(data).toString();
371//                     url += (url.includes('?') ? '&' : '?') + qs;
372//                 }
373//
374//                 // 构造 fetch 选项
375//                 const fetchOptions = {
376//                     method: method.toUpperCase(),
377//                     headers,
378//                     signal: controller.signal,
379//                     credentials: requestConfig.credentials
380//                 };
381//
382//                 if (method.toUpperCase() !== 'GET' && data != null) {
383//                     if (headers['Content-Type']?.includes('application/json')) {
384//                         fetchOptions.body = JSON.stringify(data);
385//                     } else if (data instanceof FormData) {
386//                         fetchOptions.body = data;
387//                         delete fetchOptions.headers['Content-Type'];
388//                     } else {
389//                         fetchOptions.body = data;
390//                     }
391//                 }
392//
393//                 return fetch(url, fetchOptions)
394//                     .then(res => {
395//                         clearTimeout(timeoutId);
396//                         if (!res.ok) {
397//                             throw {status: res.status, statusText: res.statusText, res};
398//                         }
399//                         switch (responseType) {
400//                             case 'text':
401//                                 return res.text();
402//                             case 'blob':
403//                                 return res.blob();
404//                             case 'arrayBuffer':
405//                                 return res.arrayBuffer();
406//                             default:
407//                                 return res.json();
408//                         }
409//                     })
410//                     .then(resData => {
411//                         // 执行响应拦截器
412//                         let respChain = Promise.resolve(resData);
413//                         interceptors.response.forEach(fn => {
414//                             respChain = respChain.then(fn);
415//                         });
416//                         return respChain;
417//                     });
418//             });
419//
420//             // 存储 promise 引用
421//             requestInfo.promise = fetchPromise;
422//             return await fetchPromise;
423//         } catch (err) {
424//             if (err.name === 'AbortError') {
425//                 throw {message: `Request aborted`};
426//             }
427//             throw err;
428//         } finally {
429//             activeRequests.delete(requestId);
430//         }
431//     }
432//
433//     // 快捷方法
434//     ['get', 'post', 'put', 'delete'].forEach(method => {
435//         request[method] = function (url, data, opts = {}) {
436//             return request({
437//                 url,
438//                 method,
439//                 data,
440//                 ...opts
441//             });
442//         };
443//     });
444//
445//     // 拦截器注册
446//     request.interceptors = {
447//         request: {use: fn => addInterceptor('request', fn)},
448//         response: {use: fn => addInterceptor('response', fn)}
449//     };
450//
451//     // 取消单个请求
452//     request.cancel = function (promise) {
453//         for (const [id, {controller, promise: storedPromise}] of activeRequests) {
454//             if (promise === storedPromise) {
455//                 controller.abort();
456//                 activeRequests.delete(id);
457//                 break;
458//             }
459//         }
460//     };
461//
462//     // 取消所有请求
463//     request.cancelAll = function () {
464//         activeRequests.forEach(({controller}) => {
465//             controller.abort();
466//         });
467//         activeRequests.clear();
468//     };
469//
470//     return request;
471// }
472
473function createRequest(config = {}) {
474    const activeRequests = new Map();
475
476    const defaults = {
477        baseURL: '',
478        timeout: 10_000,
479        headers: {
480            'Content-Type': 'application/json'
481        },
482        responseType: 'json',
483        withCredentials: true,
484        async: true   // 接口请求是同步还是异步 默认 true异步
485    };
486
487    const requestConfig = {
488        ...defaults,
489        ...config,
490        headers: {
491            ...defaults.headers,
492            ...(config.headers || {})
493        }
494    };
495
496    const interceptors = {
497        request: [],
498        response: []
499    };
500
501    function addInterceptor(type, handler) {
502        if (interceptors[type]) {
503            interceptors[type].push(handler);
504        }
505    }
506
507    // ⭐修复:构建查询字符串的工具(支持数组与对象)
508    function buildQuery(obj) {
509        if (!obj) return '';
510        const parts = [];
511        Object.keys(obj).forEach(key => {
512            const val = obj[key];
513            if (val === null || typeof val === 'undefined') return;
514            if (Array.isArray(val)) {
515                val.forEach(v => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`));
516            } else if (typeof val === 'object') {
517                // 对象类型用 JSON 字符串化
518                parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(val))}`);
519            } else {
520                parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`);
521            }
522        });
523        return parts.join('&');
524    }
525
526    /**
527     * 发起请求主函数 (XHR 实现)
528     */
529    function request(options) {
530        const requestId = Symbol('requestId');
531        // 先放一个占位,后面会赋 xhr / timeoutId / promise
532        const requestInfo = {xhr: null, timeoutId: null, promise: null};
533        activeRequests.set(requestId, requestInfo);
534
535        const merged = {
536            ...requestConfig,
537            ...options,
538            headers: {
539                ...requestConfig.headers,
540                ...(options.headers || {})
541            }
542        };
543
544        // 处理 baseURL
545        if (merged.baseURL && !/^https?:\/\//.test(merged.url)) {
546            merged.url = merged.baseURL.replace(/\/$/, '') + '/' + merged.url.replace(/^\//, '');
547        }
548
549        // ⭐优化 任何请求发出前必须先确保 sessionId 已就绪(没有则先调用账号接口生成)
550        function ensureSessionReady() {
551            // 去掉查询参数和末尾斜杠,避免 /api/customer/account/info?xx=1 这类写法漏判
552            const normalizedUrl = (merged.url || '').split('?')[0].replace(/\/+$/, '');
553            if (normalizedUrl === '/api/customer/account/info') {
554                // 该接口本身用于获取/生成 sessionId,跳过等待,避免死循环
555                return Promise.resolve();
556            }
557            if (typeof requestConfig.ensureSession === 'function') {
558                return requestConfig.ensureSession();
559            }
560            return Promise.resolve();
561        }
562
563        // 执行请求拦截器(先确保 sessionId 就绪,再进拦截器,保证拦截器里能拿到最新 sessionId)
564        let chain = Promise.resolve()
565            .then(ensureSessionReady)
566            .then(() => merged);
567        interceptors.request.forEach(fn => {
568            chain = chain.then(fn);
569        });
570
571        // ⭐修复:在这里处理 GET 的 params/data(并把 body 使用 payload)
572        const xhrPromise = chain.then(processed => {
573            return new Promise((resolve, reject) => {
574                // 注意:同时支持 data 和 params(优先 data)
575                let {url, method = 'GET', data, params, headers, responseType, timeout} = processed;
576                const payload = (typeof data !== 'undefined') ? data : params; // ⭐修复:支持 params 别名
577                // ⚡新增逻辑:GET 且 format=page_html 时,如果当前页面 URL 含 preview_theme_id,则附加上
578                // console.log('如果当前页面 URL 含 preview_theme_id,则附加上', payload)
579                if (method.toUpperCase() === 'GET' && payload && payload.format) {
580                    try {
581                        const currentUrl = window.location.href;
582                        const match = currentUrl.match(/[?&]preview_theme_id=([^&]+)/);
583                        if (match && match[1] && !payload.preview_theme_id) {
584                            payload.preview_theme_id = match[1];
585                        }
586                    } catch (e) {
587                        console.warn('preview_theme_id check failed', e);
588                    }
589                }
590                // ⭐修复:如果是 GET,把 payload 序列化到 URL 上
591                if (method.toUpperCase() === 'GET' && payload && typeof payload === 'object') {
592                    const qs = buildQuery(payload);
593                    if (qs) {
594                        url += (url.includes('?') ? '&' : '?') + qs;
595                    }
596                }
597
598                const xhr = new XMLHttpRequest();
599                requestInfo.xhr = xhr;
600
601                xhr.open(method.toUpperCase(), url, merged.async);
602                xhr.timeout = timeout || requestConfig.timeout;
603                xhr.withCredentials = requestConfig.withCredentials;
604
605                // 设置 headers
606                for (const key in headers) {
607                    if (headers[key] != null) {
608                        xhr.setRequestHeader(key, headers[key]);
609                    }
610                }
611
612                // 设置 responseType(XHR 不直接支持 json,使用 responseText 解析)
613                if (['blob', 'arraybuffer', 'text'].includes(responseType)) {
614                    xhr.responseType = responseType;
615                }
616
617                xhr.onload = () => {
618                    clearTimeout(requestInfo.timeoutId);
619                    // 检查HTTP状态码是否在200-299之间(表示请求成功)
620                    if (xhr.status >= 200 && xhr.status < 300) {
621                        // 声明变量用于存储解析后的响应数据
622                        let result;
623
624                        // 尝试解析响应数据
625                        try {
626                            // 如果响应类型是JSON或未指定类型,则解析为JSON
627                            if (responseType === 'json' || !responseType) {
628                                // 如果有响应文本则解析为JSON,否则使用空对象
629                                result = xhr.responseText ? JSON.parse(xhr.responseText) : {};
630                            } else {
631                                // 非JSON类型(如文本、二进制等)直接使用原始响应
632                                result = xhr.response;
633                            }
634                        } catch (err) {
635                            // 如果JSON解析失败,返回解析错误
636                            return reject({message: 'Parse error:' + url, error: err, xhr});
637                        }
638                        if (options.returnFullResponse) {//如果需要返回完整的result信息,那么包上一层
639                            $log('需要返回完整信息')
640                            result = {
641                                ...result,//顺序不能乱,...result要在上面
642                                data: {
643                                    data: result,
644                                },
645
646                            }
647                        }
648                        // 将解析后的结果包装成Promise对
648象
649                        let respChain = Promise.resolve(result);
650
651                        // 遍历所有注册的响应拦截器
652                        interceptors.response.forEach(fn => {
653                            // 将拦截器串联起来,上一个拦截器的输出作为下一个的输入
654                            respChain = respChain.then(fn);
655                        });
656
657                        // 返回处理后的Promise链
658                        resolve(respChain);
659                    } else {
660                        // 如果HTTP状态码不在200-299范围内,返回错误信息
661                        reject({status: xhr.status, statusText: xhr.statusText, xhr});
662                    }
663                };
664
665                xhr.onerror = () => {
666                    clearTimeout(requestInfo.timeoutId);
667                    reject({message: 'Network error', xhr});
668                };
669
670                xhr.ontimeout = () => {
671                    reject({message: 'Request timeout', xhr});
672                };
673
674                // ⭐修复:发送数据时使用 payload(而不是只看 data)
675                if (method.toUpperCase() === 'GET' || payload == null) {
676                    xhr.send();
677                } else {
678                    if (
679                        headers['Content-Type'] &&
680                        headers['Content-Type'].includes('application/json') &&
681                        !(payload instanceof FormData)
682                    ) {
683                        xhr.send(JSON.stringify(payload));
684                    } else {
685                        xhr.send(payload);
686                    }
687                }
688
689                // 记录 timeoutId(虽然 XHR 有 xhr.timeout,但我们也跟踪)
690                // 注:上面使用 xhr.timeout + ontimeout 已够用,但保留此字段以兼容之前逻辑
691                requestInfo.timeoutId = null;
692            });
693        });
694
695        requestInfo.promise = xhrPromise;
696
697        return xhrPromise.finally(() => {
698            activeRequests.delete(requestId);
699        });
700    }
701
702    // 快捷方法
703    ['get', 'post', 'put', 'delete'].forEach(method => {
704        request[method] = function (url, data, opts = {}) {
705            return request({
706                url,
707                method,
708                data,
709                ...opts
710            });
711        };
712    });
713
714    // 拦截器注册
715    request.interceptors = {
716        request: {use: fn => addInterceptor('request', fn)},
717        response: {use: fn => addInterceptor('response', fn)}
718    };
719
720    // 取消单个请求
721    request.cancel = function (promise) {
722        for (const [id, {xhr, promise: storedPromise}] of activeRequests) {
723            if (promise === storedPromise && xhr) {
724                xhr.abort();
725                activeRequests.delete(id);
726                break;
727            }
728        }
729    };
730
731    // 取消所有请求
732    request.cancelAll = function () {
733        activeRequests.forEach(({xhr}) => {
734            if (xhr) xhr.abort();
735        });
736        activeRequests.clear();
737    };
738
739    return request;
740}
741
742
743// 发起请求
744// api.get('/users', { page: 1 })
745//     .then(res => console.log(res))
746//     .catch(err => console.error(err));
747
748
749// 发起请求
750// const req1 = api.get('/api/data1');
751// const req2 = api.post('/api/data2', { key: 'value' });
752
753// 取消单个请求
754// api.cancel(req1)
755
756// 取消所有请求
757// api.cancelAll();
758
759const CLIENT_TYPE = '40'
760let language_code = '' //语言
761
762//语言
763const LANGUAGE_CODE = window.baseData.language_code
764//国家
765const CURRENCY_CODE = window.baseData.currency_code
766/*
767 1.前端如果没有sessionId, 那么从cookie[ic_session_id]中读取, 如果存在,则直接使用该值,不需要调用api生成sessionId。如果不存在,则调用原来的api生成sessionId
768 2.如果前端有sessionId,那么写入 cookie[ic_session_id] (将session写入cookie,覆盖),注意cookie的网域为`'/'` ,如果存在
769 */
770
771// ⭐优化:获取最新 sessionId(localStorage 优先,其次 cookie[ic_session_id])
772function getCurrentSessionId() {
773    return localStorage.getItem('sessionId') || '';
774}
775
776// ⭐优化:fecify-session-id 不再写死,改为每次请求前在请求拦截器中动态读取最新值
777const api = createRequest({
778    baseURL: '',
779    timeout: 60000,//超时时间 1分钟
780    headers: {
781        'client-type': CLIENT_TYPE,
782        'language-code': LANGUAGE_CODE,//语言code
783        'currency-code': CURRENCY_CODE//国家code
784    },
785    ensureSession: getSessionFn, // ⭐新增:请求发出前自动判断/生成 sessionId(参考 Vue 项目 myAxios)
786    returnFullResponse: false  // 全局默认返回 response.data 如果为 true 将返回 所有的response 包含 code 码
787});
788
789
790// 添加请求拦截器
791api.interceptors.request.use(config => {
792    // ⭐优化:每次请求动态设置 fecify-session-id,确保请求用的是最新 sessionId
793    const sessionID = getCurrentSessionId();
794    if (config.url === '/api/customer/account/info') {
795        // 账号信息接口本身用于获取/生成 sessionId:有则带上,没有则空
796        config.headers['fecify-session-id'] = sessionID || '';
797    } else {
798        // 其它接口:有则带上,没有则用 fec 占位(sessionId 获取失败不阻塞请求)
799        config.headers['fecify-session-id'] = sessionID || '';
800    }
801    $log('请求发出前:', config);
802    return config;
803});
804
805// 添加响应拦截器
806api.interceptors.response.use(response => {
807    // $log('收到响应le ------:', response);
808    // 可以在这里处理响应数据
809    // 如果是 Blob
810    if (response instanceof Blob) {
811        $log('blob----------')
812        return {data: response};
813    }
814    // 判断 code 字段
815    // response 可能是对象(解析出来的 json),通常长这样:{code: 200, data: {...}, message: ''}
816    // 兼容后端各种情形
817
818    if (response && typeof response === 'object' && 'code' in response) {//是个正常的对象
819        if (response.code === 200) {
820            $log('正常流程', response,)
821            return response.data; // 正常流程
822        } else {
823            $log(' code 不为 200')
824            // code 不为 200,抛出错误~,让外面 catch,可以把整个 response 或 message 都抛出去
825            return Promise.reject(response); // 或者 Promise.reject(response.message)
826        }
827    } else {//连对象都不是 属于错误数据格式
828        return Promise.reject(response); // 或者 Promise.reject(response.message)
829
830    }
831    // 兼容以前逻辑
832    return response && response.data !== undefined ? response.data : response;
833});
834// 使用快捷方法
835// api.get('/users')
836//     .then(users => {
837//         console.log('用户列表:', users);
838//     });
839//
840// api.post('/users', { name: 'Alice', age: 25 })
841//     .then(result => {
842//         console.log('创建结果:', result);
843//     });
844
845
846// ⭐优化:共享同一个“session 等待”Promise,避免并发请求时重复轮询/重复请求账号接口
847let sessionReadyPromise = null
848
849// 获取sessionId
850function getSessionFn() {
851    // 已有 sessionId(localStorage 或 cookie[ic_session_id])直接返回
852    const sessionID = getCurrentSessionId()
853    if (sessionID) {
854        // cookie 有而 localStorage 没有时,回写 localStorage
855        if (!localStorage.getItem('sessionId')) {
856            localStorage.setItem('sessionId', sessionID)
857        }
858        return Promise.resolve(true)
859    }
860
861    // 多个请求同时进来时复用同一个等待中的 Promise
862    if (sessionReadyPromise) {
863        return sessionReadyPromise
864    }
865
866    sessionReadyPromise = new Promise(resolve => {
867        let Interval = 0
868        let InterTime = null
869
870        getAccountInfo()// 通过账号信息接口获取/生成 sessionId
871
872        clearInterval(InterTime)
873        InterTime = setInterval(() => {
874            const id = getCurrentSessionId()
875            if (id) {
876                // 获取成功:写入 localStorage(cookie 由 getAccountInfo 负责同步)
877                localStorage.setItem('sessionId', id)
878                clearInterval(InterTime)
879                Interval = 0
880                sessionReadyPromise = null
881                resolve(true)
882                return
883            }
884            if (Interval > 1000) {
885                // 最长等待 20 秒(1000 次 × 20ms),失败不阻塞请求,继续用占位 sessionId 发出
886                clearInterval(InterTime)
887                sessionReadyPromise = null
888                console.error('Failed to obtain the login status. Procedure')
889                resolve(false)
890                return
891            }
892            Interval++
893        }, 20)//每20毫秒 + 1   1000次 过了20秒钟  最长等待时间20秒
894    })
895
896    return sessionReadyPromise
897}
898
899// 页面加载时预取 sessionId(提前发起账号信息请求,后续请求基本能直接命中缓存)
900getSessionFn()
901
902// ================= 接口请求 ==================
903/**
904 * 获取账户信息
905 * @param {Object} store Vuex store 对象
906 * @returns {Promise} 返回账户信息的Promise
907 */
908function getAccountInfo(store) {
909
910    return new Promise(resolve => {
911
912
913        // 检查全局锁
914        if (window.globalLockP) {
915            clearTimeout(window.tempTimeout);
916            window.tempTimeout = setTimeout(() => {
917                if (window.globalLockP) {
918                    window.globalLockP = false; // 10秒自动解锁
919                }
920            }, 1000);
921            return resolve(null);//还在锁定状态,返回 null
922        }
923
924
925        // 上锁
926        window.globalLockP = true;
927        const currentTime = new Date().getTime();
928        const time = 1000 * 60 * 60 * 24; // 24小时过期
929        const loginStatus = localStorage.getItem('loginStatus');
930        const localStorageSessionID = localStorage.getItem('sessionId');
931        const ic_cookie_session_id = Cookies.get('ic_session_id') || '';
932        let sessionID = '';
933
934        // 处理 session ID
935        if (localStorageSessionID) {
936            sessionID = localStorageSessionID;
937            Cookies.set('ic_session_id', sessionID, {expires: 730, path: '/'});
938        } else if (ic_cookie_session_id) {
939            sessionID = ic_cookie_session_id;
940            localStorage.setItem('sessionId', sessionID);
941        }
942
943        const visit_country_code = Cookies.get('visit_country_code');
944
945        // 检查本地缓存是否有效
946        if (loginStatus && sessionID && visit_country_code) {
947            const cookieSessionID = Cookies.get('ff_session_id');
948            if (cookieSessionID !== sessionID) {
949                Cookies.set('ff_session_id', sessionID, {expires: 730, path: '/'});
950            }
951
952            const parsedLoginStatus = JSON.parse(loginStatus);
953            parsedLoginStatus.sessionId = sessionID;
954
955            if (parsedLoginStatus._expiration && (currentTime - parsedLoginStatus._expiration) < time) {
956                window.loginStatus = parsedLoginStatus;
957                window.globalLockP = false;
958                return resolve(window.loginStatus);
959            }
960        }
961
962
963        // 使用封装的API方法发起请求
964        api.get("/api/customer/account/info")
965            .then(res => {
966
967
968                res._expiration = currentTime;
969                const sessionID = localStorage.getItem('sessionId') || '';
970
971                if (sessionID) {
972                    res.sessionId = sessionID;
973                    const cookieSessionID = Cookies.get('ff_session_id');
974                    if (cookieSessionID !== sessionID) {
975                        Cookies.set('ff_session_id', sessionID, {expires: 730, path: '/'});
976                    }
977                }
978
979                const visit_country_code = res.visit_country_code || 'null';
980                const local_visit_country_code = Cookies.get(visit_country_code);
981
982                if (!local_visit_country_code || visit_country_code === 'null') {
983                    Cookies.set('visit_country_code', visit_country_code, {expires: 730, path: '/'});
984                }
985
986                const setItem = {
987                    isLogined: res.isLogined,
988                    sessionId: res.sessionId,
989                    customer: res.customer,
990                    _expiration: res._expiration
991                };
992
993                localStorage.setItem('loginStatus', JSON.stringify(setItem));
994                window.loginStatus = setItem;
995
996                if (res.is_block_access) {
997                    const blockAccessInfo = {
998                        is_block_access: res.is_block_access || '',
999                        _expiration: currentTime
1000                    };
1001                    sessionStorage.setItem('blockAccessInfo', JSON.stringify(blockAccessInfo));
1002                }
1003
1004                if (!sessionID) {
1005                    const id = res.sessionId;
1006                    localStorage.setItem('sessionId', id);
1007                    Cookies.set('ff_session_id', id, {expires: 730, path: '/'});
1008                    Cookies.set('ic_session_id', id, {expires: 730, path: '/'});
1009                }
1010
1011                window.globalLockP = false;
1012                resolve(window.loginStatus);
1013            })
1014            .catch(error => {
1015                // console.error('获取账户信息失败:', error);
1016
1017                const obj = {
1018                    isLogined: false,
1019                    customer: {
1020                        email: "",
1021                        first_name: "",
1022                        last_name: ""
1023                    },
1024                    _expiration: currentTime
1025                };
1026
1027                localStorage.setItem('loginStatus', JSON.stringify(obj));
1028                window.globalLockP = false;
1029                resolve(null);
1030            });
1031    });
1032}
1033
1034document.addEventListener('DOMContentLoaded', function () {
1035    function loadRealImage(img) {
1036        var realSrc = img.getAttribute('data-src');
1037        if (realSrc) {
1038            img.src = realSrc;
1039        }
1040    }
1041
1042    // 懒加载
1043    var imgs = document.querySelectorAll('.liquid-image-lazy');
1044    if ('IntersectionObserver' in window) {
1045        let observer = new IntersectionObserver(function (entries, obs) {
1046            entries.forEach(function (entry) {
1047                if (entry.isIntersecting) {
1048                    loadRealImage(entry.target);
1049                    obs.unobserve(entry.target);
1050                }
1051            });
1052        }, {rootMargin: '0px 0px 100px 0px'});
1053
1054        imgs.forEach(function (img) {
1055            observer.observe(img);
1056        });
1057    } else {
1058        // fallback: 直接加载
1059        imgs.forEach(loadRealImage);
1060    }
1061
1062    // 加载失败替换
1063    document.body.addEventListener('error', function (e) {
1064        var img = e.target;
1065        if (img.classList && img.classList.contains('liquid-image')) {
1066            var errImg = img.getAttribute('data-error-img');
1067            if (img.src !== errImg && errImg) {
1068                img.src = errImg;
1069            }
1070        }
1071    }, true);
1072});
1073
1074/**
1075 *
1076 * @url {string}  'https://xxxx.com/xxx/demo/type/1/id/2?search=123'
1077 * @param {string}  ' url里的key名'   'search'
1078 * @param_data  {string}  url里的key值    '456'
1079 * @returns {string}  'https://xxxx.com/xxx/demo/type/1/id/2?search=456'
1080 * 给url 增加跳转参数 也可以改变原有参数值
1081 */
1082function changeURLArg(url, param, param_data) {
1083    var pattern = param + '=([^&]*)';
1084    var replaceText = param + '=' + param_data;
1085    if (url.match(pattern)) {
1086        var tmp = '/(' + param + '=)([^&]*)/gi';
1087        tmp = url.replace(eval(tmp), replaceText);
1088        return tmp;
1089    } else {
1090        if (url.match('[\?]')) {
1091            return url + '&' + replaceText;
1092        } else {
1093            return url + '?' + replaceText;
1094        }
1095    }
1096}
1097
1098// 将字符串安全地转换为 JavaScript 数组,并在字符串不符合转换条件时返回空数组
1099function safeJsonStringToArray(jsonStr) {
1100    try {
1101        // 尝试解析 JSON 字符串
1102        const parsed = JSON.parse(jsonStr);
1103
1104        // 检查解析结果是否是一个数组
1105        if (Array.isArray(parsed)) {
1106            return parsed;
1107        } else {
1108            // 不是数组,返回空数组
1109            return [];
1110        }
1111    } catch (error) {
1112        // 解析失败,返回空数组
1113        return [];
1114    }
1115}
1116
1117/**
1118 *
1119 * @param val  {string}  判断是否是邮箱  邮件格式校验 邮箱格式校验
1120 * @returns {boolean}
1121 */
1122function testEmail(val) {
1123    // 不能出现连续的 ..
1124    if (val.includes('..')) {
1125        return false;
1126    }
1127    let reg = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
1128    return reg.test(val)
1129}
1130
1131function $log(...info) {//输出console 打印信息
1132    if (window.baseData && window.baseData.debug) {//开发环境
1133        console.log('[debug]', ...info)
1134    }
1135}
1136
1137// 着陆页
1138function get_fist_loading_url() {
1139    let fist_loading_url = sessionStorage.getItem('fist_loading_url') || null
1140    if (!fist_loading_url) {//查不到说明没有存储落地页
1141        sessionStorage.setItem('fist_loading_url', location.href)
1142        return location.href
1143    }
1144    return fist_loading_url //查到了返回落地页
1145}
1146
1147get_fist_loading_url()
1148
1149function getGclidQuery() {//是否存在参数gclid,如果存在,那么需要在事件 gtag("event  中加入  gclid:"xxxxxx"
1150    // 存储30天有效期
1151    const gclid = new URLSearchParams(window.location.search).get("gclid");//谷歌广告参数
1152    if (gclid) {
1153        Cookies.set(
1154            'gg_gclid',
1155            gclid,
1156            {expires: 30, path: '/'}//30天过期
1157        )
1158        // Cookies.get('gg_gclid')
1159    }
1160
1161}
1162
1163getGclidQuery()
1164
1165function fReloadAccountInfo() {//退出账户刷新状态
1166    return new Promise(resolve => {
1167        localStorage.removeItem('loginStatus')
1168        sessionStorage.removeItem('cart_qty')//推出登录了所以购物车也要重新刷新
1169        localStorage.removeItem('userIsAffiliate')//退出登录了网盟商删除
1170        resolve()
1171    })
1172}
1173
1174/*
1175* 改变浏览器 URL 上的 query 信息  如果 baidu.com?step=2   =>   baidu.com?step=3
1176*
1177*    setUrlVariant('step', this.mapCode[dataStep])
1178* */
1179function setUrlVariant(key, val) { //改变浏览器 URL 上的 query 信息  如果 baidu.com?step=2   =>   baidu.com?step=3
1180    $log('setUrlVariant', key, val)
1181
1182    function changeURLArg(url, param, param_data) {
1183        var pattern = param + '=([^&]*)';
1184        var replaceText = param + '=' + param_data;
1185        if (url.match(pattern)) {
1186            var tmp = '/(' + param + '=)([^&]*)/gi';
1187            tmp = url.replace(eval(tmp), replaceText);
1188            return tmp;
1189        } else {
1190            if (url.match('[\?]')) {
1191                return url + '&' + replaceText;
1192            } else {
1193                return url + '?' + replaceText;
1194            }
1195        }
1196    }
1197
1198    let newUrl = changeURLArg(window.location.href, key, val)
1199    history.replaceState("", "", newUrl);//替换为当前url
1200}
1201
1202//设备信息
1203function fOsInfo() {
1204    var ua = navigator.userAgent,
1205        isWindowsPhone = /(?:Windows Phone)/.test(ua),
1206        isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone,
1207        isAndroid = /(?:Android)/.test(ua),
1208        isFireFox = /(?:Firefox)/.test(ua),
1209        isChrome = /(?:Chrome|CriOS)/.test(ua),
1210        isSafari = ua.indexOf("Safari") != -1 && ua.indexOf("Version") != -1, //通过浏览器,机型和touch事件判断
1211        isIphone = ua.indexOf("iPhone") != -1 && ua.indexOf("Version") != -1,
1212        isIPad = isSafari && !isIphone && !isAndroid && 'ontouchend' in document,
1213        isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (isFireFox &&
1214            /(?:Tablet)/.test(ua)) || isIPad,
1215        isPhone2 = /(?:iPhone)/.test(ua) && !isTablet,
1216        isPc = !isPhone2 && !isAndroid && !isSymbian;
1217
1218    return {
1219        isPad: isTablet,
1220        isPhone: isPhone2,
1221        isAndroid: isAndroid,
1222        isPc: isPc
1223    };
1224}
1225
1226// 判断是否为微信浏览器
1227function isWeixinBrowser() {
1228    let ua = navigator.userAgent.toLowerCase();
1229    return /micromessenger/.test(ua);
1230}
1231
1232function $getAddonsPerm(addonsName) { //  使用方法 $getAddonsPerm.call(this,插件名)
1233    let addons = window?.baseData?.addons || []
1234    return addons.includes(addonsName)
1235}
1236
1237function browserJudge(needversion = false) { //浏览器agent
1238    var document = window.document,
1239        navigator = window.navigator,
1240        agent = navigator.userAgent.toLowerCase(),
1241        //IE8+支持.返回浏览器渲染当前文档所用的模式
1242        //IE6,IE7:undefined.IE8:8(兼容模式返回7).IE9:9(兼容模式返回7||8)
1243        //IE10:10(兼容模式7||8||9)
1244        IEMode = document.documentMode,
1245        //chorme
1246        chrome = window.chrome || false,
1247        System = {
1248            //user-agent
1249            agent: agent,
1250            //是否为IE
1251            isIE: /msie/.test(agent),
1252            //Gecko内核
1253            isGecko: agent.indexOf("gecko") > 0 && agent.indexOf("like gecko") < 0,
1254            //webkit内核
1255            isWebkit: agent.indexOf("webkit") > 0,
1256            //是否为标准模式
1257            isStrict: document.compatMode === "CSS1Compat",
1258            //是否支持subtitle
1259            supportSubTitle: function () {
1260                return "track" in document.createElement("track");
1261            },
1262            //是否支持scoped
1263            supportScope: function () {
1264                return "scoped" in document.createElement("style");
1265            },
1266            //获取IE的版本号
1267            ieVersion: function () {
1268                try {
1269                    return agent.match(/msie ([\d.]+)/)[1] || 0;
1270                } catch (e) {
1271                    console.log("error");
1272                    return IEMode;
1273                }
1274            },
1275            //Opera版本号
1276            operaVersion: function () {
1277                try {
1278                    if (window.opera) {
1279                        return agent.match(/opera.([\d.]+)/)[1];
1280                    } else if (agent.indexOf("opr") > 0) {
1281                        return agent.match(/opr\/([\d.]+)/)[1];
1282                    }
1283                } catch (e) {
1284                    console.log("error");
1285                    return 0;
1286                }
1287            },
1288            //描述:version过滤.如31.0.252.152 只保留31.0
1289            versionFilter: function () {
1290                let start = ''
1291                let end = ''
1292                if (arguments.length === 1 && typeof arguments[0] === "string") {
1293                    var version = arguments[0];
1294                    start = version.indexOf(".");
1295                    if (start > 0) {
1296                        end = version.indexOf(".", start + 1);
1297                        if (end !== -1) {
1298                            return version.substr(0, end);
1299                        }
1300                    }
1301                    return version;
1302                } else if (arguments.length === 1) {
1303                    return arguments[0];
1304                }
1305                return 0;
1306            }
1307        };
1308
1309    try {
1310        //浏览器类型(IE、Opera、Chrome、Safari、Firefox)
1311        System.type = System.isIE ? "IE" :
1312            window.opera || (agent.indexOf("opr") > 0) ? "Opera" :
1313                (agent.indexOf("chrome") > 0) ? "Chrome" :
1314                    //safari也提供了专门的判定方式
1315                    agent.indexOf("safari") > 0 ? "Safari" :
1316                        (agent.indexOf("firefox") > 0) ? "Firefox" :
1317                            'unknow';
1318        isWeixinBrowser() ? System.type = 'micromessenger' : ''
1319
1320        //版本号
1321        System.version = (System.type === "IE") ? System.ieVersion() :
1322            (System.type === "Firefox") ? agent.match(/firefox\/([\d.]+)/)[1] :
1323                (System.type === "Chrome") ? agent.match(/chrome\/([\d.]+)/)[1] :
1324                    (System.type === "Opera") ? System.operaVersion() :
1325                        (System.type === "Safari") ? agent.match(/version\/([\d.]+)/)[1] :
1326                            "0";
1327
1328        //浏览器外壳
1329        System.shell = function () {
1330            if (isWeixinBrowser()) {
1331                return "微信浏览器";
1332            }
1333            //遨游浏览器
1334            if (agent.indexOf("maxthon") > 0) {
1335                System.version = agent.match(/maxthon\/([\d.]+)/)[1] || System.version;
1336                return "傲游浏览器";
1337            }
1338            //QQ浏览器
1339            if (agent.indexOf("qqbrowser") > 0) {
1340                System.version = agent.match(/qqbrowser\/([\d.]+)/)[1] || System.version;
1341                return "QQ浏览器";
1342            }
1343
1344            //搜狗浏览器
1345            if (agent.indexOf("se 2.x") > 0) {
1346                return '搜狗浏览器';
1347            }
1348
1349            //Chrome:也可以使用window.chrome && window.chrome.webstore判断
1350            if (chrome && System.type !== "Opera") {
1351                var external = window.external,
1352                    clientInfo = window.clientInformation,
1353                    //客户端语言:zh-cn,zh.360下面会返回undefined
1354                    clientLanguage = clientInfo.languages;
1355
1356                //猎豹浏览器:或者agent.indexOf("lbbrowser")>0
1357                if (external && 'LiebaoGetVersion' in external) {
1358                    return '猎豹浏览器';
1359                }
1360                //百度浏览器
1361                if (agent.indexOf("bidubrowser") > 0) {
1362                    System.version = agent.match(/bidubrowser\/([\d.]+)/)[1] ||
1363                        agent.match(/chrome\/([\d.]+)/)[1];
1364                    return "百度浏览器";
1365                }
1366                //360极速浏览器和360安全浏览器
1367                if (System.supportSubTitle() && typeof clientLanguage === "undefined") {
1368                    //object.key()返回一个数组.包含可枚举属性和方法名称
1369                    var storeKeyLen = Object.keys(chrome.webstore).length,
1370                        v8Locale = "v8Locale" in window;
1371                    return storeKeyLen > 1 ? '360极速浏览器' : '360安全浏览器';
1372                }
1373                return "Chrome";
1374            }
1375        };
1376
1377        //浏览器名称(如果是壳浏览器,则返回壳名称)
1378        System.name = System.shell();
1379        //对版本号进行过滤过处理
1380        System.version = System.versionFilter(System.version);
1381        return System
1382
1383    } catch (e) {
1384        console.log("error");
1385    }
1386    if (needversion) {
1387        return System.type + ' ' + System.version
1388    } else {
1389        return System.type
1390    }
1391}
1392
1393function fGetClientInfo() {
1394    let os = fOsInfo()
1395    let fist_loading_url = get_fist_loading_url()//获取着陆页
1396    let client_type = ''
1397    var w = document.documentElement.clientWidth || document.body.clientWidth;
1398    var h = document.documentElement.clientHeight || document.body.clientHeight;
1399    if (isWeixinBrowser()) {//微信浏览器    1 PC  2 平板 3 手机
1400        client_type = 3
1401    } else if (os.isPad) {
1402        client_type = 2
1403    } else if (os.isPc) {
1404        client_type = 1
1405    } else if (os.isPhone || os.isAndroid) {
1406        client_type = 3
1407    } else {//获取不到就默认为1 pc
1408        client_type = 1
1409    }
1410    // // let user_agent = isWeixinBrowser() ? 'micromessenger': browserJudge()
1411    // let user_agent = browserJudge()
1412    // console.log('user_agent',user_agent)
1413    let browserJudgeInfo = browserJudge()
1414
1415    let resultParams = {
1416        client_type: client_type,//设备类型,1代表pc,2代表平板,3代表手机
1417        browser_lang: navigator.browserLanguage || navigator.language,//浏览器语言
1418        browser_height: h,//浏览器高度
1419        browser_width: w,//浏览器宽度
1420        // user_agent: browserJudgeInfo.agent,//浏览器agent
1421        user_agent: browserJudgeInfo.name || browserJudgeInfo.type, //故意的 不用传agent就传浏览器类型
1422        referring_site: document.referrer || '',
1423        fist_loading_url: fist_loading_url,//着陆页url
1424        browser_name: browserJudgeInfo.name || browserJudgeInfo.type
1425    }
1426    let baseData = window.baseData || {}
1427    if (baseData && baseData.addons && baseData.addons.indexOf("affiliate") > -1) {//网盟插件存在并且有追踪ID
1428        let f_tracking_id = Cookies.get('f_tracking_id') || ''
1429        if (f_tracking_id) {
1430            resultParams['affiliate_tracking_id'] = f_tracking_id
1431        }
1432    }
1433    return resultParams
1434
1435}
1436
1437
1438function $T(txt, params) {//根据当前选择语言渲染对应语言
1439    let code = window['baseData']?.language_code
1440    if (!code) {
1441        return txt
1442    }
1443    if (!params) {
1444        return txt
1445    }
1446    if (params.hasOwnProperty(code)) return params[code]
1447    return txt
1448}
1449
1450/**
1451 * 获取登录信息,如果登录返回 customer,否则返回 false
1452 * @returns {Object|boolean}
1453 */
1454function $getLoginInfo() {
1455    const obj = localStorage.getItem('loginStatus');
1456    if (!obj) return false;
1457
1458    let info;
1459    try {
1460        info = JSON.parse(obj);
1461    } catch (e) {
1462        // 存储的数据不是有效 JSON
1463        return false;
1464    }
1465
1466    if (info && info.isLogined === true && info.customer) {
1467        return info.customer;
1468    }
1469    return false;
1470}
1471
1472// 全局变量,记录当前是否移动端
1473var $isMobile = isMobileDevice();
1474var $isPad = isClientIsPad();
1475
1476
1477/**
1478 * 防抖函数:在频繁触发的情况下(如 resize),只有最后一次会生效
1479 * @param {function} fn  要执行的函数
1480 * @param {number} delay 延迟多少毫秒执行(单位 ms)
1481 * @returns {function}   防抖包装后的新函数
1482 */
1483function debounce(fn, delay) {
1484    let timer;
1485    return function () {
1486        clearTimeout(timer);
1487        timer = setTimeout(fn, delay);
1488    }
1489}
1490
1491/**
1492 * 屏幕尺寸变化时的处理函数
1493 * 1. 获取当前是否为移动端
1494 * 2. 如果和上一次的状态发生了变化,则更新 $isMobile
1495 * 3. 派发自定义事件,便于其它模块监听响应
1496 */
1497const onResize = debounce(() => {
1498    // 判断当前状态
1499    const nowIsMobile = isMobileDevice();
1500    // 状态发生变化时才处理
1501    if (nowIsMobile !== $isMobile) {
1502        $isMobile = nowIsMobile; // 更新全局变量
1503
1504        // 根据状态派发事件
1505        if ($isMobile) {
1506            // 切换到移动端
1507            window.FecTheme.config.mqlMobile = true;
1508            document.dispatchEvent(new Event('matchMobile'));
1509        } else {
1510            // 切换到桌面端
1511            window.FecTheme.config.mqlMobile = false;
1512            document.dispatchEvent(new Event('unmatchMobile'));
1513        }
1514    }
1515    // 判断当前状态
1516    const nowIsPad = isClientIsPad();
1517    // 状态发生变化时才处理
1518    if (nowIsPad !== $isPad) {
1519        $isPad = nowIsPad; // 更新全局变量
1520    }
1521}, 200); // 200 毫秒防抖
1522
1523// 监听浏览器窗口尺寸变化事件
1524window.addEventListener('resize', onResize);
1525
1526
1527/**
1528 * 判断当前环境是否为移动端
1529 * 1. 屏幕宽度小于 768 像素,或者
1530 * 2. userAgent 包含主流移动设备关键词
1531 * 返回 true 表示移动端,false 表示桌面端
1532 */
1533function isMobileDevice() {
1534    return window.innerWidth < 768 ||
1535        /mobile|android|iphone|ipod|ipad|windows phone|blackberry/i.test(navigator.userAgent.toLowerCase());
1536}
1537
1538// 判断是否为Pad
1539function isClientIsPad() {
1540
1541    var ua = navigator.userAgent.toLowerCase();
1542    let status1 = /ipad/i.test(ua);
1543    let status2 = (navigator.platform === 'iPad');
1544    let clientWidth = document.documentElement.clientWidth
1545    if (status1 || status2 || (clientWidth > 750 && clientWidth <= 1200)) {
1546        return true
1547    } else {
1548        return false
1549    }
1550}
1551
1552
1553/**
1554 *
1555 * @param {string}  'https://xxxx.com/xxx/demo/type/1/id/2'
1556 * @returns  {object}  {type:'1',id:'2'}
1557 * 根据url 获取 对象
1558 */
1559function getUrlParam(url) {
1560    let reg_url = /^[^\?]+\?([\w\W]+)$/,
1561        reg_para = /([^&=]+)=([\w\W]*?)(&|$|#)/g,
1562        arr_url = reg_url.exec(url),
1563        ret = {};
1564
1565    if (arr_url && arr_url[1]) {
1566        let str_para = arr_url[1], result;
1567
1568        while ((result = reg_para.exec(str_para)) != null) {
1569
1570            let key = decodeURIComponent(
1571                result[1].replace('[]', '')
1572            );
1573
1574            // 关键修复:+ 转空格
1575            let value = decodeURIComponent(
1576                result[2].replace(/\+/g, ' ')
1577            );
1578
1579            if (ret[key] || ret[key] === '0' || result[1].indexOf('[]') > -1) {
1580
1581                if (Array.isArray(ret[key])) {
1582
1583                    ret[key].push(value);
1584
1585                } else if (!ret[key]) {
1586
1587                    ret[key] = [value];
1588
1589                } else {
1590
1591                    let val = ret[key];
1592                    ret[key] = [val, value];
1593                }
1594
1595            } else {
1596
1597                ret[key] = value;
1598            }
1599        }
1600    }
1601
1602    return ret;
1603}
1604
1605/**将对象转换为url参数形式
1606 * @property {Object} param 将要转换为URL参数的字符串对象
1607 * @property {String} key URL 参数字符串的前缀
1608 * @property {Boolean} encode 是否进行URL编码,默认为true
1609 * @return {String} URL参数字符串
1610 */
1611function urlEncode(param, key, encode) {
1612    if (param == null) return '';
1613    var paramStr = '';
1614    var t = typeof (param);
1615    if (t == 'string' || t == 'number' || t == 'boolean') {
1616        paramStr += '&' + key + '=' + ((encode == null || encode) ? encodeURIComponent(param) : param);
1617    } else {
1618        for (var i in param) {
1619            var k = key == null ? i : key + (param instanceof Array ? '[' + ']' : '.' + i);//去掉下标
1620            // var k = key == null ? i: key + (param instanceof Array ? '[' + i + ']': '.' + i); //带有下标
1621            paramStr += urlEncode(param[i], k, encode);
1622        }
1623    }
1624    return paramStr;
1625}
1626
1627function google_trace() {//增强型电子商务
1628    return window?.site_base_data?.google_trace || '';
1629}
1630
1631function google_conversion() {
1632    return window.site_base_data.google_conversion || []
1633}
1634
1635
1636function bind_enquiry_trace() {
1637    /*
1638    * contacts
1639    * 单商品询价
1640    * 多商品询价
1641    *
1642    * */
1643    console.log('询价 + contacts --------- bing')
1644
1645    let bing_trace = window?.baseData?.bing_trace || false
1646    if (!bing_trace) return
1647    window.uetq = window.uetq || [];
1648    window.uetq.push('event', 'submit_lead_form', {});
1649}
1650
1651function formatMoney(amount, el) {
1652    // 只取模板
1653    let scriptElement = document.querySelector(el);
1654    if (!scriptElement) return '';
1655
1656    // 缓存原始模板,只取一次
1657    if (!scriptElement._cachedTemplate) {
1658        scriptElement._cachedTemplate = scriptElement.innerHTML.trim();
1659    }
1660    const template = scriptElement._cachedTemplate;
1661
1662    // 抓原始价格
1663    const priceSpanElement = template.match(/<span>([^<]+)<\/span>/);
1664    if (!priceSpanElement) return template;
1665
1666    let originalPriceText = priceSpanElement[1];
1667
1668    // 判断格式
1669    const hasDecimal = originalPriceText.includes('.');
1670    const hasComma = originalPriceText.includes(',');
1671    let formattedPrice;
1672
1673    if (hasDecimal && /\.\d{2}$/.test(originalPriceText)) {
1674        // 小数点,两位小数
1675        let [intPart, decimalPart] = parseFloat(amount).toFixed(2).split('.');
1676        intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1677        formattedPrice = intPart + '.' + decimalPart;
1678    } else if (hasDecimal && !/\.\d{2}$/.test(originalPriceText)) {
1679        // . 作为千分位
1680        formattedPrice = Math.round(amount)
1681            .toString()
1682            .replace(/\B(?=(\d{3})+(?!\d))/g, ".");
1683    } else if (hasComma) {
1684        // , 作为千分位
1685        formattedPrice = Math.round(amount)
1686            .toString()
1687            .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1688    } else {
1689        // 纯整数
1690        formattedPrice = Math.round(amount).toString();
1691    }
1692
1693    // 返回新的结果
1694    return template.replace(/<span>[^<]+<\/span>/, `<span>${formattedPrice}</span>`);
1695}
1696
1697/**
1698 * 动态加载第三方 SDK
1699 * @param {string} url - SDK 脚本地址
1700 * @param {Object} [attributes={}] - 可选,自定义属性(例如 { "data-namespace": "paypalSubscription" })
1701 * @returns {Promise} - 脚本加载完成后 resolve
1702 * 使用示例:
1703 *   loadSdk('https://pay.google.com/gp/p/js/pay.js', { "data-namespace": "googlePay" }).then(...)
1704 */
1705// 用一个对象管理不同 URL 的 Promise
1706const sdkPromises = {};
1707
1708function loadSdk(url, attributes = {}) {
1709    // 生成唯一 key:url + JSON.stringify(attributes)
1710    const key = url + JSON.stringify(attributes);
1711
1712    // 如果已有对应 key 的 Promise,直接返回
1713    if (sdkPromises[key]) {
1714        return sdkPromises[key];
1715    }
1716
1717    // 创建新的 Promise
1718    sdkPromises[key] = new Promise((resolve, reject) => {
1719        // 检查 DOM 里是否已有完全相同属性的 script
1720        const selectorParts = [`script[src="${url}"]`];
1721        for (const [k, v] of Object.entries(attributes)) {
1722            selectorParts.push(`[${k}="${v}"]`);
1723        }
1724        const selector = selectorParts.join('');
1725        if (document.querySelector(selector)) {
1726            resolve();
1727            return;
1728        }
1729
1730        const script = document.createElement('script');
1731        script.src = url;
1732        script.async = true;
1733
1734        // 设置自定义属性
1735        for (const [key, value] of Object.entries(attributes)) {
1736            script.setAttribute(key, value);
1737        }
1738
1739        script.onload = () => {
1740            $log(`SDK 加载成功: ${url}`, attributes);
1741            resolve(window.paypal || null); // 这里按需调整返回对象
1742        };
1743
1744        script.onerror = () => {
1745            console.error(`SDK 加载失败: ${url}`, attributes);
1746            delete sdkPromises[key]; // 失败后允许重试
1747            reject(new Error(`SDK 加载失败: ${url}`));
1748        };
1749
1750        document.head.appendChild(script);
1751    });
1752
1753    return sdkPromises[key];
1754}
1755
1756function SetToArr(params) {
1757    if (!params) return params
1758    if (Array.isArray(params)) return params
1759    let arr = []
1760    for (let k in params) {
1761        arr.push(params[k])
1762    }
1763    return arr
1764
1765}
1766
1767function setCurrencyCookie(code) {//
1768    /*
1769    php会写入cookie,这是我写入的
1770    但是如果用户使用了cf,
1771    那么源代码部分会从cf直接拿到,
1772    php这边就不执行了,
1773    因此,需要js写入cookie(相当于帮助php,将当前的货币写入cookie)。
1774
1775    if (baseData.currency.default_currency_code(默认货币) == baseData.currency.current_currency_code (当前货币))  则:
1776       查看是否存在cookie:f2_currency_code,如果存在,则删除掉这个cookie(注意是删除掉,而不是清空值)
1777
1778    if (baseData.currency.default_currency_code(默认货币) != baseData.currency.current_currency_code (当前货币))  则:
1779       查看是否存在cookie:f2_currency_code,如果存在cookie,则将值设置为当前货币,如果不存在cookie,则创建该cookie并赋值
1780
1781    该cookie,关闭浏览器,则清空即可
1782
1783      */
1784    let baseData = window?.baseData
1785    let current_currency_code = code || baseData?.currency_code || ''
1786    let default_currency_code = baseData?.default_currency_code || ''
1787    // console.log('setCurrencyCookie-调用了吗')
1788
1789    if (current_currency_code) {
1790        Cookies.set(
1791            'f_currency_code',
1792            current_currency_code,
1793            {expires: 730, path: '/'}//两年过期
1794        )
1795    }
1796
1797    if (current_currency_code && default_currency_code) {//默认货币和当前货币 都存在
1798        if (current_currency_code === default_currency_code) {
1799            // 相同直接删除 f2_currency_code
1800            Cookies.remove('f2_currency_code')
1801        } else {//不相同需要存一个 f2_currency_code
1802            Cookies.set(
1803                'f2_currency_code',
1804                current_currency_code,
1805                {expires: 730, path: '/'}//两年过期
1806            )
1807        }
1808    }
1809}
1810
1811document.addEventListener("DOMContentLoaded", function () {
1812    setCurrencyCookie();
1813});
1814
1815// 登录完成后事件
1816/*
1817    formData 登录参数
1818    targetUrl 跳转链接
1819 */
1820async function accountBaseLoginEvent(formData, targetUrl, callback) {
1821    const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
1822    try {
1823        await baseGetAccountInfo();//设置账号状态,只设置账户信息和登录状态
1824        $log('开始获取收藏数量')
1825        await baseGetFavoriteCount()//获取商品收藏个数
1826        $log('收藏数量完毕')
1827        if (window.common_shop_type == 10) {//B2C店铺
1828            //后续 设置网盟分析插件及状态 等待该函数执行完毕
1829            await setAffiliateInfo()//设置网盟分析插件及状态 等待该函数执行完毕
1830        }
1831
1832        // 登录事件
1833        if (formData.email) {
1834            FecEvents.emit(FecTheme.pubSubEvents.accountLogin, {
1835                email: formData.email,
1836            })
1837        }
1838
1839        //如果开启了 omnisend 打点事件 等待一秒
1840        if (window.common_shop_type == 10) {//B2C店铺
1841            if (window.$omnisendIdentifyContact && typeof window.$omnisendIdentifyContact === 'function') {
1842                await wait(1000);
1843            }
1844        }
1845
1846        if (targetUrl) {
1847            window.location.href = targetUrl
1848        }
1849        if (callback && typeof callback === 'function') {
1850            callback()
1851        }
1852    } catch (err) {
1853        console.error('login erorr:', err);
1854    }
1855
1856}
1857
1858function baseGetAccountInfo() {
1859    $log('login-getAccountInfo------------');
1860    // after login , update account info
1861    return new Promise((resolve, reject) => {
1862        api.get('/api/customer/account/info').then(response => {
1863            //设置用户信息 如邮箱号,用户名,登录状态。 不要更新其他信息
1864            $log('getAccountInfo success');
1865            let loginStatus = localStorage.getItem('loginStatus')
1866            if (loginStatus) {
1867                loginStatus = JSON.parse(loginStatus)
1868                loginStatus.customer = response.customer
1869                loginStatus.isLogined = response.isLogined
1870                localStorage.setItem('loginStatus', JSON.stringify(loginStatus))
1871            }
1872            $log('getAccountInfo fail-发送订阅事件');
1873            // 事件订阅=> 发送给头部导航的个人中心icon 更新鼠标放入时显示的UI是去登录 还是去 去个人中心
1874
1875            FecEvents.emit(FecTheme.pubSubEvents.logInAndLogOutChange, {
1876                data: {}//
1877            });
1878            resolve()
1879
1880        }).catch(res => {
1881            resolve()
1882
1883        }).finally(res => {
1884
1885
1886        });
1887    })
1888
1889}
1890
1891function baseGetFavoriteCount() {//获取商品收藏个数
1892    return new Promise(resolve => {
1893        let isStatus = window?.common_prodfavorite || false
1894        $log('收藏数量')
1895        if (!isStatus) {
1896            resolve()
1897            return
1898        }
1899        api.post('/api/apps/prodfavorite/favorite/get-favorite-count').then(res => {
1900            $log('收藏数量', res)
1901
1902            let favoriteCountList = res?.product_ids || [];
1903            // 使用 Set 去重
1904            favoriteCountList = [...new Set(favoriteCountList)];
1905            let count = favoriteCountList.length || res?.count;
1906            // 保存到 localStorage
1907            localStorage.setItem('f-wishlist-products-ids', JSON.stringify(favoriteCountList));
1908            localStorage.setItem('wishlist_qty', JSON.stringify(count));
1909            setTimeout(() => {
1910                resolve()
1911            }, 100)
1912        }).catch(() => {
1913            setTimeout(() => {
1914                resolve()
1915            }, 100)
1916
1917        })
1918    })
1919}
1920
1921function setAffiliateInfo() {//设置网盟状态
1922    return new Promise(resolve => {
1923        if (!window.baseData.addons.includes('affiliate')) {//先查看有没有该插件
1924            resolve()//没插件权限直接返回
1925            return
1926        }
1927        // 获取真实路径(去除多语言前缀)
1928        let pathName = getRealPath(location.pathname)
1929        if (pathName === '/affiliate/guide') {//引导注册页不需要执行
1930            resolve()//没插件权限直接返回
1931            return
1932        }
1933        api.get('/api/apps/affiliate/account/get-status').then(res => {//获取账号状态
1934            /*
1935                值1:账户未登录
1936                值2:已经登录 && 未申请网盟商用户
1937                值3:已经登录 && 激活的网盟商用户
1938                值4:已经登录 && 已申请但未激活网盟用户(初始状态 或者 拒绝状态)
1939            * */
1940            let status = res?.status || ''
1941            if (status == 3) {
1942                let sessionId = localStorage.getItem('sessionId');
1943                localStorage.setItem('userIsAffiliate', sessionId)//当前用户是网盟商,存到缓存里
1944            }
1945            resolve()//有插件权限并且已经存储完毕
1946        })
1947
1948// 辅助函数:获取真实路径(去除语言前缀)
1949        function getRealPath(pathname) {
1950            // 匹配常见的语言代码:cn, en, fr, de, ja, ko 等(2-3个字母)
1951            const langPattern = /^\/[a-z]{2,3}(\/|$)/
1952
1953            if (langPattern.test(pathname)) {
1954                // 去掉第一段(语言前缀),保留剩余路径
1955                const parts = pathname.split('/')
1956                parts.shift() // 移除第一个空字符串
1957                parts.shift() // 移除语言代码
1958                return '/' + parts.join('/')
1959            }
1960
1961            return pathname
1962        }
1963    })
1964}
1965
1966/**
1967 *
1968 * @param targetUrl "https://xxxx.xxxxx.cn/ja?preview_theme_id=775";
1969 * @param path   跳转的具体页
1970 * @returns {string}  拼接完成
1971 */
1972function $buildUrl(targetUrl, path) {
1973    // 如果 path 是以 http 开头,直接返回 path
1974    if (path.startsWith('http')) {
1975        return path;
1976    }
1977    // 确保 targetUrl 以单个 / 结尾
1978    if (targetUrl.endsWith('/')) {
1979        targetUrl = targetUrl.slice(0, -1);  // 去掉最后的斜杠
1980    }
1981
1982    // 确保 path 不以 / 开头
1983    if (path.startsWith('/')) {
1984        path = path.slice(1);  // 去掉 path 开头的斜杠
1985    }
1986
1987    // 分离 targetUrl 的路径部分和查询参数部分
1988    let [baseUrl, queryString] = targetUrl.split('?');
1989
1990    // 如果 path 包含查询参数,拆分 path 部分和查询参数部分
1991    let [pathUrl, pathQuery] = path.split('?');
1992
1993    // 拼接路径部分
1994    let finalUrl = baseUrl + '/' + pathUrl;
1995
1996    // 处理查询参数
1997    let queryParams = [];
1998
1999    // 如果 targetUrl 有查询参数,先添加它们
2000    if (queryString) {
2001        queryParams.push(queryString);
2002    }
2003
2004    // 如果 path 有查询参数,添加它们(避免重复)
2005    if (pathQuery) {
2006        // 把 path 的查询参数分成一个数组,并合并到 targetUrl 的查询参数中
2007        let pathParams = new URLSearchParams(pathQuery);
2008        let targetParams = new URLSearchParams(queryParams.join('&'));
2009
2010        // 合并查询参数
2011        pathParams.forEach((value, key) => {
2012            targetParams.set(key, value);
2013        });
2014
2015        queryParams = [...targetParams.entries()].map(([key, value]) => `${key}=${value}`).join('&');
2016    }
2017
2018    // 最终拼接 URL
2019    if (queryParams.length > 0) {
2020        finalUrl += '?' + queryParams;
2021    }
2022
2023    return finalUrl;
2024}
2025
2026// 格式化货币
2027function $formatMoney(amount) {
2028    // 只取模板
2029    let scriptElement = document.querySelector('script[data-common-price-template]');
2030    if (!scriptElement) return '';
2031
2032    // 缓存原始模板,只取一次
2033    if (!scriptElement._cachedTemplate) {
2034        scriptElement._cachedTemplate = scriptElement.innerHTML.trim();
2035    }
2036    const template = scriptElement._cachedTemplate;
2037
2038    // 抓原始价格
2039    const priceSpanElement = template.match(/<span>([^<]+)<\/span>/);
2040    if (!priceSpanElement) return template;
2041
2042    let originalPriceText = priceSpanElement[1];
2043
2044    // 判断格式
2045    const hasDecimal = originalPriceText.includes('.');
2046    const hasComma = originalPriceText.includes(',');
2047    let formattedPrice;
2048
2049    if (hasDecimal && /\.\d{2}$/.test(originalPriceText)) {
2050        // 小数点,两位小数
2051        let [intPart, decimalPart] = parseFloat(amount).toFixed(2).split('.');
2052        intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2053        formattedPrice = intPart + '.' + decimalPart;
2054    } else if (hasDecimal && !/\.\d{2}$/.test(originalPriceText)) {
2055        // . 作为千分位
2056        formattedPrice = Math.round(amount)
2057            .toString()
2058            .replace(/\B(?=(\d{3})+(?!\d))/g, ".");
2059    } else if (hasComma) {
2060        // , 作为千分位
2061        formattedPrice = Math.round(amount)
2062            .toString()
2063            .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
2064    } else {
2065        // 纯整数
2066        formattedPrice = Math.round(amount).toString();
2067    }
2068
2069    // 返回新的结果
2070    return template.replace(/<span>[^<]+<\/span>/, `<span>${formattedPrice}</span>`);
2071}
2072
2073function $media(url) {
2074    if (!url) return
2075    if (!url.indexOf) return
2076    let attachment_url = window.common_image_url
2077    if (url.indexOf('http') === 0 || url.indexOf('https') === 0 || isBase64Image(url)) {//如果有完整URL那么直接返回完整的URL 或者如果是 base64位图片也直接返回
2078        return url
2079    }
2080    if (url.indexOf('/') === 0) {
2081        return attachment_url + url
2082    } else {
2083        return attachment_url + '/' + url
2084    }
2085
2086    function isBase64Image(str) {
2087        // Base64格式验证
2088        const base64Regex = /^data:image\/([a-zA-Z]*);base64,([^\s]*)$/;
2089
2090        if (!base64Regex.test(str)) {
2091            return false;
2092        }
2093
2094        // 提取MIME类型和Base64数据
2095        const matches = str.match(base64Regex);
2096        const mimeType = matches[1];
2097        const base64Data = matches[2];
2098
2099        // 检查MIME类型是否为常见图片格式
2100        const validImageTypes = ['jpeg', 'jpg', 'png', 'gif', 'bmp', 'webp', 'svg+xml'];
2101        if (!validImageTypes.includes(mimeType.toLowerCase())) {
2102            return false;
2103        }
2104
2105        // 验证Base64数据格式
2106        const base64DataRegex = /^[A-Za-z0-9+/]*={0,2}$/;
2107        if (!base64DataRegex.test(base64Data)) {
2108            return false;
2109        }
2110
2111        // 检查数据长度是否为4的倍数(Base64要求)
2112        if (base64Data.length % 4 !== 0) {
2113            return false;
2114        }
2115
2116        return true;
2117    }
2118
2119}
2120
2121//获取邮件打点
2122function $getAdvEmail(googleConversion) {
2123    let arr = SetToArr(googleConversion)
2124    let strs = []
2125    arr.forEach(item => {
2126        let str = null
2127        if (item.conversion_goal === 'email' && item.conversion_id.length && item.conversion_label.length) {
2128            str = `${item.conversion_id}/${item.conversion_label}`
2129            strs.push(str)
2130        }
2131    })
2132    if (strs && strs.length) return strs
2133    return false
2134}
2135
2136//获取邮件
2137function $getAdvWhatsapp(googleConversion) {
2138    let arr = SetToArr(googleConversion)
2139    let strs = []
2140    arr.forEach(item => {
2141        let str = null
2142        if (item.conversion_goal === 'whatsapp' && item.conversion_id.length && item.conversion_label.length) {
2143            str = `${item.conversion_id}/${item.conversion_label}`
2144            strs.push(str)
2145        }
2146    })
2147    if (strs && strs.length) return strs
2148    return false
2149}
2150
2151//获取手机事件
2152function $getAdvPhone(googleConversion) {
2153    let arr = SetToArr(googleConversion)
2154    let strs = []
2155    arr.forEach(item => {
2156        let str = null
2157        if (item.conversion_goal === 'phone' && item.conversion_id.length && item.conversion_label.length) {
2158            str = `${item.conversion_id}/${item.conversion_label}`
2159            strs.push(str)
2160        }
2161    })
2162    if (strs && strs.length) return strs
2163    return false
2164}
2165
2166//获取联系人
2167function $getAdvPerson(googleConversion) {
2168    let arr = SetToArr(googleConversion)
2169    let strs = []
2170    arr.forEach(item => {
2171        let str = null
2172        if (item.conversion_goal === 'person' && item.conversion_id.length && item.conversion_label.length) {
2173            str = `${item.conversion_id}/${item.conversion_label}`
2174            strs.push(str)
2175        }
2176    })
2177    if (strs && strs.length) return strs
2178    return false
2179}
2180
2181function $getAdvInquiry(googleConversion) { // 提交潜在客户表单: contacts 单商品询价 多商品询价
2182    let arr = SetToArr(googleConversion)
2183    console.log(arr)
2184    let strs = []
2185    arr.forEach(item => {
2186        let str = null
2187        if (item.conversion_goal === 'inquiry' && item.conversion_id.length && item.conversion_label.length) {
2188            str = `${item.conversion_id}/${item.conversion_label}`
2189            strs.push(str)
2190        }
2191    })
2192    if (strs && strs.length) return strs
2193    return false
2194}
2195
2196function $getAdvCart(googleConversion) { // 提交潜在客户表单: contacts 单商品询价 多商品询价
2197    let arr = SetToArr(googleConversion)
2198    let strs = []
2199    arr.forEach(item => {
2200        let str = null
2201        if (item.conversion_goal === 'cart' && item.conversion_id.length && item.conversion_label.length) {
2202            str = `${item.conversion_id}/${item.conversion_label}`
2203            strs.push(str)
2204        }
2205    })
2206    if (strs && strs.length) return strs
2207    return false
2208}
2209
2210function $getAdvCheckout(googleConversion) {
2211    let arr = SetToArr(googleConversion)
2212    let strs = []
2213    arr.forEach(item => {
2214        if (item.conversion_goal === 'checkout' && item.conversion_id.length && item.conversion_label.length) {
2215            strs.push(`${item.conversion_id}/${item.conversion_label}`)
2216        }
2217    })
2218    if (strs && strs.length) return strs
2219    return false
2220}
2221
2222function $getAdvPurchase(googleConversion) {
2223    let arr = SetToArr(googleConversion)
2224    let strs = []
2225    arr.forEach(item => {
2226        if (item.conversion_goal === 'purchase' && item.conversion_id.length && item.conversion_label.length) {
2227            strs.push(`${item.conversion_id}/${item.conversion_label}`)
2228        }
2229    })
2230    if (strs && strs.length) return strs
2231    return false
2232}
2233
2234
2235let overlayTimeout;  // 用于存储倒计时的 setTimeout 引用
2236//全屏遮罩最大显示 10秒,传入 true 或者 false
2237function $toggleOverlay(show) {
2238    // 检查是否已经存在遮罩层
2239    let overlay = document.getElementById('f-overlay');
2240
2241    if (show) {
2242        // 如果遮罩层不存在,则创建一个新的
2243        if (!overlay) {
2244            overlay = document.createElement('div');
2245            overlay.id = 'f-overlay';
2246            overlay.style.position = 'fixed';  // 定位为固定,覆盖全屏
2247            overlay.style.top = '0';
2248            overlay.style.left = '0';
2249            overlay.style.width = '100%';
2250            overlay.style.height = '100%';
2251            // overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';  // 半透明的黑色背景
2252            overlay.style.backgroundColor = 'rgba(0, 0, 0, 0)';  // 纯透明
2253            overlay.style.zIndex = '999999';  // 确保遮罩层在最上层
2254            overlay.style.display = 'flex';
2255            overlay.style.justifyContent = 'center';
2256            overlay.style.alignItems = 'center';
2257            document.body.appendChild(overlay);
2258
2259            // 如果存在上一次的倒计时,取消掉
2260            if (overlayTimeout) {
2261                clearTimeout(overlayTimeout);
2262            }
2263
2264            // 自动在 10 秒后移除遮罩层
2265            overlayTimeout = setTimeout(() => {
2266                $toggleOverlay(false);  // 10 秒后自动关闭遮罩层
2267            }, 10000);  // 10 秒 = 10000 毫秒
2268        }
2269    } else {
2270        // 如果遮罩层存在,则移除它
2271        if (overlay) {
2272            overlay.remove();
2273        }
2274        // 取消上一次的倒计时
2275        if (overlayTimeout) {
2276            clearTimeout(overlayTimeout);
2277        }
2278    }
2279}
2280
2281function $mobileAndTabletCheck() {
2282    let flag = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|mibrowser|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i);
2283    return flag ? true : false;
2284}
2285
2286function $htmlToText(html) {
2287    // 创建一个 DOM 元素来解析 HTML 字符串
2288    const doc = new DOMParser().parseFromString(html, 'text/html');
2289
2290    // 获取纯文本
2291    let text = doc.body.textContent || doc.body.innerText;
2292
2293    // 如果需要更多处理(如清理额外的空格或标签),可以添加额外的正则或逻辑
2294    text = text.replace(/\s+/g, ' ').trim(); // 去除多余的空格并修剪文本
2295
2296    return text;
2297}
2298
2299/**
2300 * 货币格式化转换
2301 * @param {string} moneyIcon 货币符号
2302 * @param {number|string} price 显示的金额
2303 * @param {number|string} currentCurrencyCode 货币code值
2304 * @returns {string} 处理完成后显示的货币格式
2305 */
2306function $currencyConversion(moneyIcon, price, currentCurrencyCode) {
2307    const currentLanguage = window?.baseData?.language_code || '';
2308
2309    // 空值处理
2310    if (price === null || price === undefined || price === '') {
2311        return '';
2312    }
2313
2314    // 只有这些货币需要四舍五入取整(没有小数点)
2315    const noDecimalCurrencies = ['HUF', 'TWD', 'JPY', 'VND', 'IDR'];
2316    const needRound = noDecimalCurrencies.includes(currentCurrencyCode);
2317
2318    let numericPrice = parseFloat(price);
2319    let formattedNumber = '';
2320
2321    if (needRound) {
2322        // 需要四舍五入取整的货币
2323        numericPrice = Math.round(numericPrice);
2324        // 获取千分位分隔符:印尼盾和越南盾用 .,其他用 ,
2325        const thousandSeparator = (currentCurrencyCode === 'IDR' || currentCurrencyCode === 'VND') ? '.' : ',';
2326        formattedNumber = this.analysis(numericPrice, thousandSeparator, false);
2327    } else {
2328        // 其他货币:保留原始小数(通常两位),千分符用逗号
2329        // 先格式化为两位小数
2330        numericPrice = numericPrice.toFixed(2);
2331        formattedNumber = this.analysis(numericPrice, ',', false);
2332    }
2333
2334    if (typeof formattedNumber !== 'string') formattedNumber = String(formattedNumber);
2335
2336    // 俄罗斯货币特殊处理:需要在符号后加空格
2337    const isRUB = currentCurrencyCode === 'RUB';
2338    const shouldAddSpace = isRUB && currentLanguage !== 'ar';
2339    const spaceBetween = shouldAddSpace ? ' ' : '';
2340
2341    const isArabic = currentLanguage === 'ar';
2342    const isVND = currentCurrencyCode === 'VND';
2343
2344    // 货币符号样式
2345    const iconStyle = `font-family: Tahoma, Arial, sans-serif;${isArabic ? 'margin-right: 4px' : ''}`;
2346    const iconElement = `<i style="${iconStyle}">${moneyIcon}</i>`;
2347    const numberElement = `<span>${formattedNumber}</span>`;
2348
2349    let priceFormat = '';
2350
2351    if (isVND) {
2352        // 越南盾:数字在前,符号在后
2353        priceFormat = `${numberElement}<i style="margin-left: 4px;font-family: Tahoma, Arial, sans-serif;">${moneyIcon}</i>`;
2354    } else {
2355        // 默认:符号在前,数字在后
2356        priceFormat = `${iconElement}${spaceBetween}${numberElement}`;
2357    }
2358
2359    return priceFormat;
2360}
2361
2362/**
2363 * 金额数字添加千分位分隔符
2364 * @param {number|string} opt 价格(数字或数字字符串)
2365 * @param {string} symbol 千分位分隔符号(默认 . )
2366 * @param {boolean}
2366 point 是否需要添加 .00 后缀(已废弃,保留兼容性)
2367 * @returns {string} 带分隔符的金额字符串
2368 */
2369function analysis(opt, symbol = '.', point = false) {
2370    if (opt === null || opt === undefined || opt === '') return '0';
2371
2372    let str = opt.toString();
2373
2374    // 处理小数情况
2375    if (str.includes('.')) {
2376        const parts = str.split('.');
2377        let integerPart = parts[0];
2378        const decimalPart = parts[1];
2379        // 整数部分添加千分位分隔符(使用传入的 symbol)
2380        integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, symbol);
2381        return `${integerPart}.${decimalPart}`;
2382    } else {
2383        // 整数部分添加千分位分隔符
2384        const ret = str.replace(/\B(?=(\d{3})+(?!\d))/g, symbol);
2385        if (point) {
2386            return ret + '.00';
2387        }
2388        return ret;
2389    }
2390}

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.