Shopify 实现一个产品订阅模块(Dawn主题模板)

参考网站

Add subscriptions to your theme

预览

前置条件

需创建多个同样但价格不一的产品

需要以当前产品创建多个产品,来设置捆绑数量,比如1件单独销售、2件起售、n件起售,享受不同的折扣和销售计划的折扣。(本质上还是一件产品,不同的价格)

如果是一个产品的销售计划直接复制参考网站代码

安装app创建销售计划

需要安装Shopify官方提供的Subscriptions设置产品订阅计划

设置过后插件通过admin api的muation,这样商店产品就有了“销售计划”这个属性。

创建产品元字段

主要是为了能把默认产品和具有订阅的产品相关联

代码实现

原理

shopify默认加购是form表单,然后使用ajax向shopify发送加购请求body是一个FormData,只需要添加隐藏的input,name为selling_plan,value是selling_plan的id即可。

技术实现

创建selling-plans-integration.liquid(snippets)、selling-plan.css(assets)、selling-plan.js(assets)

创建一个buy-buttons副本buy-selling-buttons,避免影响全局的buy-buttons,

buy-selling-buttons 里面循环元子字段绑定的产品,进而创建顶部tab,再同样循环加购表单,在表单里面render selling-plans-integration.liquid,然后点击tab显示隐藏订阅即可。

我们把所有值都绑定在select的option里(不同的订阅plan,如按周、月、年订阅),切换option拿到值动态渲染,点击一次性加购选项清空所有表单selling_plan的value值,点击订阅选项找到所有表单的select选中的option绑定在dom上的selling_plan的id并赋予到selling_plan这个隐藏的input上,就是这么个逻辑。

代码

