仿饿了么外卖APP,部分总结以及部分实现如下:
App.vue
1.App.vue在HTML中使用router-link标签来导航,默认被渲染成一个a标签,通过传入to属性指定链接;
<div class="tab-item"> <router-link to="/goods">商品</router-link> </div> <div class="tab-item"> <router-link to="/ratings">评论</router-link> </div> <div class="tab-item"> <router-link to="/seller">商家</router-link> </div>
渲染后:
2.在main.js中配置路由
- 导入Vue和VueRouter,并调用Vue.use(VueRouter)
- 定义组件:import子组件
import Vue from 'vue'; import VueRouter from 'vue-router'; import goods from 'components/goods/goods.vue'; import ratings from 'components/ratings/ratings.vue'; import seller from 'components/seller/seller.vue'; import App from './App.vue';
- 定义路由:每个路由映射一个组件
const routes = [ { path: '/goods', component: goods }, { path: '/ratings', component: ratings }, { path: '/seller', component: seller } ];
- 创建router实例:传入routes配置
const router = new VueRouter({ routes: routes });
- 创建和挂载根实例:通过router配置参数注入路由,从而让整个应用都有路由功能
const app = new Vue({ el: '#app', template: '<App/>', components: {App}, router });
3.在App.vue中添加router-view(最顶层的出口,渲染最高级路由匹配到的组件),使用keep-alive,把切换出去的组件保存在内存中,保留它的状态或避免重新渲染
<keep-alive> <router-view :seller="seller"></router-view> </keep-alive>
4.引入data.json中的数据
created() { this.$http.get('/api/seller').then((response) => { response = response.body; if (response.errno === ERR_OK) {//const ERR_OK = 0; this.seller = response.data; } }); }
5.引入header组件,并将seller数据传给header组件
在JS中import header组件,并在components中注册,在template模板中使用header
<v-header :seller="seller"></v-header>
header.vue
1.接收父组件传来的数据
props: { seller: { type: Object } }
2.写模板以及方法
在img中引入图片来源:
<img width="64" height="64" :src="seller.avatar">
如果有优惠活动,显示第一个优惠活动:data.json中定义了优惠活动的type序号,对应classMap中的活动名称
<div v-if="seller.supports" class="support"> <span class="icon" :class="classMap[seller.supports[0].type]"></span> <span class="text">{{seller.supports[0].description}}</span> </div>
created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; },
点击展开活动详情及商家公告:弹出层绝对定位,@click控制弹出层的v-show的true或false值,并设置默认值为false
<div class="support-count" v-if="seller.supports" @click="showDetail"> <span class="count">{{seller.supports.length}}个</span> <i class="icon-keyboard_arrow_right"></i> </div>
methods: { showDetail() { this.detailShow = true; } }
<div class="detail" v-show="detailShow">
data() { return { detailShow:false }; }
遍历优惠活动的小图标及文字:在公共样式表中封装bg-image()样式,在不同的分辨率下显示不同大小的图标,通过优惠活动的type选择不同的样式
<ul v-if="seller.supports" class="supports"> <li class="support-item" v-for="(item,index) in seller.supports"> <span class="icon" :class="classMap[seller.supports[index].type]"></span> <span class="text">{{seller.supports[index].description}}</span> </li> </ul>
bg-image($url) background-image:url($url+"@2x.png") @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3) background-image:url($url+"@3x.png")
&.decrease bg-image('decrease_2') &.discount bg-image('discount_2') &.guarantee bg-image('guarantee_2') &.invoice bg-image('invoice_2') &.special bg-image('special_2')
3.引入star组件,传入star图标的size和score
<star :size="48" :score="seller.score"></star>
star.vue
<div class="star" :class="starType"> <span v-for="itemClass in itemClasses" :class="itemClass" class="star-item" track-by="$index"></span> </div>
1.在计算属性中,starType根据传入的size来定义样式
starType() { return 'star-' + this.size; }
2.itemClasses返回评星状态,根据传入的score来计算满星、半星和无星的个数,result数组装入星星的状态
const CLS_ON = 'on'; const CLS_OFF = 'off'; const CLS_HALF = 'half';
itemClasses() { let result = []; let score = Math.floor(this.score * 2) / 2; let hasDecimal = score % 1 !== 0; let integer = Math.floor(score); for (let i = 0; i < integer; i++) { result.push(CLS_ON); } if (hasDecimal) { result.push(CLS_HALF); } while (result.length < LENGTH) { result.push(CLS_OFF); } return result; }
3.:class="itemClass"表示遍历出的星星的状态,状态不同,显示星星的状态图标即不同,class为on即显示满星,以此类推
&.star-48 .star-item 20px height: 20px margin-right: 22px background-size:20px 20px &:last-child margin-right:0 &.on bg-image("star48_on") &.half bg-image("star48_half") &.off bg-image("star48_off")
goods.vue
1.通过props导入数据并在li中循环,若是优惠活动,则加上小图标,并获得数组索引,在计算属性中获取内容栏滚动位置的索引,改变菜单栏的背景颜色
<ul> <li v-for="(item,index) in goods" class="menu-item" :class="{'current':currentIndex===index}" @click="selectMenu(index,$event)"> <span class="text border-1px"> <span v-show="item.type>0" class="icon" :class="classMap[item.type]"></span>{{item.name}} </span> </li> </ul>
currentIndex() { for (let i = 0; i < this.listHeight.length; i++) { let height1 = this.listHeight[i]; let height2 = this.listHeight[i + 1]; if (!height2 || (this.scrollY >= height1 && this.scrollY < height2)) { return i; } } return 0; }
2.绑定菜单栏和内容栏的点击及滚动:菜单栏绑定点击事件,获取点击的index的元素,内容栏滚动到该元素
<li v-for="item in goods" class="food-list food-list-hook">
selectMenu(index, event) { if (!event._constructed) { return; } let foodList = this.$refs.foodsWrapper.getElementsByClassName('food-list-hook'); let el = foodList[index]; this.foodsScroll.scrollToElement(el, 300); }
3.引入购物车组件,并传入选中的food以及配送费、最低配送价格,food.count为cartcontrol组件中的变量,为商品个数
<shopcart :selectFoods="selectFoods" :deliveryPrice="seller.deliveryPrice" :minPrice="seller.minPrice"></shopcart>
selectFoods() { let foods = []; this.goods.forEach((good) => { good.foods.forEach((food) => { if (food.count) { foods.push(food); } }); }); return foods; }
4.导入food组件,展开菜品详情页,并传入已选中的菜品,ref 被用来给元素或子组件注册引用信息
<food :food="selectedFood" ref="food"></food>
shopcart.vue
判断是否达到结算要求
<div class="pay" :class="payClass"> {{payDesc}} </div>
payDesc() { if (this.totalPrice === 0) { return `¥${this.minPrice}元起送`; } else if (this.totalPrice < this.minPrice) { let diff = this.minPrice - this.totalPrice; return `还差¥${diff}元起送`; } else { return '去结算'; } }, payClass() { if (this.totalPrice < this.minPrice) { return 'not-enough'; } else { return 'enough'; } }
food.vue
点击加入购物车,商品数量显示为1,购物车加入数量及价格
<div class="cartcontrol-wrapper"> <cartcontrol :food="food"></cartcontrol> </div> <div @click="addFirst" class="buy" v-show="!food.count || food.count===0">加入购物车</div>
引入ratingSelect组件,显示商品评价的类别及是否只看有内容的评价,v-on接收来自子组件的参数
<ratingselect v-on:ratingTypeSelect="chooseType" v-on:contentToggle="chooseOnly" :selct-type="selectType" :only-content="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
使用过滤器,引入公共js方法,将时间戳转换为yyyy:MM:dd hh:mm的时间格式
<div class="time">{{rating.rateTime | formatDate}}</div>
import {formatDate} from '../../common/js/date';
//date.js
export function formatDate(date, fmt) { if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); } let o = { 'M+' :date.getMonth() + 1, 'd+' :date.getDate(), 'h+' :date.getHours(), 'm+' :date.getMinutes(), 's+' :date.getSeconds() }; for (let k in o) { if (new RegExp(`(${k})`).test(fmt)) { let str = o[k] + ''; fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str)); } } return fmt; }; function padLeftZero(str) { return ('00' + str).substr(str.length); }
filters: { formatDate(time) { let date = new Date(time); return formatDate(date, 'yyyy-MM-dd hh:mm'); } }
实现父组件与子组件通信,选择评论类别,评论改变
chooseType(type) { this.selectType = type; this.$nextTick(() => { this.scroll.refresh(); }); }, chooseOnly(onlyContent) { this.onlyContent = onlyContent; this.$nextTick(() => { this.scroll.refresh(); }); }
ratingSelect.vue
评论类别选择按钮
<span @click="select(2,$event)" class="block positive" :class="{'active':selectType===2}">{{desc.all}}<span class="count">{{ratings.length}}</span></span>
将评论类别及是否只显示内容传入父组件
select(type, event) { if (!event._constructed) { return; } this.selectType = type; this.$emit('ratingTypeSelect', type); }, toggleContent(event) { if (!event._constructed) { return; } this.onlyContent = !this.onlyContent; this.$emit('contentToggle', this.onlyContent); }
seller.vue
商家实景,遍历实景图片,并横向滑动
<div class="pic-wrapper" ref="picWrapper"> <ul class="pic-list" ref="picList"> <li class="pic-item" v-for="pic in seller.pics"> <img :src="pic" width="120" height="90"> </li> </ul> </div>
_initPics() { if (this.seller.pics) { let picWidth = 120; let margin = 6; let width = (picWidth + margin) * this.seller.pics.length - margin; this.$refs.picList.style.width = width + 'px'; this.$nextTick(() => { this.picScroll = new BScroll(this.$refs.picWrapper, { scrollX: true, eventPassScroll: 'vertical' }); }); } }
转自:http://blog.csdn.net/qq_14863671/article/details/54412254