专栏首页魏晓蕾的专栏【Vue.js】Vue.js中的Vuex、Vue-Ajax和京东购物车项目实战

【Vue.js】Vue.js中的Vuex、Vue-Ajax和京东购物车项目实战

1、Vuex

1. 单向数据流理念

组成部分

  • state:驱动应用的数据源
  • view:以声明方式将 state 映射到视图
  • actions:响应在 view 上的用户输入导致的状态变化

图示

传统数据通信存在的问题

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:(1)多个视图依赖于同一状态,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力;(2)来自不同视图的行为需要变更同一状态,经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。

解决问题的钥匙

把组件的共享状态抽取出来,以一个全局单例模式管理,在这种模式下,项目中的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。Vuex就是这样一个状态管理模式。

2. Vuex是什么

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
  • 它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
  • 一句话:Vuex相当于一个数据银行,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写)

3. Vuex的组成

图示如下:

state

  • vuex 管理的状态对象
  • 它应该是唯一的:
const state = {
    name: 'zhangsan'
}

mutations

  • 包含多个直接更新 state 的方法(回调函数)的对象
  • 在action中通过commit(‘mutation 名称’)触发mutations中更新state的方法
  • 只能包含同步的代码, 不能写异步代码
const mutations = {
   aaaa (state, {data}) {
       // 更新 state 的 data 属性
   }
}

actions

  • 包含多个事件回调函数的对象
  • Mutation必须是同步的,Action是异步的Mutation
  • 组件中通过 $store.dispatch(‘action 名称’, data)触发
  • 可以包含异步代码(定时器, ajax)
const actions = {
   bbbb({commit, state}, data1) {
         commit('aaaa', {data1})
   }
}

getters

  • 有时候我们需要从 store 中的 state 中派生出一些状态,我们可以理解为vuex中数据的computed功能
## store.js

getters:{
  	money: state => `¥${state.count*1000}`
},
## page.vue

computed: {
  	money() {
    	return this.$store.getters.money;
 	}
}

mapState

  • 更方便的使用api,当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余
  • 为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性
...mapState({
    count:state=>state.count
}),

mapActions

  • 方便快捷的使用action
methods:{
    ...mapActions(['dealCount']),
    ...mapMutations(['count'])
},
  • this.$store.dispatch可以变为
this.dealCount({
    amount: 10
})

mapMutions

...mapMutations(['add'])
this.add()

Modules

  • 面对复杂的应用程序,当管理的状态比较多时, 我们需要将Vuex的store对象分割成多个模块(modules)
const  moduleA = {
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
}

const  moduleB = {
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
}

const store = new Vuex.Store({
     modules: {
           a:  moduleA,
           b:  moduleB
     }
});

4. 代码示例

初始化项目

>> vue create lk-vuex-demo
>> vue add vuex
>> npm run serve
## store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        count: 0 // 初始化数据
    },
    mutations: {
        INCREMENT(state){
            state.count++;
        },
        DECREMENT(state){
            state.count--;
        }
    },
    actions: {
        increment({commit}){
            commit('INCREMENT');
        },
        decrement({commit}){
            commit('DECREMENT');
        },
        incrementIfEven({commit, state}){
           if(state.count % 2 === 0){
               commit('INCREMENT');
           }
        },
        incrementAsync({commit}){
            setTimeout(()=>{
                commit('INCREMENT');
            }, 1000);
        }
    },
    getters: {
        evenOrOdd(state){
            return state.count % 2 === 0 ? '偶数': '奇数'
        }
    }
})
## main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');
## App.vue

<template>
    <div id="app">
         <Counter />
    </div>
</template>

<script>
    import Counter from './components/Counter'
    export default {
        name: 'app',
        components: {
            Counter
        }
    }
</script>

<style>
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
    }
</style>
## Counter01.vue

<template>
    <div>
        <p>点击了{{count}}次</p>
        <button @click="increment">增加+1</button>
        <button @click="decrement">减少-1</button>
    </div>
</template>

<script>
    // import {mapMutations} from 'vuex'
    export default {
        name: "Counter",
        computed: {
            count(){
                return this.$store.state.count
            }
        },
        methods:{
           //  ...mapMutations(['INCREMENT', 'DECREMENT']),
            increment(){
                // this.INCREMENT();
                // this.$store.commit('INCREMENT');
                this.$store.dispatch('increment');
            },
            decrement(){
                // this.DECREMENT();
                // this.$store.commit('DECREMENT');
                this.$store.dispatch('decrement');
            }
        }
    }
