View all
  • ✨ 10% OFF Sitewide ✨ Plus $5 Off $199+ · $10 Off $299+

    ✨ 10% OFF Sitewide ✨ Plus $5 Off $199+ · $10 Off $299+

    Shop now

✨ 10% OFF Sitewide ✨ Plus $5 Off $199+ · $10 Off $299+

✨ 10% OFF Sitewide ✨ Plus $5 Off $199+ · $10 Off $299+

Search

Matching Family Christmas Pajamas | LOVE FAMILY

Search Search
Account
Log in Create an account
Cart
  • NEW ARRIVALS
  • BEST SELLERS
  • CHRISTMAS PAJAMAS
    CHRISTMAS PAJAMAS
    One family, so many ways to match →
    Organic cotton. Timeless tartan →
    • SHOP PAJAMAS
      SHOP PAJAMAS
      • Shop All Christmas Pajamas
      • New Arrivals
      • Best Sellers
      • Exclusive Designs
      • Halloween Family Pajamas
    • POPULAR THEMES
      POPULAR THEMES
      • 2026 Pajamas
      • Cousin Crew Pajamas
      • Peanuts Pajamas
      • Reindeer Pajamas
      • Gnome Pajamas
      • Christmas Vacation Pajamas
  • SHOP BY STYLE
    SHOP BY STYLE
    • PAJAMA STYLES
      PAJAMA STYLES
      • Classic Button-Down
      • 100% Cotton Pajamas
      • Onesie Pajamas
      • Short-Sleeve Pajamas
      • Personalized Pajamas
    • CHRISTMAS APPAREL
      CHRISTMAS APPAREL
      • Family Matching Sweatshirts
      • Christmas Pants
      • Ugly Christmas Sweaters
      • Christmas Socks
    Finally, matching pajamas with nightgowns →
    Cozy from head to toe →
  • SHOP BY THEME
    SHOP BY THEME
    • Christmas Classics
      Christmas Classics
      • 2026 Collection
      • Santa Pajamas
      • Snowman Pajamas
      • Gnome Pajamas
      • Nativity Pajamas
      • Gingerbread Pajamas
      • Reindeer Pajamas
      • Elf Pajamas
      • Nutcracker Pajamas
      • Believe Pajamas
    • Characters & Movies
      Characters & Movies
      • Christmas Vacation Pajamas
      • A Christmas Story Pajamas
      • Home Alone Pajamas
      • Peanuts Pajamas
      • Disney Pajamas
      • Polar Express Pajamas
      • Bluey Pajamas
      • Star Wars Pajamas
      • Stitch Pajamas
    • Family & Crew
    • Funny Christmas
    • Animals & Cute
      Animals & Cute
      • Bigfoot Pajamas
      • Dinosaur Pajamas
      • Animal Pajamas
    • Sports & Hobbies
    Christmas is better with the cousin crew →
    The characters your whole family loves →
  • GIFTS & DÉCOR
    GIFTS & DÉCOR
    Little details make Christmas feel magical →
    Bring a warm holiday glow home →
    • ORNAMENTS & GIFTS
      ORNAMENTS & GIFTS
      • Christmas Ornaments
      • Memorial Ornaments
      • Christmas Collectible
      • Christmas Cards
    • HOME & HOLIDAY DÉCOR
      HOME & HOLIDAY DÉCOR
      • Christmas Stockings
      • Christmas Tablecloth
      • Tree Skirts
      • Christmas Wreath
      • Christmas Candles
  • OUR STORY
  • Log in Create an account
  • NEW ARRIVALS
  • BEST SELLERS
  • CHRISTMAS PAJAMAS
    One family, so many ways to match →
    Organic cotton. Timeless tartan →
    SHOP PAJAMAS
    • Shop All Christmas Pajamas
    • New Arrivals
    • Best Sellers
    • Exclusive Designs
    • Halloween Family Pajamas
    POPULAR THEMES
    • 2026 Pajamas
    • Cousin Crew Pajamas
    • Peanuts Pajamas
    • Reindeer Pajamas
    • Gnome Pajamas
    • Christmas Vacation Pajamas
  • SHOP BY STYLE
    PAJAMA STYLES
    • Classic Button-Down
    • 100% Cotton Pajamas
    • Onesie Pajamas
    • Short-Sleeve Pajamas
    • Personalized Pajamas
    CHRISTMAS APPAREL
    • Family Matching Sweatshirts
    • Christmas Pants
    • Ugly Christmas Sweaters
    • Christmas Socks
    Finally, matching pajamas with nightgowns →
    Cozy from head to toe →
  • SHOP BY THEME
    Christmas Classics
    • 2026 Collection
    • Santa Pajamas
    • Snowman Pajamas
    • Gnome Pajamas
    • Nativity Pajamas
    • Gingerbread Pajamas
    • Reindeer Pajamas
    • Elf Pajamas
    • Nutcracker Pajamas
    • Believe Pajamas
    Characters & Movies
    • Christmas Vacation Pajamas
    • A Christmas Story Pajamas
    • Home Alone Pajamas
    • Peanuts Pajamas
    • Disney Pajamas
    • Polar Express Pajamas
    • Bluey Pajamas
    • Star Wars Pajamas
    • Stitch Pajamas
    Family & Crew
    Funny Christmas
    Animals & Cute
    • Bigfoot Pajamas
    • Dinosaur Pajamas
    • Animal Pajamas
    Sports & Hobbies
    Christmas is better with the cousin crew →
    The characters your whole family loves →
  • GIFTS & DÉCOR
    Little details make Christmas feel magical →
    Bring a warm holiday glow home →
    ORNAMENTS & GIFTS
    • Christmas Ornaments
    • Memorial Ornaments
    • Christmas Collectible
    • Christmas Cards
    HOME & HOLIDAY DÉCOR
    • Christmas Stockings
    • Christmas Tablecloth
    • Tree Skirts
    • Christmas Wreath
    • Christmas Candles
  • OUR STORY
  • More links
    Top searched
    Traditional Reindeer Print Gray Matching Family Christmas Pajamas
    from 
    $15.99
    $29.99
    Stewart Tartan 100% Cotton Flannel Matching Family Pajamas
    from 
    $24.99
    $49.99
    Reindeer Buffalo Check Matching Family Christmas Pajamas
    from 
    $16.99
    $22.99
    Fair Isle Christmas Tree & Reindeer Print Matching Family Pajamas
    from 
    $12.99
    $36.99
    Santa & Snowflake Navy Family Matching Christmas Pajamas
    from 
    $20.99
    $29.99
    Cousin Crew Family Christmas Pajamas – Green Plaid Reindeer Print
    from 
    $19.99
    $32.99
    Sign in or create account Login Sign In Login Verify your email Create your account Almost done
    Welcome to Matching Family Christmas Pajamas | LOVE FAMILY
    Welcome to Matching Family Christmas Pajamas | LOVE FAMILY

    We sent a 6-digit code to

    · Email verified
    Email is required
    Please enter a valid email.
    Send code Resend s
    We'll email a code to verify your email
    Please enter a valid code
    Please enter a valid code
    First name is required.
    Last name is required.
    Password is required.
    Password must be between 6-16 characters long.
    Forgot password?
    Wrong email? Go back
    Wrong email? Go back
    Use verification code Use password instead
    OR
    Don't have an account? Create one
    OR
    Login for more discounts
    © 2026 Matching Family Christmas Pajamas | LOVE FAMILY All rights reserved.

    Goes great with

    Taxes and shipping calculated at checkout
    Your cart is currently empty.

    Cart

    Goes great with

    cart
    cart
    Your cart is currently empty.
    Continue shopping
    class SpzCustomDiscountBundle extends SPZ.BaseElement { constructor(element) { super(element); } isLayoutSupported(layout) { return layout == SPZCore.Layout.LOGIC; } mountCallback() { // 同一页可能有多个 block 各自渲染本 snippet,导致逻辑元素 spz-custom-discount-toast(#appsAddCartToastFunc) 重复。 // 仅保留 DOM 中第一个实例,其余重复实例移除自身,保证逻辑元素及其 action 引用(appsAddCartToastFunc)唯一。 const funcs = document.querySelectorAll('#appsAddCartToastFunc'); if (funcs.length > 1 && this.element !== funcs[0]) { this.element.remove(); return; } // 全局折扣 toast 的弹出点分散在多个模板/组件里(含声明式 @atcError),无法逐一在弹出前前置, // 故挂载时幂等地去重并挂到 body;加购 toast 改为弹出前按需去重(见 showAddToCartToast) this.ensureSingleToastInBody_('cart_match_discount_toast_wrap'); } unmountCallback() {} // 同一页面可能有多个 block 各自渲染本 toast snippet(如商详的 discount_automatic + 购物车抽屉里的折扣横幅), // 导致 toast 外层出现重复 id。重复 id 下 getElementById/querySelector 及 SPZ 动作引用都只命中第一个, // 且被关进抽屉的那份会被抽屉的 transform/overflow 裁剪/错位。这里在挂载时幂等去重: // 只保留一份、移除多余的,并把保留的挂到 body(body 是 position:fixed 最安全的落点,避免被任何祖先裁剪)。可反复调用。 ensureSingleToastInBody_(id) { const els = Array.from(document.querySelectorAll('#' + id)); if (!els.length) return; els.slice(1).forEach((el) => el.remove()); const keep = els[0]; if (keep.parentNode !== document.body) { document.body.appendChild(keep); } } // 主题购物车抽屉是否打开 isInCartDrawer_() { return !!document.querySelector('[data-section-type="cart_drawer"] spz-sidebar[open]'); } setupAction_() { this.registerAction('showAddToCartToast', () => { // 抽屉内加购:主题本就不会有加购效果,统一弹插件自己的 toast // 非抽屉:主题有加购代理则沿用主题加购效果,否则插件兜底 const proxyEl = document.getElementById('add-cart-event-proxy'); const inDrawer = this.isInCartDrawer_(); if (!inDrawer && proxyEl) { return; } // 弹出前按需去重并挂到 body(幂等),避免重复 id 命中错对象或被抽屉裁剪/错位 this.ensureSingleToastInBody_('apps_add_cart_toast_wrap'); const toastEl = document.querySelector('#apps-match-drawer-add_to_cart_toast') SPZ.whenApiDefined(toastEl).then((apis) => { apis.showToast("Added successfully"); }); }); } buildCallback() { this.setupAction_(); }; } SPZ.defineElement('spz-custom-discount-toast', SpzCustomDiscountBundle);
    const TAG = 'spz-custom-discount-default'; const API = { CAMPAIGN: "\/api\/storefront\/promotion\/landing_page\/campaign?discount_id=", PRODUCT_LIST: "\/api\/storefront\/promotion\/landing_page\/product\/list", DISCOUNT_I18N: "\/api\/discount-i18n" }; const SHOP_ID = "241653"; const E_TAB_MAP = { scenario_buy: { value: "1", domId: "cart_product_list_buy_products" }, scenario_obtain: { value: "2", domId: "cart_product_list_obtain_products" } }; const SORT_DICT = { 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' } }; const SORT_OPTIONS = [ { 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" } ]; // 凑单弹窗与首页限时促销卡片(flashsale_collection)共用样式 id "discount-match-drawer",会产生重复 id。 // 故 JS 不按 id 定位弹窗,改用弹窗根节点上专用的 js- 前缀钩子类唯一定位——与样式解耦, // 天然避免命中首页卡片,也不受开弹窗时 appendChild 重排 DOM 顺序的影响(此前正是它导致加购后第一次关不掉)。 const getDiscountMatchPopupEl = () => document.querySelector('.js-discount-match-popup'); class SpzCustomDiscountDefault extends SPZ.BaseElement { constructor(element) { super(element); this.templates_ = null; this.discountId = ''; // 活动 id this.discountType = ""; this.discountI18n = {}; this.bundleProducts = []; this.tabContentIdMap = {}; this.currentTab = E_TAB_MAP.scenario_buy.value; this.modelMap = { [E_TAB_MAP.scenario_buy.value]: {}, [E_TAB_MAP.scenario_obtain.value]: {} } } static deferredMount() { return false; } buildCallback() { this.action_ = SPZServices.actionServiceForDoc(this.element); this.templates_ = SPZServices.templatesForDoc(this.element); this.xhr_ = SPZServices.xhrFor(this.win); this.setupAction_(); this.initTabContentMap_(); SPZUtils.Event.listen(document.getElementById('discount-match-mask'), 'click', (e) => { e.stopPropagation(); this.handleCloseDiscountDrawer_(); }); } initTabContentMap_() { Object.entries(E_TAB_MAP).forEach(([key, valueObj]) => { this.tabContentIdMap[valueObj.value] = valueObj.domId; }); } // 初始化 initData_(data) { this.discountId = data.discount_info.id; this.discountType = data.discount_info.discount_type; const model_buy_map = { discount_id: this.discountId, //活动id scenario: 1, // 枚举值,1:购买商品,2:获得商品 sort: { by :"recommend", direction :"asc"}, page: 2, //分页码 limit: 20, // 每页数量 loading: false, // 请求数据标示 has_more: true // 是否还有数据 }; const model_get_map = { discount_id: this.discountId, scenario: 2, sort: { by :"recommend", direction :"asc"}, page: 2, limit: 20, loading: false, has_more: true }; this.modelMap = { [E_TAB_MAP.scenario_buy.value]: model_buy_map, [E_TAB_MAP.scenario_obtain.value]: model_get_map } this.renderSort_(); } getDiscountI18n_() { this.xhr_.fetchJson(API.DISCOUNT_I18N, { method: "GET", }).then((res)=>{ this.discountI18n = res; this.bindEvent(); }); } renderSort_() { const renderSort = (sortId, model) => { const $sortElement = document.getElementById(sortId); if ($sortElement) { SPZ.whenApiDefined($sortElement).then((api) => { const defaultValue = `${model.sort.by}_${model.sort.direction}` || "title_asc"; api.doRender_({ options: SORT_OPTIONS, defaultValue, id: sortId }); }); } }; renderSort('spz_custom_sort_products_x', this.modelMap[E_TAB_MAP.scenario_buy.value]); renderSort('spz_custom_sort_products_y', this.modelMap[E_TAB_MAP.scenario_obtain.value]); } // 获取数据,拼接html模板 async getData(cb) { // 请求数据 const model = this.modelMap[this.currentTab]; if (!model.has_more || model.loading) return; model.loading = true; this.handleLoading_({type: 'product', action: 'show'}); const $root = getDiscountMatchPopupEl() || document; const $content = $root.querySelector(`#${this.tabContentIdMap[this.currentTab]} .discount-default__productlist-wrap`) || $root.querySelector(`.discount-default__productlist-wrap`); //查询活动商品接口 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: SHOP_ID } } this.xhr_.fetchJson(API.PRODUCT_LIST, { method: "POST", body: reqBody }).then(async(res)=>{ this.handleLoading_({type: 'product', action: 'close'}); const count = res.count; model.has_more = res.has_more; if (count > 0) { model.page++; if (res.products && res.products.length > 0) { const products = res.products.map((product) => { return { ...product, discount_id: model.discount_id, apply_scenario: model.scenario, url: appDiscountUtils.globalizePath(product.url), image_padding_bottom: appDiscountUtils.image_padding_bottom(product.image.width, product.image.height,'no-limit'), discount_type: this.discountType } }); // 获取商品列表渲染模板, dom挂载 const renderApi = await SPZ.whenApiDefined(document.querySelector('#app-discount_match_drawer_products_render')); const el = await renderApi.getRenderTemplate({ products: products, discountI18n: this.discountI18n }); const childNodes = el.querySelectorAll('.as-render-product-item'); if (childNodes && childNodes.length > 0) { $content.append(...el.childNodes); } this.bundleProducts = [...this.bundleProducts, ...res.products]; SPZ.whenApiDefined(document.getElementById("appDiscountBundleProductsFunc")).then((api) => { api.setBundleData(this.bundleProducts); }) if(this.discountType === 'DT_MIX_MATCH_BUNDLE') { const productSelector = SPZCore.Dom.scopedQuerySelector(getDiscountMatchPopupEl() || document.body, `#promotionProductSelector`); productSelector && SPZ.whenApiDefined(productSelector).then((api) => { api.init(); }); } // 监听load去掉灰色背景 document.dispatchEvent(new CustomEvent('fire.load.img')); // 触发懒加载 cb && cb(products); window.lazyLoadInstance && window.lazyLoadInstance.update(); } else { } } model.loading = false; }).catch((err)=>{ console.error(err); model.loading = false; this.handleLoading_({type: 'product', action: 'close'}); }).finally(() => { if(this.discountType === 'DT_MIX_MATCH_BUNDLE') { SPZ.whenApiDefined(document.getElementById("appDiscountBundleProductsFunc")).then((api) => { api.handleChangeSort(); }) } }) } bindEvent() { // 监听滚动,请求数据 const $el = document.querySelector(".discount-match-drawer-content"); if ($el) { $el.addEventListener("scroll", this.win.SPZCore.Types.debounce(this.win, () => { const model = this.modelMap[this.currentTab]; if (!model.loading && model.has_more && appDiscountUtils.isToPageEnd(`[data-drawer-id='${this.discountId}']`)) { this.getData(); } }, 50)); } window.addEventListener("resize", this.setMatchDrawerHeight_); } // 商品排序 handleSort_(data) { const sortKey = data.value; this.modelMap[this.currentTab].sort = SORT_DICT[sortKey || 'title_asc']; this.modelMap[this.currentTab].page = 1; this.modelMap[this.currentTab].has_more = true; this.clearProductListDom_(); this.getData((products) => {}); } // tab 切换 tabChange_(value) { this.currentTab = value || E_TAB_MAP.scenario_buy.value; } setMatchDrawerHeight_() { //设置凑单弹窗高度,一些移动端浏览器上方帮助栏会影响视口大小,因此使用window.innerHeight //注意:resize 监听未 bind this,此函数不能依赖 this const modalContent = document.querySelector(".discount-match-drawer"); if (!modalContent) return; const wrapper = getDiscountMatchPopupEl(); const windowHeight = window.innerHeight; //运行时检测购物车抽屉是否仍打开 //不能写死 side="right":不同主题抽屉弹出方向不同(右侧 / 底部等),只按 cart_drawer //section 下是否有 open 的 sidebar 判定;宽度/定位后续统一用 getBoundingClientRect() 同步 const $drawer = document.querySelector('[data-section-type="cart_drawer"] spz-sidebar[open]'); if (!$drawer) { //购物车页场景:全屏底部弹层,默认 90vh 全宽 modalContent.style.maxHeight = windowHeight * 0.9 + "px"; modalContent.style.removeProperty("height"); modalContent.style.removeProperty("width"); wrapper && wrapper.classList.remove("in-cart-drawer"); return; } //抽屉场景:距顶 40px,并匹配抽屉宽度(靠右/提层级/2列由 in-cart-drawer 的 CSS 处理) const h = (windowHeight - 40) + "px"; modalContent.style.maxHeight = h; modalContent.style.height = h; //class 绑定抽屉存在性(保证靠右/提层级/2列生效),width 仅在可测量时再设, //避免 drawer 开启动画期间 rect.width 暂为 0 时弹窗以全宽浮在错误位置 wrapper && wrapper.classList.add("in-cart-drawer"); const rect = $drawer.getBoundingClientRect(); if (rect.width > 0) { modalContent.style.setProperty("width", rect.width + "px", "important"); } } showDiscountToast(message) { SPZ.whenApiDefined(document.getElementById("cart_match_discount_toast")).then((apis) => { apis.showToast(message); }); } handleLoading_(event) { const { type, action } = event; const loadingElementId = type === 'product' ? '#discount-match-drawer-products_loading' : '#apps-discount-whole-loading'; const loadingElement = document.querySelector(loadingElementId); if (loadingElement) { SPZ.whenApiDefined(loadingElement).then((api) => { if (action === 'show') { api.show_(); } else { api.close_(); } }); } } setupAction_() { // Buy X Get Y Tab 切换 this.registerAction('handleBuyXGetYTabChange', (invocation) => { const { panelId } = invocation.args.data; this.tabChange_(panelId); }); // 监听排序组件 选中选项 this.registerAction('handleSort', (invocation) => { const data = invocation.args.data; if (this.discountType === "DT_MIX_MATCH_BUNDLE") { const bundleFun = document.getElementById("appDiscountBundleProductsFunc"); SPZ.whenApiDefined(bundleFun).then((api) => { const oldProductInfo = api.productStyleInfo; const newData = oldProductInfo.filter(item => { const currentProductOption = SPZCore.Dom.scopedQuerySelector(document.body, `#promotionSelectOption-${item.product_id}`); return currentProductOption && currentProductOption.hasAttribute('selected') }) api.init(newData); }) .then(() => { this.handleSort_(data); }) } else { this.handleSort_(data); } }); //初始化弹窗数据 this.registerAction('showDiscountDrawer', async (data) => { const discountId = data.args.discountId || ''; const activeTab = data.args.activeTab || E_TAB_MAP.scenario_buy.value; this.tabChange_(activeTab); this.triggerEvent_('setTab', { activeTab }); this.getDiscountI18n_(); this.getCampaignDetail_(discountId).then(() => { this.handleShowDiscountDrawer_(); }); }); // 关闭弹窗 this.registerAction('removeDiscountElement', () => { this.handleCloseDiscountDrawer_(); }); // 切换多个活动 this.registerAction('discountSelectorChange', (invocation) => { const { targetOption } = invocation.args.data; this.handleDiscountSelectorChange_(targetOption); }); } handleDiscountSelectorChange_(targetOption) { this.modelMap[this.currentTab].sort = SORT_DICT['title_asc']; this.modelMap[this.currentTab].page = 1; this.modelMap[this.currentTab].has_more = true; this.clearProductListDom_(); this.handleLoading_({ type: 'product', action: 'show' }); this.getCampaignDetail_(targetOption).finally(() => { this.handleLoading_({ type: 'product', action: 'close' }); }); } // 清空商品列表dom, 重新请求排序数据渲染 clearProductListDom_() { const $root = getDiscountMatchPopupEl() || document; const $productList = $root.querySelector(`#${this.tabContentIdMap[this.currentTab]} .discount-default__productlist-wrap`) || $root.querySelector(`.discount-default__productlist-wrap`); if ($productList) $productList.innerHTML = ''; } handleShowDiscountDrawer_() { this.setMatchDrawerHeight_(); const $el = getDiscountMatchPopupEl(); if (!$el) return; document.body.appendChild($el); $el.style.display="block"; document.documentElement.classList.add("i-spzhtml-scroll-disabled"); } handleCloseDiscountDrawer_() { const $drawer = getDiscountMatchPopupEl(); if ($drawer) $drawer.style.display = "none"; document.documentElement.classList.remove("i-spzhtml-scroll-disabled"); this.clearProductListDom_(); const bannerEL = document.querySelector(".discount_match_drawer_banner_inner"); const productEL = ($drawer || document).querySelector(".discount-default"); bannerEL && SPZCore.Dom.removeChildren(bannerEL); productEL && SPZCore.Dom.removeChildren(productEL); const bundleFun = document.getElementById("appDiscountBundleProductsFunc"); SPZ.whenApiDefined(bundleFun).then((api) => { api.init(); }) const bundleButtonEL = document.querySelector(".app-bundle-discount_bottom_container"); bundleButtonEL && SPZCore.Dom.removeChildren(bundleButtonEL); } getCampaignDetail_(discount_id) { return this.xhr_.fetchJson(API.CAMPAIGN + discount_id, { method: "GET" }).then((data) => { this.bundleProducts = data.buy_product_info.product; if(data.discount_info.progress == "PROGRESS_FINISHED") { this.showDiscountToast("The discount has finished"); return Promise.reject({}); } else { SPZ.whenApiDefined(document.getElementById("discount-match-drawer-container")).then(apis => { apis.render(data, true).then(() => { this.initData_(data); const type = data.discount_info.discount_type; document.querySelector(".discount-match-drawer").dataset.discountType = type; //给捆绑组件传值 if(type === "DT_MIX_MATCH_BUNDLE" || type === "DT_CLASSIC_BUNDLE") { // 重置 const bundleButtonEL = document.querySelector(".app-bundle-discount_bottom_container"); bundleButtonEL && SPZCore.Dom.removeChildren(bundleButtonEL); //购物车默认加购 data.landing_page_info.button.redirect_page = "cart"; SPZ.whenApiDefined(document.getElementById("appDiscountBundleProductsFunc")).then((api) => { api.init(); // 切换 Tab 重置数据 api.setBundleData(data.buy_product_info.product, data.landing_page_info, data.discount_info.id, data.discount_info.discount_type, data.discount_info); }); if(type === "DT_CLASSIC_BUNDLE") { SPZ.whenApiDefined((getDiscountMatchPopupEl() || document).querySelector("#promotionBottomContainer")).then((api) => { const buttonData = { setting: data.landing_page_info, ...data.buy_product_info.total_price } api.render(buttonData, true); }) } } }); }); } }).catch(e => { e.then((data) => { if(data.code == 10003001) { this.showDiscountToast("The discount has finished"); } }); }); } 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) (function () { 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); } 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; }); } 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) })() 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) // 凑单弹窗与首页限时促销卡片共用样式 id "discount-match-drawer",会产生重复 id。 // 用弹窗根节点上专用的 js- 前缀钩子类唯一定位作用域根,避免命中首页卡片导致弹窗内元素查询失败。 const getDiscountMatchPopupEl = () => document.querySelector('.js-discount-match-popup'); class SpzCustomDiscountBundleProducts extends SPZ.BaseElement { constructor(element) { super(element); this.xhr_ = SPZServices.xhrFor(this.win); this.getDiscountPriceApi = "\/api\/storefront\/promotion\/calculate\/discounted_price"; this.buyNowApi = "\/api\/checkout\/order"; this.batchAtcApi = "\/api\/cart\/batch"; // 款式信息集合 this.productStyleInfo = []; // 弹窗内选择款式集合 this.modalVariantInfo = []; this.show_classic_bundle_spu_style = false; this.bundleProducts = []; //捆绑商品 this.bundleConfig = {}; //下方按钮配置 this.discountId = ""; this.discountType = ""; this.discountInfo = ""; this.lineItems = []; this.tempCss = {}; this.renderQuickShop_ = this.win.SPZCore.Types.debounce(this.win, this.renderQuickShopModal.bind(this), 500); } isLayoutSupported(layout) { return layout == SPZCore.Layout.LOGIC; } buildCallback() { this.setupAction_(); }; init(data = []) { this.productStyleInfo = data; } handleRequestError_(data) { this.showToast(data?.message || data?.errors?.[0] || 'Unknown error'); }; showToast(message) { SPZ.whenApiDefined(document.getElementById("cart_match_discount_toast")).then((apis) => { apis.showToast(message); }); } //外部组件调用传值 setBundleData(products, config = "", id = "", type = "", info = {}) { this.bundleProducts = products; if(config) { this.bundleConfig = config; this.discountId = id; this.discountType = type; this.discountInfo = info; this.show_classic_bundle_spu_style = type === 'DT_CLASSIC_BUNDLE' && info.enable_min_purchase_qty && info.min_purchase_qty_type == 'spu'; // 经典捆绑初始化商品数据 if(type == 'DT_CLASSIC_BUNDLE') { this.productStyleInfo = products.map((item) => { return this.getFilteredVariants_(item, 'single'); }); } } } handleChangeSort() { const result = this.productStyleInfo.reduce((map, item) => { if (!map[item.product_id]) { map[item.product_id] = []; } map[item.product_id].push(item); return map; }, {}); Object.values(result).forEach((item) => { this.handleSpzVariantRender_(item, item[0].product_id); this.handleProductOption_(item[0].product_id, true); }); } // 调用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); }); } // 更新价格 updateProductPrice_(data) { const bottomBtnContainer = SPZCore.Dom.scopedQuerySelector(getDiscountMatchPopupEl() || document.body, `#promotionBottomContainer`); if (data.length == 0) { bottomBtnContainer && SPZ.whenApiDefined(bottomBtnContainer).then((api) => { const renderInfo = { setting: this.bundleConfig, ...{ original_price: 0, received_discounts: 0, picked_qty: 0 } } api.render({original_price: 0, received_discounts: 0}, true); }); return; } const reqBody = { discount_id: this.discountId, customer: { customer_id: '', email: '', }, sales_channel: { sale_channel_type: "online", sale_channel_id: '241653' }, line_items: data } // 如果已经有一个请求在等待,那么取消这个请求 if (this.debounceTimer) { clearTimeout(this.debounceTimer); } this.debounceTimer = setTimeout(() => { this.xhr_.fetchJson(this.getDiscountPriceApi, { method: "post", body: reqBody }).then((res)=>{ // 更新商品列表价格 Object.keys(res.line_items).forEach((key) => { const currentProductPrice = SPZCore.Dom.scopedQuerySelector(document.body, `#appDiscountProductPrice-${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) => { const data = { setting: this.bundleConfig, ...{ ...res.total_price, picked_qty } } api.render(data, true); }); }).catch((err)=>{ this.handleRequestError_(err); }).finally(()=>{ }) }, 100); } // 还原商品价格 resetProductPrice_(data) { const {price, compare_at_price, id} = data; const currentProductPrice = SPZCore.Dom.scopedQuerySelector(document.body, `#appDiscountProductPrice-${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 isSelected = currentProductOption.hasAttribute('selected'); // !show 取消选中 // !isSelected 选中商品 if (!show || !isSelected) { const productSelector = SPZCore.Dom.scopedQuerySelector(getDiscountMatchPopupEl() || document.body, `#promotionProductSelector`); productSelector && SPZ.whenApiDefined(productSelector).then((api) => { api.toggle_({ option: productId, value: show }); }); } } // 混搭弹窗内的前端库存校验 handleModalInventoryCheck_(data) { if(this.discountType == 'DT_MIX_MATCH_BUNDLE' || this.discountType == 'DT_CLASSIC_BUNDLE') { const currentVariantAddNum = this.modalVariantInfo.find((item) => item.variant_id == data.variant_id)?.quantity || 0; const quickShopBody = SPZCore.Dom.scopedQuerySelector(document.body, '#apps-discount-quick-shop-body'); if(!!data.variant && currentVariantAddNum == Number(data.variant.available_quantity)) { quickShopBody && quickShopBody.setAttribute('status', 'soldout'); } else { quickShopBody && quickShopBody.setAttribute('status', 'available'); } } else { return; } } // 添加商品子款式 renderVariantTag() { let variantInfo; const quickShopBody = SPZCore.Dom.scopedQuerySelector(document.body, '#apps-discount-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.discountType === '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.discountInfo.enable_min_purchase_qty == true && this.discountInfo.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(getDiscountMatchPopupEl() || 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, '#apps-discount-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, '#apps-discount-quick-view'); quickView && SPZ.whenApiDefined(quickView).then((api)=>{ api.close(); }); return; } // 若 this.productStyleInfo 中已存在与productId相同的商品,则不再添加 否则替换 const index = this.productStyleInfo.findIndex((item) => item.product_id == productId); if (index != -1) { this.productStyleInfo[index] = this.getFilteredVariants_(variantInfo); } else { this.productStyleInfo.push(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, '#apps-discount-quick-view'); quickView && SPZ.whenApiDefined(quickView).then((api)=>{ api.close(); }); }); } // 单变体点击添加按钮 renderSingleVariant(data) { const { product_id } = data; const currentProduct = this.bundleProducts.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 } = data; const { product_id, variant_id, variant, quantity, product, discount_min_purchase_qty } = data; const isSingle = type == 'single'; const variantData = isSingle ? (variants[0] || data) : variant; const productData = isSingle ? data : product; let item_quantity = 0; if (this.discountType === '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.bundleProducts.find((item) => item.id == product_id)?.product_type || this.bundleProducts.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, } } handleLoading_ (event) { const { type, action } = event; const loadingElementId = type === 'product' ? '#discount-match-drawer-products_loading' : '#apps-discount-whole-loading'; 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); } // 渲染加购弹窗内容 async renderQuickShopModal(data){ this.handleLoading_({type: 'whole', action: 'show'}); this.xhr_.fetchJson(`/api/storefront/promotion/landing_page/product?product_id=${data.product_id}&discount_id=${this.discountId}&apply_scenario=1`, { method: "get", }).then(async(res)=>{ //flash主题放block有层级问题 if(/Flash/.test(window.C_SETTINGS.theme.merchant_theme_name) && document.querySelector(".productInfoSection")) { this.tempCss.zIndex = document.querySelector(".product-info-body").style.zIndex; document.querySelector('.product-info-body').style.zIndex="1048"; } this.handleLoading_({type: 'whole', action: 'close'}); const $quickShop = await SPZ.whenApiDefined(document.querySelector('#apps-discount-quick-view-render')); // 定义默认渲染的子款式 const selectedVariant = res.product.variants.find((v)=> (v.available && v.is_hit_discount)) || 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, show_classic_bundle_spu_style: this.show_classic_bundle_spu_style, discountType: this.discountType}; $quickShop.render(data); // 打开加购弹窗 SPZ.whenApiDefined(document.querySelector(`#apps-discount-quick-view`)).then((api)=>{ api.open(); }); }).catch((err)=>{ this.handleLoading_({type: 'whole', action: 'close'}); }) } // 获取选中的商品ids getDefaultSelectorOptions_() { try { const selectedOptions = SPZCore.Dom.scopedQuerySelectorAll(document.body, '[id^="promotionSelectOption-"][selected]'); return SPZCore.Types.toArray(selectedOptions).map((item) => item.getAttribute('option')); } catch (error) { return []; } } // 删除商品子款式 deleteVariantTag(data) { const { product_id, variant_id } = data; if(this.discountInfo.enable_min_purchase_qty == true && this.discountInfo.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(getDiscountMatchPopupEl() || 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); if (!isMultiStyle) return; this.handleProductOption_(product_id, true); const selected = this.getDefaultSelectorOptions_(); this.updateProductPrice_(this.productStyleInfo.filter((item) => selected.includes(item.product_id))); } else { this.handleProductOption_(product_id, false); this.resetProductPrice_(this.bundleProducts.find((item) => item.id == product_id)); } } // 加购弹窗未参与活动 加购按钮不可点击 TODO 拆出来 handleNotHitDiscount_(data) { const $quickShopBody = document.querySelector('#apps-discount-quick-shop-body'); //当前子框式未命中活动 if(data.variant.is_hit_discount == false) { $quickShopBody.setAttribute('variantstatus', 'notHitDiscount') } else { $quickShopBody.setAttribute('variantstatus', '') } } setupAction_() { // 子款式 未参与活动 this.registerAction('handleNotHitDiscount', (invocation) => { const data = invocation.args.data; this.handleNotHitDiscount_(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('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); }); //TODO 加购下单逻辑单独拆组件 this.registerAction('handleClick', (data) => { if(this.discountType == 'DT_CLASSIC_BUNDLE') { this.lineItems = this.productStyleInfo; } else { 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; }, []); this.lineItems = this.productStyleInfo.filter((item) => idArr.includes(item.product_id)); } const action = data.args.action === "cart"; if(action) { //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: Number(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: Number(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; } this.handleRequestError_(data); }) .catch(async (error) => { await error.then((data) => { this.handleRequestError_(data); }); }); } }); this.registerAction('resetModalVariantInfo', () => { //flash主题放block有层级问题 if(/Flash/.test(window.C_SETTINGS.theme.merchant_theme_name) && document.querySelector(".productInfoSection")) { document.querySelector('.product-info-body').style.zIndex = this.tempCss.zIndex; } this.modalVariantInfo = []; }); this.registerAction('handleModalInventoryCheck', (invocation) => { const data = invocation.args.data; this.handleModalInventoryCheck_(data); }); }; }; SPZ.defineElement('spz-custom-discount-bundle-products', SpzCustomDiscountBundleProducts); class SpzCustomDiscountAtcAction extends SPZ.BaseElement { constructor(element) { super(element); this.xhr_ = SPZServices.xhrFor(this.win); this.product_title = ""; } isLayoutSupported(layout) { return layout == SPZCore.Layout.LOGIC; } async getProductTitle_ (data){ this.product_title = data.product_title; } handleLoading_ (event) { const { type, action } = event; const loadingElementId = '#apps-discount-whole-loading'; const loadingElement = document.querySelector(loadingElementId); if (loadingElement) { SPZ.whenApiDefined(loadingElement).then((api) => { if (action === 'show') { api.show_(); } else { api.close_(); } }); } } // 渲染加购弹窗内容 async renderQuickShop_ (data) { this.handleLoading_({type: 'whole', action: 'show'}); const apply_scenario = data.apply_scenario; const discount_id = data.discount_id; const product_id = data.product_id; const limit_purchase = data.limit_purchase; this.xhr_.fetchJson(`/api/storefront/promotion/landing_page/product?product_id=${product_id}&discount_id=${discount_id}&apply_scenario=${apply_scenario}`, { method: "get", }).then(async(res)=>{ this.handleLoading_({type: 'whole', action: 'close'}); const $quickShop = await SPZ.whenApiDefined(document.querySelector('#apps-discount-quick-view-render')); // 定义默认渲染的子款式 const selectedVariant = res.product.variants.find((v)=> (v.available && v.is_hit_discount)) || 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}; data.need_atc = true; data.limit_purchase = limit_purchase; $quickShop.render(data); // 打开加购弹窗 SPZ.whenApiDefined(document.querySelector(`#apps-discount-quick-view`)).then((api)=>{ api.open(); }); }).catch((err)=>{ this.handleLoading_({type: 'whole', action: 'close'}); }) } // 加入购物车 addToCart_(data) { const apply_scenario = data.apply_scenario; const discount_id = data.discount_id; const product_id = data.product_id; this.xhr_.fetchJson(`/api/storefront/promotion/landing_page/product?product_id=${data.product_id}&discount_id=${discount_id}&apply_scenario=${apply_scenario}`, { method: "get", }).then(async(res)=>{ // 单款式加购 const variant_id = res.product.variants[0]?.id; const $productFormInput = document.querySelector(`#apps-discount-product-form-${data.product_id} input[name='variant_id'`); $productFormInput.value = variant_id; SPZ.whenApiDefined(document.querySelector(`#apps-discount-product-form-${data.product_id}`)).then((api)=>{ api.handleAddToCart_(); }); }).catch((err)=> { this.handleLoading_({type: 'whole', action: 'close'}); }) } // 加购弹窗未参与活动 加购按钮不可点击 handleNotHitDiscount_(data) { const $quickShopBody = document.querySelector('#apps-discount-quick-shop-body'); const $limitTip = document.querySelector('.apps_discount_limit_purchase_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', '') } } setupAction_() { this.registerAction('renderQuickShop', (invocation) => { const data = invocation.args; this.renderQuickShop_(data); }); // 加购 this.registerAction('addToCart', (invocation) => { const data = invocation.args; this.addToCart_(data); }); // 子款式 未参与活动 this.registerAction('handleNotHitDiscount', (invocation) => { const data = invocation.args.data; this.handleNotHitDiscount_(data); }); this.registerAction('getCartCount', (invocation) => { //一些老主题还未用spz重构,需要手动触发一次老方法以更新购物车图标的数量 window.$ && $(document).trigger('dj.common.cart.change'); }); this.registerAction('getProductTitle', (invocation) => { const data = invocation.args; this.getProductTitle_(data); }); this.registerAction('handleButton', (invocation) => { const data = invocation.args; window.location.href = this.product_title; }); }; buildCallback() { this.setupAction_(); }; // 同一页里 flashsale_collection(首页限时促销卡片) 与凑单弹窗容器都渲染了本快速加购栈, // 会产生重复 fixed id——尤其加购弹窗 #apps-discount-quick-view。它“打开”走 querySelector, // “关闭”走声明式动作 apps-discount-quick-view.close(按 id 解析),重复时二者可能命中不同副本, // 导致加购成功后弹窗第一次关不掉、第二次才关。参考 toast 的处理,运行时保证这些单例只保留一份。 mountCallback() { const funcs = document.querySelectorAll('#appDiscountRenderQuickShopFunc'); // 逻辑元件重复:仅保留第一个,其余移除自身,保证按 id 解析到的动作实例唯一。 if (funcs.length > 1 && this.element !== funcs[0]) { this.element.remove(); return; } // 加购弹窗重复:保留第一个,移除多余(移除弹窗即连带移除其内部 render/表单等子节点)。 const modals = document.querySelectorAll('#apps-discount-quick-view'); Array.from(modals).slice(1).forEach((el) => el.remove()); } } SPZ.defineElement('spz-custom-discount-atc-action', SpzCustomDiscountAtcAction);
    const TAG = 'spz-custom-discount-cart-banner-info'; const API = { DISPLAY_TEXT: "\/api\/storefront\/promotion\/cart_page\/display_text", CART_SELECT: "\/api\/cart\/cart-select?r=", } class SpzCustomDiscountCartBannerInfo extends SPZ.BaseElement { constructor(element) { super(element); this.xhr_ = SPZServices.xhrFor(this.win); this.displayTextData = {}; // 活动数据文案数据 this.selectedCategory = ''; // 当前选中的分类 this.selectedDiscountId = ''; // 当前选中的折扣 id this.codes = []; // 优惠码数组 } isLayoutSupported(layout) { return layout == SPZCore.Layout.LOGIC; } mountCallback() { this.getDisplayText_(); SPZUtils.Event.listen(document, 'dj.cartChange', (e) => { this.getDisplayText_(); }); //该事件又购物车优惠码组件触发,传参为当前应用的优惠码数组 SPZUtils.Event.listen(document, 'dj.applyDiscountCode', (e) => { this.codes = e.detail.codes; this.getDisplayText_(); }); this.registerAction('renderDiscountSelectors', (invocation) => { const selected = invocation.args?.data; const groups = this.displayTextData.groups || []; const discounts = groups.find(v => v.category == selected)?.discounts || []; // 当前分类下的折扣 const defaultDiscountId = discounts[0]?.discount_id; // 默认选中第一个折扣 this.selectedCategory = selected; this.selectedDiscountId = defaultDiscountId; this.renderDiscountSelectors_(); // 渲染 Tab this.renderDiscountText_(); // 渲染活动进度文案 }); this.registerAction('discountSelectorChange', (invocation) => { const { targetOption } = invocation?.args?.data || {}; this.selectedDiscountId = targetOption; this.renderDiscountText_(); // 切换活动更新活动进度文案 }); } getLineItems_(line_items) { return line_items.map((item) => { return { id: item.id, product_id: item.product_id, variant_id: item.variant_id, quantity: item.quantity, price: item.price, origin_price: item.origin_price, compare_at_price: item.compare_at_price, trunk_price: item.trunk_price, total_price: item.total_price, inventory_tracking: item.inventory_tracking, inventory_policy: item.inventory_policy, inventory_quantity: item.inventory_quantity, custom: item.custom, product_type: item.product_type, properties: item.properties }; }); } getDisplayText_(redo = true) { this.xhr_.fetchJson(API.CART_SELECT + Math.random().toString(36).slice(-4), { method: "GET" } ).then(async (cartData) => { const cartObj = cartData.cart || {}; if(!cartObj || cartObj.line_items?.length <= 0) { // 清空购物车时,清空活动数据 this.displayTextData = {}; this.selectedCategory = ''; this.selectedDiscountId = ''; this.renderDiscountBanner_(); return; } this.xhr_.fetchJson(API.DISPLAY_TEXT, { method: "POST", body: { line_items: this.getLineItems_(cartObj.line_items), sales_channel: { sale_channel_type: "online", sale_channel_id: window.C_SETTINGS.shop.shop_id }, codes: this.codes || [] } }).then(data => { this.displayTextData = data; this.renderDiscountBanner_(redo); // 凑单弹窗加购商品后,重新获取活动进度文案 this.renderDiscountText_(redo); }).catch(err => { }); }); } renderDiscountBanner_(redo = true) { const bannerEl = document.getElementById("app-discount-cart-banner"); if (bannerEl) { SPZ.whenApiDefined(bannerEl).then(apis => { apis.render(this.displayTextData, redo); }); } } renderDiscountSelectors_(redo = true) { const groups = this.displayTextData.groups || []; const discounts = groups.find(v => v.category == this.selectedCategory)?.discounts || []; if (discounts.length <= 0) { return; } const selectorsEl = document.getElementById("app-discount-selectors-render"); if (selectorsEl) { SPZ.whenApiDefined(selectorsEl).then(apis => { apis.render(discounts, redo); }); } } renderDiscountText_(redo = true) { if (!this.selectedCategory || !this.selectedDiscountId) { return; } const groups = this.displayTextData.groups || []; const discounts = groups.find(v => v.category == this.selectedCategory)?.discounts || []; const discount = discounts.find(v => v.discount_id == this.selectedDiscountId) || {}; const textEl = document.getElementById("app-discount-text-render"); if (textEl) { SPZ.whenApiDefined(textEl).then(apis => { apis.render(discount, redo); }); } } } SPZ.defineElement(TAG, SpzCustomDiscountCartBannerInfo);
    Taxes and shipping calculated at checkout