分50個步驟高仿美團外賣(1)

分50個步驟高仿美團外賣(1)

github地址

每個步驟都有對應的一份代碼和截圖,按部就班。



(1)初始化vue項目

效果

環境初始化

  1. 安裝node。官網直達
  2. 安裝淘寶npm鏡像。 npm install -g cnpm --registry=https://registry.npm.taobao.org
  3. 安裝vue腳手架vue-cli。 npm install vue-cli -g

建立項目

  1. 用vue-cli建立項目meituan vue init webpack meituanwaimai
  2. 建立過程當中會提示輸入項目名稱,都直接按enter鍵,vue會自動使用默認的。
  3. 建立過程當中會提示是否須要vue-router、eslint、unit tests等,所有輸入no。

安裝依賴,啓動項目

  1. 切換到項目目錄 cd meituan
  2. 安裝依賴 cnpm install
  3. 啓動運行 cnpm run dev
  4. 若是瀏覽器沒有自動啓動運行,請查看命令行輸出的打印信息,把最後的網址拷貝到瀏覽器地址欄便可。我這裏是 http://localhost:8080
  5. 在瀏覽器中右鍵審查元素(檢查按鈕)打開chrome開發者工具,打開手機預覽模式



(2)實現底部tabbar

效果

新建一個tabbar.vue文件,讓tabbar固定在頁面底部,用flex佈局實現3等份,並經過背景位置來正確顯示每一項。

<template>
  <ul class="tab-bar">
    <li ><i class="icon-index"></i><span>首頁</span></li>
    <li ><i class="icon-order"></i><span>訂單</span></li>
    <li ><i class="icon-mine"></i><span>個人</span></li>
  </ul>
</template>

<script>
export default {
  components: {},
  data () {
    return {}
  },
  props: {},
  watch: {},
  methods: {},
  filters: {},
  computed: {},
  created () {},
  mounted () {},
  destroyed () {}
}
</script>

<style scoped>
.tab-bar {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 49px;
  text-align: center;
  border-top: 1px solid rgb(182, 182, 182);
  background-color: #fcfcfc;
  display: flex;
  flex-direction: row;
}
 li {
  flex: 1;
  color: #999;
}
i {
  display: block;
  width: 25px;
  height: 25px;
  margin: 3px auto;
  background: url('./tabbarBg.png') no-repeat;
  background-size: 25px auto;
}
span {
  display: block;
  font-size: 12px;
}
.icon-index {
  background-position: 0 -75px;
}
.icon-order {
  background-position: 0 -25px;
}
.icon-mine {
  background-position: 0 -125px;
}

</style>

其它

  1. 刪除helloworld.vue 文件
  2. 在app.vue中引入 tabbar.vue 並使用
  3. 在main.js中引入css樣式重置reset.css



(3)使用vue-router實現路由功能,並加上選中高亮樣式

效果

安裝vue-router

cnpm install vue-router --save css

書寫vue-router的代碼

本例的vue-router就是一個vue-router最簡單的用法,把每一個組件對應路徑就行了。而後在main.js中引入它,而後綁定在vue實例中就能夠了。
import Vue from 'vue'
import Router from 'vue-router'

import Index from '@/components/index/index'
import Order from '@/components/order/order'
import Mine from '@/components/mine/mine'

Vue.use(Router)

export default new Router({
  routes: [
    // 根路徑
    {
      path: '/',
      redirect: '/index'
    },
    // 首頁
    {
      path: '/index',
      component: Index
    },

    // 訂單
    {
      path: '/order',
      component: Order
    },
    // 個人
    {
      path: '/mine',
      component: Mine
    }
  ]
})

修改tabbar代碼

選中樣式會默認加上router-link-active樣式,只要在這個樣式中把選中圖片替換掉就好(這裏是改變圖片位置)。

把li替換成router-link,tag表示對應的標籤,to表示點擊它會router-view會顯示的組件html

<template>
  <ul class="tab-bar">
    <router-link tag="li" to="/index"><i class="icon-index"></i><span>首頁</span></router-link>
    <router-link tag="li" to="/order"><i class="icon-order"></i><span>訂單</span></router-link>
    <router-link tag="li" to="/mine"><i class="icon-mine"></i><span>個人</span></router-link>
  </ul>
</template>

