Joyor S10-S-Z Electric Scooter – Dual 1000W Motors, 85km Range, Full Suspension, Hydraulic Brakes

€628.98
€1,079.99
-42%
Style:  S10S-Z
Quantity

Description

Joyor S10S (New Z-Version) - The Ultimate 2000W Dual Motor Scooter

Experience the upgraded performance of the Joyor S10S-Z (2026 Edition). Engineered for both urban commuting and off-road adventures, this beast combines raw power with advanced safety features. With dual 1000W motors, hydraulic brakes, and newly integrated turn signals, it offers the best value in its class.

Why Choose the Joyor S10S-Z?

🚀 2000W Dual Motor Power

Conquer hills with ease. The Joyor S10S features two powerful 1000W DC brushless motors, delivering a peak speed of 65km/h. Whether you need rapid acceleration for city traffic or high torque for off-road climbing, this scooter delivers unmatched performance under €1000.

🔋 85km Long Range Battery

Say goodbye to range anxiety. Equipped with a high-capacity 60V 18Ah Lithium battery and Smart Power Management, the S10S-Z provides a travel distance of up to 70-85km on a single charge. Perfect for long-distance commuting without daily charging.

🛑 Hydraulic Brakes & Safety

Safety is upgraded on the Z-Version. It features responsive hydraulic disc brakes (front and rear) for immediate stopping power. Plus, the new integrated turn signals and ultra-bright 1.1W LED headlights ensure you are visible day and night.

🌲 10-Inch Off-Road Tires

Ride smoothly on any terrain. The large 10-inch pneumatic off-road tires combined with a dual suspension system (swingarm design) absorb shocks effectively, providing a stable ride on bumps, gravel, or city potholes.

🔔 Z-Version Upgrade Note: This model (Joyor S10S-Z) is the latest 2026 version of the classic Joyor S10S. It includes enhanced wiring and side turn signals for European road safety compliance.

Technical Specifications

Brand & Model JOYOR S10S-Z (Upgraded S10S)
Motor Power 2000W Dual Motor (2 x 1000W)
Max Speed 65 km/h (Can be limited to 25km/h)
Battery Range 70 - 85 km (Depending on rider weight & terrain)
Battery Specs 60V / 18.0Ah Lithium-ion
Braking System Hydraulic Disc Brakes (Front & Rear)
Tires 10-inch Pneumatic Off-Road Tires
Max Load 120 kg (265 lbs) - Great for heavy adults
Weight 27 kg (Net Weight)
Features Cruise Control, Color LCD Display, USB Port, Turn Signals
Unfolded Dimensions 1215 x 610 x 1230 mm

What's in the Box?

  • 1 x Joyor S10S-Z Electric Scooter (Assembled)
  • 1 x 60V Charger
  • 1 x User Manual & Legal Statement
  • 1 x Tool Kit (Hexagon Wrench & Screws)
  • 1 x Warranty Card (Official EU Support)

Frequently Asked Questions (FAQ)

Q: What is the difference between Joyor S10S and S10S-Z?
A: The S10S-Z is the upgraded 2026 version. It has the same powerful 2000W dual motors but adds turn signals and improved internal wiring for better safety.

Q: Is the Joyor S10S waterproof?
A: The scooter has an IP54 rating, meaning it is resistant to water splashes and light rain. However, we do not recommend riding in heavy rain or submerging it.

Q: Does this scooter have hydraulic brakes?
A: Yes! The Joyor S10S-Z is equipped with premium hydraulic disc brakes on both the front and rear wheels for maximum stopping power.

Dual Motor Power for Just €629

Joyor S10S-ZDual 1000W Brushless Motors | 35% Grade Climbing

Engineered with front and rear 1000W brushless motors for maximum torque and hill-climbing capability up to 35% incline. Built for urban efficiency and off-road readiness.

60V 18Ah High-Capacity Battery | Up to 75km Range

The S10-S-Z features a built-in 60V 18Ah lithium battery, delivering extended range of up to 85km on a single charge — perfect for daily commutes and weekend getaways.

120mm Wear-Resistant Brake Discs | Precision & Durability

Engineered with high-grade brake pads and 120mm discs, rigorously tested for longevity and consistent stopping power — ensuring reliable control and safety in every ride.

Patented Dual Elastomer Suspension | Rocker Arm Design

Engineered with high-elasticity elastomers and a rocker arm structure, this patented suspension system absorbs bumps seamlessly — delivering flat-road comfort on any terrain.

10” Inflatable Off-Road Tires | All-Terrain Grip & Stability

Designed for adventure, the 10-inch pneumatic tires provide superior traction on mud, gravel, and steep slopes, ensuring confident handling and smooth control on rugged terrain.

High-Definition Color Display | Intelligent Ride Control

Featuring a vibrant full-color LCD screen with real-time data including speed, battery level, gear status, and riding modes. Intuitive interface for seamless control and enhanced visibility in all conditions.

