Home
/
Ausom DT2 Pro E-scooter 2*1100W Motor, 52V 23.4Ah, 10*3.0 inch Tire, 68km/h Max Speed, 115km Max Range
${Array(data.total || 0).fill().map((val, idx) => idx).map((val, idx) => `
`).join('')}
1/5
${data.index + 1}/${data.total}
${Array(data.total || 0).fill().map((val, idx) => idx).map((val, idx) => `
`).join('')}
1/5
${data.index + 1}/${data.total}
Highlights
High-Performance Dual Motor System
Powered by dual 1100W motors and a 52V system, the DT2 PRO delivers exhilarating acceleration and a top speed of 42 mph. Whether you're tackling steep inclines or cruising at high speeds, its torque-rich performance ensures smooth and powerful rides. With a single/dual motor switch, you can easily toggle between efficiency and peak performance with a single touch.
Advanced Hydraulic Braking with E-ABS
Equipped with a hydraulic disc brake system, the DT2 PRO ensures superior stopping power with minimal effort. The oil-based hydraulic brakes provide 30% shorter braking distances compared to standard mechanical disc brakes, enhancing control and safety. Integrated anti-lock braking (E-ABS) prevents wheel lock-up, delivering stable and responsive braking even at high speeds.
Precision Suspension System for Maximum Comfort
The rocker-arm suspension system incorporates aerospace landing gear technology, effectively absorbing shocks and vibrations while providing enhanced stability and comfort on rough terrain. Whether navigating city streets or off-road trails, it ensures a smooth and controlled ride.
Extended Range with High-Capacity Battery
With a 52V 23.4Ah (1216.8Wh) battery, the DT2 PRO offers an impressive range of up to 71 miles on a single charge. This long-range capability makes it ideal for extended commutes and adventure rides without frequent recharging. Dual charging ports further enhance convenience, reducing downtime between rides
Intelligent Security and Smart Features
Designed with dual-layer security, the DT2 PRO features NFC and passcode locks, preventing unauthorized access. A hidden AirTag slot adds an extra layer of anti-theft protection. The 4.5'' LCD display provides real-time ride data.
Optimized Riding Experience with Ergonomic Design
The 10-inch tubeless tires offer exceptional traction and puncture resistance, enhancing ride stability. A wider and longer deck supports up to 135kg(297 lbs), ensuring a comfortable stance. Integrated turn signals, front and rear lights, and ambient lighting improve nighttime visibility. The forged folding mechanism ensures durability and easy portability.
Ausom Electric Scooter DT2 Pro
Power Ahead, Refined Ride
2200W | 42MPH | 71 Miles
42 MPH Top Speed | Forged Aluminum Frame | NFC & Passcode Lock
Dual Charging Ports | Comprehensive Lighting System | E-ABS & Hydraulic Brakes
2912W Max Power | 71-Mile Max Range | 10" Tubeless Tires | 4.5" LCD Display
Stronger Motor, Extreme Speed
Dual 52V motors reach 42 MPH, offering fast acceleration and exhilarating high-speed performance.
Max Power 2912W | Max Speed 42MPH | Triple Modes: 18/29/33 mph (Single Motor), 18/31/42 mph (Dual Motors)
All-Terrain Dominance
The 10'' x 3.0'' tubeless puncture-resistant tire features a deep tread and reinforced sidewall, ensuring excellent grip, stability, and durability on any terrain.
Ride Without Limits
Conquer inclines with ease, tackling slopes of up to 53% grade, and enjoy unmatched stability on both mountain trails and city streets.
Ride Farther, Worry Less
1216.8Wh battery ensures up to 71 miles of uninterrupted travel-explore without concern for recharging.
Advanced Suspension, Flawless Ride
Dual swingarm suspension absorbs bumps, ensuring a smooth, comfortable ride on any surface.
Hydraulic & E-ABS Braking System
Smooth braking with 30% shorter stopping distance for greater control and safety.
Style and Safety, All Lit Up
Comprehensive lighting system enhances visibility and safety for night rides.
Less Downtime, More Freedom
Charge faster with dual ports and Type-C fast charging.
Effortless Folding
The space-saving design folds effortlessly in just three steps.
Easy Storage & Carrying
Quickly fold your scooter for convenient trunk storage and easy carrying.
Smart Display, Smarter Rides
Focus on the road with all ride data in view.
Speed | Battery | Distance | Light | Cruise
Fortified Security, Peace of Mind
Secure your scooter with NFC unlocking and a discreet AirTag slot for added protection.
High-Strength Aluminum Frame
Made from 6061 aluminum alloy, it reduces vibrations and structural fatigue, ensuring greater strength and stability.
Wide Deck & Robust Support
Spacious deck with 297lbs weight capacity for exceptional comfort and stability for all riders.
Cutting-Edge Features
Finger Throttle | Ergonomic Handle | Stable Kickstand
Brake Lever | Dual-Drive Switch | Horn
Dispaly Guide
Specification
General
Brand: Ausom Model: DT2 Pro Color: Gray & Yellow
Basic Spec.
Unfolded Size: 52 x 27 x 53.5 in / 1320 x 685 x 1360mm Folded Size: 52 x 27 x 21.6 in / 1320 x 685 x 550mm N.W.: 72 lbs / 33kg Length of Handlebar: 27 in / 685mm Footboard Top to Ground: 10.4 in / 264mm Footboard Bottom to Ground: 7 in / 180mm Size of Footbard: 20.7 x 7in / 525 x 180mm Recommended Rider Heights: 4'11"-6'7" / 150cm-200cm Uphill: 53% Max Load: 297 lbs / 135kg Waterproof Level: IP54 Material of Frame: 6061 Aluminium Alloy Max Range: 71 miles / 115km Single Motor Speed Level (Eco/Sport/Race): 18/29/33 mph; 29/47/53km/h Dual Motor Speed Level (Eco/Sport/Race): 18/31/42 mph; 29/51/68km/h Braking System: E-ABS+Front & Rear Hydraulic Disc Brakes Brake Disc: 5.7 in (Outside Diameter) Suspension: Swingarm Suspension
Motor
Rated Voltage: 52V Power: 1100W x 2 Max RPM: 1400 Max Output Torque: 32N.M
Battery
Capacity & Voltage: 23.4Ah, 52V Power Capacity: 1216.8Wh Charging Time: 12.5h (by one charger), 6h (by two chargers) (Only one charger included) Protection System: Yes
Charger
Input: 100-240V Output: 58.8V, 2A
Tire
Size: 10"x3.0" Type: Vacuum Rubber Tires Best Tire Pressure: 45 PSI
Display
Type: LCD Size: 4.5 in NFC: Yes
Package
Size: 50.8 x 10.7 x 24.8 in / 1290 x 272 x 630mm G.W.: 86 lb/ 39kg
Package List
1 x E-Scooter 1 x User Manual 1 x Charger 1 x T10 Tamper-Proof Hex Key 1 x 3mm Allen Key 1 x 4mm Allen Key 3 x NFC Card (One with Lanyard) 2 x Gloves
Note: The Ausom lab achieved maximum range with a load of 143 lbs, riding at a constant speed of 9 mph on a flat road at 68°F without strong winds.
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}%`}
`
}()}
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)
${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') || '1745483216467';
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') || '1745483216467';
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 ? '36b8942b-9244-4012-9200-a075e0aa7616' : 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 ? '36b8942b-9244-4012-9200-a075e0aa7616' : 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_({
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();
});
}
}
renderHideSkeleton = () => {
const skeletonEle = document.querySelector('#revue_skeleton');
if (skeletonEle) {
skeletonEle.classList.add('hidden');
}
}
renderEmptyComment = () => {
const emptyEle = document.querySelector(`#revue-empty-1745483216467`);
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,
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 = '36b8942b-9244-4012-9200-a075e0aa7616';
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) => {
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 = '1745483216467';
window.reviewProductSettings = {};
const default_settings = {
"star_least": "5",
"only_featured": false,
"only_media": false,
"review_insufficient": "no_reviews",
"mini_quantity": 5,
"actions": "empty",
"pc_layout": "single_column",
"m_loading_type": "modal",
"m_modal_page_limit": "3",
"page_limit": 10,
"display_product_link": false,
"hide_review_section": true,
"title": "Reviews",
"title_color": "rgba(51, 51, 51, 1)",
"primary_color": "rgba(48, 53, 77, 1)",
"section_bg_color": "rgba(255, 255, 255, 1)",
"background_color_new": "rgba(255, 255, 255, 1)"
};
// 兼容旧数据,去除html标签
const user_settings = {
"description_text": "Here are what our customers say.",
"star_least": "5",
"only_featured": false,
"only_media": false,
"review_insufficient": "no_reviews",
"mini_quantity": 5,
"actions": "hide",
"pc_layout": "single_column",
"m_loading_type": "modal",
"m_modal_page_limit": "3",
"comment_page_limit": 10,
"page_limit": 10,
"display_product_link": false,
"hide_review_section": true,
"title": "Customer Reviews",
"accent_color": null,
"title_color": "rgba(51, 51, 51, 1)",
"text_color": "rgba(48, 53, 77, 1)",
"section_bg_color": null,
"background_color_new": null
};
window.reviewProductSettings[section_id] = Object.assign({}, default_settings, user_settings, {
page_limit: user_settings.comment_page_limit || user_settings.page_limit || default_settings.page_limit
});
${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 rules = data.data.rules;
return `
`
}()}
${function(){
const isCart = data.data.isCart;
const isCollection = data.data.isCollection;
const isProduct = data.data.isProduct;
const isIndex = data.data.isIndex;
return `
${isCart ? 'The items in the shopping cart do not participate in any recommendation rule. Add the participating items to your shopping cart to check the design.' : ''}
${isProduct ? 'This product did not participated in any recommendation rule. Switch to another product to check the design.' : ''}
${isCollection ? 'The items in this collection do not participate in any recommendation rule. Switch the participating items to check the design.' : ''}
${isIndex ? 'The home page do not participate in any recommendation rule.' : ''}
(This prompt would not display on client-side)
Recommended Products
`
}()}
${function(){
const rule = data.data;
const quickShopButtonVisible = !rule.config.quick_shop_button_type || rule.config.quick_shop_button_type === 'button';
const getImageHeight = function(image){
const image_size = rule.config.image_size || 0;
const imageWidth = image.width || 600;
const imageHeight = image.height || 800;
let ratio = 0;
if(image_size == 0){
ratio = (imageHeight / imageWidth).toFixed(2);
}else if(image_size == 1){
ratio = 1.5;
}else if(image_size == 2){
ratio = 1;
}else if(image_size == 3){
ratio = 0.75;
}
return imageWidth * ratio;
};
const toQuery = obj =>
Object.keys(obj)
.map(k =>
Array.isArray(obj[k])
? obj[k].map(v => `${k}[]=${encodeURIComponent(v)}`).join('&')
: `${k}=${encodeURIComponent(obj[k])}`
)
.join('&');
return `
`
}()}
const isSpecialHeroTheme = window.SHOPLAZZA?.theme?.merchant_theme_name == 'Hero' && window.SHOPLAZZA?.theme?.merchant_theme_c_version == '2.2.19';
const specialHeroThemeClassName = 'hero_2_2_19_smart_recommend_block';
class SpzSmartBlockComponent extends SPZ.BaseElement {
constructor(element) {
super(element);
this.templates_ = null;
this.container_ = null;
this.i18n_ = {};
this.config_ = {};
this.show_type_ = 3;
this.product_resource_id_ = '';
this.collection_resource_id_ = '';
this.cart_items_ = [];
this.customer_id_ = '';
this.order_id_ = '';
}
static deferredMount() {
return false;
}
isLayoutSupported(layout) {
return layout == SPZCore.Layout.CONTAINER;
}
buildCallback() {
const template_type = window.C_SETTINGS.meta.page.template_type;
if (template_type === 1) {
this.show_type_ = 3;
this.product_resource_id_ = window.C_SETTINGS.meta.page.resource_id;
} else if (template_type === 2) {
this.show_type_ = 4;
this.collection_resource_id_ = window.C_SETTINGS.meta.page.resource_id;
} else if (template_type === 15){
this.show_type_ = 5;
} else if (template_type === 13){
this.show_type_ = 6;
} else if (template_type === 20){
this.show_type_ = 7;
this.customer_id_ = window.C_SETTINGS.customer.customer_id;
} else if (template_type === 35){
this.show_type_ = 8;
this.order_id_ = window.location.pathname.split('/').pop();
}
this.templates_ = SPZServices.templatesForDoc(this.element);
this.setAction_();
}
mountCallback() {
const that = this;
const themeName = window.C_SETTINGS.theme.merchant_theme_name;
const isGeek = /Geek/.test(themeName);
this.fetchRules().then((res) => {
if (res && res.rules && res.rules.length) {
const blockEl = document.getElementById('smart_recommend_block');
this.initBlockClass(blockEl);
this.initItemClass(blockEl);
SPZ.whenApiDefined(blockEl).then((api) => {
api.render({data: res}, true).then(() => {
if (isGeek && that.show_type_ === 6) {
blockEl.querySelector('.plugin_container_wrpper').style.padding = '30px 0';
}
const recommendStyle = document.createElement('style');
recommendStyle.innerHTML = `
.plugin__recommend_container,.app-recommend-card {
display: none !important;
}
`;
document.head.appendChild(recommendStyle);
const fetchList = [];
res.rules.forEach((rule) => {
fetchList.push(this.fetchRuleProductList(rule.id));
});
const fetchAll = Promise.all(fetchList);
fetchAll.then((p_res) => {
res.rules.forEach((rule, index) => {
rule.products = p_res[index] && p_res[index].products;
if (rule.products && rule.products.length) {
const modalRender = document.getElementById('smart_recommend_js_root');
const $dest = document.getElementById('cart');
const isLifeStyle = /Life.*Style/.test(window.C_SETTINGS.theme.merchant_theme_name);
if (modalRender && isLifeStyle && $dest.clientWidth > 767) {
modalRender.classList.add('zb-mt-[-180px]')
}
}
const ruleEl = document.getElementById('smart_recommend_rule_' + rule.id);
SPZ.whenApiDefined(ruleEl).then((api) => {
api.render({data: rule}, true).then(() => {
that.impressListen(`#smart_recommend_rule_ul_${rule.id}`, function(){
that.trackRuleImpress(rule);
});
const btnElList = document.querySelectorAll(`#smart_recommend_rule_ul_${rule.id} button`);
btnElList.forEach((btnEl) => {
if (btnEl && rule.config && rule.config.quick_shop_button_bg_color && rule.config.quick_shop_button_text_color) {
btnEl.style.backgroundColor = rule.config.quick_shop_button_bg_color;
btnEl.style.color = rule.config.quick_shop_button_text_color;
}
});
if (isSpecialHeroTheme) {
ruleEl.querySelectorAll(`.smart_recommend_title`).forEach(dom=>{
dom.classList.add('type-title-font-family');
});
document.querySelectorAll(`.${specialHeroThemeClassName} #smart_recommend_rule_ul_${rule.id} .zb-recommend-price-line-through .money`).forEach(dom=>{
dom.classList.add('type-body-font-family');
});
};
});
});
});
});
})
})
} else {
if (window.top !== window.self) {
const template_type = window.C_SETTINGS.meta.page.template_type;
const holderEl = document.getElementById('smart_recommend_preview_no_data_placeholder');
SPZ.whenApiDefined(holderEl).then((api) => {
api.render({data: { isCart: template_type === 13, isCollection: template_type === 2, isProduct: template_type === 1, isIndex: template_type === 15 }}, true);
});
}
}
});
}
initBlockClass(blockEl) {
if (!blockEl) return;
if (blockEl.parentElement && blockEl.parentElement.offsetWidth === document.body.clientWidth) {
blockEl.classList.add('smart_recommend_block_fullscreen');
};
if (isSpecialHeroTheme) {
blockEl.classList.add(specialHeroThemeClassName);
};
}
initItemClass(blockEl) {
if (blockEl) {
const containerWidth = blockEl.offsetWidth;
let itemWidth = '';
if (containerWidth > 780) {
itemWidth = '16%';
} else if (containerWidth > 600) {
itemWidth = '20%';
} else {
itemWidth = '24%';
}
const itemStyleEl = document.createElement('style');
itemStyleEl.innerHTML = `.zb-recommend-li-item{ width: ${itemWidth}; }`;
document.body.appendChild(itemStyleEl);
}
}
setAction_() {
this.registerAction('quickShop', (data) => {
const that = this;
const product_id = data.args.product_id;
const productIndex = data.args.productIndex;
const rule_id = data.args.rule_id;
const ssp = data.args.ssp;
const scm = data.args.scm;
const cfb = data.args.cfb;
const ifb = data.args.ifb;
const modalRender = document.getElementById('smart_recommend_product_modal_render');
if (modalRender) {
document.body.appendChild(modalRender);
}
if (product_id) {
this.fetchProductData(product_id).then((res) => {
const product = res.products && res.products.length && res.products[0] || {};
product.cfb = cfb;
product.ifb = ifb;
SPZ.whenApiDefined(modalRender).then((api) => {
api.render({product: product, productIndex: productIndex, rule_id: rule_id, ssp: ssp, scm: scm, show_type: that.show_type_}, true).then(() => {
const modalEl = document.getElementById('smart_recommend_product_modal');
SPZ.whenApiDefined(modalEl).then((modal) => {
that.impressListen('#smart_recommend_product_modal', function(){
that.trackQuickShop({ rule_id: rule_id, product_id: product_id });
});
modal.open();
});
const formEl = document.getElementById('smart_recommend_product_form');
SPZ.whenApiDefined(formEl).then((form) => {
form.setProduct(product);
});
const variantEl = document.getElementById('smart_recommend_product_variants');
SPZ.whenApiDefined(variantEl).then((variant) => {
variant.handleRender(product);
});
});
})
});
}
});
this.registerAction('handleScroll', (data) => {
this.directTo(data.args.rule_id, data.args.direction);
});
this.registerAction('handleProductChange', (data) => {
const variant = data.args.data.variant;
const product = data.args.data.product;
const imageRenderEl = document.getElementById('smart_recommend_product_image');
SPZ.whenApiDefined(imageRenderEl).then((api) => {
api.render({ variant: variant, product: product });
});
});
this.registerAction('handleAtcSuccess', (detail) => {
const data = detail.args;
data.data.product = data.data.product || {};
data.data.variant = data.data.variant || {};
const product_id = data.data.product.id;
const product_title = data.data.product.title;
const variant_id = data.data.variant.id;
const price = data.data.variant.price;
const rule_id = data.rule_id;
const aid = `smart_recommend.${this.show_type_}.${rule_id}`;
const ifb = data.data.product.ifb;
const cfb = data.data.product.cfb;
const ssp = data.ssp;
const scm = data.scm;
const spm = `smart_recommend_${this.show_type_}.${data.spmIndex}`;
const params = {
id: product_id,
product_id: product_id,
number: 1,
name: product_title,
variant_id: variant_id,
childrenId: variant_id,
item_price: price,
source: 'add_to_cart',
_extra: {
aid: aid,
ifb: ifb,
cfb: cfb,
scm: scm,
spm: `..${window.C_SETTINGS.meta.page.template_name}.${spm}`,
ssp: ssp,
}
};
this.tranckAddToCart(params);
});
this.registerAction('addATCHook', (data) => {
const params = data.args;
const spm = `smart_recommend_${this.show_type_}.${params.spmIndex}`;
this.myInterceptor_ = window.djInterceptors && window.djInterceptors.track.use({
event: 'dj.addToCart',
params: {
aid: `smart_recommend.${this.show_type_}.` + params.rule_id,
ssp: params.ssp,
scm: params.scm,
cfb: params.cfb,
spm: `..${window.C_SETTINGS.meta.page.template_name}.${spm}`,
},
once: true
});
});
}
tranckAddToCart(detail) {
if (window.$) {
window.$(document.body).trigger('dj.addToCart', detail);
}
}
fetchRules() {
const payload = {
show_type: this.show_type_,
};
let that = this;
if (this.show_type_ === 6) {
let line_items = [];
return this.fetchCart().then((res) => {
if (res && res.cart && res.cart.line_items) {
line_items = res.cart.line_items.map((item) => {
return { product_id: item.product_id, variant_id: item.variant_id, quantity: item.quantity, price: item.price }
});
}
payload.line_items = line_items;
that.cart_items_ = line_items;
return that.fetchRulesRequest(payload);
});
} else {
if (this.show_type_ === 3) {
payload.line_items = [{ product_id: this.product_resource_id_ }];
} else if (this.show_type_ === 4) {
payload.collection_id = this.collection_resource_id_;
} else if (this.show_type_ === 7) {
payload.customer_id = this.customer_id_;
} else if (this.show_type_ === 8) {
payload.order_id = this.order_id_;
}
return this.fetchRulesRequest(payload);
}
}
fetchRulesRequest(payload) {
return fetch(window.C_SETTINGS.routes.root + "/api/possum/recommend_query", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
}).then(function(res){
if(res.ok){
return res.json();
}
});
}
fetchCart() {
return fetch(`/api/cart/cart-select?r=${Math.random().toString(36).slice(-4)}`)
.then((res) => {
if (res.ok) {
return res.json();
}
});
}
fetchRuleProductList(rule_id) {
const payload = {
page: 1,
limit: 100,
fields: ["title", "url", "image", "min_price_variant.price", "min_price_variant.compare_at_price"],
rule_id: rule_id,
};
if (this.show_type_ === 3) {
payload.line_items = [{ product_id: this.product_resource_id_ }];
} else if (this.show_type_ === 4) {
payload.collection_id = this.collection_resource_id_;
} else if (this.show_type_ === 6) {
payload.line_items = this.cart_items_;
} else if (this.show_type_ === 7) {
payload.customer_id = this.customer_id_;
} else if (this.show_type_ === 8) {
payload.order_id = this.order_id_;
}
return fetch(window.C_SETTINGS.routes.root + "/api/possum/recommend_products", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
}).then(function(res){
if(res.ok){
return res.json();
}
}).catch(function(err){
console.log(err);
});
}
fetchProductData(product_id) {
return fetch(window.C_SETTINGS.routes.root + "/api/possum/products", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
product_ids: [product_id],
fields: [ "images", "options", "min_price_variant", "variants"]
})
}).then(function(res){
if(res.ok){
return res.json();
}
}).catch(function(err){
console.log(err);
const loadingEl = document.getElementById('smart_recommend_loading');
if (loadingEl) {
loadingEl.style.display = 'none';
}
});
}
getStyle(ele, style) {
if (!ele) return;
if (window.getComputedStyle) {
return window.getComputedStyle(ele)[style];
}
return ele.currentStyle[style];
}
directTo(id, direction) {
const scrollElement = document.getElementById(`smart_recommend_rule_ul_${id}`);
const blockWidth = parseInt(this.getStyle(scrollElement, 'width'));
const scrollLength = (blockWidth * 0.19 - 12) * 5;
const scrollPoint = scrollElement.scrollWidth - scrollElement.clientWidth;
if (!scrollElement) return;
if (direction === 'left') {
if (document.dir === 'rtl') {
scrollElement.scrollTo({
left: Math.abs(scrollElement.scrollLeft) >= scrollPoint - 100 ? 0 : scrollElement.scrollLeft - scrollLength,
behavior: 'smooth'
});
return;
}
scrollElement.scrollTo({
left: Math.max(scrollElement.scrollLeft - scrollLength, 0),
behavior: 'smooth'
});
} else {
if (document.dir === 'rtl') {
scrollElement.scrollTo({
left: Math.abs(scrollElement.scrollLeft) >= scrollPoint + 100 ? 0 : scrollElement.scrollLeft + scrollLength,
behavior: 'smooth'
});
return;
}
scrollElement.scrollTo({
left: scrollElement.scrollLeft >= scrollPoint - 100 ? 0 : scrollElement.scrollLeft + scrollLength,
behavior: 'smooth'
});
}
}
trackRuleImpress(rule) {
if (window.sa && window.sa.track) {
window.sa.track("plugin_common", {
plugin_name: "upsell",
event_type: "impressions",
rule_id: rule.id,
ssp: rule.ssp,
scm: rule.scm,
show_type: this.show_type_,
support_app_block: window.C_SETTINGS.theme.support_app_block
});
window.sa.track("module_impressions", {
aid: `smart_recommend.${this.show_type_}.${rule.id}`,
support_app_block: window.C_SETTINGS.theme.support_app_block
});
}
}
trackQuickShop(data) {
window.sa && sa.track && sa.track("plugin_common", {
plugin_name: "upsell",
event_type: "quick_shop",
rule_id: data.rule_id,
product_id: data.product_id,
show_type: this.show_type_,
});
}
impressListen(selector, cb) {
const el = document.querySelector(selector);
const onImpress = (e) => {
if (e) {
e.stopPropagation();
}
cb();
};
if (el && !el.getAttribute('imprsd')) {
el.addEventListener('impress', onImpress)
} else if (el) {
onImpress();
}
}
}
SPZ.defineElement('spz-custom-smart-block', SpzSmartBlockComponent);
${(function(){
const product = data.product;
const toQuery = obj =>
Object.keys(obj)
.map(k =>
Array.isArray(obj[k])
? obj[k].map(v => `${k}[]=${encodeURIComponent(v)}`).join('&')
: `${k}=${encodeURIComponent(obj[k])}`
)
.join('&');
return `
${product.images.map((image) => {
return ` `
}).join('')}
Add To Cart
Buy Now
`;
})()}
${(function(){
const product = data.product;
const avail_variants = product.variants.filter(function(variant){
return variant.available;
});
const selected_variant = product.min_price_variant.available ? product.min_price_variant : avail_variants.length && avail_variants[0];
return `
${option.name}
${
option.values.map(function(value, index){
const checked = selected_variant["option"+option.position] == value ? "checked": "";
return `
${value}
`
}).join("")
}
`
})()}
(function() {
const STATUS = {
LOADING: 'loading',
FINISH: 'finish'
};
const RESULT = {
EMPTY: 'data-empty'
};
class SPZCustomCartSku extends SPZ.BaseElement {
renderData = [];
/**
* add to cart reselect item, and delete process:
* 1. record reselect id before show reselect modal
* 2. add to cart success, mark `needDeleteReselectRecord` to true
* 3. close reselect modal / drawer
* 4. spz-cart re-render, triggered mounted event
* 5. call refresh
*/
// mark delete reselect id
recordReselectId = null;
// mark should delete reselect record after spz-cart mounted event
needDeleteReselectRecord = false;
refreshLock = false;
addToCartSuccess = false;
// cache paused refresh data
refreshDataCache = [];
// cache similar products data, for manual add to cart
similarData = [];
constructor(element) {
super(element);
this.xhr_ = SPZServices.xhrFor(this.win);
this.action_ = SPZServices.actionServiceForDoc(this.element);
}
setupAction_() {
this.registerAction('refresh', (invocation) => {
const data = invocation && invocation.args && invocation.args.data || {};
this.refresh(data);
});
this.registerAction('deleteReselect', SPZCore.Types.debounce(
this.win,
(invocation) => {
this.deleteReselect(invocation?.args?.id);
},
200
));
this.registerAction('deleteInvalid', SPZCore.Types.debounce(
this.win,
(invocation) => {
this.deleteInvalid(invocation?.args?.id);
},
200
));
this.registerAction('setReselectRecordId', (invocation) => {
this.setReselectRecordId(invocation?.args?.id);
});
this.registerAction('clearNeedReselectRecord', (invocation) => {
this.clearNeedReselectRecord();
});
this.registerAction('registerDeleteReselectTask', (invocation) => {
this.registerDeleteReselectTask(invocation?.args?.data);
});
this.registerAction('lockRefresh', (invocation) => {
this.lockRefresh();
});
this.registerAction('releaseRefreshLock', (invocation) => {
this.releaseRefreshLock();
});
this.registerAction('manualAddToCart', (invocation) => {
this.manualAddToCart(invocation?.args?.id);
});
this.registerAction('syncSimilarData', (invocation) => {
this.syncSimilarData(invocation?.args?.data);
});
// DEBUG
this.registerAction('log', (invocation) => {
const data = invocation && invocation.args && invocation.args.data || {};
console.log('log', invocation);
});
}
buildCallback() {
this.setupAction_();
}
isLayoutSupported(layout) {
return true;
}
/**
* 数据去重
* @param {Array} data
*/
uniq_(data) {
const result = [];
for(let i = 0; i < data.length; i++) {
if(!result.includes(data[i])) {
result.push(data[i]);
}
}
return result;
}
checkRefreshLock() {
if (this.refreshLock) {
return false;
}
return true;
}
setLoading(isLoading) {
SPZCore.Dom.toggleAttribute(this.element, STATUS.LOADING, isLoading);
SPZCore.Dom.toggleAttribute(this.element, STATUS.FINISH, !isLoading);
}
setDataResult(data) {
const isDataEmpty = !data.reselectSkus.length && !data.invalidSkus.length && !data.line_items.length;
SPZCore.Dom.toggleAttribute(this.element, RESULT.EMPTY, isDataEmpty);
}
// 存在多次请求顺序问题
async refresh(_data) {
// 接口失败时返回数据不为对象
if (!(typeof _data === 'object' && _data.line_items && _data.line_items.length > 0)) {
const newData = {
line_items: [],
reselectSkus: [],
invalidSkus: [],
displayInvalidSkus: []
};
this.trigger_('refreshError', newData);
this.setLoading(false);
return;
}
if (!this.checkRefreshLock()) {
this.setRefreshCache(_data);
return;
}
this.setLoading(true);
let data = _data;
if (this.needDeleteReselectRecord && this.recordReselectId) {
let hasError = false;
try {
data = await this.deleteReselectRecordBeforeRefresh(_data);
} catch (e) {
hasError = true;
console.error(e);
const newData = {
...data,
reselectSkus: [],
invalidSkus: [],
displayInvalidSkus: []
};
this.trigger_('refreshError', newData);
this.setLoading(false);
this.setDataResult(newData);
return;
}
this.needDeleteReselectRecord = false;
if (hasError) {
return;
}
}
// 获取失效商品
const invisibleItems = data.ineffectives;
// 获取失效商品内仅售罄商品的sku
const soldOutItems = invisibleItems.filter(item => item.reason === "line_item_sold_out");
// 通过失效 sku 获取 spu, 注意去重
const soldOutSpuIds = this.uniq_(soldOutItems.map(item => item.product_id));
const querySpuIds = soldOutSpuIds.map(spuId => `ids[]=${spuId}`).join('&');
if (!soldOutSpuIds.length) {
const newData = {
...data,
reselectSkus: [],
invalidSkus: [],
displayInvalidSkus: []
};
this.trigger_('refreshSuccess', newData);
this.renderData = newData;
this.setLoading(false);
this.setDataResult(newData);
return;
}
// 请求 spu 下其他 sku 库存
this.xhr_.fetchJson(`/api/product/list?limit=${soldOutSpuIds.length}&${querySpuIds}`)
.then(res => {
const reselectSkus = [];
const invalidSkus = [];
// spu 维度展示
const displayInvalidSkus = [];
res.data.list.forEach(product => {
// spu 匹配, 存在多 sku 情况
const soldOutSkus = soldOutItems.filter(item => item.product_id === product.id);
if (!soldOutSkus.length) {
return;
}
// 通过失效 sku 获取 spu 下其他 sku 库存, 存在其他可用库存时标记为 "Reselect" 按钮可用
//const allowReselect = product.variants
// .filter(variant => variant.option1 === soldOutProduct.variant.option1 && variant.option2 === soldOutProduct.variant.option2 && variant.option3 === soldOutProduct.variant.option3)
// .some(variant => variant.available);
// 查询售罄 sku 对应商品是否可加购, 标记为 Reselect 可用项
if (product.available) {
reselectSkus.push(...soldOutSkus);
// 若商品不可用(全部 sku 售罄), 归纳到失效商品列表
} else {
invalidSkus.push(...soldOutSkus);
// spu 维度, 仅添加一项 sku
displayInvalidSkus.push(soldOutSkus[0]);
}
});
const newData = {
...data,
reselectSkus,
invalidSkus,
displayInvalidSkus
};
this.trigger_('refreshSuccess', newData);
this.renderData = newData;
this.setLoading(false);
this.setDataResult(newData);
})
.catch(err => {
this.setLoading(false);
console.error(err);
this.trigger_('refreshError', data);
}).finally(() => {
});
}
async deleteReselect(id, ignoreEmit) {
if (!id) {
return;
}
const targetSku = this.renderData.reselectSkus.find(item => item.id === id);
try {
const res = await this.xhr_.fetchJson(`/api/cart/${targetSku.variant_id}`, {
method: 'DELETE',
body: {
id: targetSku.id,
product_id: targetSku.product_id,
variant_id: targetSku.variant_id,
}
});
const newData = {
...this.renderData,
reselectSkus: this.renderData.reselectSkus.filter(item => item.id !== id),
};
this.renderData = newData;
!ignoreEmit && this.trigger_('deleteSuccess', newData);
} catch (err) {
console.error(err);
!ignoreEmit && this.trigger_('deleteError', err);
}
}
deleteInvalid(id) {
if (!id) {
return;
}
const targetSku = this.renderData.invalidSkus.find(item => item.id === id);
this.xhr_.fetchJson(`/api/cart/${targetSku.variant_id}`, {
method: 'DELETE',
body: {
id: targetSku.id,
product_id: targetSku.product_id,
variant_id: targetSku.variant_id,
}
})
.then(res => {
const newData = {
...this.renderData,
invalidSkus: this.renderData.invalidSkus.filter(item => item.id !== id),
displayInvalidSkus: this.renderData.displayInvalidSkus.filter(item => item.id !== id),
};
this.trigger_('deleteSuccess', newData);
this.renderData = newData;
})
.catch(err => {
console.error(err);
this.trigger_('deleteError', err);
});
}
// record reselect sku id before show reselect modal
setReselectRecordId(id) {
if (!id) {
return;
}
this.recordReselectId = id;
}
async deleteReselectRecordBeforeRefresh(data) {
if (!this.recordReselectId) {
return;
}
await this.deleteReselect(this.recordReselectId, true);
return {
...data,
ineffectives: data.ineffectives.filter(item => item.id !== this.recordReselectId)
}
}
clearNeedReselectRecord() {
this.needDeleteReselectRecord = false;
}
registerDeleteReselectTask(productData) {
const targetSku = this.renderData.reselectSkus.find(item => item.id === this.recordReselectId);
if (targetSku?.variant_id && productData?.variant.id) {
if (targetSku.variant_id === productData?.variant.id) {
this.needDeleteReselectRecord = false;
return;
}
}
this.needDeleteReselectRecord = true;
}
// pause cart refresh(trigger by similar products)
lockRefresh() {
if (this.refreshLock) {
return;
}
this.refreshLock = true;
}
releaseRefreshLock() {
if (!this.refreshLock) {
return;
}
this.refreshLock = false;
this.refreshWithCache();
}
// direct add_to_cart(trigger by similar products)
async manualAddToCart(id) {
const target = this.similarData.find(item => item.id === id);
this.lockRefresh();
try {
const res = await this.xhr_.fetchJson(`/api/cart`, {
method: 'POST',
body: {
note: '',
product_id: target.id,
quantity: '1',
variant_id: target.variants[0].id,
refer_info: {
source: 'add_to_cart'
}
}
});
const newCartItems = res.data.items;
this.setRefreshCache(newCartItems, true);
} catch (err) {
console.error(err);
}
}
syncSimilarData(data) {
this.similarData = data.data;
}
setRefreshCache(data, patch) {
if (patch) {
this.refreshDataCache = {
...this.refreshDataCache,
line_items: [...this.refreshDataCache.line_items, ...data]
};
} else {
this.refreshDataCache = data;
}
}
refreshWithCache() {
this.refresh(this.refreshDataCache);
}
mountCallback() {
}
unmountCallback() {
}
/**
* trigger event
* @param {Object} data
*/
trigger_(name, data) {
const event = SPZUtils.Event.create(this.win, `spz-custom-cart-sku.${name}`, {
data,
});
this.action_.trigger(this.element, name, event);
}
}
SPZ.defineElement('spz-custom-cart-sku', SPZCustomCartSku);
}())
(function () {
class SPZCustomCartTrack extends SPZ.BaseElement {
constructor(element) {
super(element);
this.action_ = SPZServices.actionServiceForDoc(this.element);
}
isLayoutSupported(layout) {
return true;
}
buildCallback() {
this.setupAction_();
}
hash(val) {
const hashKey = Object.keys(val).sort((a, b) => a - b).reduce((acc, k) => {
acc += `{'${k}':'${val[k]}'}`;
return acc;
}, '');
return hashKey;
}
trackExtra(key, value) {
console.log('trackExtra...', key, value);
if (!window.sa) {
return;
}
const hashKey = this.hash(value);
let hasExtraInfo = false;
if (window.sa.eventInfo && window.sa.eventInfo[key] && window.sa.eventInfo[key].extra_properties) {
hasExtraInfo = window.sa.eventInfo[key].extra_properties.some(p => {
return hashKey === this.hash(p);
});
}
if (hasExtraInfo) {
return;
}
window.sa && window.sa.registerExtraInfo(key, value);
}
delExtraTrack(key, value) {
const hashKey = this.hash(value);
if (window.sa.eventInfo && window.sa.eventInfo[key] && window.sa.eventInfo[key].extra_properties) {
window.sa.eventInfo[key].extra_properties = window.sa.eventInfo[key].extra_properties.filter(p => hashKey !== this.hash(p));
}
}
track(key, value) {
console.log('tracking...', key, value);
window.sa && window.sa.track(key, value);
}
setupAction_() {
this.registerAction('registerReselectAtc', () => {
this.trackExtra('add_to_cart', {
function_name: 'Farida',
action_type: 'reselect'
});
});
this.registerAction('clearReselectAtc', () => {
this.delExtraTrack('add_to_cart', {
function_name: 'Farida',
action_type: 'reselect'
});
});
this.registerAction('registerSimilarAtc', () => {
this.trackExtra('add_to_cart', {
function_name: 'Farida',
action_type: 'similar_product'
});
});
this.registerAction('clearSimilarAtc', () => {
this.delExtraTrack('add_to_cart', {
function_name: 'Farida',
action_type: 'similar_product'
});
});
const clickParams = {
business_type: 'product_plugin',
event_name: 'function_click',
function_name: 'Farida',
plugin_name: 'Farida',
template_name: 'cart',
template_type: '13',
module: 'online_store',
module_type: 'online_store',
tab_name: '',
card_name: '',
event_developer: 'ccbken',
event_type: 'click',
};
this.registerAction('trackDelReselect', () => {
this.track('function_click', {
...clickParams,
event_info: JSON.stringify({
action_type: 'cart_delete',
element_type: 'sku'
})
});
});
this.registerAction('trackClickReselect', () => {
this.track('function_click', {
...clickParams,
event_info: JSON.stringify({
action_type: 'reselect',
element_type: 'sku'
})
});
});
this.registerAction('trackDelSimilar', () => {
this.track('function_click', {
...clickParams,
event_info: JSON.stringify({
action_type: 'cart_delete',
element_type: 'spu'
})
});
});
this.registerAction('trackClickSimilar', () => {
this.track('function_click', {
...clickParams,
event_info: JSON.stringify({
action_type: 'reselect',
element_type: 'spu'
})
});
});
this.registerAction('trackOpenSimilar', () => {
this.track('function_expose', {
...clickParams,
event_name: 'function_expose',
event_type: 'expose',
event_info: JSON.stringify({
popup_name: 'farida_product_popup'
})
});
});
}
}
SPZ.defineElement('spz-custom-cart-track', SPZCustomCartTrack);
}())