PageSourceSearch

https://www.wendyhair.com/dist/front/static/new_default_theme/product-info.js?v=640

js wendyhair.com collected 2026-10-05 22:02:29 UTC 65,172 bytes, 1,329 lines download raw bytes

1// 判断 customElements 注册表里是否已经有 "product-info" 组件
2if (!customElements.get("product-info")) {
3    // 自定义 HTML 元素:商品信息组件
4    class ProductInfo extends HTMLElement {
5        abortController = undefined;                  // 控制 fetch 的中断
6        pendingRequestUrl = null;                     // 当前待发起请求的 URL
7        preProcessHtmlCallbacks = [];                 // HTML 替换前预处理回调
8        postProcessHtmlCallbacks = [];                // HTML 替换后处理回调
9
10        constructor() {
11            super();
12            this.onWindowLoad = this.onWindowLoad.bind(this); // 绑定 window load 事件
13            this.productNoHandleRedirectHome = this.dataset.productNoHandleRedirectHome
14            this.realLevel1TagList = []//初始化时筛选真实存在的1级标签属性
15            this.realOption = {}//选中商品后 筛选真实存在的 2 3级商品属性
16            /*
17           * 如果值为1,,当前台商城,商品详情页,如果商品不存在,则跳转到首页
18               如果值不为1,则显示nor found
19           * */
20            if (!this.productId && window.baseData.page_type === "product") {//没有商品 id
21                if (Number(this.productNoHandleRedirectHome) === 1) {//productNoHandleRedirectHome === 1 跳转至首页
22                    window.location.href = window.common_base_url
23                } else if (Number(this.productNoHandleRedirectHome) === 2) {//显示 404页
24                    let notFoundTitle = this.dataset.notFoundTitle
25                    let notFoundTips = this.dataset.notFoundTips
26                    let notFoundButton = this.dataset.notFoundButton
27                    let mainContent = document.querySelector("#main-content")
28                    // 替换成 404内容
29                    mainContent.innerHTML = `
30                        <div class="section_id_main">
31                            <div class="f-page404">
32                                <span class="f-page404__icon">
33                                    <svg class="f-svg-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 464c-119.1 0-216-96.9-216-216S128.9 40 248 40s216 96.9 216 216-96.9 216-216 216zm0-152c-44.4 0-86.2 19.6-114.8 53.8-5.7 6.8-4.8 16.9 2 22.5 6.8 5.7 16.9 4.8 22.5-2 22.4-26.8 55.3-42.2 90.2-42.2s67.8 15.4 90.2 42.2c5.3 6.4 15.4 8 22.5 2 6.8-5.7 7.7-15.8 2-22.5C334.2 339.6 292.4 320 248 320zm-80-80c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32z"></path></svg>
34                                </span>
35                                <h1 class="h1" >${notFoundTitle}</h1>
36                                <p class="h1">${notFoundTips}</p>
37                                <a href="${window.common_base_url}" class="f-button  f-button--primary">${notFoundButton}</a>
38                            </div>
39                        </div>`
40                }
41            }
42        }
43
44        // ---------- 属性 Getter,方便代码里到处获取相关属性 -----------
45        get productId() {
46            return this.getAttribute('data-product-id');
47        }
48
49        get useScene() {
50            //使用场景 这个非常重要, 指定取值赋值
51            // 用于得到 =>  <script type="application/json" data-use-scene="{{ scene }}"data - selected - variant = "{{ product.id }}" > {{selected_variant | json}}</script>
52            if (this.getAttribute('data-use-scene')) {
53                return this.getAttribute('data-use-scene');
54            }
55            return 'block'
56        }
57
58        get sectionId() {
59            return this.dataset.originalSection || this.dataset.sectionId;
60        }
61
62        get disableSelectedVariantDefault() {
63            // 默认规格是否禁用选中
64            return this.dataset.disableSelectedVariantDefault === "1" || false;
65        }
66
67        get enableVariantGroupImages() {
68            // 是否开启规格组图片
69            return this.dataset.enableVariantGroupImages === "1" || false;
70        }
71
72        get enableHistoryState() {
73            // 是否支持变体切换同步到浏览器历史
74            return this.dataset.enableHistoryState === "1" || false;
75        }
76
77        get viewMode() {
78            // 显示模式(主商品区/弹窗预览等)
79            return this.dataset.viewMode || 'main-product'
80        }
81
82        // 获取 pickup-availability 组件
83        get pickupAvailability() {
84            return this.querySelector(`pickup-availability`);
85        }
86
87        // 获取 product-form 组件
88        get productForm() {
89            return this.querySelector('product-form');
90        }
91
92        // 获取多媒体图集容器
93        get productMedia() {
94            return this.querySelector(`[id^="MediaGallery-${this.productId}"]`);
95        }
96
97        // 获取变体选择器
98        get variantPicker() {
99            return this.querySelector('variant-picker');
100        }
101
102        // 获取主加购数量输入框
103        get quantityInput() {
104            return this.querySelector(`f-quantity-input input`);
105        }
106
107        // 获取底部悬浮加购数量输入框
108        get quantityInputStickyAtc() {
109            return this.querySelector(`.f-sticky-addtocart f-quantity-input input`);
110        }
111
112        // 悬浮加购栏组件
113        get stickyAtc() {
114            return this.querySelector('sticky-atc');
115        }
116
117        // ---------- Web Component 生命周期 ----------
118        connectedCallback() {
119            $log('Web Component 生命周期', this.productId);
120            // 主商品区初始化监听
121            if (this.viewMode === "main-product") {
122                window.addEventListener("load", this.onWindowLoad);
123            }
124            this.init();
125        }
126
127        disconnectedCallback() {
128            // 组件销毁时清理事件监听
129            window.removeEventListener("load", this.onWindowLoad);
130            this.cartUpdateUnsubscriber?.();      // 可能存在的购物车变更订阅取消
131        }
132
133        // ---------- 初始化方法 ----------
134        init() {
135            this.currentAllVariant = this.getSelectedAllVariant(this); // 当前变体对象
136            this.currentVariant = this.getSelectedVariant(this); // 当前变体对象
137            $log('当前选中的', this.currentVariant);
138            $log('真实的一级规格', this.realLevel1TagList)
139            this.quantityInputs = [];
140
141            if (this.quantityInput) {
142                this.quantityInputs.push(this.quantityInput);
143            }
144            if (this.quantityInputStickyAtc) {
145                this.quantityInputs.push(this.quantityInputStickyAtc);
146            }
147
148            if (this.disableSelectedVariantDefault) {
149                this.handleDisableSelectedVariantDefault();
150            }
151
152            // 规格/选项变更事件订阅(比如切换颜色尺码)
153            FecEvents.on(FecTheme.pubSubEvents.optionValueSelectionChange, (payload) => {
154                $log('订阅事件-当前选中的变体:', payload.data);
155                // 推荐:独立出处理函数
156                const fn = this.handleOptionValueChange.bind(this);
157                fn(payload);
158                // 通常会去更新价格/图片/库存等
159            });
160
161            $log('商品详情页初始化', window.location)
162            // 主图状态处理与媒介初始化(视频/轮播图等)
163            // this.showFeaturedMedia = this.dataset.showFeaturedMedia === "1" || false;
164
165            this.initialMedias = this.productMedia ? this.productMedia.querySelectorAll('.f-product-media--item') : null;
166            this.initialThumbs = this.productMedia ? this.productMedia.querySelectorAll(".f-product-media--slider__thumbnails [data-media-type]") : null;
167
168            // 如果启用变体组图片,则解析该数据
169            if (this.enableVariantGroupImages) {
170                this.variantGroupImagesData = this.getVariantGroupImagesData();
171                // this.showFeaturedMedia = false;
172            }
173
174
175            // 初始化数量相关功能(递增递减、限购)
176            this.initQuantityHandlers();
177            $log('准备进行-根据当前变体状态,更新主图', this.enableVariantGroupImages, this.currentVariant)
178            // 根据当前变体状态,更新主图
179            if (this.currentVariant) {
180                this.updateMedia(this.currentVariant);
181            }
182            this.updateSpecificationAttributeDisablesFunction()//根据选中的规格属性确定是否禁止购买,并为规格设置禁用样式
183            // 更新顶部 url
184            const productUrl = this.dataset.productUrl || this.pendingRequestUrl || this.dataset.url;
185            this.updateURL(this.currentVariant?.id);
186
187            // 无库存不能购买选项 并且没有库存 禁止购买
188            if (this.dataset.needsInventory === '1' && this.currentVariant.qty <= 0) {
189                this.setUnavailable();
190            }
191        }
192
193        // 解析变体组图片配置数据(JSON)
194        getVariantGroupImagesData() {
195            return (
196                JSON.parse(
197                    this.querySelector('#variantGroup[type="application/json"]')
198                        .textContent
199                ) || {}
200            );
201        }
202
203        // 根据选中的规格属性确定是否禁止购买,并为规格设置禁用样式
204        updateSpecificationAttributeDisablesFunction() {
205            $log('updateSpecificationAttributeDisablesFunction-根据选中的规格属性确定是否禁止购买,并为规格设置禁用样式-1',
206                this.currentVariant,
207                this.dataset.needsInventory);
208            if (!(this.currentAllVariant && this.currentAllVariant.length > 0)) {
209                return;
210            }
211            $log('当前选中', this.currentVariant);
212
213            // ----- 新增:动态判断是否为单一属性维度(只有 option1 有多种值)-----
214            const distinctOpt1 = new Set();
215            const distinctOpt2 = new Set();
216            const distinctOpt3 = new Set();
217            this.currentAllVariant.forEach(v => {
218                if (v.option1) distinctOpt1.add(v.option1);
219                if (v.option2) distinctOpt2.add(v.option2);
220                if (v.option3) distinctOpt3.add(v.option3);
221            });
222            const isSingleDimension = distinctOpt1.size > 0 && distinctOpt2.size <= 1 && distinctOpt3.size <= 1;
223            // ------------------------------------------------------------
224
225            // --- 判断库存 没库存的要禁用 ---
226            let finalHasStock = false;
227            if (this.dataset.needsInventory === '1' && this.currentVariant.qty > 0) {
228                finalHasStock = true;
229            }
230
231            // 单一属性维度商品处理(新增)
232            if (isSingleDimension) {
233                $log('单属性维度商品(如只有尺寸),启用独立禁用与自动切换');
234                if (this.dataset.needsInventory === '1') {
235                    const option1Inputs = this.querySelectorAll('variant-picker .f-product-option [data-position="1"]');
236                    // 收集有库存的 option1 值
237                    const availableOption1 = new Set();
238                    this.currentAllVariant.forEach(v => {
239                        if (v.qty > 0) availableOption1.add(v.option1);
240                    });
241                    let firstAvailableValue = null;
242                    option1Inputs.forEach(input => {
243                        const value = input.value;
244                        const hasStock = availableOption1.has(value);
245                        const parentNode = input.closest('.f-product-option--node');
246                        if (hasStock) {
247                            input.disabled = false;
248                            parentNode && parentNode.classList.remove('f-product-option--node__unavailable');
249                            if (!firstAvailableValue) firstAvailableValue = value;
250                        } else {
251                            input.disabled = true;
252                            parentNode && parentNode.classList.add('f-product-option--node__unavailable');
253                        }
254                    });
255                    // 如果当前变体无库存,自动切换到第一个有库存的选项
256                    if (this.currentVariant && this.currentVariant.qty <= 0 && firstAvailableValue) {
257                        const availableInput = Array.from(option1Inputs).find(inp => inp.value === firstAvailableValue);
258                        if (availableInput && !availableInput.checked) {
259                            if (availableInput.type === 'radio' || availableInput.type === 'checkbox') {
260                                availableInput.checked = true;
261                                const changeEvent = new Event('change', { bubbles: true });
262                                availableInput.dispatchEvent(changeEvent);
263                            } else if (availableInput.tagName === 'SELECT') {
264                                const selectEl = availableInput;
265                                const option = Array.from(selectEl.options).find(opt => opt.value === firstAvailableValue);
266                                if (option) {
267                                    option.selected = true;
268                                    const changeEvent = new Event('change', { bubbles: true });
269                                    selectEl.dispatchEvent(changeEvent);
270                                }
271                            }
272                        }
273                    }
274                }
275                finalHasStock = !(this.dataset.needsInventory === '1' && this.currentVariant?.qty <= 0);
276            }
277            else {
278                // ========== 多规格商品:以下代码与原代码完全一致,一字不改 ==========
279                $log('是多规格商品')
280                finalHasStock = true
281                // 单独判断规格1
282                // 跟踪库存并且库存为0 并且 不允许购买
283                if (this.dataset.needsInventory === '1') {
284                    const option1PossInputs = this.querySelectorAll('variant-picker .f-product-option [data-position="1"]');
285                    option1PossInputs.forEach(input => {
286                        const value = input.value;
287                        $log('多规格value', value)
288
289                        let hasStock = false
290                        // 保存一下,只要有一个组合有库存,就算整体有库存
291                        if (this.realLevel1TagList.includes(this.currentVariant.option1)) {
292                            hasStock = true
293                        }
294                        // 设置当前 input 是否可选 没库存的就要禁止点击
295                        const parentNode = input.closest('.f-product-option--node');
296                        if (hasStock) {
297                            input.disabled = false;
298                            parentNode && parentNode.classList.remove('f-product-option--node__unavailable');
299                        } else {
300                            input.disabled = true;
301                            parentNode && parentNode.classList.add('f-product-option--node__unavailable');
302                        }
303                    });
304                }
305
306                // 判断规格2
307                let option1 = this.currentVariant.option1 ? this.currentVariant.option1 : ''
308                let option2 = this.currentVariant.option2 ? this.currentVariant.option2 : ''
309                let option3 = this.currentVariant.option3 ? this.currentVariant.option3 : ''
310                let level1List = []
311                let level2List = []
312                let level2OptionList = []//二级属性 根据一级选择的属性 确定出 2级有的属性
313                let level3OptionList = []//三级属性 根据一级选择的属性 确定出 3级有的属性
314
315                this.currentAllVariant.forEach(item => {
316                    // option3 可选标签 需要确定option1 和 option2 是选好的固定的
317                    if (item.option1 == option1) {
318                        level1List.push(item)
319                    }
320                })
321                this.currentAllVariant.forEach(item => {
322                    // option3 可选标签 需要确定option1 和 option2 是选好的固定的
323                    if (item.option1 == option1 && item.option2 == o
323ption2) {
324                        level2List.push(item)
325                    }
326                })
327                level1List.forEach(item => {//生成option2 可选标签
328                    if (item.option2 && item.option2.length && level2OptionList.indexOf(item.option2) === -1) {
329                        level2OptionList.push(item.option2)
330                    }
331                })
332                level2List.forEach(item => {
333                    if (item.option3 && item.option3.length && level3OptionList.indexOf(item.option3) === -1) {
334                        level3OptionList.push(item.option3)
335                    }
336                })
337                // this.realOption = {
338                //     1: level2OptionList,
339                //     2: level3OptionList
340                // }
341                let realOption = {
342                    2: level2OptionList,
343                    3: level3OptionList
344                }
345                $log('得到的-level2OptionList', level2OptionList)
346                $log('得到的-level3OptionList', level3OptionList)
347
348                for (let pos = 2; pos <= 3; pos++) {
349                    const possInputs = this.querySelectorAll('variant-picker .f-product-option [data-position="' + pos + '"]');
350                    possInputs.forEach(input => {
351                        const value = input.value;
352                        let canChoose = false;
353
354                        // 检查该值是否包含在 realOption[pos] 中
355                        if (realOption[pos].includes(value)) {
356                            canChoose = true;
357                        }
358
359                        // 获取父节点以便控制状态
360                        const parentNode = input.closest('.f-product-option--node');
361
362                        // 设置当前 input 是否可选,没库存的就要禁止点击
363                        if (canChoose) {
364                            input.disabled = false;
365                            parentNode && parentNode.classList.remove('f-product-option--node__unavailable');
366                        } else {
367                            input.disabled = true;
368                            parentNode && parentNode.classList.add('f-product-option--node__unavailable');
369                        }
370
371                        // 如果 value 和 option[pos] 匹配,设置为选中状态
372                        if (value === this.currentVariant['option' + pos]) {
373                            if (input.type === 'radio' || input.type === 'checkbox') {
374                                input.checked = true; // 设置为选中
375                            } else if (input.type === 'select-one') {
376                                input.selected = true; // 如果是下拉框,设置为选中
377                            }
378                        } else {
379                            if (input.type === 'radio' || input.type === 'checkbox') {
380                                input.checked = false; // 取消选中
381                            } else if (input.type === 'select-one') {
382                                input.selected = false; // 如果是下拉框,取消选中
383                            }
384                        }
385                    });
386                }
387            }
388            $log('finalHasStock最终', finalHasStock)
389
390            //  循环结束后,再统一设置按钮
391            const paymentButton = document.querySelector('.fec-payment-button__button');
392            // const addButtonUpdated = ''
393            const addButtonUpdated = document.getElementById(`ProductSubmitButton-${this.productId}`);
394            $log('加入购物车按钮样子', addButtonUpdated)
395            let customeowInfoSkus = this.getCustomeowInfo()//定制喵数据
396            const productForm = document.getElementById(`product-form-${this.productId}`);
397            let currentSkuIsCustom = 2
398            if (customeowInfoSkus && customeowInfoSkus.skus && customeowInfoSkus.skus.includes(this.currentVariant.sku)) {//当前变体是否是定制商品
399                currentSkuIsCustom = 1
400            } else {
401                currentSkuIsCustom = 2
402            }
403            if (productForm) {
404                productForm.dataset.currentSkuIsCustom = currentSkuIsCustom
405            }
406
407            // 悬浮栏购物
408            let stickyAtc = document.querySelector('sticky-atc');
409            if (stickyAtc) {
410                const btn = stickyAtc.querySelector('button[type="submit"]');
411                const btnText = stickyAtc.querySelector('button[type="submit"] .f-add-to-cart--text');
412                const option = stickyAtc.querySelector('.f-product-option--dropdown-select');
413                //无库存无能购买的话再设置禁用状态
414                if (currentSkuIsCustom == 1) {
415
416                    if (finalHasStock) {//有库存
417                        btn.disabled = false;
418                        option.disabled = false;
419                    } else {//没库存
420                        btn.disabled = true;
421                        option.disabled = true;
422                    }
423                }
424                if (currentSkuIsCustom == 1) {//是定制商品
425
426                    btnText.innerHTML = addButtonUpdated.getAttribute('data-customeow-text');
427                } else {
428                    btnText.innerHTML = btn.getAttribute('data-atc-text')
429                }
430
431
432            }
433            if (paymentButton) {//立即购买
434                if (this.dataset.needsInventory === '1') {//无库存无能购买的话再设置禁用状态
435                    paymentButton.disabled = !finalHasStock;
436                    if (finalHasStock) {//有库存
437                        let PayPalBox = document.querySelector('product-paypal-payment-express')//PayPal
438                        // 容器盒里有 立即购买按钮 PayPal结账
439                        if (PayPalBox) {
440                            PayPalBox.removeAttribute('disabled');
441
442                        }
443                    } else {//没库存
444                        let PayPalBox = document.querySelector('product-paypal-payment-express')//PayPal
445                        // 容器盒里有 立即购买按钮 PayPal结账
446                        if (PayPalBox) {
447                            // 设置 disabled 属性(适用于按钮或带有交互的容器)
448                            PayPalBox.setAttribute('disabled', 'disabled');
449                        }
450
451                    }
452                }
453            }
454            if (addButtonUpdated) {//加入购物车
455                $log('addButtonUpdated-显示加入购物车', addButtonUpdated);
456                let addButtonUpdatedText = addButtonUpdated.querySelector('.f-add-to-cart--text')
457                // if (finalHasStock) {
458                //     if (this.dataset.needsInventory === '1') {//无库存无能购买的话再设置禁用状态
459                //         addButtonUpdated.disabled = false;
460                //     }
461                //
462                //     if (currentSkuIsCustom == 1) {//是定制商品
463                //         addButtonUpdatedText.innerHTML = addButtonUpdated.getAttribute('data-customeow-text');
464                //     } else {
465                //         addButtonUpdatedText.innerHTML = addButtonUpdated.getAttribute('data-atc-text');
466                //     }
467                // } else {
468                //     addButtonUpdated.disabled = true;
469                //     addButtonUpdatedText.innerHTML = addButtonUpdated.getAttribute('data-oos-text');
470                //
471                // }
472            }
473            this.setColorImageTooltipText()//开始设置是否显示色卡的说明信息
474            this.checkedBuyMinCount()//检查最少购买数量
475        }
476
477        // ---------- 用户每次切换规格(option)时被 FecEvents (时间订阅 )调用 ----------
478        handleOptionValueChange({data: {event, target, selectedOptionValues}}) {//多规格数据改变了
479            $log('handleOptionValueChange', event, target, selectedOptionValues)
480
481            if (!this.contains(event.target)) return; // 只处理本商品组件内部的事件
482
483            // 当前商品的新URL,及是否需要切换产品(比如选择了新sku)
484            const productUrl = target.dataset.productUrl || this.pendingRequestUrl || this.dataset.url;
485            const shouldSwapProduct = this.dataset.url !== productUrl;
486            // TODO 需要不需要全页面刷新 ,这里可以在做商品组的时候判断是否需要全页面刷新
487            const shouldFetchFullPage = (this.dataset.updateUrl === 'true' || this.dataset.updateUrl === '1') && shouldSwapProduct;
488            //现在走的都是 handleUpdateProductInfo 这个函数
489            shouldSwapProduct ? this.handleSwapProduct(productUrl, shouldFetchFullPage, this.viewMode)
490                : this.handleUpdateProductInfo(productUrl, selectedOptionValues, this.viewMode)
491            // 渲染商品区块页面
492            // this.renderProductInfo({
493            //     requestUrl: this.buildRequestUrlWithParams(productUrl, selectedOptionValues, shouldFetchFullPage),
494            //     targetId: target.id,
495            //     callback: shouldSwapProduct
496            //         ? this.handleSwapProduct(productUrl, shouldFetchFullPage, this.viewMode)
497            //         : this.handleUpdateProductInfo(productUrl, this.viewMode),
498            // });
499        }
500
501        // ------- 核心:渲染页面(局部或全页) ------
502        renderProductInfo({requestUrl, targetId, callback}) {
503            this.abortController?.abort(); // 取消上一次请求
504            this.abortController = new AbortController(); // 新的 AbortController
505
506            // 此处省略 fetch 请求实现,见业务代码...
507        }
508
509        // 工具:拼接选项请求参数 URL
510        buildRequestUrlWithParams(url, optionValues, shouldFetchFullPage = false) {
511            const params = [];
512            !shouldFetchFullPage && params.push(`section_id=${this.productId}`);
513            if (optionValues.length) {
514                params.push(`option_values=${optionValues.join(',')}`);
515            }
516            return `${url}?${params.join('&')}`;
517        }
518
519        getCustomeowInfo() {//定制喵
520            let selectedVariant = document.querySelector('[data-customeow-info]')?.innerHTML;
521            if (selectedVariant) {
522                return !!selectedVariant ? JSON.parse(selectedVariant) : null;
523            }
524        }
525
526        // 处理仅更新局部商品信息的回调(价格、库存、图片区域等)
527        handleUpdateProductInfo(productUrl, selectedOptionValues, viewMode) {
528            $log('更新的对象', productUrl, selectedOptionValues, viewMode)
529            //本地更新
530            let html = this
531            // 快速预览(弹窗)处理
532            const quickView = html.querySelector('#MainProduct-quick-view__content');
533            if (quickView && viewMode === 'quick-view') {
534                html = new DOMParser().parseFromString(quickView.innerHTML, "text/html");
535            }
536            this.setNewSelectedVariant(selectedOptionValues)//根据选择的规格值,遍历得到具体的变体商品
537            // 获取新的变体对象
538            this.currentVariant = this.getSelectedVariant(this);//根据选中的规格值,匹配到具体的变体商品,并设置到 variant-picker [data-selected-variant] 里
539
540            $log('新的variant', this.currentVariant)
541            //更新规格是否禁用
542            this.updateSpecificationAttributeDisablesFunction()
543            $log('开始更新-updateURL', productUrl, this.currentVariant)
544            // 更新URL数据等
545            this.updateURL(this.currentVariant?.id);
546            // this.updateVariantInputs(variant?.id);
547
548            // 无库存不能购买选项 并且没有库存 禁止购买
549            if (this.dataset.needsInventory === '1' && this.currentVariant.qty <= 0) {
550                this.setUnavailable();
551                return;
552            }
553
554            // 根据规格图片策略,更新时间轴/主图/轮播
555            this.updateMedia(this.currentVariant);
556            // 批量更新多个特定区块内容(价格、库存、SKU、体积价等)
557            const updateSourceFromDestination = (id, shouldHide = (source) => false) => {
558                const domInfo = document.getElementById(`${id}-${this.productId}`);
559                $log('updateSourceFromDestination', domInfo, this.productId);
560                if (id === 'Price' && domInfo) {//价格更新
561                    $log('domInfo', domInfo)
562                    let salePrice = domInfo.querySelector('.f-price-item--sale span');
563                    let comparePrice = domInfo.querySelector('.f-strikethrough-price span');
564                    let productPriceInfo = domInfo.querySelector('.product-price-info');
565                    $log('salePrice', salePrice, this.currentVariant)
566                    $log('comparePrice', comparePrice,)
567                    $log('productPriceInfo', productPriceInfo,)
568                    if (salePrice) {//售价替换
569                        salePrice.innerHTML = this.currentVariant.price
570                    }
571                    if (Number(this.currentVariant.compare_at_price) > Number(this.currentVariant.price)) {
572                        $log('大于了吗',)
573                    }
574                    if (comparePrice && this.currentVariant.compare_at_price > 0 && (Number(this.currentVariant.compare_at_price) > Number(this.currentVariant.price))) {//划线价替换
575                        productPriceInfo.classList.remove('f-price--on-sale')
576                        $log('有没有划线价')
577                        if (this.currentVariant.compare_at_price > 0) {
578                            comparePrice.textContent = this.currentVariant.compare_at_price;
579                            // comparePrice.closest('.f-price-item--regular').style.display = 'block';
580                            productPriceInfo.classList.add('f-price--on-sale')
581                            $log('有没有划线价1')
582                        } else {
583                            $log('有没有划线价2')
584
585                            // 没有划线价就隐藏它
586                            productPriceInfo.classList.remove('f-price--on-sale')
587
588                            // comparePrice.closest('.f-price-item--regular').style.display = 'none';
589                        }
590                    } else {
591                        $log('有没有划线价3')
592                        productPriceInfo.classList.remove('f-price--on-sale')
593                    }
594
595                    let regularPrice = domInfo.querySelector('.f-price-item--regular span');
596                    if (regularPrice) {
597                        regularPrice.innerHTML = this.currentVariant.price
598                    }
599                }
600            };
601            updateSourceFromDestination('Price');//更新价格
602
603            // updateSourceFromDestination("Inventory");//库存
604            // updateSourceFromDestination("Sku");
605            // updateSourceFromDestination("Availability");//可用性
606            // updateSourceFromDestination("Volume");//体积
607            // updateSourceFromDestination('PricePerItem');
608            const inventoryComponent = document.getElementById('inventory-status');//更新库存
609            if (inventoryComponent) {
610                inventoryComponent.update(this.currentVariant);
611            }
612
613
614            this.updateQuantityRules(this.productId, this.productId, html);
615            this.querySelector(`#QuantityRules-${this.dataset.section}`)?.classList.remove('m:hidden');
616            this.querySelector(`#VolumeNote-${this.dataset.section}`)?.classList.remove('m:hidden');
617
618            // 加购按钮状态
619            const addButtonUpdated = document.getElementById(`ProductSubmitButton-${this.productId}`);
620            // this.toggleAddButton(
621            //     addButtonUpdated ? addButtonUpdated.hasAttribute('disabled') : true,
622            //     window.MinimogStrings.soldOut
623            // );
624
625            // 派发变体变更自定义事件
626            document.dispatchEvent(
627                new CustomEvent('variant:changed', {
628                    detail: {
629                        variant: this.currentVariant,
630                    },
631                })
632            );
633
634        }
635
636        // 处理 “切换同页不同商品” 的回调
637        handleSwapProduct(productUrl, updateFullPage, viewMode) {
638            $log('handleSwapProduct-----', productUrl, updateFullPage, viewMode);
639            // 返回的是一个实际处理 HTML 的函数
640            return (html) => {
641                // 如果是弹窗快速预览
642                const quickView = html.querySelector('#MainProduct-quick-view__
642content');
643                if (quickView && viewMode === 'quick-view') {
644                    html = new DOMParser().parseFromString(quickView.innerHTML, "text/html");
645                }
646
647                // 查找切换后的 product-info 根节点
648                const selector = updateFullPage ? "product-info[id^='MainProduct']" : 'product-info';
649                const productInfo = html.querySelector(selector);
650
651                // 拿到新商品的当前变体对象
652                const variant = this.getSelectedVariant(productInfo);
653
654                productInfo.dataset.disableSelectedVariantDefault = false;
655
656                this.updateURL(variant?.id);
657
658                // 替换页面局部或全页内容
659                if (updateFullPage) {
660                    // 替换 <title>
661                    document.querySelector('head title').innerHTML = html.querySelector('head title').innerHTML;
662                    // 替换主内容区块,有过渡动画
663                    HTMLUpdateUtility.viewTransition(
664                        document.querySelector('main'),
665                        html.querySelector('main'),
666                        this.preProcessHtmlCallbacks,
667                        this.postProcessHtmlCallbacks
668                    );
669                } else {
670                    // 局部(组件内)内容替换
671                    HTMLUpdateUtility.viewTransition(
672                        this,
673                        productInfo,
674                        this.preProcessHtmlCallbacks,
675                        this.postProcessHtmlCallbacks
676                    );
677                }
678
679                // 记录切换后的变体
680                this.currentVariant = variant;
681            };
682        }
683
684
685        // 解析当前选中的变体对象(通过 variant-picker 的 data-selected-variant)
686        getSelectedVariant(productInfoNode) {
687            try {
688                const selectedVariant = productInfoNode?.querySelector(`variant-picker script[data-use-scene="${this.useScene}"][data-selected-variant="${this.productId}"]`)?.innerHTML;
689                return selectedVariant && selectedVariant !== 'null' && selectedVariant !== '(null)null' ? JSON.parse(selectedVariant) : null;
690            } catch {
691                return null;
692            }
693        }
694
695        getSelectedAllVariant(productInfoNode) {//全部变体数据
696
697            $log('needsInventory', this.dataset.needsInventory === '1');
698            let allWapVariant = productInfoNode.querySelector(`variant-picker script[data-use-scene="${this.useScene}"][data-all-variants="${this.productId}"]`)?.innerHTML;
699            if (allWapVariant) {
700
701                if (this.dataset.needsInventory === '1') {
702                    // 跟踪库存并不允许购买库存为0的商品
703                    let arr = []
704                    let newAllWapVariant = []
705                    try {
706                        arr = JSON.parse(allWapVariant) || null
707                    } catch (e) {
708                        arr = []
709                    }
710                    newAllWapVariant = arr.filter(item => {
711                        // 找到属性里所有的一级属性 存入数组 在循环一级属性时会判断是否禁用
712                        if (item.qty > 0) {
713                            if (this.realLevel1TagList.indexOf(item.option1) === -1) {
714                                this.realLevel1TagList.push(item.option1)
715                            }
716                        }
717                        return item.qty > 0
718                    })
719                    if (newAllWapVariant.length < arr.length) {//说明长度变短了
720                        return newAllWapVariant
721                    } else {
722                        return arr
723                    }
724
725                } else {
726                    try {
727                        return allWapVariant && allWapVariant !== '(null)null' && allWapVariant !== 'null' ? JSON.parse(allWapVariant) : null;
728                    } catch {
729                        return null;
730                    }
731                }
732
733            }
734        }
735
736        // 设置选中的变体对象(通过 variant-picker 的 data-selected-variant)
737        setNewSelectedVariant(selectedOptionValues) {//规格值变化后,设置新的变体值,放到 'variant-picker [data-selected-variant]'里
738            const chosenVariant = this.findSelectedVariant(selectedOptionValues, this.currentAllVariant);
739            let selectedVariantNode = this.querySelector(`variant-picker script[data-use-scene="${this.useScene}"][data-selected-variant="${this.productId}"]`);
740            if (chosenVariant && selectedVariantNode) {
741                selectedVariantNode.innerHTML = JSON.stringify(chosenVariant);
742            }
743        }
744
745        /**
746         * @param {Array} selectedValues 用户已经选中的选项 [{ name, value, position }]
747         * @param {Array} variants 商品所有变体数组
748         * @return {Object|null} 找到则返回变体对象,否则返回 null
749         */
750        findSelectedVariant(selectedValues, variants) {
751            $log('findSelectedVariant-2', variants)
752            // 遍历所有变体
753            for (const variant of variants) {
754                // 检查所有选项是否都匹配
755                const allMatch = selectedValues.every(selected => {
756                    // 注意 optionX 的值有可能是空字符串,也有可能未定义
757                    return variant[selected.position] === selected.value;
758                });
759                if (allMatch) {
760                    return variant; // 找到后直接返回
761                }
762            }
763            return null; // 未找到
764        }
765
766        // 切换 URL 地址栏参数(按需 variant id)
767        updateURL(id) {
768            $log('enableHistoryState', this.enableHistoryState)
769            if (!this.enableHistoryState || !id) return;
770            window.setUrlVariant('variant', id)
771        }
772
773        // 更新表单的变体 id 隐藏输入框
774        // updateVariantInputs(variantId) {
775        //     document
776        //         .querySelectorAll(`#product-form-${this.dataset.sectionId}, #product-form-installment-${this.dataset.sectionId}`)
777        //         .forEach((productForm) => {
778        //             const input = productForm.querySelector('input[name="id"]');
779        //             input.value = variantId ?? '';
780        //             input.dispatchEvent(new Event('change', {bubbles: true}));
781        //         });
782        // }
783
784
785        // 某种规格不可用(比如页面重载或无货)
786        setUnavailable() {
787            // console.log('没库存')
788            // const productForm = document.getElementById(
789            //     `product-form-${this.sectionId}`
790            // );
791            // const addButton = productForm.querySelector('[name="add"]');
792            // if (addButton) {
793            //     this.toggleAddButton(true, 'Out of stock');
794            // }
795            // // 隐藏相关区块
796            // const selectors = [
797            //     "Price", "Inventory", "Sku", "Availability"
798            // ].map((id) => `#${id}-${this.sectionId}`).join(", ");
799            // document
800            //     .querySelectorAll(selectors)
801            //     .forEach((selector) => selector.classList.add('m:hidden'));
802        }
803
804        // 启用/禁用加购按钮,以及文字切换
805        toggleAddButton(disable = true, text, modifyClass = true) {
806            const productForm = document.getElementById(`product-form-${this.productId}`);
807            if (!productForm) return;
808            const addButton = productForm.querySelector('[name="add"]');
809            const addButtonText = productForm.querySelector('[name="add"] > span.f-add-to-cart--text');
810            if (!addButton) return;
811
812            if (disable) {
813                addButton.setAttribute('disabled', 'disabled');
814                if (text) addButtonText.textContent = text;
815            } else {
816                addButton.removeAttribute('disabled');
817                addButtonText.innerHTML = 'Add To Cart';
818            }
819        }
820
821        // 切换变体后,更新商品主图轮播区
822        updateMedia(variant) {
823            $log('更新轮播图', variant, this.productMedia);
824            if (!this.productMedia) return;
825            // enableVariantGroupImages => 是否开启规格组图片
826            if (this.enableVariantGroupImages && this.variantGroupImagesData.enable) {
827                $log('是商品规格图模式')
828                this.handleVariantGroupImage(variant);
829                // 汽车型图集兼容 swiper 滑块、缩略图同步动画等
830                if (this.productMedia.slider) {
831                    this.productMedia.slider.updateSlides();
832                    this.productMedia.navSlider.updateSlides();
833                    this.productMedia.slider.slideTo(0);
834                    this.productMedia.navSlider.slideTo(0);
835                    this.productMedia.handleSlideChange();
836                }
837                if (this.productMedia.enableImageZoom) {
838                    this.productMedia.handlePhotoswipe();
839                }
840            } else {
841                // 普通图片处理,延迟机制保证 setActiveMedia 存在
842                $log('普通图片处理', this.productMedia.slider.slides)
843                if (this.productMedia.slider && this.productMedia.slider.slides && this.productMedia.slider.slides.length) {
844                    for (let i = 0; i < this.productMedia.slider.slides.length; i++) {
845                        let slider = this.productMedia.slider.slides[i];
846                        if (slider.dataset.groupMediaSrc === variant.image) {
847                            this.productMedia.slider.slideTo(i);
848                            this.productMedia.navSlider.slideTo(i);
849                            break
850                        }
851                    }
852
853                }
854
855            }
856        }
857
858        // “禁用自动选中变体”逻辑初始化(全部选项都不选)
859        handleDisableSelectedVariantDefault() {
860            let urlParams = new URLSearchParams(window.location.search);
861            if (urlParams.has("variant")) return;
862
863            this.pickerFields = this.querySelectorAll("[data-picker-field]");
864
865            this.pickerFields && this.pickerFields.forEach((field) => {
866                let pickerType = field.dataset.pickerField;
867                switch (pickerType) {
868                    case "select":
869                        let selectBox = field.querySelector("select");
870                        let option = document.createElement("option");
871                        option.text = this.dataset.variantOptionNoneText;
872                        option.setAttribute("disabled", "");
873                        option.setAttribute("selected", "");
874                        selectBox.add(option, 0);
875                        break;
876                    default:
877                        let checkedInputs = field.querySelectorAll("input:checked");
878                        checkedInputs &&
879                        checkedInputs.forEach(function (input) {
880                            input.removeAttribute("checked");
881                        });
882                        break;
883                }
884                field.dataset.selectedValue = "";
885                if (field.querySelector(".option-label--selected")) field.querySelector(".option-label--selected").textContent = "";
886            });
887
888            this.setUnavailable();
889            this.currentVariant = null;
890        }
891
892        // 变体组图片处理(指定变体的图片、顺序、缩略图)
893        handleVariantGroupImage(variant) {
894            $log('变体组图片处理-商品规格图')
895            const selectedVariantData = this.variantGroupImagesData.mapping.find(
896                (item) => Number(item.id) === variant.id
897            );
898            $log('变体组图片处理-商品规格图', selectedVariantData)
899
900            // 内部工具:返回排序好的媒体节点数组
901            const selectedMedias = (initialItems) => {
902                const selectedVariantMedias = Array.from(initialItems)
903                    .map((media) => {
904                        const index = selectedVariantData.media.indexOf(media.dataset.mediaId);
905                        const mediaType = media.dataset.mediaType;
906                        if (index !== -1 || mediaType !== 'image') {
907                            return media;
908                        }
909                        return null;
910                    })
911                    .filter(Boolean);
912
913                // 主图列表为空则显示全部
914                if (selectedVariantMedias.length <= 0) {
915                    return Array.from(initialItems).map((media, index) => {
916                        media.dataset.index = index;
917                        media.removeAttribute('style');
918                        media.querySelector('.f-product-media').setAttribute("style", `--animation-order: ${index + 1};`);
919                        return media;
920                    });
921                }
922
923                // 变体图片排序: mapping 顺序优先
924                const orderedMedias = Object.values(selectedVariantData.media)
925                    .map(id => selectedVariantMedias.find(media => media.dataset.mediaId === id))
926                    .filter(Boolean);
927                const remainingMedias = selectedVariantMedias.filter(media => !Object.values(selectedVariantData.media).includes(media.dataset.mediaId));
928
929                // 合并,保证动画顺序样式
930                return Array.from([...orderedMedias, ...remainingMedias]).map((media, index) => {
931                    media.dataset.index = index;
932                    media.removeAttribute('style');
933                    media.querySelector('.f-product-media').setAttribute("style", `--animation-order: ${index + 1};`);
934                    return media;
935                });
936            }
937
938            // 媒体和缩略图 DOM 容器
939            const mediaWrapper = this.productMedia.querySelector(".f-media-gallery__list");
940            const thumbWrapper = this.productMedia.querySelector(".f-product-media--slider__thumbnails .swiper-wrapper");
941            const layout = this.productMedia.dataset.layout;
942
943            // 先清空,再重新插入变体对应的媒体 DOM
944            mediaWrapper.innerHTML = "";
945            thumbWrapper.innerHTML = "";
946
947            selectedMedias(this.initialMedias).forEach((media, index) => {
948                // 布局2需大格图-跨2列
949                if (layout === "layout-2") {
950                    media.classList.remove("f-col-span-2");
951                    if (index % 3 === 0) {
952                        media.classList.add("f-col-span-2");
953                    }
954                }
955                mediaWrapper.append(media);
956            });
957            selectedMedias(this.initialThumbs).forEach((media) => {
958                media.classList.toggle("swiper-slide-thumb-active", media.dataset.index === "0");
959                thumbWrapper.append(media);
960            });
961        }
962
963        // -------- 数量输入区功能处理 ---------
964        initQuantityHandlers() {
965            if (this.quantityInputs.length <= 0) return;
966            this.setQuantityBoundries();
967            if (!this.hasAttribute("data-original-section-id")) {
968                // 购物车变化时同步规则,通常用于限制每人限购之类
969                // this.cartUpdateUnsubscriber = MinimogEvents.subscribe(
970                //   MinimogTheme.pubSubEvents.cartUpdate,
971
972                // );
973            }
974        }
975
976        // 设置数量输入框的限购规则
977        setQuantityBoundries() {
978            this.quantityInputs.forEach((input) => {
979                const quantityInputWrapper = input.closest('f-quantity-input');
980                quantityInputWrapper.setQuantityBoundries(this.productId, this.productId);
981            });
982        }
983
984
985        // 具体更新页面数量规则 DOM
986        updateQuantityRules(sectionId, productId, parsedHTML) {
987            if (this.quantityInputs.length <= 0) return;
988            this.quantityInputs.forEach((input) => {
989                const quantityInputWrapper = input.closest('f-quantity-input');
990                quantityInputWrapper.updateQuantityRules(sectionId, productId, parsedHTML);
991            });
992            this.setQuantityBoundries();
993        }
994
995        // --------- 记录最近浏览(cookie 中) -----------
996        addRecentViewedProduct() {
997            // const cookies = Cookies.get('f-recent-viewed-products')
998            // let products = cookies ? JSON.parse(cookies) : []
999            // if (products.indexOf(MinimogSettings.productHandle) === -1) {
1000            //   products.unshift(MinimogSettings.productHandle)
1001            //   products = products.slice(0, 20)
1002            //   Cookies.set('f-recent-viewed-products', JSON.stringify(products));
1003            // }
1004        }
1005
1006        // ---------- 页面加载后回调 (商品页头部状态等) ----------
1007        onWindowLoad() {
1008            this.acc = [];
1009            // 处理对比按钮/心愿单按钮
1010            // MinimogTheme.CompareProduct && MinimogTheme.CompareProduct.setCompareButtonsState();
1011            // MinimogTheme.Wishlist && MinimogTheme.Wishlist.setWishlistButtonsState();
1012            this.addRecentViewedProduct();
1013
1014            // 针对 collapsible(商品详情可折叠项)内评价区块,事件处理
1015            addEventDelegate({
1016                context: this,
1017                selector: (window.__minimog_review_selector || '') + '.f-product-collapsible .jdgm-widget-actions-wrapper, .f-product-collapsible .spr-summary-actions-newreview',
1018                handler: (e) => {
1019                    const index = e.target.closest('.f-product-collapsible').dataset.index
1020                    setTimeout(() => {
1021                        this.acc[Number(index)].setContentHeight()
1022                    }, 300)
1023                },
1024                capture: true
1025            })
1026        }
1027
1028        // 开始设置是否显示色卡的说明信息
1029        setColorImageTooltipText() {
1030            // $log('开始设置是否显示色卡的说明信息', this.currentVariant)
1031            let colorCard = this.querySelector(['[data-picker-type="color"]']) || null
1032
1033            // 定义一个变量来记录是否找到了匹配项
1034            let foundMatchingTooltip = false;
1035
1036            if (colorCard) {
1037                // $log('有商品属性色卡', colorCard)
1038                // 获取所有 .f-color-image-tooltip 元素
1039                let tooltips = colorCard.querySelectorAll('.f-color-image-tooltip');
1040                // 使用 for 循环进行遍历
1041                for (let i = 0; i < tooltips.length; i++) {
1042                    let tooltip = tooltips[i];
1043                    let optionPosition = tooltip.getAttribute('data-option-position');
1044                    let optionName = tooltip.getAttribute('data-option-name');
1045                    let value = tooltip.getAttribute('data-value');
1046                    let optionKey = 'option' + optionPosition;
1047                    // $log('data-option-position:', optionPosition);
1048                    // $log('data-option-name:', optionName);
1049                    // console.log('data-option-value:', value);
1050                    // 如果当前选项值和当前变体匹配,找到对应标签并退出循环
1051                    if (this.currentVariant[optionKey] === value) {
1052                        // $log('找到这个标签了', tooltip);
1053
1054                        // 只替换 f-color-image-tooltip-content 的内容
1055                        let contentContainer = colorCard.querySelector('.f-color-image-tooltip-content');
1056                        if (contentContainer) {
1057                            // 清空之前的内容,插入新的tooltip标签
1058                            contentContainer.innerHTML = ''; // 清空原内容
1059                            contentContainer.appendChild(tooltip.cloneNode(true)); // 重新插入匹配的tooltip
1060                        }
1061
1062                        // 设置为已找到匹配的标签
1063                        foundMatchingTooltip = true;
1064                        break;
1064 // 一旦找到匹配的标签,退出循环
1065                    }
1066
1067                }
1068                // 如果没有找到匹配的标签,删除 colorCard 最后的 tooltip(如果有)
1069                if (!foundMatchingTooltip) {
1070                    let contentContainer = colorCard.querySelector('.f-color-image-tooltip-content');
1071                    if (contentContainer) {
1072                        contentContainer.innerHTML = '';
1073                    }
1074                }
1075            }
1076
1077        }
1078
1079        //检查最少购买数量
1080        checkedBuyMinCount() {
1081            $log('currentVariant', this.currentVariant)
1082            if (this.currentVariant && this.currentVariant.buy_min_count && this.currentVariant.buy_min_count > 1) {//如果商家端后台设置了 那么用商家端后台的,否则返回默认1
1083                let buy_min_count = Number(this.currentVariant.buy_min_count)
1084                $log('buy_min_count', this.sectionId)
1085                let quantityInput = this.querySelector('#Quantity-' + this.sectionId)
1086                if (quantityInput) {
1087                    quantityInput.min = buy_min_count;       // 设置 min 属性
1088                    quantityInput.value = buy_min_count;   // 设置 value
1089                    const el = document.querySelector(`f-quantity-input[data-section-id="${this.sectionId}"]`);
1090                    if (el) {
1091                        el.onInputChange()
1092                    }
1093                }
1094            } else {
1095                let quantityInput = this.querySelector('#Quantity-' + this.sectionId)
1096                if (quantityInput) {
1097                    quantityInput.min = 1;       // 设置 min 属性
1098                    quantityInput.value = 1;   // 设置 value
1099                    const el = document.querySelector(`f-quantity-input[data-section-id="${this.sectionId}"]`);
1100                    if (el) {
1101                        el.onInputChange()
1102                    }
1103                }
1104            }
1105        }
1106
1107
1108    }
1109
1110    // 注册组件
1111    customElements.define("product-info", ProductInfo);
1112}
1113
1114class ProductListAttr extends HTMLElement {
1115    connectedCallback() {
1116        $log('ProductListAttr', this.dataset)
1117        if (this.dataset.defaultHiden === '1') {//默认隐藏,需要判断是否登录
1118            if ($getLoginInfo()) {
1119                // 如果已经登录,去掉隐藏
1120                this.classList.remove('m:hidden');
1121            }
1122        }
1123    }
1124}
1125
1126customElements.define("f-product-list-attr", ProductListAttr);
1127
1128
1129class ProductWholesalePrice extends HTMLElement {//商品价格批发
1130    connectedCallback() {
1131        this.productId = this.dataset.productId;
1132        // this.updateWholesalePrice()
1133        // 规格/选项变更事件订阅(比如切换颜色尺码)
1134        this.firstWholesaleText = this.dataset.firstWholesaleText;//批发价格如果是第一个显示的文字(xx个起批)
1135        this.wholesaleText = this.dataset.wholesaleText;//第一个以外的文字,比如>=xx件
1136        FecEvents.on(FecTheme.pubSubEvents.optionValueSelectionChange, (payload) => {
1137            this.updateWholesalePrice()//更新商品批发价格
1138        });
1139    }
1140
1141    updateWholesalePrice() {//更新商品批发价格
1142        let selectedVariant = this.getSelectedVariant()
1143        $log('商品价格批发-订阅事件-当前选中的变体:', selectedVariant);
1144        if (selectedVariant && selectedVariant.wholesale_price && selectedVariant.wholesale_price.length > 0) {
1145            $log('有批发价格')
1146            this.classList.remove('m:hidden');
1147            // const price = this.formatMoney(this.state.shippingCost.curr_shipping_cost || 5.99);
1148            $log('this.firstWholesaleText', this.firstWholesaleText)
1149            $log('this.wholesaleText', this.wholesaleText)
1150            this.renderWholesalePrice(selectedVariant)
1151        } else {
1152            $log('没有批发价格')
1153            this.classList.add('m:hidden');
1154
1155        }
1156
1157    }
1158
1159    /**
1160     * 渲染批发价
1161     * @param {Object} selectedVariant - 当前变体数据
1162     */
1163    renderWholesalePrice(selectedVariant) {
1164        const box = this.querySelector('.wholesale-price-list');
1165        if (!box) return;
1166
1167        const list = Array.isArray(selectedVariant?.wholesale_price)
1168            ? selectedVariant.wholesale_price
1169            : [];
1170
1171        if (!list.length) {
1172            box.innerHTML = '';
1173            this.classList.add('m:hidden');
1174            return;
1175        }
1176
1177        this.classList.remove('m:hidden');
1178
1179        let html = '';
1180
1181        list.forEach((item, idx) => {
1182            // 格式化价格
1183            const priceHTML = this.formatMoney(item.price);
1184
1185            // 数量文案
1186            let numberHTML = '';
1187            if (idx === 0) {
1188                // 第一条:替换 firstWholesaleText 里的 10086
1189                numberHTML = `
1190          <span class="w-number pc" data-role="number">
1191            ${this.firstWholesaleText.replace('10086', item.qty)}
1192          </span>
1193          <span class="w-number wap" data-role="number">
1194            ${this.wholesaleText.replace('10086', item.qty)}
1195          </span>`;
1196            } else {
1197                // 其他条:替换 wholesaleText 里的 10086
1198                numberHTML = `
1199          <span class="w-number" data-role="number">
1200            ${this.wholesaleText.replace('10086', item.qty)}
1201          </span>`;
1202            }
1203
1204            html += `
1205        <div class="wholesale-price-item">
1206          <p class="w-price" data-role="price">${priceHTML}</p>
1207          ${numberHTML}
1208        </div>`;
1209        });
1210
1211        // 替换整个 wholesale-price-list 内部
1212        box.innerHTML = html;
1213    }
1214
1215    // 解析当前选中的变体对象(通过 variant-picker 的 data-selected-variant)
1216    getSelectedVariant() {//默认选中的数据
1217        $log('getSelectedVariant-默认选中的数据', this.productId)
1218        let selectedVariant = document.querySelector(`variant-picker [data-selected-variant="${this.productId}"]`)?.innerHTML;
1219        if (selectedVariant) {
1220            return !!selectedVariant ? JSON.parse(selectedVariant) : null;
1221        }
1222    }
1223
1224    // 格式化货币
1225    formatMoney(amount) {
1226        // 只取模板
1227        let scriptElement = document.querySelector('script[data-product-wholesale-price-template]');
1228        if (!scriptElement) return '';
1229
1230        // 缓存原始模板,只取一次
1231        if (!scriptElement._cachedTemplate) {
1232            scriptElement._cachedTemplate = scriptElement.innerHTML.trim();
1233        }
1234        const template = scriptElement._cachedTemplate;
1235
1236        // 抓原始价格
1237        const priceSpanElement = template.match(/<span>([^<]+)<\/span>/);
1238        if (!priceSpanElement) return template;
1239
1240        let originalPriceText = priceSpanElement[1];
1241
1242        // 判断格式
1243        const hasDecimal = originalPriceText.includes('.');
1244        const hasComma = originalPriceText.includes(',');
1245        let formattedPrice;
1246
1247        if (hasDecimal && /\.\d{2}$/.test(originalPriceText)) {
1248            // 小数点,两位小数
1249            let [intPart, decimalPart] = parseFloat(amount).toFixed(2).split('.');
1250            intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1251            formattedPrice = intPart + '.' + decimalPart;
1252        } else if (hasDecimal && !/\.\d{2}$/.test(originalPriceText)) {
1253            // . 作为千分位
1254            formattedPrice = Math.round(amount)
1255                .toString()
1256                .replace(/\B(?=(\d{3})+(?!\d))/g, ".");
1257        } else if (hasComma) {
1258            // , 作为千分位
1259            formattedPrice = Math.round(amount)
1260                .toString()
1261                .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1262        } else {
1263            // 纯整数
1264            formattedPrice = Math.round(amount).toString();
1265        }
1266
1267        // 返回新的结果
1268        return template.replace(/<span>[^<]+<\/span>/, `<span>${formattedPrice}</span>`);
1269    }
1270
1271
1272}
1273
1274customElements.define("product-wholesale-price", ProductWholesalePrice);
1275
1276
1277if (!customElements.get("f-product-reviews")) {
1278    class FProductReviews extends HTMLElement {
1279        constructor() {
1280            super();
1281        }
1282
1283        connectedCallback() {
1284
1285            let tabs = document.querySelector('f-product-details-tabs');
1286            if (tabs) {
1287                let headers = document.querySelectorAll('.f-tabs__header-inner .f-tab-header')
1288                let viewBtn = this.querySelector('.f-view-btn')
1289                let reviewsShow = false
1290                let reviewsIndex = -1
1291                if (headers.length) {
1292                    for (let i = 0; i < headers.length; i++) {
1293                        let header = headers[i];
1294                        if (header.dataset.blockType === 'reviews') {
1295                            reviewsIndex = i//确认下标
1296                            reviewsShow = true;
1297                            break;
1298                        }
1299                    }
1300
1301                }
1302                if (reviewsShow) {
1303                    viewBtn.classList.remove('m:hidden');
1304                    viewBtn.addEventListener('click', () => {
1305                        tabs.setActiveTab(reviewsIndex)
1306                        requestAnimationFrame(() => {
1307                            const headerHeight = parseInt(
1308                                getComputedStyle(document.documentElement).getPropertyValue('--f-header-height')
1309                            ) || 0;
1310
1311                            const rect = tabs.getBoundingClientRect();
1312                            const elementTop = window.scrollY + rect.top - headerHeight;
1313
1314                            window.scrollTo({
1315                                top: elementTop,
1316                                behavior: 'smooth'
1317                            });
1318                            $log('点击了查看', reviewsIndex)
1319                        })
1320                    })
1321                }
1322            }
1323
1324        }
1325    }
1326
1327    customElements.define("f-product-reviews", FProductReviews);
1328}
1329

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.