selling-plans-integration.liquid
{%- assign current_variant = product.selected_or_first_available_variant | default: product.variants.first -%}
{% if product.selling_plan_groups.size > 0 %}
  <div class="selling_plan_app_container" data-section-id='{{ section.id }}'>
    {% for variant in product.variants %}
      {% if variant.selling_plan_allocations.size > 0 %}
        <section data-variant-id='{{ variant.id }}' class='selling_plan_theme_integration {% if variant.id != current_variant.id %}selling_plan_theme_integration--hidden{% endif %}'>
          <fieldset>
            <div>
              <div class="radio-container">
                <div class="radio-container--subscribe">
                  <label>
                    <input
                      value="sub"
                      type='radio'
                      name="purchaseOption_{{ section.id }}_{{ variant.id }}"
                      {% if variant.available == false %}disabled{% endif %}
                      data-variant-id='{{ variant.id }}'
                      data-variant-saving ='{{ variantSavingPrice }}'
                      checked
                    />
                    Subscribe & Save
                  </label>
                </div>
                {% unless product.requires_selling_plan %}
                  <div class="radio-container--one-time">
                    <label>
                      <input
                        value="one"
                        type='radio'
                        name="purchaseOption_{{ section.id }}_{{ variant.id }}"
                      />
                      One-time purchase
                    </label>
                  </div>
                {% endunless %}
              </div>
              {% assign variant_selling_price = product.selected_or_first_available_selling_plan_allocation.price %}
              {% assign variant_compare_at_price = product.selected_or_first_available_variant.compare_at_price %}
              {% assign variant_price = product.selected_or_first_available_variant.price %}
              <div class="price-wrapper">
                <div class="price-container selling-price">
                  {% if variant_selling_price == variant_price %}
                    <div class="price-regular">{{ variant_selling_price | money }}</div>
                  {% else %}
                    <div class="price-sale">
                      <div class="price-item--sale">{{ variant_selling_price | money }}</div>
                      <s class="price-item--regular">
                        {% if variant_compare_at_price %}
                          {{ variant_compare_at_price | money }}
                        {% else %}
                          {{ variant_price | money }}
                        {% endif %}
                      </s>
                      {% if variant_compare_at_price %}
                        {% assign real_compare_price = variant_compare_at_price %}
                      {% else %}
                        {% assign real_compare_price = variant_price %}
                      {% endif %}
                      {% assign saveing = real_compare_price| minus: variant_selling_price %}
                      <span class="save-price">Save {{ saveing | money }}</span>
                    </div>
                  {% endif %}
                </div>
                <div class="price-container product-price">
                  {% if variant_price == variant_compare_at_price or variant_compare_at_price == blank %}
                    <div class="price-regular">{{ variant_price | money }}</div>
                  {% else %}
                    <div class="price-sale">
                      <div class="price-item--sale">{{ variant_price | money }}</div>
                      <s class="price-item--regular">{{ variant_compare_at_price| money }}</s>
                    </div>
                  {% endif %}
                </div>
              </div>

              <div class="policy-wrapper">
                <div class="policy-advantage">
                  <ul>
                    <li>
                      <svg t="1744189199600" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20488" width="24" height="24"><path d="M512 1024C229.239467 1024 0 794.760533 0 512 0 229.239467 229.239467 0 512 0c282.760533 0 512 229.239467 512 512 0 282.760533-229.239467 512-512 512z m245.521067-707.413333l-298.939734 275.387733-169.984-147.421867-38.263466 39.662934 208.145066 230.6048 331.229867-365.226667-32.187733-33.006933z" fill="#00629b" p-id="20489"></path></svg>
                      <b>Free</b> shipping on every delivery</li>
                    <li>
                      <svg t="1744189199600" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20488" width="24" height="24"><path d="M512 1024C229.239467 1024 0 794.760533 0 512 0 229.239467 229.239467 0 512 0c282.760533 0 512 229.239467 512 512 0 282.760533-229.239467 512-512 512z m245.521067-707.413333l-298.939734 275.387733-169.984-147.421867-38.263466 39.662934 208.145066 230.6048 331.229867-365.226667-32.187733-33.006933z" fill="#00629b" p-id="20489"></path></svg>
                      <b>Free</b> surprise gift with 2nd delivery
                    </li>
                    <li>
                      <svg t="1744189199600" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20488" width="24" height="24"><path d="M512 1024C229.239467 1024 0 794.760533 0 512 0 229.239467 229.239467 0 512 0c282.760533 0 512 229.239467 512 512 0 282.760533-229.239467 512-512 512z m245.521067-707.413333l-298.939734 275.387733-169.984-147.421867-38.263466 39.662934 208.145066 230.6048 331.229867-365.226667-32.187733-33.006933z" fill="#00629b" p-id="20489"></path></svg>
                      <b>Cancel, pause or modify anytime</b>
                    </li>
                  </ul>
                </div>
                <div class="policy-not-advantage">
                  <div>
                    <p><b>Not ready to subscribe?</b></p>
                    <p><b>Here's what you'll miss out on:</b></p>
                  </div> 
                  <ul>
                    {% assign extra_money = variant_price | minus: variant_selling_price %}
                    <li>
                      <svg t="1744189679350" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28386" width="18" height="18"><path d="M512 981.333333C251.733333 981.333333 42.666667 772.266667 42.666667 512S251.733333 42.666667 512 42.666667s469.333333 209.066667 469.333333 469.333333-209.066667 469.333333-469.333333 469.333333z m0-409.6l98.133333 98.133334c8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866667-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733334L571.733333 512l98.133334-98.133333c8.533333-8.533333 12.8-17.066667 12.8-29.866667 0-12.8-4.266667-21.333333-12.8-29.866667-8.533333-8.533333-17.066667-12.8-29.866667-12.8-12.8 0-21.333333 4.266667-29.866667 12.8L512 452.266667 413.866667 354.133333C405.333333 345.6 396.8 341.333333 384 341.333333c-12.8 0-21.333333 4.266667-29.866667 12.8-8.533333 8.533333-12.8 17.066667-12.8 29.866667 0 12.8 4.266667 21.333333 12.8 29.866667l98.133334 98.133333-98.133334 98.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733334 8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866667-12.8l98.133333-98.133334z" p-id="28387" fill="#e16531"></path></svg>
                      <b>{{ extra_money | money }}</b> in extra savings</li>
                    <li>
                      <svg t="1744189679350" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28386" width="18" height="18"><path d="M512 981.333333C251.733333 981.333333 42.666667 772.266667 42.666667 512S251.733333 42.666667 512 42.666667s469.333333 209.066667 469.333333 469.333333-209.066667 469.333333-469.333333 469.333333z m0-409.6l98.133333 98.133334c8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866667-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733334L571.733333 512l98.133334-98.133333c8.533333-8.533333 12.8-17.066667 12.8-29.866667 0-12.8-4.266667-21.333333-12.8-29.866667-8.533333-8.533333-17.066667-12.8-29.866667-12.8-12.8 0-21.333333 4.266667-29.866667 12.8L512 452.266667 413.866667 354.133333C405.333333 345.6 396.8 341.333333 384 341.333333c-12.8 0-21.333333 4.266667-29.866667 12.8-8.533333 8.533333-12.8 17.066667-12.8 29.866667 0 12.8 4.266667 21.333333 12.8 29.866667l98.133334 98.133333-98.133334 98.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733334 8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866667-12.8l98.133333-98.133334z" p-id="28387" fill="#e16531"></path></svg>
                      <b>Free</b> shipping</li>
                    <li>
                      <svg t="1744189679350" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28386" width="18" height="18"><path d="M512 981.333333C251.733333 981.333333 42.666667 772.266667 42.666667 512S251.733333 42.666667 512 42.666667s469.333333 209.066667 469.333333 469.333333-209.066667 469.333333-469.333333 469.333333z m0-409.6l98.133333 98.133334c8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866667-12.8 17.066667-17.066667 17.066667-42.666667 0-59.733334L571.733333 512l98.133334-98.133333c8.533333-8.533333 12.8-17.066667 12.8-29.866667 0-12.8-4.266667-21.333333-12.8-29.866667-8.533333-8.533333-17.066667-12.8-29.866667-12.8-12.8 0-21.333333 4.266667-29.866667 12.8L512 452.266667 413.866667 354.133333C405.333333 345.6 396.8 341.333333 384 341.333333c-12.8 0-21.333333 4.266667-29.866667 12.8-8.533333 8.533333-12.8 17.066667-12.8 29.866667 0 12.8 4.266667 21.333333 12.8 29.866667l98.133334 98.133333-98.133334 98.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733334 8.533333 8.533333 17.066667 12.8 29.866667 12.8 12.8 0 21.333333-4.266667 29.866667-12.8l98.133333-98.133334z" p-id="28387" fill="#e16531"></path></svg>
                      <b>Free</b> surprise gift with 2nd delivery</li>
                  </ul>
                </div>
              </div>
              {% assign group_ids = variant.selling_plan_allocations | map: 'selling_plan_group_id' | uniq %}
              {% for group_id in group_ids %}
                {%liquid
                   assign group = product | map: 'selling_plan_groups' | where: 'id', group_id | first
                   assign allocations = variant | map: 'selling_plan_allocations' | where: 'selling_plan_group_id', group_id

                   if forloop.first
                    assign first_selling_plan_group = true
                  else
                    assign first_selling_plan_group = false
                  endif
                %}
                <div class="selling-select-container">
                  <select>
                    {% for allocation in allocations %}

                      {%liquid
                        if forloop.first and product.requires_selling_plan and first_selling_plan_group
                          assign plan_checked = 'checked'
                        else
                          assign plan_checked = nil
                        endif
                        assign allocationPrice = allocation.price | money | escape
                        assign allocationSavingPrice = real_compare_price | minus: allocation.price | money | escape
                      %}
                      <option
                        {% if variant.available == false %}disabled{% endif %}
                        aria-label='{{ allocation.selling_plan.name }}. Product price {{ allocationPrice }}'
                        data-radio-type='selling_plan'
                        data-selling-plan-id='{{ allocation.selling_plan.id }}'
                        data-selling-plan-group-id='{{ section.id }}_{{ group_id }}_{{ variant.id }}'
                        data-selling-plan-adjustment='{{ allocation.selling_plan.price_adjustments.size }}'
                        data-variant-price='{{ allocationPrice }}'
                        data-variant-saving='{{ allocationSavingPrice }}'
                        {{ plan_checked }}
                        >
                        {{ allocation.selling_plan.name | split: ',' | first }}
                      </option>
                    {% endfor %}
                  </select>
                </div>
              {% endfor %}
            </div>
          </fieldset>
        </section>
      {% endif %}
    {% endfor %}
    <input
      name='selling_plan'
      class='selected-selling-plan-id'
      value="{{ product.selected_or_first_available_variant.selling_plan_allocations.first.selling_plan.id }}"
      type='hidden'
    >
  </div>
{% endif %}
selling-plan.css
.product-selling-plan-form {
  color: #000;
}
.product-selling-plan-form .product-form {
  margin: 0;
}
.product-selling-plan-form fieldset {
  border: none;
  padding: 0;
}
.product-selling-plan-form .selling-name-list__item .label-flare {
  position: absolute;
  top: 0;
  width: 100%;
  background-color: #e4eef6;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  color: #00629b;
  text-transform: uppercase;
  text-wrap: nowrap;
  padding: 4px;
}
.product-selling-plan-form .selling-name-list__item.selected .label-flare {
  background-color: #00629b;
  color: #fff;
}
.product-selling-plan-form .selling-name-list__item .label-title {
  font-weight: bold;
  font-size: 18px;
}
.product-selling-plan-form .selling-name-list__item {
  overflow: hidden;
  flex: 1;
  position: relative;
  display: flex;
  justify-content: center;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  background-color: #fcfeff;
  border: 1px solid #E4EEF6;
  border-radius: 6px;
  text-align: center;
}
.product-selling-plan-form .selling-name-list__item.has-tag {
  padding: 20px 10px 0px;
}
.product-selling-plan-form .bottom-price {
  font-size: 14px;
}
.product-selling-plan-form .selling-name-list__item.selected {
  background-color: #f3faff;
  border-color: #00629b;
}
.product-selling-plan-form .price-wrapper {
  min-height: 40px;
  margin: 20px 0;
}
.product-selling-plan-form .product-form-list {
  margin-top: 20px;
  background-color: #F4FAFF;
  border-radius: 4px;
  padding: 10px;
}

