(一) 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文件改动也能够触发自动刷新
var hotClient = require('webpack-hot-middleware/client');
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>
import App from '../App'
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,
children: [
{
path: '',
redirect: '/home'
},
{
path: '/home',
component: home
},
{
path: '/city/:cityid',
component: city
},
{
path: '/msite',
component: msite,
meta: { keepAlive: false },
},
.........
]
}]
-
配置项目整体依赖
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.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
});
export const msiteAddress = geohash => fetch('/v1/position/pois', {
geohash
});
export const msiteFoodTypes = geohash => fetch('/v2/index_entry', {
geohash,
group_type: '1'
});
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);
};
export const searchRestaurant = (geohash, keyword) => fetch('/v4/restaurants', {
'extras': 'restaurant_activity',
geohash,
keyword,
type: 'search'
});
export const foodCategory = (latitude, longitude) => fetch('/shopping/v2/restaurant/category', {
latitude,
longitude
});
export const foodDelivery = (latitude, longitude) => fetch('/shopping/v1/restaurants/delivery_modes', {
latitude,
longitude,
kw: ''
});
export const foodActivity = (latitude, longitude) => fetch('/shopping/v1/restaurants/activity_attributes', {
latitude,
longitude,
kw: ''
});
export const shopDetails = (shopid, latitude, longitude) => fetch('/shopping/restaurant/' + shopid, {
latitude,
longitude: longitude + '&extras=activities&extras=album&extras=license&extras=identification&extras=statistics'
});
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>
|