<script>
export default {
  components: {},
  data () {
    return {}
  },
  props: {},
  watch: {},
  methods: {},
  filters: {},
  computed: {},
  created () {},
  mounted () {},
  destroyed () {}
}
</script>

<style scoped>
.tab-bar {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 49px;
  text-align: center;
  border-top: 1px solid rgb(182, 182, 182);
  background-color: #fcfcfc;
  display: flex;
  flex-direction: row;
}
 li {
  flex: 1;
  color: #999;
}
i {
  display: block;
  width: 25px;
  height: 25px;
  margin: 3px auto;
  background: url('./tabbarBg.png') no-repeat;
  background-size: 25px auto;
}
span {
  display: block;
  font-size: 12px;
}
.icon-index {
  background-position: 0 -75px;
}
.icon-order {
  background-position: 0 -25px;
}
.icon-mine {
  background-position: 0 -125px;
}
.router-link-active {
    color: #333;
}
.router-link-active .icon-index {
  background-position: 0 -50px;
}
.router-link-active .icon-order {
  background-position: 0 0px;
}
.router-link-active .icon-mine {
  background-position: 0 -100px;
}

</style>

其它

在reset.css中把html、body高度設置爲100%,把app組件的高度也設置爲100%就能全屏高度



(4)引入mint-ui,爲首頁加入輪播圖

效果

安裝mint-ui

cnpm install mint-ui --save vue

在main.js中引入mint-ui

import MintUI from 'mint-ui'
    import 'mint-ui/lib/style.css'
    Vue.use(MintUI)

在index.vue中使用輪播圖組件 mt-swipe

<div class="slider">
  <mt-swipe :auto="3000">
    <mt-swipe-item v-for="item in swipeData" :key="item.pic">
      <img :src="item.pic">
    </mt-swipe-item>
  </mt-swipe>
</div>
    
data () {
    return {
      swipeData: [
        {pic: require('./img/swipe/1.jpg')},
        {pic: require('./img/swipe/2.jpg')}
      ],
    }
}
    
.slider {
  height: 170px;
  font-size: 30px;
  text-align: center;
  overflow: hidden;
}
.slider img {
  width: 100%;
}



(5)添加外賣分類欄

效果

添加分類的數據,並把輪播圖多餘的數據註釋掉

data () {
    return {
      swipeData: [
        {
          pic: require('./img/swipe/1.png')
        },
        // {
        //   pic: require('./img/swipe/1.png')
        // }
      ],
      types:[
        {
          icon: require('./img/types/ms.png'),
          title: '美食'
        },
        {
          icon: require('./img/types/mtcs.png'),
          title: '美團超市'
        },
        {
          icon: require('./img/types/sxgs.png'),
          title: '生鮮果蔬'
        },
        {
          icon: require('./img/types/tdyp.png'),
          title: '甜點飲品'
        },
        {
          icon: require('./img/types/mtzs.png'),
          title: '美團專送'
        },
        {
          icon: require('./img/types/zcyx.png'),
          title: '正餐優選'
        },
        {
          icon: require('./img/types/kcxc.png'),
          title: '快餐小吃'
        },
        {
          icon: require('./img/types/hbps.png'),
          title: '漢堡披薩'
        }
      ]
    }
  }

新建type-item.vue

<template>
  <div class="types-item">
        <img src="../../index/img/types/hbps.png" >
        <span>漢堡披薩</span>
  </div>
</template>


<style scoped>
.types-item {
    float: left;
    width: 25%;
    padding-top: 14px;
}
.types-item img {
  display: block;
    width: 50px;
    margin: 0 auto 12px;
}
.types-item span {
    display: block;
    font-size: 14px;
    line-height: 14px;
    text-align: center;
    color: #2f2f2f;
}

</style>

在index.vue 中引入type-item.vue 並使用

<!-- 種類 -->
<div class="types">
  <types-item ></types-item>
  <types-item ></types-item>
  <types-item ></types-item>
  <types-item ></types-item>

  <types-item ></types-item>
  <types-item ></types-item>
  <types-item ></types-item>
  <types-item ></types-item>
</div>

import typesItem from '@/components/base/types-item/types-item'

其它

引入.vue文件時使用的@是在webpack.base.conf.js文件的alias位置配置的,在須要寫路徑時均可以在這裏配置絕對路勁的簡寫方式



