一、前言

项目源自网上一个Vue项目。原项目是基于Vue Cli 2 并且采用了UI组件库。为了练手,我采用原生CSS写样式,然后自己进行了响应式设计。后台应用我是直接拿来用的,后期复习到Node的时候我会自己来做一遍。我做这个项目最重要的目的是把学过的理论知识实践一下,虽然当前还有一些bug还没修复,但是我想先把做过的东西总结一下。

前端项目地址:vue-shop
后端项目地址:vue-shop-server

当前完成的功能展示如下:


当前bug待解决:

  • 浏览器首次打开手机模拟器时,首页swiper以及better-scroll都无法滑动,刷新后就没事了,而我在自己的手机Chrome浏览器测试完全正常。

  • 内置浏览器版本过低,打开失败。

功能待完善:

  • 搜索列表显示。
  • 订单组件显示购物车食物。

下面会挑选我觉得项目中比较重要的地方来讲解。

二、使用Vue Cli 3构建项目

项目启动了半个月后,我发现低版本包传到Github上面后有安全问题,于是我决定将原来的版本更新到 Vue Cli 3,这里采用图形界面的方式创建新项目。

vue ui

环境的搭建我相信大家看过很多文章了,所以我不再赘述。创建项目过程中,会让你选一些工具,它们可以集成到项目中去,我选的是:

  • Babel
  • Vuex
  • Router
  • CSS Pre-processors(CSS预处理器)

然后下一步选择预处理器类型,代码风格采用哪种模式,是否使用history模式的路由,最后选择什么时候自动修复错误格式的代码,我的选择如下:


点击创建项目后,稍等片刻就好啦。然后你此次的配置会保存一个预设,你下次再次创建的时候就可以使用这个预设了。我简单介绍一下脚手架的目录。

这款风格是带有分号的,如嫌麻烦可以在刚开始创建项目的时候选择标准模式(standard)。

三、项目配置

注意:本文提及的所有模块的已经默认安装好了。

3.1 环境变量配置

项目启动的时候不同的环境会载入不同的环境变量。官方提供了以下几种方式来载入环境变量:

.env   # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 在特定环境下载入,由mode决定
.env.[mode].local # 在特定环境下载入,由mode决定,但会被 git 忽略

被指定模式的环境变量比没有指定的优先级要高,但如果脚手架工具启动时已经存在一个环境变量,它将用有最高优先级。那他们的环境是如何确定的呢?

脚手架工具中在process.env中已经定义好了一个环境变量NODE_ENV。我们可以在package.json中的script属性中指定--mode参数来指定NODE_ENV的值从而确定一个模式,比如我们指定一个alpha模式:

"script": {"alpha": "vue-cli-service build --mode test"
}

脚手架工具已经集成了三种模式:developmentproductiontest,通过运行以下命令来启动不同环境,然后加载.env.*文件载入环境变量:

  • development 模式用于 vue-cli-service serve
  • production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e
  • test 模式用于 vue-cli-service test:unit

下面是我项目中的脚本命令:

这三种环境其实已经能满足我们日常开发需求了。项目根目录下我创建了三个文件来载入不同环境的变量:


环境变量命名的风格也是有规定,它是一种键-值的结构,而且最好是VUE_APP_开头,因为如果你在配置文件中使用的话,不是以VUE_APP_开头的变量就不会载入。

我在development环境中定义了两个环境变量:

VUE_APP_BASE = "http://localhost:4000/" # 代理服务器地址
VUE_APP_IMAGE = "https://fuss10.elemecdn.com" # 图片地址

大家可以根据自己的实际开发需求来定义,我为了方便就只模拟了development环境。

3.2 vue.config.js 配置

Vue cli 3 创建的项目会集成一些常见的webpack配置,但是有时候我们还是要自己去配置一些选项,官方提供了vue.config.js,它会在项目启动的时候加载。