.product-selling-plan-form .product-form {
  display: none;
  pointer-events: none;
}
.product-selling-plan-form .product-form.show {
  pointer-events: auto;
  display: block;
}
.product-selling-plan-form.onetime-form .selling-name-list .one-price,
.product-selling-plan-form.subscription-form .selling-name-list .selling-save-price,
.product-selling-plan-form.onetime-form .policy-not-advantage,
.product-selling-plan-form.subscription-form .policy-advantage,
.product-selling-plan-form.onetime-form .product-price,
.product-selling-plan-form.subscription-form .selling-select-container,
.product-selling-plan-form.subscription-form .selling-price {
  display: block;
}
.product-selling-plan-form.onetime-form .selling-name-list .selling-save-price,
.product-selling-plan-form.subscription-form .selling-name-list .one-price,
.product-selling-plan-form.subscription-form .policy-not-advantage,
.product-selling-plan-form.onetime-form .policy-advantage,
.product-selling-plan-form.onetime-form .selling-price,
.product-selling-plan-form.onetime-form .selling-select-container,
.product-selling-plan-form.subscription-form .product-price {
  display: none;
}

.product-selling-plan-form.subscription-form .radio-container--subscribe {
  font-weight: 600;
}

.product-selling-plan-form.onetime-form .radio-container--subscribe {
  font-weight: 600;
}

