用了十多天看完一份vue实战视频,实现了一个仿饿了么外卖APP。
部分总结以及部分实现如下:
1.App.vue在HTML中使用router-link标签来导航,默认被渲染成一个a标签,通过传入to属性指定链接;
商品
评论
商家
2.在main.js中配置路由
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
}
];
const router = new VueRouter({
routes: routes
});
const app = new Vue({
el: '#app',
template: ' ',
components: {App},
router
});
3.在App.vue中添加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
1.接收父组件传来的数据
props: {
seller: {
type: Object
}
}
2.写模板以及方法
在img中引入图片来源:
如果有优惠活动,显示第一个优惠活动:data.json中定义了优惠活动的type序号,对应classMap中的活动名称
{{seller.supports[0].description}}
created() { this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; },
点击展开活动详情及商家公告:弹出层绝对定位,@click控制弹出层的v-show的true或false值,并设置默认值为false
{{seller.supports.length}}个
methods: {
showDetail() {
this.detailShow = true;
}
}
data() { return { detailShow:false }; }
遍历优惠活动的小图标及文字:在公共样式表中封装bg-image()样式,在不同的分辨率下显示不同大小的图标,通过优惠活动的type选择不同的样式
-
{{seller.supports[index].description}}
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.vue
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
width: 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中循环,若是优惠活动,则加上小图标,并获得数组索引,在计算属性中获取内容栏滚动位置的索引,改变菜单栏的背景颜色
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的元素,内容栏滚动到该元素
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组件中的变量,为商品个数
selectFoods() {
let foods = [];
this.goods.forEach((good) => {
good.foods.forEach((food) => {
if (food.count) {
foods.push(food);
}
});
});
return foods;
}
4.导入food组件,展开菜品详情页,并传入已选中的菜品,ref 被用来给元素或子组件注册引用信息
shopcart.vue
判断是否达到结算要求
{{payDesc}}
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,购物车加入数量及价格
加入购物车
引入ratingSelect组件,显示商品评价的类别及是否只看有内容的评价,v-on接收来自子组件的参数
使用过滤器,引入公共js方法,将时间戳转换为yyyy:MM:dd hh:mm的时间格式
{{rating.rateTime | formatDate}}
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
评论类别选择按钮
{{desc.all}}{{ratings.length}}
将评论类别及是否只显示内容传入父组件
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
商家实景,遍历实景图片,并横向滑动
-
_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'
});
});
}
}