(6)正確顯示分類欄

效果

用正確的數據填充分類的內容

index.vue
<div class="types">
  <types-item v-for="item in types" :icon="item.icon" 
  :title="item.title" :key="item.title"></types-item>
</div>

types-item.vue
<template>
  <div class="types-item">
        <img :src="icon" >
        <span v-text="title"></span>
  </div>
</template>

props: {
    icon:{
      type:String,
      default:''
    },
    title:{
      type:String,
      default:''
    }
},



(7)構造商鋪列表數據,顯示商鋪圖片

效果

商鋪數據+詳細註釋

這裏數據僅僅爲3條,項目中複製了4分共12條數據,列表中須要須要對數據判斷的幾個地方:node

  1. 是否 品牌店、新店
  2. 是否可 領取代金券、返商家券、開發票
  3. 是否美團專送
shopList: [
    {
        "id": 1000,//id
        "type":1,//1表明新店、2表明品牌店、0表明都不是
        "name": "你的名字",//店名
        "delivery_type": 1,//1表明美團專送,0表明不是
        "pic_url": "http://p0.meituan.net/0.84.63/xianfu/54c60749841a6612df373dc259e8da73108176.jpg",//圖片地址
        "avg_delivery_time": 30,//平均送達時間
        "delivery_distance": 2032,//平均送達距離(單位爲米,超過一公里要轉化成公里)
        "min_price": 20,//起送價
        "delivery_price": 3,//配送費
        "wm_poi_score": 4.7,//評分
        "month_sale_num": 33,//月售
        "shop_discount": {
            "code":0,//是否能領代金券
            "dis_money":[2,4]//返回的代金券(多張)
        },
        "shop_return_price": {
            "code":0,//是否返券
            "min_price":20,//返券起始價格
            "dis_money":4//商家返券數
        },
        "shop_return_invoice": {
            "code":0,//是否開發票
            "min_price":0,//開票起始價格
        },   
    },
    {
        "id": 1001,//id
        "type":0,//1表明新店、2表明品牌店、0表明都不是
        "name": "叫了個炸雞(鼓樓店)",//店名
        "delivery_type": 1,//1表明美團專送,0表明不是
        "pic_url": "http://p0.meituan.net/0.84.63/xianfu/00b9386a556a39010186a609ce9289e370566.jpg",//圖片地址
        "avg_delivery_time": 30,//平均送達時間
        "delivery_distance": 2502,//平均送達距離(單位爲米,超過一公里要轉化成公里)
        "min_price": 20,//起送價
        "delivery_price": 3,//配送費
        "wm_poi_score": 4.2,//評分
        "month_sale_num": 3857,//月售
        "shop_discount": {
            "code":1,//是否能領代金券
            "dis_money":[1,5,10]//返回的代金券(多張)
        },
        "shop_return_price": {
            "code":0,//是否返券
            "min_price":20,//返券起始價格
            "dis_money":1//商家返券數
        },
        "shop_return_invoice": {
            "code":0,//是否開發票
            "min_price":0,//開票起始價格
        },   
    },
    {
        "id": 1002,//id
        "type":2,//1表明新店、2表明品牌店、0表明都不是
        "name": "望湘園(南京國際廣場店)",//店名
        "delivery_type": 0,//1表明美團專送,0表明不是
        "pic_url": "http://p1.meituan.net/0.84.63/xianfu/2d91e93c70f91696907f040392c4835f13918.jpg",//圖片地址
        "avg_delivery_time": 30,//平均送達時間
        "delivery_distance": 889,//平均送達距離(單位爲米,超過一公里要轉化成公里)
        "min_price": 20,//起送價
        "delivery_price": 3.5,//配送費
        "wm_poi_score": 4.7,//評分
        "month_sale_num": 1435,//月售
        "shop_discount": {
            "code":0,//是否能領代金券
            "dis_money":[2,4]//返回的代金券(多張)
        },
        "shop_return_price": {
            "code":0,//是否返券
            "min_price":20,//返券起始價格
            "dis_money":4//商家返券數
        },
        "shop_return_invoice": {
            "code":1,//是否開發票
            "min_price":0,//開票起始價格
        },   
    }
]

簡單顯示店鋪圖片和標識

