PageSourceSearch

https://www.furola.com/dist/front/static/new_default_theme/addons_customervip.js?v=641

js furola.com collected 2026-10-04 07:04:07 UTC 15,964 bytes, 378 lines download raw bytes

1/*
2    功能:批量获取商品会员价并渲染到页面
3    优化:防抖合并请求、去重、避免重复请求相同商品ID列表
4* */
5if (!customElements.get('f-customer-vip-price')) {
6    class FCustomerVipPrice extends HTMLElement {
7        constructor() {
8            super();
9            this.apiUrl = '/api/apps/customervip/product/get-product-prices';
10            this.productApiUrl = '/api/apps/customervip/product/get-product-variant-prices';
11            // 待处理的商品队列: productId => [dom节点数组]
12            this.queue = new Map(); // productId => [nodes]
13            this.requestedIds = new Set(); // 已请求过的 productId
14            this.initialized = false;
15            this.scheduled = false;
16            this.lastRequestedKey = ''; // 记录上一次请求的 key(productIds.join(','))
17            this.isVipPriceShow = false
18            this.productInfoId = '' // 商品详情页或者快速购买的商品 ID
19            this.productInfoNode = null // 商品详情页或快速购买的价格节点
20            this.product_variant_prices = []//商品详情页变体选择后的价格
21            // 检查登录状态和会员等级
22            let loginStatus = localStorage.getItem('loginStatus')
23            if (loginStatus) loginStatus = JSON.parse(loginStatus)
24            let customervip_id = loginStatus?.customer?.customervip_id || 0
25            if ($getLoginInfo() && customervip_id) {
26                this.isVipPriceShow = true
27            } else {
28                this.isVipPriceShow = false
29            }
30        }
31
32        connectedCallback() {
33            if (!this.isVipPriceShow) { // 没有插件或者不是会员
34                return;
35            }
36            $log('会员价格开始初始化')
37            if (this.initialized) return;
38            this.initialized = true;
39
40            this.collectNewNodes(); // 初始化
41            this.initObserver(); // 监听新增商品
42            if (window?.baseData?.page_type === 'product') {
43                // 与主产品表单同步变体变化 主产品表单选中了产品,那么这里同步跟着变化
44                this.syncWithMainProductForm()//监听变体选择
45            }
46        }
47
48        // 初始化MutationObserver,监听DOM新增节点
49        initObserver() {
50            const observer = new MutationObserver(mutations => {
51                for (const mutation of mutations) {
52                    mutation.addedNodes.forEach(node => {
53                        // nodeType 节点类型 => 1 表示元素节点 3 表示文本节点 8 表示注释节点
54                        if (node.nodeType !== 1) return;
55                        if (node.querySelectorAll) this.collectNewNodes(node);
56                    });
57                }
58            });
59            observer.observe(document.body, {childList: true, subtree: true});
60        }
61
62        /**
63         * 收集新出现的商品节点 商品详情页 + 商品列表
64         * @param {Document|Element} root - 搜索根节点
65         */
66        collectNewNodes(root = document) {
67            $log('collectNewNodes');
68
69            // 1. 处理商品详情页或快速购买弹窗的价格节点(优先处理)
70            const productInfoNode = root.querySelector('product-info .product-block-group--price f-product-prices');
71
72            if (productInfoNode) {
73                try {
74                    const productInfo = root.querySelector('product-info');
75                    if (productInfo && productInfo.dataset.productId) {
76                        this.productInfoId = productInfo.dataset.productId;
77                        this.productInfoNode = productInfoNode;
78
79                        $log('检测到商品详情页或快速购买弹窗', {
80                            productInfoId: this.productInfoId,
81                            node: productInfoNode
82                        });
83
84                        // 如果这个商品ID还没有被请求过,立即单独处理
85                        this.fetchSingleProductPrice(this.productInfoId, productInfoNode);
86                    }
87                } catch (e) {
88                    console.error('f-customer-vip-price: 获取商品详情页信息失败', e);
89                }
90            }
91
92            // 2. 处理商品列表中的价格节点(商品卡片内)
93            const pricesNodes = root.querySelectorAll('f-product-prices');
94
95            pricesNodes.forEach(node => {
96                // 跳过已经处理过的商品详情页节点
97                if (node === this.productInfoNode) return;
98
99                const card = node.closest('.f-product-card');
100                if (!card) return;
101
102                // 查找最近的父级 data-product-id 元素获取商品ID
103                const productId = card.getAttribute('data-product-id');
104                if (!productId || this.requestedIds.has(productId)) return;
105
106                // 将节点加入队列,同一个商品ID可能对应多个节点
107                if (!this.queue.has(productId)) this.queue.set(productId, []);
108                if (!this.queue.get(productId).includes(node)) {
109                    this.queue.get(productId).push(node);
110                    $log(`添加商品卡片价格节点: 商品ID ${productId}`);
111                }
112            });
113
114            // 如果有商品卡片节点,安排微任务批量处理
115            if (this.queue.size > 0) {
116                // $log('待处理商品卡片队列:', Array.from(this.queue.keys()));
117                this.scheduleFetch();
118            }
119        }
120
121        /**
122         * 手动刷新会员价格
123         * 在商品列表更新后手动调用此方法
124         * @param {HTMLElement} container - 可选,指定刷新的容器范围,不传则刷新整个页面
125         */
126        refresh(container = document) {
127            $log('手动刷新会员价格', container);
128
129            if (!this.isVipPriceShow) {
130                $log('会员价格功能未启用');
131                return;
132            }
133
134            // 清空已请求的商品ID记录,允许重新请求
135            this.requestedIds.clear();
136
137            // 清空队列
138            this.queue.clear();
139
140            // 清空上次请求记录
141            this.lastRequestedKey = '';
142
143            // 重新收集节点
144            this.collectNewNodes(container);
145
146            // 如果是商品详情页,重新获取变体价格
147            if (window?.baseData?.page_type === 'product' && this.productInfoId) {
148                this.fetchSingleProductPrice(this.productInfoId, this.productInfoNode);
149            }
150        }
151
152
153        /**
154         * 单独获取单个商品的价格并渲染(用于商品详情页或快速购买弹窗)
155         * @param {string} productId - 商品ID
156         * @param {Element} node - 价格节点
157         */
158        async fetchSingleProductPrice(productId, node) {
159            if (!productId || !node) return;
160
161            $log('单独处理商品详情页/快速购买价格', {productId, node});
162
163            try {
164                // 标记为已请求,避免重复请求
165                this.requestedIds.add(productId);
166
167                const data = await api.post(this.productApiUrl, {
168                    product_id: productId
169                });
170
171                let product_prices = data?.variant_prices || {};
172                $log('商品详情页变体价格', product_prices)
173                if (Object.keys(product_prices).length > 0) {
174                    this.product_variant_prices = product_prices;
175                    this.renderProductVipPrice(this.getSelectedVariant());
176                } else {
177                    $log('商品详情页/快速购买无会员价数据', productId);
178                }
179            } catch (err) {
180                console.error('f-customer-vip-price: 获取商品详情页会员价失败', err);
181                // 如果失败,从requestedIds中移除,允许重试
182                this.requestedIds.delete(productId);
183            }
184        }
185
186        syncWithMainProductForm() {
187            // 订阅变体变化事件
188            FecEvents.on(FecTheme.pubSubEvents.optionValueSelected, (payload) => {
189                $log('会员价格-订阅事件-当前选中的变体:', payload.data);
190                this.renderProductVipPrice(payload.data)
191            });
192
193        }
194
195        renderProductVipPrice(selectedVariant) {
196            let variantPrice = this.product_variant_prices[selectedVariant?.id]
197            $log('renderProductVipPrice', selectedVariant, variantPrice);
198
199            if (variantPrice && variantPrice.price) {
200                $log('开始更新',this.product_variant_prices,variantPrice,selectedVariant)
201                if (this.productInfoNode) {
202                    this.renderVipPrice(this.productInfoNode, variantPrice);
203
204                }
205                // 悬浮购物栏
206                let stickyAtc = document.querySelector('sticky-atc f-product-prices')
207                if (stickyAtc) {
208                    this.renderVipPrice(stickyAtc, variantPrice);
209
210                }
211            }
212
213        }
214
215        getSelectedVariant() {//获取商品详情页当前选中变体
216            let productId = this.productInfoId;
217            let selectedVariant = document.querySelector(`variant-picker [data-selected-variant="${productId}"]`)?.innerHTML
218            if (selectedVariant) {
219                return JSON.parse(selectedVariant);
220            } else {
221                return null;
222            }
223        }
224
225        scheduleFetch() {
226            if (this.scheduled) return;
227            this.scheduled = true;
228            // 在当前同步代码执行完毕后,微任务中执行实际请求
229            Promise.resolve().then(() => {
230                this.scheduled = false;
231                this.fetchAndRender();
232            });
233        }
234
235        /**
236         * 批量获取会员价并渲染到对应节点(用于商品列表)
237         */
238        async fetchAndRender() {
239            const productIds = Array.from(this.queue.keys());
240            if (!productIds.length) return;
241
242            // 判断是否重复请求:如果本次待请求的商品ID组合与上次完全相同,则跳过
243            const currentKey = productIds.sort().join(',');
244            if (currentKey === this.lastRequestedKey) {
245                $log('⚠️ f-customer-vip-price: 跳过重复请求', currentKey);
246                return;
247            }
248            this.lastRequestedKey = currentKey;
249
250            try {
251                $log('批量获取商品列表会员价', productIds);
252
253                const data = await api.post(this.apiUrl, {
254                    product_ids: productIds
255                });
256
257                let product_prices = data.product_prices;
258
259                productIds.forEach(productId => {
260                    const nodes = this.queue.get(productId) || [];
261
262                    if (product_prices[productId] &&
263                        product_prices[productId].price &&
264                        product_prices[productId].price > 0) {
265
266                        nodes.forEach(node => {
267                            this.renderVipPrice(node, product_prices[productId]);
268                        });
269                        $log(`商品列表会员价渲染成功: 商品ID ${productId}`);
270                    } else {
271                        $log(`商品列表无会员价数据: 商品ID ${productId}`);
272                    }
273
274                    this.requestedIds.add(productId);
275                    this.queue.delete(productId);
276                });
277            } catch (err) {
278                console.error('f-customer-vip-price: 批量获取会员价失败', err);
279            }
280        }
281
282        /**
283         * 渲染会员价格元素
284         * @param {Element} node - 价格节点(f-product-prices)
285         * @param {Object} priceData - 价格数据
286         */
287        renderVipPrice(node, priceData) {
288            $log('渲染会员价格元素', node, priceData);
289            if (!node || !node.parentNode) return;
290
291            // 检查 node 是否有内容(非空白文本)因为有价格屏蔽,所以可能内部没价格,vip价格也不需要显示了
292            const hasContent = node.textContent && node.textContent.trim().length > 0;
293            if (!hasContent) {
294                $log('node 无内容,跳过渲染会员价');
295                return;
296            }
297            // 检查是否已经插入过会员价元素
298            const nextElement = node.nextElementSibling;
299            if (nextElement && nextElement.classList &&
300                nextElement.classList.contains('customer-vip-price-box')) {
301                // 如果已存在,更新价格
302                const priceTextNode = Array.from(nextElement.childNodes).find(
303                    child => child.nodeType === 3 && child.textContent.trim()
304                );
305                if (priceTextNode) {
306                    priceTextNode.textContent = $formatMoney(priceData?.price);
307                    $log('更新已存在的会员价', priceData?.price);
308                } else {
309                    // 备用方案:直接更新innerHTML
310                    const vipSpan = nextElement.querySelector('.customer-vip-price');
311                    if (vipSpan) {
312                        nextElement.innerHTML = `${$formatMoney(priceData?.price)}<span class="customer-vip-price">VIP</span>`;
313                    }
314                    $log('通过innerHTML更新会员价', priceData?.price);
315                }
316                return;
317            }
318
319            // 创建新元素
320            let productClass = ''
321            if (!node.closest('.f-product-card')) {
322                productClass = 'm:text-xl md:m:text-2xl';
323            }
324            const newElement = document.createElement('div');
325            newElement.className = `customer-vip-price-box f-price__regular ${productClass}`;
326            newElement.innerHTML = `${$formatMoney(priceData?.price)}
327        <span class="customer-vip-price">VIP</span>
328    `;
329
330            // 在 node 后面插入新元素
331            if (node.nextSibling) {
332                node.parentNode.insertBefore(newElement, node.nextSibling);
333            } else {
334                node.parentNode.appendChild(newElement);
335            }
336
337            $log('会员价格渲染完成', {
338                price: priceData?.price,
339                nodeType: node.closest('.f-product-card') ? '商品卡片' : '商品详情/快速购买'
340            });
341        }
342    }
343
344    customElements.define('f-customer-vip-price', FCustomerVipPrice);
345}
346
347// 将实例挂载到全局,方便手动调用
348if (!window.FCustomerVipPrice) {
349    window.FCustomerVipPrice = {
350        instance: null,
351
352        /**
353         * 获取会员价格组件实例
354         */
355        getInstance: function() {
356            if (!this.instance) {
357                const element = document.querySelector('f-customer-vip-price');
358                if (element) {
359                    this.instance = element;
360                }
361            }
362            return this.instance;
363        },
364
365        /**
366         * 手动刷新所有会员价格
367         * @param {HTMLElement} container - 可选,指定刷新的容器范围
368         */
369        refresh: function(container) {
370            const instance = this.getInstance();
371            if (instance && instance.refresh) {
372                instance.refresh(container);
373            } else {
374                $log('找不到 f-customer-vip-price 实例');
375            }
376        }
377    };
378}

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.