十一、建立电子商务商店——添加收银台

在过去的几章中,我们一直在创建一个电子商务商店。到目前为止,我们已经创建了一个产品页面,允许我们查看图像和产品变化,可能是尺寸或样式。我们还创建了一个带有过滤器和分页的分类页面,其中包括一个主页分类页面,该页面具有特定的选定产品。

我们的用户可以浏览和筛选产品,并查看有关特定产品的更多信息。我们现在要做的是:

  • 构建允许用户将产品添加和删除到其购物篮中的功能
  • 允许用户签出
  • 添加订单确认页面

作为提醒,我们不会记录任何账单详情,但我们会制作一个订单确认屏幕。

创建篮子数组占位符

为了帮助我们在整个应用中保持篮子中的产品,我们将在 Vuex 商店中存储用户选择的产品。这将以对象数组的形式出现。每个对象将包含多个关键信息,这些信息允许我们在无需每次查询 Vuex 商店的情况下显示购物篮中的产品。它还允许我们存储有关产品页面当前状态的详细信息,并在选择变体时记住图像更新。

我们将为添加到购物篮中的每种产品存储的详细信息如下:

  • 产品名称
  • 产品句柄,以便我们可以链接回产品
  • 所选变体标题(显示在“选择”框中)
  • 当前选定的图像,因此我们可以在签出窗口中显示适当的图像
  • 变更详细信息,包括价格和重量以及其他详细信息
  • 变体 SKU,这将帮助我们确定产品是否已添加
  • 数量,用户已添加到其购物篮中的商品数量

由于我们将在一个数组中包含的对象中存储所有这些信息,因此我们需要在存储中创建一个占位符数组。向名为basket的存储中的state对象添加一个新键,并使其成为空白数组:

const store = new Vuex.Store({
  state: {
    products: {},
    categories: {},

    categoryHome: {
      title: 'Welcome to the Shop',
      handle: 'home',
      products: [
        ...
      ]
    },

    basket: []

  },

  mutations: {
    ...
  },

  actions: {
    ...
  },

  getters: {
    ...
  }
});

向商店添加产品信息

当我们的basket数组准备好接收数据后,我们现在可以创建一个变体来添加产品对象。打开ProductPage.js文件并更新addToBasket方法以调用$store提交函数,而不是我们设置的alert

我们需要将产品添加到篮子中的所有信息都存储在ProductPage组件上,因此我们可以使用this关键字将组件实例传递给commit()函数。这将在我们构建变异时变得清晰。

将函数调用添加到ProductPage方法中:

methods: {
  ...

 addToBasket() {
 this.$store.commit('addToBasket', this);
 }
}

创建门店突变,将产品添加到购物篮中

导航到 Vuex 存储并创建一个名为addToBasket的新突变。这将接受state作为第一个参数,组件实例作为第二个参数。通过传递实例,我们可以访问组件上的变量、方法和计算值:

mutations: {
  products(state, payload) {
    ...
  },

  categories(state, payload) {
    ...
  },

 addToBasket(state, item) {

 }
}

我们现在可以继续将产品添加到basket阵列中。第一步是添加具有上述属性的 product 对象。由于它是一个数组,我们可以使用push()函数添加对象。

接下来,向数组中添加一个对象,使用item及其属性来构建该对象。通过访问ProductPage组件,我们可以使用variantTitle方法按照选择框中显示的方式构建变体标题。默认设置数量为1

addToBasket(state, item) {
  state.basket.push({
 sku: item.variation.sku,
 title: item.product.title,
 handle: item.slug,
 image: item.image,
 variationTitle: item.variantTitle(item.variation),
 variation: item.variation,
 quantity: 1
 });
}

现在将产品添加到basket数组中。但是,当您将两个相同的项目添加到篮子中时,会出现问题。它没有增加quantity,而是简单地添加了第二个产品。

这可以通过检查阵列中是否已经存在sku来解决。如果有,我们可以增加该物品的数量,如果没有,我们可以向basket数组添加一个新物品。sku对于每种产品的每种变化都是唯一的。或者,我们可以使用条形码属性。

