实现的功能:

基于vue.js的知识点(webpack,Vuex,Vue-router)开发电商网站项目,实现的功能包括:商品列表按照价格、销量排序;商品列表按照品牌、价格过滤;动态的购物车,使用优惠码等。

main.js:

    路由配置(router),使用History路由模式

    状态管理(Vuex,store),设置了state、getters、mutations、actions

router.js

   路由的页面配置放在router.js文件内单独维护,设置了list(默认页面)、product/:id、cart的路由列表。

style.css

全局使用的CSS样式,在main.js中导入(import './style.css'),每个vue文件scoped的样式一同提取输出到main.css文件。

plugins:[
        //重命名提取后的css文件
        new ExtractTextPlugin({
            filename:'[name].css',
            allChunks:true
        }),
        //vue-loader在15.*后的版本需要伴生VueLoaderPlugin的,否则启动报错
        new VueLoaderPlugin()
    ]

项目根目录下views目录下放置每个路由页面的.vue文件;components目录存放公共组件(product.vue定义了每个产品框的内容);images目录存放项目用到的照片。

1.模块拆分:

    商品列表(list)用于展示相关的所有商品,具有筛选和排序两种过滤方法;筛选条件可以叠加,可以按照价格、销量等在筛选的基础上进行排序,最终筛选出符合要求的商品。

    排序为单选,初始按“默认”进行排序,价格可分为升序和降序两种排序,销量按照降序;品牌和颜色为单选,单次点击选中,再次点击取消选中;

    初次打开商品列表页请求一次远程数据(用setTimeout模拟异步,真实场景通过Ajax获取),获取到全部的商品数据,然后筛选和排序在本地完成。

    商品列表主要有两个模块,路由组件(views/list.vue),负责数据的请求、过滤相关的逻辑;商品简介组件(components/produce.vue,每个商品卡片),鼠标划过时,显示“加入购物车”的按钮;两个模块的样式都直接写在各自vue文件的<style scoped>中。

 

2.商品简介组件:

    每个商品的选项包括标题、价格、颜色等,为方便父子间传递,在product.vue中设置一个property:info来接收一个对象格式的数据,父级可直接将获取到的数据传递过来,省去拆分的工作。info数据结构如下:

product.js
{
        id: 2,
        name: 'BeatsX 入耳式耳机',
        brand: 'Beats',
        image: '../images/2.jpeg',
        sales: 11000,
        cost: 1188,
        color: '白色'
    }

    颜色比较特殊,直接返回的中文无法对应到具体的色值,在product.vue的data选项中定义map,用于映射颜色和色值。

<router-link>最终会渲染成一个<a>标签,链接到:to定义的url(商品详情页),id作为参数通过vue-router传递。

components/product.vue
<router-link :to="'/product/' + info.id" class="product-main">

    鼠标悬浮在卡片上时会显示“加入购物车”按钮的实现是绑定@click事件,使用prevent修饰符来阻止冒泡,否则在点击按钮的同时,也会点击到<a>标签进入详情页。

components/product.vue
<div class="product-add-cart" @click.prevent="handleCart">加入购物车</div>

    “加入购物车”按钮先设置了handleCart方法,通过Vuex出发mutation保存到购物车,参数为商品的id。

components/product.vue
handleCart () {
                this.$store.commit('addCart', this.info.id);
            }

3.列表按照价格、销量排序(views/list.vue)

    数据部分,列表相关的数据都通过Vuex来维护,获取商品列表的数据是异步获取的,写在Vuex的actions里。(真实场景中数据应当是通过ajax从服务端获取,实例是用setTimeout来模拟异步,并用本地数据来mock);数据位置是/product.js

    main.js中导入数据,并在Vuex中声明数据列表相关的state、mutations、actions

    通过action的getProductList方法获取数据(product_data对应于product.js中的数据),由mutation的setProduction方法将数据设置到productList

main.js