我主要配置了三个地方,其他的配置大家可以参考官方文档。

  • 配置全局的stylus样式
  • 配置别名(alias
  • 配置代理(proxy
const path = require('path')function resolve(dir) {return path.join(__dirname, dir)
}// 全局mixins.styl样式
function addStyleResource(rule) {rule.use('style-resource').loader('style-resources-loader').options({patterns: [resolve('src/assets/styles/stylus/index.styl')]})
}module.exports = {lintOnSave: process.env.NODE_ENV !== 'production', // 开发环境保存时开启代码检查productionSourceMap: false, // 生产环境下关闭SourceMapchainWebpack: config => {const types = ['vue-modules', 'vue', 'normal-modules', 'normal']types.forEach(type =>addStyleResource(config.module.rule('stylus').oneOf(type)))config.resolve.alias.set('styles', resolve('src/assets/styles')).set('images', resolve('src/assets/images')).set('components', resolve('src/components'))},devServer: {proxy: {'/': {target: process.env.VUE_APP_BASE, // 代理地址changeOrigin: true // 是否支持跨域}}}
}

3.3 代码风格和约束

不同的团队有不同的规范,这里是我自己个人比较喜欢的代码风格,以后同团队协作开发的时候还是要向团队看齐的。

3.3.1 eslint + prettier配置

有两种方式来创建eslint配置:

  • 内置注释
  • 创建.eslintrc.*文件或在package.json中的eslintConfig属性中配置

我选择的是后者。

 "eslintConfig": {"root": true, // 将ESLint限制在当前项目,不再向上查找配置"env": {"node": true // 指定node环境,使用该环境下全局变量的预设},"extends": ["plugin:vue/essential", // 使用vue插件"@vue/prettier", // prttttier 预设"eslint:recommended" //使用eslint推荐的规则],"rules": { // 拓展或者覆盖默认规则"prettier/prettier": ["error", //被prettier标记的地方抛出错误信息。{"tabWidth": 2, // 几个缩进空格"useTabs": false, // 是否使用tab缩进"semi": false, // 句尾是否有分号"singleQuote": true, // 是否是用单引号"bracketSpacing": true,"jsxBracketSameLine": false,"space-before-function-paren": true}]},"parserOptions": {"parser": "babel-eslint" // 解析器,支持检测ES6+语法},"overrides": [{"files": ["**/__tests__/*.{j,t}s?(x)","**/tests/unit/**/*.spec.{j,t}s?(x)"],"env": {"jest": true}}]}

3.3.2 组件约束

在我的项目中组件分为两种:路由组件UI组件。组件命名规则如下:

  • 组件采用驼峰命名,而且必须是一个文件夹,文件夹里面放一个index.vue存放父组件。
  • UI组件无逻辑的必须是BASE开头。
  • 组件命名不超过三个英文单词。
  • 组件根目录下面有必要的话创建components存放父组件下的其他组件,必须是耦合命名,比如Home父组件下的HomeShoplist


然后就是组件属性的书写顺序(常见的):

  • el (根组件)
  • name
  • components
  • props
  • data
  • computed
  • watch
  • 生命函数钩子
  • methods

四、响应式设计

响应式指的是不同的终端设备下都能很好的显示网页,通常一套代码就可以搞定。

我采用的方案是viewport + vw + rem + media query + %的方式来实现。

4.1 设置meta标签

<meta name="viewport" content="width=device-width,initial-scale=1.0">

它的作用如下:

  • 设置初始视口(visual viewport)大小为设备的宽度
  • 设置初始缩放比例为1.0(作用等同于width=device-width

这个标签只对移动端页面初始渲染的时候起作用,只不过它还不是一个W3C规范。

4.2 响应式单位

除了上面提到的百分比单位%,还有类似有vw,vh,rem,em等常见单位,另外配上媒体查询(media query)和JS来辅助,我们的响应式设计就基本上完美了。

我看了很多文章,下面是我个人比较喜欢的两种方案:

  • 核心:rem + js 配菜:em + media query + %
  • 核心:vw + rem 配菜: em + media query + %

实现思路如下:

  • rem 是相对于html的font-size大小,默认16px(不同浏览器有差异),也就是说1rem = 16px

  • 将布局视口分成100份,动态获取初始布局视口的1%的宽度,将值设置给font-size,这样就得到了不同设备下的单位rem值

  • 算出某个元素和画布(设计稿)的占比,得出rem的单位系数。

下面说说两种方案怎么个整法。

4.2.1 rem + js 方案

(1)动态获取布局视口宽度,将1%的值赋值给html的font-size。

 (function(){var html = document.documentElement;window.addEventListener('resize', function() {html.style.fontSize = html.clientWidth / 100 + 'px';})}())

(2)算出某个元素与设计图的占比,然后把%换成rem即可。这里计算过于繁琐,可以在stylus的中定义一个函数,将设计图某个元素的px转化成rem单位,此后我们只需要在项目中的CSS使用这个函数即可。

// $px 就是设计图某个元素的px
$ueWidth = 375
px2rem($px)(($px * 100 / $ueWidth))rem

这个方案有个缺点,就是JS和CSS有一定的耦合性。如果不考虑这个小缺点,这个方案还是相当不错的。另外说一点就是,font-size是具有继承性的,那整个页面的字体就变得很诡异了。除此之外,字体使用rem单位后,字体并不会线性变化,所以我们需要配菜上场了。

(3)通过媒体查询初始化设备字体大小。

这里分割点大家可以参考设计师或者网上给的设计方案,我这里参考了浏览器手机模拟器上的分割点。

/* 分割点 *//* iphone 5 */
@media screen and (min-width: 320px) {body {font-size: 14px;}
}
/* iphoneX */
@media screen and (min-width: 375px) {body {font-size: 16px;}
}
/* iphone6 7 8 plus */
@media screen and (min-width: 414px) {body {font-size: 18px;}
}
/* ipad */
@media screen and (min-width: 768px) {body {font-size: 20px;}
}
/* ipad pro */
@media screen and (min-width: 1024px) {body {font-size: 24px;}
}

然后所有的子元素的字体使用em相对单位来设置,注意这个单位是相对父级元素的字体大小,当然如果你不想计算,你也可以在stylus定义相关函数。总体来说,虽然有一点点瑕疵,但是这种方案对移动端兼容性很好,应用的人数也很多。

4.2.2 vw + rem

vw是CSS3中出现的,它是视口单位,桌面端指的是浏览器的可视区域;移动端指的就是布局视口。1vw代表布局视口的1%,如果不考虑兼容性,我们给html的font-size为1vw不可以替换上面的那种方案了吗。我们只需要把JS代码去掉就可以了。

html {font-size: 1vw;
}

以上就是我实践的响应式设计,虽然到目前为止还有许多瑕疵,但是我会慢慢完善下去的。

五、对Axios进行二次封装

5.1 axios请求的简单配置

下面是Axios的简单配置,根据项目需求合理地去添加配置,具体可以参考官方文档。

src目录下建立一个utils文件夹,主要是放所有的工具模块,比如正则验证等,然后在此目录下创建request.js

引入相关依赖模块

import Axios from 'axios' // 引入axios库
import router from '@/router' // 引入路由
import store from '@/store'

定义公共变量和函数

const SECONDS = 12 // 请求超时秒数/*** 跳转登录页* 携带当前页面路由,在登录页面完成登录后返回当前页面*/
const toLogin = () => {router.replace({path: '/login',query: {redirect: router.currentRoute.fullPath}})
}/*** 错误统一处理* @param {Number} status 状态码*/
const errorHandle = (status, other) => {// 状态码判断switch (status) {// 401: 未登录状态,跳转登录页case 401:toLogin()break// 404请求不存在case 404:alert('请求的资源不存在')breakcase 500:alert('服务器错误')breakdefault:alert(other)break}
}

注意:这里的错误提示完全可以使用UI组件来替代,我比较懒就写了个alert,其次是状态码,这个需要和后台协商确定。

创建Axios实例


// 创建axios实例
const instance = axios.create({baseURL: '', // 之前已经在环境变量中配置好了,这个必须为空,否则会报严重的错误timeout: 1000 * SECONDS
})

默认配置

instance.default.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

这样配置后,post传递数据默认格式就是类似这样的name=laocao&age=22了,当然还有其他默认配置,根据PM给的需求来定,这是我自己的项目,所以…需求我自己定。

拦截器

/*** 请求拦截器* 每次请求前,如果存在token则在请求头中携带token*/instance.interceptors.request.use(config => {// 登录流程控制中,根据本地是否存在token判断用户的登录情况// 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token// 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码// 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作const token = store.state.tokentoken && (config.headers['Authorization'] = token)return config},error => Promise.error(error)
)// 响应拦截器
instance.interceptors.response.use(// 状态码判断res => (res.status === 200 ? Promise.resolve(res) : Promise.reject(res)),// 请求失败错误处理error => {const { response } = errorif (response) {// 请求已发出,但是不在2xx的范围errorHandle(response.status, response.data.message)return Promise.reject(response)} else {// 处理断网的情况// eg:请求超时或断网时,更新state的network状态// network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏// 关于断网组件中的刷新重新获取数据,会在断网组件中说明if (!window.navigator.onLine) {store.commit('changeNetwork', false)} else {return Promise.reject(error)}}}
)

响应拦截器中的token过期处理和断网这两种情况我没有考虑到项目中。

断网处理具体做法是dispatch一个action或者直接commit一个mutation来改变组件的状态,可以引入相关断网组件提示用户,我这里犯懒就没写了。

导出实例

export default instance

到这里,简单的配置我们算是做完了,实际中肯定不是那么简单的,我也是刚学习,不求快,快则囫囵吞枣会噎着。

5.2 统一管理Api请求

我们需要在src目录下创建一个api文件夹,然后在该目录下创建一个index.js来统一管理api。

引入axios实例

import axios from '@/utils/request'

统一管理url

// 统一url管理
const url = {getLocationUrl: '/position',getShoplistsByLocationUrl: '/shops',...
}

请求函数

请求我只考虑了get和post请求,通过实例来调用axios中的getpost方法就可以实现请求,并且它们都会返回一个Promise对象。为了减少篇幅,就列举三种比较有代表性的请求函数。

/*** 获取食品分类列表** @returns {Promise}*/
function getShopCategoryApi() {return axios.get(url.getShopCategoryUrl)
}/*** 请求手机验证码** @param {Number} phone* @returns {Promise}*/
function getMessageCodeApi(phone) {return axios.get(url.getMessageCodeUrl, {params: {phone}})
}/***  用户使用密码登陆** @param {Object} { name, pwd, captcha }* @returns {Promise}*/
function postUserLoginByPasswordApi({ name, pwd, captcha }) {return axios.post(url.postUserLoginByPasswordUrl, {name,pwd,captcha})
}

注意的是,get请求如果需要传参需要在get方法的第二个参数传入一个对象,该对象有一个params属性,它也是一个对象,参数写在里面即可。post方法和get不同的是,参数直接就在方法的第二个参数写就行。

导出api

最后,我们统一把这些api导出去。

export default {getLocationApi,getShoplistsByLocationApi,getShopCategoryApi,...
}

添加链接描述

5.3 挂载到Vue的原型上

引入

import api from '@/api'

挂载

Vue.prototype.$axios = api // 将所有api请求挂在Vue原型中

使用
以后我们就可以通过以下的方式来访问某个api啦:

this.$api.getLocationApi()

六、Vuex结构设计

项目采用了Vuex来作状态管理,来存储组件的公共状态。我这个项目不是特别大,所以我就没有采用Module方式来进行模块化管理,否则会变得很复杂。但是项目特别大的话,推荐采用模块化管理Vuex的状态。

6.1 目录设计

我们都知道Vuex有一些核心概念:stateactionmutationgetter,它们构成了Vuex整个结构。简单介绍一下,在组件中我们想改变它的状态我们首先需要分发action,它可以是异步的也可以是同步的,然后通过commit触发相关的mutation,从而进行状态更新,最后在组件中通过计算属性响应式来数据然后驱动更新视图。

上面的mutation官方把它近似地看作了事件,我觉得这十分合理。事件具有两个很重要的特点:事件类型和回调函数。所以我们可以专门定义一个mutation-types来管理事件类型。下面我们来说说如何请求数据和管理数据吧。

6.2 流程

我就拿一个例子来说吧,比如现在有一个需求:请求首页食物分类信息数据并展示在页面上。

6.2.1 state

定义好一个categories数组,存放分类信息数据:

export default {categories: [], // 食品分类
}

在需要相关数据的组件中来引入状态:

import { mapState } from 'vuex'// 通过计算属性来响应数据变化
computed: {...mapState(['categories'])
}

当然还有其他方式来引入状态,不过还是要推荐上面那种方式,因为当组件依赖的状态越来越多时,很显然上面的那种方式更好。

6.2.2 mutation-type

接下来,我们要定义一个事件类型来标识事件,从而触发响应类型的状态变更。

export const RECEIVE_CATEGORIES = 'receive_categories' // 接收食物分类信息

6.2.3 mutation

有了事件类型,我们可以根据类型来注册对应的mutation。/我们可以使用 ES6 风格的计算属性命名功能来命名一个事件:

// 引入事件类型
import { RECEIVE_CATEGORIES } from './mutation-type'// 注册事件,回调
export default {// 当RECEIVE_CATEGORIES事件触发时,执行回调改变状态[RECEIVE_CATEGORIES](state, { categories }) {state.categories  = categories }
}

上述回调中,有两个参数:statepayload。它们都是对象,第二个参数来在action提交上来的数据,可以通过解构赋值直接拿到数据。

6.2.4 action

action都是请求数据变更的操作,通常都是异步操作,我们可以借助async和await来拿到数据,并且把数据提交给响应的mutation。这里会有一个小问题就是,我们之前把api挂载到了Vue原型上面,但在Vuex里面的this指向的是store对象,所以我们在index.js需要重新挂载一下。

// 引入事件类型
import { RECEIVE_CATEGORIES } from './mutation-type'
// 请求食物分类信息
async getCategories({ commit }) {const {data: { data }} = await this.$axios.getShopCategoryApi()commit({type: RECEIVE_CATEGORIES,categories: data})
}

我们只需要在组件中通过dispatch就可以触发action了:

mounted() {this.$store.dispatch('getCategories')
}

当然也完全可以采用mapAction的方式:

// 引入mapAction
import { mapActions } from 'vuex'
// 注册函数
methods: {...mapActions(['getCategories'])
}

6.2.5 index

结构写完后,我们需要在index里面把store对象暴露给全局,这样所有组件才能访问到Vuex。getters是计算属性,和state差不多,所以就没说明了。

import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
import api from '@/api'Vuex.Store.prototype.$axios = api // 将所有api请求挂在Vuex原型中
Vue.use(Vuex)export default new Vuex.Store({state,mutations,actions,getters
})

就这样,我们的Vuex设计就结束了,其余的请求无非就是照搬这个模式来做了。

七、使用Mockjs来模拟接口

首先我们需要在根目录下创建mock文件夹,然后在此目录下创建一个data目录,存储不同接口的数据,然后在index中讲mock接口导出,最后在main.js中加载即可。当然,我这个过于简单了,可以通过模块化管理不同的mock数据接口。
`
下面是index中的代码:

import Mock from 'mockjs'
import { goods, ratings, info } from './data/shop.json'// 请求mock接口
Mock.mock('/foods', { code: 0, data: goods })
Mock.mock('/commends', { code: 0, data: ratings })
Mock.mock('/info', { code: 0, data: info })

八、项目难点和问题

8.1 难点

8.1.1 swiper组件分页逻辑

分析

目前有一个iconList数组,这个数组存储了食物分类的信息,但是每一页只能显示8个icon,当数组长度超过了8我们就需要放到下一页显示。那么很显然我们需要一个二维数组,一维数组存储每一页,二维数组存储每一页的icon。我们可以遍历这个数组,通过下标来判断是否需要分页,当下标为8时,说明需要放到第2页显示。

代码实现

// 定义一个计算属性
computed: {page() {let pages = []// 遍历iconListthis.iconList.forEach((item, index) => {// 得到当前页码let page = Math.floor(index / 8)// 创建二维数组if (!pages[page]) {pages[page] = []}pages[page].push(item)})// 返回该二维数组return pages}
}

下面的步骤就是在组件遍历数组了,这个比较简单,就不写代码了。

8.1.3 better-scroll实现分类联动


我希望滑动右边列表然后左边列表切换到对应的分类,并且样式也同步变化,同样的,我点击左边的分类右边也需要滑动到对应的分类位置。

分析

在渲染左边分类列表的时候,每一个分类项都会有一个下标index,当滑动右边分类列表的时候,我们希望动态计算出当前分类的currentIndex是否和左边的index相等,如果相等的话,就给左边当前分类添加样式就可以了。现在问题就是我们该如何动态计算右边当前分类的下标呢?思路是这样的:

  • 获取右边分类初始渲染后每个分类距离父级的高度,并且放到数组中
  • 监听右边分类的滑动事件,实时获取滑动距离
  • 如果滑动的距离在某个高度范围内,计算出那个高度范围内所在的分类下标
  • 判断左边分类下标是否等于右边分类下标决定是否显示样式

而点击左边分类项滑动右边的分类就比较简单了:

  • 得到左边被点击分类的下标
  • 使用scrollTo方法让右边滑动到对应下标的分类最顶端

实现

定义初始距离数据,实时滑动距离


data() {return {scrollY: 0,detailTops: []}
}

获取初始分类高度和实时滑动距离:

methods: {// 初始化Bscroll对象_initDetailScroll() {this.detailScroll = new BScroll('.foods-detail', {probeType: 3, // 滑动类型,必须指定,否则无法滑动click: true // 是否允许原生的click事件})// 监听滑动事件,实时获取滑动距离this.detailScroll.on('scroll', ({ y }) => {this.scrollY = Math.abs(y)})this.detailTops = this._getClientHeight('detail')},_getClientHeight(ele) {const eleLists = [...this.$refs[ele].children]let tempArr = []let top = 0tempArr.push(top) // 将第一个li元素的距离放入数组eleLists.forEach(item => {top += item.clientHeighttempArr.push(top)})return tempArr},
}

定义计算属性currentIndex

computed: {currentIndex() {let index = 0index = this.detailTops.findIndex((ele, index, args) => {return this.scrollY >= ele && this.scrollY < args[index + 1]})return index}
}

而点击左边分类来让右边滑动到对应的位置就很简单了,只需要得出左边分类当前index,然后通过BSroll对象相关API就可以让右边滑动了:

 // 点击左边分类,右边滑到目标位置this.detailScroll.scrollTo(0, -this.detailTops[index], 300)

8.2 问题

8.2.1 使用better-scroll首次加载不滑动

这里我们需要注意Bscroll对象创建的时机,必须是数据请求到了之后才能创建这个对象,我们可以在进行异步请求后执行回调去创建Bscroll对象。但是创建Bscroll对象又涉及到了更新DOM,这又是一个异步的过程,所以我们需要使用this.$nextTick方法。

  mounted() {// 请求数据后才初始化Bscrollthis.$store.dispatch('getShopLists', () => {this.$nextTick(() => {this._initCategoryScroll()this._initDetailScroll()})})},

然后我们在对应的action中进行判断,如果回调存在就执行,否则不执行。

  async getShopLists({ commit }, callback) {const { data } = await this.$axios.getShopFoodListsApi()if (data.code === 0) {commit({type: RECEIVE_SHOP_FOODS,foodLists: data['data']})callback && callback()}},

8.2.2 向已有数据对象添加新属性不会有响应式效果

官方推荐使用Vue.set方法来解决这个问题:

Vue.set(object, property, value)

九、总结

这篇文章写完花了2天多,我写下来发现有很多多余的东西,浪费了不该浪费的时间,写文章真的是一件很有技巧的事情,可能我学的还不够精细。另外,还有很多问题需要解决,我才发现前端这条路上的坑真的够多,没办法既然已经入坑,那就继续玩里头深挖吧。

参考

【1】 Vue.js
【2】Rem布局的原理解析
【3】vue中Axios的封装和API接口的管理
【4】视口简介

Vue 实现仿美团外卖APP的总结相关推荐

  1. Vue开发的仿美团外卖Html5前端页面

    今天给大家开源一个仿美团外卖的Vue项目,介绍Vue和vue-router的基本用法. 工程结构 工程目录结构比较简单,如下图所示. 运行效果 部分运行效果如下图,实现了基本的页面切换,导航菜单,购物 ...

  2. Android Studio初学者实例:Fragment学习--仿美团外卖界面

    本次课程为Fragment为主题,课程的示例仿美团外卖界面,不同于底部导航栏的Fragment案例,此界面分为左侧切换与顶部切换.本文先是发布代码与效果,后续讲解将会在后续补充.先看看效果: 首先是布 ...

  3. 基于Android的仿美团外卖系统设计与实现 文档+源码+视频

    基于Android的仿美团外卖系统设计与实现 演示视频 摘 要 为了巩固所学 Android 基础知识,要开发一款仿美团外卖的项目,该项目与我们平常看到的美团外卖项目界面比较类似,展示的内容包括店铺. ...

  4. Android 高仿美团外卖详情页

    目录 1.需求分析 2.具体实现 2.1效果展示 2.2布局分析 2.3代码分析 2.3.1自定义 CoordinatorLayout.Behavior 2.3.2自定义 RecyclerView.I ...

  5. 仿美团外卖菜单界面的实现

    仿美团外卖菜单界面的实现 布局文件 总布局 <?xml version="1.0" encoding="utf-8"?> <LinearLay ...

  6. CH12-综合项目—仿美团外卖

    文章目录 目标 一.项目分析 目标 项目概述 开发环境 模块说明 二.效果展示 目标 店铺界面 店铺详情界面 店铺详情界面 确认清空购物车的对话框 菜品详情界面 订单界面和支付界面 三.服务器数据准备 ...

  7. 进云仿美团外卖平台 v1.39源码

    简介: 进云仿美团外卖源码是一个进云源生插件,支持多商户+多样化配送费模式+本土外卖平台+支持第三方配送,运行需要进云框架支撑! 特点: 1.多样化配送费模式: 2.板块-绑定商户分类机制: 3.板块 ...

  8. 美团外卖app可行性分析

    小组:韩睿哲,王永强,孟烈,徐殿强 1 引言 1.1编写目的 年轻人追求时尚,快捷,因此外卖行业拥有广阔的消费群体:团购的兴起,也促进了人们的消费欲望,人们继续一个外卖平台,来满足他们的欲望.O2o模 ...

  9. 【uniapp前端组件】仿美团外卖商品列表

    仿美团外卖商品列表 简易实现美团外卖商品列表,没什么技术难点,简单来说就是两个scroll-view协作,并且两个scroll-view不会滑动冲突. 实际效果 仿美团外卖商品列表实际项目效果 简介 ...

最新文章

  1. gis属性表怎么导成excel_第022篇:ArcGIS中将属性表直接导出为Excel的方法
  2. EOS账户系统(3)账户的权限
  3. c mysql 查不到数据_怎么检测不到MYSQL数据库的存在
  4. Java编程——服务器设计方案之应用限流
  5. PWN-PRACTICE-BUUCTF-5
  6. oracle 对两列加唯一性束_oracle中创建unique唯一约束(单列和多列) 。
  7. Linux文件句柄占用数量查看与设置
  8. java获取服务器超时_java – Eclipse中的Tomcat服务器超时
  9. linux搭建一个配置简单的nginx反向代理服务器 2个tomcat
  10. matlab脉冲补偿,基于LabVIEW和Matlab的纳秒脉冲测量信号补偿研究
  11. iOS NSURLSession 指南
  12. 【NLP开发】Python实现聊天机器人(微软小冰)
  13. IDEA 导出UML类图
  14. 传智黑马python18期_传智博客黑马Python就业14期资料
  15. 英语学习之‘加减乘除’
  16. 2020年十大数字客户体验(CX)软件平台
  17. github上12306抢票使用说明
  18. 微信公众号开发 - 配置表设计以及接入公众号接口开发
  19. uC/Modbus 用户手册——第一节
  20. E22-400M22S(SX1268) CubeMX HAL

热门文章

  1. Linux提权思路+实战【很不错的文章】
  2. 3GPP R16的Conditional handover配置确认
  3. 英国政产学研机构联合启动面向复杂路况的自动驾驶计划
  4. 医疗专业计算机考试题库,计算机专业考试题库(附答案)
  5. 台式计算机mtbf认证,手提电脑MTBF认证
  6. CrowdSourcing-众包分析
  7. 为什么要选择炫云云渲染?它有哪些优势?
  8. 电动车智能头盔(自行车智能头盔)方案
  9. 【嵌入式百科】001——字长、比特、字节、字、双字
  10. python-人工智能基础