</script>

<style scoped>

</style>
## Counter02.vue

<template>
    <div>
        <p>点击了{{count}}次, count是{{evenOrOdd}}</p>
        <button @click="increment">增加+1</button>
        <button @click="decrement">减少-1</button>
        <button @click="incrementIfEven">偶数+1</button>
        <button @click="incrementAsync">异步+1</button>
    </div>
</template>

<script>
    import {mapState, mapGetters, mapActions} from 'vuex'
    export default {
        name: "Counter",
        computed: {
            ...mapState(['count']),
            ...mapGetters(['evenOrOdd'])
        },
        methods:{
           ...mapActions(['increment', 'decrement', 'incrementIfEven', 'incrementAsync'])
        }
    }
</script>

<style scoped>

</style>
## Counter03.vue

<template>
    <div>
        <p>点击了{{count}}次, count是{{evenOrOdd}}</p>
        <button @click="increment">增加+1</button>
        <button @click="decrement">减少-1</button>
        <button @click="incrementIfEven">偶数+1</button>
        <button @click="incrementAsync">异步+1</button>
    </div>
</template>

<script>
    export default {
        name: "Counter",
        computed: {
            count(){
                return this.$store.state.count
            },
            evenOrOdd(){
                return this.$store.getters.evenOrOdd
            }
        },
        methods:{
            increment(){
                this.$store.dispatch('increment');
            },
            decrement(){
                this.$store.dispatch('decrement');
            },
            incrementIfEven(){
                this.$store.dispatch('incrementIfEven');
            },
            incrementAsync(){
                this.$store.dispatch('incrementAsync');
            }
        }
    }
</script>

<style scoped>

</style>

2、Vue-Ajax

Vue 项目中常用的 2 个 Ajax

  • vue-resource:vue 插件,非官方库,vue1.x 使用广泛
  • axios:通用的 ajax 请求库,官方推荐,vue2.x 使用广泛

axios 的使用

  • 官方文档:https://github.com/pagekit/vue-resource/blob/develop/docs/http.md
  • 安装:
>> npm install axios --save
// 引入模块
import axios from 'axios'
// 发送 ajax 请求
axios.get(url)
.then(response => {
      console.log(response.data) ; 		// 得到返回结果数据
}).catch(error => {
      console.log(error.message);
}

测试接口

https://www.easy-mock.com/mock/5d40032d6a3ae527e747fea9/example/itlike/p_list

GET请求实操

axios.get('https://www.easy-mock.com/mock/5d40032d6a3ae527e747fea9/example/itlike/p_list').then((response) => {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});

3、Vuex版本TodoList

代码结构:

## main.js

import Vue from 'vue'
import App from './App.vue'
import './assets/index.css'
import store from './store/index'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');
## index.js

/*
  Vuex核心管理模块 - Store对象
*/

import Vue from 'vue'
import Vuex from 'vuex'

import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'

Vue.use(Vuex);

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
});
## state.js

/*
  状态对象模块
*/
import localStorageUtil from './../utils/localStorageUtil'

export default {
   todos: localStorageUtil.readTodos()
}
## mutations.js

/*
  多个可以直接同步更新状态的方法 对象模块
*/

import {ADD_TODO, DELETE_TODO, SELECT_ALL_TODO, DELETE_FINISHED_TODO} from './mutations-type'

export default {
    [ADD_TODO](state, {todo}){ 				// ADD_TODO并不是方法名, add_to
        state.todos.unshift(todo);
    },
    [DELETE_TODO](state, {index}){
        state.todos.splice(index, 1);
    },
    [SELECT_ALL_TODO](state, {isCheck}){
        state.todos.forEach(todo => {
            todo.finished = isCheck
        })
    },
    [DELETE_FINISHED_TODO](state){
        state.todos = state.todos.filter(todo=> !todo.finished)
    },
}
## mutations-type.js

/*
  包含多个mutations中方法名称的常量
 */
export const ADD_TODO = 'add_todo'; 							// 添加todo
export const DELETE_TODO = 'delete_todo'; 						// 删除todo
export const SELECT_ALL_TODO = 'select_all_todo'; 				// 全选/取消全选的todo
export const DELETE_FINISHED_TODO = 'delete_finished_todo'; 	// 清除已经完成的todo
## actions.js