const store = new Vuex.Store({
state: {
        productList: [],
        cartList: []
    },
mutations: {
        // 添加商品列表
        setProductList (state, data) {
            state.productList = data;
        }
}
actions: {
        // 请求商品列表
        getProductList (context) {
            // 真实环境通过 ajax 获取,这里用异步模拟
            setTimeout(() => {
                context.commit('setProductList', product_data);
            }, 500);
        }
}
}

    数据准备好之后,再关注视图部分,在根实例app.vue中挂载路由并设置导航条:

    数据cartList是购物车中添加的商品;路由视图<router-view>挂载了所有的路由组件

app.vue
<template>
    <div>
        <div class="header">
            <router-link to="/list" class="header-title">电商网站示例</router-link>
            <div class="header-menu">
                <router-link to="/cart" class="header-menu-cart">
                    购物车
                    <span v-if="cartList.length">{{ cartList.length }}</span>
                </router-link>
            </div>
        </div>
        <router-view></router-view>
    </div>
</template>
<script>
    export default {
        computed: {
            cartList () {
                return this.$store.state.cartList;
            }
        }
    }
</script>

    商品列表list.vue在初始化时调用Vuex的action触发请求数据操作,并设置计算属性从Vuex中读取数据productList

list.vue
<Product v-for="item in list" :info="item" :key="item.id"></Product>
import Product from '../components/product.vue';
export default {
        components: { Product },
        computed: {
            list () {
                // 从Vuex获取商品列表数据
                return this.$store.state.productList;
            }
        mounted () {
            // 初始化时,通过Vuex的action请求数据
            this.$store.dispatch('getProductList');
        }
}

    实现按照价格、销量排序,不能直接使用数据list,也不能直接重置list(过滤不是一次性的,不能破坏原数据,否则无法复原),所以用计算属性来动态返回过滤后的数据。

    计算属性 filteredAndOrderedList将list进一步过滤,返回筛选、排序后的数据,排序依据于data:order;排序直接使用javas数组的sort方法对前后两个值比较大小;把<Production>循环的数据由list改为 filteredAndOrderedList 后,显示的就是过滤后的数据,之后在视图中通过操作改变order。

list.vue
<Product v-for="item in filteredAndOrderedList" :info="item" :key="item.id"></Product>
filteredAndOrderedList () {
                // 复制原始数据
                let list = [...this.list];
                // 按品牌过滤
                if (this.filterBrand !== '') {
                    list = list.filter(item => item.brand === this.filterBrand);
                }
                // 按颜色过滤
                if (this.filterColor !== '') {
                    list = list.filter(item => item.color === this.filterColor);
                }
                // 排序
                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;
            }

    需要在list.vue的模板里加入排序按钮,并绑定相关事件。“默认”和“销量”只能单词点击,“价格”按钮可以点击切换为升序和降序两种状态;通过判断order的状态,给3个按钮绑定class(.on)来高亮显示当前排序的按钮。

list.vue
            <div class="list-control-order">
                <span>排序:</span>
                <span
                        class="list-control-order-item"
                        :class="{on: order === ''}"
                        @click="handleOrderDefault">默认</span>
                <span
                        class="list-control-order-item"
                        :class="{on: order === 'sales'}"
                        @click="handleOrderSales">
                    销量
                    <template v-if="order === 'sales'">↓</template>
                </span>
                <span
                        class="list-control-order-item"
                        :class="{on: order.indexOf('cost') > -1}"
                        @click="handleOrderCost">
                    价格
                    <template v-if="order === 'cost-asc'">↑</template>
                    <template v-if="order === 'cost-desc'">↓</template>
                </span>
            </div>

4.列表按照品牌、颜色筛选

    首先准备数据,品牌和颜色的数据可以作为getters从Vuex的productList里遍历获取。使用map方法把productList(product.js中数据)里的brand或color数据过滤出来,然后用getFilterArray方法对数组去重;getters里的brands和colors依赖数据productList,与计算属性原理类似,只要维护好productList,brands和colors就可以自动更新。

main.js
//数组去重
function getFilterArray (array) {
    const res = [];
    const json = {};
    for (let i = 0; i < array.length; i++){
        const _self = array[i];
        if(!json[_self]){
            res.push(_self);
            json[_self] = 1;
        }
    }
    return res;
}
// 数据过滤
const store = new Vuex.Store({
    state: {
        productList: [],
    },
    getters: {
        brands: state => {
            const brands = state.productList.map(item => item.brand);
            return getFilterArray(brands);
        },
        colors: state => {
            const colors = state.productList.map(item => item.color);
            return getFilterArray(colors);
        }
    }   
}

    然后在list.vue中把Vuex里的品牌和颜色数据引入,完成列表的过滤;

list.vue
export default {
        components: { Product },
        computed: {
            list () {
                // 从Vuex获取商品列表数据
                return this.$store.state.productList;
            },
            brands () {
                return this.$store.getters.brands;
            },
            colors () {
                return this.$store.getters.colors;
            }
    }
}

    品牌和颜色都是单选(单次点击选中,再次点击取消选中),可以协同过滤。

5.商品详情页

    views目录下新建product.vue文件作为“商品详情页”;商品详情的路由接收一个参数id,以id作为接口的索引,查询出所有相关的数据;为了使业务更好的解耦,从商品列表页跳转至详情页时,只传递商品的id,不需要其他数据,其他相信的数据重新获取;通过$route可以获取当前路由的参数,并在页面初始化是请求该商品的数据,示例使用setTimeout来模拟异步,真实场景下应该通过Ajax来请求数据。示例从数据源(product.js)里通过数组的find()方法拿到指定id的数据,完成数据mock。

views/product.vue
// 导入本地数据做匹配用,真实场景不需要
import product_data from '../product.js';

export default {
        data () {
            return {
                // 获取当前路由中的参数
                id: parseInt(this.$route.params.id),
                product: null
            }
        },
        methods: {
            getProduct () {
                // 真实环境通过 ajax 获取,这里用异步模拟
                setTimeout(() => {
                    this.product = product_data.find(item => item.id === this.id);
                }, 500);
            },
            // 加入购物车
            handleAddToCart () {
                this.$store.commit('addCart', this.id);
            }
        },
        mounted () {
            this.getProduct();
        }
    }

    将数据写入模板,示例将10张产品的图片依次展示作为商品的内容。

6.购物车

    购物车中需要完成结算,每件商品至少选择一件,可以删除,每件商品有价格小计;可以使用优惠码,使用后在总价的基础上减少500元;总价会根据购买商品的数量动态计算;右上角的 购物车入口也会显示当前购物车商品的数量。

    商品加入购物车是通过Vuex来完成的,在main.js中,先定义Vuex中的state和mutations;数组cartList中保存购物车记录,数据格式为数组,包含商品id和购买数量两个数据(遵循解耦,其他信息通过id间接获取)。

main.js
const store = new Vuex.Store({
    state: {
        productList: [],
        cartList: []
    },
    mutations: {
       addCart (state, id) {
            // 先判断购物车是否已有,如果有,数量+1
            const isAdded = state.cartList.find(item => item.id === id);
            if (isAdded) {
                isAdded.count ++;
            } else {
                state.cartList.push({
                    id: id,
                    count: 1
                })
            }
        } 
    }
}

    购物车数据准备完毕,显示数据并动态修改数据;在app.vue中定义购物车入口和已添加商品数量(即list页面右上角购物车入口和显示数量);

    views目录新建cart.vue文件,在router.js中添加购物车路由;在cart.vue中,准备动态数据:Vuex中的购物车数据cartList,product.js中所有商品的数据,将product.js中的数组转换为字典productDictList,方便快速选取,商品总数countAll,总费用costAll。这些数据都使用计算属性实现,productDictList是对象,key是商品id,value是商品信息,数据是product.js中每项的内容,通过id可以快速便捷的获取对应商品的信息。

cart.vue
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;
            }

    下单前对每个商品的数量进行加减,或者删除商品,先将购物车cartList循环渲染,并完成表格样式。

    handleCount方法用于修改购物车商品数量,最小为1;handleDelete方法用于删除商品;两者都根据接收的参数(循环cartList中的索引),并从数据cartList获取具体商品信息;两个方法通过$store.commit提交给Vuex的mutations来操作数据。

    使用优惠码:优惠码的功能使用两个数据:promotionCode和promotion,前者用于双向绑定输入框数据,后者是优惠金额。优惠价默认是0,可以不再判断是否使用了优惠码。

cart.vue
handleCheckCode () {
                if (this.promotionCode === '') {
                    window.alert('请输入优惠码');
                    return;
                }
                if (this.promotionCode !== 'Vue.js') {
                    window.alert('优惠码验证失败');
                } else {
                    this.promotion = 500;
                }
            }

    下单的操作通过Vuex的action完成,下单成功后,清空购物车数据;因为下单要通知服务端,所以需要在action中操作:

cart.vue
// 通知Vuex完成下单
            handleOrder () {
                this.$store.dispatch('buy').then(() => {
                    window.alert('购买成功');
                })
            }
main.js
mutations: {
    // 清空购物车
        emptyCart (state) {
            state.cartList = [];
        }
},
actions: {
     // 购买
        buy (context) {
            // 真实环境应通过 ajax 提交购买请求后再清空购物列表
            return new Promise(resolve=> {
                setTimeout(() => {
                    context.commit('emptyCart');
                    resolve();
                }, 500)
            });
        }
}

在action中,使用setTimeout模拟异步,并通过返回一个Promise对象来通知cart.vue的handleOrder购物完成。

项目代码Github地址:https://github.com/guocaiyao/VUE/tree/master/VuePractice/shopping

Logo

前往低代码交流专区

更多推荐