(一) MVVM概念
MVVM是视图层概念,主要关注于视图层分离:MVVM把前端的视图层分为Model,View,VM ViewModel.
app.js
:项目的入口模块,一切请求都要进入这里处理(无路由分发的功能,需要调用router.js)
router.js
:路由分发模块:为了保证路由模块的职能单一,不负责具体业务逻辑的处理。(业务处理调用controller模块)
controller
:业务逻辑处理层。封装具体代码,不负责处理数据的CRUD(CRUD需要调用Model层.
Model
:只负责操作数据库,执行对应的SQL语句,进行数据的CRUD(C:create; R:read; U:update; D:delete)
View视图层
:每当用户操作界面,如果需要进行业务处理,如果需要进行业务处理,都会通过网络请求后端的服务器,此时请求会被后端的App.js监听。
M:
保存的是每个页面中单独的数据;
VM:
是调度者,分割了M和V,每当V层想要获取保存数据的时候,都要由VM做中间处理。
V:
是每个页面中的HTML结构
MVVM让数据双向绑定(由VM提供,VM是核心)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7JIFnKu7-1630673191818)(C:\Users\xyx\Downloads\mvc.png)]
(二)前期准备
- 全局安装vue-cli脚手架工具:
cnpm install -g vue-cli
- 初始化sell项目:
vue init webpack flash-waimai-mobile
- 进入sell目录:
cd flash-waimai-mobile
- 安装依赖(依据package.json文件):
cnpm install
- 运行项目(package.json中配置):
cnpm run dev
或者node build/dev-server.js
(三)mobile大体框架与开发步骤
1. build模块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDwxx8fl-1630673191830)(C:\Users\xyx\AppData\Roaming\Typora\typora-user-images\image-20210819162840108.png)]
目录和作用如下:
build.js
生产环境构建代码
utils.js
构建工具相关
dev-client.js
-配合dev-server.js监听html文件改动也能够触发自动刷新
// 引入 webpack-hot-middleware/client
var hotClient = require('webpack-hot-middleware/client');
// 订阅事件,当 event.action === 'reload' 时执行页面刷新
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload();
}
})
webpack.base.conf.js
webpack配置路径别名
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js'
},
resolve: {
extensions: ['', '.js', '.vue', '.less', '.css', '.scss'],
fallback: [path.join(__dirname, '../node_modules')],
alias: {
// 路径别名配置(自定义)
'vue$': 'vue/dist/vue.common.js',
'src': path.resolve(__dirname, '../src'),
'assets': path.resolve(__dirname, '../src/assets'),
'components': path.resolve(__dirname, '../src/components')
}
},
resolveLoader: {
fallback: [path.join(__dirname, '../node_modules')]
},
......
webpack.dev.conf.js
webpack开发环境设置,构建本地服务器
webpack.pro.conf.js
webpack生产环境配置
2. 整体路由配置
-
安装ajax异步请求插件vue-resource:
cnpm install vue-resource --save-dev
-
配置项目整体路由
// 文件位置:src/APP.vue <template> <div> <!--路由刷新缓存--> <transition name="router-fade" mode="out-in"> <keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> </transition> <transition name="router-fade" mode="out-in"> <router-view v-if="!$route.meta.keepAlive"></router-view> </transition> <svg-icon></svg-icon> </div> </template> <script> import svgIcon from './components/common/svg'; export default { components:{ svgIcon }, } </script> <style lang="scss"> @import './style/common'; .router-fade-enter-active, .router-fade-leave-active { transition: opacity .3s; } .router-fade-enter, .router-fade-leave-active { opacity: 0; } </style>
// 文件位置:src/router/index.js // 截取部分代码 import App from '../App' //通过webpack来分模块加载路由 const home = r => require.ensure([], () => r(require('../page/home/home')), 'home') const city = r => require.ensure([], () => r(require('../page/city/city')), 'city') const msite = r => require.ensure([], () => r(require('../page/msite/msite')), 'msite') const search = r => require.ensure([], () => r(require('../page/search/search')), 'search') .............. export default [{ path: '/', component: App, //顶层路由,对应index.html children: [ //二级路由。对应App.vue //地址为空时跳转home页面 { path: '', redirect: '/home' }, //首页城市列表页 { path: '/home', component: home }, //当前选择城市页 { path: '/city/:cityid', component: city }, //所有商铺列表页 { path: '/msite', component: msite, meta: { keepAlive: false }, }, ......... ] }]
-
配置项目整体依赖
// 文件位置:src/main.js import Vue from 'vue' import VueRouter from 'vue-router' import routes from './router/router' import store from './store/' import {routerMode} from './config/env' import './config/rem' // 使用Vue-resource必须放在前面,放在后面报错 Vue.use(VueRouter) const router = new VueRouter({ routes, mode: routerMode, strict: process.env.NODE_ENV !== 'production', scrollBehavior (to, from, savedPosition) { if (savedPosition) { return savedPosition } else { if (from.meta.keepAlive) { from.meta.savedPosition = document.body.scrollTop; } return { x: 0, y: to.meta.savedPosition || 0 } } } }) new Vue({ router, store, }).$mount('#app')
3. 通用组件开发
-
通用样式
- 总体样式
//文件位置src/style/common.scss body, div, span, header, footer, nav, section, aside, article, ul, dl, dt, dd, li, a, p, h1, h2, h3, h4,h5, h6, i, b, textarea, button, input, select, figure, figcaption, { padding: 0; margin: 0; list-style: none; font-style: normal; text-decoration: none; border: none; color: #333; font-weight: normal; font-family: "Microsoft Yahei"; box-sizing: border-box; -webkit-tap-highlight-color:transparent; -webkit-font-smoothing: antialiased; &:hover{ outline: none; } } /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ ::-webkit-scrollbar { width: 0px; height: 0px; background-color: #F5F5F5; } /*定义滚动条轨道 内阴影+圆角*/ ::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0); border-radius: 10px; background-color: #F5F5F5; } /*定义滑块 内阴影+圆角*/ ::-webkit-scrollbar-thumb { border-radius: 10px; -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); background-color: #555; } input[type="button"], input[type="submit"], input[type="search"], input[type="reset"] { -webkit-appearance: none; } textarea { -webkit-appearance: none;} html,body{ height: 100%; width: 100%; background-color: #F5F5F5; } .clear:after{ content: ''; display: block; clear: both; } .clear{ zoom:1; } .back_img{ background-repeat: no-repeat; background-size: 100% 100%; } .margin{ margin: 0 auto; } .left{ float: left; } .right{ float: right; } .hide{ display: none; } .show{ display: block; } .ellipsis{ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .paddingTop{ padding-top: 1.95rem; } @keyframes backOpacity{ 0% { opacity: 1 } 25% { opacity: .5 } 50% { opacity: 1 } 75% { opacity: .5 } 100% { opacity: 1 } } .animation_opactiy{ animation: backOpacity 2s ease-in-out infinite; } //文件位置src/style/mixin.scss $blue: #3190e8; $bc: #e4e4e4; $fc:#fff; // 背景图片地址和大小 @mixin bis($url) { background-image: url($url); background-repeat: no-repeat; background-size: 100% 100%; } @mixin borderRadius($radius) { -webkit-border-radius: $radius; -moz-border-radius: $radius; -ms-border-radius: $radius; -o-border-radius: $radius; border-radius: $radius; } //定位全屏 @mixin allcover{ position:absolute; top:0; right:0; } //定位上下左右居中 @mixin center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } //定位上下居中 @mixin ct { position: absolute; top: 50%; transform: translateY(-50%); } //定位左右居中 @mixin cl { position: absolute; left: 50%; transform: translateX(-50%); } //宽高 @mixin wh($width, $height){ width: $width; height: $height; } //字体大小、行高、字体 @mixin font($size, $line-height, $family: 'Microsoft YaHei') { font: #{$size}/#{$line-height} $family; } //字体大小,颜色 @mixin sc($size, $color){ font-size: $size; color: $color; } //flex 布局和 子元素 对其方式 @mixin fj($type: space-between){ display: flex; justify-content: $type; }
- 构建通用版块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQMUK42F-1630673191836)(C:\Users\xyx\AppData\Roaming\Typora\typora-user-images\image-20210819195116555.png)]
alertTip.vue:使用mixin样式,作用是移动端小弹窗。
buyCart.vue:购物模块,用于添加商品。
computeTime.vue:下单后支付时间倒计时模块。
loading.vue:使用mixin样式,加载时屏幕上显示弹跳小水果。
ratingstar.vue:五星好评效果。
shoplist.vue:商铺展示,数据最多20位。
svg.vue:字体显示。
以商铺展示为例:
<template> <div class="shoplist_container"> <ul v-load-more="loaderMore" v-if="shopListArr.length" type="1"> <router-link :to="{path: 'shop', query:{geohash, id: item.id}}" v-for="item in shopListArr" tag='li' :key="item.id" class="shop_li"> <section> <img :src="imgBaseUrl + item.image_path" class="shop_img"> </section> <hgroup class="shop_right"> <header class="shop_detail_header"> <h4 :class="item.is_premium? 'premium': ''" class="" class="shop_title ellipsis">{{item.name}}</h4> <ul class="shop_detail_ul"> <li v-for="item in item.supports" :key="item.id" class="supports">{{item.icon_name}}</li> </ul> </header> <h5 class="rating_order_num"> <section class="rating_order_num_left"> <section class="rating_section"> <rating-star :rating='item.rating'></rating-star> <span class="rating_num">{{item.rating}}</span> </section> <section class="order_section"> 月售{{item.recent_order_num}}单 </section> </section> <section class="rating_order_num_right"> <span class="delivery_style delivery_left" v-if="item.delivery_mode">{{item.delivery_mode.text}}</span> <span class="delivery_style delivery_right" v-if="zhunshi(item.supports)">准时达</span> </section> </h5> <h5 class="fee_distance"> <p class="fee"> ¥{{item.float_minimum_order_amount}}起送 <span class="segmentation">|</span> {{item.piecewise_agent_fee.tips}} </p> <p class="distance_time"> <template v-if="Number(item.distance)">{{item.distance > 1000? (item.distance/1000).toFixed(2) + 'km': item.distance + 'm'}} <span class="segmentation">|</span> </template> <template v-else> {{item.distance}} <span class="segmentation">|</span> </template> <span class="order_time">{{item.order_lead_time}}</span> </p> </h5> </hgroup> </router-link> </ul> <ul v-else class="animation_opactiy"> <li class="list_back_li" v-for="item in 10" :key="item"> <img src="../../images/shopback.svg" class="list_back_svg"> </li> </ul> <p v-if="touchend" class="empty_data">没有更多了</p> <aside class="return_top" @click="backTop" v-if="showBackStatus"> <svg class="back_top_svg"> <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#backtop"></use> </svg> </aside> <div ref="abc" style="background-color: red;"></div> <transition name="loading"> <loading v-show="showLoading"></loading> </transition> </div> </template> <script> import {mapState} from 'vuex' import {shopList} from 'src/service/getData' import {imgBaseUrl} from 'src/config/env' import {showBack, animate} from 'src/config/mUtils' import {loadMore, getImgPath} from './mixin' import loading from './loading' import ratingStar from './ratingStar' export default { data(){ return { offset: 0, // 批次加载店铺列表,每次加载20个 limit = 20 shopListArr:[], // 店铺列表数据 preventRepeatReuqest: false, //到达底部加载数据,防止重复加载 showBackStatus: false, //显示返回顶部按钮 showLoading: true, //显示加载动画 touchend: false, //没有更多数据 imgBaseUrl, } }, mounted(){ this.initData(); }, components: { loading, ratingStar, }, props: ['restaurantCategoryId', 'restaurantCategoryIds', 'sortByType', 'deliveryMode', 'supportIds', 'confirmSelect', 'geohash'], mixins: [loadMore, getImgPath], computed: { ...mapState([ 'latitude','longitude' ]), }, updated(){ // console.log(this.supportIds, this.sortByType) }, methods: { async initData(){ //获取数据 let resResponse = await shopList(this.latitude, this.longitude, this.offset, this.restaurantCategoryId); console.log('resresponse',resResponse) let res = resResponse.records console.log('res',res) this.shopListArr = [...res]; if (res.length < 20) { this.touchend = true; } this.hideLoading(); //开始监听scrollTop的值,达到一定程度后显示返回顶部按钮 showBack(status => { this.showBackStatus = status; }); }, //到达底部加载更多数据 async loaderMore(){ if (this.touchend) { return } //防止重复请求 if (this.preventRepeatReuqest) { return } this.showLoading = true; this.preventRepeatReuqest = true; //数据的定位加20位 this.offset += 20; let res = await shopList(this.latitude, this.longitude, this.offset, this.restaurantCategoryId); this.hideLoading(); this.shopListArr = [...this.shopListArr, ...res]; //当获取数据小于20,说明没有更多数据,不需要再次请求数据 if (res.length < 20) { this.touchend = true; return } this.preventRepeatReuqest = false; }, //返回顶部 backTop(){ animate(document.body, {scrollTop: '0'}, 400,'ease-out'); }, //监听父级传来的数据发生变化时,触发此函数重新根据属性值获取数据 async listenPropChange(){ this.showLoading = true; this.offset = 0; let resResponse = await shopList(this.latitude, this.longitude, this.offset, '' , this.restaurantCategoryIds, this.sortByType, this.deliveryMode, this.supportIds); let res = resResponse.records this.hideLoading(); //考虑到本地模拟数据是引用类型,所以返回一个新的数组 this.shopListArr = [...res]; }, //开发环境与编译环境loading隐藏方式不同 hideLoading(){ this.showLoading = false; }, zhunshi(supports){ let zhunStatus; if ((supports instanceof Array) && supports.length) { supports.forEach(item => { if (item.icon_name === '准') { zhunStatus = true; } }) }else{ zhunStatus = false; } return zhunStatus }, }, watch: { //监听父级传来的restaurantCategoryIds,当值发生变化的时候重新获取餐馆数据,作用于排序和筛选 restaurantCategoryIds: function (value){ console.log('watchids',value) this.listenPropChange(); }, //监听父级传来的排序方式 sortByType: function (value){ this.listenPropChange(); }, //监听父级的确认按钮是否被点击,并且返回一个自定义事件通知父级,已经接收到数据,此时父级才可以清除已选状态 confirmSelect: function (value){ this.listenPropChange(); } } } </script> <style lang="scss" scoped> @import 'src/style/mixin'; .shoplist_container{ background-color: #fff; margin-bottom: 2rem; } .shop_li{ display: flex; border-bottom: 0.025rem solid #f1f1f1; padding: 0.7rem 0.4rem; } .shop_img{ @include wh(2.7rem, 2.7rem); display: block; margin-right: 0.4rem; } .list_back_li{ height: 4.85rem; .list_back_svg{ @include wh(100%, 100%) } } .shop_right{ flex: auto; .shop_detail_header{ @include fj; align-items: center; .shop_title{ width: 8.5rem; color: #333; padding-top: .01rem; @include font(0.65rem, 0.65rem, 'PingFangSC-Regular'); font-weight: 700; } .premium::before{ content: '品牌'; display: inline-block; font-size: 0.5rem; line-height: .6rem; color: #333; background-color: #ffd930; padding: 0 0.1rem; border-radius: 0.1rem; margin-right: 0.2rem; } .shop_detail_ul{ display: flex; transform: scale(.8); margin-right: -0.3rem; .supports{ @include sc(0.5rem, #999); border: 0.025rem solid #f1f1f1; padding: 0 0.04rem; border-radius: 0.08rem; margin-left: 0.05rem; } } } .rating_order_num{ @include fj(space-between); height: 0.6rem; margin-top: 0.52rem; .rating_order_num_left{ @include fj(flex-start); .rating_section{ display: flex; .rating_num{ @include sc(0.4rem, #ff6000); margin: 0 0.2rem; } } .order_section{ transform: scale(.8); margin-left: -0.2rem; @include sc(0.4rem, #666); } } .rating_order_num_right{ display: flex; align-items: center; transform: scale(.7); min-width: 5rem; justify-content: flex-end; margin-right: -0.8rem; .delivery_style{ font-size: 0.4rem; padding: 0.04rem 0.08rem 0; border-radius: 0.08rem; margin-left: 0.08rem; border: 1px; } .delivery_left{ color: #fff; background-color: $blue; border: 0.025rem solid $blue; } .delivery_right{ color: $blue; border: 0.025rem solid $blue; } } } .fee_distance{ margin-top: 0.52rem; @include fj; @include sc(0.5rem, #333); .fee{ transform: scale(.9); @include sc(0.5rem, #666); } .distance_time{ transform: scale(.9); span{ color: #999; } .order_time{ color: $blue; } .segmentation{ color: #ccc; } } } } .loader_more{ @include font(0.6rem, 3); text-align: center; color: #999; } .empty_data{ @inlude sc(0.5rem, #666); text-align: center; line-height: 2rem; } .return_top{ position: fixed; bottom: 3rem; right: 1rem; .back_top_svg{ @include wh(2rem, 2rem); } } .loading-enter-active, .loading-leave-active { transition: opacity 1s } .loading-enter, .loading-leave-active { opacity: 0 } </style>
4. 数据获取
import fetch from '../config/fetch'
import {getStore} from '../config/mUtils'
/**
* 获取首页默认地址
*/
export const cityGuess = () => fetch('/v1/cities', {
type: 'guess'
});
/**
* 获取首页热门城市
*/
export const hotcity = () => fetch('/v1/cities', {
type: 'hot'
});
/**
* 获取首页所有城市
*/
export const groupcity = () => fetch('/v1/cities', {
type: 'group'
});
/**
* 获取当前所在城市
*/
export const currentcity = number => fetch('/v1/cities/' + number);
/**
* 获取搜索地址
*/
export const searchplace = (cityid, value) => fetch('/v1/pois', {
type: 'search',
city_id: cityid,
keyword: value
});
/**
* 获取msite页面地址信息
*/
export const msiteAddress = geohash => fetch('/v1/position/pois', {
geohash
});
/**
* 获取msite页面食品分类列表
*/
export const msiteFoodTypes = geohash => fetch('/v2/index_entry', {
geohash,
group_type: '1'
});
/**
* 获取msite商铺列表
*/
export const shopList = (latitude, longitude, offset, restaurant_category_id = '', restaurant_category_ids = '', order_by = '', delivery_mode = '', support_ids = []) => {
let supportStr = '';
support_ids.forEach(item => {
if (item.status) {
supportStr += '&support_ids[]=' + item.id;
}
});
let data = {
latitude,
longitude,
offset,
limit: '20',
'extras': 'activities',
keyword: '',
restaurant_category_id,
'restaurant_category_ids': restaurant_category_ids,
order_by,
'delivery_mode': delivery_mode + supportStr
};
return fetch('/shopping/restaurants', data);
};
/**
* 获取search页面搜索结果
*/
export const searchRestaurant = (geohash, keyword) => fetch('/v4/restaurants', {
'extras': 'restaurant_activity',
geohash,
keyword,
type: 'search'
});
/**
* 获取food页面的 category 种类列表
*/
export const foodCategory = (latitude, longitude) => fetch('/shopping/v2/restaurant/category', {
latitude,
longitude
});
/**
* 获取food页面的配送方式
*/
export const foodDelivery = (latitude, longitude) => fetch('/shopping/v1/restaurants/delivery_modes', {
latitude,
longitude,
kw: ''
});
/**
* 获取food页面的商家属性活动列表
*/
export const foodActivity = (latitude, longitude) => fetch('/shopping/v1/restaurants/activity_attributes', {
latitude,
longitude,
kw: ''
});
/**
* 获取shop页面商铺详情
*/
export const shopDetails = (shopid, latitude, longitude) => fetch('/shopping/restaurant/' + shopid, {
latitude,
longitude: longitude + '&extras=activities&extras=album&extras=license&extras=identification&extras=statistics'
});
/**
* 获取shop页面菜单列表
*/
export const foodMenu = restaurant_id => fetch('/shopping/v2/menu', {
restaurant_id
});
/**
* 获取商铺评价列表
*/
export const getRatingList = (shopid, offset, tag_name = '') => fetch('/ugc/v2/restaurants/' + shopid + '/ratings', {
has_content: true,
offset,
limit: 10,
tag_name
});
/**
* 获取商铺评价分数
*/
export const ratingScores = shopid => fetch('/ugc/v2/restaurants/' + shopid + '/ratings/scores');
/**
* 获取商铺评价分类
*/
export const ratingTags = shopid => fetch('/ugc/v2/restaurants/' + shopid + '/ratings/tags');
/**
* 获取短信验证码
*/
export const mobileCode = phone => fetch('/v4/mobile/verify_code/send', {
mobile: phone,
scene: 'login',
type: 'sms'
}, 'POST');
/**
* 获取图片验证码
*/
export const getcaptchas = () => fetch('/v1/captchas', {}, 'POST');
/**
* 检测帐号是否存在
*/
export const checkExsis = (checkNumber, type) => fetch('/v1/users/exists', {
[type]: checkNumber,
type
});
/**
* 发送帐号
*/
export const sendMobile = (sendData, captcha_code, type, password) => fetch('/v1/mobile/verify_code/send', {
action: "send",
captcha_code,
[type]: sendData,
type: "sms",
way: type,
password,
}, 'POST');
/**
* 确认订单
*/
export const checkout = (geohash, entities, shopid) => fetch('/v1/carts/checkout', {
come_from: "web",
geohash,
entities,
restaurant_id: shopid,
}, 'POST');
/**
* 获取快速备注列表
*/
export const getRemark = (id, sig) => fetch('/v1/carts/' + id + '/remarks', {
sig
});
/**
* 获取地址列表
*/
export const getAddress = (id, sig) => fetch('/v1/carts/' + id + '/addresses', {
sig
});
/**
* 搜索地址
*/
export const searchNearby = keyword => fetch('/v1/pois', {
type: 'nearby',
keyword
});
/**
* 添加地址
*/
export const postAddAddress = (userId, address, address_detail, geohash, name, phone, phone_bk, poi_type, sex, tag, tag_type) => fetch('/v1/users/' + userId + '/addresses', {
address,
address_detail,
geohash,
name,
phone,
phone_bk,
poi_type,
sex,
tag,
tag_type,
}, 'POST');
/**
* 下订单
*/
export const placeOrders = (user_id, cart_id, address_id, description, entities, geohash, sig) => fetch('/v1/users/' + user_id + '/carts/' + cart_id + '/orders', {
address_id,
come_from: "mobile_web",
deliver_time: "",
description,
entities,
geohash,
paymethod_id: 1,
sig,
}, 'POST');
/**
* 重新发送订单验证码
*/
export const rePostVerify = (cart_id, sig, type) => fetch('/v1/carts/' + cart_id + '/verify_code', {
sig,
type,
}, 'POST');
/**
* 下订单
*/
export const validateOrders = ({
user_id,
cart_id,
address_id,
description,
entities,
geohash,
sig,
validation_code,
validation_token
}) => fetch('/v1/users/' + user_id + '/carts/' + cart_id + '/orders', {
address_id,
come_from: "mobile_web",
deliver_time: "",
description,
entities,
geohash,
paymethod_id: 1,
sig,
validation_code,
validation_token,
}, 'POST');
/**
* 重新发送订单验证码
*/
export const payRequest = (merchantOrderNo, userId) => fetch('/payapi/payment/queryOrder', {
merchantId: 5,
merchantOrderNo,
source: 'MOBILE_WAP',
userId,
version: '1.0.0',
});
/**
* 获取服务中心信息
*/
export const getService = () => fetch('/v3/profile/explain');
/**
*兑换会员卡
*/
export const vipCart = (id, number, password) => fetch('/member/v1/users/' + id + '/delivery_card/physical_card/bind', {
number,
password
}, 'POST')
/**
* 获取红包
*/
export const getHongbaoNum = id => fetch('/promotion/v2/users/' + id + '/hongbaos?limit=20&offset=0');
/**
* 获取过期红包
*/
export const getExpired = id => fetch('/promotion/v2/users/' + id + '/expired_hongbaos?limit=20&offset=0');
/**
* 兑换红包
*/
export const exChangeHongbao = (id, exchange_code, captcha_code) => fetch('/v1/users/' + id + '/hongbao/exchange', {
exchange_code,
captcha_code,
}, 'POST');
/**
* 获取用户信息
*/
export const getUser = () => fetch('/v1/users', {user_id: getStore('user_id')});
/**
* 手机号登录
*/
var sendLogin = (code, mobile, validate_token) => fetch('/v1/login/app_mobile', {
code,
mobile,
validate_token
}, 'POST');
/**
* 获取订单列表
*/
export const getOrderList = (user_id, offset) => fetch('/bos/v2/users/' + user_id + '/orders', {
limit: 10,
offset,
t: new Date().getTime()
});
export const finishOrder = (user_id, orderid) => fetch('/bos/v1/users/' + user_id + '/orders/' + orderid + '/finish');
/**
* 获取订单详情
*/
export const getOrderDetail = (user_id, orderid) => fetch('/bos/v1/users/' + user_id + '/orders/' + orderid + '/snapshot');
/**
*个人中心里编辑地址
*/
export const getAddressList = (user_id) => fetch('/v1/users/' + user_id + '/addresses')
/**
*个人中心里搜索地址
*/
export const getSearchAddress = (keyword) => fetch('v1/pois', {
keyword: keyword,
type: 'nearby'
})
/**
* 删除地址
*/
export const deleteAddress = (userid, addressid) => fetch('/v1/users/' + userid + '/addresses/' + addressid, {}, 'DELETE')
/**
* 账号密码登录
*/
export const accountLogin = (username, password, captchaCode, captchCodeId) => fetch('/v1/users/v2/login', {username, password, captchaCode, captchCodeId}, 'POST');
/**
* 退出登录
*/
export const signout = () => fetch('/v1/users/v2/signout');
/**
* 改密码
*/
export const changePassword = (username, oldpassWord, newpassword, confirmpassword, captcha_code) => fetch('/v2/changepassword', {
username,
oldpassWord,
newpassword,
confirmpassword,
captcha_code
}, 'POST');
5. 具体功能开发
详见注释
<template>
<div class="food_container">
<head-top :head-title="headTitle" goBack="true"></head-top>
<section class="sort_container">
<!-- 分类 -->
<div class="sort_item" :class="{choose_type:sortBy == 'food'}" >
<div class="sort_item_container" @click="chooseType('food')">
<div class="sort_item_border">
<span :class="{category_title: sortBy == 'food'}">{{foodTitle}}</span>
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg" version="1.1" class="sort_icon">
<polygon points="0,3 10,3 5,8"/>
</svg>
</div>
</div>
<transition name="showlist" v-show="category">
<section v-show="sortBy == 'food'" class="category_container sort_detail_type">
<section class="category_left">
<ul>
<li v-for="(item, index) in category" :key="index" class="category_left_li" :class="{category_active:restaurant_category_id == item.id}" @click="selectCategoryName(item.id, index)">
<section>
<img :src="getImgPath(item.image_url)" v-if="index" class="category_icon">
<span>{{item.name}}</span>
</section>
<section>
<span class="category_count">{{item.count}}</span>
<svg v-if="index" width="8" height="8" xmlns="http://www.w3.org/2000/svg" version="1.1" class="category_arrow" >
<path d="M0 0 L6 4 L0 8" stroke="#bbb" stroke-width="1" fill="none"/>
</svg>
</section>
</li>
</ul>
</section>
<section class="category_right">
<ul>
<li v-for="(item, index) in categoryDetail" v-if="index" :key="index" class="category_right_li" @click="getCategoryIds(item.id, item.name)" :class="{category_right_choosed: restaurant_category_ids == item.id || (!restaurant_category_ids)&&index == 0}">
<span>{{item.name}}</span>
<span>{{item.count}}</span>
</li>
</ul>
</section>
</section>
</transition>
</div>
<!-- 排序 -->
<div class="sort_item" :class="{choose_type:sortBy == 'sort'}">
<div class="sort_item_container" @click="chooseType('sort')">
<div class="sort_item_border">
<span :class="{category_title: sortBy == 'sort'}">排序</span>
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg" version="1.1" class="sort_icon">
<polygon points="0,3 10,3 5,8"/>
</svg>
</div>
</div>
<transition name="showlist">
<section v-show="sortBy == 'sort'" class="sort_detail_type">
<ul class="sort_list_container" @click="sortList($event)">
<li class="sort_list_li">
<svg>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#default"></use>
</svg>
<p data="0" :class="{sort_select: sortByType == 0}">
<span>智能排序</span>
<svg v-if="sortByType == 0">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
</p>
</li>
<li class="sort_list_li">
<svg>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#distance"></use>
</svg>
<p data="5" :class="{sort_select: sortByType == 5}">
<span>距离最近</span>
<svg v-if="sortByType == 5">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
</p>
</li>
<li class="sort_list_li">
<svg>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#hot"></use>
</svg>
<p data="6" :class="{sort_select: sortByType == 6}">
<span>销量最高</span>
<svg v-if="sortByType == 6">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
</p>
</li>
<li class="sort_list_li">
<svg>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#price"></use>
</svg>
<p data="1" :class="{sort_select: sortByType == 1}">
<span>起送价最低</span>
<svg v-if="sortByType == 1">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
</p>
</li>
<li class="sort_list_li">
<svg>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#speed"></use>
</svg>
<p data="2" :class="{sort_select: sortByType == 2}">
<span>配送速度最快</span>
<svg v-if="sortByType == 2">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
</p>
</li>
<li class="sort_list_li">
<svg>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#rating"></use>
</svg>
<p data="3" :class="{sort_select: sortByType == 3}">
<span>评分最高</span>
<svg v-if="sortByType == 3">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
</p>
</li>
</ul>
</section>
</transition>
</div>
<!-- 筛选 -->
<div class="sort_item" :class="{choose_type:sortBy == 'activity'}">
<div class="sort_item_container" @click="chooseType('activity')">
<span :class="{category_title: sortBy == 'activity'}">筛选</span>
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg" version="1.1" class="sort_icon">
<polygon points="0,3 10,3 5,8"/>
</svg>
</div>
<transition name="showlist">
<section v-show="sortBy == 'activity'" class="sort_detail_type filter_container">
<section style="width: 100%;">
<header class="filter_header_style">配送方式</header>
<ul class="filter_ul">
<li v-for="(item, index) in Delivery" :key="index" class="filter_li" @click="selectDeliveryMode(item.id)">
<svg :style="{opacity: (item.id == 0)&&(delivery_mode !== 0)? 0: 1}">
<use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="delivery_mode == item.id? '#selected':'#fengniao'"></use>
</svg>
<span :class="{selected_filter: delivery_mode == item.id}">{{item.text}}</span>
</li>
</ul>
</section>
<section style="width: 100%;">
<header class="filter_header_style">商家属性(可以多选)</header>
<ul class="filter_ul" style="paddingBottom: .5rem;">
<li v-for="(item,index) in Activity" :key="index" class="filter_li" @click="selectSupportIds(index, item.id)">
<svg v-show="support_ids[index].status" class="activity_svg">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#selected"></use>
</svg>
<span class="filter_icon" :style="{color: '#' + item.icon_color, borderColor: '#' + item.icon_color}" v-show="!support_ids[index].status">{{item.icon_name}}</span>
<span :class="{selected_filter: support_ids[index].status}">{{item.name}}</span>
</li>
</ul>
</section>
<footer class="confirm_filter">
<div class="clear_all filter_button_style" @click="clearSelect">清空</div>
<div class="confirm_select filter_button_style" @click="confirmSelectFun">确定<span v-show="filterNum">({{filterNum}})</span></div>
</footer>
</section>
</transition>
</div>
</section>
<transition name="showcover">
<div class="back_cover" v-show="sortBy"></div>
</transition>
<section class="shop_list_container">
<shop-list :geohash="geohash" :restaurantCategoryId="restaurant_category_id" :restaurantCategoryIds="restaurant_category_ids" :sortByType='sortByType' :deliveryMode="delivery_mode" :confirmSelect="confirmStatus" :supportIds="support_ids" v-if="latitude"></shop-list>
<shop-list :geohash="geohash" :restaurantCategoryId="restaurant_category_id" :restaurantCategoryIds="restaurant_category_ids" :sortByType='sortByType' :deliveryMode="delivery_mode" :confirmSelect="confirmStatus" :supportIds="support_ids" v-if="latitude"></shop-list>
</section>
</div>
</template>
<script>
import { mapState, mapMutations } from "vuex";
import headTop from "src/components/header/head";
import shopList from "src/components/common/shoplist";
import { getImgPath } from "src/components/common/mixin";
import {
msiteAddress,
foodCategory,
foodDelivery,
foodActivity
} from "src/service/getData";
export default {
data() {
return {
geohash: "", // city页面传递过来的地址geohash
headTitle: "", // msiet页面头部标题
foodTitle: "", // 排序左侧头部标题
restaurant_category_id: "", // 食品类型id值
restaurant_category_ids: "", //筛选类型的id
sortBy: "", // 筛选的条件
category: null, // category分类左侧数据
categoryDetail: null, // category分类右侧的详细数据
sortByType: null, // 根据何种方式排序
Delivery: null, // 配送方式数据
Activity: null, // 商家支持活动数据
delivery_mode: null, // 选中的配送方式
support_ids: [], // 选中的商铺活动列表
filterNum: 0, // 所选中的所有样式的集合
confirmStatus: false // 确认选择
};
},
created() {
this.initData();
},
mixins: [getImgPath],
components: {
headTop,
shopList
},
computed: {
...mapState(["latitude", "longitude"])
},
methods: {
...mapMutations(["RECORD_ADDRESS"]),
//初始化获取数据
async initData() {
//获取从msite页面传递过来的参数
this.geohash = this.$route.query.geohash;
this.headTitle = this.$route.query.title;
this.foodTitle = this.headTitle;
this.restaurant_category_id = this.$route.query.restaurant_category_id;
console.log('geohash',this.geohash)
console.log('restaurant_category_id',this.restaurant_category_id)
//防止刷新页面时,vuex状态丢失,经度纬度需要重新获取,并存入vuex
if (!this.latitude) {
console.log('经度' ,this.latitude)
//获取位置信息
let res = await msiteAddress(this.geohash);
console.log('位置信息',res)
// 记录当前经度纬度进入vuex
this.RECORD_ADDRESS(res);
}
//获取category分类左侧数据
this.category = await foodCategory(this.latitude, this.longitude);
console.log(this.category)
//初始化时定位当前category分类左侧默认选择项,在右侧展示出其sub_categories列表
this.category.forEach(item => {
if (this.restaurant_category_id == item.id) {
this.categoryDetail = item.sub_categories;
}
})
console.log(1)
//获取筛选列表的配送方式
this.Delivery = await foodDelivery(this.latitude, this.longitude);
//获取筛选列表的商铺活动
console.log('delivery',this.Delivery)
this.Activity = await foodActivity(this.latitude, this.longitude);
console.log('ac1',this.Activity)
//记录support_ids的状态,默认不选中,点击状态取反,status为true时为选中状态
this.Activity.forEach((item, index) => {
this.support_ids[index] = { status: false, id: item.id };
});
console.log('activity',this.Activity)
},
// 点击顶部三个选项,展示不同的列表,选中当前选项进行展示,同时收回其他选项
async chooseType(type) {
if (this.sortBy !== type) {
this.sortBy = type;
//food选项中头部标题发生改变,需要特殊处理
if (type == "food") {
this.foodTitle = "分类";
} else {
//将foodTitle 和 headTitle 进行同步
this.foodTitle = this.headTitle;
}
} else {
//再次点击相同选项时收回列表
this.sortBy = "";
if (type == "food") {
//将foodTitle 和 headTitle 进行同步
this.foodTitle = this.headTitle;
}
}
},
//选中Category左侧列表的某个选项时,右侧渲染相应的sub_categories列表
selectCategoryName(id, index) {
//第一个选项 -- 全部商家 因为没有自己的列表,所以点击则默认获取选所有数据
if (index === 0) {
this.restaurant_category_ids = null;
this.sortBy = "";
//不是第一个选项时,右侧展示其子级sub_categories的列表
} else {
this.restaurant_category_id = id;
this.categoryDetail = this.category[index].sub_categories;
}
},
//选中Category右侧列表的某个选项时,进行筛选,重新获取数据并渲染
getCategoryIds(id, name) {
console.log(id, name)
this.restaurant_category_ids = id;
this.restaurant_category_id = id;
this.sortBy = "";
this.foodTitle = this.headTitle = name;
},
//点击某个排序方式,获取事件对象的data值,并根据获取的值重新获取数据渲染
sortList(event) {
let node;
// 如果点击的是 span 中的文字,则需要获取到 span 的父标签 p
if (event.target.nodeName.toUpperCase() !== "P") {
node = event.target.parentNode;
} else {
node = event.target;
}
this.sortByType = node.getAttribute("data");
this.sortBy = "";
},
//筛选选项中的配送方式选择
selectDeliveryMode(id) {
//delivery_mode为空时,选中当前项,并且filterNum加一
if (this.delivery_mode == null) {
this.filterNum++;
this.delivery_mode = id;
//delivery_mode为当前已有值时,清空所选项,并且filterNum减一
} else if (this.delivery_mode == id) {
this.filterNum--;
this.delivery_mode = null;
//delivery_mode已有值且不等于当前选择值,则赋值delivery_mode为当前所选id
} else {
this.delivery_mode = id;
}
},
//点击商家活动,状态取反
selectSupportIds(index, id) {
//数组替换新的值
this.support_ids.splice(index, 1, {
status: !this.support_ids[index].status,
id
});
//重新计算filterNum的个数
this.filterNum = this.delivery_mode == null ? 0 : 1;
this.support_ids.forEach(item => {
if (item.status) {
this.filterNum++;
}
});
},
//只有点击清空按钮才清空数据,否则一直保持原有状态
clearSelect() {
this.support_ids.map(item => (item.status = false));
this.filterNum = 0;
this.delivery_mode = null;
},
//点击确认时,将需要筛选的id值传递给子组件,并且收回列表
confirmSelectFun() {
//状态改变时,因为子组件进行了监听,会重新获取数据进行筛选
this.confirmStatus = !this.confirmStatus;
this.sortBy = "";
}
}
};
</script>
<style lang="scss" scoped>
@import "src/style/mixin";
.food_container {
padding-top: 3.6rem;
}
.sort_container {
background-color: #fff;
border-bottom: 0.025rem solid #f1f1f1;
position: fixed;
top: 1.95rem;
right: 0;
width: 100%;
display: flex;
z-index: 13;
box-sizing: border-box;
.sort_item {
@include sc(0.55rem, #444);
@include wh(33.3%, 1.6rem);
text-align: center;
line-height: 1rem;
.sort_item_container {
@include wh(100%, 100%);
position: relative;
z-index: 14;
background-color: #fff;
box-sizing: border-box;
padding-top: 0.3rem;
.sort_item_border {
height: 1rem;
border-right: 0.025rem solid $bc;
}
}
.sort_icon {
vertical-align: middle;
transition: all 0.3s;
fill: #666;
}
}
.choose_type {
.sort_item_container {
.category_title {
color: $blue;
}
.sort_icon {
transform: rotate(180deg);
fill: $blue;
}
}
}
.showlist-enter-active,
.showlist-leave-active {
transition: all 0.3s;
transform: translateY(0);
}
.showlist-enter,
.showlist-leave-active {
opacity: 0;
transform: translateY(-100%);
}
.sort_detail_type {
width: 100%;
position: absolute;
display: flex;
top: 1.6rem;
left: 0;
border-top: 0.025rem solid $bc;
background-color: #fff;
}
.category_container {
.category_left {
flex: 1;
background-color: #f1f1f1;
height: 16rem;
overflow-y: auto;
span {
@include sc(0.5rem, #666);
line-height: 1.8rem;
}
.category_left_li {
@include fj;
padding: 0 0.5rem;
.category_icon {
@include wh(0.8rem, 0.8rem);
vertical-align: middle;
margin-right: 0.2rem;
}
.category_count {
background-color: #ccc;
@include sc(0.4rem, #fff);
padding: 0 0.1rem;
border: 0.025rem solid #ccc;
border-radius: 0.8rem;
vertical-align: middle;
margin-right: 0.25rem;
}
.category_arrow {
vertical-align: middle;
}
}
.category_active {
background-color: #fff;
}
}
.category_right {
flex: 1;
background-color: #fff;
padding-left: 0.5rem;
height: 16rem;
overflow-y: auto;
.category_right_li {
@include fj;
height: 1.8rem;
line-height: 1.8rem;
padding-right: 0.5rem;
border-bottom: 0.025rem solid $bc;
span {
color: #666;
}
}
.category_right_choosed {
span {
color: $blue;
}
}
}
}
.sort_list_container {
width: 100%;
.sort_list_li {
height: 2.5rem;
display: flex;
align-items: center;
svg {
@include wh(0.7rem, 0.7rem);
margin: 0 0.3rem 0 0.8rem;
}
p {
line-height: 2.5rem;
flex: auto;
text-align: left;
text-indent: 0.25rem;
border-bottom: 0.025rem solid $bc;
@include fj;
align-items: center;
span {
color: #666;
}
}
.sort_select {
span {
color: $blue;
}
}
}
}
.filter_container {
flex-direction: column;
align-items: flex-start;
min-height: 10.6rem;
background-color: #f1f1f1;
.filter_header_style {
@include sc(0.4rem, #333);
line-height: 1.5rem;
height: 1.5rem;
text-align: left;
padding-left: 0.5rem;
background-color: #fff;
}
.filter_ul {
display: flex;
flex-wrap: wrap;
padding: 0 0.5rem;
background-color: #fff;
.filter_li {
display: flex;
align-items: center;
border: 0.025rem solid #eee;
@include wh(4.7rem, 1.4rem);
margin-right: 0.25rem;
border-radius: 0.125rem;
padding: 0 0.25rem;
margin-bottom: 0.25rem;
svg {
@include wh(0.8rem, 0.8rem);
margin-right: 0.125rem;
}
span {
@include sc(0.4rem, #333);
}
.filter_icon {
@include wh(0.8rem, 0.8rem);
font-size: 0.5rem;
border: 0.025rem solid $bc;
border-radius: 0.15rem;
margin-right: 0.25rem;
line-height: 0.8rem;
text-align: center;
}
.activity_svg {
margin-right: 0.25rem;
}
.selected_filter {
color: $blue;
}
}
}
.confirm_filter {
display: flex;
background-color: #f1f1f1;
width: 100%;
padding: 0.3rem 0.2rem;
.filter_button_style {
@include wh(50%, 1.8rem);
font-size: 0.8rem;
line-height: 1.8rem;
border-radius: 0.2rem;
}
.clear_all {
background-color: #fff;
margin-right: 0.5rem;
border: 0.025rem solid #fff;
}
.confirm_select {
background-color: #56d176;
color: #fff;
border: 0.025rem solid #56d176;
span {
color: #fff;
}
}
}
}
}
.showcover-enter-active,
.showcover-leave-active {
transition: opacity 0.3s;
}
.showcover-enter,
.showcover-leave-active {
opacity: 0;
}
.back_cover {
position: fixed;
@include wh(100%, 100%);
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
</style>