index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Webpack App</title>
    <link rel="stylesheet" type="text/css" href="/dist/main.css">
</head>
<body>
    <div id="app">
        
    </div>
    <script type="text/javascript" src="/dist/main.js"></script>
</body>
</html>

商品列表页主要有两个模块,

路由组件views / list.vue,负责数据的请求,国剧相关逻辑。

另一个是商品简介组件的组件/ product.vue,鼠标经过时,显示加入购物车的按钮;

prodect.vue

首先分析接收的数据结构:

//信息

{

ID:1,

名称: 'Airpods',

品牌:“苹果”,

图片: 'HTTP://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',

销售:10000,

费用:1288

颜色: '白色'

}

根据产品数据在素文字进行配置,因为颜色比较特殊,中文中无法对应具体的色值,所以在数据中定义一个映射,用于映射颜色和色值。

export default {
    /*
      info数据格式如下:
      {
          id:1,
          name:'Airpods',
          brand:'Apple',
          image:'http://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
          sales:10000,
          cost:1288,
          color:'白色'
          }
    */
    props:{info:Object},
    data(){
        return{
            colors:{
                '白色':'#ffffff',
                '金色':'#dac272',
                '蓝色':'#233472',
                '红色':'#2352e'
            }
        }
    }
}

鼠标悬停在卡片上时候,右上角会显示“加入购物车”按钮,

product.vue

<template>
    <div class="product">
        <router-link
         :to="'/product/'+info.id"
         class="product-main">
         <img :src="info.image">
         <h4>{{info.name}}</h4>
         <div class="product-color"
          :style="{background:colors[info.color]}">
       <!-- 
           中括号内部根据色号匹配CSS样式
       -->
         </div>
   <div class="product-cost">
¥{{info.cost}}
   </div>
   <div
    class="product-add-cart"
    @click.prevent="handleCart"
   >
 加入购物车
   </div>
        </router-link>
    </div>
</template>

<script>
export default {
    /*
      info数据格式如下:
      {
          id:1,
          name:'Airpods',
          brand:'Apple',
          image:'http://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
          sales:10000,
          cost:1288,
          color:'白色'
          }
    */
    props:{info:Object},
    data(){
        return{
            colors:{
                '白色':'#ffffff',
                '金色':'#dac272',
                '蓝色':'#233472',
                '红色':'#2352e'
            }
        }
    },
    methods:{
        hadleCart(){
            //VUEX共用数据调用,提交ID
            this.$store.commit('addCart',this.info.id);
        }
    }
}
</script>
<style scoped>

.product{
  width:25%;
  float:left;
}
.product-main{
  display:block;
  margin:16px;
 padding:16px;
 border:1px solid #dddee1;
border-radius:6px;
overflow:hidden;
background:#fff;
 text-align:center;
position:relative;
}
.product-main img{
 width:100%;
}
h4{
 color:#222;
overflow:hidden;
text-overflow:hidden;
white-space:nowrap
}
.product-main:hover h4{
 color:#0070c9
}
.product-color{
display:block;
width:16px;
height:16px;
border:1px solid #dddee1;
border-radius:50%;
margin:6px auto;
}
.prodyct-cost{
  color:#de4037;
margin-top:6px;

}
.product-add-cart{
 display:none;
padding:4px 8px;
background:#2d8cf0;
color:#fff;
font-size:12px;
border-radius:3px;
cursor:pointer;
position:absolute;
top:5px;
right:5px;
}
.product-main:hover .product-add-cart{
 display:inline-block;
}
</style>

产品数据:(异步模拟)

product.js