使用本机findJavaScript 函数,我们可以识别任何 SKU 与传入 SKU 匹配的产品:

addToBasket(state, item) {
 let product = state.basket.find(p => {
 if(p.sku == item.variation.sku) {
 }
 });

  state.basket.push({
    sku: item.variation.sku,
    title: item.product.title,
    handle: item.slug,
    image: item.image,
    variationTitle: item.variantTitle(item.variation),
    variation: item.variation,
    quantity: 1
  });
}

如果匹配,我们可以使用 JavaScript 中的++符号将该对象的数量增加 1。如果没有,我们可以将新对象添加到basket数组中。使用find功能时,如果产品存在,我们可以退货。如果没有,我们可以添加一个新项目:

addToBasket(state, item) {
  let product = state.basket.find(p => {
    if(p.sku == item.variation.sku) {
      p.quantity++;

 return p;
    }
  });

  if(!product) {
    state.basket.push({
      sku: item.variation.sku,
      title: item.product.title,
      handle: item.slug,
      image: item.image,
      variationTitle: item.variantTitle(item.variation),
      variation: item.variation,
      quantity: 1
    });
 }
}

我们现在有一个篮子,当项目添加到篮子中时,它会被填充,当它已经存在时,它会增加。

为了提高应用的可用性,我们应该在用户向购物篮中添加物品时给他们一些反馈。这可以通过简单地更新 addtobasket 按钮并在站点标题中显示产品计数,以及指向 Basket 的链接来实现。

添加项目时更新“添加到购物篮”按钮

作为对我们商店的可用性改进,我们将在用户单击“添加到购物篮”按钮时更新该按钮。这将更改为“添加到篮子中”,并在返回到以前的状态之前,在设定的时间段(例如,两秒钟)内应用一个类。CSS 类将允许您以不同的方式设置按钮样式,例如,将背景更改为绿色或稍微改变它。

这将通过使用组件上的数据属性来实现,在添加项时将其设置为truefalse。CSS 类和文本将使用此属性确定要显示的内容,setTimeoutJavaScript 函数将更改属性的状态。

打开ProductPage组件,向名为addedToBasket的数据对象添加一个新键。默认设置为false

data() {
  return {
    slug: this.$route.params.slug,
    productNotFound: false,
    image: false,
    variation: false,
    addedToBasket: false
  }
}

更新按钮文本以允许此变化。因为已经有一个三元if,在这个三元if中,我们将用另一个三元if嵌套它们。如果需要,可以将其抽象为一个方法。

根据addedToBasket变量是否为真,用额外的三元运算符替换按钮中的Add to basket条件。我们还可以基于此属性添加条件类:

<button 
  @click="addToBasket()" 
  :class="(addedToBasket) ? 'isAdded' : ''" 
  :disabled="!variation.quantity"
>
  {{ 
    (variation.quantity) ? 
    ((addedToBasket) ? 'Added to your basket' : 'Add to basket') : 
    'Out of stock'
  }}
</button>

刷新应用并导航到产品,以确保显示正确的文本。将addedToBasket变量更新为true,以确保所有内容都按应有的方式显示。将其设置回false

接下来,在addToBasket()方法中,将属性设置为 true。将项目添加到购物篮时,应更新文本:

addToBasket() {
  this.$store.commit('addToBasket', this);

 this.addedToBasket = true;
}

单击按钮时,文本现在将更新,但它将永远不会重置。之后添加一个setTimeoutJavaScript 函数,经过一段时间后将其设置回false

addToBasket() {
  this.$store.commit('addToBasket', this);

  this.addedToBasket = true;
  setTimeout(() => this.addedToBasket = false, 2000);
}

setTimeout的计时单位为毫秒,因此2000等于两秒。请随意调整和发挥这个数字,因为你认为适合。

最后一个添加是,如果变更被更新或产品被更改,则将该值重置回false。将语句添加到两个watch函数:

watch: {
  variation(v) {
    if(v.hasOwnProperty('image')) {
      this.updateImage(v.image);
    }

    this.addedToBasket = false;
  },

  '$route'(to) {
    this.slug = to.params.slug;
    this.addedToBasket = false;
  }
}

