本章概要

  • 购物车

    • 购物车状态管理配置
    • 购物车组件
  • 结算页面
  • 用户管理
    • 用户状态管理配置
    • 用户注册组件
    • 用户登录组件

17.8 购物车

在一个电商网站中,购物车在很多页面都需要用到,因此非常适合放在 Vuex 的 store 中进行集中管理。在本项目中,采用模块化的方式管理应用中不同的状态。

17.8.1 购物车状态管理配置

在项目的 store 目录下新建 modules 文件夹,在该文件下新建 cart.js。如下:

store/modules/cart.js

const state = {items: []
}
//mutations
const mutations = {//添加商品到购物车中pushProductToCart(state, { id, imgUrl, title, price, quantity }) {if (!quantity)quantity = 1;state.items.push({ id, imgUrl, title, price, quantity });},//增加商品数量incrementItemQuantity(state, { id, quantity }) {let cartItem = state.items.find(item => item.id == id);cartItem.quantity += quantity;},//用于清空购物车setCartItems(state, { items }) {state.items = items},//删除购物车中的商品deleteCartItem(state, id) {let index = state.items.findIndex(item => item.id === id);if (index > -1)state.items.splice(index, 1);}
}//getters
const getters = {//计算购物车中所有商品的总价cartTotalPrice: (state) => {return state.items.reduce((total, product) => {return total + product.price * product.quantity}, 0)},//计算购物车中单项商品的价格cartItemPrice: (state) => (id) => {if (state.items.length > 0) {const cartItem = state.items.find(item => item.id === id);if (cartItem) {return cartItem.price * cartItem.quantity;}}},//获取购物车中商品的数量itemsCount: (state) => {return state.items.length;}
}//actions
const actions = {//增加任意数量的商品到购物车addProductToCart({ state, commit },{ id, imgUrl, title, price, inventory, quantity }) {if (inventory > 0) {const cartItem = state.items.find(item => item.id == id);if (!cartItem) {commit('pushProductToCart', { id, imgUrl, title, price, quantity })} else {commit('incrementItemQuantity', { id, quantity })}}}
}export default {namespaced: true,state,mutations,getters,actions
}

items 数组用于保存购物车中所有商品信息的状态属性。
接下来,编辑 store 目录下的 index.js ,导入 cart 模块。如下:

store/index.js

import { createStore } from 'vuex'
import cart from './modules/cart'
import createPersistedState from "vuex-persistedstate"export default new Vuex.Store({modules: {cart},plugins: [createPersistedState()]
})

在刷新浏览器窗口时,store 中存储的状态信息会被重置,这样就会导致加入购物车中的商品信息丢失。所以一般会选择一种浏览器端持久存储方案解决这个问题,比较常见且简单的方案就是 localStorage ,保存在 store 中的状态信息也要同步加入 localStorage ,在刷新浏览器窗口前,或者用用户重新访问网站时,从 localStorage 中读取状态信息保存到 store 中。
在整个应用期间,需要考虑各种情况下 store 与 localStorage 数据同步的问题,这比较麻烦。为此,可以使用一个第三方的插件解决 store 与 localStorage 数据同步的问题,即 vuex-persistedstate 插件。
首先安装 vuex-persistedstate 插件,在 Visual Studio Code 的终端窗口中执行以下命令进行安装。

npm install vuex-persistedstate -S

vuex-persistedstate 插件的使用非常简单,只需要两句代码就可以实现 store 的持久化存储,这会将整个 store 的状态以 vuex 为键名存储到 localStorage 中。
如果只想持久化存储 store 中的部分状态信息,那么可以在调用 createPersistedState() 方法时传递一个选项对象,在该选项对象的 reducer() 函数中返回要存储的数据。例如:

plugins:[createPersistedState({reducer (data){return {// 设置只存储 cart 模块中的状态cart:data.cart,// 或者设置只存储 cart 模块中的 items 数据// products:data.cart.items}}
})]

reducer() 函数的 data 参数是完整的 state 对象。
如果想改变底层使用的存储机制,如使用 sessioniStorage,那么可以在选项对象中通过 storage 指定。代码如下:

plugins:[createPersistedState({reducer (data){storage:window.sessionStorage,...}
})]

配置好 Vuex 的状态管理后,就可以开始编写购物车组件了。

17.8.2 购物车组件

在 views 目录下新建 ShoppingCart。如下:

views/ShoppingCart.vue

<template><div class="shoppingCart"><table><tr><th></th><th>商品名称</th><th>单价</th><th>数量</th><th>金额</th><th>操作</th></tr><tr v-for="book in books" :key="book.id"><td><img :src="book.imgUrl"></td><td><router-link :to="{ name: 'book', params: { id: book.id } }" target="_blank">{{ book.title }}</router-link></td><td>{{ currency(book.price) }}</td><td><button @click="handleSubtract(book)">-</button>{{ book.quantity }}<button @click="handleAdd(book.id)">+</button></td><td>{{ currency(cartItemPrice(book.id)) }}</td><td><button @click="deleteCartItem(book.id)">删除</button></td></tr></table><p><span><button class="checkout" @click="checkout">结算</button></span><span>总价:{{ currency(cartTotalPrice) }}</span></p></div>
</template><script>
import { mapGetters, mapState, mapMutations } from 'vuex'
export default {name: "ShoppingCart",inject: ['currency'],computed: {...mapState('cart', {books: 'items'}),...mapGetters('cart', ['cartItemPrice','cartTotalPrice'])},methods: {itemPrice(price, count) {return price * count;},...mapMutations('cart', ['deleteCartItem','incrementItemQuantity','setCartItems']),handleAdd(id) {this.incrementItemQuantity({ id: id, quantity: 1 });},handleSubtract(book) {let quantity = book.quantity - 1;if (quantity <= 0) {this.deleteCartItem(book.id);}elsethis.incrementItemQuantity({ id: book.id, quantity: -1 });},checkout() {this.$router.push("/check");}}
};
</script>
<style scoped>
.shoppingCart {text-align: center;margin-left: 45px;width: 96%;margin-top: 70px;
}.shoppingCart table {border: solid 1px black;width: 100%;background-color: #eee;}.shoppingCart th {height: 50px;
}.shoppingCart th,
.shoppingCart td {border-bottom: solid 1px #ddd;text-align: center;
}.shoppingCart span {float: right;padding-right: 15px;
}.shoppingCart img {width: 60px;height: 60px;
}.shoppingCart .checkout {float: right;width: 60px;height: 30px;margin: 0;border: none;color: white;background-color: red;cursor: pointer;
}
</style>

ShoppingCart 组件提供了两种方式删除购物车中的某项商品:
(1)单击“删除”按钮,将直接删除购物车中的该商品
(2)用户单击数量下的减号按钮时,如果判断数量减一后为零,则删除该商品

17.9 结算页面

在购物车页面中单击“结算”按钮,则进入结算页面,结算页面再一次列出购物车中的所有商品,不同的是,在结算页面不能再对商品进行修改。
在 views 目录下新建 Checkout.vue。如下:

views/checkout.vue

<template><div class="shoppingCart"><h1 v-if="success">{{ msg }}</h1><table><caption>商品结算</caption><tr><th></th><th>商品名称</th><th>单价</th><th>数量</th><th>金额</th></tr><tr v-for="book in books" :key="book.id"><td><img :src="book.imgUrl"></td><td><router-link :to="{ name: 'book', params: { id: book.id } }" target="_blank">{{ book.title }}</router-link></td><td>{{ currency(book.price) }}</td><td>{{ book.quantity }}</td><td>{{ currency(cartItemPrice(book.id)) }}</td></tr></table><p><span><button class="pay" @click="pay">付款</button></span><span>总价:{{ currency(cartTotalPrice) }}</span></p></div>
</template><script>
import { mapGetters, mapState, mapMutations } from 'vuex'
export default {name: "Checkout",data() {return {success: false,msg: '付款成功!'};},inject: ['currency'],computed: {...mapState('cart', {books: 'items'}),...mapGetters('cart', ['cartItemPrice','cartTotalPrice'])},methods: {itemPrice(price, count) {return price * count;},...mapMutations('cart', ['setCartItems']),pay() {this.setCartItems({ items: [] });this.success = true;}}
};
</script>
<style scoped>
.shoppingCart {text-align: center;margin-left: 45px;width: 96%;margin-top: 70px;
}.shoppingCart h1 {color: red;
}.shoppingCart table {border: solid 1px black;width: 100%;background-color: #eee;}.shoppingCart table>caption {font-size: 1.5em;font-weight: bold;margin: 5px 0 8px 0;
}.shoppingCart th {height: 50px;
}.shoppingCart th,
.shoppingCart td {border-bottom: solid 1px #ddd;text-align: center;
}.shoppingCart span {float: right;padding-right: 15px;
}.shoppingCart img {width: 60px;height: 60px;
}.shoppingCart .pay {float: right;width: 60px;height: 30px;margin: 0;border: none;color: white;background-color: red;cursor: pointer;
}
</style>

在线支付涉及各个支付平台或银联的调用接口,所以本项目的购物车流程到这一步就结束了,当用户单击“付款”按钮时,只是简单地清空购物车,稍后提示用户“付款成功”。

17.10 用户管理

在实际场景中,当用户提交购物订单准备结算时,系统会判断用户是否已经登录,如果没有登录,会提示用户先进行登录,本节实现用户注册和用户登录组件。

17.10.1 用户状态管理配置

用户登录后的状态需要保存,不仅可以用于向用户显示欢迎信息,还可以用于对受保护的资源进行权限验证。同样,用户的状态存储也使用 Vuex 管理。
在 store/modules 目录下新建 user.js 。如下:

store/modules/user.js

const state = {user: null
}
// mutations
const mutations = {saveUser(state, { username, id }) {state.user = { username, id }},deleteUser(state) {state.user = null;}
}export default {namespaced: true,state,mutations,
}

对于前端,存储用户名和用户 ID 已经足以,像用户中心等功能的实现,是需要重新向服务端去请求数据的。
编辑 store/index.js 文件,导入 user 模块,并在 modules 选项下进行注册。如下:

store/index.js

import { createStore } from 'vuex'import cart from './modules/cart'
import user from './modules/user'
import createPersistedState from "vuex-persistedstate"export default createStore({modules: {cart,user},plugins: [createPersistedState()]
})

17.10.2 用户注册组件

当用户单击 Header 组件中的 “注册”链接时,将跳转到用户注册页面。
在 components 目录下新建 UserRegister.vue 。如下:

components/UserRegister.vue

<template><div class="register"><form><div class="lable"><label class="error">{{ message }}</label><input name="username" type="text" v-model="username" placeholder="请输入用户名" /><input type="password" v-model.trim="password" placeholder="请输入密码" /><input type="password" v-model.trim="password2" placeholder="请输入确认密码" /><input type="tel" v-model.trim="mobile" placeholder="请输入手机号" /></div><div class="submit"><input type="submit" @click.prevent="register" value="注册" /></div></form></div>
</template><script>
import { mapMutations } from 'vuex';
export default {name: "UserRegister",props: [""],data() {return {username: "",password: "",password2: "",mobile: "",message: ''};},watch: {username(newVal) {// 取消上一次请求if (newVal) {this.cancelRequest();this.axios.get("/user/" + newVal, {cancelToken: new this.axios.CancelToken(cancel => this.cancel = cancel)}).then(response => {if (response.data.code == 200) {let isExist = response.data.data;if (isExist) {this.message = "该用户名已经存在";} else {this.message = "";}}}).catch(error => {if (this.axios.isCancel(error)) {//如果是请求被取消产生的错误,输出取消请求的原因console.log("请求取消:", error.message);//alert(error.message);//throw new Error("请求取消:" + error.message)} else {//处理错误console.log(error);//throw new Error(error.message)}});}}},methods: {register() {this.message = '';if (!this.checkForm())return;this.axios.post("/user/register",{ username: this.username, password: this.password, mobile: this.mobile }).then(response => {if (response.data.code === 200) {this.saveUser(response.data.data);this.username = '';this.password = '';this.password2 = '';this.mobile = '';this.$router.push("/");} else if (response.data.code === 500) {this.message = "用户注册失败";}}).catch(error => {alert(error.message)})},cancelRequest() {if (typeof this.cancel === "function") {this.cancel("终止请求");}},checkForm() {if (!this.username || !this.password || !this.password2 || !this.mobile) {this.$msgBox.show({ title: "所有字段不能为空" });return false;}if (this.password !== this.password2) {this.$msgBox.show({ title: "密码和确认密码必须相同" });return false;}return true;},...mapMutations('user', ['saveUser'])},
};
</script>
<style scoped>
.register {margin: 5em auto 0;width: 44%;
}.register input {padding: 15px;width: 94%;font-size: 1.1em;margin: 18px 0px;color: gray;float: left;cursor: pointer;font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;outline: none;font-weight: 600;margin-left: 3px;background: #eee;transition: all 0.3s ease-out;border: solid 1px #ccc;
}.register input:hover {color: rgb(180, 86, 9);border-left: solid 6px #40A46F;
}.register .submit {padding: 5px 4px;text-align: center;
}.register input[type="submit"] {padding: 17px 17px;color: #fff;float: right;font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;background: #40a46f;border: solid 1px #40a46f;cursor: pointer;font-size: 18px;transition: all 0.5s ease-out;outline: none;width: 100%;
}.register .submit input[type="submit"]:hover {background: #07793d;border: solid 1px #07793d;
}.register .error {color: red;font-weight: bold;font-size: 1.1em;
}
</style>

说明:

红框处在这里实现了一个功能,当用户输入用户名时,实时去服务端检测该用户名是否已经存在,如果存在,则提示用户,这是通过 Vue 的监听器来实现的。
不过由于 v-model 指令内部实现机制的原因(对于文本输入框,默认绑定的是 input 事件),如果用户快速输入或快速用退格键删除用户名时,监听器将触发多次,由此导致频繁地向服务端发起请求。为了解决这个问题,可以利用 axios 的 cancel token 取消重复的请求。
使用 axios 发送请求时,可以传递一个配置对象,在配置对象中使用 cacelToken 选项,通过传递一个 executor() 函数到 CancelToken 的构造函数中创建 cancel token。
将 cancel() 函数保存为组件实例的方法,之后如果要取消请求,调用 this.cancel() 即可。cancel() 函数可以接收一个可选的消息字符串参数,用于给出取消请求的原因。同一个 cancel token 可以取消多个请求。
在发生错误时,可以在 catch() 方法中使用 this.axios.isCancel(error) 判断该错误是否是由取消请求而引发的。
当然,这里也可以通过修改 v-model 的监听事件为 change 解决快速输入和删除导致的重复请求问题,只需要给 v-model 指令添加 .lazy 修饰符即可。
用户名是否已注册的判断,请求的服务端数据接口如下:
http://111.229.37.167/api/user/{用户名}
返回的数据结构如下:

{"code": 200,"data": true  //如果要注册的用户名存在,则返回 false
}

用户注册请求的服务端数据接口如下:
http://111.229.37.167/api/user/register。
需要采用 Post() 方法向该接口发起请求,提交的数据是一个 JSON 格式的对象,该对象要包含 username、password 和 mobile 三个字段。
返回的数据结构如下:

{"code":200,"data":{"id":18,"username":"小鱼儿","password":"1234","mobile":"13222222222"}
}

实际开发时,服务端不把密码返回给前端,如果前端需要用到密码,则可以采用加密形式传输。
当用户注册成功后,将用户名和 ID 保存到 store 中,并跳转到根目录下,即网站的首页。然后 Header 组件会自动渲染出用户名,显示欢迎信息。

17.10.3 用户登录组件

当用户单击 Header 组件中的“登录”链接时,将跳转到用户登录页面。
在 components 目录下新建 UserLogin.vue 。如下:

components/UserLogin.vue

<template><div class="login"><div class="error">{{ message }}</div><form><div class="lable"><input name="username" type="text" v-model.trim="username" placeholder="请输入用户名" /><input type="password" v-model.trim="password" placeholder="请输入密码" /></div><div class="submit"><input type="submit" @click.prevent="login" value="登录" /></div></form></div>
</template><script>
import { mapMutations } from 'vuex';
export default {name: "UserLogin",data() {return {username: '',password: '',message: ''};},methods: {login() {this.message = '';if (!this.checkForm())return;this.axios.post("/user/login",{ username: this.username, password: this.password }).then(response => {if (response.data.code === 200) {this.saveUser(response.data.data);this.username = '';this.password = '';//如果存在查询参数if (this.$route.query.redirect) {const redirect = this.$route.query.redirect;//跳转至进入登录页前的路由this.$router.replace(redirect);} else {// 否则跳转至首页this.$router.replace('/');}} else if (response.data.code === 500) {this.message = "用户登录失败";} else if (response.data.code === 400) {this.message = "用户名或密码错误";}}).catch(error => {console.log(error.message);})},...mapMutations('user', ['saveUser']),checkForm() {if (!this.username || !this.password) {this.$msgBox.show({ title: "用户名和密码不能为空" });return false;}return true;}}
};
</script>
<style scoped>
.login {margin: 5em auto 0;width: 44%;
}.login input {padding: 15px;width: 94%;font-size: 1.1em;margin: 18px 0px;color: gray;float: left;cursor: pointer;font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;outline: none;font-weight: 600;margin-left: 3px;background: #eee;transition: all 0.3s ease-out;border: solid 1px #ccc;
}.login input:hover {color: rgb(180, 86, 9);border-left: solid 6px #40A46F;
}.login {padding: 5px 4px;text-align: center;
}input[type="submit"] {padding: 17px 17px;color: #fff;float: right;font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;background: #40a46f;border: solid 1px #40a46f;cursor: pointer;font-size: 18px;transition: all 0.5s ease-out;outline: none;width: 100%;
}.submit input[type="submit"]:hover {background: #07793d;border: solid 1px #07793d;
}.login .error {color: red;font-weight: bold;font-size: 1.1em;
}
</style>

用户登录组件并不复杂,值得一提的就是在用户登录后需要跳转到进入登录页面前的路由,这会让用户体验更好,实现方式已经在 14.10.1 小节介绍过了,本项目也是利用 beforeEach() 注册的全局前置守卫保存用户登录前的路由路径,可以参看 17.11 节。
用户登录请求的数据接口如下:
http://111.229.37.167/api/user/login
同样是以 Post() 方法发起请求,提交的数据是一个 JSON 格式的对象,该对象要包含 username 和 password 两个字段。
返回的数据格式与用户注册返回的数据格式相同。

十七、网上商城项目(5)相关推荐

  1. 十七、网上商城项目(1)

    本章概要 脚手架项目搭建 安装与配置 axios 首页 页面头部组件 头部搜索框组件 头部购物车组件 头部组件 本章结合前面所学知识,开发一个网上商城项目. 成品如下 17.1 脚手架项目搭建 选择好 ...

  2. 【SSH网上商城项目实战16】Hibernate的二级缓存处理首页的热门显示

    转自:https://blog.csdn.net/eson_15/article/details/51405911 网上商城首页都有热门商品,那么这些商品的点击率是很高的,当用户点击某个热门商品后需要 ...

  3. java web网上商城项目实战与源码

    java web网上商城项目实战与源码 点击这里,轻松完成毕设https://x-x.fun/i/AAbf595445aBT

  4. 【SSH网上商城项目实战21】从Demo中看易宝支付的流程

    这一节我们先写一个简单点的Demo来测试易宝支付的流程,熟悉这个流程后,再做实际的开发,因为是一个Demo,所以我没有考虑一些设计模式的东西,就是直接实现支付功能.实现支付功能需要易宝给我们提供的AP ...

  5. 商城项目中信息的集合怎么存储_网上商城项目_数据库设计说明书.doc

    秘密 第 PAGE 2 页 共 NUMPAGES 10 页 信用卡网上商城项目 数据库设计说明书 文件修订历史 修订时间 修订说明 作者 审核 2010.08.05 编写数据字典 谭星佑 曾玉贞 20 ...

  6. Django框架学习之网上商城项目一(后端设计)

    目录 一.项目需求分析 1.项目介绍 1.技术难点 2.系统功能 3.项目环境 4.后台管理页面 二.数据库模型设计 一.准备工作 二.用户认证数据库模型设计 1. app/users/models. ...

  7. 【SSH网上商城项目实战】之环境搭建填坑

    此篇主要是记录我在从零开始走一遍倪升武大神的[SSH网上商城项目实战]过程中遇到的一些坑并记录解决方法.关于这个项目,大家可以去倪升武的博客学习了解,SSH网上商城项目实战请戳倪升武的项目实战专题. ...

  8. 【SSH网上商城项目实战01】整合Struts2、Hibernate4.3和Spring4.2

    转自:https://blog.csdn.net/eson_15/article/details/51277324 今天开始做一个网上商城的项目,首先从搭建环境开始,一步步整合S2SH.这篇博文主要总 ...

  9. 【SSH网上商城项目实战20】在线支付平台的介绍

    之前已经完成了首页的显示,用户添加购物车,确认订单等功能,下面就是支付功能的开发了.用户确认了订单后会直接跳转到支付页面进行在线支付,在线支付需要第三方的接口,这一节主要介绍一些关于第三方支付的内容, ...

  10. java servlet项目源码下载_java网上商城项目源码(jsp.servlet+javabean+mysql+jdbc)

    [实例简介] 网上商城所有基本功能实现. 包含所有图片等资源 包含数据库创建脚步 开发环境 jdk1.7 myeclipse10 tomcat6.0 mysql 5 [实例截图] [核心代码] 325 ...

最新文章

  1. 不容错过的Pandas小技巧:万能转格式、轻松合并、压缩数据,让数据分析更高效...
  2. 未读消息(小红点),前端 与 RabbitMQ 实时消息推送实践,贼简单~
  3. websocket实现方式
  4. [置顶] 状态压缩DP 简单入门题 11题
  5. mysql+inser+select_解析MySQL中INSERT INTO SELECT的使用
  6. calender获取日期前几月_java获取当前时间和前一天日期(实现代码)
  7. 如何应对当下的 996?
  8. java代码实现 取放_java大对象存取的简单实现的代码
  9. 12帧跑步动画分解图_跑步动画原理讲解
  10. 诺奖经济大师,数学天才赌徒,和“神秘的股市财富公式”
  11. Python原生服务端签名生成请求订单信息「orderString」
  12. 10. Joining Data with dplyr in R
  13. 分享20个高质量的学习网站!
  14. 20162312Java结对编程之挑战出题
  15. 损失函数 - 交叉熵损失函数
  16. cf_332b - Maximum Absurdity
  17. 广汽丰田-“饮水思源”活动专题网站
  18. 极简栈溢出程序逆向分析
  19. 马上加薪!测试,你的职业发展...
  20. sunday算法c语言实现,C / C++学习笔记:实现Sunday算法

热门文章

  1. 电信联通共享检测技术及防封杀
  2. 《人类简史》1.0-三概念:肌肉+两性+恶性循环案例(大花猫冯夏)
  3. ES--IK分词器安装
  4. 进程调度算法——C++实现 [ FCFS,SJF,HPR,HRN + 开源代码 + 详细解析 ]
  5. 在微信中分享页面之调用微信sdk接口
  6. 2018互联网金融公司排名——Top100(附完整榜单)
  7. 第一阶段项目(2 body)
  8. MATLAB中内置的BP神经网络函数 help newff翻译【学习笔记】
  9. 高斯定理证明(HTML)
  10. SF25 | 日内交易策略开发(一)黄金日内交易模型