/*
  包含多个间接更新state的方法  对象模块
*/
import {ADD_TODO, DELETE_TODO, SELECT_ALL_TODO, DELETE_FINISHED_TODO} from './mutations-type'

export default {
    addTodo({commit}, todo){
        commit(ADD_TODO, {todo});
    },
    delTodo({commit}, index){
        commit(DELETE_TODO, {index});
    },
    selectedAllTodo({commit}, isCheck){
        commit(SELECT_ALL_TODO, {isCheck});
    },
    delFinishedTodos({commit}){
        commit(DELETE_FINISHED_TODO);
    }
}
## getters.js

/*
   服务于 state
*/

export default {
    // 任务总数量
    todosCount(state){
        return state.todos.length;
    },
    // 已经完成的任务数量
    finishedCount(state) {
        return state.todos.reduce((total, todo) => total + (todo.finished ? 1 : 0), 0);
    },
    // 判断是否是全选
    isCheck(state, getters){
        return getters.finishedCount === getters.todosCount && getters.todosCount > 0
    }
}
## localStorageUtil.js

const LK_TODO = 'lk_todo';
export default {
    readTodos(){
        return JSON.parse(localStorage.getItem(LK_TODO) || '[]');
    },
    saveTodos(todos){
        console.log(todos);
        localStorage.setItem(LK_TODO, JSON.stringify(todos));
    }
}
## App.vue

<template>
    <div class="todo-container">
        <div class="todo-wrap">
            <Header/>
            <List/>
            <Footer/>
            <button @click="reqData">获取网络数据</button>
        </div>
    </div>
</template>

<script>
    // 引入组件
    import Header from './components/Header'
    import List from './components/List'
    import Footer from './components/Footer'

    import axios from 'axios'

    export default {
        name: 'app',
        components: {
            Header,
            List,
            Footer
        },
        methods: {
            reqData(){
                axios.get('https://www.easy-mock.com/mock/5d40032d6a3ae527e747fea9/example/itlike/p_list').then((response)=>{
                    console.log(response);
                }).catch((error)=>{
                    console.log(error);
                })
            }
        }
    }
</script>

<style>
    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>
## Header.vue

<template>
    <div class="todo-header">
        <input
            type="text"
            placeholder="请输入今天的任务清单,按回车键确认"
            v-model="title"
            @keyup.enter="addItem"
        />
    </div>
</template>

<script>
    export default {
        name: "Header",
        data(){
            return {
                title: ''
            }
        },
        methods: {
            addItem(){
                // 1. 判断是否为空
                const title = this.title.trim();
                if(!title){
                    alert('输入的任务不能为空!');
                    return;
                }
                // 2. 生成一个todo对象
                let todo = {title, finished: false};
                // 3. 调用父组件的插入方法
                this.$store.dispatch('addTodo', todo);
                // 4. 清空输入框
                this.title = '';
            }
        }
    }
</script>

<style scoped>
    .todo-header input {
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 4px 7px;
        outline: none;
    }

    .todo-header input:focus {
        outline: none;
        border-color: rgba(255, 0, 0, 0.8);
        box-shadow: inset 0 1px 1px rgba(255, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6);
    }
</style>
## List.vue

<template>
    <ul class="todo-main">
        <Item
          v-for="(todo, index) in todos"
          :todo="todo"
          :index ="index"
        />
    </ul>
</template>

<script>
    import localStorageUtil from './../utils/localStorageUtil'
    import Item from './Item'
    import {mapState} from 'vuex'
    export default {
        name: "List",
        computed:{
          ...mapState(['todos'])
        },
        components: {
            Item
        },
        watch: {
            todos: {
                deep: true,
                handler: localStorageUtil.saveTodos
            }
        }
    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }
</style>
## Item.vue

<template>
    <li
      @mouseenter="dealShow(true)"
      @mouseleave="dealShow(false)"
      :style="{backgroundColor: bgColor}"
    >
        <label>
            <input type="checkbox" v-model="todo.finished"/>
            <span>{{todo.title}}</span>
        </label>
        <button v-show="isShowDelButton"  class="btn btn-warning" @click="delItem">删除</button>
    </li>
</template>