在应用的标题中显示产品计数

在购物车的下一个购物车标题中显示购物车的常见商品编号。为了实现这一点,我们将使用 Vuex getter 计算并返回篮子中的项目数。

打开index.html文件,在应用 HTML 中添加<header>元素,并插入占位符span——一旦我们设置了路由,我们将把它转换为链接。在量程内,输出一个cartQuantity变量:

<div id="app">
  <header>
 <span>Cart {{ cartQuantity }}</span>
 </header>
  <main>
    <router-view></router-view>
  </main>
  <aside>
    <router-view name="sidebar"></router-view>
  </aside>
</div>

导航到您的Vue实例并创建一个包含cartQuantity函数的computed对象:

new Vue({
  el: '#app',

  store,
  router,

 computed: {
 cartQuantity() {

 }
 },

  created() {
    CSV.fetch({url: './data/csv-files/bicycles.csv'}).then(data => {
      this.$store.dispatch('initializeShop', this.$formatProducts(data));
    });
  }
});

如果我们的标题要比购物车链接包含更多的项目,建议将其抽象为一个单独的组件,以保留包含的方法、布局和功能。然而,由于在我们的示例应用中它只具有这一个链接,因此将该函数添加到Vue实例就足够了。

在名为cartQuantity的存储中创建一个新的 getter。作为占位符,返回1。需要使用state来计算数量,因此现在请确保已传递到函数中:

getters: {
  ...

 cartQuantity: (state) => { 
 return 1;
 }
}

返回您的Vue实例并返回 getter 的结果。理想情况下,我们希望在括号中显示basket的计数,但我们只希望在有项目时显示括号。在 computed 函数中,检查此 getter 的结果,如果结果存在,则输出带括号的结果:

cartQuantity() {
  const quantity = this.$store.getters.cartQuantity;
 return quantity ? `(${quantity})` : '';
}

在 Vuex getter 中更改结果应该显示括号中的数字,或者根本不显示。

计算篮子数量

有了显示逻辑,我们现在可以继续计算篮子中有多少物品。我们可以计算basket数组中的项数,但是,这只会告诉我们现在有多少不同的产品,而不会告诉我们是否多次添加相同的产品。

相反,我们需要循环遍历篮子中的每个产品,并将数量相加。创建一个名为quantity的变量,并将其设置为0。循环遍历篮子项目,并将item.quantity变量添加到quantity变量中。最后,返回具有正确总和的变量:

cartQuantity: (state) => {
 let quantity = 0;
 for(let item of state.basket) {
 quantity += item.quantity;
 }
 return quantity;
}

导航到应用并将一些项目添加到您的购物篮中,以验证购物篮计数是否正确计算。

最终确定 Shop Vue 路由 URL

我们现在正处于一个阶段,我们可以最终确定我们商店的 URL,包括创建重定向和结帐链接。回到第 8 章介绍 Vue 路由和加载基于 URL 的组件,我们可以看到我们缺少哪些组件。这些是:

  • /category-重定向至/
  • /product-重定向至/
  • /basket-加载OrderBasket部件
  • /checkout-加载OrderCheckout部件
  • /complete-加载OrderConfirmation部件

在 routes 数组中的适当位置创建重定向。在 routes 数组的底部,为Order组件创建三条新路由:

routes: [
  {
    path: '/',
    name: 'Home',
    ...
  },
  {
 path: '/category',
 redirect: {name: 'Home'}
 },
  {
    path: '/category/:slug',
    name: 'Category',
    ...
  },
  {
 path: '/product',
 redirect: {name: 'Home'}
 },
  {
    path: '/product/:slug',
    name: 'Product',
    component: ProductPage
  },
  {
path: '/basket',
 name: 'Basket',
 component: OrderBasket
 },
 {
 path: '/checkout',
 name: 'Checkout',
 component: OrderCheckout
 },
 {
 path: '/complete',
 name: 'Confirmation',
 component: OrderConfirmation
 },

  {
    path: '/404', 
    alias: '*',
    component: PageNotFound
  }
]

我们现在可以用一个router-link更新我们应用标题中的占位符<span>