index.vue
<!-- 周邊商鋪 -->
<div class="nearby">
  <shop-list-item v-for="item in shopList" :item= "item"></shop-list-item>
</div>

shop-list-item.vue
<div class="seller-list-item" >
    <div class="left">
<span :class="{pingpai:item.type===2,xindian:item.type===1,hide:item.type===0}">{{item.type===1?"新店":"品牌"}}</span>
        <img :src="item.pic_url">
    </div>
</div>



(8)添加完整的商品列表

效果

主要是佈局,而後根據穿過來的數據顯示就行了,須要稍微注意下的點都在截圖中標出來了

  1. 須要判斷是否顯示的有新店、品牌店、是否可領券、是否支付後返券、是否開發票、是否美團轉送
  2. 須要對字符串處理的有可領的代金券(可能有多張)、店鋪距離(超過1000m要用km顯示)
  3. 美團專送的左上角和右下角的三角形能夠用border的實現
  4. 在求代金券的最大值和最小值的時候用了es6的模式匹配
<div class="seller-list-item" >
    <div class="left">
        <span :class="{pingpai:item.type===2,xindian:item.type===1,hide:item.type===0}">{{item.type===1?"新店":"品牌"}}</span>
      <img :src="item.pic_url">
    </div>

    <div class="content">
      <div class="name">{{item.name}}</div>
      <div class="mid">
        <span class="fl">* * * * *</span>
        <span class="count fl">月售{{ item.month_sale_num }}</span>
        <span class="distance fr">{{ distance }}</span>
        <span class="time fr">{{ item.avg_delivery_time }}分鐘 |</span>
      </div>

      <div class="down">
        <span >起送 {{ item.min_price }} |</span>
        <span >配送 {{ item.delivery_price }} |</span>
        <!-- 數據中忘了平均消費價格,這裏直接用起送價格數據代替 -->
        <span >人均 {{ item.min_price + 6 }}</span>

        <div class="zhuansong" v-show="item.delivery_type === 1">
          <span >美團專送</span>
        </div>
      </div>

      <ul class="activety">
        <li class="jian" v-show="item.shop_discount.code"><img src="./jian.png" ><span>{{replacePrice}}</span></li>
        <li class="fan" v-show="item.shop_return_price.code"><img src="./fan.png" ><span>{{"實際支付" + item.shop_return_price.min_price + "元返" + item.shop_return_price.dis_money +"元商家代金券"}}</span></li>
        <li class="fapiao" v-show="item.shop_return_invoice.code"><img src="./piao.png" ><span>{{"本店支持開發票,開票金額" + item.shop_return_invoice.min_price + "元起"}}</span></li>
      </ul>
    </div>                                 
  </div>
methods: {
    findMaxAndMin(arr) {
      var min = arr[0]
      var max = arr[0]
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] > max) {
          max = arr[i]
        }
        if (arr[i] < min) {
          min = arr[i]
        }
      }
      return {max,min}
    }
  },
computed: {
    // 距離km、m換算
    distance() {
      if (this.item.delivery_distance > 1000) {
        return (this.item.delivery_distance / 1000.0).toFixed(1) + 'km'
      }else {
        return this.item.delivery_distance + 'm'
      }
    },
    // 代金券分一張和多張
    replacePrice () {
      if (this.item.shop_discount.dis_money.length === 1) {
          return "可領" + this.item.shop_discount.dis_money[0] +"元代金券"
      }else {
        let {max,min} = this.findMaxAndMin(this.item.shop_discount.dis_money)
        return "可領" + min + "~" + max +"元代金券"
      }
    }
},
.seller-list-item {
  margin-bottom: 5px;
  display: flex;
  flex-direction: row;
  padding: 12.5px 0 12.5px 0;
  overflow: hidden;
  margin-left:10px;
  border-bottom: solid 1px #eee;
}
/*左側圖片*/
.left {
    position: relative;
  flex: 0 0 86px;
  width: 86px; 
}
.left span {
    display: inline-block;
    text-align: center;
    position: absolute;
    left: 0;
    top: 0;
    width: 28px;
    height: 14px;
    line-height: 14px;
    font-size: 12px;
    color: white;
}
span.hide {
    display: none;
}
span.pingpai {
    background-color: #ffa627;
}
span.xindian {
    background-color: #21c56c;
}
.left img {
    border:solid 1px #e4e4e4;
  display: block;
  width: 84px;
  height: 63px;
  margin: 0 auto;
}