<script>
    export default {
        name: "Item",
        props: {
            todo: Object,
            index: Number 				// 当前任务在总任务数组中的下标位置
        },
        data(){
          return{
              isShowDelButton: false,  // false 隐藏 true 显示
              bgColor: '#fff'
          }
        },
        methods: {
            dealShow(isShow){
                // 控制按钮的显示和隐藏
                this.isShowDelButton =  isShow;
                // 控制背景颜色
                this.bgColor = isShow ? '#ddd' : '#fff';
            },

            delItem(){
                if(window.confirm(`您确定删除 ${this.todo.title} 吗?`)){
                     this.$store.dispatch('delTodo', this.index);
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        padding: 4px 10px;
        float: right;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }
</style>
## Footer.vue

<template>
    <div class="todo-footer">
        <label>
            <input slot="isCheck" type="checkbox" v-model="selectedAllOrNot"/>
        </label>
        <span>
            <span slot="finish">已完成{{finishedCount}}件 / 总计{{todosCount}}件</span>
        </span>
        <button  slot="delete" class="btn btn-warning" @click="delFinishedTodos">清除已完成任务</button>
    </div>
</template>

<script>
    import {mapGetters, mapActions} from 'vuex'
    export default {
        name: "Footer",
        computed:{
            ...mapGetters(['todosCount', 'finishedCount', 'isCheck']),
            selectedAllOrNot: {
                get(){ // 决定是否勾选
                    return this.isCheck;
                },
                set(value){
                    this.selectedAllTodo(value)
                }
            }
        },
        methods: {
            ...mapActions(['selectedAllTodo', 'delFinishedTodos'])
        }
    }
</script>

<style scoped>
    .todo-footer {
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
    }

    .todo-footer label {
        display: inline-block;
        margin-right: 20px;
        cursor: pointer;
    }

    .todo-footer label input {
        position: relative;
        top: -1px;
        vertical-align: middle;
    }

    .todo-footer button {
        float: right;
        margin-top: 5px;
    }
</style>

4、京东购物车项目实战

代码结构:

## main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');
## store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {

  },
  mutations: {

  },
  actions: {

  }
})
App.vue

<template>
    <div id="app">
        <Cart />
    </div>
</template>

<script>
    import Cart from './components/Cart'

    export default {
        name: 'app',
        components: {
            Cart
        }
    }
</script>

<style>

</style>
## Cart.vue

<template>
    <div>
        <!--头部区域-->
        <header class="header">
            <a href="index.html" class="icon-back"></a>
            <h3>购物车</h3>
            <a href="" class="icon-menu"></a>
        </header>
        <!--安全提示-->
        <section class="jd-safe-tip">
            <p class="tip-word">
                您正在安全购物环境中,请放心购物
            </p>
        </section>
        <!--中间内容-->
        <main class="jd-shop-cart-list">
            <section>
                <div class="shop-cart-list-title">
                    <div class="left">
                        <span class="cart-title">撩课自营</span>
                    </div>
                    <span class="right">您享受满100元免运费服务</span>
                </div>
                <div class="shop-cart-list-con" v-for="(shop, index) in shopListArr" :key="shop.shopId">
                    <div class="left">
                        <a
                             href="javascript:;"
                             class="cart-check-box"
                             :checked="shop.checked"
                             @click="singerShopSelected(shop)"
                        >
                        </a>
                    </div>
                    <div class="center">
                        <img :src="shop.shopImage" :alt="shop.shopName">
                    </div>
                    <div class="right">
                        <a href="#">{{shop.shopName}}</a>
                        <div class="shop-price">
                            <div class="singer-price">{{shop.shopPrice | moneyFormat}}</div>
                            <div class="total-price">总价:{{ shop.shopPrice * shop.shopNumber | moneyFormat}}</div>
                        </div>
                        <div class="shop-deal">
                            <span @click="singerShopPrice(shop, false)">-</span>
                            <input disabled="flase" type="number" v-model="shop.shopNumber">
                            <span @click="singerShopPrice(shop, true)">+</span>
                        </div>
                        <div class="shop-deal-right" @click="clickTrash(shop, $event)">
                            <span></span>
                            <span></span>
                        </div>
                    </div>
                </div>
            </section>
        </main>
        <!--面板-->
        <div ref="panel" class="panel" style="display: none;">
            <div ref="panelContent" class="panel-content">
                <div class="panel-title">您确认删除这个商品吗?</div>
                <div class="panel-footer">
                    <a @click.prevent="hidePanel" href="javascript:;" class="cancel">取消</a>
                    <a @click.prevent="delShop" href="javascript:;" class="submit">确定</a>
                </div>
            </div>
        </div>
        <!--底部通栏-->
        <div id="tab_bar">
            <div class="tab-bar-left">
                <a
                     href="javascript:;"
                     class="cart-check-box"
                     :checked="isSelectedAll"
                     @click="selectedAll(isSelectedAll)"
                ></a>
                <span style="font-size: 16px;">全选</span>
                <div class="select-all">
                    合计:<span class="total-price">{{totalPrice | moneyFormat}}</span>
                </div>
            </div>
            <div class="tab-bar-right">
                <a href="index.html" class="pay">去结算</a>
            </div>
        </div>
    </div>
