仿肯德基宅急送App-Vue實戰

前言

Vue學習有一段時間了,就想着用Vue來寫個項目練練手,弄了半個月,到今天爲止也算勉強能看了。
因爲不知道怎麼拿手機App的接口,而且KFC電腦端官網真的...一言難盡,因此項目全部數據都是我截圖而後寫在EasyMock裏的,有須要的同窗能夠自取javascript

首頁 商品頁 外賣頁css


技術棧

vue + webpack + vuex + axioshtml

文件目錄

│  App.vue
│  main.js
│
├─assets
│      logo.png
│
├─components
│  │  cartcontrol.vue
│  │  code.vue
│  │  coupon.vue
│  │  mineHeader.vue
│  │  scroll.vue
│  │  shopHeader.vue
│  │  sidebar.vue
│  │  submitBar.vue
│  │  takeout.vue
│  │  wallet.vue
│  │
│  └─tabs
│          Other.vue
│          Outward.vue
│          Selfhelp.vue
│          Vgold.vue
│
├─pages
│  ├─home
│  │      home.vue
│  │
│  ├─mine
│  │      mine.vue
│  │
│  ├─order
│  │      order.vue
│  │
│  └─shop
│          shop.vue
│
├─router
│      index.js
│
└─vuex
    │  store.js
    │  types.js
    │
    └─modules
            com.js
            cou.js
            take.js
複製代碼

效果展現

定義的組件

better-scroll

由於每一個頁面都須要滑動,因此一開始就把scroll組件封裝好,以後使用的話引入一下就好了vue

<template>
  <div ref="wrapper">
    <slot></slot>
  </div>
</template>

<script>
import BScroll from 'better-scroll';
const DIRECTION_H = 'horizontal';
const DIRECTION_V = 'vertical';
export default {
  name: 'scroll',
  props: {
    /**
      * 1 滾動的時候會派發scroll事件,會節流。
      * 2 滾動的時候實時派發scroll事件,不會節流。
      * 3 除了實時派發scroll事件,在swipe的狀況下仍然能實時派發scroll事件
      */
    probeType: {
      type: Number,
      default: 1
    },
    /**
      * 點擊列表是否派發click事件
      */
    click: {
      type: Boolean,
      default: true
    },
    /**
      * 是否開啓橫向滾動
      */
    scrollX: {
      type: Boolean,
      default: false
    },
    /**
      * 是否派發滾動事件
      */
    listenScroll: {
      type: Boolean,
      default: false
    },
    /**
      * 列表的數據
      */
    data: {
      type: Array,
      default: null
    },
    pullup: {
      type: Boolean,
      default: false
    },
    pulldown: {
      type: Boolean,
      default: false
    },
    beforeScroll: {
      type: Boolean,
      default: false
    },
    /**
      * 當數據更新後,刷新scroll的延時。
      */
    refreshDelay: {
      type: Number,
      default: 20
    },
    direction: {
      type: String,
      default: DIRECTION_V
    }
  },
  methods: {
    _initScroll() {
      if(!this.$refs.wrapper) {
        return
      }
      this.scroll = new BScroll(this.$refs.wrapper, {
        probeType: this.probeType,
        click: this.click,
        eventPassthrough: this.direction === DIRECTION_V ? DIRECTION_H : DIRECTION_V
      })
      // 是否派發滾動事件
      if (this.listenScroll) {
        this.scroll.on('scroll', (pos) => {
          this.$emit('scroll', pos)
        })
      }
      // 是否派發滾動到底部事件,用於上拉加載
      if (this.pullup) {
        this.scroll.on('scrollEnd', () => {
          if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
            this.$emit('scrollToEnd')
          }
        })
      }

      // 是否派發頂部下拉事件,用於下拉刷新
      if (this.pulldown) {
        this.scroll.on('touchend', (pos) => {
          // 下拉動做
          if (pos.y > 50) {
            this.$emit('pulldown')
          }
        })
      }

      // 是否派發列表滾動開始的事件
      if (this.beforeScroll) {
        this.scroll.on('beforeScrollStart', () => {
          this.$emit('beforeScroll')
        })
      }
    },
    disable() {
      // 代理better-scroll的disable方法
      this.scroll && this.scroll.disable()
    },
    enable() {
      // 代理better-scroll的enable方法
      this.scroll && this.scroll.enable()
    },
    refresh() {
      // 代理better-scroll的refresh方法
      this.scroll && this.scroll.refresh()
    },
    scrollTo() {
      // 代理better-scroll的scrollTo方法
      this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
    },
    scrollToElement() {
      // 代理better-scroll的scrollToElement方法
      this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
    },
  },
  mounted() {
    setTimeout(() => {
      this._initScroll()
    },20)
  },
  watch: {
    data () {
      setTimeout(() => {
        this.refresh()
      },this.refreshDelay)
    }
  },
}
</script>