/*右側內容*/
.content {
  /*background-color: red;*/
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 0 15px 0 10px;
}
.content .name {
  font-size: 17px;
  color: #333;
  overflow: hidden;
  font-weight: bold;
  overflow: hidden;
}
/*月售、送達時間、距離*/
.content .mid {
  flex: 1;
  margin-top: 7px;
  color: #666;
  font-size: 12px;
}
.mid .count {
  margin-left: 10px;
}
.mid .distance {
  margin-left: 5px;
  margin-top: 2px;
}
.mid .time {

}
/*起送配送人均*/
.down {
  flex: 1;
  margin-top: 7px;
  font-size: 13px;
  color: #656565;
}
.down .zhuansong {
  float: right;
  font-size: 13px;
  background-color: #ffd161;
  color: #333;
}
.zhuansong span {
  height: 15px;
  line-height: 15px;
  padding: 0 6px;
  display: inline-block;
  position: relative;
}

.zhuansong span::before,
.zhuansong span::after {
    content: '';
    position: absolute;
    border: 3px solid #fff;
    width: 0;
    height: 0;
    font-size: 0;
}

.zhuansong span::before {
  left: 0;
  top: 0;
  border-color: #fff #ffd161 #ffd161 #fff;
}
.zhuansong span::after {
  right: 0;
  bottom: 0;
  border-color: #ffd161 #fff  #fff #ffd161;
}



/*活動*/
.activety {
  flex: 1;
  margin-top: 7px;
  color: #898989;
  font-size: 12px;
  text-align: left;

}
.activety li {
  height: 17px;
  margin-bottom: 4px;
}

.activety li img {
  width: 14px;
  height: 14px;
  vertical-align: middle;
}

.activety li span {
  vertical-align: middle;
  margin-left: 6px;
}



(9)五角星組件和完善頁面

效果

評分星星組件

把星星分爲全星、半星、和無星星三種,好比3.7分就是3個全星星、1個半星星、一個無星星
<div>
    <i class="starItem" :class="{half:(score<1 && score> 0),zero:(score <= 0)}"></i>
    <i class="starItem" :class="{half:(score<2 && score> 1),zero:(score <= 1)}"></i>
    <i class="starItem" :class="{half:(score<3 && score> 2),zero:(score <= 2)}"></i>
    <i class="starItem" :class="{half:(score<4 && score> 3),zero:(score <= 3)}"></i>
    <i class="starItem" :class="{half:(score<5 && score> 4),zero:(score <= 4)}"></i>
</div>

.starItem {
    background: url(./star.png) no-repeat;
  background-size: cover;
  width: 10px;
  height: 10px;
  float: left;
  margin-right: 4px;
  background-position: 0 0;
}
.half {
  background-position: -14px 0;
}
.zero {
  background-position: -28px 0;
}

其它

  1. 跟商鋪列表加上了頭部 (注意頭部左右兩邊的線是經過插入空內容加border實現的)
  2. 在列表後面加了一個空的div,以避免滑動到最底下出現部分被遮擋bug
  3. 在商鋪列表加了分割條



(10)mock首頁數據

效果

打開自動打開瀏覽器功能

新版vue-cli關閉了自動啓動瀏覽器,能夠經過吧build/config/index.js 中大概第18行的 autoOpenBrowser: false,改成true,而後從新啓動npm run dev ,就能自動打開瀏覽器了

mock首頁數據

在webpack.dev.conf.js 中加入代碼返回數據,webpack3內置了express,before方法的參數app就是express的實例對象
before(app) {
  var shops = require('../mock/shop.json')

  app.get('/api/shops', function(req, res) {
    res.json({
      code: 0,
      data: shops
    });
  });
}
安裝axios,在main.js中引入axios

cnpm i axios --save webpack

import axios from 'axios'
Vue.prototype.axios = axios
在index.vue中獲取數據
this.axios.get('/api/shops',{
    params: {

    }
}).then(res =>{
    if (res.data.code === 0) {
        this.shopList = res.data.data
    }
}).catch(error => {
    console.log(error)
})
相關文章
相關標籤/搜索