</template>

<script>
    import './../assets/css/base.css'
    import './../assets/css/cart.css'
    import axios from 'axios'

    export default {
        name: "Cart",
        data() {
            return {                
                shopListArr: [],			// 购物车中的商品数据
                totalPrice: 0,
                isSelectedAll: false, 		// 标识是否全选
                up: '', 					// 盖子
                currentDelShop: {}, 		// 要删除的商品
            }
        },
        created() {
            this.getProduct();
        },
        methods: {
            // 1. 获取网络数据
            getProduct() {
                axios.get('http://demo.itlike.com/web/jdm/api/shoplist').then((response) => {
                    if (response.data.status === 200) {
                        this.shopListArr = response.data.result.shopList;
                    }
                }).catch((error) => {
                    alert('网络出现异常!');
                })
            },
            //  2. 单个商品的加减
            singerShopPrice(shop, flag) { // true +  false -
                if (flag) { 					// 加
                    shop.shopNumber += 1;
                } else { 						// 减
                    if (shop.shopNumber <= 1) {
                        shop.shopNumber = 1;
                        alert('只有一件商品啦~');
                        return;
                    }
                    shop.shopNumber -= 1;
                }
                // 2.1 计算总价
                this.getAllShopPrice();
            },

            // 3. 全选
            selectedAll(flag) {
                // 3.1 属性控制
                this.isSelectedAll = !flag;
                // 3.2 遍历购物车中所有的商品数据
                this.shopListArr.forEach((value, index) => {
                    // 3.3 判断
                    if (typeof value.checked === 'undefined') { // 当前对象中没有该属性
                        this.$set(value, 'checked', !flag);
                    } else {
                        value.checked = !flag;
                    }
                });
                // 3.3 计算总价
                this.getAllShopPrice();
            },

            // 4. 单个商品的选中和全校选中
            singerShopSelected(shop) {
                // 4.1 判断有没有该属性
                if (typeof shop.checked === 'undefined') { 		// 当前对象中没有该属性
                    this.$set(shop, 'checked', true);
                } else {
                    shop.checked = !shop.checked;
                }
                // 4.2 判断是否全选
                this.hasSelectedAll();
                // 4.3 计算总价
                this.getAllShopPrice();
            },

            // 5. 判断是否要全选
            hasSelectedAll() {
                let flag = true;
                this.shopListArr.forEach((value, index) => {
                    if (!value.checked) {
                        flag = false;
                    }
                });
                this.isSelectedAll = flag && this.shopListArr.length > 0;
            },

            // 6. 计算商品的总价格
            getAllShopPrice() {
                let tPrice = 0;
                //  6.1 遍历所有的商品
                this.shopListArr.forEach((value, index) => {
                    // 6.2 判断是否选中
                    if (value.checked) {
                        tPrice += value.shopPrice * value.shopNumber;
                    }
                });
                // 6.3 更新总价格
                this.totalPrice = tPrice;
            },

            //  7. 点击垃圾篓
            clickTrash(shop, event) {
                // 7.1 获取父标签
                let trashes = event.target.parentNode;
                let up = trashes.firstElementChild;
                // console.log(up);

                // 7.2 加过渡
                up.style.transition = 'all .2s ease';
                up.style.webkitTransition = 'all .2s ease';

                // 7.3 实现动画
                up.style.transformOrigin = '0 0.5rem';
                up.style.webkitTransformOrigin = '0 0.5rem';
                up.style.transform = 'rotate(-45deg)';
                up.style.webkitTransform = 'rotate(-45deg)';
                this.up = up;

                // 7.4 显示面板
                this.$refs.panel.style.display = 'block';
                this.$refs.panelContent.className = 'panel-content jump';

                // 7.5 计算要被删除的商品
                this.currentDelShop = shop;
            },

            // 8. 点击取消
            hidePanel(){
                // 8.1 面板隐藏
                this.$refs.panel.style.display = 'none';
                this.$refs.panelContent.className = 'panel-content';

                // 8.2 盖子闭合
                this.up.style.transform = 'rotate(0deg)';
                this.up.style.webkitTransform = 'rotate(0deg)';
            },

            // 9. 删除当前的商品
            delShop(){
                // 9.1 隐藏面板
                this.$refs.panel.style.display = 'none';
                // 获取索引
                let index = this.shopListArr.indexOf(this.currentDelShop);
                this.shopListArr.splice(index, 1);

                // 9.2 计算总价
                this.getAllShopPrice();
                this.hasSelectedAll();
            }
        },
        filters: {
            // 格式化金钱
            moneyFormat(money) {
                return '¥' + Number(money).toFixed(2)
            }
        }
    }
