01:00
总体来说,功能不多,不过还是值得分析一下的:
a. 点击跳转到详情页;
b. 详情页含有定位,定位跳转到map;
c. 购票跳转到购票操作
a. 下单时间分组;
b. 瀑布流载入
c. 取消操作
d. toast插件(在这里toast,喜欢的欢迎star)
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;
先来看下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"})
首页的banner和懒载入就不赘述了,直接来看点击跳转到到详情页的配置:
routers :
{ path: /buy/:lid, name:buy, meta: { title: 购票 }, component: (resolve) => require([../views/buy.vue], resolve) },
通过id的传递,在buy组件里获取:
this.$route.params.lid
然后就可以根据这个id获取数据渲染详情信息啦。
地图组件的经纬度也是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; } } };
尴尬的后台数据就不吐槽了~ 直接来说下瀑布流吧
往回看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;
} } }
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插件,效果也是不错的,载入动画就交给你们了。
好啦,一个简单的webApp就这样完成一半啦。