As usual = = : 不看gif了,有点卡 ~ 看视频吧 @.@

视频封面

01:00

总体来说,功能不多,不过还是值得分析一下的:

1.首页 banner + 瀑布流,

a. 点击跳转到详情页;

b. 详情页含有定位,定位跳转到map;

c. 购票跳转到购票操作

2.订单

a. 下单时间分组;

b. 瀑布流载入

c. 取消操作

d. toast插件(在这里toast,喜欢的欢迎star

3.项目目录结构:

4.开始吧

vue-cli的脚手架构建就不赘述了~

a. App.vue构建:

<template>
<div id="app">
<router-view/>
<footer-nav></footer-nav>
</div>

</template>

底部按钮组件是公用的,可以提取出来做个footer-nav组件放在components层下,视图的切换展示就交给router-view了。

b. vue-router :

目录:

routers 配置的时候,组件可以做下按需载入:

const routers=[
{
path:./index,
name:index,
meta:{
title:首页
},
component:(resolve)=>require([../views/index.vue],resolve)
},
...
{
path:*,
redirect:./index
}
]

export default routers;

index.js : (细节都在注释里面啦~)

import Vue from vue;
import Router from vue-router;
import _Routers from ./routers

Vue.use(Router);

const RouterConfig = { //router配置
mode:hash,
routes:_Routers
}

const router = new Router(RouterConfig);

router.beforeEach((to,from,next)=>{
window.document.title=to.meta.title //标题头设置
//底部按钮切换可以在这里做监听,稍后附上 。 。 。
})

router.afterEach((to, from, next) => { //视图切换的时候滚动条置顶
window.scrollTo(0, 0);
});

export default router;

5. 实现底部菜单按钮的切换功能

先来看下footer.vue :

<template>
<div class="footerNav">
<div flex="dir:left main:center crss:center box:mean">
<router-link :to="{name:efuzhou_index}">
<div class="toIndex footerNavItem" :class="[currentIndex==0?active:]">
<span class="iconBox"></span>
<span>首页</span>
</div>
</router-link>
<router-link :to="{name:order}">
<div class="toOrder footerNavItem" :class="[currentIndex==1?active:]">
<span class="iconBox"></span>
<span>订单</span>
</div>
</router-link>
</div>

</div>
</template>

按钮之外用router-link来实现视图的切换,在 :to之后通过组件的name来渲染组件

然后就是重点了, class的样式绑定与按钮组件的切换时密切联系的,这个时候我们就可以在之前router/index.js里的beforeEach进行监听操作了,当然,只做这点准备是不够的,我们需要创建第一个store module ---- footer,也是最简单的一个:

const state = {
currentIndex: 0 //默认是0首页,与router的
}

const mutations = {
setIndexFooter(state) { //切换到首页
state.currentIndex = 0
},

setOrderFooter(state) { //切换到订单
state.currentIndex = 1;
},

setNoneFooter(state) { //切换到其他视图
state.currentIndex = -1;
}
}

const getters = {
_currentIndex: state => state.currentIndex //实时监听当前视图
}

const actions = {
setIndex(context) {
context.commit(setIndexFooter);
},

setOrder(context) {
context.commit(setOrderFooter);
},

setNone(context) {
context.commit(setNoneFooter);
}
}

export default {
state,
getters,
mutations,
actions
}

好啦,配置完store footer就可以继续下一步了,还记的meta的title吗?这个时候就可以用上了, 要注意的是这里store需要单独引入

import store from ../store/index;

router.beforeEach((to,from,next)=>{
window.document.title=to.meta.title //标题头设置
//底部按钮切换
if (to.meta.title === 产品列表) {
store.dispatch(setIndex);
} else if (to.meta.title === 订单) {
store.dispatch(setOrder); //派发action
} else {
store.dispatch(setNone);
}
next();
})

在footer.vue组件上就需要映射对应的 _currentIndex 来渲染底部组件:

import {mapGetters} from vuex;

computed:mapGetters({currentIndex:"_currentIndex"})

6.点击跳转到详情页:

首页的banner和懒载入就不赘述了,直接来看点击跳转到到详情页的配置:

routers :

{
path: /buy/:lid,
name:buy,
meta: {
title: 购票
},
component: (resolve) => require([../views/buy.vue], resolve)
},

通过id的传递,在buy组件里获取:

this.$route.params.lid

然后就可以根据这个id获取数据渲染详情信息啦。

7. 地图:

地图组件的经纬度也是router-link 参数传递过来的,用的是百度地图的api。

a. 首先,在页面引入

<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=你的密钥"></script>

b. 组件内:

import BMap from "BMap"; //引入BMap
import _AllHeight from "@/util/js/getAllHeight";
export default {
name: "myMap",
data() {
return {
latitude: 0,
longtitude: 0,
mapHeight: 800
};
},
created() {
this.loadMap();
},
mounted() {
this.ready();
this.getDeviceHeight();
},
methods: {
loadMap: function() { //经纬度参数获取
this.address = this.$route.params.address;
this.latitude = this.$route.params.latitude;
this.longtitude = this.$route.params.longtitude;
},

ready() { //地图渲染
const _this = this;
const map = new BMap.Map("allmap");
let point = new BMap.Point(_this.longtitude, _this.latitude); // 创建点坐标
map.centerAndZoom(point, 15);

const marker = new BMap.Marker(point); // 创建标注
map.addOverlay(marker); //将标注添加到地图中
map.panTo(point);

map.enableScrollWheelZoom(true);
map.addControl(new BMap.NavigationControl());
map.addControl(new BMap.ScaleControl());
},

getDeviceHeight() {
const device_h = _AllHeight.getDeviceHeight();
this.mapHeight = device_h;
}
}
};

8.订单组件

尴尬的后台数据就不吐槽了~ 直接来说下瀑布流吧

往回看map组件内有 import _AllHeight from "@/util/js/getAllHeight";

一起来看下吧:

/**
* @description 高度距离计算
*
*
* use:
*
* import _AllHeight from ../util/getAllHeight;
*
* _AllHeight.windowScroll((dis)=>{
* if(dis===0){
* _this.$store.dispatch(addMore);
* }
* })
*/

const _getAllHeight = {

/**
* @description 窗口可视化范围高度
* @returns px
*/
getDocumentHeight:function () {
let clientHeight = 0;
if (document.documentElement.clientHeight && document.body.clientHeight) {
clientHeight = (document.body.clientHeight < document.documentElement.clientHeight) ? document.body.clientHeight : document.documentElement.clientHeight;
} else {
clientHeight = (document.body.clientHeight > document.documentElement.clientHeight) ? document.body.clientHeight : document.documentElement.clientHeight;
}
return clientHeight;
},

/**
* @description 窗口滚动条高度
* @returns px
*/
getScrollTop:function () {
let scrollTop = 0;
if (document.documentElement && document.documentElement.scrollTop) {
scrollTop = document.documentElement.scrollTop;
} else if (document.body) {
scrollTop = document.body.scrollTop;
}
return scrollTop;
},

/**
* @description 文档内容高度
* @returns px
*/
getDeviceHeight:function () {
return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
},

/**
* @description 窗口滚动事件
* @author GJ-Chen
*/
windowScroll:function (callback,time) {
const _this = this;
var scroll = null;
window.onscroll = function () {
if(scroll) return;
scroll = setTimeout(function(){
_this.getAllHeight(callback);
scroll = null;
},time||200);
}
},

/**
* @description 自适应参数获取
* @author GJ-Chen
*/
windowResize:function () {
const _this = this;

window.onresize = function () {
_this.getAllHeight();
}
},

/**
* @description 高度参数获取
*/
getAllHeight:function (callback) {
var deviceHeight = this.getDocumentHeight();
var scrollTop = this.getScrollTop();
var contentHeight = this.getDeviceHeight();
var disBottom = contentHeight - scrollTop - deviceHeight;
callback(disBottom);
},
}

export default _getAllHeight

写好了就可以直接拿来用啦,通过判断滚动条距离底部的高度来进行载入更多的监听:

接下来就是 store order 了来进行数据的处理了, 先说下后台数据的返回,根据前端请求的页码和每页条目数目的设定返回对应的数据。 我们可以在getter通过每次请求获得的数据的长度来计算是否还有更多数据,返回boolean , 在组件上映射这个boolean,通过它来渲染组件是否显示载入动画和没有更多;其次就是载入更多的数据的解构添加后的渲染了,要注意的是当前视图的切换问题,要添加到对应的list上,可以通过之前路由设定的currentIndex判断来进行操作。一起来看下吧:

import getData from Server/getData;
import { getApiUrl } from @/util/js/getApiAccount

const api = 介面;
const page_size = 5; //初始换每页限制的条目数量

const state = {
unuseList: [],
usedList: [],
currentIndex: 0,
noMoreUnuse: false,
noMoreUsed: false,
len_unuse: 0,
len_used: 0,
isLoading: false, //loading动画的
unuse_page: 1, //未使用初始化页码
used_page: 1 //历史订单初始化页码
}

const getters = {
_unuseList: state => state.unuseList,
_usedList: state => state.usedList,
_noMoreUnuse: state => { //是否还有更多
if (state.len_unuse < page_size) {
state.noMoreUnuse = true
} else {
state.noMoreUnuse = false
}
return state.noMoreUnuse
},

_noMoreUsed: state => {

if (state.len_used < page_size) {
state.noMoreUsed = true
} else {
state.noMoreUsed = false
}
return state.noMoreUsed
},

_isLoading: state => state.isLoading
}

const mutations = {

/**
* @description 未使用订单
* @param {any} state
* @param {any} data
*/
setunuseList(state, data) {
state.unuseList = data
},

/**
* @description 历史订单
* @param {any} state
* @param {any} data
*/
setusedList(state, data) {
state.usedList = data
},

/**
* @description 视图设定
* @param {any} state
* @param {any} index
*/
setCurrentView(state, index) {
state.currentIndex = index;
},
}

const actions = {
/**
* @description 初始化未使用订单
* @param {any} context
*/
getUnuseList(context) {
getData(api, { type: unuse, page: 1, pageSize: page_size }, (res) => {
if (res.code == 200) {
context.commit(setunuseList, res.data.list);
state.len_unuse = res.data.list.length
}
})
},

/**
* @description 初始化历史订单
* @param {any} context
*/
getUsedList(context) {

getData(api, { type: history, page: 1, pageSize: page_size }, (res) => {
if (res.code == 200) {
context.commit(setusedList, res.data.list);
state.len_used = res.data.list.length
}
});

},

/**
* @description 视图切换
* @param {any} context
* @param {any} index
*/
setView(context, index) {
context.commit(setCurrentView, index);
},

/**
* @description 载入更多
* @param {any} context
* @param {any} state
*/
addMore({ commit, state }) {
state.isLoading = true;
switch (state.currentIndex) {
case 0: //未使用载入更多
let unuseAddList = [...state.unuseList];
if (!state.noMoreUnuse) {
getData(api, { type: unuse, page: ++state.unuse_page, pageSize: page_size }, (res) => {
state.len_unuse = res.data.list.length; //当前请求的数据列表长度设置
if (res.code === 200) {
res.data.list.forEach((item) => {
unuseAddList.push(item);
})
}
commit(setunuseList, unuseAddList);
})
}

break;
case 1:
let usedAddList = [...state.usedList];
if (!state.noMoreUsed) { //已使用载入更多
getData(api, { type: history, page: ++state.used_page, pageSize: page_size }, (res) => {
state.len_used = res.data.list.length;
if (res.code === 200) {
res.data.list.forEach((item) => {
usedAddList.push(item);
})
}
commit(setusedList, usedAddList);
})
}
break;

}
}
}

export default {
state,
getters,
mutations,
actions
}

order组件:

<template>
<div>
<div class="tabs">
<van-tabs :active="active" @click="handleTabClick">
<van-tab v-for="tab in tabs" :title="tab.type" :key="tab.view">
<div class="bg" v-if="(tab.view === unuse ? unuseList : usedList).length">
<div class="orderItem" v-for="item in (tab.view === unuse ? unuseList : usedList)" :key="item.ordertime">
<div class="ordertime">
<p class="font-gray" v-if="item.ordertime">下单时间:{{item.ordertime}}</p>
<div v-for="ticket in item.tickets" :key="ticket.tid">
<order-item :order="item" ></order-item>
</div>
</div>
</div>
<div class="loadingAnimation" v-if="( (currentIndex === 1 && !noMoreUsed && isLoading) ||(currentIndex === 0 && !noMoreUnuse && isLoading))">
<van-loading type="spinner" color="black" />
</div>
<div style="text-align:center" v-if="(currentIndex === 0 && noMoreUnuse)">
<p class="font-gray">没有更多啦</p>
</div>
<div style="text-align:center" v-if="(currentIndex === 1 && noMoreUsed)">
<p class="font-gray">没有更多啦</p>
</div>
</div>
<div v-else>
<div class="bg_noMore">
<p class="font-gray">暂无更多数据~</p>
</div>
</div>
</van-tab>
</van-tabs>
</div>
</div>
</template>

import orderItem from "@/components/orderItem.vue";
import { mapGetters } from "Vuex";
import _AllHeight from "@/util/js/getAllHeight";

export default {
name: "order",
components: { orderItem },
data() {
return {
active: 2,
tabs: [
{
type: "未使用订单",
view: "unuse"
},
{
type: "历史订单",
view: "used"
}
],
currentIndex: 0,
disBottom: -1
};
},
computed: mapGetters({
unuseList: "_unuseList",
usedList: "_usedList",
noMoreUnuse: "_noMoreUnuse",
noMoreUsed: "_noMoreUsed",
isLoading: "_isLoading"
}),
created() {
this.initData();
this.$bus.on("refresh", this.initData);
},

beforeDestroy() {
this.$bus.off("refresh", this.initData);
},

mounted() {
const _this = this;
_AllHeight.windowScroll(dis => { //判断滚动条距离底部高度来进行操作
if (dis === 0) {
_this.$store.dispatch("addMore"); //载入更多
}
});
},

methods: {
initData() { //数据初始化
this.$store.dispatch("getUnuseList");
this.$store.dispatch("getUsedList");
},

handleTabClick(index) {
window.scrollTo(0, 0);
this.currentIndex = index;
this.$store.dispatch("setView", index);
}
}
};

好啦,接下来就是每个orderItem的操作了,底部的取消订单按钮要记得加上 .prevent 。

说下订单详情页吧,二维码的用的是QRcode转换生成,方便用户直接扫码使用,取消订单操作是一样的,取消成功之后使用了全局的vue-bus来提交刷新数据事件:

/**
* @description use:
* import bus from ./util/js/vue-bus;
* Vue.use(bus);
*
*
* this.$bus.emit(refresh);
*
* created() {
* this.initData();
* this.$bus.on("refresh", this.initData);
* },
*
* beforeDestroy() {
* this.$bus.off("refresh", this.initData);
* }
*
*
*/
const install = (Vue) => {
const Bus = new Vue({
methods: {
emit(event, ...args) {
this.$emit(event, ...args);
},

on(event, callback) { //created 使用
this.$on(event, callback);
},

off(event, callback) { //beforeDestory 解除
this.$off(event, callback);
}
}
})

Vue.prototype.$bus = Bus;
}

export default install;

取消之后的提醒用的是上一篇的toast插件,至于载入动画,也可以用之前的loading-animation插件,效果也是不错的,载入动画就交给你们了。

SoldierAb/vue-toast-v.1.1?

github.com
图标

好啦,一个简单的webApp就这样完成一半啦。


推荐阅读:
相关文章