.product-selling-plan-form .selling-name-list {
  display: flex;
  gap: 20px;
}
.product-selling-plan-form .price-sale {
  display: flex;
  align-items: center;
  gap: 10px;
}
.product-selling-plan-form .price-sale > span {
  margin-left: 10px;
  background-color: #ced1ff;
  padding: 5px 10px;
  font-weight: 600;
  text-align: center;
  border-radius: 2px;
}
.product-selling-plan-form .selling-select-container {
  margin: 20px 0;
}
.product-selling-plan-form select {
  display: block;
  width: 100%;
  height: 40px;
  font-size: 14px;
  padding: 0 20px;
  appearance: none;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right 20px center;
  background-size: 16px;
}

.product-selling-plan-form .radio-container {
  display: flex;
}
.product-selling-plan-form .radio-container label {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
}
.product-selling-plan-form .radio-container > div {
  flex: 1;
}
.product-selling-plan-form .radio-container > div input {
  width: 16px;
  height: 16px;
  accent-color: #00629b;
  cursor: pointer;
  margin: 0;
}
.product-selling-plan-form .price-regular,
.product-selling-plan-form .price-item--sale,
.product-selling-plan-form .price-item--regular {
  font-weight: bold;
  font-size: 24px;
  min-height: 40px;
}

.product-selling-plan-form .price-sale .price-item--regular {
  color: #666;
}
.policy-not-advantage, 
.policy-advantage {
  margin: 20px 0;
}

.policy-not-advantage p {
  margin: 0;
}