</script>

<style scoped>

</style>
## base.css

*, ::before, ::after{
    margin: 0;padding: 0;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    /* 去除移动端点击产生的高亮状态 */
    -webkit-tap-highlight-color: transparent;
}

html{font-size: 10px;font-family: 'Microsoft Yahei', sans-serif;color: #000;}
a{text-decoration: none;color: #666666;}
ul,ol{list-style: none;}
input{
    border: none;
    outline: none;
    /* 针对iOS浏览器, 清除默认非扁平化分格 */
    -webkit-appearance: none;
}

.clearfix::before,
.clearfix::after{
    content: '';
    height: 0;
    line-height: 0;
    display: block;
    visibility: hidden;
    clear: both;
}

[class^='icon-'],
[class*=' icon-']{
    background: url("../images/sprites.png") no-repeat;
    -webkit-background-size: 20rem 20rem;
    background-size: 20rem 20rem;
}
## cart.css

body{
    background-color: #f5f5f5;
    font-size: 1.4rem;
    padding-top: 4.4rem;
}

.header{
    z-index: 999;
}

/*************************导航样式***************************/
.header{
    width: 100%;
    height: 4.4rem;
    background: url("./../images/header-bg.png") repeat-x;
    -webkit-background-size: 0.1rem 4.4rem;
    background-size: 0.1rem 4.4rem;
    position: fixed;
    left: 0;
    top: 0;
}

.header .icon-back,
.header .icon-menu{
    width: 4rem;
    height: 4rem;
    position: absolute;
    top: 0;
    padding: 10px;
    /* 背景图的定位从内容开始计算 */
    -webkit-background-origin: content-box;
    background-origin: content-box;
    /* 背景图从内容开始显示 */
    background-clip: content-box;
}

.header .icon-back{
    left: 0;
    background-position: -2rem 0;
}

.header .icon-menu{
    right: 0;
    background-position: -6rem 0;
}

.header h3{
    width: 100%;
    height: 4.4rem;
    line-height: 4.4rem;
    text-align: center;
    /*background-color: red;*/
    padding-left: 4rem;
    padding-right: 4rem;
    overflow: hidden;
    font-size: 1.6rem;
    color: #666666;
}

.header form{
    width: 100%;
    height: 4.4rem;
    padding-left: 4rem;
    padding-right: 4rem;
}

.header form input{
    width: 100%;
    height: 3.4rem;
    border: 1px solid #e0e0e0;
    margin-top: 0.5rem;
    padding-left: 0.5rem;
}

/* 安全提示 */
.jd-safe-tip{
    height: 3.6rem;
    line-height: 3.6rem;
    background-color: #fff;
    border-bottom: 1px solid #e0e0e0;
    text-align: center;
}

.jd-safe-tip .tip-word{
    position: relative;
    /*background-color: red;*/
    /* 改变标签类型 */
    display: inline-block;
}

.jd-safe-tip .tip-word::before{
    content: '';
    width: 1.8rem;
    height: 1.8rem;
    background: url("./../images/safe_icon.png ") no-repeat;
    -webkit-background-size: 1.8rem 1.8rem;
    background-size: 1.8rem 1.8rem;
    position: absolute;
    left: -2.1rem;
    top: 0.9rem;
}

/* 列表内容 */
.jd-shop-cart-list{
   padding-bottom: 6rem;
}

.jd-shop-cart-list section{
    margin-top: 1.5rem;
    border-top: 0.1rem solid #e0e0e0;
    background-color: #fff;
}

.jd-shop-cart-list section .shop-cart-list-title{
    display: flex;
    justify-content: space-between;
    height: 4.4rem;
    line-height: 4.4rem;
}

.jd-shop-cart-list section  .shop-cart-list-title .left{
    flex: 1;

    /*background-color: red;*/
    padding-left: 8px;

    display: flex;
    /*justify-content: space-between;*/
    align-items: center;
}

.jd-shop-cart-list section .cart-logo{
    background: url("./../images/buy-logo.png") no-repeat;
    -webkit-background-size: 1.5rem 1.5rem;
    background-size: 1.5rem 1.5rem;
    width: 1.5rem;
    height: 1.5rem;
    margin: 0 0.5rem;
}

.cart-check-box{
    background: url("./../images/shop-icon.png ") no-repeat;
    -webkit-background-size: 5rem 10rem;
    background-size: 5rem 10rem;
    width: 2rem;
    height: 2rem;
}

.cart-check-box[checked]{
    background-position: -2.5rem 0;
}

.jd-shop-cart-list section  .shop-cart-list-title .right{
    /* background-color: red; */
    flex: 1;
    color: red;
}

.shop-cart-list-con{
    /* background-color: red; */
    /* 伸缩布局 */
    display: flex;
    height: 10rem;
    border-bottom:  0.1rem solid #e0e0e0;
    margin-bottom: 0.7rem;
}

.shop-cart-list-con .left{
    /* background: purple; */
    flex: 1;
    display: flex;
    /* justify-content: center; */
}

.shop-cart-list-con .left a{
    display: inline-block;
    margin-top: 0.5rem;
    margin-left: 0.7rem;
}


.shop-cart-list-con .center{
    /* background: blue; */
    flex: 3;
}

.shop-cart-list-con .center img{
    width: 100%;
    height: 85%;
}

.shop-cart-list-con .right{
    /* background: orangered; */
    flex: 9;
    display: flex;
    flex-direction: column;
    margin-left: 0.5rem;
    margin-right: 0.5rem;

    position: relative;
}

.shop-cart-list-con .right a{
    height: 4rem;
    line-height: 2rem;
    overflow: hidden;
    /* background-color: red; */
    margin-bottom: 0.3rem;
}

.shop-cart-list-con .right .shop-deal span{
    border: 1px solid #e0e0e0;
    display: inline-block;
    width: 3rem;
    height: 2.5rem;
    line-height: 2.5rem;
    text-align: center;
    float: left;
}

.shop-cart-list-con .right .shop-deal span:first-child{
    border-top-left-radius: 0.3rem;
    border-bottom-left-radius: 0.3rem;
}

.shop-cart-list-con .right .shop-deal span:last-child{
    border-top-right-radius: 0.3rem;
    border-bottom-right-radius: 0.3rem;
}

.shop-cart-list-con .right .shop-deal input{
    border-top: 1px solid #e0e0e0;
    border-bottom: 1px solid #e0e0e0;
    float: left;
    width: 5rem;
    height: 2.5rem;
    text-align: center;
}

.shop-cart-list-con .right .shop-deal-right{
    width: 3rem;
    height: 3rem;
    /*background-color: red;*/
    position: absolute;
    right: 0.5rem;
    bottom: 0.1rem;
}

.shop-cart-list-con .right .shop-deal-right span:first-child{
    background: url("./../images/delete_up.png") no-repeat;
    -webkit-background-size: 1.8rem 0.4rem;
    background-size: 1.8rem 0.4rem;
    width: 1.8rem;
    height: 0.4rem;
    display: block;
    margin: 0 auto;
}

.shop-cart-list-con .right .shop-deal-right span:last-child{
    background: url("./../images/delete_down.png") no-repeat;
    -webkit-background-size: 1.7rem 1.7rem;
    background-size: 1.7rem 1.7rem;
    width: 1.7rem;
    height: 1.7rem;
    display: block;
    margin: -0.3rem auto 0;
}

.shop-price{
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.5rem;
}

.shop-price .total-price{
    color: red;
}

/* 面板 */
.panel{
    width: 100%;
    height: 100%;
    position: fixed;
    left: 0;
    top: 0;
    background-color: rgba(0, 0, 0, .6);
    z-index: 1000;
}

.panel-content{
    width:84%;
    position: absolute;
    left:8%;
    top: 200px;
    background-color: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 5px;
    padding: 15px;
}

.panel-title{
    text-align: center;
    font-size: 17px;
    padding-bottom: 30px;
    border-bottom: 1px solid #e0e0e0;
    margin-bottom: 10px;
}

.panel-footer{
    width: 100%;
    height: 50px;
    /* background-color: green; */
}

.panel-footer a{
    width: 120px;
    height: 40px;
    border: 1px solid #e0e0e0;
    margin-top: 10px;
    text-align: center;
    line-height: 40px;
    font-size: 18px;
    border-radius: 5px;
}

.panel-footer .cancel{
    float: left;

}

.panel-footer .submit{
    float: right;
    background-color: #E9232C;
    color:#fff;
    border: none;
}

.panel-is-show{
   display: none;
}

/* 实现动画效果 */
.jump{
    animation: jump 1s ease;
}

@keyframes jump {
    0%{
        opacity: 0;
        transform: translateY(-300rem);
        -webkit-transform: translateY(-300rem);
    }

    25%{
        opacity: 0.3;
        transform: translateY(1rem);
        -webkit-transform: translateY(1rem);
    }

    50%{
        opacity: 0.6;
        transform: translateY(3rem);
        -webkit-transform: translateY(3rem);
    }

    80%{
        opacity: 0.8;
        transform: translateY(-1rem);
        -webkit-transform: translateY(-1rem);
    }

    90%{
        opacity: 1;
        transform: translateY(0.5rem);
        -webkit-transform: translateY(0.5rem);
    }

    100%{
        opacity: 1;
        transform: none;
        -webkit-transform: none;
    }
}

/* 底部通栏 */
#tab_bar{
    position: fixed;
    left:0;
    bottom:0;
    width:100%;
    height: 44px;
    background-color: #fff;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: 5px 5px 5px #000;
}

.tab-bar-left{
    display: flex;
    align-items: center;
    margin-left: 7px;
}

.tab-bar-left .select-all{
    margin-left: 8px;
    font-size: 16px;
}

.tab-bar-right .pay{
    width: 90px;
    height: 44px;
    background-color: #E9232C;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 18px;
    color: #fff;
}

运行结果:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • HTML5+CSS3项目实战之河马牙医首页、百度首页、Mac桌面、简书首页、登录注册页面、苏宁易购首页

    魏晓蕾
  • HTML5项目实战之旅行社网站——移动端流体布局

    魏晓蕾
  • 【Hive】Hive介绍及Hive环境搭建

    版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/...

    魏晓蕾
  • 从项目中学习HTML+CSS

    最近由于工作原因以及自己的懈怠,已经很久都没有更新过博客了。通过这段时间,我发现坚持一件事情是真的很难,都说万事开头难,但是在放弃这件事上好像开头了后面就顺理成...

    Masimaro
  • CSS简笔画logo系列:纯CSS绘制“Adidas” Logo

    看图很简单咯,Adidas Logo就是用3个“梭形”组成,然后添加3条和底色一样颜色的线覆盖在上面即可。

    Javanx
  • Vue 2.x折腾记 - (20) JSX在业务中的具体实践以及跟React书写的差异化

    Vue的jsx,能够支持部分vue独有的特性,比如拿到computed, 指令及自定义事件;

    CRPER
  • 百度百科的一个小效果

    百度百科的一个小效果,感觉不错,取下来保存 方法就是普通的onMouseOve事件 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTM...

    练小习
  • 个人总结(css3新特性)

    css3这个相信大家不陌生了,是个非常有趣,神奇的东西!有了css3,js都可以少写很多!我之前也写过关于css3的文章,也封装过css3的一些小动画。个人觉得...

    守候i
  • 可视化格式模型-clear特性

    ‘clear’特性 该特性表明一个元素框的哪一边不可以和先前的浮动框相邻。’clear’特性不考虑它自身包含的浮动子元素和不处于同一个Block formatt...

    练小习
  • Hive基础学习

    假设我们现在建立一张student表,它有两个字段,id(int)和name(string)。

    超哥的杂货铺

扫码关注云+社区

领取腾讯云代金券