Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable
Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable

Over The Sink Dish Drying Rack 3 Tier Large Dish Rack Metal Over The Counter Dish Drying Rack for Kitchen Sink Shlef Adjustable

Price
$0.00
$299.99
Save  $-299.99
Quantity
Free worldwide shipping
Free returns
Sustainably made
Secure payments
Vendor by: ALSO GO
SKU: HG0188-BK#WM-WEC-USNP2024022242b7

✔️Description:
🔹This 3-tier dish rack with cover is made from heavy-duty carbon steel, capable of holding lots of dishes and utensils. 
🔹The cover keeps your dishes protected from dust, dirt, and other debris, ensuring that they stay clean and dry until you're ready to put them away.
🔹 It can be disassembled and washable to keep it clean. This drying rack helps you store and dry your kitchenware orderly, making your countertop dry and neat all the time. And you can also fetch what you need easily.

✔️Specification:

  • Product Name: Drying Dish Rack Over Sink
  • Material: Carbon steel + Plastic
  • Color: Black
  • Whole Size: 87*30*85cm / 34.25*11.8*33.46inch
  • Weight: 6.5kg/14 lbs
  • Shape: As Picture Shows
  • Pattern: Solid
  • Style: Modern, 3 Layer

✔️Package Contents:

  • 1*Over The Sink Dish Drying Rack
  • 1*Manual

✔️Note: 

  • 1. Please allow minor differs due to manual measurement.
  • 2. The picture may be a little different from that of real item due to the different display and different light. 
  • 3. Due to different producing batches, product details might be a little different. If you mind the difference, please buy with caution. Thank you for your understanding.
  • 4. If you have any questions about the product, please contact us.
  • 【Special Design with Lid】This over the sink dish drying rack with a convenient lid not only provides an extra layer of protection for your drying dishes but also allows you to keep your countertop clean, which ensures that your dishes dry efficiently while maintaining a clutter-free and organized kitchen. The anti-slip foot cover at the bottom is designed to keep it stable and not easy to shake. It is durable and sturdy. No worry about the counter mess at all.
  • 【Versatile and Spacious】Over the sink dish rack offers ample space for drying a variety of dishes, including plates, bowls, cups, and utensils. The adjustable arms and holders allow you to customize the sink dish rack to fit different sink sizes, ensuring a snug and secure fit. With its versatile design, our rack accommodates various kitchen setups, making it a perfect choice for small or large kitchens. The tool holder can also be placed on the side.
  • 【Premium Quality and Durability】Crafted from high-quality material, this over the sink drying rack is built to last. The sturdy construction ensures stability and durability, even when supporting heavy dishes. The rust-resistant coating guarantees long-lasting performance, making it a reliable companion for your daily dish drying needs. Invest in a durable and premium-quality over the sink dish drying rack that will withstand the test of time.
  • 【Convenient and Hygienic】The dish drying rack promotes proper air circulation, allowing your dishes to dry quickly and hygienically. The removable parts make cleaning a breeze, and the over sink dish rack's compact design allows for easy storage when not in use. Experience the convenience and hygiene of our thoughtfully designed dish rack. All the parts can be dissembled and washable so you can clean and sanitize them occasionally. It’s designed to save space in your cooking and cleaning area. A must-have for your kitchen!
  • 【Modern and Stylish】Designed to complement any kitchen decor, our over the sink dish rack features a modern and sleek design. Its clean lines and elegant finish enhance the visual appeal of your kitchen while providing a functional solution for drying dishes. Elevate your kitchen aesthetics with our stylish and functional over the sink dish drying rack, and enjoy the perfect blend of form and function.
  • 【Easy to Assemble】No other tools needed, you can easily assemble the dish rack drainer together. Just follow the help of the installation manual. All parts needed are included. The draining tableware rack is detachable and easy to clean.
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.color = this.element.getAttribute('color') || '#000000'; this.show_percentage = 'false'; this.show_percentage_num = 100; 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') || '1702262244369'; 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') || '1702262244369'; 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(); }) } fetchCommentConfig_ = async () => { const response = await fetch('/api/comment-config'); return response.json(); } 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(); } 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(); } 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 ? '89f5e544-5570-434d-8f90-f937362f9a9f' : this.isCartPage ? '' : '', collection_id: this.isCollectionPage ? '239b78b4-bc24-4284-9c3e-127e6087cc8e' : '', 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 ? '89f5e544-5570-434d-8f90-f937362f9a9f' : this.isCartPage ? '' : '', collection_id: this.isCollectionPage ? '239b78b4-bc24-4284-9c3e-127e6087cc8e' : '', 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; let lessThanCount = 1; // 评论不足逻辑 if(this.actions === "hide") { // 不展示评论组件 if(this.review_insufficient === 'less_than') { lessThanCount = this.mini_quantity; // 无评论或少于一定数量 } } else if(this.actions === "all_product") { lessThanCount = 1; } if(commentListData.count < lessThanCount) { // 是否隐藏评论组件 if(this.hide_review_section) { this.renderNoData(); return null; } else { this.renderRevueEmpty(); } this.nodata = true; } 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_({ starData: commentSummaryData, listData: commentListData, comment_avg_star: commentSummaryData.comment_avg_star, comment_count: commentSummaryData.comment_count, }); // 有评论逻辑 this.renderStarCounts(commentSummaryData); 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(); }); } } renderRevueEmpty = (section) => { const emptyEle = document.querySelector(`#revue_empty-${this.section_id}`); const writeReviewBtn = document.querySelector(`#revue_write_review_btn_single`); if (emptyEle) { emptyEle.classList.remove('hidden'); } if (writeReviewBtn) { writeReviewBtn.classList.remove('hidden'); } const skeletonEle = document.querySelector('#revue_skeleton'); if (skeletonEle) { skeletonEle.classList.add('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, }); }); } } 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); }); } } 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) .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, 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) => { api.renderStarCounts({}, `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 = '1702262244369'; window.reviewProductSettings = {}; const default_settings = { "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", "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)", }; const user_settings = JSON.parse('{"star_least":"1","layout":"list","title":"Customer Reviews","description_text":"Here are what our customers say.","comment_page_limit":"20","image_size":"natural","title_color":"rgba(51, 51, 51, 1)","sub_title_color":"rgba(48, 53, 77, 0.5)","text_color":"rgba(48, 53, 77, 1)","btn_color":"rgba(0, 0, 0, 1)","reply_color":"rgba(126, 132, 156, 1)","reply_bg_color":"rgba(248, 248, 247, 1)","background_color":"rgba(255, 255, 255, 1)","like_color":"rgba(242, 120, 79, 1)","like_bg_color":"rgba(229, 229, 229, 1)"}'); window.reviewProductSettings[section_id] = { ...default_settings, ...user_settings, page_limit: user_settings.comment_page_limit || user_settings.page_limit || default_settings.page_limit };