<style>

</style>
複製代碼

slot 插槽是一塊模板,顯示不顯示,以及怎樣顯示由父組件來決定, 也就是把你想要滑動的區域插進去,剩下的內容都是官方文檔定義好的,複製一遍就行了java

固定頭部

頭部相對頁面是固定的,這裏我把頭部都封裝成了組件,在主頁面引入頭部,要滑動的部分放入上面定義好的scroll組件便可webpack

側邊欄以及彈出框

起初個人想法是用router-link直接跳轉,而後發現這樣作頁面會自帶導航欄,因而我決定經過CSS動態綁定來實現它
<template>
  <div class="sidebar">
    <div class="sidebar-con" :class="{showbar: showSidebar}">
      <div class="navbar_left" @click="backTo">
        <img src="../pages/mine/zuo.png" alt="">
      </div>
        <van-tree-select :height="850" :items="items" :main-active-index="mainActiveIndex" :active-id="activeId" @navclick="onNavClick" @itemclick="onItemClick"/>
    </div>
  </div>
</template>
複製代碼

樣式用的是Vant UI組件,最外面綁定了一個動態樣式showbar,而後把總體的初始位置設在屏幕以外,當傳入參數爲true時再回來,用Vuex管理它的狀態ios

.sidebar-con {
  position: absolute;
  top: 0;
  left: -400px;
  transform: translateZ(0);
  opacity: 0;
  width: 100%;
  z-index: 1002;
  height: 100%;
  overflow: auto;
  transition: all 0.3s ease;
}
.showbar {
  transform: translateX(400px);
  opacity: 1;
}
複製代碼

Vuex狀態管理

const state = {
  showSidebar: false
}

const mutations = {
  [types.COM_SHOW_SIDE_BAR] (state, status) {
    state.showSidebar = status
  }
}

const actions = {
  setShowSidebar ({commit}, status) {
    commit(types.COM_SHOW_SIDE_BAR, status)
  }
}

const getters = {
  showSidebar: state => state.showSidebar
}
複製代碼

用mapGetter拿到對象,而後傳給computed屬性,對象能夠直接使用git

computed: {
    ...mapGetters([
      'showSidebar'
    ])
  },
複製代碼

當須要顯示的時候使用dispatch將參數傳入 this.$store.dispatch('setShowSidebar', true)github

總體代碼web

<template>
  <div class="sidebar">
    <div class="sidebar-con" :class="{showbar: showSidebar}">
      <div class="navbar_left" @click="backTo">
        <img src="../pages/mine/zuo.png" alt="">
      </div>
        <van-tree-select :height="850" :items="items" :main-active-index="mainActiveIndex" :active-id="activeId" @navclick="onNavClick" @itemclick="onItemClick"/>
    </div>
  </div>
</template>