.policy-not-advantage ul {
  margin-top: 10px;
}
.policy-wrapper li {
  display: flex;
  align-items: center;
  list-style: none;
  gap: 4px;
}
.policy-wrapper li svg {
  margin-right: 6px;
  width: 18px;
  height: 18px;
}
.policy-wrapper ul {
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
selling-plan.js
class ProductSellingPlanHandler {
  constructor() {
    this.radioContainers = document.querySelectorAll('.product-selling-plan-form .radio-container');
    this.productSellingPlanForm = document.querySelector('.product-selling-plan-form');
    this.sellingNameListItem = document.querySelectorAll('.selling-name-list__item');
    this.selectContainers = document.querySelectorAll('.selling-select-container');
    this.init();
  }

  init() {
    this.setupRadioChangeEvents();
    this.setupSellingNameListItemClickEvents();
    this.setupSelectChangeEvents();
  }

  setupRadioChangeEvents() {
    this.radioContainers.forEach(container => {
      const radios = container.querySelectorAll('input[type="radio"]');
      radios.forEach(radio => {
        radio.addEventListener('change', () => {
          this.handleRadioChange(radio);
        });
      });
    });
  }

  handleRadioChange(radio) {
    const value = radio.value;
    if (value === 'one') {
      this.productSellingPlanForm.classList.add('onetime-form');
      this.productSellingPlanForm.classList.remove('subscription-form');
      this.clearSellingInputValue();
    } else {
      this.productSellingPlanForm.classList.add('subscription-form');
      this.productSellingPlanForm.classList.remove('onetime-form');
      this.setSellingInputValue();
    }
    const isChecked = radio.checked;
    this.radioContainers.forEach(otherContainer => {
      const otherRadios = otherContainer.querySelectorAll('input[type="radio"]');
      otherRadios.forEach(otherRadio => {
        if (otherRadio.value === value) {
          otherRadio.checked = isChecked;
        }
      });
    });
  }

  setupSellingNameListItemClickEvents() {
    this.sellingNameListItem.forEach(el => {
      el.addEventListener('click', () => {
        this.handleSellingNameListItemClick(el);
      });
    });
  }

  handleSellingNameListItemClick(el) {
    this.sellingNameListItem.forEach(item => item.classList.remove('selected'));
    el.classList.add('selected');
    const productId = el.getAttribute('data-product-id');
    const forms = document.querySelectorAll('.product-form');
    forms.forEach(form => {
      if (form.getAttribute('data-product-id') === productId) {
        form.classList.add('show');
      } else {
        form.classList.remove('show');
      }
    });
  }

  setupSelectChangeEvents() {
    this.selectContainers.forEach((container, idx) => {
      const select = container.querySelector('select');
      select.addEventListener('change', () => {
        this.handleSelectChange(select, idx);
      });
    });
  }

  handleSelectChange(select, idx) {
    const selectedOption = select.options[select.selectedIndex];
    const sellingPlanId = selectedOption.getAttribute('data-selling-plan-id');
    const productForm = select.closest('.product-form');
    const selectedSellingPlanIdElement = productForm.querySelector('.selected-selling-plan-id');
    selectedSellingPlanIdElement.value = sellingPlanId;

    const price = selectedOption.dataset.variantPrice;
    const saving = selectedOption.dataset.variantSaving;
    const priceItemSale = productForm.querySelector('.price-item--sale');
    const priceItemSaving = productForm.querySelector('.save-price');
    priceItemSale.innerHTML = price;
    priceItemSaving.innerHTML = `Save ${saving}`;

    this.sellingNameListItem.forEach((el, i) => {
      if (idx === i) {
        el.querySelector('.selling-save-price').innerHTML = `Save ${saving}`;
      }
    });
  }

  clearSellingInputValue() {
    const inputContainers = document.querySelectorAll('.selected-selling-plan-id');
    inputContainers.forEach(input => {
      input.value = '';
    });
  }

  setSellingInputValue() {
    const inputContainers = document.querySelectorAll('.selected-selling-plan-id');
    inputContainers.forEach(input => {
      const sellingPlanAppContainer = input.closest('.selling_plan_app_container');
      if (sellingPlanAppContainer) {
        const selectedOption = sellingPlanAppContainer.querySelector('select');
        if (selectedOption) {
          const selectedOptionId = selectedOption.options[selectedOption.selectedIndex].getAttribute('data-selling-plan-id');
          input.value = selectedOptionId;
        }
      }
    });
  }
}

new ProductSellingPlanHandler();
buy-selling-buttons.liquid
{% comment %}
  Renders product buy-buttons.
  Accepts:
  - product: {Object} product object.
  - block: {Object} passing the block information.
  - product_form_id: {String} product form id.
  - section_id: {String} id of section to which this snippet belongs.
  - show_pickup_availability: {Boolean} for the pickup availability. If true the pickup availability is rendered, false - not rendered (optional).

  Usage:
  {% render 'buy-buttons', block: block, product: product, product_form_id: product_form_id, section_id: section.id, show_pickup_availability: true %}
{% endcomment %}
{{ 'selling-plan.css' | asset_url | stylesheet_tag }}
<div class="product-selling-plan-form subscription-form">
  {%- if product != blank -%}
    {% if product.metafields.custom.selling_plan_products != blank %}
      <div class="selling-name-list">
        {% for product_item in product.metafields.custom.selling_plan_products.value %}
          {% if product_item.selected_or_first_available_variant.compare_at_price %}
            {% assign real_compare_price = product_item.selected_or_first_available_variant.compare_at_price %}
          {% else %}
            {% assign real_compare_price = product_item.selected_or_first_available_variant.price %}
          {% endif %}
          {% assign saveing = real_compare_price | minus: product_item.selected_or_first_available_selling_plan_allocation.price %}
          <legend data-product-id="{{ product_item.id }}" class="selling-name-list__item {% if product_item.metafields.custom.selling_tag %}has-tag{% endif %} {% if forloop.first %}selected{% endif %}">
            {% if product_item.metafields.custom.selling_tag %}
              <span class="label-flare">{{ product_item.metafields.custom.selling_tag }}</span>
            {% endif %}
            <span class="label-title">{{ product_item.metafields.custom.selling_name }}</span>
            <span class="selling-save-price bottom-price">
              Save {{ saveing | money }}
            </span>
            <span class="one-price bottom-price">
              {% if product_item.selected_or_first_available_variant.compare_at_price > product_item.selected_or_first_available_variant.price %}
                {% assign saveing_price = product_item.selected_or_first_available_variant.compare_at_price | minus: product_item.selected_or_first_available_variant.price %}
                Save {{ saveing_price | money }}
              {% endif %}
            </span>
          </legend>
        {% endfor %}
      </div>
      <div class="product-form-list">
        {% assign section_id = section.id | append: '-' | append: block.id %}
        {% for product in product.metafields.custom.selling_plan_products.value %}
          <product-form
            data-product-id="{{ product.id }}"
            class="product-form {% if forloop.first %}show{% endif %}"
            data-hide-errors="{{ gift_card_recipient_feature_active }}"
            data-section-id="{{ section.id }}"
          >
            <div class="product-form__error-message-wrapper" role="alert" hidden>
              <span class="svg-wrapper">
                {{- 'icon-error.svg' | inline_asset_content -}}
              </span>
              <span class="product-form__error-message"></span>
            </div>
  
            {%- form 'product',
              product,
              id: product_form_id,
              class: 'form',
              novalidate: 'novalidate',
              data-type: 'add-to-cart-form'
            -%}
              {% render 'selling-plans-integration', product: product  %}
              <input
                type="hidden"
                name="id"
                value="{{ product.selected_or_first_available_variant.id }}"
                {% if product.selected_or_first_available_variant.available == false
                  or quantity_rule_soldout
                  or product.selected_or_first_available_variant == null
                %}
                  disabled
                {% endif %}
                class="product-variant-id"
              >
  
              <div class="product-form__buttons">
                {%- liquid
                  assign check_against_inventory = true
                  if product.selected_or_first_available_variant.inventory_management != 'shopify' or product.selected_or_first_available_variant.inventory_policy == 'continue'
                    assign check_against_inventory = false
                  endif
                  if product.selected_or_first_available_variant.quantity_rule.min > product.selected_or_first_available_variant.inventory_quantity and check_against_inventory
                    assign quantity_rule_soldout = true
                  endif
                -%}
                <button
                  id="ProductSubmitButton-{{ section_id }}"
                  type="submit"
                  name="add"
                  class="product-form__submit button button--full-width {% if show_dynamic_checkout %}button--secondary{% else %}button--primary{% endif %}"
                  {% if product.selected_or_first_available_variant.available == false
                    or quantity_rule_soldout
                    or product.selected_or_first_available_variant == null
                  %}
                    disabled
                  {% endif %}
                >
                  <span>
                    {%- if product.selected_or_first_available_variant == null -%}
                      {{ 'products.product.unavailable' | t }}
                    {%- elsif product.selected_or_first_available_variant.available == false or quantity_rule_soldout -%}
                      {{ 'products.product.sold_out' | t }}
                    {%- else -%}
                      {{ 'products.product.add_to_cart' | t }}
                    {%- endif -%}
                  </span>
                  {%- render 'loading-spinner' -%}
                </button>
              </div>
            {%- endform -%}
          </product-form>
        {% endfor %}
      </div>
    {% else %}
      <product-form
        class="product-form"
        data-hide-errors="{{ gift_card_recipient_feature_active }}"
        data-section-id="{{ section.id }}"
      >
        <div class="product-form__error-message-wrapper" role="alert" hidden>
          <span class="svg-wrapper">
            {{- 'icon-error.svg' | inline_asset_content -}}
          </span>
          <span class="product-form__error-message"></span>
        </div>

        {%- form 'product',
          product,
          id: product_form_id,
          class: 'form',
          novalidate: 'novalidate',
          data-type: 'add-to-cart-form'
        -%}
          <input
            type="hidden"
            name="id"
            value="{{ product.selected_or_first_available_variant.id }}"
            {% if product.selected_or_first_available_variant.available == false
              or quantity_rule_soldout
              or product.selected_or_first_available_variant == null
            %}
              disabled
            {% endif %}
            class="product-variant-id"
          >
          {%- if gift_card_recipient_feature_active -%}
            {%- render 'gift-card-recipient-form', product: product, form: form, section: section -%}
          {%- endif -%}

          <div class="product-form__buttons">
            {%- liquid
              assign check_against_inventory = true
              if product.selected_or_first_available_variant.inventory_management != 'shopify' or product.selected_or_first_available_variant.inventory_policy == 'continue'
                assign check_against_inventory = false
              endif
              if product.selected_or_first_available_variant.quantity_rule.min > product.selected_or_first_available_variant.inventory_quantity and check_against_inventory
                assign quantity_rule_soldout = true
              endif
            -%}
            <button
              id="ProductSubmitButton-{{ section_id }}"
              type="submit"
              name="add"
              class="product-form__submit button button--full-width"
              {% if product.selected_or_first_available_variant.available == false
                or quantity_rule_soldout
                or product.selected_or_first_available_variant == null
              %}
                disabled
              {% endif %}
            >
              <span>
                {%- if product.selected_or_first_available_variant == null -%}
                  {{ 'products.product.unavailable' | t }}
                {%- elsif product.selected_or_first_available_variant.available == false or quantity_rule_soldout -%}
                  {{ 'products.product.sold_out' | t }}
                {%- else -%}
                  {{ 'products.product.add_to_cart' | t }}
                {%- endif -%}
              </span>
              {%- render 'loading-spinner' -%}
            </button>
          </div>
        {%- endform -%}
      </product-form>
    {% endif %}
  {%- else -%}
    <div class="product-form">
      <div class="product-form__buttons form">
        <button
          type="submit"
          name="add"
          class="product-form__submit button button--full-width button--primary"
          disabled
        >
          {{ 'products.product.sold_out' | t }}
        </button>
      </div>
    </div>
  {%- endif -%}
</div>
<script src="{{ 'selling-plan.js' | asset_url }}" defer="defer"></script>
main-product.js

最后需要隐藏原生的数量框和价格框

开发语言:Java 框架:SSM(Spring、Spring MVC、MyBatis) JDK版本:JDK 1.8 或以上 开发工具:Eclipse 或 IntelliJ IDEA Maven版本:Maven 3.3 或以上 数据库:MySQL 5.7 或以上 此压缩包包含了本毕业设计项目的完整内容,具体包括源代码、毕业论文以及演示PPT模板。 项目配置完成后即可运行,若需添加额外功能,可根据需求自行扩展。 运行条件 确保已安装 JDK 1.8 或更高版本,并正确配置 Java 环境变量。 使用 Eclipse 或 IntelliJ IDEA 打开项目,导入 Maven 依赖,确保依赖包下载完成。 配置数据库环境,确保 MySQL 服务正常运行,并导入项目中提供的数据库脚本。 在 IDE 中启动项目,确认所有服务正常运行。 主要功能简述: 用户管理:系统管理员负责管理所有用户信息,包括学生、任课老师、班主任、院系领导和学校领导的账号创建、权限分配等。 数据维护:管理员可以动态更新和维护系统所需的数据,如学生信息、课程安排、学年安排等,确保系统的正常运行。 系统配置:管理员可以对系统进行配置,如设置数据库连接参数、调整系统参数等,以满足不同的使用需求。 身份验证:系统采用用户名和密码进行身份验证,确保只有授权用户才能访问系统。不同用户类型(学生、任课老师、班主任、院系领导、学校领导、系统管理员)具有不同的操作权限。 权限控制:系统根据用户类型分配不同的操作权限,确保用户只能访问和操作其权限范围内的功能和数据。 数据安全:系统采取多种措施保障数据安全,如数据库加密、访问控制等,防止数据泄露和非法访问。 请假审批流程:系统支持请假申请的逐级审批,包括班主任审批和院系领导审批(针对超过三天的请假)。学生可以随时查看请假申请的审批进展情况。 请假记录管理:系统记录学生的所有请假记录,包括请假时间、原因、审批状态及审批意见等,供学生和审批人员查询。 学生在线请假:学生可以通过系统在线填写请假申请,包括请假的起止日期和请假原因,并提交给班主任审批。超过三天的请假需经班主任审批后,再由院系领导审批。 出勤信息记录:任课老师可以在线记录学生的上课出勤情况,包括迟到、早退、旷课和请假等状态。 出勤信息查询:学生、任课老师、班主任、院系领导和学校领导均可根据权限查看不同范围的学生上课出勤信息。学生可以查看自己所有学年的出勤信息,任课老师可以查看所教班级的出勤信息,班主任和院系领导可以查看本班或本院系的出勤信息,学校领导可以查看全校的出勤信息。 出勤统计与分析:系统提供出勤统计功能,可以按班级、学期等条件统计学生的出勤情况,帮助管理人员了解学生的出勤状况
开发语言:Java 框架:SSM(Spring、Spring MVC、MyBatis) JDK版本:JDK 1.8 或以上 开发工具:Eclipse 或 IntelliJ IDEA Maven版本:Maven 3.3 或以上 数据库:MySQL 5.7 或以上 此压缩包包含了本毕业设计项目的完整内容,具体包括源代码、毕业论文以及演示PPT模板。 项目配置完成后即可运行,若需添加额外功能,可根据需求自行扩展。 运行条件 确保已安装 JDK 1.8 或更高版本,并正确配置 Java 环境变量。 使用 Eclipse 或 IntelliJ IDEA 打开项目,导入 Maven 依赖,确保依赖包下载完成。 配置数据库环境,确保 MySQL 服务正常运行,并导入项目中提供的数据库脚本。 在 IDE 中启动项目,确认所有服务正常运行。 主要功能简述: 用户管理:系统管理员负责管理所有用户信息,包括学生、任课老师、班主任、院系领导和学校领导的账号创建、权限分配等。 数据维护:管理员可以动态更新和维护系统所需的数据,如学生信息、课程安排、学年安排等,确保系统的正常运行。 系统配置:管理员可以对系统进行配置,如设置数据库连接参数、调整系统参数等,以满足不同的使用需求。 身份验证:系统采用用户名和密码进行身份验证,确保只有授权用户才能访问系统。不同用户类型(学生、任课老师、班主任、院系领导、学校领导、系统管理员)具有不同的操作权限。 权限控制:系统根据用户类型分配不同的操作权限,确保用户只能访问和操作其权限范围内的功能和数据。 数据安全:系统采取多种措施保障数据安全,如数据库加密、访问控制等,防止数据泄露和非法访问。 请假审批流程:系统支持请假申请的逐级审批,包括班主任审批和院系领导审批(针对超过三天的请假)。学生可以随时查看请假申请的审批进展情况。 请假记录管理:系统记录学生的所有请假记录,包括请假时间、原因、审批状态及审批意见等,供学生和审批人员查询。 学生在线请假:学生可以通过系统在线填写请假申请,包括请假的起止日期和请假原因,并提交给班主任审批。超过三天的请假需经班主任审批后,再由院系领导审批。 出勤信息记录:任课老师可以在线记录学生的上课出勤情况,包括迟到、早退、旷课和请假等状态。 出勤信息查询:学生、任课老师、班主任、院系领导和学校领导均可根据权限查看不同范围的学生上课出勤信息。学生可以查看自己所有学年的出勤信息,任课老师可以查看所教班级的出勤信息,班主任和院系领导可以查看本班或本院系的出勤信息,学校领导可以查看全校的出勤信息。 出勤统计与分析:系统提供出勤统计功能,可以按班级、学期等条件统计学生的出勤情况,帮助管理人员了解学生的出勤状况
此压缩包包含了本毕业设计项目的完整内容,具体包括源代码、毕业论文以及演示PPT模板。 开发语言:Java 框架:SSM(Spring、Spring MVC、MyBatis) JDK版本:JDK 1.8 或以上 开发工具:Eclipse 或 IntelliJ IDEA Maven版本:Maven 3.3 或以上 数据库:MySQL 5.7 或以上 项目配置完成后即可运行,若需添加额外功能,可根据需求自行扩展。 运行条件 确保已安装 JDK 1.8 或更高版本,并正确配置 Java 环境变量。 使用 Eclipse 或 IntelliJ IDEA 打开项目,导入 Maven 依赖,确保依赖包下载完成。 配置数据库环境,确保 MySQL 服务正常运行,并导入项目中提供的数据库脚本。 在 IDE 中启动项目,确认所有服务正常运行。 主要功能简述: 请假审批流程:系统支持请假申请的逐级审批,包括班主任审批和院系领导审批(针对超过三天的请假)。学生可以随时查看请假申请的审批进展情况。 请假记录管理:系统记录学生的所有请假记录,包括请假时间、原因、审批状态及审批意见等,供学生和审批人员查询。 学生在线请假:学生可以通过系统在线填写请假申请,包括请假的起止日期和请假原因,并提交给班主任审批。超过三天的请假需经班主任审批后,再由院系领导审批。 出勤信息记录:任课老师可以在线记录学生的上课出勤情况,包括迟到、早退、旷课和请假等状态。 出勤信息查询:学生、任课老师、班主任、院系领导和学校领导均可根据权限查看不同范围的学生上课出勤信息。学生可以查看自己所有学年的出勤信息,任课老师可以查看所教班级的出勤信息,班主任和院系领导可以查看本班或本院系的出勤信息,学校领导可以查看全校的出勤信息。 出勤统计与分析:系统提供出勤统计功能,可以按班级、学期等条件统计学生的出勤情况,帮助管理人员了解学生的出勤状况。 用户管理:系统管理员负责管理所有用户信息,包括学生、任课老师、班主任、院系领导和学校领导的账号创建、权限分配等。 数据维护:管理员可以动态更新和维护系统所需的数据,如学生信息、课程安排、学年安排等,确保系统的正常运行。 系统配置:管理员可以对系统进行配置,如设置数据库连接参数、调整系统参数等,以满足不同的使用需求。 身份验证:系统采用用户名和密码进行身份验证,确保只有授权用户才能访问系统。不同用户类型(学生、任课老师、班主任、院系领导、学校领导、系统管理员)具有不同的操作权限。 权限控制:系统根据用户类型分配不同的操作权限,确保用户只能访问和操作其权限范围内的功能和数据。 数据安全:系统采取多种措施保障数据安全,如数据库加密、访问控制等,防止数据泄露和非法访问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值