const TAG = 'spz-custom-utils';
const DEFAULT_DELAY_TIME = 100;
class SpzCustomUtils 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() {
}
//判断是否为移动端
isMobile() {
/* 判断机型与处理 */
const u = navigator.userAgent
const isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; // android终端
const isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // ios终端
return (isAndroid || isiOS);
};
/**
* url query param to object
* @param {string} url
* @returns {object} query object
*/
params(url) {
url = url || window.location.href;
let params = {};
url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (str, key, value) {
try {
params[key] = decodeURIComponent(value);
} catch (e) {
params[key] = value;
}
});
return params;
};
/**
* @param fn {Function} 实际要执行的函数
* @param delay {Number} 延迟时间,单位是毫秒(ms)
* @return {Function} 返回一个“防反跳”了的函数
*/
debounce(fn, delay) {
// 定时器,用来 setTimeout
let timer;
// 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
return function () {
// 保存函数调用时的上下文和参数,传递给 fn
const context = this;
const args = arguments;
// 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
clearTimeout(timer);
// 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
// 再过 delay 毫秒就执行 fn
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
};
/* 节流防抖 */
throttle(func, wait, mustRun) {
var timeout,
startTime = new Date();
return function () {
var context = this,
args = arguments,
curTime = new Date();
clearTimeout(timeout);
// 如果达到了规定的触发时间间隔,触发 handler
if (mustRun && curTime - startTime >= mustRun) {
func.apply(context, args);
startTime = curTime;
// 没达到触发间隔,重新设定定时器
} else {
timeout = setTimeout(func, wait);
}
};
};
//滚动加载方法
isToPageEnd(id) {
const $el = document.querySelector(`[data-section-id='${id}']`);
const scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; //滚动条距离顶部的高度
const clientHeight = window.innerHeight; //当前可视的页面高度
const scrollHeight = document.body.scrollHeight; //当前页面的总高度
const elOffsetTop = $el.getBoundingClientRect().top + window.pageYOffset - document.documentElement.clientTop; // 元素距离文档顶部距离
// 如果改卡片下面还有卡片或者dom,计算滚动加载需要考虑这个高度
const toBottom = scrollHeight - ($el.offsetHeight + elOffsetTop); //元素到浏览器底部的高度
if (scrollTop + clientHeight + toBottom + 100 >= scrollHeight) {
return true;
}
return false;
};
/**
* url 添加前缀
* @param {string} path , 必须是前面有斜杠前缀的路径
* @returns string
*/
prefixionPath(prefix,urlPath) {
if(typeof prefix !== 'string') return ;
if(typeof urlPath !== 'string') return ;
if(urlPath.indexOf('/') !== 0){
throw new Error('prefixPath: urlPath must be start with /');
}
if(prefix.indexOf('/') !== 0){
throw new Error('prefixPath: prefix must be start with /');
}
return prefix+urlPath;
}
/**
* @param {string} urlPath
* @returns {string}
* @example globalizePath('/path_a/path_b')// => '/en/path_a/path_b'
*/
globalizePath(urlPath) {
if(typeof urlPath !== 'string') return ;
if(urlPath.indexOf('/') !== 0){
urlPath = '/'+urlPath;
}
let prefix = ((SHOPLAZZA && SHOPLAZZA.routes && SHOPLAZZA.routes.root) || '');
if(prefix.length>0){
if(prefix.indexOf('/') !== 0){
prefix = '/'+prefix;
}
return this.prefixionPath(prefix,urlPath);
}else{
return urlPath;
}
}
image_padding_bottom(width, height, origin) {
origin = origin || 'limit';
if (width && height) {
const hw_ratio = height / width;
if (origin == 'limit') {
if (hw_ratio < 0.62) {
return '62%';
} else if (hw_ratio > 1.6) {
return '160%';
}
}
return parseInt(hw_ratio * 100) + '%';
}
return '100%';
}
getNumber(str) {
str = str + '';
return str.match(/\d+(\.\d+)?/g) ? Number(str.match(/\d+(\.\d+)?/g)[0]) : str;
};
// 处理货币符号
finance_money_with_shop_symbol(price, onlyNumber) {
const symbol = onlyNumber ? '' : window.SHOPLAZZA.currency_symbol;
const position = window.SHOPLAZZA ? window.SHOPLAZZA.currency_symbol_pos : 'left';
const format = window.SHOPLAZZA ? window.SHOPLAZZA.money_format : 'amount';
if (position == 'right') {
return Number(Number(price) * 1).format(format) + symbol;
}
return symbol + Number(Number(price) * 1).format(format);
};
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, SpzCustomUtils)
${function() {
const options = data.options;
const defaultValue = data.defaultValue || options[0].value;
let defaultText = options[0].text;
const hasOption = Array.apply(null, options).find((option) => {
return option.value === defaultValue;
});
if(hasOption){
defaultText = hasOption.text;
}
return `
`
}()}
const TAG = 'spz-custom-sort';
class SpzCustomSort extends SPZ.BaseElement {
constructor(element) {
super(element);
this.spz_custom_id = '';
}
static deferredMount() {
return false;
}
buildCallback() {
this.action_ = SPZServices.actionServiceForDoc(this.element);
this.templates_ = SPZServices.templatesForDoc(this.element);
this.xhr_ = SPZServices.xhrFor(this.win);
this.setupAction_();
}
init() {
this.bindEvent();
}
bindEvent() {
const $selectList = SPZCore.Dom.scopedQuerySelectorAll(
this.element,
".sort_custom_content li"
);
const $customerSelect = SPZCore.Dom.scopedQuerySelector(
this.element,
".sort_custom_select"
);
// 选择下拉选项
Array.from($selectList).forEach((node) => {
SPZUtils.Event.listen(node, 'click', ()=> {
let value = node.getAttribute('value');
let text = node.getAttribute('text');
// 触发selectChange 事件
this.triggerEvent_('selectChange', {
value: value,
name: value
})
$customerSelect.innerHTML = text;
const panelChilds = this.element.querySelectorAll(".sort_custom_panel li");
// 清空其他选项的勾选状态
Array.from(panelChilds).forEach((el) => {
if(el.getAttribute('value') == value) {
el.classList.add("active")
} else {
el.classList.remove('active');
}
})
});
})
}
// 渲染界面
async doRender_(data) {
// 操作该组件的dom id
this.spz_custom_id = data.id;
return this.templates_
.findAndRenderTemplate(this.element, data)
.then((el) => {
const children = this.element.querySelector('*:not(template)');
children && SPZCore.Dom.removeElement(children);
this.element.appendChild(el);
}).then(() => {
this.init();
});
}
setupAction_() {
this.registerAction('render', async(invocation) => {
const data = invocation.args.data;
this.doRender_(data)
});
this.registerAction('handleSelect', async(invocation) => {
const data = invocation.args.data;
});
this.registerAction('handleDropdownOpen', async(invocation) => {
const $selectDropDown = SPZCore.Dom.scopedQuerySelector(
this.element,
".select_drop_down"
);
$selectDropDown.classList.add('select_drop_down_rotate');
});
this.registerAction('handleDropdownClose', async(invocation) => {
const $selectDropDown = SPZCore.Dom.scopedQuerySelector(
this.element,
".select_drop_down"
);
$selectDropDown.classList.remove('select_drop_down_rotate');
});
}
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, SpzCustomSort)
${(function() {
const products = data.products;
const discountType = data.discount_info.discount_type;
const progress = 'PROGRESS_ONGOING';
const product_grid_image_size = "natural";
const product_display = originData?.product_display;
const show_atc = product_display ? product_display?.show_add_to_cart : true;
const show_discount_label = product_display?.show_discount_label || false;
const show_sales_progress = product_display?.sales_progress?.enabled || false;
const sales_progress_format = product_display?.sales_progress?.format || '';
const limit_purchase_tip = data.limit_purchase_tip || "";
const productConfig = product_display?.config && JSON.parse(product_display?.config).color;
const sale_progress_bg = productConfig.sale_bar_background_color;
const sale_progress_bg_start = productConfig.progress_sale_bar_background_color_start;
const sale_progress_bg_end = productConfig.progress_sale_bar_background_color_end;
const sale_count_text = productConfig.sale_bar_atc_color;
const discount_label_start = productConfig.discount_sign_background_color_start;
const discount_label_end = productConfig.discount_sign_background_color_end;
const discount_label_text = productConfig.discount_sign_color;
if(!products.length) return '';
return products.map(product => {
let price = product.price || 0;
let priceMin = product.price_min || 0;
let priceMax = product.price_max || 0;
let compareAtPriceMax = product.compare_at_price_max || 0;
let compareAtPrice = product.compare_at_price || 0;
let title = product.title || '';
let id = product.id || '';
let url = product.url || '';
let type = product.type || '';
let is_sold_out = false;
if (product.available == false && product.inventory_policy != 'continue') {
is_sold_out = true;
}
const soldOutLang1 = "Sold out";
const soldOutLang2 = "Sold out";
const noNeedBtn = discountType !== "DT_MIX_MATCH_BUNDLE" && discountType !== "DT_CLASSIC_BUNDLE";
let imageWidth;
if (product.image.width) {
imageWidth = product.image.width;
} else {
imageWidth = "300px";
}
let imageHeight;
if (product.image.height) {
imageHeight= product.image.height;
} else {
imageHeight = "300px";
}
if (product_grid_image_size !== 'natural') {
imageHeight = (imageWidth * parseFloat(product_grid_image_size)) / 100;
}
const flash_sale_info = product?.flash_sale_info;
const is_flashsale_sold_out = flash_sale_info?.discount_sales_rate == "100";
const off_ratio = flash_sale_info?.off_ratio;
price = flash_sale_info?.discount_price || price;
const defaultVariantTitle = product.variants[0].title.replace('-', '/') || '';
const variantDiscountInfo = product.variants[0].discount_info || {};
const minPurchaseQty = product.discount_min_purchase_qty || product.variants[0].discount_info.discount_min_purchase_qty || 0;
if (product.published) {
return `
x ${minPurchaseQty}
${product.title}
Almost sold out
${flash_sale_info?.discount_sales}
${flash_sale_info?.discount_sales_rate}%
sold
${function(){
if (soldOutLang1){
return `
${soldOutLang1}
`
}else{
return `
${soldOutLang2}
`
}
}()}
`
} else {
return ``;
}
}).join('');
})()}
const TAG = "spz-custom-render-products";
class SpzCustomProducts extends SPZ.BaseElement {
constructor(element) {
super(element);
}
buildCallback() {
this.action_ = SPZServices.actionServiceForDoc(this.element);
this.templates_ = SPZServices.templatesForDoc(this.element);
this.xhr_ = SPZServices.xhrFor(this.win);
this.setupAction_();
}
doRender_(data) {
return this.templates_
.findAndRenderTemplate(this.element, data)
.then((el) => {
const children = this.element.querySelector('*:not(template)');
children && SPZCore.Dom.removeElement(children);
this.element.appendChild(el);
});
}
getRenderTemplate(data) {
const renderData = data || {};
return this.templates_
.findAndRenderTemplate(this.element, renderData)
.then((el) => {
const children = this.element.querySelector('*:not(template)');
children && SPZCore.Dom.removeElement(children);
return el;
});
}
setupAction_() {
this.registerAction('test', (invocation) => {
});
}
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, SpzCustomProducts)
${function() {
return `
`;
}()}
const TAG = 'spz-custom-discount-default-server';
const E_DISCOUNT_PROGRESS = {
ProgressFinished : "PROGRESS_FINISHED",
ProgressNotStarted : "PROGRESS_NOT_STARTED",
ProgressOngoing : "PROGRESS_ONGOING"
};
class SpzCustomDiscountDefault extends SPZ.BaseElement {
constructor(element) {
super(element);
this.templates_ = null;
this.section_id = 15890337540001 || 1;
this.discountObj = window.discountObj;
this.discount_info = this.discountObj.discount_info;
this.product_display = this.discountObj.landing_page_info.product_display;
this.buy_product_info = this.discountObj.buy_product_info;
this.obtain_product_info = this.discountObj.obtain_product_info;
this.discount_id = this.discount_info.id;// 活动id
this.discountI18n = {};
const THEME_NAME = window.SHOPLAZZA.theme.merchant_theme_name || window.SHOP_PARAMS.theme_name;
this.isHero = /Hero/.test(THEME_NAME);
// PROGRESS_ONGOING: 进行中 PROGRESS_NOT_STARTED: 未开始 PROGRESS_FINISHED: 已结束
this.E_DISCOUNT_PROGRESS = {
ProgressFinished : "PROGRESS_FINISHED",
ProgressNotStarted : "PROGRESS_NOT_STARTED",
ProgressOngoing : "PROGRESS_ONGOING"
};
this.E_TAB_MAP = {
scenario_buy : {
value: "1",
domId: "product_list_buy_products"
},
scenario_obtain : {
value: "2",
domId: "product_list_obtain_products"
}
}
this.tabContentIdMap = {};
this.currentTab = this.E_TAB_MAP.scenario_buy.value;
this.model_buy = {
discount_id: this.discount_id, //活动id
scenario: 1, // 枚举值,1:购买商品,2:获得商品
sort: { by: 'recommend', direction: 'asc' },
page: 2, //分页码
limit: 20, // 每页数量
loading: false, // 请求数据标示
has_more: this.buy_product_info.has_more // 是否还有数据
};
this.model_get = {
discount_id: this.discount_id, //活动id
scenario: 2, // 枚举值,1:购买商品,2:获得商品
sort: { by: 'recommend', direction: 'asc' },
page: 2,
limit: 20,
loading: false,
has_more: this.obtain_product_info?.has_more
};
this.modelMap = {
[this.E_TAB_MAP.scenario_buy.value]: this.model_buy,
[this.E_TAB_MAP.scenario_obtain.value]: this.model_get,
}
this.sortDict = {
recommend_asc: { by: 'recommend', direction: 'asc' },
title_asc: { by: 'title', direction: 'asc' },
title_desc: { by: 'title', direction: 'desc' },
price_asc: { by: 'price', direction: 'asc' },
price_desc: { by: 'price', direction: 'desc' },
created_at_desc: { by: 'created_at', direction: 'desc' },
sales_desc: { by: 'sales', direction: 'desc' },
add_to_cart_count_desc: { by: 'add_to_cart_count', direction: 'desc' },
views_desc: { by: 'views', direction: 'desc' }
};
this.sortOptions = [
{
value: 'recommend_asc',
text: "Recommend"
},
{
value: 'price_asc',
text: "Price, low to high"
},
{
value: 'price_desc',
text: "Price, high to low"
},
{
value: 'title_asc',
text: "Name, A to Z"
},
{
value: 'title_desc',
text: "Name, Z to A"
},
{
value: 'created_at_desc',
text: "Newest in"
},
{
value: 'sales_desc',
text: "Total sales, high to low"
},
{
value: 'add_to_cart_count_desc',
text: "Purchases, high to low"
},
{
value: 'views_desc',
text: "Page views, high to low"
}
];
// 直出商品数据 + 异步请求商品数据
this.products = this.buy_product_info.product;
this.productUrl = "";
// 款式信息集合
this.productStyleInfo = [];
// 弹窗内选择款式集合
this.modalVariantInfo = [];
// 加购商品列表
this.lineItems = [];
this.buyNowApi = "\/api\/checkout\/order";
this.batchAtcApi = "\/api\/cart\/batch";
this.debounceTimer = null;
this.discount_type = this.discount_info.discount_type;
}
static deferredMount() {
return false;
}
buildCallback() {
this.action_ = SPZServices.actionServiceForDoc(this.element);
this.templates_ = SPZServices.templatesForDoc(this.element);
this.xhr_ = SPZServices.xhrFor(this.win);
this.setupAction_();
Object.entries(this.E_TAB_MAP).forEach(([key, valueObj]) => {
this.tabContentIdMap[valueObj.value] = valueObj.domId;
})
}
async mountCallback() {
this.utilsApi_ = await SPZ.whenApiDefined(document.querySelector('#spz_custom_utils'));
this.init();
this.handleRenderSort();
}
init() {
this.xhr_.fetchJson(`/api/discount-i18n`, {
method: "get",
}).then((res)=>{
this.discountI18n = res;
this.bindEvent();
})
// url 携带 sort_by参数
var queryParams = this.utilsApi_.params();
var sortValue = queryParams.sort_by;
if (sortValue) {
this.model_buy.sort = this.sortDict[sortValue];
}
// 经典捆绑初始化商品数据
if(this.discount_type == 'DT_CLASSIC_BUNDLE') {
this.productStyleInfo.push(...this.buy_product_info.product.map((item) => {
return this.getFilteredVariants_(item, 'single');
}));
}
}
handleRenderSort() {
// 渲染排序
const sort_x_id = 'promotionSortProductsX';
const $sortX = document.getElementById(sort_x_id)
$sortX && SPZ.whenApiDefined($sortX).then((api) => {
// 渲染排序列表
api.doRender_({options: this.sortOptions, defaultValue: 'recommend_asc', id: sort_x_id });
})
const sort_y_id = 'promotionSortProductsY';
const $sortY = document.getElementById(sort_y_id)
$sortY && SPZ.whenApiDefined($sortY).then((api) => {
// 渲染排序列表
api.doRender_({options: this.sortOptions, defaultValue: 'recommend_asc' , id: sort_y_id});
})
}
// 获取数据,拼接html模板
async getData() {
// 请求数据
let model = this.modelMap[this.currentTab];
if (!model.has_more || model.loading) {
return;
}
model.loading = true;
this.handleLoading_({type: 'product', action: 'show'});
let $content = document.querySelector(`#${this.tabContentIdMap[this.currentTab]} .discount-default__productlist-wrap`) || document.querySelector(`.discount-default__productlist-wrap`);
let $defaultEmpty = $content && $content.querySelector('.discount_default_empty');
//查询活动商品接口
const reqBody = {
discount_id: model.discount_id,
page: model.page,
limit: model.limit,
apply_scenario: model.scenario,
sort: model.sort,
sales_channel: {
sale_channel_type: "online",
sale_channel_id: '829796'
}
}
this.xhr_.fetchJson(`/api/storefront/promotion/landing_page/product/list`, {
method: "post",
body: reqBody
}).then(async(res)=>{
// 更新参与活动的商品数量
const productCount = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionEventProductCount`);
productCount && SPZ.whenApiDefined(productCount).then((api) => {
api.render(res.count, true);
});
this.products.push(...res.products);
this.handleLoading_({type: 'product', action: 'close'});
const count = res.count;
model.has_more = res.has_more;
if (count > 0) {
$defaultEmpty && ($defaultEmpty.style.display = 'none');
model.page++;
if (res.products && res.products.length > 0) {
let products = res.products.map((product) => {
return {
...product,
url: this.utilsApi_.globalizePath(product.url),
image_padding_bottom: this.utilsApi_.image_padding_bottom(product.image.width, product.image.height,'no-limit')
}
});
// 获取商品列表渲染模板, dom挂载
const renderApi = await SPZ.whenApiDefined(document.querySelector('#discounts_products_render'));
const el = await renderApi.getRenderTemplate({
products: products,
discountI18n: this.discountI18n,
discount_info: this.discount_info,
product_display: this.product_display
});
const childNodes = el.querySelectorAll('.as-render-product-item');
if (childNodes && childNodes.length > 0) {
$content.append(...el.childNodes);
}
if(this.discount_type == 'DT_CLASSIC_BUNDLE') {
// 遍历$content 插入商品垂直虚线分割
const productListAsync = $content.querySelectorAll('.as-render-product-item');
if (productListAsync.length > 0) {
productListAsync.forEach((item, index) => {
const htmlStr = `<span class="promotion_dotted_line"></span>
<div class="promotion_plus_bundle">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M1 6H11M6 1L6 11" stroke="black" stroke-width="1.6" stroke-linecap="round"/>
</svg>
</div>
<span class="promotion_dotted_line"></span>`;
this.createAndInsertSeparator_('promotion_separator md:hidden', (index + 1) % 4 !== 0 && index !== productListAsync.length - 1, htmlStr, $content, item);
this.createAndInsertSeparator_('promotion_separator lg:hidden', (index + 1) % 2 !== 0 && index !== productListAsync.length - 1, htmlStr, $content, item);
});
}
}
if(this.discount_type == 'DT_MIX_MATCH_BUNDLE') {
const productSelector = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionProductSelector`);
productSelector && SPZ.whenApiDefined(productSelector).then((api) => {
api.init();
});
const currentPageSelectedProducts = res.products.filter(item => this.productStyleInfo.map(item => item.product_id).includes(item.id));
this.updateProductPrice_(currentPageSelectedProducts);
}
}
} else {
// 空列表
const $emptyTemplate = document.querySelector('#promotionDiscountEmpty .discount_default_empty');
const $cloneEmptyTemplate = $emptyTemplate.cloneNode(true);
$content.innerHTML = '';
$content.append($cloneEmptyTemplate);
$defaultEmpty && ($defaultEmpty.style.display = 'flex');
}
model.loading = false;
}).catch((err)=>{
this.handleRequestError_(err);
}).finally(()=>{
model.loading = false;
this.handleLoading_({type: 'product', action: 'close'});
// 经典spu纬度需要该商品信息: is_classic_bundle_product_list_variant_tag
if(this.discount_type == 'DT_CLASSIC_BUNDLE' && this.discount_info.enable_min_purchase_qty == true && this.discount_info.min_purchase_qty_type == 'spu') {
this.productStyleInfo = this.productStyleInfo.map((item) => {
return {
...item,
is_classic_bundle_product_list_variant_tag: true,
}
});
}
const result = this.productStyleInfo.reduce((map, item) => {
if (!map[item.product_id]) {
map[item.product_id] = [];
}
map[item.product_id].push(item);
return map;
}, {});
// 渲染变体tags
if(this.discount_type == 'DT_MIX_MATCH_BUNDLE' || this.discount_type == 'DT_CLASSIC_BUNDLE') {
Object.values(result).forEach((item) => {
this.handleSpzVariantRender_(item, item[0].product_id);
this.handleProductOption_(item[0].product_id, true);
});
}
// 渲染经典额外变体
if(this.discount_type == 'DT_CLASSIC_BUNDLE' && this.discount_info.enable_min_purchase_qty == true && this.discount_info.min_purchase_qty_type == 'spu') {
Object.values(result).forEach((item) => {
if(item[0].is_multi_style && item[0].discount_min_purchase_qty > 1) {
const classicSpuTag = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionClassicSpuTags-${item[0].product_id}`);
classicSpuTag && SPZ.whenApiDefined(classicSpuTag).then((api) => {
api.render(item, true);
});
}
});
}
// 渲染经典捆绑商品最低购买数量
if(this.discount_type == 'DT_CLASSIC_BUNDLE') {
Object.values(result).forEach((item) => {
this.handleMinPurchaseQtyUpdate_({discount_min_purchase_qty: item[0].discount_min_purchase_qty}, item[0].product_id);
});
}
})
}
createAndInsertSeparator_(className, condition, htmlStr, $content, item) {
if (condition) {
const separator = document.createElement('div');
separator.className = className;
separator.innerHTML = htmlStr;
$content.insertBefore(separator, item.nextSibling);
}
}
bindEvent() {
// 监听滚动,请求数据
window.addEventListener("scroll", this.utilsApi_.debounce(
() => {
// 判断是否到底
const model = this.modelMap[this.currentTab];
if (!model.loading && model.has_more && this.utilsApi_.isToPageEnd(this.section_id)) {
this.getData();
}
},
10,
50
))
}
// 商品排序
handleSort_(data) {
let sortKey = data.value;
this.modelMap[this.currentTab].sort = this.sortDict[sortKey || 'recommend_asc'];
this.modelMap[this.currentTab].page = 1;
this.modelMap[this.currentTab].has_more = true;
this.productStyleInfo = this.handleMixMatchBundleFilterSelected_(this.productStyleInfo);
// 清空商品列表dom, 重新请求排序数据渲染
let $productList = document.querySelector(`#${this.tabContentIdMap[this.currentTab]} .discount-default__productlist-wrap`) || document.querySelector(`.discount-default__productlist-wrap`);;
$productList && ($productList.innerHTML = '');
this.getData();
}
// tab 切换
tabChange_(value) {
this.currentTab = value || this.E_TAB_MAP.scenario_buy.value;
}
// 渲染界面
doRender_(data) {
return this.templates_
.findAndRenderTemplate(this.element, data)
.then((el) => {
const children = this.element.querySelector('*:not(template)');
children && SPZCore.Dom.removeElement(children);
this.element.appendChild(el);
});
}
// 捆绑商品加购/立即购买
handleBundleAddToCart_(data) {
const { action } = data;
if(this.discount_type == 'DT_CLASSIC_BUNDLE') {
this.lineItems = this.productStyleInfo;
} else {
this.lineItems = this.handleMixMatchBundleFilterSelected_(this.productStyleInfo);
}
if(action == 'cart') {
//add to cart
this.xhr_
.fetchJson(this.batchAtcApi, {
method: 'POST',
body: {
line_items: this.lineItems.map((item) => {
return {
product_id: item.product_id,
variant_id: item.variant_id,
quantity: item.quantity
}
})
}
})
.then(data => {
setTimeout(() => {
window.location.href = '/cart';
});
})
.catch(async (error) => {
await error.then((data) => {
this.handleRequestError_(data);
});
});
} else {
//checkout
this.xhr_
.fetchJson(this.buyNowApi, {
method: 'POST',
body: {
line_items: (this.lineItems || []).map((product) => {
return {
quantity: product.quantity,
variant_id: product.variant_id,
note: product.note || '',
properties: product.properties || {}
}
}),
refer_info: { source: 'buy_now' }
}
})
.then(async (data) => {
if (data.state === 'success') {
window.location.href = data.data?.checkout_url;
} else {
this.handleRequestError_(data);
}
})
.catch(async (error) => {
await error.then((data) => {
this.handleRequestError_(data);
});
});
}
}
handleRequestError_(data) {
const message = data?.message || data?.errors?.[0] || 'Unknown error';
const toast = SPZCore.Dom.scopedQuerySelector(document.body, '#discount_toast');
toast && SPZ.whenApiDefined(toast).then((api) => {
api.showToast(message);
});
};
// 渲染加购弹窗内容
async renderQuickShop(data) {
this.handleLoading_({type: 'whole', action: 'show'});
const apply_scenario = this.modelMap[this.currentTab].scenario;
this.xhr_.fetchJson(`/api/storefront/promotion/landing_page/product?product_id=${data.product_id}&discount_id=${this.discount_id}&apply_scenario=${apply_scenario}`, {
method: "get",
}).then(async(res)=>{
this.handleLoading_({type: 'whole', action: 'close'});
const $quickShop = await SPZ.whenApiDefined(document.querySelector('#promotion-quick-view-render'));
// 定义默认渲染的子款式
const selectedVariant = res.product.variants.find((v)=> (v.available && v.is_hit_discount == true)) || res.product.variants[0];
let selectedValues = {};
selectedVariant.options.length && selectedVariant.options.forEach(item => {
selectedValues[item.name] = item.value;
})
// 默认选中的 子款式、 options
res.product.defaultSelectValues = selectedValues;
let data = {...res.product, product:res.product, selectedVariant};
$quickShop.render(data);
// 打开加购弹窗
SPZ.whenApiDefined(document.querySelector(`#promotion-quick-view`)).then((api)=>{
api.open();
});
}).catch((err)=>{
this.handleLoading_({type: 'whole', action: 'close'});
})
}
// 单变体点击添加按钮
renderSingleVariant(data) {
const { product_id } = data;
const currentProduct = this.products.find((product) => product.id == product_id);
// 若当前商品已存在,则不再添加 而是更新数量
const index = this.productStyleInfo.findIndex((item) => item.product_id == product_id);
if (index != -1) {
this.productStyleInfo[index].quantity = Number(this.productStyleInfo[index].quantity) + 1;
this.updateProductPrice_(this.productStyleInfo);
} else {
this.productStyleInfo.push(this.getFilteredVariants_(currentProduct, 'single'));
}
const renderProductArr = this.productStyleInfo.filter((item) => item.product_id == product_id);
this.handleSpzVariantRender_(renderProductArr, product_id);
this.handleProductOption_(product_id, true);
}
// 过滤选中商品的子款式 获取有用的信息 product_id,variant_id,price,compare_at_price,quantity,title,variant_title
getFilteredVariants_(data, type = '') {
const { id, title, variants, inventory_tracking, inventory_policy, inventory_quantity, product_type, discount_min_purchase_qty } = data;
const { product_id, variant_id, variant, quantity, product } = data;
const isSingle = type == 'single';
const variantData = isSingle ? (variants[0] || data) : variant;
const productData = isSingle ? data : product;
let item_quantity = 0;
if (this.discount_type === 'DT_MIX_MATCH_BUNDLE') {
item_quantity = isSingle ? 1 : Number(quantity);
} else if (type === 'classic_spu') {
item_quantity = 1;
} else {
item_quantity = discount_min_purchase_qty || productData.discount_min_purchase_qty || variantData.discount_info.discount_min_purchase_qty || 1;
}
return {
product_id: isSingle ? id : product_id,
variant_id: variantData?.id || '',
price: variantData?.price || '0.00',
compare_at_price: variantData?.compare_at_price || '0.00',
quantity: item_quantity,
inventory_tracking: productData.inventory_tracking,
inventory_policy: productData.inventory_policy,
inventory_quantity: productData.inventory_quantity,
product_type: productData.product_type || this.products.find((item) => item.id == product_id)?.product_type || this.products.find((item) => item.id == id)?.product_type || '',
title: productData.title,
variant_title: variantData?.options.map((option) => option.value).join('/') || '',
is_multi_style: productData.variants.length > 1,
discount_min_purchase_qty: discount_min_purchase_qty || productData.discount_min_purchase_qty || variantData.discount_info.discount_min_purchase_qty || 0,
}
}
// 更新价格方法
updateProductPrice_(data) {
const bottomBtnContainer = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionBottomContainer`);
if (data.length == 0) {
bottomBtnContainer && SPZ.whenApiDefined(bottomBtnContainer).then((api) => {
api.render({original_price: 0, received_discounts: 0}, true);
});
return;
}
data = this.handleMixMatchBundleFilterSelected_(data);
const reqBody = {
discount_id: this.discount_id,
customer: {
customer_id: '',
email: '',
},
sales_channel: {
sale_channel_type: "online",
sale_channel_id: '829796'
},
line_items: data
}
// 如果已经有一个请求在等待,那么取消这个请求
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.handleLoading_({type: 'whole', action: 'show'});
this.debounceTimer = setTimeout(() => {
this.xhr_.fetchJson(`/api/storefront/promotion/calculate/discounted_price`, {
method: "post",
body: reqBody
}).then((res)=>{
// 更新商品列表价格
Object.keys(res.line_items).forEach((key) => {
const currentProductPrice = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionProductPrice-${key}`);
currentProductPrice && SPZ.whenApiDefined(currentProductPrice).then((api) => {
api.render(res.line_items[key], true);
});
});
// 更新底部按钮总价/总折扣价
const picked_qty = data.reduce((acc, item) => {
return acc + item.quantity;
}, 0);
bottomBtnContainer && SPZ.whenApiDefined(bottomBtnContainer).then((api) => {
api.render({...res.total_price, picked_qty}, true);
});
}).catch(async (err)=>{
await err.then((data) => {
this.handleRequestError_(data);
});
}).finally(()=>{
this.handleLoading_({type: 'whole', action: 'close'});
})
}, 100);
}
// 还原商品价格
resetProductPrice_(data) {
const {price, compare_at_price, id} = data;
const currentProductPrice = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionProductPrice-${id}`);
currentProductPrice && SPZ.whenApiDefined(currentProductPrice).then((api) => {
api.render({total_received_discounts: price, total_price: compare_at_price}, true);
});
}
// 处理与selector组件的交互
handleProductOption_(productId, show) {
const currentProductOption = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionSelectOption-${productId}`);
if(!currentProductOption) return;
currentProductOption.toggleAttribute('show', show);
const productSelector = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionProductSelector`);
productSelector && SPZ.whenApiDefined(productSelector).then((api) => {
api.toggle_({option: productId, value: show});
});
}
// 调用spz-tag组件的doRender方法
handleSpzVariantRender_(data, id) {
const spzVariantTag = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionSpzVariantTags-${id}`);
spzVariantTag && SPZ.whenApiDefined(spzVariantTag).then((api) => {
api.render(data, true);
});
}
// 执行经典捆绑最低购买数量更新
handleMinPurchaseQtyUpdate_(data, id) {
const minPruchaseQty = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionMinPurchaseQty-${id}`);
minPruchaseQty && SPZ.whenApiDefined(minPruchaseQty).then((api) => {
api.render(data, true);
});
}
// 添加商品子款式
renderVariantTag() {
let variantInfo;
const quickShopBody = SPZCore.Dom.scopedQuerySelector(document.body, '#promotion-quick-shop-body');
quickShopBody && SPZ.whenApiDefined(quickShopBody).then((api) => {
variantInfo = api.getVariantsData();
const productId = variantInfo.product_id;
const variantId = variantInfo.variant_id;
const minPruchaseQtyRender = variantInfo.product.discount_min_purchase_qty || variantInfo.variant.discount_info.discount_min_purchase_qty;
if(this.discount_type == 'DT_MIX_MATCH_BUNDLE') {
const index = this.productStyleInfo.findIndex((item) => item.variant_id == variantInfo.variant_id);
if (index != -1) {
this.productStyleInfo[index].quantity = Number(this.productStyleInfo[index].quantity) + Number(variantInfo.quantity);
this.updateProductPrice_(this.productStyleInfo);
} else {
this.productStyleInfo.push(this.getFilteredVariants_(variantInfo));
// 若当前商品已选中,更新商品价格
const currentProductOption = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionSelectOption-${productId}`);
const isSelected = currentProductOption && currentProductOption.hasAttribute('selected');
isSelected && this.updateProductPrice_(this.productStyleInfo);
}
const selectedVariantsFilter = this.productStyleInfo.filter((item) => item.product_id == productId);
this.handleSpzVariantRender_(selectedVariantsFilter, productId);
this.handleProductOption_(productId, true);
} else {
if(this.discount_info.enable_min_purchase_qty == true && this.discount_info.min_purchase_qty_type == 'spu' && minPruchaseQtyRender > 1) {
const index = this.modalVariantInfo.findIndex((item) => item.variant_id == variantId);
if (index != -1) {
this.modalVariantInfo[index].quantity = Number(this.modalVariantInfo[index].quantity) + 1;
} else {
this.modalVariantInfo.push(this.getFilteredVariants_(variantInfo, 'classic_spu'));
}
const modalVariantTag = SPZCore.Dom.scopedQuerySelector(document.body, '#promotionModalVariantTagRender');
modalVariantTag && SPZ.whenApiDefined(modalVariantTag).then((api) => {
api.render(this.modalVariantInfo, true);
});
this.handleModalInventoryCheck_(variantInfo);
const selectedVariantsNum = this.modalVariantInfo.reduce((acc, item) => {
return acc + item.quantity;
}, 0);
if(selectedVariantsNum == minPruchaseQtyRender) {
this.handleSpzVariantRender_([this.getFilteredVariants_(variantInfo)], productId);
this.productStyleInfo = this.productStyleInfo.filter((item) => item.product_id != productId).concat(this.modalVariantInfo);
const renderData = this.productStyleInfo.filter((item) => item.product_id == productId).map((item) => {
return {
...item,
is_classic_bundle_product_list_variant_tag: true
}
});
const classicSpuTag = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionClassicSpuTags-${productId}`);
classicSpuTag && SPZ.whenApiDefined(classicSpuTag).then((api) => {
api.render(renderData, true);
});
this.updateProductPrice_(this.productStyleInfo);
const quickView = SPZCore.Dom.scopedQuerySelector(document.body, '#promotion-quick-view');
quickView && SPZ.whenApiDefined(quickView).then((api)=>{
api.close();
});
this.modalVariantInfo = [];
} else {
return;
}
}
// this.productStyleInfo 中已存在与productId, variantId都相同的商品 则直接return 关闭弹窗
const isExist = this.productStyleInfo.some((item) => item.product_id == productId && item.variant_id == variantId);
if (isExist) {
const quickView = SPZCore.Dom.scopedQuerySelector(document.body, '#promotion-quick-view');
quickView && SPZ.whenApiDefined(quickView).then((api)=>{
api.close();
});
return;
}
// 更新this.productStyleInfo中的商品款式信息
const index = this.productStyleInfo.findIndex((item) => item.product_id == productId);
if (index != -1) {
this.productStyleInfo[index] = this.getFilteredVariants_(variantInfo);
}
const selectedVariantsFilter = this.productStyleInfo.filter((item) => item.product_id == productId);
this.handleSpzVariantRender_(selectedVariantsFilter, productId);
this.handleMinPurchaseQtyUpdate_({discount_min_purchase_qty: minPruchaseQtyRender}, productId);
this.updateProductPrice_(this.productStyleInfo);
}
const quickView = SPZCore.Dom.scopedQuerySelector(document.body, '#promotion-quick-view');
quickView && SPZ.whenApiDefined(quickView).then((api)=>{
api.close();
});
});
}
// 混搭弹窗内的前端库存校验
handleModalInventoryCheck_(data) {
if(this.discount_type == 'DT_CLASSIC_BUNDLE') {
const currentVariantAddNum = this.modalVariantInfo.find((item) => item.variant_id == data.variant_id)?.quantity || 0;
const quickShopBody = SPZCore.Dom.scopedQuerySelector(document.body, '#promotion-quick-shop-body');
if(!!data.variant && currentVariantAddNum == Number(data.variant.available_quantity)) {
quickShopBody && quickShopBody.setAttribute('status', 'soldout');
} else {
quickShopBody && quickShopBody.setAttribute('status', 'available');
}
}
}
// 删除商品子款式
deleteVariantTag(data) {
const { product_id, variant_id } = data;
if(this.discount_info.enable_min_purchase_qty == true && this.discount_info.min_purchase_qty_type == 'spu') {
const modalProductVariants = this.modalVariantInfo.filter((item) => item.product_id == product_id && item.variant_id != variant_id);
const modalVariantTag = SPZCore.Dom.scopedQuerySelector(document.body, '#promotionModalVariantTagRender');
modalVariantTag && SPZ.whenApiDefined(modalVariantTag).then((api) => {
api.render(modalProductVariants, true);
});
this.handleModalInventoryCheck_(data);
this.modalVariantInfo = modalProductVariants;
return;
}
const currentProductVariants = this.productStyleInfo.filter((item) => item.product_id == product_id && item.variant_id != variant_id);
this.handleSpzVariantRender_(currentProductVariants, product_id);
// 更新selectedVariants
this.productStyleInfo = this.productStyleInfo.filter((item) => item.variant_id != variant_id);
if(currentProductVariants.length > 0) {
// currentProductVariants 中只要有一项是多款式商品,就更新价格
const isMultiStyle = currentProductVariants.some((item) => item.is_multi_style);
isMultiStyle && this.updateProductPrice_(this.productStyleInfo);
this.handleProductOption_(product_id, true);
} else {
this.handleProductOption_(product_id, false);
this.resetProductPrice_(this.products.find((item) => item.id == product_id));
}
}
// 加购弹窗未参与活动 加购按钮不可点击
handleNotHitDiscount_(data) {
const $quickShopBody = document.querySelector('#promotion-quick-shop-body');
const $limitTip = document.querySelector('.promotion_flashsale_tip');
//当前子框式未命中活动
if(data.variant.is_hit_discount == false) {
$quickShopBody.setAttribute('variantstatus', 'notHitDiscount');
$limitTip && $limitTip.setAttribute('selectedvariantstatus', 'notHitDiscount');
} else {
$quickShopBody.setAttribute('variantstatus', '')
$limitTip && $limitTip.setAttribute('selectedvariantstatus', '')
}
}
//切换快速加购弹窗内限时促销限购提示
handleFlashsaleTip_(data) {
const flashsaleEl = document.querySelector('#quick-shop-flashsale-tip');
SPZ.whenApiDefined(flashsaleEl).then((api) => {
api.render(data);
});
}
// loading
handleLoading_(event) {
const { type, action } = event;
const loadingElementId = type == 'product' ? '#promotionProductsLoading' : '#promotionWholeLoading';
const loadingElement = document.querySelector(loadingElementId);
if (loadingElement) {
SPZ.whenApiDefined(loadingElement).then((api) => {
if (action == 'show') {
api.show_();
} else {
api.close_();
}
});
}
}
handleSelectProduct(productArr) {
// 从this.productStyleInfo 过滤出选中的商品
const selectedProducts = this.productStyleInfo.filter((item) => productArr.includes(item.product_id));
this.updateProductPrice_(selectedProducts);
}
handleMixMatchBundleFilterSelected_(data) {
const selectedOptions = SPZCore.Dom.scopedQuerySelectorAll(document.body, '[id^="promotionSelectOption-"]');
const idArr = [...selectedOptions].reduce((acc, item) => {
if (item.hasAttribute('selected')) {
const optionValue = item.getAttribute('option');
if (optionValue) {
acc.push(optionValue);
}
}
return acc;
}, []);
if(this.discount_type == 'DT_MIX_MATCH_BUNDLE') {
return data.filter((item) => idArr.includes(item.product_id));
}
return data;
}
handleCopyDiscountCode_(data) {
sessionStorage.setItem('other-copied-coupon', data);
const message = 'Discount code copied !';
const toast = SPZCore.Dom.scopedQuerySelector(document.body, '#discount_toast');
toast && SPZ.whenApiDefined(toast).then((api) => {
api.showToast(message);
});
}
setupAction_() {
this.registerAction('handleTabChange', (invocation) => {
const { panelId } = invocation.args.data;
this.tabChange_(panelId);
});
// 监听排序组件 选中选项
this.registerAction('handleSort', (invocation) => {
const data = invocation.args.data;
this.handleSort_(data);
});
// 渲染加购弹窗
this.registerAction('renderQuickShop', (invocation) => {
const data = invocation.args;
this.renderQuickShop(data);
});
this.registerAction('renderSingleVariant', (invocation) => {
const data = invocation.args;
this.renderSingleVariant(data);
});
this.registerAction('getCartCount', (invocation) => {
//一些老主题还未用spz重构,需要手动触发一次老方法以更新购物车图标的数量
window.$ && $(document).trigger('dj.common.cart.change');
});
// 捆绑商品加购/立即购买
this.registerAction('handleBundleAddToCart', (invocation) => {
const data = invocation.args;
this.handleBundleAddToCart_(data);
});
// 子款式 未参与活动
this.registerAction('handleNotHitDiscount', (invocation) => {
const data = invocation.args.data;
this.handleNotHitDiscount_(data);
});
// 限时促销限购提示
this.registerAction('handleFlashsaleTip', (invocation) => {
const data = invocation.args.data;
this.handleFlashsaleTip_(data);
});
// 加购提示
this.registerAction('handleAddToCartToast', (invocation) => {
const themeAddToCartToastEl = document.querySelector('#add-cart-event-proxy');
if(themeAddToCartToastEl) return;
const langValue = 'Added successfully';
this.triggerEvent_("addToCartToast", langValue);
});
this.registerAction('handleGoShopping', () => {
window.location.href = this.productUrl;
});
this.registerAction('getProductUrl', (invocation) => {
const data = invocation.args;
this.productUrl = data.product_url;
});
this.registerAction('getVariantInfo', (invocation) => {
this.renderVariantTag();
});
this.registerAction('deleteVariantTag', (invocation) => {
const data = invocation.args;
this.deleteVariantTag(data);
});
this.registerAction('getSelectedProduct', (invocation) => {
const data = invocation.args.data;
this.handleSelectProduct(data);
});
this.registerAction('pageReload', () => {
//活动进行中 长期活动 = 虚拟倒计时
const VirtualCountdown = this.discount_info.progress === "PROGRESS_ONGOING" && this.discount_info.ends_at == -1;
if(VirtualCountdown) return;
window.location.reload();
});
this.registerAction('resetModalVariantInfo', () => {
this.modalVariantInfo = [];
});
this.registerAction('handleModalInventoryCheck', (invocation) => {
const data = invocation.args.data;
this.handleModalInventoryCheck_(data);
});
this.registerAction('copyDiscountCode', (invocation) => {
const data = invocation.args.data;
this.handleCopyDiscountCode_(data);
});
}
triggerEvent_(name, data) {
const event = SPZUtils.Event.create(this.win, `${ TAG }.${ name }`, data || {});
this.action_.trigger(this.element, name, event);
}
isLayoutSupported(layout) {
return layout == SPZCore.Layout.CONTAINER;
}
}
SPZ.defineElement(TAG, SpzCustomDiscountDefault)
The activity inventory has been sold out, flash sale discount will not be applied.
Continue Shopping
${function() {
const productData = data;
let product_change_event = '';
let mouse_over_event = '';
let mouse_out_event = '';
const product_options = productData.options.filter(Boolean) || [];
for (let opt of product_options) {
const nameEscape = opt.name.replace(/\/|\\|\s|\'|\"|`|\<|\>/g, '');
product_change_event = product_change_event + `quick-shop-selected-variant-${opt.id}.rerender(data=event.selectedValues.${opt.name});`;
mouse_out_event = mouse_out_event + `quick-shop-selected-variant-${opt.id}.rerender(data=event.selectData.${opt.name});`;
mouse_over_event = mouse_over_event + `@${nameEscape}Mouseover="quick-shop-selected-variant-${opt.id}.rerender(data=event);"`;
}
const selectedVariant = productData.variants.find(v => (v.available && v.is_hit_discount == true)) || productData.variants[0];
return `
`;
}()}
${function() {
const selectedVariant = data.variant || data.variants[0];
const image = selectedVariant.image || data.product.image;
const imageWidth = image?.width || 120;
const imageHeight = image?.height || 120;
return `
`
}()}
${function() {
const defaultVariant = data.variants?.find(v => (v.available && v.is_hit_discount == true)) || data.variants?.[0];
const selectedVariant = data.variant || defaultVariant;
const isHasRrice = (selectedVariant.price || selectedVariant.price == 0) ? true : false;
if(selectedVariant.flash_sale_info && selectedVariant.flash_sale_info.discount_price) {
selectedVariant.price = selectedVariant.flash_sale_info.discount_price;
}
if(data.flash_sale_info && data.flash_sale_info.discount_price) {
data.price = data.flash_sale_info.discount_price;
}
return !!selectedVariant ? `
` : `
-
`;
}()}
${function(){
const defaultVariant = data.variants?.find(v => (v.available && v.is_hit_discount == true)) || data.variants?.[0];
const selectedVariant = data.variant || defaultVariant;
let show_flashsale_limit_tip = false;
let limit_user_product_discount;
if(selectedVariant.flash_sale_info && selectedVariant.flash_sale_info?.limit_user_product_type != "LUPT_NO_LIMIT" && selectedVariant.flash_sale_info?.limit_user_product_discount > 0) {
show_flashsale_limit_tip = true;
limit_user_product_discount = selectedVariant.flash_sale_info?.limit_user_product_discount;
}
if(data.flash_sale_info && data.flash_sale_info?.limit_user_product_type != "LUPT_NO_LIMIT" && data.flash_sale_info?.limit_user_product_discount > 0) {
show_flashsale_limit_tip = true;
limit_user_product_discount = data.flash_sale_info?.limit_user_product_discount;
}
return `
Promo products limited to ${limit_user_product_discount} item per person.
`
}()}
${function() {
const value = (data.originData && data.originData.value) || data.value;
const isHasValue = value ? true : false;
return `
${value}
`
}()}
(function() {
function setupMarquee() {
var marquee = document.querySelector('#promotionBanner .marquee');
if (!marquee) return;
var text = marquee.querySelector('.discount_banner_text');
if (!text) return;
// 仅在文本溢出容器时才启用滚动
if (text.scrollWidth <= marquee.clientWidth + 1) return;
marquee.classList.add('is-scrolling');
// 复制一份文本,与原始文本拼成 .marquee-track 实现无缝循环
var track = document.createElement('div');
track.className = 'marquee-track';
var clone = text.cloneNode(true);
clone.setAttribute('aria-hidden', 'true');
marquee.removeChild(text);
track.appendChild(text);
track.appendChild(clone);
marquee.appendChild(track);
// 速度 ~ 50px/s,对照单份文本宽度计算时长
var duration = text.offsetWidth / 50;
track.style.animation = 'marquee ' + duration + 's linear infinite';
}
if (document.readyState === 'complete') {
setupMarquee();
} else {
window.addEventListener('load', setupMarquee);
}
})();
(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);
}())