# mt-app **Repository Path**: denyan7373/mt-app ## Basic Information - **Project Name**: mt-app - **Description**: Vue2.x高效还原美团外卖App - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-08-15 - **Last Updated**: 2021-11-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Vue2.x高效还原美团外卖App ## 介绍 这是一个利用vue+express仿美团外卖app的项目。需要有[vue](https://cn.vuejs.org/)和[es6](http://es6.ruanyifeng.com/)的基础,[express](http://expressjs.com/zh-cn/)会一点点就行。 本代码来源于腾讯课堂米斯特吴的课程,如有需要,请[购买课程](https://ke.qq.com/course/464832#term_id=100556268) ## 最终效果展示 先上最终效果图: ![](./static/img/display.gif) ## 初始化框架 这是Vue CLI 2.X的创建方式,建议用最新的 [Vue CLI](https://cli.vuejs.org/zh/guide/creating-a-project.html) `vue init webpack mt-app` ## mock数据及配置路由 ### SVG转成图标 [将SVG矢量图转成图标](https://icomoon.io/app/#/select) 将下载的 `style.css` 放在 `src/common/css/icon.css` 中 `fonts` 文件夹拷贝到 `src/common` 下 ### mock数据 在 `build/webpack.dev.conf.js` 中加载data数据 ```js cd mt-app npm install npm run dev ``` 访问以下网址可查看mock的数据 [localhost:8080/api/goods](localhost:8080/api/goods) [localhost:8080/api/ratings](localhost:8080/api/ratings) [localhost:8080/api/seller](localhost:8080/api/seller) ### css样式重置 [css样式重置](https://meyerweb.com/eric/tools/css/reset/) ```bash mkdir static/css touch static/css/reset.css ``` 在 `index.html` 中添加 `` ### 配置路由 在 `src/components` 中添加5个组件 ``` bash mkdir src/components/header src/components/nav src/components/goods src/components/ratings src/components/seller touch src/components/header/Header.vue touch src/components/nav/Nav.vue touch src/components/goods/Goods.vue touch src/components/ratings/Ratings.vue touch src/components/seller/Seller.vue ``` 在 `src/App.vue` 中引入 `Header` 和 `Nav`,并注册组件 `npm install vue-router --save` 在 `src/main.js` 中 ```js import VueRouter from 'vue-router'; Vue.use(VueRouter) ``` 实例化组件 ```js import Goods from '@/components/goods/Goods'; import Ratings from '@/components/ratings/Ratings'; import Seller from '@/components/seller/Seller'; ``` 创建Routes ```js const routes = [ {path:"/",redirect:"/goods"}, // 重定向 {path:"/goods",component:Goods}, {path:"/ratings",component:Ratings}, {path:"/seller",component:Seller} ] ``` 实例化router ```js const router = new VueRouter({ routes }) ``` 挂载实例 ```js new Vue({ el: '#app', router, components: { App }, template: '' }) ``` 为了实现点击可跳转,设置tab页选中的样式,设置tab的下划线。 需要在 `src/main.js` 中添加属性 `linkActiveClass` ```js const router = new VueRouter({ routes, linkActiveClass:"active" // 点击选项卡颜色变化 }) ``` 代码见 `src/components/nav/Nav.vue` ## 头部结构和样式设计 引入下载好的css `@import url(../../common/css/icon.css);` 此时会报错:找不到font,需要修改 `icon.css`,在5个 `fonts` 前加上 `../` 即可 ### 顶部通栏 响应式导航,搜索框的宽度占100%,左右两侧用padding撑开,放置按钮 1. 向左的返回箭头 2. 搜索框 3. 拼单 & 更多 ### 主题内容 1. 麦当劳图标 2. 餐厅名称 3. 五角星图片 & 收藏 ### 公告内容 1. "首"字 2. 公告信息 3. 有?个活动 & 右箭头,点击可展开【公告详情】 ### 背景内容 1. 背景图片 2. 利用computed返回 `background-image` ### 公告详情 1. 过渡动画 `transition` 2. 蒙版,用 `v-show` 来控制蒙版的显示/隐藏 3. 相关内容容器 + M图片 + 餐厅名称 + 星级评价,引入了 `app-star` 组件 + 起送、配送、30分钟 + 配送时间 + 新用户折扣信息 4. 关闭内容容器 代码见 `src/components/header/Header.vue` 和 `src/components/star/Star.vue` ## 点菜页面设计(核心功能) 分类列表为左侧的导航栏,商品列表为右侧的食物列表。分类列表和商品列表均由**专场**和**具体分类**组成 ### 分类列表 1. 整体布局采用 `flex` 2. 高度设置原则:距离顶部190px,底部51px,超出的部分hidden 3. 左侧导航,宽度固定为85px,菜单名字超过2行部分,用...代替 ```css -webkit-line-clamp: 2; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; ``` 4. 右侧食物的宽度,会随着屏幕宽度自适应拉伸 5. 设置icon大小 ### 商品列表 滚动组件:[better-scroll](https://github.com/ustbhuangyi/better-scroll) `npm install better-scroll --save` ```js import BScroll from 'better-scroll' // 实例化滚动容器,fetch数据后,执行滚动方法 this.menuScroll = new BScroll(this.$refs.menuScroll) this.foodScroll = new BScroll(this.$refs.foodScroll,{ probeType:3, // 不仅在屏幕滑动的过程中,而且在momentum滚动动画运行过程中实时派发scroll事件 click:true // better-scroll默认会阻止浏览器的原生click事件。当设置为true,better-scroll会派发一个click事件,激活+/-组件的点击事件 }) // foodScroll 监听事件 this.foodScroll.on("scroll",(pos) => { this.scrollY = Math.abs(Math.round(pos.y)) }) ``` 右侧滚动联动左侧菜单, 需在 `this.$nextTick()` 中执行 1. 计算分类的区间高度 2. 监听滚动的位置,注意[scroll](https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/events.html#scroll)事件中的 `probeType`以及[click](https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options.html#click)事件 3. 根据滚动位置确认下标,与左侧对应(类似轮播图),见 `currentIndex` 属性 4. 通过下标实现点击左侧,滚动右侧(类似轮播图),见 `@click="selectMenu()"` 方法,重点关注[scrollToElement](https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/api.html#scrolltoelementel-time-offsetx-offsety-easing)方法 代码见 `src/components/goods/Goods.vue` ## 购物车(核心功能) ### 创建购物车组件 ```bash # 创建购物车组件 mkdir src/components/shopcart touch src/components/shopcart/Shopcart.vue ``` 购物车,使用flex布局,底部左侧 `content-left` 随着屏幕宽度拉伸,底部右侧 `content-right`固定宽度110px,数据从 `Goods.vue` 传递给 `Shopcart.vue` **底部左侧**: 购物车icon & 另需配送费,需引入图标库 `@import url(../../common/css/icon.css);` 样式有两种:购物车为空(灰色)/不为空(黄色) **底部右侧**: 0元起送/去结算 ### 创建+/-组件 ```bash # 创建+/-组件 mkdir src/components/cartcontrol touch src/components/cartcontrol/CartControl.vue ``` 在 `Goods.vue` 中注册 `app-cart-control` 组件,并进行父子组件传值。 因为 `better-scroll` 默认会阻止浏览器的原生click事件,所以为了激活+/-组件的点击事件,必须给`this.foodScroll` 显示的指定 `click:true`。 `+号` 底色为黑色,`-号` 底色为灰色。 当 `count` 属性不存在时,利用 `Vue.set(this.food,"count",1)` 将count添加到 `this.food` 这个对象中,而且是响应式的。 当购物数量减为0时,通过 `v-show` 让 `-号` 和 `数量` 消失,通过 `transition` 让-号产生滚动的动画。 ### 让+/-组件与购物车联动 联动通过 `Goods.vue` 中的 `selectFoods` 计算属性来实现。再利用父子组件传值的方式,将 `selectFoods` 传递给购物车组件 `app-shopcart`,并计算出加入购物车的总个数 `totalCount` 以及加入购物车的总价格 `totalPrice`,总个数和总价格分别用 `v-show="totalCount"` 和 `v-show="totalPrice"` 控制显示/隐藏。 ### 让+/-组件与左侧导航栏联动 当购物车数量增减后,想让左侧导航栏右上角的数字产生联动效果,需修改`Goods.vue` 中的分类列表——除【专场】外的导航——`calculateCount(item.spus)`。此时,导航栏右上角会显示的单个分类的购物车总数量。父级需加上相对定位,不然影响导航栏右上角的数字的显示位置,也就是 `menu-item` 的样式需要加上 `position: relative;` 代码见 `src/components/goods/Goods.vue` 和 `src/components/shopcart/Shopcart.vue` 和 `src/components/cartcontrol/CartControl.vue` ## 购物车列表(核心功能) 点击购物车,弹框显示购物列表。用 `@click="toggleList"` 和 `v-show="listShow"` 控制购物车列表的显示/隐藏。点击购物车时,购物车列表上方会出现一个蒙版,点击蒙版,会隐藏购物车列表。给父级加上 `class="shopcart"`,蒙版的样式 `class="shopcart-mask"`, 通过 `v-show="listShow"` 和 `@click="hideMask"` 控制蒙版的显示/隐藏。蒙版的 `z-index: 98;`, `shopcart-wrapper` 的 `z-index: 99;` 在 `Shopcart.vue` 中添加【购物车列表】即可,一共由4个部分组成 1. 顶部 `list-top`,包括新用户优惠详情,用 `v-if="poiInfo.discounts2"` 控制显示/隐藏 2. 头部 `list-header`,包括1号口袋和清空购物车。清空购物车可通过 `@click="clearAll"` 实现。 3. 内容 `list-content`,包括左侧的商品名称、单位/描述(2选1显示)、价格以及右侧的+/-号组件 `CartControl.vue`。使用 `` 传参。 商品名称、单位/描述(2选1显示)、价格可通过遍历 `selectFoods` 拿到数据。其中 `transform: translateY(-100%);` 表示内容的高度=列表撑开的高度,但是列表的最大高度为360px,超出的部分用 `better-scroll` 滚动显示。 4. 底部 `list-bottom`,这个好像没用到,注释也没啥影响 代码见 `src/components/shopcart/Shopcart.vue` ## 商品详情页面(核心功能) 点击单个食物,会跳转到商品详情页面 在 `Goods.vue` ——具体的商品列表中添加点击事件 `@click="showDetail(food)"`,再传递给 `` 组件。利用 `ref="foodView"`属性实现父级调用子级的方法。 ```bash mkdir /src/components/productDetail touch /src/components/productDetail/ProductDetail.vue ``` 商品详情页面的动画依旧使用了 `transition` ```html
……
``` 页面结构: 1. 4个图片:食物大图、关闭按钮、分享按钮、更多按钮 2. 商品名称、月售、网友推荐(用v-show控制显示/隐藏)、价格、计价单位。 如果已加入购物车(count>0),就显示+/-号组件,否则显示【选规格】。【选规格】绑定了一个点击事件 `@click="addProduct"` 去加载+/-号组件,需利用 `Vue.set(this.food,"count",1)` 添加 `count` 属性。 3. 外卖评价 这里有两个坑要处理: 1. 在父页面点击+/-号组件时,也会跳转到商品详情页面(此时你只是想加购物车,并不想跳转到商品详情页面)。因此,我们需要阻止当前的冒泡事件。需修改 `src/components/cartcontrol/CartControl.vue` 中+号的点击事件为 `@click.stop.prevent="increaseCart"`。也就是点击+号之外的任何地方,都会跳转到商品详情页面,而点击+号只会加入购物车,不进行页面的跳转。关闭按钮添加一个点击事件 `@click="closeView"` 即可。 2. 网络请求的图片,需要在跳转到商品详情页面前,给img容器预留空间。当图片没有网络请求下来前,预留位置,请求下来后,将图片填充到预留位置上。否则底下的名称、价格、评价等会往上跑,布局会很丑。 ```css .food .food-wrapper .food-content .img-wrapper{ position: relative; width: 100%; /* 会根据当前的宽度,撑开一个等宽的高度,形成一个正方形的容器 */ height: 0; padding-top: 100%; } ``` 代码见 `src/components/goods/Goods.vue` 和 `src/components/productDetail/ProductDetail.vue` ## 商品详情(评价列表) 商品详情页面中,当商品无评价时,显示空白。有评价时,显示相应的评价信息 待解决的问题: 1. 评价过多,需滚动显示,在 `showView()` 中初始化 `better-scroll`,需设置 `ref="foodView"` 2. 评价和内容之间,需添加分割线。而分割线有很多地方要用,将分割线抽离成一个组件比较合适 ```bash # 新建分割线组件 mkdir src/components/split touch src/components/split/Split.vue ``` 在 `ProductDetail.vue` 中引入并注册组件后,可通过 `` 插入分割线 外卖评价的结构,均需要加上 `v-if="food.rating"` 做判断,否则拿不到数据,因为数据嵌套层数过深(unbelievable) ```html
``` 代码见 `src/components/productDetail/ProductDetail.vue` 和 `src/components/split/Split.vue`,。 ## 评价页面 评价的Tab页,标题上需显示数量。需要在进入app时,就获取到值。所以要在 `App.vue` 的 `created()` 中请求 `ratings`,再将 `commentNum` 传递给组件 `` ### 评价页面的结构 ```html
``` 五角星组件需要传递 `score` 值:`` ### 3个Tab页 设置3个常量: const ALL = 2 // 全部 const PICTURE = 1 // 有图 const COMMENT = 0 // 点评 对应的样式分别为: ```html :class="{'active':selectType==2}" :class="{'active':selectType==1}" :class="{'active':selectType==0}" ``` 通过 `selectTypeFn()` 方法来确定哪个Tab页的样式需要高亮。好聪明的做法~~~ 当切换到【点评】Tab时,点评前的图片为黑色,否则为黄色。用 `v-show="selectType == 0` 控制样式 ### 标签 只展示 `label_star > 0` 的标签,遍历 `labels` 数组即可。 ### 评价列表 当全部/有图/点评3个Tab切换时,评价列表的内容也要跟着改变,通过计算属性 `selectComments` 来实现,过滤一下数据即可。 内容过多时,需滚动显示。还是熟悉的配方,还是熟悉的套路 ```js this.$nextTick(()=>{ if(!this.scroll){ this.scroll = new BScroll(this.$refs.ratingView,{ click:true }) }else{ this.scroll.refresh() } }) ``` 评价列表项跟 `ProductDetail.vue` 几乎一模一样,copy过来,改下遍历的数据源就行。 评价时间戳需利用 `selectComments()` 转换成 `yyyy.MM.dd` 格式 代码见 `src/App.vue` 和 `src/components/ratings/Ratings.vue` ## 商家页面 ### 功能点 商家店铺图片,实现横向滚动 ```js this.$nextTick(() => { // 判断当前数组是否有图片 if(this.seller.poi_env.thumbnails_url_list){ // 获取单张图片的可视宽度 let imgW = this.$refs.picsItem[0].clientWidth // margin-right let marginR = 11 // 整个横向滚动条的宽度 = (单张图片的宽度 + margin-right的宽度) * 图片的张数 let width = (imgW + marginR) * this.seller.poi_env.thumbnails_url_list.length this.$refs.picsList.style.width = width + "px" this.scroll = new BScroll(this.$refs.picsView,{ // 横向滚动 scrollX:true }) } // 让整个商家页面,可以纵向滚动 this.sellerView = new BScroll(this.$refs.sellerView) }) ``` ### 商家页面的结构 ```html
``` ## 路由优化和项目调试 优化点: 1. 在 `App.vue` 中添加 `keep-alive` (用于保留组件状态或避免重新渲染),可避免每次tab页切换都去请求网络数据(F12——Network——刷新) & 切换Tab页后,在返回上一个Tab时,购物车数据已清空。相当于用户点菜后,看了一眼评价信息,就要重新加入购物车,重新点菜,这谁受得了! ```html ``` 2. 非模拟器调试时,点击左侧导航栏,右侧会联动,但是联动错位了。需修改 `Goods.vue` 中 `better-scroll` 的初始化条件 ```js this.menuScroll = new BScroll(this.$refs.menuScroll,{ click:true }) ``` 3. 非模拟器调试时,点击 `-号`,会弹开商品详情页面(本来应该只减少购物车的数量),需修改 `CartControl.vue` 中 `-号` 的点击事件中,阻止冒泡事件,即 `@click.stop.prevent="decreaseCart"` 4. 模拟器测试时,点击左侧导航栏后,右侧没有联动,说明左侧导航栏没有添加点击事件。修改同第2点 ## 真机调试 你可能需要了解一下:[移动端调试神器](https://juejin.im/post/5b72e1f66fb9a009d018fb94) 真机测试,要想获取远程的IP地址(在手机的浏览器中输入该IP地址可访问app),需要将电脑和手机处于同一WiFi环境下。 MAC:系统偏好设置——网络——状态,就能看到一个IP地址,如:`192.168.0.102` 此时你在浏览器输入`192.168.0.102:8081`,是不能直接访问的。 需要修改 `config/index.js` 文件 1. 将 `port` 改成 `8081` (因为利用weinre进行真机测试时端口号会冲突,weinre的默认端口为8080) 2. 将 `host` 改成 `0.0.0.0` 3. 修改config后,必须要重启项目 4. 此时在电脑和手机的浏览器中输入:`192.168.0.102:8081` 均可以看到app页面了 假设模拟器测试正常(浏览器可通过F12标记元素),而真机测试发现有问题,你就需要借助移动端调试的工具了。以 `weinre` 举例 ```bash sudo npm install -g weinre weinre --boundHost 192.168.0.102 # 回车会显示一个地址:http://192.168.0.102:8080 ``` 拷贝【Target Script】——【Example】底下的 `` 至代码根目录的 `index.html` 文件的 `` 中。远程调试后,记得删除该 `` 刷新浏览器,点击【Access Points】——【debug client user interface】后的 `http://192.168.0.102:8080/client/#anonymous`,刷新手机,【Targets】底下就会出现内容了。此时你就可以在电脑上操控手机了,点击【Elements】可调试,跟浏览器F12的效果差不多。 ## 项目打包并运行调试 打包项目前,先检查 `config/index.js` 下 `build` 环境的参数,将`productionSourceMap` 改成 `false` ,避免打包的时候生成很多sourcemap的、协助我们调试的内容,毕竟很占空间。 `npm run build` 后会生成一个 `dist` 文件夹。 此时会有一个提示,打包的文件需要在服务环境下运行,直接打开 `index.html` 无效。 ```bash Tip: built files are meant to be served over an HTTP server. Opening index.html over file://won't work ``` 在项目根目录创建一个 `app-server.js` ```js var express = require('express'); var port = 8088; var app = express(); var router = express.Router(); router.get('/', function(req,res,next){ req.url = '/index.html'; next(); }); app.use(router); // 接口数据 // 1、读取json数据 var goods = require("./data/goods.json") var ratings = require("./data/ratings.json") var seller = require("./data/seller.json") // 2、路由 var routes = express.Router(); // 3、编写接口 routes.get('/goods', (req,res) => { res.json(goods); }); routes.get('/ratings', (req,res) => { res.json(ratings); }); routes.get('/seller', (req,res) => { res.json(seller); }); app.use('/api',routes); // 定义static目录,指向./dist目录 app.use(express.static('./dist')); // 启动express module.express = app.listen(port, function(err){ if(err){ console.log(err); return; } console.log('http://localhost:' + port + '/n'); }); ``` 运行 `node app-server.js`,回车后在浏览器打开 `http://localhost:8088` 进行调试