<header>
  <router-link :to="{name: 'Basket'}">Cart {{ cartQuantity }}</router-link>
</header>

构建订单流程和 ListProducts 组件

对于检验的三个步骤,我们将在所有三个步骤中使用相同的组件:ListProducts组件。在OrderCheckoutOrderConfirmation组件中,它将处于固定的、不可编辑的状态,而当它处于OrderBasket组件中时,用户需要能够根据需要更新数量和删除项目。

由于我们将在收银台工作,我们需要产品存在于basket阵列中。为了避免我们每次刷新应用时都必须查找产品并将其添加到购物篮中,我们可以通过对商店中的阵列进行硬编码来确保basket阵列中有一些产品。

要实现这一点,请导航到一些产品并将它们添加到您的购物篮中。确保有良好的产品选择和测试数量。接下来,在浏览器中打开 JavaScript 控制台并输入以下命令:

console.log(JSON.stringify(store.state.basket));

这将输出产品数组的字符串。复制此文件并将其粘贴到您的店铺中,替换basket阵列:

state: {
  products: {},
  categories: {},

  categoryHome: {
    title: 'Welcome to the Shop',
    handle: 'home',
    products: [
      ...
    ]
  },

  basket: [{"sku":...}]
},

页面加载时,标题中的购物车计数应更新为您添加的正确项目数。

我们现在可以继续构建我们的签出流程。购物篮中的产品显示比结帐和订单确认屏幕更复杂,因此,不同寻常的是,我们将反向工作。从订单确认页面开始,移动到结帐页面,在我们进入购物篮之前增加了复杂性,增加了退出产品的能力。

订单确认屏幕

订单确认屏幕是订单完成后显示的屏幕。这确认了购买的物品,可能包括预期的交付日期。

OrderConfirmation.js文件中创建一个模板,模板上有一个<h1>和一些与订单完成相关的内容:

const OrderConfirmation = {
  name: 'OrderConfirmation',

  template: `<div>
    <h1>Order Complete!</h1>
    <p>Thanks for shopping with us - you can expect your products within 2 - 3 working days</p>
  </div>`
};

在浏览器中打开应用,将产品添加到购物篮中,并完成订单以确认其工作正常。下一步是包括ListProducts组件。首先,确保ListProducts组件已正确初始化,并具有初始模板:

const ListPurchases = {
  name: 'ListPurchases',

  template: `<table></table>`
};

components对象添加到OrderConfirmation组件中,并包括ListProducts组件。接下来,将其包括在模板中:

const OrderConfirmation = {
  name: 'OrderConfirmation',

  template: `<div>
    <h1>Order Complete!</h1>
    <p>Thanks for shopping with us - you can expect your products within 2 - 3 working days</p>
    <list-purchases />
  </div>`,

 components: {
 ListPurchases
 }
};

再次打开ListPurchases组件,开始显示产品。此组件的默认状态是列出篮子中的产品以及所选的变体。将显示每个产品的价格,如果数量超过一个,则显示价格。最后,将显示总计。

第一步是将篮子列表放入我们的组件中。使用products函数创建一个computed对象。这应返回篮子产品:

const ListPurchases = {
  name: 'ListPurchases',

  template: `<table></table>`,

  computed: {
 products() {
 return this.$store.state.basket;
 }
 }
};

我们现在可以使用篮子中的产品,我们可以在显示所需信息的表格中循环浏览这些产品。这包括缩略图、产品和变体标题、价格、数量和项目的总价。将标题行也添加到表中,以便用户知道该列是什么:

  template: `<table>
    <thead>
      <tr>
        <th></th>
        <th>Title</th>
        <th>Unit price</th>
        <th>Quantity</th>
        <th>Price</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="product in products">
        <td>
          <img 
            :src="product.image.source" 
            :alt="product.image.alt || product.variationTitle"
            width="80"
          >
        </td>
        <td>
          <router-link :to="{name: 'Product', params: {slug: product.handle}}">
            {{ product.title }}
          </router-link><br>
          {{ product.variationTitle }}
        </td>
        <td>{{ product.variation.price }}</td>
        <td>{{ product.quantity }}</td>
        <td>{{ product.variation.price * product.quantity }}</td>
      </tr>
    </tbody>
  </table>`,