More Details
const isSpecialHeroTheme = window.SHOPLAZZA?.theme?.merchant_theme_name == 'Hero' && window.SHOPLAZZA?.theme?.merchant_theme_c_version == '2.2.19'; const specialHeroThemeClassName = 'hero_2_2_19_smart_recommend_block'; class SpzSmartBlockComponent extends SPZ.BaseElement { constructor(element) { super(element); this.templates_ = null; this.container_ = null; this.i18n_ = {}; this.config_ = {}; this.show_type_ = 3; this.product_resource_id_ = ''; this.collection_resource_id_ = ''; this.cart_items_ = []; this.customer_id_ = ''; this.order_id_ = ''; } static deferredMount() { return false; } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback() { const template_type = window.C_SETTINGS.meta.page.template_type; if (template_type === 1) { this.show_type_ = 3; this.product_resource_id_ = window.C_SETTINGS.meta.page.resource_id; } else if (template_type === 2) { this.show_type_ = 4; this.collection_resource_id_ = window.C_SETTINGS.meta.page.resource_id; } else if (template_type === 15){ this.show_type_ = 5; } else if (template_type === 13){ this.show_type_ = 6; } else if (template_type === 20){ this.show_type_ = 7; this.customer_id_ = window.C_SETTINGS.customer.customer_id; } else if (template_type === 35){ this.show_type_ = 8; this.order_id_ = window.location.pathname.split('/').pop(); } this.templates_ = SPZServices.templatesForDoc(this.element); this.setAction_(); } mountCallback() { const that = this; const themeName = window.C_SETTINGS.theme.merchant_theme_name; const isGeek = /Geek/.test(themeName); this.fetchRules().then((res) => { if (res && res.rules && res.rules.length) { const blockEl = document.getElementById('smart_recommend_block'); this.initBlockClass(blockEl); this.initItemClass(blockEl); SPZ.whenApiDefined(blockEl).then((api) => { api.render({data: res}, true).then(() => { if (isGeek && that.show_type_ === 6) { blockEl.querySelector('.plugin_container_wrpper').style.padding = '30px 0'; } const recommendStyle = document.createElement('style'); recommendStyle.innerHTML = ` .plugin__recommend_container,.app-recommend-card { display: none !important; } `; document.head.appendChild(recommendStyle); const fetchList = []; res.rules.forEach((rule) => { fetchList.push(this.fetchRuleProductList(rule.id)); }); const fetchAll = Promise.all(fetchList); fetchAll.then((p_res) => { res.rules.forEach((rule, index) => { rule.products = p_res[index] && p_res[index].products; if (rule.products && rule.products.length) { const modalRender = document.getElementById('smart_recommend_js_root'); const $dest = document.getElementById('cart'); const isLifeStyle = /Life.*Style/.test(window.C_SETTINGS.theme.merchant_theme_name); if (modalRender && isLifeStyle && $dest.clientWidth > 767) { modalRender.classList.add('zb-mt-[-180px]') } } const ruleEl = document.getElementById('smart_recommend_rule_' + rule.id); SPZ.whenApiDefined(ruleEl).then((api) => { api.render({data: rule}, true).then(() => { that.impressListen(`#smart_recommend_rule_ul_${rule.id}`, function(){ that.trackRuleImpress(rule); }); const btnElList = document.querySelectorAll(`#smart_recommend_rule_ul_${rule.id} button`); btnElList.forEach((btnEl) => { if (btnEl && rule.config && rule.config.quick_shop_button_bg_color && rule.config.quick_shop_button_text_color) { btnEl.style.backgroundColor = rule.config.quick_shop_button_bg_color; btnEl.style.color = rule.config.quick_shop_button_text_color; } }); if (isSpecialHeroTheme) { ruleEl.querySelectorAll(`.smart_recommend_title`).forEach(dom=>{ dom.classList.add('type-title-font-family'); }); document.querySelectorAll(`.${specialHeroThemeClassName} #smart_recommend_rule_ul_${rule.id} .zb-recommend-price-line-through .money`).forEach(dom=>{ dom.classList.add('type-body-font-family'); }); }; }); }); }); }); }) }) } else { if (window.top !== window.self) { const template_type = window.C_SETTINGS.meta.page.template_type; const holderEl = document.getElementById('smart_recommend_preview_no_data_placeholder'); SPZ.whenApiDefined(holderEl).then((api) => { api.render({data: { isCart: template_type === 13, isCollection: template_type === 2, isProduct: template_type === 1, isIndex: template_type === 15 }}, true); }); } } }); } initBlockClass(blockEl) { if (!blockEl) return; if (blockEl.parentElement && blockEl.parentElement.offsetWidth === document.body.clientWidth) { blockEl.classList.add('smart_recommend_block_fullscreen'); }; if (isSpecialHeroTheme) { blockEl.classList.add(specialHeroThemeClassName); }; } initItemClass(blockEl) { if (blockEl) { const containerWidth = blockEl.offsetWidth; let itemWidth = ''; if (containerWidth > 780) { itemWidth = '16%'; } else if (containerWidth > 600) { itemWidth = '20%'; } else { itemWidth = '24%'; } const itemStyleEl = document.createElement('style'); itemStyleEl.innerHTML = `.zb-recommend-li-item{ width: ${itemWidth}; }`; document.body.appendChild(itemStyleEl); } } setAction_() { this.registerAction('quickShop', (data) => { const that = this; const product_id = data.args.product_id; const productIndex = data.args.productIndex; const rule_id = data.args.rule_id; const ssp = data.args.ssp; const scm = data.args.scm; const cfb = data.args.cfb; const ifb = data.args.ifb; const modalRender = document.getElementById('smart_recommend_product_modal_render'); if (modalRender) { document.body.appendChild(modalRender); } if (product_id) { this.fetchProductData(product_id).then((res) => { const product = res.products && res.products.length && res.products[0] || {}; product.cfb = cfb; product.ifb = ifb; SPZ.whenApiDefined(modalRender).then((api) => { api.render({product: product, productIndex: productIndex, rule_id: rule_id, ssp: ssp, scm: scm, show_type: that.show_type_}, true).then(() => { const modalEl = document.getElementById('smart_recommend_product_modal'); SPZ.whenApiDefined(modalEl).then((modal) => { that.impressListen('#smart_recommend_product_modal', function(){ that.trackQuickShop({ rule_id: rule_id, product_id: product_id }); }); modal.open(); }); const formEl = document.getElementById('smart_recommend_product_form'); SPZ.whenApiDefined(formEl).then((form) => { form.setProduct(product); }); const variantEl = document.getElementById('smart_recommend_product_variants'); SPZ.whenApiDefined(variantEl).then((variant) => { variant.handleRender(product); }); }); }) }); } }); this.registerAction('handleScroll', (data) => { this.directTo(data.args.rule_id, data.args.direction); }); this.registerAction('handleProductChange', (data) => { const variant = data.args.data.variant; const product = data.args.data.product; const imageRenderEl = document.getElementById('smart_recommend_product_image'); SPZ.whenApiDefined(imageRenderEl).then((api) => { api.render({ variant: variant, product: product }); }); }); this.registerAction('handleAtcSuccess', (detail) => { const data = detail.args; data.data.product = data.data.product || {}; data.data.variant = data.data.variant || {}; const product_id = data.data.product.id; const product_title = data.data.product.title; const variant_id = data.data.variant.id; const price = data.data.variant.price; const rule_id = data.rule_id; const aid = `smart_recommend.${this.show_type_}.${rule_id}`; const ifb = data.data.product.ifb; const cfb = data.data.product.cfb; const ssp = data.ssp; const scm = data.scm; const spm = `smart_recommend_${this.show_type_}.${data.spmIndex}`; const params = { id: product_id, product_id: product_id, number: 1, name: product_title, variant_id: variant_id, childrenId: variant_id, item_price: price, source: 'add_to_cart', _extra: { aid: aid, ifb: ifb, cfb: cfb, scm: scm, spm: `..${window.C_SETTINGS.meta.page.template_name}.${spm}`, ssp: ssp, } }; this.tranckAddToCart(params); }); this.registerAction('addATCHook', (data) => { const params = data.args; const spm = `smart_recommend_${this.show_type_}.${params.spmIndex}`; this.myInterceptor_ = window.djInterceptors && window.djInterceptors.track.use({ event: 'dj.addToCart', params: { aid: `smart_recommend.${this.show_type_}.` + params.rule_id, ssp: params.ssp, scm: params.scm, cfb: params.cfb, spm: `..${window.C_SETTINGS.meta.page.template_name}.${spm}`, }, once: true }); }); } tranckAddToCart(detail) { if (window.$) { window.$(document.body).trigger('dj.addToCart', detail); } } fetchRules() { const payload = { show_type: this.show_type_, }; let that = this; if (this.show_type_ === 6) { let line_items = []; return this.fetchCart().then((res) => { if (res && res.cart && res.cart.line_items) { line_items = res.cart.line_items.map((item) => { return { product_id: item.product_id, variant_id: item.variant_id, quantity: item.quantity, price: item.price } }); } payload.line_items = line_items; that.cart_items_ = line_items; return that.fetchRulesRequest(payload); }); } else { if (this.show_type_ === 3) { payload.line_items = [{ product_id: this.product_resource_id_ }]; } else if (this.show_type_ === 4) { payload.collection_id = this.collection_resource_id_; } else if (this.show_type_ === 7) { payload.customer_id = this.customer_id_; } else if (this.show_type_ === 8) { payload.order_id = this.order_id_; } return this.fetchRulesRequest(payload); } } fetchRulesRequest(payload) { return fetch(window.C_SETTINGS.routes.root + "/api/possum/recommend_query", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function(res){ if(res.ok){ return res.json(); } }); } fetchCart() { return fetch(`/api/cart/cart-select?r=${Math.random().toString(36).slice(-4)}`) .then((res) => { if (res.ok) { return res.json(); } }); } fetchRuleProductList(rule_id) { const payload = { page: 1, limit: 100, fields: ["title", "url", "image", "min_price_variant.price", "min_price_variant.compare_at_price"], rule_id: rule_id, }; if (this.show_type_ === 3) { payload.line_items = [{ product_id: this.product_resource_id_ }]; } else if (this.show_type_ === 4) { payload.collection_id = this.collection_resource_id_; } else if (this.show_type_ === 6) { payload.line_items = this.cart_items_; } else if (this.show_type_ === 7) { payload.customer_id = this.customer_id_; } else if (this.show_type_ === 8) { payload.order_id = this.order_id_; } return fetch(window.C_SETTINGS.routes.root + "/api/possum/recommend_products", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function(res){ if(res.ok){ return res.json(); } }).catch(function(err){ console.log(err); }); } fetchProductData(product_id) { return fetch(window.C_SETTINGS.routes.root + "/api/possum/products", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_ids: [product_id], fields: [ "images", "options", "min_price_variant", "variants"] }) }).then(function(res){ if(res.ok){ return res.json(); } }).catch(function(err){ console.log(err); const loadingEl = document.getElementById('smart_recommend_loading'); if (loadingEl) { loadingEl.style.display = 'none'; } }); } getStyle(ele, style) { if (!ele) return; if (window.getComputedStyle) { return window.getComputedStyle(ele)[style]; } return ele.currentStyle[style]; } directTo(id, direction) { const scrollElement = document.getElementById(`smart_recommend_rule_ul_${id}`); const blockWidth = parseInt(this.getStyle(scrollElement, 'width')); const scrollLength = (blockWidth * 0.19 - 12) * 5; const scrollPoint = scrollElement.scrollWidth - scrollElement.clientWidth; if (!scrollElement) return; if (direction === 'left') { if (document.dir === 'rtl') { scrollElement.scrollTo({ left: Math.abs(scrollElement.scrollLeft) >= scrollPoint - 100 ? 0 : scrollElement.scrollLeft - scrollLength, behavior: 'smooth' }); return; } scrollElement.scrollTo({ left: Math.max(scrollElement.scrollLeft - scrollLength, 0), behavior: 'smooth' }); } else { if (document.dir === 'rtl') { scrollElement.scrollTo({ left: Math.abs(scrollElement.scrollLeft) >= scrollPoint + 100 ? 0 : scrollElement.scrollLeft + scrollLength, behavior: 'smooth' }); return; } scrollElement.scrollTo({ left: scrollElement.scrollLeft >= scrollPoint - 100 ? 0 : scrollElement.scrollLeft + scrollLength, behavior: 'smooth' }); } } trackRuleImpress(rule) { if (window.sa && window.sa.track) { window.sa.track("plugin_common", { plugin_name: "upsell", event_type: "impressions", rule_id: rule.id, ssp: rule.ssp, scm: rule.scm, show_type: this.show_type_, support_app_block: window.C_SETTINGS.theme.support_app_block }); window.sa.track("module_impressions", { aid: `smart_recommend.${this.show_type_}.${rule.id}`, support_app_block: window.C_SETTINGS.theme.support_app_block }); } } trackQuickShop(data) { window.sa && sa.track && sa.track("plugin_common", { plugin_name: "upsell", event_type: "quick_shop", rule_id: data.rule_id, product_id: data.product_id, show_type: this.show_type_, }); } impressListen(selector, cb) { const el = document.querySelector(selector); const onImpress = (e) => { if (e) { e.stopPropagation(); } cb(); }; if (el && !el.getAttribute('imprsd')) { el.addEventListener('impress', onImpress) } else if (el) { onImpress(); } } } SPZ.defineElement('spz-custom-smart-block', SpzSmartBlockComponent);
const TAG = 'spz-custom-revue-util'; const DEFAULT_DELAY_TIME = 100; class SpzCustomRevueUtil extends SPZ.BaseElement { constructor(element) { super(element); this.templates_ = SPZServices.templatesForDoc(); } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); } static deferredMount() { return false; } mountCallback() { } debounceRender(el, thisEl, containerStr) { return this.smoothRender_(el, thisEl, containerStr).then(() => this.attemptToFit_(thisEl)); } smoothRender_(newEl, thisEl, containerStr) { const that = this; that.appendAsUnvisibleContainer_(newEl, thisEl); const components = newEl.querySelectorAll('[layout]'); return Promise.race([ Promise.all( Array.prototype.map.call(components, (e) => SPZ.whenDefined(e).then(() => e.whenBuilt()) ) ), SPZServices.timerFor(that.win).promise(DEFAULT_DELAY_TIME), ]).then(() => { return containerStr !== 'form_' ? thisEl.mutateElement(() => that.quickReplace(thisEl, newEl)) : thisEl.mutateElement(() => that.quickReplaceForm(thisEl, newEl)); }); } quickReplace(thisEl, newEl) { thisEl.container_ && this.toggleVisible_(thisEl.container_); this.toggleVisible_(newEl, true); thisEl.container_ && SPZCore.Dom.removeElement(thisEl.container_); thisEl.container_ = newEl; }; quickReplaceForm(thisEl, newEl) { thisEl.form_ && this.toggleVisible_(thisEl.form_); this.toggleVisible_(newEl, true); const children = thisEl.form_.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.toggleVisible_(thisEl.form_, true); thisEl.form_.appendChild(newEl); }; appendAsUnvisibleContainer_(el, thisEl) { this.toggleVisible_(el); thisEl.element.appendChild(el); } attemptToFit_(thisEl) { const fitFunc = () => { thisEl.mutateElement(this.setElementHeight_.bind(thisEl)); }; const container = thisEl.container_ || thisEl.form_; if (container) { const children = container.querySelectorAll('*:not(template)'); const spzChildren = Array.prototype.filter .call(children, SPZUtils.isSpzElement) .filter((e) => !(e.isMount && e.isMount())); spzChildren .map((e) => SPZ.whenDefined(e).then(() => e.whenMounted())) .forEach((p) => p.then(() => fitFunc())); } return fitFunc(); } setElementHeight_() { const targetHeight = (this.container_ || this.form_)?./*OK*/ scrollHeight; const height = this.element./*OK*/ offsetHeight; if (height !== targetHeight) { SPZCore.Dom.setStyles(this.element, { height: `${targetHeight}px`, }); } } toggleVisible_(el, visible = false) { if (!visible) { el.classList.add('i-spzhtml-layout-fill'); SPZCore.Dom.setStyles(el, { 'z-index': -100000, 'opacity': 0, }); } else { el.classList.remove('i-spzhtml-layout-fill'); SPZCore.Dom.setStyles(el, { 'z-index': 'auto', 'opacity': 1, }); } } setMinWidth_() { const targetWidth = this.container_?./*OK*/ scrollWidth; const width = this.element./*OK*/ offsetWidth; if (width !== targetWidth) { SPZCore.Dom.setStyles(this.element, { 'min-width': `${targetWidth}px`, }); } } triggerEvent_ = (name, data) => { const event = SPZUtils.Event.create(this.win, `${TAG}.${name}`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SpzCustomRevueUtil); const TAG = 'spz-custom-revue-render'; class SPZCustomRevueRender extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); } mountCallback = () => {} render = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { if (this.element.children.length > 0) { this.element.children[0].style.display = 'none'; } this.element.appendChild(el); // const utilsEl = document.getElementById('spz_custom_revue_util'); // utilsEl && SPZ.whenApiDefined(utilsEl).then((api) => { // api.debounceRender(el, this); // }); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueRender) const TAG = 'spz-custom-revue-star'; class SPZCustomRevueStar extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.starNum = this.element.getAttribute('starNum'); this.starTotal = this.element.getAttribute('starTotal'); this.showStarText = this.element.getAttribute('showStarText'); this.starColor = this.element.getAttribute('color'); this.interact = this.element.getAttribute('interact'); this.starSize = this.element.getAttribute('starSize') || 14; } mountCallback = () => { this.doRender_({ starTotal: this.starTotal, totalArray: Array.from({ length: Number(this.starTotal) }, (v, k) => k + 1), starNum: this.starNum, showStarText: this.showStarText, starColor: this.starColor, starSize: this.starSize }).then(() => { if (this.interact) { this.addEventListeners_(); } }); } addEventListeners_ = () => { const stars = document.querySelectorAll('.revue-star__star'); stars.forEach(star => { star.addEventListener('click', event => { const starEl = star.closest('.revue-star__star'); const starIndex = Number(starEl.dataset.index); let isHalf = event.offsetX < star.offsetWidth / 2; // rtl if (document.documentElement.getAttribute('dir') === 'rtl') { isHalf = event.offsetX > star.offsetWidth / 2; } const starValue = isHalf ? starIndex - 0.5 : starIndex; this.starClickHandler_({ value: starValue }); }); }); } renderStar = () => { const isRtl = document.documentElement.getAttribute('dir') === 'rtl'; const stars = this.element.querySelectorAll('.revue-star__star'); stars.forEach((star, i) => { const starIndex = i + 1; const starEl = star.querySelector('svg:nth-child(2)'); const isHalf = this.starNum % 1 > 0 && Math.ceil(this.starNum) === starIndex; const isSolid = starIndex <= Math.ceil(this.starNum); starEl.style.display = isSolid ? 'block' : 'none'; if (isHalf) { if (isRtl) { // RTL布局下,如果是半星,显示星星的右半边 starEl.style.clipPath = `polygon(50% 0, 100% 0, 100% 100%, 50% 100%)`; } else { // LTR布局下,如果是半星,显示星星的左半边 starEl.style.clipPath = `polygon(0 0, 50% 0, 50% 100%, 0 100%)`; } } else { starEl.style.clipPath = `polygon(0 0, 100% 0, 100% 100%, 0 100%)` } }); const showCountEle = this.element.querySelector('#revue-star-show-count'); showCountEle && SPZ.whenApiDefined(showCountEle).then((api) => { api.render({ starNum: this.starNum, starTotal: this.starTotal }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, { starSize: this.starSize, ...data }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }) .then(() => { this.starNum = data.starNum; this.renderStar(); }); } starClickHandler_ = (event) => { this.starNum = event.value; this.renderStar(); this.triggerEvent_('change', { value: event.value }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueStar) const TAG = 'spz-custom-revue-progress'; class SPZCustomRevueProgress extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > (window.breakpoint || 960); this.height = '6px'; this.show_percentage = this.element.getAttribute('show_percentage') || 'false'; this.show_percentage_num = this.element.getAttribute('show_percentage_num') || 100; this.color = this.element.getAttribute('color') || '#000000'; this.count = this.element.getAttribute('count'); this.total = this.element.getAttribute('total'); } mountCallback = () => { this.doRender_({ count: Number(this.count), total: Number(this.total), height: this.height, color: this.color, show_percentage: this.show_percentage, show_percentage_num: this.show_percentage_num }).then(() => { }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueProgress) const TAG = 'spz-custom-revue-like'; class SPZCustomRevueLike extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.grayColor = this.element.getAttribute('gray_color') || "#BDBDBD"; this.likedColor = this.element.getAttribute('like_color') || "#FFCB44"; this.color = this.grayColor; this.count = this.element.getAttribute('count'); this.revueId = this.element.getAttribute('revue-id'); this.location = this.element.getAttribute('location'); } mountCallback = () => { const likes = sessionStorage.getItem('likes') ? JSON.parse(sessionStorage.getItem('likes')) : []; const like = likes.find(item => item.id === this.revueId); if (like) { this.color = like.like_status === 1 ? this.likedColor : this.grayColor; } // 如果location是modal,则找到相同revue-id的list的元素,拿到其count,存在list count变了,但是modal的count没变的情况 if (this.location === 'modal') { const listElement = document.querySelector(`spz-custom-revue-like[revue-id="${this.revueId}"] .revue-like-count`); if (listElement) { this.count = listElement.getAttribute('data-real-count'); } } this.doRender_({ color: this.color, count: this.count }).then(() => { this.addEventListeners_(); if(this.location === 'list') { // modal数量变更,list同步变更 document.addEventListener('like-clicked', (e) => { if (e.detail.location !== this.location && e.detail.id === this.revueId) { this.color = e.detail.like_status === 1 ? this.likedColor : this.grayColor; this.count = e.detail.count; this.element.querySelector('.revue-like__icon').querySelector('svg').setAttribute('fill', this.color); this.element.querySelector('.revue-like__icon').querySelector('svg').querySelector('path').setAttribute('fill', this.color); this.element.querySelector('.revue-like-count').innerText = this.count > 99 ? '99+' : this.count < 1 ? '' : this.count; this.element.querySelector('.revue-like-count').setAttribute('data-real-count', this.count); if(this.count > 0){ this.element.querySelector('.revue-like-count').classList.remove('hidden'); }else{ this.element.querySelector('.revue-like-count').classList.add('hidden'); } } }); } }); } addEventListeners_ = () => { const icon = this.element.querySelector('.revue-like__icon'); icon.addEventListener('click', (e) => { e.stopPropagation(); const likeStatus = this.color === this.likedColor ? 0 : 1; this.color = this.color === this.likedColor ? this.grayColor : this.likedColor; this.count = likeStatus === 1 ? parseInt(this.count) + 1 : parseInt(this.count) - 1; icon.querySelector('svg').setAttribute('fill', this.color); icon.querySelector('svg').querySelector('path').setAttribute('fill', this.color); this.element.querySelector('.revue-like-count').innerText = this.count > 99 ? '99+' : this.count < 1 ? '' : this.count; this.element.querySelector('.revue-like-count').setAttribute('data-real-count', this.count); if(this.count > 0){ this.element.querySelector('.revue-like-count').classList.remove('hidden'); }else{ this.element.querySelector('.revue-like-count').classList.add('hidden'); } this.postLike(likeStatus); if (this.location === 'modal') { const clickedEvent = new CustomEvent('like-clicked', { detail: { id: this.revueId, like_status: likeStatus, count: this.count, location: this.location } }); document.dispatchEvent(clickedEvent); } }); } setLikeToStorage = (likeToStore) => { if (typeof (Storage) !== 'function') return; const likesInStore = sessionStorage.getItem('likes') ? JSON.parse(sessionStorage.getItem('likes')) : []; const reviewIndex = likesInStore.findIndex(item => item.id === likeToStore.id); if (reviewIndex !== -1) { likesInStore[reviewIndex].like_status = likeToStore.like_status; likesInStore[reviewIndex].count = likeToStore.count; } else { likesInStore.push(likeToStore); } sessionStorage.setItem('likes', JSON.stringify(likesInStore)); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } postLike = (likeStatus) => { fetch('/api/comment/like', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this.revueId, status: likeStatus }) }).then((res) => { if (res.status === 200) { this.setLikeToStorage({ id: this.revueId, like_status: likeStatus, count: this.count }); } }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueLike) const TAG = 'spz-custom-revue-media'; class SPZCustomRevueMedia extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.imgCover = this.element.getAttribute('img-cover') ?? false; this.pc_layout = this.element.getAttribute('pc-layout') ?? ''; // data-images 格式为 xxxx.png?width=1&height=1,xxxx.png?width=1&height=1 const images = this.element.getAttribute('data-images').split(',') || []; const parsedImages = images.map(image => { return this.mediaParse_(image); }); this.images = parsedImages; this.isPC = window.innerWidth > 960; } mountCallback = () => { this.doRender_({ images: this.images, isPC: this.isPC, imgCover: this.imgCover, pc_layout: this.pc_layout }).then(() => { this.addEventListeners_(); }); } addEventListeners_ = () => { const images = this.element.querySelectorAll('.revue-image-item'); images.forEach((image, index) => { image.addEventListener('click', () => { const carousel = document.querySelector('#revue-image-carousel-render'); carousel && SPZ.whenApiDefined(carousel).then((api) => { const width = this.isPC ? 460 : window.innerWidth * 0.9; const height = this.isPC ? 630 : 500; api.render({ images: this.images, index: index, width: width, height: height }); }); }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } mediaParse_ = function (url) { var result = {}; try { url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (str, key, value) { try { result[key] = decodeURIComponent(value); } catch (e) { result[key] = value; } }); result.preview_image = url.split('?')[0]; } catch (e) {}; return result; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueMedia) const TAG = 'spz-custom-revue-sort'; class SPZCustomRevueSort extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > 960; this.width = this.isPC ? `${this.element.getAttribute('width') || 150}px` : '100%'; this.randomStr = Math.random().toString(36).substr(2); this.sectionId = this.element.getAttribute('section-id') || '1743495266775'; this.prefix = this.element.getAttribute('prefix'); } mountCallback = () => { const data = { width: this.width, randomStr: this.randomStr }; this.doRender_(data).then(() => { let revueSortListRender = this.isPC ? this.element.querySelector(`#${this.prefix}-revue-sort-list-render-${this.sectionId}`) : this.element.querySelector(`#${this.prefix}-revue-sort-dropdown-render-${this.sectionId}`); revueSortListRender && SPZ.whenApiDefined(revueSortListRender).then((api) => { api.render(data).then(() => { if (this.isPC) { this.addEventListenersForPC_(); } else { this.addEventListenersForMobile_(); } }); }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } addEventListenersForPC_ = () => { const revueSelectList = this.element.querySelector('.revue_select_list'); const revueSelectItem = this.element.querySelectorAll('.revue_select_item'); const revueSelectSortIcon = this.element.querySelector(`#${this.prefix}-revue_select_sort_icon-${this.sectionId}`); revueSelectItem.forEach(item => { item.addEventListener('click', () => { const sort = item.getAttribute('data-sort'); const direction = item.getAttribute('data-direction'); this.triggerEvent_('sort', { sort, direction }); this.element.querySelector('.revue_select_label').innerText = item.innerText; revueSelectList.classList.remove('revue_select_list_active'); const revueChecked = this.element.querySelector(`#${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); const pcDropdownEle = document.querySelector(`#${this.prefix}-revue-sort-pc-dropdown-${this.sectionId}`); if (!revueSelectSortIcon.classList.contains('up_icon')) { return; } revueSelectSortIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); }); window.addEventListener('scroll', (e) => { if (!revueSelectSortIcon || !revueSelectSortIcon.classList.contains('up_icon')) { return; } revueSelectSortIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); } addEventListenersForMobile_ = () => { const revueSortDropdownRender = document.querySelector(`#${this.prefix}-revue-sort-dropdown-render-${this.sectionId}`); revueSortDropdownRender && SPZ.whenApiDefined(revueSortDropdownRender).then(async (api) => { await api.render(); const revueSortDropdownItem = document.querySelectorAll(`#${this.prefix}-revue-sort-dropdown-${this.sectionId} .revue_sort_dropdown_item`); revueSortDropdownItem.forEach(item => { item.addEventListener('click', () => { const sort = item.getAttribute('data-sort'); const direction = item.getAttribute('data-direction'); revueSortDropdownItem.forEach((_item)=>{_item.classList.remove('selected')}) item.classList.add('selected'); // 抛出事件 this.triggerEvent_('sort', { sort, direction }); // 移除revue_checked元素,复制一个新的到当前选中的元素 const revueChecked = document.querySelector(`#${this.prefix}-revue-sort-dropdown-${this.sectionId} #${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); const mDropdownEle = document.querySelector(`#${this.prefix}-revue-sort-dropdown-${this.sectionId}`); SPZ.whenApiDefined(mDropdownEle).then((api) => { api.close(); }); }); }); }) } } SPZ.defineElement(TAG, SPZCustomRevueSort) const TAG = 'spz-custom-revue-type'; class SPZCustomRevueType extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > 960; this.width = this.isPC ? `${this.element.getAttribute('width') || 150}px` : '100%'; this.randomStr = Math.random().toString(36).substr(2); this.sectionId = this.element.getAttribute('section-id') || '1743495266775'; this.prefix = this.element.getAttribute('prefix'); } mountCallback = () => { } render = (data) => { const renderData = { ...data, width: this.width, randomStr: this.randomStr }; return this.templates_ .findAndRenderTemplate(this.element, renderData, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { let revueTypeListRender = this.isPC ? this.element.querySelector(`#${this.prefix}-revue-type-list-render-${this.sectionId}`) : this.element.querySelector(`#${this.prefix}-revue-type-dropdown-render-${this.sectionId}`); revueTypeListRender && SPZ.whenApiDefined(revueTypeListRender).then((api) => { api.render(renderData).then(() => { if (this.isPC) { this.addEventListenersForPC_(); } else { this.addEventListenersForMobile_(); } }); }); }); } addEventListenersForPC_ = () => { const revueSelectList = this.element.querySelector('.revue_select_list'); const revueSelectItem = this.element.querySelectorAll('.revue_select_item'); const revueSelectTypeIcon = this.element.querySelector(`#${this.prefix}-revue_select_type_icon-${this.sectionId}`); revueSelectItem.forEach(item => { item.addEventListener('click', () => { const type = item.getAttribute('data-type'); const direction = item.getAttribute('data-direction'); this.triggerEvent_('type', { type, direction }); this.element.querySelector('.revue_select_label').innerText = item.innerText; revueSelectList.classList.remove('revue_select_list_active'); const revueChecked = this.element.querySelector(`#${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); if (!revueSelectTypeIcon.classList.contains('up_icon')) { return; } const pcDropdownEle = this.element.querySelector(`#${this.prefix}-revue-type-pc-dropdown-${this.sectionId}`); revueSelectTypeIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); }); window.addEventListener('scroll', (e) => { if (!revueSelectTypeIcon.classList.contains('up_icon')) { return; } revueSelectTypeIcon.classList.remove('up_icon'); SPZ.whenApiDefined(pcDropdownEle).then((api) => { api.close(); }); }); } addEventListenersForMobile_ = () => { const revueTypeDropdownItem = this.element.querySelectorAll(`#${this.prefix}-revue-type-dropdown-${this.sectionId} .revue_type_dropdown_item`); revueTypeDropdownItem.forEach(item => { item.addEventListener('click', () => { const type = item.getAttribute('data-type'); const direction = item.getAttribute('data-direction'); revueTypeDropdownItem.forEach((_item)=>{_item.classList.remove('selected')}) item.classList.add('selected'); // 抛出事件 this.triggerEvent_('type', { type, direction }); // 移除revue_checked元素,复制一个新的到当前选中的元素 const revueChecked = this.element.querySelector(`#${this.prefix}-revue-type-dropdown-${this.sectionId} #${this.prefix}-revue_checked`); revueChecked && SPZCore.Dom.removeElement(revueChecked); const revueCheckedClone = revueChecked.cloneNode(true); item.appendChild(revueCheckedClone); const mDropdownEle = this.element.querySelector(`#${this.prefix}-revue-type-dropdown-${this.sectionId}`); SPZ.whenApiDefined(mDropdownEle).then((api) => { api.close(); }); }); }); } } SPZ.defineElement(TAG, SPZCustomRevueType) const TAG = 'spz-custom-revue-pagination'; class SPZCustomRevuePagination extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > (window.breakpoint || 960); this.numItems = this.numItems(); this.pageSize = this.pageSize(); } mountCallback = () => { this.doRender_({ numPages: this.numPages(), pageNum: this.currentPageNumber(), useCallback: true }).then(() => { }); } currentPageNumber() { let pageNum = this.element.getAttribute('page-num'); if (pageNum) return parseInt(pageNum); } numPages() { return Math.ceil(this.numItems / this.pageSize); } numItems() { return parseInt(this.element.getAttribute('num-items')); } pageSize() { return parseInt(this.element.getAttribute('page-size')) || 10; } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, data, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevuePagination) const TAG = 'spz-custom-revue-product'; class SpzCustomRevueProduct extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.section_id = this.element.getAttribute('section-id'); this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.setupAction_(); const url = new URL(window.location.href); this.isPC = window.innerWidth > (window.breakpoint || 960); this.nodata = false; this.firstRender = true; this.commentConfig = {}; this.commentSummary = {}; this.commentList = {}; this.panelId = 'all'; this.sort = 'created_at'; this.direction = 'desc'; this.pageNum = 1; this.pageSize = +window.reviewProductSettings[this.section_id].page_limit; this.pc_layout = window.reviewProductSettings[this.section_id].pc_layout; this.star_least = +window.reviewProductSettings[this.section_id].star_least; this.only_media = window.reviewProductSettings[this.section_id].only_media; this.product_id = window.SHOPLAZZA.meta.page.resource_id; this.isProductPage = '1' == 1; this.isCollectionPage = '1' == 2; this.isCartPage = '1' == 13; this.review_insufficient = window.reviewProductSettings[this.section_id].review_insufficient; // 评论不足类型 this.mini_quantity = window.reviewProductSettings[this.section_id].mini_quantity; // 评论少于一定数量 this.actions = window.reviewProductSettings[this.section_id].actions; // 评论处理方式 this.only_media = window.reviewProductSettings[this.section_id].only_media; // 只显示有图片的评论 this.only_featured = window.reviewProductSettings[this.section_id].only_featured ?? false; // 只显示精选评论 this.display_product_link = window.reviewProductSettings[this.section_id].display_product_link ?? false; // 是否显示商品链接 this.m_loading_type = window.reviewProductSettings[this.section_id].m_loading_type; // 移动端加载方式 this.m_modal_page_limit = window.reviewProductSettings[this.section_id].m_modal_page_limit; // 移动端弹窗加载限制 this.hide_review_section = window.reviewProductSettings[this.section_id].hide_review_section; // 无数据是否隐藏评论组件 this.accent_color = window.reviewProductSettings[this.section_id].accent_color; // 主题色 } mountCallback = () => { this.templates_ .findAndRenderTemplate(this.element, { isPC: this.isPC }, null) .then((el) => { this.element.appendChild(el); this.renderPage(); }) } /* fetch api/comment-config */ fetchCommentConfig_ = async () => { const response = await fetch('/api/comment-config'); return response.json(); } /* api/comment/count-star?product_id=` + `${product.id}` + `&star_least=${block.settings.star_least}*/ fetchCommentSummary_ = async(data) => { const response = await fetch(`/api/v1/comments/summary`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); return response.json(); } /* api/comment/list?star_least=5&onlyimg=0&limit=10&offset=0&sort_by=created_at&product_id=6e9e3113-87fe-49ad-8764-a2333463adea&status=1&sort_direction=desc&show_reply=1 */ fetchCommentList_ = async(data) => { // const response = await fetch(`/api/comment/list?show_product=1&star_least=${data.star_least}&onlyimg=${data.onlyimg}&limit=${data.limit}&offset=${data.offset}&sort_by=${data.sort_by || 'created_at'}&product_id=${data.productId}&status=1&sort_direction=${data.sort_direction || 'desc'}&show_reply=${data.show_reply}`); const response = await fetch('/api/v1/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); return response.json(); } /* fetch api/comment/theme-config?theme_id= */ fetchThemeConfig_ = async(themeId) => { const response = await fetch(`/api/comment/theme-config?theme_id=${themeId}`); return response.json(); } getCommentConfig = () => { return this.fetchCommentConfig_() } getCommentSummary = (data = {}) => { const fetchData = { star_least: this.star_least, product_ids: this.isProductPage ? '9d855425-bbb8-4353-a864-5cbee555ece2' : this.isCartPage ? '' : '', collection_id: this.isCollectionPage ? '' : '', filter_type: this.isProductPage ? 'product' : this.isCollectionPage ? 'collection' : 'store', fill_min_threshold: this.review_insufficient === 'less_than' ? this.mini_quantity : undefined, fill_strategy: this.actions === 'all_product' ? 'store' : '', only_media: this.only_media ? this.only_media : this.panelId !== 'all', only_featured: this.only_featured, ...data, } return this.fetchCommentSummary_(fetchData) } getCommentList = (data = {}) => { const fetchData = { show_product: true, filter_type: (this.isProductPage || this.isCartPage) ? 'product' : this.isCollectionPage ? 'collection' : 'store', star_least: this.star_least, show_reply: true, limit: this.pageSize, offset: (this.pageNum - 1) * this.pageSize, only_media: this.only_media ? this.only_media : this.panelId !== 'all', sort_by: this.sort, sort_direction: this.direction, product_ids: this.isProductPage ? '9d855425-bbb8-4353-a864-5cbee555ece2' : this.isCartPage ? '' : '', collection_id: this.isCollectionPage ? '' : '', only_featured: this.only_featured, fill_strategy: this.actions === 'all_product' ? 'store' : '', fill_min_threshold: this.review_insufficient === 'less_than' ? this.mini_quantity : undefined, ...data, } return this.fetchCommentList_(fetchData) } getPageData = () => { return Promise.all([ this.getCommentConfig(), this.getCommentSummary(), this.getCommentList() ]) } renderPage = async () => { const [commentConfigRes, commentSummaryRes, commentListRes] = await this.getPageData(); let commentConfigData = commentConfigRes.data || {}; let commentSummaryData = commentSummaryRes.data || {}; let commentListData = commentListRes.data || []; this.commentConfig = commentConfigData; this.commentSummary = commentSummaryData; this.commentList = commentListData; this.accent_color = this.accent_color || this.commentConfig.star_color; // 评论不足逻辑:计算最小评论数量阈值 const lessThanCount = (this.actions === "hide" || this.actions === "empty") && this.review_insufficient === 'less_than' ? this.mini_quantity : 1; // 如果评论数量不足,处理空状态 if (commentListData.count < lessThanCount) { this.renderHideSkeleton(); if (this.hide_review_section || this.actions === "hide") { this.renderNoData(); } else if (this.actions === "empty") { // 商品详情页显示空评论状态,其他页面隐藏评论区域 if (this.isProductPage) { this.renderEmptyComment(); } else { this.renderNoData(); } } this.nodata = true; return; } window.addEventListener('resize', SPZCore.Types.throttle(window, this.onResize, 300)); this.renderPageData([this.commentConfig, this.commentSummary, this.commentList]); } onResize = () => { if(this.nodata) { return; } // 判断是否需要重新渲染 if((this.isPC && window.innerWidth > (window.breakpoint || 960)) || (!this.isPC && window.innerWidth < (window.breakpoint || 960))) { return; } this.isPC = window.innerWidth > (window.breakpoint || 960); this.panelId = 'all'; this.sort = 'created_at'; this.direction = 'desc'; this.pageNum = 1; this.templates_ .findAndRenderTemplate(this.element, { isPC: this.isPC }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); this.renderPageData([this.commentConfig, this.commentSummary, this.commentList]); }) } renderPageData = (data) => { const [commentConfigData, commentSummaryData, commentListData] = data; // 渲染头部 this.renderHeader_({ ...commentConfigData, starData: commentSummaryData, listData: commentListData, comment_avg_star: commentSummaryData.comment_avg_star, comment_count: commentSummaryData.comment_count, }); // 有评论逻辑 this.renderStarCounts({ ...commentSummaryData, ...commentConfigData }); if(this.isPC && this.pc_layout === 'single_column') { this.renderCommentTab({ listData: commentListData, isPC: this.isPC, }, `revue-tab-${this.section_id}`); } else { this.renderList_({ listData: commentListData, config: this.commentConfig, shop_name: window.SHOPLAZZA.shop.shop_name, isPC: this.isPC, star_color: this.accent_color, }); } } renderNoData = () => { const sectionEle = document.querySelector(`#revue-product-compo`); if (sectionEle) { sectionEle.setAttribute('hidden', 'true'); } if(window.top === window.self) { // c端不渲染 return; } // b端渲染 const noDataPlaceholder = document.querySelector(`#revue_no_data_placeholder_${this.section_id}`); if(noDataPlaceholder) { SPZ.whenApiDefined(noDataPlaceholder).then(async (api) => { await api.render(); }); } } renderHideSkeleton = () => { const skeletonEle = document.querySelector('#revue_skeleton'); if (skeletonEle) { skeletonEle.classList.add('hidden'); } } renderEmptyComment = () => { const emptyEle = document.querySelector(`#revue-empty-1743495266775`); if(emptyEle) { emptyEle.classList.remove('hidden'); } } renderHeader_ = (data) => { const headerEle = document.querySelector(`#app-review-revue-header-${this.section_id}`); if (headerEle) { SPZ.whenApiDefined(headerEle).then(async (api) => { api.render({ ...data, star_color: this.accent_color, isPC: this.isPC, }); }); } } renderStarCounts = (data, eleId = `revue-summary-${this.section_id}`) => { const ndata = { ...this.commentSummary, star_color: this.accent_color, isPC: this.isPC, ...data, } const summaryEle = document.querySelector(`#${eleId}`); if (summaryEle) { SPZ.whenApiDefined(summaryEle).then((api) => { api.render({ ...ndata, }); }); } } /* 渲染单列布局 (有 tab 和 list) */ renderCommentTab = (data, eleId) => { const elementId = eleId || `revue-tab-${this.section_id}`; const ndata = { listData: this.commentList, isPC: this.isPC, ...data } const tabEle = document.querySelector(`#${elementId}`); let listId; if (tabEle) { SPZ.whenApiDefined(tabEle).then(async (api) => { await api.render({ ...ndata, // suffix: "list", }); if(eleId) { listId = `revue-comment-list-${this.section_id}_tab`; } this.renderList_({ ...ndata, // suffix: "list", }, listId); }); } } /* 只渲染 list */ renderList_ = (data, eleId) => { const listEle = document.querySelector(`#revue-comment-list`); if (listEle && !eleId) { SPZ.whenApiDefined(listEle).then(async (api) => { await api.render({ ...data, // suffix: "list", pageSize: this.pageSize, hasmore: data.listData.has_more, }) let nlist = data.listData.list.map(item => { return { ...item, config: this.commentConfig, star_color: this.accent_color, shop_name: window.SHOPLAZZA.shop.shop_name, current_panel: this.panelId, pageNum: this.pageNum, suffix: data.suffix, show_link: this.display_product_link, } }) let hasmore = data.listData.has_more; if(!this.isPC && this.m_loading_type === 'modal') { nlist = nlist.slice(0, this.m_modal_page_limit); hasmore = true; } api.renderList({ ...data, list: nlist, count: this.panelId === 'all' ? data.listData.count : data.listData.image_count, // suffix: "list", hasmore: hasmore, pageSize: this.pageSize }) }) return; } const viewallListEle = document.querySelector(`#${eleId}`); if (viewallListEle) { SPZ.whenApiDefined(viewallListEle).then(async (api) => { await api.render({ ...data, pageSize: this.pageSize, hasmore: data.listData.has_more, }); let nlist = data.listData.list.map(item => { return { ...item, config: this.commentConfig, star_color: this.accent_color, shop_name: window.SHOPLAZZA.shop.shop_name, current_panel: this.panelId, pageNum: this.pageNum, suffix: data.suffix, show_link: this.display_product_link, } }) api.renderList({ ...data, list: nlist, count: this.panelId === 'all' ? data.listData.count : data.listData.image_count, hasmore: data.listData.has_more, pageSize: this.pageSize, }) }); } } renderCommentList = (data, eleId = 'revue-comment-list', renderType = 'list', redo = false) => { const listEle = document.querySelector(`#${eleId}`); if (listEle) { SPZ.whenApiDefined(listEle).then((api) => { let nlist = data.listData.list.map(item => { return { ...item, config: this.commentConfig, star_color: this.accent_color, shop_name: window.SHOPLAZZA.shop.shop_name, current_panel: this.panelId, pageNum: this.pageNum, hasmore: data.listData.has_more, show_link: this.display_product_link, // suffix: data.suffix, } }) if(!this.isPC && this.m_loading_type === 'modal' && renderType === 'list') { nlist = nlist.slice(0, this.m_modal_page_limit); } api.renderList({ count: this.panelId === 'all' ? data.listData.count : data.listData.image_count, list: nlist, // suffix: "list", hasmore: data.listData.has_more, pageSize: this.pageSize }, redo); }); return; } } renderByScrollPagination = async (eleId, renderType) => { this.pageNum = this.pageNum + 1; const params = {} const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, false); } setupAction_ = () => { this.registerAction('renderTabChangeList', async (invocation) => { // 兼容 ljs-tab 首次加载会触发 tabchange 事件 if(this.firstRender) { this.firstRender = false; return; } const panelId = invocation.args.data.panelId; const { eleId, renderType } = invocation.args; this.panelId = panelId; this.pageNum = 1; this.modalHasMore = true; const params = { // only_media: panelId !== 'all', } const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, true); }); this.registerAction('renderTypeChangeList', async (invocation) => { const { type } = invocation.args.data; const { eleId, renderType } = invocation.args; this.panelId = type; this.pageNum = 1; this.modalHasMore = true; const params = { // only_media: type !== 'all', } const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, true); }); this.registerAction('renderSortedList', async(invocation) => { const { sort, direction } = invocation.args.data; const eleId = invocation.args.eleId; const renderType = invocation.args.renderType; this.sort = sort; this.direction = direction; this.pageNum = 1; this.modalHasMore = true; const params = { sort_by: sort, sort_direction: direction, } const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, true); }); this.registerAction('renderByPagination', async(invocation) => { const { pageNum, eleId, renderType } = invocation.args; this.pageNum = pageNum; const params = {} const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, `revue-comment-list-${this.section_id}_tab`, 'tab', true); const tabsEle = document.querySelector('#revue-product-compo'); if (tabsEle) { tabsEle.scrollIntoView({ behavior: 'smooth' }); } }); this.registerAction('renderByViewMore', async(invocation) => { const { eleId, renderType } = invocation.args; this.pageNum = this.pageNum + 1; const params = {} const res = await this.getCommentList(params); this.renderCommentList({ listData: res.data, }, eleId, renderType, false); }); this.registerAction('refresh', async(invocation) => { this.panelId = 'all'; this.sort = 'created_at'; this.direction = 'desc'; this.pageNum = 1; this.templates_ .findAndRenderTemplate(this.element, { isPC: this.isPC }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); this.renderPage(); }); const productEle = document.querySelector(`#revue-viewall-modal-comp`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { api.refresh(); }); } }); } } SPZ.defineElement(TAG, SpzCustomRevueProduct) (function() { const TAG = 'spz-custom-new-revue'; class SpzCustomNewRevue extends SPZ.BaseElement { constructor(element) { super(element); this.config_ = null; this.loading_ = false; this.accent_color = this.element.getAttribute('accent-color'); this.sectionId = this.element.getAttribute('section-id'); this.prefix = this.element.getAttribute('prefix'); } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.form_ = SPZCore.Dom.scopedQuerySelector( this.element, 'form' ); this.hasShowLengthInputs_ = SPZCore.Dom.scopedQuerySelectorAll( this.form_, '[showlength]' ); [...this.hasShowLengthInputs_].forEach(item => { const countRecordDom = SPZCore.Dom.scopedQuerySelector( this.form_, `#${item.id} ~ div[type="count-record"]` ); if (!countRecordDom) { console.error(`Cannot find count record DOM element for input ${item.id}`); return; } item.addEventListener('input', (e) => { countRecordDom.innerText = `${e.target.value.length}/3000`; }); }); this.setupAction_(); this.getRevueConfigData_(); } setupAction_() { this.registerAction('submitForm', async(invocation) => { if (this.loading_) { return; } this.loading_ = true; const formData = Object.entries(invocation.args.data).reduce((acc, [key, value]) => { if (key === 'star' || key === 'type') { acc[key] = Number(value[0]); } else { acc[key] = value[0]; } return acc; }, {}); try { const data = await fetch('/api/comment', { method: "post", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData) }).then(res => res.json()); if (data.state === 0) { this.triggerEvent_('submitSuccess', { panelId: 'with_photo', message: '' }); return; } throw new Error(data.msg); } catch(e) { e = await e; this.triggerEvent_('submitError', {data: e.message}); } finally { this.loading_ = false; } }); this.registerAction('renderFormStar', async(invocation) => { this.triggerEvent_('rerenderFormStar', { star_color: this.starColor_ }); }) } mountCallback() { } getRevueConfigData_ = () => { fetch('/api/comment-config') .then(res => res.json()) .then(data => { this.config_ = data.data; // anonymous_permission 是否支持匿名 if (!this.config_.anonymous_permission) { const anonymousInput = this.form_.querySelector(`#${this.prefix}-revue-anonymous-${this.sectionId}`); anonymousInput.value = 'false'; anonymousInput.parentNode.classList.add('hidden', 'anonymous-permission-hidden'); } this.starColor_ = this.config_.star_color; if(this.accent_color && this.accent_color != 'null'){ this.starColor_ = this.accent_color; } // render star // star_color 星星颜色 const starEl = this.form_.querySelector(`#${this.prefix}-revue_write_modal_star-${this.sectionId}`); if (starEl) { SPZ.whenApiDefined(starEl).then((api) => { api.render({ star_color: this.starColor_ }); }); } }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported = (layout) => { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SpzCustomNewRevue); })() (function() { const TAG = 'spz-custom-revue-product-info-script'; class SpzCustomRevueProductInfoScript extends SPZ.BaseElement { constructor(element) { super(element); /** @private {!Element} */ this.product_id = null; } async buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.product_id = this.getProductId_(); this.triggerEvent_('init', { product_id: this.product_id }); try { const data = await this.getProductInfo_(); if (data?.data?.product) { this.triggerEvent_('finish', data.data.product); } } catch (error) { console.error('Failed to fetch product info:', error); // Handle the error appropriately } } getProductId_ = () => { return window.SHOPLAZZA.meta.page.resource_id; } async getProductInfo_() { if (!this.product_id) { console.error('Product ID is undefined or null'); return null; } try { const response = await fetch(`/api/products/${this.product_id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error fetching product info:', error); throw error; // Rethrow to be caught by the caller } } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported = (layout) => { return layout == SPZCore.Layout.LOGIC; } } SPZ.defineElement(TAG, SpzCustomRevueProductInfoScript); })() const TAG = 'spz-custom-revue-star'; class SPZCustomRevueStar extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } buildCallback = () => { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.starNum = this.element.getAttribute('starNum'); this.starTotal = this.element.getAttribute('starTotal'); this.showStarText = this.element.getAttribute('showStarText'); this.starColor = this.element.getAttribute('color'); this.interact = this.element.getAttribute('interact'); this.starSize = this.element.getAttribute('starSize') || 14; } mountCallback = () => { this.doRender_({ starTotal: this.starTotal, totalArray: Array.from({ length: Number(this.starTotal) }, (v, k) => k + 1), starNum: this.starNum, showStarText: this.showStarText, starColor: this.starColor, starSize: this.starSize }).then(() => { if (this.interact) { this.addEventListeners_(); } }); } addEventListeners_ = () => { const stars = document.querySelectorAll('.revue-star__star'); stars.forEach(star => { star.addEventListener('click', event => { const starEl = star.closest('.revue-star__star'); const starIndex = Number(starEl.dataset.index); let isHalf = event.offsetX < star.offsetWidth / 2; // rtl if (document.documentElement.getAttribute('dir') === 'rtl') { isHalf = event.offsetX > star.offsetWidth / 2; } const starValue = isHalf ? starIndex - 0.5 : starIndex; this.starClickHandler_({ value: starValue }); }); }); } renderStar = () => { const isRtl = document.documentElement.getAttribute('dir') === 'rtl'; const stars = this.element.querySelectorAll('.revue-star__star'); stars.forEach((star, i) => { const starIndex = i + 1; const starEl = star.querySelector('svg:nth-child(2)'); const isHalf = this.starNum % 1 > 0 && Math.ceil(this.starNum) === starIndex; const isSolid = starIndex <= Math.ceil(this.starNum); starEl.style.display = isSolid ? 'block' : 'none'; if (isHalf) { if (isRtl) { // RTL布局下,如果是半星,显示星星的右半边 starEl.style.clipPath = `polygon(50% 0, 100% 0, 100% 100%, 50% 100%)`; } else { // LTR布局下,如果是半星,显示星星的左半边 starEl.style.clipPath = `polygon(0 0, 50% 0, 50% 100%, 0 100%)`; } } else { starEl.style.clipPath = `polygon(0 0, 100% 0, 100% 100%, 0 100%)` } }); const showCountEle = this.element.querySelector('#revue-star-show-count'); showCountEle && SPZ.whenApiDefined(showCountEle).then((api) => { api.render({ starNum: this.starNum, starTotal: this.starTotal }); }); } doRender_ = (data) => { return this.templates_ .findAndRenderTemplate(this.element, { starSize: this.starSize, ...data }, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }) .then(() => { this.starNum = data.starNum; this.renderStar(); }); } starClickHandler_ = (event) => { this.starNum = event.value; this.renderStar(); this.triggerEvent_('change', { value: event.value }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SPZCustomRevueStar) (function() { const TAG = 'spz-custom-new-revue-files-show'; class SpzCustomNewRevueFilesShow extends SPZ.BaseElement { constructor(element) { super(element); /** @private {!Element} */ this.files_ = [] } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.setupAction_(); this.element.setAttribute('nums', this.files_.length); } mountCallback() { } setupAction_() { this.registerAction('upload', async(invocation) => { const uploadFileList = invocation.args?.data || []; uploadFileList.forEach(file => { if(this.files_.some(item => item.url === file.url)) return this.files_.push(file); }) this.doRender_(); }); this.registerAction('delete', async(invocation) => { this.files_ = this.files_.filter((_, index) => index !== invocation.args.index); this.doRender_(); this.triggerEvent_('delete', { count: this.files_.length, files: this.files_ }); }); this.registerAction('preview', async(invocation) => { let previewFileData = this.files_[invocation.args.index]; if (previewFileData.type === 'video') { previewFileData = {...this.parseVideoSrc_(previewFileData.url), ...previewFileData}; } this.triggerEvent_('preview', previewFileData); }); this.registerAction('clear', async(invocation) => { this.files_ = []; this.doRender_(); }); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } parseVideoSrc_(src) { const url = new URL(src); const params = new URLSearchParams(url.search); return { videoUrl: url.origin + url.pathname, mediaType: params.get('media_type'), vID: params.get('vID'), mp4: params.get('mp4'), hls: params.get('hls') }; } doRender_ = () => { this.triggerEvent_('setInputValue', { data: this.files_ .map(file => { const url = file.type === 'video' ? file.poster : file.url; return `${url}?width=${file.width}&height=${file.height}`; }) .join(',') }); this.element.setAttribute('nums', this.files_.length); return this.templates_ .findAndRenderTemplate(this.element, { files: this.files_ }) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }) } isLayoutSupported = (layout) => { return layout == SPZCore.Layout.CONTAINER; } } SPZ.defineElement(TAG, SpzCustomNewRevueFilesShow); })() const TAG = 'spz-custom-revue-header'; class SPZCustomRevueHeader extends SPZ.BaseElement { constructor(element) { super(element); this.showCount = this.element.getAttribute('show-count'); } static deferredMount() { return false; } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.showCount = this.element.getAttribute('show-count'); this.showSummary = this.element.getAttribute('show-summary'); this.showWriteReview = this.element.getAttribute('show-write-review'); this.showType = this.element.getAttribute('show-type') ; this.showSort = this.element.getAttribute('show-sort') ; this.sectionId = this.element.getAttribute('section-id'); this.viewall = this.element.getAttribute('viewall') ?? false; this.prefix = this.element.getAttribute('prefix'); } mountCallback() { } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } render(data) { const ndata = { ...data, showCount: this.showCount, showSummary: this.showSummary, showWriteReview: this.showWriteReview, showType: this.showType, showSort: this.showSort, } if(this.viewall == 'review'){ ndata.viewall = false } return this.templates_ .findAndRenderTemplate(this.element, ndata, null, true) .then(({el}) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { if(data && Object.keys(data).length > 0) { this.updateRender(data); this.setupSummaryContainerEffects_(data); } }); } updateRender(data) { this.renderStarCounts_(data); this.renderTypeSelect(data); this.renderSortSelect(data); } renderStarCounts_ (data) { const renderData = { ...data.starData, ...data, star_color: data.star_color, isPC: data.isPC, } const summaryEle = data.isPC ? this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header_pc`) : this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header`); if(summaryEle) { SPZ.whenApiDefined(summaryEle).then((api) => { api.render(renderData); }); } } renderTypeSelect(data) { const typeSelect = this.element.querySelector(`#${this.prefix}-revue-header-type-${this.sectionId}`); if(typeSelect) { SPZ.whenApiDefined(typeSelect).then((api) => { api.render(data); api.registerAction('headerType_', (invocation) => { this.triggerEvent_('headerType', invocation.args.data); }); }); } } renderSortSelect(data) { const suffix = data.suffix || this.sectionId; const sortSelect = this.element.querySelector(`#${this.prefix}-revue-header-sort-${suffix}`); if(sortSelect) { SPZ.whenApiDefined(sortSelect).then((api) => { api.registerAction('headerSort_', (invocation) => { this.triggerEvent_('headerSort', invocation.args.data); }); }); } } setupSummaryContainerEffects_(data) { if(data.isPC) { this.setupSummaryContainerHover_(); } else { this.setupSummaryContainerTap_(); } } setupSummaryContainerHover_() { const summaryContainer = this.element.querySelector(`#revue-header-summary-container-${this.sectionId}`); const summaryEle = this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header_pc`); if (!summaryContainer || !summaryEle) return; let isHovering = false; // 鼠标移入容器时显示summary SPZUtils.Event.listen(summaryContainer, 'mouseenter', () => { isHovering = true; summaryEle.removeAttribute('hidden'); const selectIcon = summaryContainer.querySelector(`#revue-header-summary-icon-${this.sectionId}`); if(selectIcon) { selectIcon.classList.add('up-icon'); } }); // 鼠标移入summary时也保持显示 SPZUtils.Event.listen(summaryEle, 'mouseenter', () => { isHovering = true; }); // 鼠标移出容器时,检查是否还在summary上 SPZUtils.Event.listen(summaryContainer, 'mouseleave', () => { isHovering = false; setTimeout(() => { if (!isHovering) { summaryEle.setAttribute('hidden', 'true'); const selectIcon = summaryContainer.querySelector(`#revue-header-summary-icon-${this.sectionId}`); if(selectIcon) { selectIcon.classList.remove('up-icon'); } } }, 50); }); // 鼠标移出summary时,检查是否还在容器上 SPZUtils.Event.listen(summaryEle, 'mouseleave', () => { isHovering = false; setTimeout(() => { if (!isHovering) { summaryEle.setAttribute('hidden', 'true'); const selectIcon = summaryEle.querySelector(`#revue-header-summary-icon-${this.sectionId}`); if(selectIcon) { selectIcon.classList.remove('up-icon'); } } }, 50); }); } setupSummaryContainerTap_() { const selectIcon = this.element.querySelector(`#revue-header-summary-icon-${this.sectionId}`); const summaryEle = this.element.querySelector(`#${this.prefix}-revue-summary-${this.sectionId}_header`); if(!summaryEle) return; let isTapped = false; // 是否显示summary SPZ.whenApiDefined(summaryEle).then((api) => { api.registerAction('display', () => { if(isTapped) { isTapped = false; summaryEle.removeAttribute('hidden'); selectIcon.classList.add('up-icon'); } else { isTapped = true; summaryEle.setAttribute('hidden', 'true'); selectIcon.classList.remove('up-icon'); } }); }); } } SPZ.defineElement(TAG, SPZCustomRevueHeader); const TAG = 'spz-custom-revue-list'; class SPZCustomRevueList extends SPZ.BaseElement { constructor(element) { super(element); } static deferredMount() { return false; } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.element_id = this.element.getAttribute('id'); this.section_id = this.element.getAttribute('section-id'); this.suffix = this.element.getAttribute('suffix'); this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.isPC = window.innerWidth > (window.breakpoint || 960); } mountCallback = () => { // this.render({}); this.setAction() } render = (data) => { const ndata = { ...data, pc_layout: window.reviewProductSettings[this.section_id].pc_layout, m_loading_type: window.reviewProductSettings[this.section_id].m_loading_type, container_id: this.element_id, suffix: this.suffix, isProductPage: this.isProductPage, } return this.templates_ .findAndRenderTemplate(this.element, ndata, null) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { this.triggerEvent_('finish', {}); this.setupIntersectionObserver(); }); } renderList = (data, redo = false) => { const listEle = document.querySelector(`#revue-list-${this.suffix}`); const viewMoreEle = document.querySelector(`#revue-list-view-more`); const loadingEle = document.querySelector(`#revue-list-scroll-loading`); const viewMoreModal = document.querySelector(`#revue-viewall-modal-comp`); const reachBottomEle = document.querySelector(`#revue-list-reach-bottom-${this.suffix}`); if(viewMoreModal) { SPZ.whenApiDefined(viewMoreModal).then((api) => { api.setMarkScrollTop() }) } if (listEle) { SPZ.whenApiDefined(listEle).then((api) => { api.listRender(data, redo); }); } if(viewMoreEle) { if(data.hasmore) { viewMoreEle.removeAttribute('hidden'); } else { viewMoreEle.setAttribute('hidden', true); } } if (loadingEle) { if(data.hasmore) { loadingEle.removeAttribute('hidden'); } else { loadingEle.setAttribute('hidden', true); } } if (reachBottomEle) { if(data.hasmore) { reachBottomEle.setAttribute('hidden', true); } else { reachBottomEle.removeAttribute('hidden'); } } } setupIntersectionObserver = () => { // 创建 Intersection Observer 实例 const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const viewallModal = document.querySelector(`#revue-viewall-modal-comp`); if (viewallModal) { SPZ.whenApiDefined(viewallModal).then((api) => { api.loadMore(); }); } } }); }, { threshold: 0.1 // 当目标元素 10% 进入视区时触发 }); const loadingElement = document.querySelector('.revue-list-scroll-loading'); if (loadingElement) { observer.observe(loadingElement); } } setAction = () => { this.registerAction('checkOverFlow', () => { // 检查普通评论 this.element.querySelectorAll('.revue_text_line_4').forEach(elem => { if (elem.scrollHeight > elem.clientHeight + 10) { elem.classList.add('overflow-text'); } else { elem.classList.remove('overflow-text'); } }); // 检查回复内容 this.element.querySelectorAll('.revue_reply').forEach(elem => { const contentElem = elem.querySelector('.revue_reply_content'); if (contentElem.scrollHeight > contentElem.clientHeight + 10) { elem.classList.add('overflow-text'); } else { elem.classList.remove('overflow-text'); } }); }); } } SPZ.defineElement(TAG, SPZCustomRevueList); const TAG = 'spz-custom-revue-viewall-modal'; class SPZCustomRevueViewallModal extends SPZ.BaseElement { constructor(element) { super(element); } triggerEvent_(name, data) { const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {}); this.action_.trigger(this.element, name, event); } isLayoutSupported(layout) { return layout == SPZCore.Layout.CONTAINER; } buildCallback = () => { this.section_id = this.element.getAttribute('section-id'); this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.firstRender = true; this.markScrollTop = 0; this.scrollTop = 0; } mountCallback = () => { this.doRender_(); this.setupAction_(); } doRender_() { this.templates_ .findAndRenderTemplate(this.element, {}) .then((el) => { const children = this.element.querySelector('*:not(template)'); children && SPZCore.Dom.removeElement(children); this.element.appendChild(el); }).then(() => { const viewallModalContentEle = document.querySelector(`#revue-viewall-modal-content-${this.section_id}`); viewallModalContentEle.addEventListener('scroll', () => { this.scrollTop = viewallModalContentEle.scrollTop; }); }) } setupAction_() { this.registerAction('renderTab', async (invocation) => { if(this.firstRender) { this.firstRender = false; const productEle = document.querySelector(`#revue-product-compo`); const summaryEle = document.querySelector(`#revue-summary-${this.section_id}_viewall`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { const commentConfig = api.commentConfig || {}; api.renderStarCounts(commentConfig, `revue-summary-${this.section_id}_viewall`); api.renderCommentTab({ viewall: false, write_review: false, scroll_loading: true }, `revue-tab-${this.section_id}_viewall`); }); } } }); this.registerAction('scrollToLast', async (invocation) => { const viewallModalContentEle = document.querySelector(`#revue-viewall-modal-content-${this.section_id}`); if(viewallModalContentEle) { requestAnimationFrame(() => { viewallModalContentEle.scrollTop = this.markScrollTop; }); } }); } setMarkScrollTop() { this.markScrollTop = this.scrollTop; } refresh() { this.firstRender = true; this.scrollTop = 0; const productEle = document.querySelector(`#revue-viewall-modal-${this.section_id}`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { api.close(); }); } } loadMore() { const productEle = document.querySelector(`#revue-product-compo`); if (productEle) { SPZ.whenApiDefined(productEle).then(async (api) => { await api.renderByScrollPagination(`revue-comment-list-${this.section_id}_tab`, 'tab'); }); } } } SPZ.defineElement(TAG, SPZCustomRevueViewallModal); let section_id = '1743495266775'; window.reviewProductSettings = {}; const default_settings = { "star_least": "5", "only_featured": false, "only_media": false, "review_insufficient": "no_reviews", "mini_quantity": 5, "actions": "empty", "pc_layout": "single_column", "m_loading_type": "modal", "m_modal_page_limit": "3", "page_limit": 10, "display_product_link": false, "hide_review_section": true, "title": "Reviews", "title_color": "rgba(51, 51, 51, 1)", "primary_color": "rgba(48, 53, 77, 1)", "section_bg_color": "rgba(255, 255, 255, 1)", "background_color_new": "rgba(255, 255, 255, 1)" }; // 兼容旧数据,去除html标签 const user_settings = { "description_text": "Here are what our customers say.", "star_least": "5", "only_featured": false, "only_media": false, "review_insufficient": "no_reviews", "mini_quantity": 5, "actions": "hide", "pc_layout": "single_column", "m_loading_type": "modal", "m_modal_page_limit": "3", "comment_page_limit": 10, "page_limit": 10, "display_product_link": false, "hide_review_section": true, "title": "Customer Reviews", "accent_color": null, "title_color": "rgba(51, 51, 51, 1)", "text_color": "rgba(48, 53, 77, 1)", "section_bg_color": null, "background_color_new": null }; window.reviewProductSettings[section_id] = Object.assign({}, default_settings, user_settings, { page_limit: user_settings.comment_page_limit || user_settings.page_limit || default_settings.page_limit });