export default [
    {
        id: 1,
        name: 'AirPods',
        brand: 'Apple',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/1.jpeg',
        sales: 10000,
        cost: 1288,
        color: '白色'
    },
    {
        id: 2,
        name: 'BeatsX 入耳式耳机',
        brand: 'Beats',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/2.jpeg',
        sales: 11000,
        cost: 1188,
        color: '白色'
    },
    {
        id: 3,
        name: 'Beats Solo3 Wireless 头戴式式耳机',
        brand: 'Beats',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/3.jpeg',
        sales: 5000,
        cost: 2288,
        color: '金色'
    },
    {
        id: 4,
        name: 'Beats Pill+ 便携式扬声器',
        brand: 'Beats',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/4.jpeg',
        sales: 3000,
        cost: 1888,
        color: '红色'
    },
    {
        id: 5,
        name: 'Sonos PLAY:1 无线扬声器',
        brand: 'Sonos',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/5.jpeg',
        sales: 8000,
        cost: 1578,
        color: '白色'
    },
    {
        id: 6,
        name: 'Powerbeats3 by Dr. Dre Wireless 入耳式耳机',
        brand: 'Beats',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/6.jpeg',
        sales: 12000,
        cost: 1488,
        color: '金色'
    },
    {
        id: 7,
        name: 'Beats EP 头戴式耳机',
        brand: 'Beats',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/7.jpeg',
        sales: 25000,
        cost: 788,
        color: '蓝色'
    },
    {
        id: 8,
        name: 'B&O PLAY BeoPlay A1 便携式蓝牙扬声器',
        brand: 'B&O',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/8.jpeg',
        sales: 15000,
        cost: 1898,
        color: '金色'
    },
    {
        id: 9,
        name: 'Bose® QuietComfort® 35 无线耳机',
        brand: 'Bose',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/9.jpeg',
        sales: 14000,
        cost: 2878,
        color: '蓝色'
    },
    {
        id: 10,
        name: 'B&O PLAY Beoplay H4 无线头戴式耳机',
        brand: 'B&O',
        image: 'http://ordfm6aah.bkt.clouddn.com/shop/10.jpeg',
        sales: 9000,
        cost: 2298,
        color: '金色'
    }
]

list组件:

<template>
    <div v-show="list.length">
        <div  class="list-control">
             <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>
     <Product v-for="item in list" :info="item" :key="item.id">
     </Product>
     <div class="product-not-found"
      v-show="!filteredAndOrderedList.length"
     >
      暂无相关商品
     </div>
    </div>
</template>
<script>
import Product from '../components/product.vue';
export default {
    components:{Product},
    data(){
        return{
            //排序依据
            //sales(销量))
            //cost-desc(价格降序)
            //cost-asc(价格升序)
            order:''
        }
    },
    methods:{
    handleOrderDefault(){
        this.order='';
    },
    handleOrderSales(){
     this.order="sales";
    },
    handleOrderCost(){
        if(this.order==='cost-desc'){
            this.order='cost-asc';
        }else{
            this.order='cost-desc';
        }
    }
    },
    computed:{
        list(){
            //从Vuex获取商品列表数据
            return this.$store.state.productList;
        },
        filteredAndOrderedList(){
            //复制原始数据
            let list=[...this.list];
            //todo按品牌过滤
            //todo按颜色过滤
            //排序
            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;
        }
    },
    mounted(){
        //初始化时,通过Vuex的action请求数据
        this.$store.dispath('getProductList');
    }
}
</script>
<style lang="scss" scoped>
  .product-not-found{
      text-align: center;
      padding: 32px;
        }
        .list-control{
            background: #fff;
            border-radius: 6px;
            margin:16px;
            padding: 16px;
            box-shadow: 0 1px 1px rgba(0,0,0,2);
        }
        .list-control-filter{
            margin-bottom: 16px;
        }
        .list-control-filter-item,
        .list-control-order-item{
           cursor: pointer;
            display: inline-block;
            border: 1px solid #e9eaec;
            border-radius: 4px;
            margin-right: 6px;
            padding: 2px 6px;

        }
        .list-control-filter-item.on,
        .list-control-order-item.on{
            background:#f2352e;
            border:1px solid #f2352e;
            color: #fff;
        }
</style>

 在router.js中配置list的路由

const routers=[
  {
      path:'/list',
      meta:{
          title:'商品列表'
      },
      component:(resolve)=>require(['./views/list.vue'],resolve)
  },
  {
      path:'*',
      redirect:'/list'
  }

];
export default routers;

在main.js中导入产品数据

