藤原拓鞋的博客
Vue是目前前端领域教火的框架,被广泛应用,插件和依赖包生态强大,诞生了许多大型应用
Vue 官网
学习了 vue 之后,利用所学知识搭建了个简易的商城,用到了 vue 开发中常用的技巧以及扩展包,通过这个小项目加深了对 vue 的学习
但因为没有做对应的后台,所以用到了浏览器本地储存作为临时仓库的模拟,以达到计算商品剩余量的目的
同样,对于登录和注册功能,也是通过 localStorage 暂存用户名与密码,所以只能暂时有一个用户~~
后台系统的话,以后我会用 node 或者 django 搭建出来的,这里先给出本前端项目地址:
https://github.com/li-car-fei/vue_shopping
如上所述,本项目暂用 localStorage 存储用户名和密码,以达到登录和注册功能
首先,我们先需要配置路由
npm i vue-router --save
安装包,然后在 index.js 中引入并且使用:
import VueRouter from "vue-router";
import Routers from "./router";
Vue.use(VueRouter);
//路由配置
const RouterConfig = {
//使用H5 history模式
mode: "history",
routes: Routers,
};
const router = new VueRouter(RouterConfig);
//跳转前设置title
router.beforeEach((to, from, next) => {
window.document.title = to.meta.title;
next();
});
//跳转后设置scroll为原点
router.afterEach((to, from, next) => {
window.scrollTo(0, 0);
});
记得最后一定要在 new Vue()
是添加 router 路由器:
const app = new Vue({
el: "#app",
router, // 添加router
store, // 添加vuex
//render代替app声明
render: (h) => {
return h(App);
},
});
配置的路由,写在了 router.js 中,如下:
//meta中存有title,用作跳转路由时改变title标题
const routers = [
{
path: "/list",
meta: {
title: "商品列表",
},
component: (resolve) => require(["./views/list.vue"], resolve),
},
{
path: "/product/:id",
meta: {
title: "商品详情",
},
component: (resolve) => require(["./views/product.vue"], resolve),
},
{
path: "/cart",
meta: {
title: "购物车",
},
component: (resolve) => require(["./views/cart.vue"], resolve),
},
{
path: "/login/:loginStatus",
meta: {
title: "登录注册",
},
component: (resolve) => require(["./views/login.vue"], resolve),
},
{
path: "*",
redirect: "/login/login", //重定向路由至登录页面,且传入的loginStatus参数是 login
},
];
export default routers;
想了解vue-router
源码解析,可看我另一篇博客:vue-router 源码解析
注意两点:
redirect
属性即可关于登录界面,最关键的是在 created 生命周期准确地判断状态
created(){
//获取路由中的传来的参数loginStatus
if(this.$route.params.loginStatus === 'logout'){
//如果是退出登录跳转过来,清空缓存
window.localStorage.clear();
//直接commit触发mutations,设置state的loginstatus为false
this.$store.commit('getLoginStatus', false);
return;
}
//获取state中登录状态
const loginStatus = this.$store.state.loginStatus;
if(loginStatus){
//如果是已经登录的用户,url跳到list
this.login = false;
this.register = false;
window.alert('您已经是登录状态')
window.location.href = '/list'
}
}
关于登录注册界面的其他操作及参数,请自行查看,注释都较为清楚
首先,我们介绍除了 router
以外,另一个全局共用的变量 vuex
vuex
可以将全局使用的变量进行统一管理,在 index.js 中 new Vue()
时进行注册即可,它可以帮组我们更好地管理全局共用的数据,防止出现多组件之间传变量,传方法的复杂应用
通过vuex
的dispatch
,commit
,getters
机制,对状态进行统一管理,想了解vuex
源码解析,可以看我的另一篇博客:vuex 源码解析
以下是 index.js 中配置的 vuex
//配置Vuex状态管理
const store = new Vuex.Store({
state: {
//商品列表信息
productList: [],
//购物车数据,数组形式,数据元素为对象(商品id,购买数量count)
cartList: [],
//当前用户账号
username: window.localStorage.getItem("username"),
//登录状态 !!转换为bool值
loginStatus: !!window.localStorage.getItem("loginStatus"),
},
getters: {
//返回品牌队列
brands: (state) => {
//获取所有product的brand并形成数组
const brands = state.productList.map((item) => item.brand);
//数组去重并返回
return util.getFilterArray(brands);
},
//返回颜色队列
colors: (state) => {
//获取所有product的color并形成数组
const colors = state.productList.map((item) => item.color);
//数组去重并返回
return util.getFilterArray(colors);
},
},
//mutations更新state,更新视图
mutations: {
//初始化时,添加商品列表
setProductList(state, data) {
state.productList = data;
},
//添加商品到购物车
addCart(state, obj) {
const id = obj.id;
//获取具体的product,以便修改其stocks库存
let product = state.productList.find((item) => item.id === id);
if (obj.inCart) {
//此商品在购物车中,数量+1,对应库存-1
let Added = state.cartList.find((item) => item.id === id);
Added.count++;
product.stocks--;
} else {
//此商品不在购物车中,加入到购物车中
state.cartList.push({
id: id,
count: 1,
});
//对应库存-1
product.stocks--;
}
},
//修改购物车商品数量
editCartCount(state, payload) {
//浅拷贝,所以修改product中count可以直接修改carList,productList
//先获取购物车与商品列表对应商品对象
const product_L = state.productList.find(
(item) => item.id === payload.id
);
const product_C = state.cartList.find((item) => item.id === payload.id);
//修改数量与对应库存
product_L.stocks -= payload.count;
product_C.count += payload.count;
},
//删除某个购物车商品
deleteCart(state, id) {
//获取购物车中此商品的数量,用于归还库存
const count = state.cartList.find((item) => item.id === id).count;
//findIndex,用于切割购物车列表
const index = state.cartList.findIndex((item) => item.id === id);
//获取对应的商品在productList中的对象
const product = state.productList.find((item) => item.id === id);
//修改库存
product.stocks += count;
//修改购物车列表
state.cartList.splice(index, 1);
},
//添加评论
addComment(state, comment_obj) {
const product = state.productList.find(
(item) => item.id === comment_obj.product_id
);
console.log(product);
const content = comment_obj.content;
const username = state.username;
product.comments.push({
username: username,
content: content,
});
},
//清空购物车即购买,无需恢复库存
emptyCart(state) {
state.cartList = [];
},
//username是commit来的用户名,修改state中用户名
getUser(state, username) {
state.username = username;
},
//flag是commit过来的登录状态,修改state中的状态
getLoginStatus(state, flag) {
state.loginStatus = flag;
},
},
actions: {
//ajax请求商品列表,暂且使用setTimeout
getProductList(context) {
setTimeout(() => {
//设置state.productList 为引入的product_data
context.commit("setProductList", product_data);
}, 150);
},
//购买
buy(context) {
//模拟ajax请求服务端响应后再清空购物车
return new Promise((resolve) => {
setTimeout(() => {
context.commit("emptyCart");
resolve("购买成功");
}, 150);
});
},
//添加商品到购物车
add(context, id) {
//首先查看登录状态,若未登录则跳转到登陆界面
if (!context.state.loginStatus) {
window.alert("请先登录,为您跳转到登录界面");
//路由跳转
window.location.href = "/login/login";
return undefined;
}
//先查看购物车中是否有该商品
const isAdded = context.state.cartList.find((item) => item.id === id);
//查看对应商品的库存
const stocks = context.state.productList.find((item) => item.id === id)
.stocks;
return new Promise((resolve, reject) => {
if (stocks) {
//有库存,修改数据
//如果购物车中不存在商品,设置count为1,存在count++
if (isAdded) {
context.commit("addCart", {
inCart: true,
id: id,
});
resolve("加一");
} else {
context.commit("addCart", {
inCart: false,
id: id,
});
resolve("加入购物车");
}
} else {
//无库存,返回信息
reject("库存不足");
}
});
},
//修改购物车数量
editCart(context, payload) {
//先查看对应商品的库存
const stocks = context.state.productList.find(
(item) => item.id === payload.id
).stocks;
return new Promise((resolve, reject) => {
//如果是+1,根据库存返回结果与修改
if (payload.count > 0) {
if (stocks) {
//有库存
context.commit("editCartCount", payload);
resolve("修改成功(+1)");
} else {
//无库存
reject("库存不足");
}
} else {
//如果是-1,直接修改即可
resolve("修改成功(-1)");
context.commit("editCartCount", payload);
}
});
},
},
});
同样的,要记得在 new Vue()
的时候添加配置
以上,我们可以看到项目的基本流程,包括添加购物车,清空购物车等等
可以看到在 add(context, id)
方法中,需要先判断登录状态,再进行库存的判断,是否加购的判断,最终返回一个 Promise 对象或者跳转路由
同样的,在 editCart(context, payload)
方法中,也是返回一个 Promise 对象,判断库存然后resolve()
或者reject()
而在商品组件添加商品和在购物车页面中修改数量时,通过如下方式调用:
//添加购物车
handleAddCart(){
//增加1件此商品至购物车,返回一个Promise
this.$store.dispatch('add', this.id).then(text=>{
//console.log(text)
},error=>{
window.alert(error)
})
},
//修改购物车数量,返回一个Promise
handleCount(index, count){
//判断是否只剩一件
if(count < 0 && this.cartList[index].count === 1) return;
this.$store.dispatch('editCart', {
id: this.cartList[index].id,
count: count
}).then(text=>{
//console.log(text)
},error=>{
window.alert(error)
})
},
同理,对于清空购物车的操作,也遵循上书方式
//清空购物车
handleOrder(){
//由于dispatch buy action 返回一个promise对象,用then()调用
this.$store.dispatch('buy').then(text => {
window.alert(text);
})
},
主页中,还有颜色,品牌的筛选,有销量和价格的排序,我认为自已的实现方法是较为笨重的,设置了filterBrand
,filterColor
作为筛选品牌和颜色的数组,order
作为排序选项,然后通过两层 forEach
进行筛选,最终得到筛选,排序后的商品数组。希望大家有更好的实现方案能够私聊我或者直接在评论区分享~~
list.vue 定义:
export default {
//使用组件product
components: { Product },
computed: {
list() {
//从Vuex获取商品列表信息
return this.$store.state.productList;
},
brands() {
//从getters中获取所有商品的品牌
return this.$store.getters.brands;
},
colors() {
return this.$store.getters.colors;
},
filteredAndOrderedList() {
//拷贝原数组
let list = [...this.list];
//品牌过滤,分别forEach遍历productlist与品牌筛选队列,匹配的product暂存push
if (this.filterBrand.length) {
let list1 = [];
list.forEach((item) => {
this.filterBrand.forEach((brand) => {
if (item.brand == brand) {
list1.push(item);
}
});
});
list = list1;
}
//颜色过滤,分别forEach遍历productlist与颜色筛选队列,匹配的product暂存push
if (this.filterColor.length) {
let list1 = [];
list.forEach((item) => {
this.filterColor.forEach((color) => {
if (item.color == color) {
list1.push(item);
}
});
});
list = list1;
}
//排序
if (this.order !== "") {
if (this.order === "sales") {
list = list.sort((a, b) => b.sales - a.sales);
} else if (this.order === "cost-desc") {
list = list.sort((a, b) => b.cost - a.cost);
} else if (this.order === "cost-asc") {
list = list.sort((a, b) => a.cost - b.cost);
}
}
return list;
},
},
data() {
return {
//品牌过滤选中的品牌
filterBrand: [],
//颜色过滤选中的颜色
filterColor: [],
//排序过滤选中的方式:
//cost-desc价格降序
//cost-asc价格升序
//sales销量
order: "",
};
},
methods: {
//品牌筛选
handleFilterBrand(brand) {
//点击品牌过滤,再次点击取消
let index = this.filterBrand.findIndex((item) => item == brand);
if (index > -1) {
//若已经筛选的品牌包含此品牌,则删除
this.filterBrand.splice(index, 1);
} else {
//将此品牌加入到品牌筛选队列中
this.filterBrand.push(brand);
}
},
//颜色筛选
handleFilterColor(color) {
//点击颜色过滤,再次点击取消
let index = this.filterColor.findIndex((item) => item == color);
if (index > -1) {
//若已经筛选的颜色包含此颜色,则删除
this.filterColor.splice(index, 1);
} else {
//将此颜色加入到颜色筛选队列中
this.filterColor.push(color);
}
},
handleOrderDefault() {
this.order = "";
},
handleOrderSales() {
this.order = "sales";
},
handleOrderCost() {
//当点击升序时将箭头更新↓,降序设置为↑
if (this.order === "cost-desc") {
this.order = "cost-asc";
} else {
this.order = "cost-desc";
}
},
},
mounted() {
//初始化时通过Vuex actions获取商品列表信息
this.$store.dispatch("getProductList");
},
};
关于购物车中,有总价的计算,商品数量的改变等,实现较为简单,此处不作赘述,看代码即可:
export default {
name: "cart",
data() {
return {
promotion: 0, //优惠金额
promotionCode: "",
productList: product_data,
};
},
computed: {
//购物车数据
cartList() {
return this.$store.state.cartList;
},
//设置字典对象,方便模板使用
productDictList() {
const dict = {};
this.productList.forEach((item) => {
dict[item.id] = item;
});
return dict;
},
//购物车商品数量总数
countAll() {
let count = 0;
this.cartList.forEach((item) => {
count += item.count;
});
return count;
},
//购物车商品总价
costAll() {
let cost = 0;
this.cartList.forEach((item) => {
cost += this.productDictList[item.id].cost * item.count;
});
return cost;
},
},
methods: {
//通知Vuex,完成下单
handleOrder() {
//由于dispatch buy action 返回一个promise对象,用then()调用
this.$store.dispatch("buy").then((text) => {
window.alert(text);
});
},
//验证优惠码,使用ccylovehs代替优惠券字符串
handleCheckCode() {
if (this.promotionCode === "") {
window.alert("请输入优惠码");
return;
}
if (this.promotionCode !== "ccylovehs") {
window.alert("优惠码输入错误");
return;
}
this.promotion = 500;
},
//修改购物车数量,返回一个Promise
handleCount(index, count) {
//判断是否只剩一件
if (count < 0 && this.cartList[index].count === 1) return;
this.$store
.dispatch("editCart", {
id: this.cartList[index].id,
count: count,
})
.then(
(text) => {
//console.log(text)
},
(error) => {
window.alert(error);
}
);
},
//根据index查找商品id进行删除
handleDelete(index) {
this.$store.commit("deleteCart", this.cartList[index].id);
},
},
};
商品详情页较之以上操作,多了评论的操作,如下:
export default {
data() {
return {
//获取路由中的参数id
id: parseInt(this.$route.params.id),
product: null,
comments: [],
user_comment: "",
};
},
methods: {
getProduct() {
setTimeout(() => {
//模拟ajax获取具体商品数据
this.product = this.$store.state.productList.find(
(item) => item.id === this.id
);
this.comments = this.product.comments;
}, 300);
},
handleAddCart() {
//增加1件此商品至购物车,返回一个Promise
this.$store.dispatch("add", this.id).then(
(text) => {
//console.log(text)
},
(error) => {
window.alert(error);
}
);
},
comment() {
const content = this.user_comment;
const id = this.id;
this.$store.commit("addComment", {
product_id: id,
content: content,
});
this.user_comment = "";
},
},
mounted() {
//初始化数据
this.getProduct();
},
};
总体讲述了此商城项目,感谢阅读
感兴趣地可以到 github 上 clone 此项目:https://github.com/li-car-fei/vue_shopping