<script> import { TreeSelect } from 'vant'; import { mapGetters } from 'vuex'; export default { data() { return { }, ], // 左側高亮元素的index mainActiveIndex: 0, // 被選中元素的id activeId: 1 }; }, computed: { ...mapGetters([ 'showSidebar' ]) }, methods: { onNavClick(index) { this.mainActiveIndex = index; }, onItemClick(data) { this.activeId = data.id; this.$emit('active', data.text) this.$store.dispatch('setShowSidebar', false) }, backTo(){ this.$store.dispatch('setShowSidebar', false) }, } } </script>

<style scoped> .sidebar-con { position: absolute; top: 0; left: -400px; transform: translateZ(0); opacity: 0; width: 100%; z-index: 1002; height: 100%; overflow: auto; transition: all 0.3s ease; } .showbar { transform: translateX(400px); opacity: 1; } .navbar_left { background-color: #da3a35; } .navbar_left img { width: 25px; height: 25px; margin-left: 3vw; margin-top: 5px; } </style>
複製代碼

外賣點餐

這裏參考的是慕課網黃軼大大的課程,課程地址

商品展現

<template>
  <div class="takeout" :class="{showtakeout: showTakeout}">
    <div class="goods">
      <div class="header">
        <div class="navbar_left" @click="backTo">
          <img src="../pages/shop/zuo.png" alt="">
        </div>
        <div class="appointment">
          <div class="btn">
            <div class="yy">預定</div>
            <div class="Kcoffee">K咖啡</div>
          </div>
          <div class="bag">
            <router-link style="color: #000" to="/coupon">
              <div class="bagtext">
                卡包<p>3</p></div>
            </router-link>
          </div>
        </div>
      </div>
      <div class="goodList">
        <div class="menu-wrapper" ref="menuWrapper">
          <ul>
            <li v-for="(item,index) in goods" :key="index" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)" >
              <span class="text border-1px">
                {{item.name}}
              </span>
            </li>
          </ul>
        </div>
        <div class="foods-wrapper" ref="foodsWrapper">
          <ul>
            <li v-for="(item,index) in goods" :key="index" class="food-list" ref="foodList">
              <h1 class="title">{{item.name}}</h1>
              <ul>
                <li v-for="(food,index) in item.foods" :key="index" class="food-item border-1px" @click="selectFood(index, $event)" >
                  <div class="icon">
                    <img :src="food.image">
                  </div>
                  <div class="content">
                    <h2 class="name">{{food.name}}</h2>
                    <div class="price">
                      <span class="now">¥{{food.price}}</span>
                    </div>
                    <div class="cartcontrol-wrapper">
                      <cartcontrol @add="addFood" :food="food"></cartcontrol>
                    </div>
                  </div>
                </li>
              </ul>
            </li>
          </ul>
        </div>
      </div>
      <submit-bar ref="shopcart" :selectFoods="selectFoods"></submit-bar>
    </div>
  </div>
</template>
複製代碼

這裏經過currentIndex和index作對比,來確認是否添加current類,經過添加current類來實現當前頁面的區域的樣式變化,他們之間的對比關係也就是menu區域和foods區域的顯示區域的對比關係

須要注意的是vue傳遞原生事件使用$event

<script>
import BScroll from 'better-scroll'
import cartcontrol from './cartcontrol'
import submitBar from './submitBar'
import { mapGetters } from 'vuex'
export default {
  name: 'takeout',
  data() {
    return {
      goods: [],
      listHeight: [],
      scrollY: 0
    }
  },
  components: {
    cartcontrol,
    submitBar
  },
  computed: {
    ...mapGetters([
      'showTakeout'
    ]),
    currentIndex () {
      for(let i = 0; i < this.listHeight.length; i++) {
        let height1 = this.listHeight[i - 1]
        let height2 = this.listHeight[i]
        if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) {
          return i
        }
      }
      return 0
    },
    selectFoods () {
      let foods = []
      this.goods.forEach(good => {
        good.foods.forEach(food => {
          if (food.count) {
            foods.push(food)
          }
        })
      })
      return foods
    }
  },
  methods: {
    backTo () {
      this.$store.dispatch('setShowTakeout', false)
    },
     selectMenu(index, event) {
      if (!event._constructed) {
        return;
      }
      let foodList = this.$refs.foodList;
      let el = foodList[index];
      this.foodsScroll.scrollToElement(el, 300);
    },
    selectFood(food, event) {
      if (!event._constructed) {
        return;
      }
      this.selectedFood = food;
    },
    _initScroll() {
      this.meunScroll = new BScroll(this.$refs.menuWrapper, {
        click: true
      })
      this.foodsScroll = new BScroll(this.$refs.foodsWrapper, {
        click: true,
        probeType: 3
      })
      this.foodsScroll.on('scroll', pos => {
        this.scrollY = Math.abs(Math.round(pos.y))
      })
    },
    _calculateHeight () {
      let foodList = this.$refs.foodList
      let height = 0
      for (let i = 0; i < foodList.length; i++) {
        let item = foodList[i]
        height += item.clientHeight
        this.listHeight.push(height)
      }
    },
  },
  created () {
    this.$http.get('https://www.easy-mock.com/mock/5ca49494ea0dc52bf3b67f4e/example/takeout')
      .then(res => {
        if (res.data.errno === 0) {
          this.goods = res.data.data
          this.$nextTick(() => {
            this._initScroll()
            this._calculateHeight()
          })
        }
      })
  }
}
</script>
複製代碼

購物車

<template>
<div class="submitBar">
  <van-submit-bar :loading="setloading" :price="totalPrice" button-text="提交訂單" @submit="onSubmit" >
  <div class="shoppingCart" @click="toggleList">
    <img src="../../images/gwc.png" alt="">
    <span v-if="selectFoods.length > 0">{{selectFoods.length}}</span>
  </div>
</van-submit-bar>
 <transition name="fold">
    <div class="shopcart-list" v-show="listShow">
      <div class="list-header">
        <h1 class="title">購物車</h1>
        <span class="empty" @click="empty">清空</span>
      </div>
      <div class="list-content" ref="listContent">
        <ul>
          <li class="food" v-for="(food, index) in selectFoods" :key="index">
            <span class="name">{{food.name}}</span>
            <div class="price">
              <span>¥{{food.price*food.count}}</span>
            </div>
            <div class="cartcontrol-wrapper">
              <cartcontrol @add="addFood" :food="food"></cartcontrol>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </transition>
  <transition name="fade">
    <div class="list-mask" @click="hideList" v-show="listShow"></div>
  </transition>