请注意,每行的价格只是单价乘以数量。我们现在有一个标准的产品清单,列出用户购买的物品。

使用 Vue 筛选器格式化价格

价格目前是一个整数,因为它在数据中。在产品页面上,我们刚刚添加了一个$符号来表示价格,然而,现在这是使用 Vue 过滤器的绝佳机会。过滤器允许您在不使用方法的情况下操作模板中的数据。过滤器可以链接,通常用于执行单个修改,例如将字符串转换为小写或将数字格式化为货币。

过滤器与管道(|操作员一起使用。例如,如果我们有一个用于小写文本的过滤器,它的使用方式如下:

{{ product.title | lowercase }}

过滤器在组件的filters对象中声明,并接受其前面输出的单个参数。

ListPurchases组件中创建一个filters对象,并在其内部创建一个名为currency()的函数。此函数接受单个参数val,并应返回内部变量:

filters: {
  currency(val) {
    return val;
  }
},

我们现在可以使用这个函数来操作价格整数。将过滤器添加到模板中的单价和总价:

<td>{{ product.variation.price | currency }}</td>
<td>{{ product.quantity }}</td>
<td>{{ product.variation.price * product.quantity | currency }}</td>

您不会注意到浏览器中的任何内容,因为我们尚未处理该值。更新函数,确保数字固定在小数点后两位,且前面有一个$

filters: {
  currency(val) {
    return ' + val.toFixed(2);
  }
},

我们的价格现在格式很好,显示正确。

计算总价

下一个添加到我们的采购清单中的是篮子的总价值。这将需要以类似于我们之前计算的篮数的方式进行计算。

新建computed功能名称:totalPrice。此函数应循环遍历产品并将价格相加,同时考虑任何多个数量:

totalPrice() {
  let total = 0;

  for(let p of this.products) {
    total += (p.variation.price * p.quantity);
  }

  return total;
}

我们现在可以更新模板,以包含总价,确保我们通过currency过滤器:

template: `<table>
  <thead>
    <tr>
      <th></th>
      <th>Title</th>
      <th>Unit price</th>
      <th>Quantity</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    <tr v-for="product in products">
      <td>
        <img 
          :src="product.image.source" 
          :alt="product.image.alt || product.variationTitle"
          width="80"
        >
      </td>
      <td>
        <router-link :to="{name: 'Product', params: {slug: product.handle}}">
          {{ product.title }}
        </router-link><br>
        {{ product.variationTitle }}
      </td>
      <td>{{ product.variation.price | currency }}</td>
      <td>{{ product.quantity }}</td>
      <td>{{ product.variation.price * product.quantity | currency }}</td>
    </tr>
  </tbody>
  <tfoot>
 <td colspan="4">
 <strong>Total:</strong>
 </td>
 <td>{{ totalPrice | currency }}</td>
 </tfoot>
</table>`,

创建订单签出页面

我们的OrderCheckout页面将具有与OrderConfirmation页面类似的组成-然而,在真正的商店中,这将是付款前的页面。此页面允许用户在导航到付款页面之前填写其账单和交付详细信息。复制OrderConfirmation页面并更新标题和信息文本:

const OrderCheckout = {
  name: 'OrderCheckout',

  template: '<div>;
    <h1>Order Confirmation</h1>
    <p>Please check the items below and fill in your details to complete your order</p>
    <list-purchases />
  </div>',

  components: {
    ListPurchases
  }
};

<list-purchases />组件下方,创建一个包含多个字段的表单,以便我们可以收集账单和交付名称及地址。对于本例,只需收集姓名、地址的第一行和邮政编码:

template: '<div>
  <h1>Order Confirmation</h1>
  <p>Please check the items below and fill in your details to complete your order</p>
  <list-purchases />

  <form>
 <fieldset>
 <h2>Billing Details</h2>
 <label for="billingName">Name:</label>
 <input type="text" id="billingName">
 <label for="billingAddress">Address:</label>
 <input type="text" id="billingAddress">
 <label for="billingZipcode">Post code/Zip code:</label>
 <input type="text" id="billingZipcode">
 </fieldset>
 <fieldset>
 <h2>Delivery Details</h2>
 <label for="deliveryName">Name:</label>
 <input type="text" id="deliveryName">
 <label for="deliveryAddress">Address:</label>
 <input type="text" id="deliveryAddress">
 <label for="deliveryZipcode">Post code/Zip code:</label>
 <input type="text" id="deliveryZipcode">
 </fieldset>
 </form>
</div>',

我们现在需要创建一个数据对象,并将每个字段绑定到一个键。要帮助对每个集合进行分组,请为deliverybilling创建一个对象,并使用正确的名称在其中创建字段:

data() {
  return {
    billing: {
      name: '',
      address: '',
      zipcode: ''
    },
    delivery: {
      name: '',
      address: '',
      zipcode: ''
    }
  }
}

v-model添加到每个输入,并将其链接到相应的数据键:

<form>
  <fieldset>
    <h2>Billing Details</h2>
    <label for="billingName">Name:</label>
    <input type="text" id="billingName" v-model="billing.name">
    <label for="billingAddress">Address:</label>
    <input type="text" id="billingAddress" v-model="billing.address">
    <label for="billingZipcode">Post code/Zip code:</label>
    <input type="text" id="billingZipcode" v-model="billing.zipcode">
  </fieldset>
  <fieldset>
    <h2>Delivery Details</h2>
    <label for="deliveryName">Name:</label>
    <input type="text" id="deliveryName" v-model="delivery.name">
    <label for="deliveryAddress">Address:</label>
    <input type="text" id="deliveryAddress" v-model="delivery.address">
    <label for="deliveryZipcode">Post code/Zip code:</label>
    <input type="text" id="deliveryZipcode" v-model="delivery.zipcode">
  </fieldset>
</form>

下一步是创建一个submit方法并整理数据,以便能够将其传递到下一个屏幕。创建一个名为submitForm()的新方法。由于本例中我们不处理付款,我们可以通过以下方法转到确认页面:

methods: {
  submitForm() {
    // this.billing = billing details
    // this.delivery = delivery details

    this.$router.push({name: 'Confirmation'});
  }
}

我们现在可以将submit事件绑定到表单并添加提交按钮。与v-bind:click属性(或@click类似,Vue 允许您使用@submit=""属性将submit事件绑定到方法。

将声明添加到<form>元素,并在表单中创建提交按钮:

<form @submit="submitForm()">
  <fieldset>
    ...
  </fieldset>

  <fieldset>
    ...
  </fieldset>

  <input type="submit" value="Purchase items">
</form>

提交表单时,应用应将您重定向到我们的确认页面。

在地址之间复制详细信息

一些商店的一个特点是能够将送货地址标记为与账单地址相同。我们有几种方法可以做到这一点,您选择如何做到这一点取决于您。目前的选择是:

  • 有一个“复制详细信息”按钮该按钮将详细信息从帐单复制到递送,但不保持同步
  • 设置一个复选框,使两个字段保持同步。选中该复选框将禁用“传送框”字段,但会用账单详细信息填充它们

对于本例,我们将对第二个选项进行编码。

在两个字段集之间创建一个复选框,该字段集通过名为sameAddressv-model绑定到数据对象中的属性:

<form @submit="submitForm()">
  <fieldset>
     ...
  </fieldset>
 <label for="sameAddress">
 <input type="checkbox" id="sameAddress" v-model ="sameAddress">
 Delivery address is the same as billing
 </label>
  <fieldset>
    ...
  </fieldset>

  <input type="submit" value="Purchase items">
</form>

在数据对象中新建一个密钥,默认设置为false

data() {
  return {
    sameAddress: false,

    billing: {
      name: '',
      address: '',
      zipcode: ''
    },
    delivery: {
      name: '',
      address: '',
      zipcode: ''
    }
  }
},

下一步是如果选中复选框,则禁用传递字段。这可以通过基于复选框结果激活disabledHTML 属性来实现。与我们在产品页面上禁用“添加到购物车”按钮的方式类似,将交货字段上的禁用属性绑定到sameAddress变量:

<fieldset>
  <h2>Delivery Details</h2>
  <label for="deliveryName">Name:</label>
  <input type="text" id="deliveryName" v-model="delivery.name" :disabled="sameAddress">
  <label for="deliveryAddress">Address:</label>
  <input type="text" id="deliveryAddress" v-model="delivery.address" :disabled="sameAddress">
  <label for="deliveryZipcode">Post code/Zip code:</label>
  <input type="text" id="deliveryZipcode" v-model="delivery.zipcode" :disabled="sameAddress">
</fieldset>

选中此框将停用字段-使用户无法输入任何数据。下一步是跨两个部分复制数据。由于我们的数据对象是相同的结构,我们可以创建一个watch函数,在选中复选框时将delivery对象设置为与billing对象相同。

sameAddress变量创建一个新的watch对象和函数。如果为true,则将交付对象设置为与计费对象相同:

watch: {
  sameAddress() {
    if(this.sameAddress) {
      this.delivery = this.billing;
    }
  }
}

添加了watch功能后,我们可以在账单地址中输入数据,选中复选框,然后填充送货地址。最好的一点是,它们现在保持同步,因此,如果您更新账单地址,则递送地址会动态更新。当您取消选中该框并编辑帐单地址时,问题就会出现。递送地址仍在更新。这是因为我们已将对象绑定在一起

添加else语句,在复选框未选中时复制账单地址

watch: {
  sameAddress() {
    if(this.sameAddress) {
      this.delivery = this.billing;
    } else {
 this.delivery = Object.assign({}, this.billing);
 }
  }
}

我们现在有一个功能正常的订单确认页面,该页面收集账单和交付详细信息。

创建可编辑的篮子

我们现在需要创造我们的篮子。这需要以与结帐和确认类似的方式显示产品,但需要让用户能够编辑购物篮内容以删除项目或更新数量。

作为起点,打开OrderBasket.js并包括list-purchases组件,正如我们在确认页面上所做的:

const OrderBasket = {
  name: 'OrderBasket',

  template: `<div>
    <h1>Basket</h1>
    <list-purchases />
  </div>`,

  components: {
    ListPurchases
  }
};

接下来我们需要做的是编辑list-purchases组件。为了确保我们能够区分不同的视图,我们将添加一个editable道具。默认设置为false,篮子中设置为true。将prop添加到篮子中的组件:

template: `<div>
  <h1>Basket</h1>
  <list-purchases :editable="true" />
</div>`,

我们现在需要告诉ListPurchases组件接受此参数,以便我们可以在组件内对其进行处理:

props: {
  editable: {
    type: Boolean,
    default: false
  }
},

创建可编辑字段

我们现在有一个道具来决定我们的篮筐是否可以编辑。这允许我们显示删除链接,并使数量成为可编辑框。

ListPurchases组件中数量 1 旁边创建一个新的表格单元格,并使其仅在采购可见时可见。使静态量也隐藏在此状态。在新单元格中,添加一个值设置为“数量”的输入框。我们还将在盒子上绑定一个blur事件。blur事件是一个本机 JavaScript 事件,在输入未聚焦时触发。在模糊状态下,触发updateQuantity方法。此方法应接受两个参数;该事件将包含该特定产品的新数量和 SKU:

<tbody>
  <tr v-for="product in products">
    <td>
      <img 
        :src="product.image.source" 
        :alt="product.image.alt || product.variationTitle"
        width="80"
      >
    </td>
    <td>
      <router-link :to="{name: 'Product', params: {slug: product.handle}}">
        {{ product.title }}
      </router-link><br>
      {{ product.variationTitle }}
    </td>
    <td>{{ product.variation.price | currency }}</td>
    <td v-if="!editable">{{ product.quantity }}</td>
    <td v-if="editable">
      <input 
 type="text"
 :value="product.quantity" 
 @blur="updateQuantity($event, product.sku)"
 >
    </td>
    <td>{{ product.variation.price * product.quantity | currency }}</td>
  </tr>
</tbody>

在组件上创建新方法。此方法应循环遍历产品,找到具有匹配 SKU 的产品,并将数量更新为整数。我们还需要使用结果更新存储,以便在页面顶部更新数量。我们将创建一个通用的变异,它接受完整的basket数组并返回新值,以允许相同的数组用于产品删除。

创建更新数量的变异,并提交一个名为updatePurchases的变异:

methods: {
  updateQuantity(e, sku) {
    let products = this.products.map(p => {
      if(p.sku == sku) {
        p.quantity = parseInt(e.target.value);
      }
      return p;
    });

    this.$store.commit('updatePurchases', products);
  }
}

在存储中,创建将state.basket设置为有效载荷的突变:

updatePurchases(state, payload) {
  state.basket = payload;
}

更新数量现在应该更新项目的总价和页面顶部的购物篮数量。

从购物车中删除项目

下一步是让用户能够从购物车中删除项目。使用点击绑定在ListPurchases组件中创建一个按钮。这个按钮可以去任何你想去的地方-我们的例子显示它作为一个额外的单元格在行尾。将单击操作绑定到名为removeItem的方法。这只需要接受 SKU 的单个参数。将以下内容添加到ListPurchases组件中:

<tbody>
  <tr v-for="product in products">
    <td>
      <img 
        :src="product.image.source" 
        :alt="product.image.alt || product.variationTitle"
        width="80"
      >
    </td>
    <td>
      <router-link :to="{name: 'Product', params: {slug: product.handle}}">
        {{ product.title }}
      </router-link><br>
      {{ product.variationTitle }}
    </td>
    <td>{{ product.variation.price | currency }}</td>
    <td v-if="!editable">{{ product.quantity }}</td>
    <td v-if="editable"><input 
      type="text"
      :value="product.quantity" 
      @blur="updateQuantity($event, product.sku)"
    ></td>
    <td>{{ product.variation.price * product.quantity | currency }}</td>
    <td v-if="editable">
 <button @click="removeItem(product.sku)">Remove item</button>
 </td>
  </tr>
</tbody>

创建removeItem方法。此方法应该过滤basket数组,只返回与传入的 SKU 不匹配的对象。过滤结果后,将结果传递给我们在updateQuantity()方法中使用的相同突变:

removeItem(sku) {
  let products = this.products.filter(p => {
    if(p.sku != sku) {
      return p;
    }
  });

  this.$store.commit('updatePurchases', products);
}

我们可以做的最后一个增强是,如果数量设置为 0,则触发removeItem方法。在updateQuantity方法中,在产品循环之前检查值。如果是0或不存在,则运行removeItem方法-将 SKU 通过:

updateQuantity(e, sku) {
  if(!parseInt(e.target.value)) {
 this.removeItem(sku);
 } else {
    let products = this.products.map(p => {
      if(p.sku == sku) {
        p.quantity = parseInt(e.target.value);
      }
      return p;
    });

    this.$store.commit('updatePurchases', products);
  }
},

完成商店水疗

最后一步是添加从OrderBasket组件到OrderCheckout页面的链接。这可以通过链接到Checkout路线来完成。这样,你的结帐就完成了,你的店铺也完成了!将以下链接添加到篮中:

template: `<div>
  <h1>Basket</h1>
  <list-purchases :editable="true" />
  <router-link :to="{name: 'Checkout'}">Proceed to Checkout</router-link>
</div>`,

总结

做得好!您已经使用Vue.js创建了一个完整的单页应用。您已经了解了如何列出产品及其变体,以及如何向篮子中添加特定变体。您已经了解了如何创建商店过滤器和类别链接,以及如何创建可编辑的购物篮。

与所有事情一样,总有需要改进的地方。你为什么不试试这些想法呢?

  • 使用localStorage保持购物篮-因此添加到购物篮中的产品将在访问和用户按刷新之间保留
  • 基于篮子中产品的重量属性计算装运使用 switch 语句创建带
  • 允许将没有变化的产品从类别列表页面添加到篮子中
  • 指示在类别页面上的该变体上筛选时哪些产品的商品缺货
  • 任何你自己的想法!