Spring Special Offer! SC13 and H25LR are only $9.99.(Limited to 500 units)
Spring Special Offer! SC13 and H25LR are only $9.99.(Limited to 500 units)
>>View more
Spring Special Offer! SC13 and H25LR are only $9.99.(Limited to 500 units)
>>View more
Spring Special Offer! SC13 and H25LR are only $9.99.(Limited to 500 units)
>>View more
4/9
${data.index + 1}/${data.total}
Far or Near, Always Clear: Dual-Beam Precision for Every Night
Instant Red Light: Protect Your Night Vision and Success in the Wild
Real-Time Battery Monitoring: Smart LED Indicator for Reliable Power Management
Hands-Free, Hassle-Free: Versatile Mounting for Every Adventure
Rugged Reliability: Built to Survive the Harshest Outdoor Environments
Sized to Go, Built to Shine: Finger-Sized Power for Your Pocket
Explore the ST10 technical specifications
More Information
3-in-1 Light Setup : Super bright 1000-lumen main beam for distance, side white light for close-up tasks, plus a red light to protect your night vision—perfect for fishing, night walks, or reading.
Easy to Use : Two separate buttons—one for the main light with 6 brightness levels, one for the side lights (white and red) with 4 levels each. Simple, quick, no confusion.
Compact & Tough : Just 7 cm (2.76 inches) long, fits easily in your pocket. Made with strong aerospace-grade aluminum, it's IP66 water-resistant and 1 meter(3.28feet) drop-resistant for all your daily adventures.
Recharge & Swap Batteries : Includes a rechargeable 14500 battery with a built-in USB-C charging port and charging indicator. Low on power? Simply swap in regular AA batteries and keep going—no hassle, no extra chargers needed.
Magnetic & Clip-On Hands-Free : Magnetic tail lets you stick it to metal surfaces, while the clip fits on hats, backpacks, or fences—freeing your hands for whatever you’re doing.
Specifications
Emitter:
TN-3535 *2 (6500K-7000K), 660nm red light*1
Working Voltage:
1.2V-4.2V
Size:
69.3 × 33.8 × 20.4 mm (≈ 2.73" × 1.33" × 0.80")
Material:
AL6061-T6 aluminium alloy
Charging:
14500 battery with built-in USB-C charging port
Weight:
49g (1.73 oz) without battery
Power Supply:
1 × 14500 battery 900 mAh (included); AA battery compatible (not included)
Note:
When using AA batteries, the power indicator does not work and the lockout mode is disabled.
Note : To prevent accidental activation, when not using the flashlight or carrying it outdoors, please loosen the tailcap to disconnect the battery or activate the lockout mode.
Product manual, click to download!
Alt
Alt
FAQs
What is the wavelength of the red light?
The ST10 red light has a wavelength of 660nm , which falls within the deep red range of the visible spectrum. It provides illumination while helping to preserve night vision .
How do I charge the ST10?
The ST10 comes with a rechargeable 14500 battery that features a built-in Type-C charging port . No separate charger is required—simply charge the battery directly. It is also compatible with AA alkaline or NiMH batteries .
Does the ST10 have a lockout mode?
Yes. When the flashlight is off, triple-click to activate Lockout mode. To unlock, triple-click again and it will enter memory mode.
Does the ST10 have a battery indicator?
Yes. There is a battery indicator located between the two switches:
Green light (solid): 30% – 100%
Red light (solid): 10% – 30%
Red light (flashing): less than 10%
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)
${function(){
return `
${data.starNum} /${data.starTotal}
`;
}()}
${function(){
return `
${data.showStarText === 'true' ? `
${data.starNum} /${data.starTotal}
` : ''}
`;
}()}
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 isPercentage = data.show_percentage === 'true' && data.total <= data.show_percentage_num;
return `
${!isPercentage ? `${data.count}` : `${(data.count / data.total * 100).toFixed(1)}%`}
`
}()}
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)
${function() {
return `
${data.count > 99 ? '99+' : data.count < 1 ? '' : data.count}
`;
}()}
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)
${function() {
return `
${function() {
if(data.imgCover) {
if(media.videosrc) {
let src = '';
if (media.videosrc) {
src = media.videosrc + '.' + media.ext;
}
const videoDom = `
`;
if(!isPC){
return `
${videoDom}
`
}
return `
${videoDom}
`
} else if(media.mp4 || media.hls) {
const videoDom = `
`;
if(!isPC){
return `
${videoDom}
`
}
return `
${videoDom}
`
} else {
if(!isPC){
return `
`
}else{
return `
`
}
}
} else {
if (media.videosrc) {
let src = '';
if (media.videosrc) {
src = media.videosrc + '.' + media.ext;
}
return `
`
} else if(media.mp4 || media.hls) {
return `
`
} else {
return `
`
}
}
}()}
`;
}()}
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)
${function() {
return `
`
}()}
${function() {
return `
Most liked
Highest ratings
Lowest ratings
`
}()}
${function() {
return `
Most liked
Highest ratings
Lowest ratings
`
}()}
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') || '1732081699872';
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)
${function() {
return `
`
}()}
${function() {
const list = data.listData;
return `
With Photos(${list.image_count})
`
}()}
${function() {
const list = data.listData;
return `
With Photos(${list.image_count})
`
}()}
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') || '1732081699872';
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 ? '57a904b3-702c-4e82-ab52-49257ecae374' : 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 ? '57a904b3-702c-4e82-ab52-49257ecae374' : 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-1732081699872`);
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);
})()
${function(){
return `
${data.starNum} /${data.starTotal}
`;
}()}
${function(){
return `
${data.showStarText === 'true' ? `
${data.starNum} /${data.starTotal}
` : ''}
`;
}()}
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);
})()
${function() {
if (!data) {
return '';
}
const {url, type, height, width, poster, mp4} = data;
if (type === 'image') {
return `
`
}
if (type === 'video') {
return `
`
}
return ``
}()}
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);
${function() {
const pc_layout = 'single_column';
const isProductPage = '1' == 1;
const product_id = '57a904b3-702c-4e82-ab52-49257ecae374';
const accent_color = '';
const randomStr = Math.random().toString(36).substring(7);
const item = data;
const config = data.config;
const formatDate = value => {
let date = new Date(value * 1000);
const day = date.toLocaleString('en-US', { day: '2-digit' });
const month = date.toLocaleString('en-US', { month: 'short' });
const year = date.toLocaleString('en-US', { year: 'numeric' });
return month + '/' + day + '/' + year;
};
return `
${formatDate(item.created_at)}
`;
}()}
${function() {
const isPC = data.isPC;
const pc_layout = data.pc_layout;
const is_pagination = isPC && pc_layout == 'single_column';
const column_type = (isPC && pc_layout == 'double_column') ? 2 : 1;
const is_view_more = data.hasmore && ((isPC && pc_layout == 'double_column') || (!isPC && data.m_loading_type === 'curr_page'));
const is_view_all = (data.viewall ?? true) && !isPC && data.m_loading_type === 'modal';
const is_write_review = (data.write_review ?? true) && !isPC;
const scroll_loading = data.scroll_loading ?? false;
const is_reach_bottom = (isPC && pc_layout == 'double_column') || !isPC;
return `
Wow you reached the bottom
View all
Write a Review
`;
}()}
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);
${function(){
const starOrder = ['one_star', 'two_star', 'three_star', 'four_star', 'five_star'];
function sortStarRatings(ratings) {
const sortedRatingsArr = [];
starOrder.map((star,index) => {
sortedRatingsArr.push(index+1);
return star;
});
return sortedRatingsArr;
};
const star_levels = sortStarRatings(data.star_detail).reverse();
return `
${data.comment_avg_star}
Total reviews: ${data.comment_count > 999 ? '999+' : data.comment_count}
`;
}()}
${function() {
return `
`
}()}
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 = '1732081699872';
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": "3",
"only_featured": false,
"only_media": false,
"review_insufficient": "no_reviews",
"mini_quantity": 5,
"actions": "hide",
"pc_layout": "single_column",
"m_loading_type": "curr_page",
"m_modal_page_limit": "3",
"comment_page_limit": 10,
"page_limit": 10,
"display_product_link": false,
"hide_review_section": false,
"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
});
${function() {
const randomStr = Math.random().toString(36).substring(7);
const list = data.listData;
const isPC = data.isPC;
const pc_layout = 'single_column';
return `
All(${list.count})
With Photos(${list.image_count})
`;
}()}
${function(){
return `
${function(){
if (media.videosrc) {
let src = '';
if (media.videosrc) {
src = media.videosrc + '.' + media.ext;
}
return `
`
} else if(media.mp4 || media.hls) {
return `
`
} else {
return `
`
}
}()}
`;
}()}
${function(){
const isPC = data.isPC;
const pc_layout = 'single_column';
return `
Customer Reviews
No reviews yet, why don't you leave the first review?
Write a Review
No reviews yet, why don't you leave the first review?
Write a Review
`;
}()}
${function(){
return `
No reviews available. The product reviews component has been hidden
Product Detail Reviews
`
}()}
${function(){
const settings_product_title = "title";
const product_grid_image_size = "100%";
const settings_product_image_hover_on = true;
let settings_product_save_label = false;
const product_sold_out_label = true;
const settings_product_swatches_name = ["style"];
const settings_collection_color_swatches = true;
const product_price_currency = ` `
const from_on = data.price_max != data.price_min ? ''.replace(/\{\{\s*price\s*\}\}/, product_price_currency) : product_price_currency;
const variantShowLimit = 2;
const private_id = 'product-tmpl-' + Math.random().toFixed(6).slice(-6)
const product_variants = data.variants || [];
const product_id = data.id;
const images = data.images || [];
const image = data.image || {};
const imageWidth = image.width;
let imageHeight = image.height;
if (product_grid_image_size !== 'natural') {
imageHeight = (imageWidth * parseFloat(product_grid_image_size)) / 100;
}
const price = Number(data.price_min);
let compareAtPrice = Number(data.compare_at_price);
let offRatio = data.off_ratio;
const type = data.type;
const isMock = data.isMock;
let product_image_hover_on = false;
for (let i = 0; i < product_variants.length; i++) {
const item = product_variants[i];
const vcap = Number(item.compare_at_price);
if (item.price == price && vcap > compareAtPrice) {
compareAtPrice = vcap;
offRatio = item.off_ratio;
}
}
let second_image = null;
if (settings_product_image_hover_on) {
for (let i = 1; i < images.length; i++) {
const img = images[i];
if (img.src && img.src.indexOf('video=') === -1) {
second_image = img;
product_image_hover_on = true;
break;
}
}
}
let sold_label_on = false;
let sale_label_on = false;
if (settings_product_save_label == null) {
settings_product_save_label = true;
}
if (!data.available && product_sold_out_label) {
sold_label_on = true;
}
if (settings_product_save_label && compareAtPrice > price && data.available) {
sale_label_on = true;
}
const diffPrice = compareAtPrice - price;
const variantValues = [];
const showVariants = [];
if (data.need_variant_image && settings_collection_color_swatches && settings_product_swatches_name.length > 0) {
for (let i = 0; i < (data.options || []).length; i++) {
const option = data.originData.options[i];
const optionName = option.name && option.name.toLowerCase();
if (settings_product_swatches_name.includes(optionName)) {
for (let j = 0; j < product_variants.length; j++) {
const variant = product_variants[j];
const value = variant.options[i].value;
if (!variantValues.includes(value)) {
variantValues.push(value);
showVariants.push(variant);
}
}
break;
}
}
}
return `
${data.available ?
`
` : ''}
`;
}()}
Your cart is reserved for
${data.mm} m
${data.ss} s
!
${data.line_items.map(item => {
const renderDiscountApp = () => {
const isEmpty = item.discount_applications && item.discount_applications.length < 1;
const isNotExist = !item.discount_applications;
if (isEmpty || isNotExist) {
return ""
}
return `
${
(item.discount_applications || []).map(discount_item => {
const discount_item_amount = discount_item.discount_amount || discount_item.amount || '';
return `
${discount_item.title}
(- )
`
}).join('')
}
`
}
return `
${item.options.map(o => `
${o.name}: ${o.value}
`).join('')}
${(item.parsedProperties || []).map((propertie)=>{
if (propertie.isImage){
return `
${propertie.name}: View image `
}else{
return `
${propertie.name}: ${propertie.value}
`
}
}).join('')}
${renderDiscountApp()}
`;
}).join('')}
${function() {
const freeAmount = 29;
const totalPrice = (data && data.total_price) || (data && data.cart && data.cart.total_price) || 0;
const amountDiff = freeAmount - totalPrice;
const percentDiff = (amountDiff > 0 ? (totalPrice / freeAmount * 100).toFixed(2) : 100) + '%';
const spendTip = "Buy {amount} more to enjoy FREE Shipping";
let tipText = "Your order is free delivery";
if (amountDiff > 0 && spendTip) {
tipText = spendTip.replace('{amount}', ` `);
}
return `
`;
}()}
${function() {
return `
Add order note
Add order note
`;
}()}
${data.total_discount > 0 ? (
`
Save
${data.discount_applications.length > 0 && data.discount_applications.map(item => {
return `
${item.title}:
`
}).join('')}
Save
`
) : ''}
Check out
Taxes and shipping calculated at checkout
${function() {
const freeAmount = 29;
const totalPrice = (data && data.total_price) || (data && data.cart && data.cart.total_price) || 0;
const amountDiff = freeAmount - totalPrice;
const percentDiff = (amountDiff > 0 ? (totalPrice / freeAmount * 100).toFixed(2) : 100) + '%';
const spendTip = "Buy {amount} more to enjoy FREE Shipping";
let tipText = "Your order is free delivery";
if (amountDiff > 0 && spendTip) {
tipText = spendTip.replace('{amount}', ` `);
}
return `
`;
}()}
${data.total_discount > 0 ? (
`
Save
${data.discount_applications.length > 0 && data.discount_applications.map(item => {
return `
${item.title}:
`
}).join('')}
Save
`
) : ''}
Check out
Taxes and shipping calculated at checkout
${function(){
const wholesale_enabled = false;
const qty = data.quantity || 1;
const currentSelectVariant = data.variant;
const defaultVariant = (data.product && data.product.variants && data.product.variants[0]) || Object.keys(data).length > 1 ? data : null;
const productVariant = null;
const variantData = currentSelectVariant || defaultVariant || productVariant;
const wholesale_price = variantData.wholesale_price || [];
if(wholesale_enabled && wholesale_price.length > 0) {
let wholesaleIndex = wholesale_price.findIndex(item => {
return item.min_quantity > qty;
});
if(wholesaleIndex < 0){
wholesaleIndex = wholesale_price.length - 1;
}else if(wholesaleIndex > 0){
wholesaleIndex = wholesaleIndex - 1;
}
const wholesalePrice = wholesale_price[wholesaleIndex] || '';
return `
`
}else {
const price = variantData && variantData.price;
return price != undefined ? `
` : ' ';
}
}()}
${function(){
const productData = data.product;
const selectedVariant = productData.variants.find(v => v.available) || productData.variants[0];
const product_options = productData.options.filter(Boolean) || [];
return `
Price
${function() {
const origin = "shop"
const product = (origin === 'shop' ? data.product : data) || {};
const selectedVariant = product.variants.find(v => v.available) || product.variants[0];
return !!selectedVariant ? `
-
` : `
-
`;
}()}
${selectedVariant.available ? "Add to cart" : "SOLD OUT"}
Buy it now
Product was out of stock.
Product is unavailable.
${function() {
const MAX_INVENTORY = 999999;
const product0 = Object.prototype.toString.call(data) == '[object Array]' ? data[0] : (data.product || data);
const inventoryQty = product0.inventory_quantity;
const inventoryPolicy = product0.inventory_policy;
const inventoryTracking = product0.inventory_tracking;
const exactInventoryStatusId = "quick-shop-exact-inventory-render";
const lowStock = 5;
const defaultVariant = product0 && product0.variants && product0.variants[0];
const selectedVariant = product0.variants.find(v => v.available) || defaultVariant;
const selectedVariantAvailableQuantity = selectedVariant && selectedVariant.available_quantity;
let actualInventory = inventoryQty;
if ((inventoryTracking && inventoryPolicy == 'continue') || !inventoryTracking) {
actualInventory = MAX_INVENTORY;
}
return `
Avaliability:
Out of stock
in stock, ready to be shipped
Low stock
`;
}()}
` }()}
${function(){
const optionName = option.name || '';
const optionId = option.id || '';
let isThumbImage = !!option.showThumbImage;
const thumbStyle = "image_with_text";
const variantType = "button";
const isSelected = (value) => {
const selected = (data.selectedOptions || []).find(v => v.name === optionName);
return selected && selected.value.length && selected.value[0] == value;
};
const getThumbImage = (value) => {
const options = data.product.options || [];
const option = options.find(o => o.name === optionName);
if (option.thumbImages) {
const thumbImage = option.thumbImages.find(t => t.value === value);
if (thumbImage && thumbImage.image) {
return {
src: thumbImage.image.src,
alt: thumbImage.image.alt
};
}
}
return {src: '', alt: ''};
};
return `
${optionName.toLowerCase()}
${optionName}:
${data.selectedOptions && data.selectedOptions.length && data.selectedOptions.find(v => v.name === optionName).value[0]}
`;
}()}
${function(){
return `${data.value} `
}()}
${function(){
const wholesale_enabled = false;
const qty = data.quantity || 1;
const currentSelectVariant = data.variant;
const defaultVariant = (data.product && data.product.variants && data.product.variants[0]) || Object.keys(data).length > 1 ? data : null;
const productVariant = null;
const variantData = currentSelectVariant || defaultVariant || productVariant;
const wholesale_price = variantData.wholesale_price || [];
if(wholesale_enabled && wholesale_price.length > 0) {
let wholesaleIndex = wholesale_price.findIndex(item => {
return item.min_quantity > qty;
});
if(wholesaleIndex < 0){
wholesaleIndex = wholesale_price.length - 1;
}else if(wholesaleIndex > 0){
wholesaleIndex = wholesaleIndex - 1;
}
const wholesalePrice = wholesale_price[wholesaleIndex] || '';
return `
`
}else {
const price = variantData && variantData.price;
return price != undefined ? `
` : ' ';
}
}()}
const carousel = document.getElementById('quick-view-images');
const selecotr = document.getElementById('quick-view-thumb-images');
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { height } = entry.contentRect;
selecotr.style.height = height + 'px';
}
});
resizeObserver.observe(carousel);
${function(){
const remove_variant_images_on = false;
let product_swatches_name = ["Style"];
product_swatches_name = product_swatches_name.map((name) => name.toLowerCase());
const variantsNamesSet = new Set(data.options.map((opt) => opt.name.toLowerCase()) || []);
const containsSwatches = product_swatches_name.some((name) => variantsNamesSet.has(name));
const variantsList = (data && data.variants) || []
const variants = variantsList.map((item) => item.image.path) || [];
const productData = data;
let images = data.images;
if(remove_variant_images_on && containsSwatches) {
images = data.images.filter((img) => !variants.includes(img.path));
}
const selectedVariant = data.variants.find(v => v.available) || data.variants[0];
const selectedIndex = !!selectedVariant ? images.findIndex(img => img.src === (selectedVariant.image && selectedVariant.image.src)) : 0;
const initialSlide = selectedIndex === -1 ? 0 : selectedIndex;
return `
${images.map((image, index) => `
`).join('')}
${images.map(image => `
`).join('')}
${images.length > 1 ? `
` : ''}
${data.title}
Price
${function() {
const origin = "view"
const product = (origin === 'shop' ? data.product : data) || {};
const selectedVariant = product.variants.find(v => v.available) || product.variants[0];
return !!selectedVariant ? `
-
` : `
-
`;
}()}
${selectedVariant.available ? "Add to cart" : "SOLD OUT"}
Buy it now
Product was out of stock.
Product is unavailable.
${function() {
const MAX_INVENTORY = 999999;
const product0 = Object.prototype.toString.call(data) == '[object Array]' ? data[0] : (data.product || data);
const inventoryQty = product0.inventory_quantity;
const inventoryPolicy = product0.inventory_policy;
const inventoryTracking = product0.inventory_tracking;
const exactInventoryStatusId = "quick-view-exact-inventory-render";
const lowStock = 5;
const defaultVariant = product0 && product0.variants && product0.variants[0];
const selectedVariant = product0.variants.find(v => v.available) || defaultVariant;
const selectedVariantAvailableQuantity = selectedVariant && selectedVariant.available_quantity;
let actualInventory = inventoryQty;
if ((inventoryTracking && inventoryPolicy == 'continue') || !inventoryTracking) {
actualInventory = MAX_INVENTORY;
}
return `
Avaliability:
Out of stock
in stock, ready to be shipped
Low stock
`;
}()}
`
}()}
${function(){
const optionName = option.name || '';
const optionId = option.id || '';
let isThumbImage = !!option.showThumbImage;
const thumbStyle = "image_with_text";
const variantType = "button";
const isSelected = (value) => {
const selected = (data.selectedOptions || []).find(v => v.name === optionName);
return selected && selected.value.length && selected.value[0] == value;
};
const getThumbImage = (value) => {
const options = data.product.options || [];
const option = options.find(o => o.name === optionName);
if (option.thumbImages) {
const thumbImage = option.thumbImages.find(t => t.value === value);
if (thumbImage && thumbImage.image) {
return {
src: thumbImage.image.src,
alt: thumbImage.image.alt
};
}
}
return {src: '', alt: ''};
};
return `
${optionName.toLowerCase()}
${optionName}:
${data.selectedOptions && data.selectedOptions.length && data.selectedOptions.find(v => v.name === optionName).value[0]}
`;
}()}
${function(){
return `${data.value} `
}()}
const TAG = "spz-custom-popup";
const DISPLAY_TYPE = {
POPUP: "PTT_POPUP" // 弹窗
};
const API = {
LIST: `/api/storefront/promotion/placement/list`, // 获取弹窗列表
REPORT: `/api/storefront/promotion/placement/data/report` // 上报数据
};
const DISPLAY_DEVICE = {
PC_AND_MOBILE: "PD_PC_MOBILE", // PC和移动端
PC: "PD_PC", // PC
MOBILE: "PD_MOBILE" // 移动端
};
const REPORT_EVENT = {
CLICK: "PE_CLICK", // 点击事件
IMPRESSION: "PE_IMPRESSION" // 曝光事件
};
class SpzCustomPopup extends SPZ.BaseElement {
constructor(element) {
super(element);
this.popupList_ = []; // 弹窗数据
this.popupZIndex = 1050; // 弹窗层级
// 节流处理 每5s内多次点击 算一次点击上报
this.throttleReport = this.win.SPZCore.Types.throttle(
this.win,
(data) => {
this.reportData(data)
},
5000
)
}
static deferredMount() {
return false;
}
buildCallback() {
this.action_ = SPZServices.actionServiceForDoc(this.element);
this.templates_ = SPZServices.templatesForDoc(this.element);
this.xhr_ = SPZServices.xhrFor(this.win);
this.setupAction_();
this.viewport_ = this.getViewport();
}
mountCallback() {
this.fetchData_();
}
// 接口请求,获取数据
fetchData_() {
const id = window.SHOPLAZZA.meta.page.template_type === 51 ? window.SHOPLAZZA.meta.page.resource_id : 0;
return this.xhr_.fetchJson(API.LIST, {
method: 'POST',
body: {
page_id: window.SHOPLAZZA.meta.page.template_type,
placement_type: DISPLAY_TYPE.POPUP,
discount_id: id
}
}).then((res) => {
// 请求成功 执行render
this.doRender_(res.list);
}).catch((err) => {
console.error(err);
});
}
// 渲染dom
doRender_(data) {
this.popupList_ = data || [];
if (this.popupList_.length > 0) {
this.popupList_.forEach((item) => {
item.config = JSON.parse(item.config);
})
}
return this.templates_
.findAndRenderTemplate(this.element, { list: this.popupList_ })
.then((el) => {
const children = this.element.querySelector('*:not(template)');
children && SPZCore.Dom.removeElement(children);
this.element.appendChild(el);
})
.then(() => {
// 遍历显示弹窗
this.popupList_.forEach((item) => {
this.showPopup_(item);
});
})
}
showPopup_(popup) {
// 展示弹窗 符合展示条件的弹窗
const $popup = document.querySelector(`#popup-${popup.id}`);
$popup && SPZ.whenApiDefined($popup).then((api)=> {
const isPC = this.viewport_.getWidth() >= 960;
const isMobile = this.viewport_.getWidth() < 960;
const isMatchPCDevice = popup.device === DISPLAY_DEVICE.PC_AND_MOBILE || popup.device === DISPLAY_DEVICE.PC;
const isMatchMobileDevice = popup.device == DISPLAY_DEVICE.PC_AND_MOBILE || popup.device === DISPLAY_DEVICE.MOBILE;
if((isPC && isMatchPCDevice) || (isMobile && isMatchMobileDevice)) {
// 根据推送时间 延迟展示弹窗
setTimeout(() => {
api.open();
}, popup.delay_seconds * 1000);
}
})
}
// 上报数据
async reportData(data) {
this.xhr_.fetchJson(API.REPORT, {
method: "POST",
body: {
placement_id: data.placement_id,
event: data.event
}
});
}
setupAction_() {
this.registerAction('handleTrack', async(invocation) => {
// 如果是主题编辑器则不用处理
if(window.top !== window.self) {
return;
}
const data = invocation.args;
const event = data.event;
// 点击上报 节流处理
if(event === REPORT_EVENT.CLICK) {
await this.throttleReport(data);
} else {
this.reportData(data);
}
});
}
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, SpzCustomPopup);