</div>
</template>
複製代碼

購物車列表的顯示和隱藏以及清空按鈕是經過數據fold來決定的,購物車列表是經過計算屬性listshow來實現,清空按鈕也是經過設置count屬性來實現,這樣都達到了不用操做dom就能夠改變dom行爲的效果。

<script>
import { SubmitBar } from 'vant';
import BScroll from 'better-scroll';
import cartcontrol from './cartcontrol';
export default {
  props: {
    selectFoods: {
      type: Array,
      default() {
        return [
          {
            price: 10,
            count: 1
          }
        ]
      }
    },
  },
  data() {
    return {
      setloading: false,
      fold: true
    }
  },
  computed: {
    totalCount () {
      let count = 0
      this.selectFoods.forEach((food) => {
        count += food.count
      })
      return count
    },
    totalPrice () {
      let total = 0
      this.selectFoods.forEach((food) => {
        total += food.price * food.count * 100
      })
      return total
    },
    listShow () {
      if (!this.totalCount) {
        this.fold = true
        return false
      }
      let show = !this.fold
      if (show) {
        this.$nextTick(() => {
          if (!this.scroll) {
            this.scroll = new BScroll(this.$refs.listContent, {
              click: true
            })
          } else {
            this.scroll.refresh()
          }
        })
      }
      return show
    }
  },
  methods: {
    toggleList(){
      console.log(this.totalCount)
      if (!this.totalCount) {
        return;
      }
      this.fold = !this.fold;
    },
    onSubmit() {
      this.setloading = true
    },
    empty() {
      this.selectFoods.forEach((food) => {
        food.count = 0;
      });
    },
    hideList() {
      this.fold = true;
    },
    addFood() {}
  },
  components: {
    cartcontrol
  }
}
</script>
複製代碼

操做按鈕

這個模塊主要經過三個小模塊實現,刪除按鈕,顯示數量塊,增長按鈕

<template>
  <div class="cartcontrol">
    <transition name="move">
      <div class="cart-decrease" v-show="food.count > 0" @click="decreaseCart">
        <div class="inner">
          <img width="15px" height="15px" src="../../images/jian.png" alt="">
        </div>
      </div>
    </transition>
    <div class="cart-count" v-show="food.count > 0">{{food.count}}</div>
    <div class="cart-add" @click="addCart">
      <img width="15px" height="15px" src="../../images/add.png" alt="">
    </div>
  </div>
</template>
複製代碼

addCart以及decreaseCart方法,默認會傳入event原生dom事件,food數據是從父組件傳入的,因此對這個數據的修改,也可以反應到父組件,也由於購物車的數據也是從父組件傳入的,使用同一個food數據,從而關聯到購物車的購買數量統計。

<script>
export default {
 name: "cartcontrol",
  props: {
    food: {
      type: Object
    }
  },
  data() {
    return {

    }
  },
  methods: {
    addCart (event) {
      console.log(event)
      if (!event._constructed) {
        return
      }
      if (!this.food.count) {
        this.$set(this.food, 'count', 1)
      } else {
        this.food.count++
      }
      this.$emit('add', event.target)
    },
    decreaseCart (event) {
      if (!event._constructed) {
        return
      }
      if (this.food.count) {
        this.food.count--
      }
    }
  },
}
</script>
複製代碼

異步問題

<div class="various" v-for="(item,index) in various" :key="index">
    <div class="title">
      <div class="strip"></div>
      <p>{{item[0].name}}</p>
      <div class="strip"></div>
    </div>
    <div class="various_img">
      <div class="various_title">
        <img :src="item[0].urll" alt="">
      </div>
      <div ref="listwrapper" class="index">
          <div class="various_list">
            <div class="various_box" v-for="(u,i) in item.slice(1)" :key="i">
              <img :src="u.url" alt=""> 
            </div>
          </div>
        </div>
      </div>
    </div>
複製代碼

這裏循環嵌套,整個DOM結構都是循環出來的,而better-scroll須要操做DOM結構,要實現橫向滑動效果,不免會有異步問題。
但是不管我使用.then或者$nextTick都沒法掛載better-scroll,查閱了大量文檔也沒法解決,最後只能使用原生的overflow-X,如果有解決辦法,歡迎提出,感激涕零!

結語

總的來講這個項目還有不少不足,實現的功能也不多,後續我會繼續改進。
若是這篇文章對你有幫助,不妨點個贊吧!
GitHub地址

相關文章
相關標籤/搜索