import product_data from './product.js';
const store=new Vuex.Store({
    state:{
      //商品列表数据
      productList:[],
      //购物车shujv 
      cartList:[]
    },
    getters:{
          brands:state=>{
              const brands=state.productList.map(item=>item.brand);
          }
    },
    mutations:{
      //添加商品列表
      setProductList(state,data){
          state.productList=data;
      }
    },
    actions:{
    //请求商品列表
    getProductList(context){
        //真实环境通过Ajax获取,这里用异步模拟
        setTimeout(()=>{
            context.commit('setProductList',product_data);
            //product_data是异步从product.js获取的数据
        })
    }

在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>

全局设置css:

style.css

*{
    margin: 0;
    padding: 0;
}
a{
    text-decoration: none;
}
body{
    background: #f8f8f9;
}
.header{
    height: 48px;
    line-height: 48px;
    background: rgba(0,0,0,.8);
    color: #fff;
}
.header-title{
    padding: 0 32px;
    float: left;
    color: #fff;
}
.header-menu{
    float: right;
    margin-right: 32px;
}
.header-menu-cart{
    color: #fff;
}
.header-menu-cart span{
    display: inline-block;
    width: 16px;
    height: 16px;
    line-height: 16px;
    text-align: center;
    border-radius: 50%;
    background: #ff5500;
    color: #fff;
    font-size: 12px;
}

在views中新建product.vue

<template>
   <div v-if="product">
       <div class="product">
           <div class="product-image">
               <img :src="product.image">

           </div>
           <div class="product-info">
                <h1 class="product-name">{{product.name}}</h1>
                <div class="product-cost">¥{{product.cost}}</div>
                <div class="product-add-cart"
                @click="handleAddToCart">加入购物车</div>
           </div>
       </div>
            <div class="product-desc">
              <h2>产品介绍</h2>
                <img :src="'http://ordfm6aah.bkt.clouddn.com/shop'+n+'jpeg'" v-for="n in 10" :key="n">
            </div>
   </div>
</template>

<script>
//导入本地数据匹配使用,真实场景不需要
import product_data from '../product.js'
export default {
    data(){
        return{
            //获取路由中的参数
            id:parseInt(this.$route.param.id),
            product:null
        }
    },
    methods:{
        //计入购物车
        handleAddToCart(){
         this.$store.commit('addCart',this.id);
        },
        getProduct(){
            //真实环境通过Ajax获取,此处异步模拟
            setTimeout(()=>{
                this.product=product_data.find(item=>item.id===this.id);
            },500);
        }
    },
    mounter(){
        //初始化,请求数据
        this.getProduct();
    }
}
</script>
 <style scoped>
  
 .product{
        margin: 32px;
        padding: 32px;
        background: #fff;
        border: 1px solid #dddee1;
        border-radius: 10px;
        overflow: hidden;
    }
    .product-image{
        width: 50%;
        height: 550px;
        float: left;
        text-align: center;
    }
    .product-image img{
        height: 100%;
    }
    .product-info{
        width: 50%;
        padding: 150px 0 250px;
        height: 150px;
        float: left;
        text-align: center;
    }
    .product-cost{
        color: #f2352e;
        margin: 8px 0;
    }
    .product-add-cart{
        display: inline-block;
        padding: 8px 64px;
        margin: 8px 0;
        background: #2d8cf0;
        color: #fff;
        border-radius: 4px;
        cursor: pointer;
    }
    .product-desc{
        background: #fff;
        margin: 32px;
        padding: 32px;
        border: 1px solid #dddee1;
        border-radius: 10px;
        text-align: center;
    }
    .product-desc img{
        display: block;
        width: 50%;
        margin: 32px auto;
        padding: 32px;
        border-bottom: 1px solid #dddee1;
    }
</style>

添加购物车组件在views:

购物车应具有:

   vuex中的购物车数据cartList;

  product.js中的所有商品数据;

   将product.js中的数组转换为字典productDictList.

   商品总数countAll

   总费用costAll

cart.vue

<template>
    <div class="cart">
        <div class="cart-header">
            <div class="cart-header-title">
              购物清单
            </div>
            <div class="cart-header-main">

                 <div class="cart-info">商品信息</div>
                 <div class="cart-price">单价</div>
                 <div class="cart-count">数量</div>
                 <div class="cart-cost">小计</div>
                 <div class="cart-delete">删除</div>
            </div>
        </div>
    <div class="cart-content">
        <div class="cart-content-main" v-for="(item,index) in cartList" :key="item.id">
     <div class="cart-info">
         <img src="productDictList[item.id].image" >
         <span >{{productDictList[item.id].name}}</span>
     </div>
    <div class="cart-price">
      ¥{{productDictList[item.id].cost}}
    </div>
    <div
     class="cart-count"><span
     class="cart-control-minus"
     @click="handleCount(index,-1)">-
         </span>
         {{item.count}}
         <span  class="cart-control-add"
     @click="handleCount(index,1)">
             +
         </span>
          </div>
         <div class="cart-cost">
             ¥{{productDictList[item.id].cost*item.count}}
             </div>  
             <div class="cart-delete">
           <span 
           class="cart-control-delete"
            @click="handleDelete(index)">
             删除

           </span>
             </div>
        </div>
        <div class="cart-empty" v-if="!cartList.length">购物车为空</div>
    </div>
    </div>
</template>

<script>
import product_data from '../product.js'
export default {
    methods:{
        handleCount(index,count){
            if(count<0&&this.cartList[index].count===1)return;
            this.$store.commit('editCartCount',{
                id:this.cartList[index].id,
                count:count
            });
        },
        handleDelete(index){
            this.$store.commit('deleteCart',this.cartList[index].id);
        }

    },
    computed:{
        cartList(){
            return this.$store.state.cartList;
        },
        productDocList(){
            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;
        }
    },
    data(){
        return{
            productList:product_data
        }
    }
}
</script>
<style scoped>
    .cart{
        margin: 32px;
        background: #fff;
        border: 1px solid #dddee1;
        border-radius: 10px;
    }
    .cart-header-title{
        padding: 16px 32px;
        border-bottom: 1px solid #dddee1;
        border-radius: 10px 10px 0 0;
        background: #f8f8f9;
    }
    .cart-header-main{
        padding: 8px 32px;
        overflow: hidden;
        border-bottom: 1px solid #dddee1;
        background: #eee;
        overflow: hidden;
    }
    .cart-empty{
        text-align: center;
        padding: 32px;
    }
    .cart-header-main div{
        text-align: center;
        float: left;
        font-size: 14px;
    }
    div.cart-info{
        width: 60%;
        text-align: left;
    }
    .cart-price{
        width: 10%;
    }
    .cart-count{
        width: 10%;
    }
    .cart-cost{
        width: 10%;
    }
    .cart-delete {
        width: 10%;
    }
    .cart-content-main{
        padding: 0 32px;
        height: 60px;
        line-height: 60px;
        text-align: center;
        border-bottom: 1px dashed #e9eaec;
        overflow: hidden;
    }
    .cart-content-main div{
        float: left;
    }
    .cart-content-main img{
        width: 40px;
        height: 40px;
        position: relative;
        top: 10px;
    }
    .cart-control-minus,
    .cart-control-add{
        display: inline-block;
        margin: 0 4px;
        width: 24px;
        height: 24px;
        line-height: 22px;
        text-align: center;
        background: #f8f8f9;
        border-radius: 50%;
        box-shadow: 0 1px 1px rgba(0,0,0,.2);
        cursor: pointer;
    }
    .cart-control-delete{
        cursor: pointer;
        color: #2d8cf0;
    }
    .cart-promotion{
        padding: 16px 32px;
    }
    .cart-control-promotion,
    .cart-control-order{
        display: inline-block;
        padding: 8px 32px;
        border-radius: 6px;
        background: #2d8cf0;
        color: #fff;
        cursor: pointer;
    }
    .cart-control-promotion{
        padding: 2px 6px;
        font-size: 12px;
        border-radius: 3px;
    }
    .cart-footer{
        padding: 32px;
        text-align: right;
    }
    .cart-footer-desc{
        display: inline-block;
        padding: 0 16px;
    }
    .cart-footer-desc span{
        color: #f2352e;
        font-size: 20px;
    }
</style>

设置路由引入:

//router.js
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: '*',
        redirect: '/list'
    }
];
export default routers;

main.js

import Vue from 'vue';
import VueRouter from 'vue-router';
import Routers from './router';
import Vuex from 'vuex';
import App from './app.vue';
import './style.css';

import product_data from './product';

Vue.use(VueRouter);
Vue.use(Vuex);

// 路由配置
const RouterConfig = {
    // 使用 HTML5 的 History 路由模式
    mode: 'history',
    routes: Routers
};
const router = new VueRouter(RouterConfig);

router.beforeEach((to, from, next) => {
    window.document.title = to.meta.title;
    next();
});

router.afterEach((to, from, next) => {
    window.scrollTo(0, 0);
});

// 数组排重
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: [],
        cartList: []
    },
    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);
        }
    },
    mutations: {
        // 添加商品列表
        setProductList (state, data) {
            state.productList = data;
        },
        // 添加到购物车
        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
                })
            }
        },
        // 修改商品数量
        editCartCount (state, payload) {
            const product = state.cartList.find(item => item.id === payload.id);
            product.count += payload.count;
        },
        // 删除商品
        deleteCart (state, id) {
            const index = state.cartList.findIndex(item => item.id === id);
            state.cartList.splice(index, 1);
        },
        // 清空购物车
        emptyCart (state) {
            state.cartList = [];
        }
    },
    actions: {
        // 请求商品列表
        getProductList (context) {
            // 真实环境通过 ajax 获取,这里用异步模拟
            setTimeout(() => {
                context.commit('setProductList', product_data);
            }, 500);
        },
        // 购买
        buy (context) {
            // 真实环境应通过 ajax 提交购买请求后再清空购物列表
            return new Promise(resolve=> {
                setTimeout(() => {
                    context.commit('emptyCart');
                    resolve();
                }, 500)
            });
        }
    }
});

new Vue({
    el: '#app',
    router: router,
    store: store,
    render: h => {
        return h(App)
    }
});

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>

 

项目上传至https://github.com/anymouschina/vue-use

Logo

前往低代码交流专区

更多推荐