vue 移动端项目总结(mint-ui)
回头看自己的代码,犹如鸡肋!!!里面有很多问题,建议大家不要看。我没时间整理╮(╯▽╰)╭
跨域解决方案
config/dev.env.js
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
API_ROOT: '"/api"'
})
config/prod.env.js ,生产的服务器(你线上运行时的服务器)
'use strict'
module.exports = {
NODE_ENV: '"production"',
API_ROOT: '"http://api.xxx.com/"'
}
config/index.js
proxyTable: {
'/api': {
// target: 'https://www.xxx.com/',
// target: 'http://m.xxx.com/',
target: 'http://api.xxx.com/',
changeOrigin: true,
secure: false,
pathRewrite: {
'^/api': ''
}
}
},
// Various Dev Server settings
// host: 'xxx.xxx.xx.x', // can be overwritten by process.env.HOST //公司本地IP
host: 'localhost', // can be overwritten by process.env.HOST
port: 8085, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
接口请求的时候
export const _HomeNavList = params => {
return req('post', rootUrl+ '/xxxx/xxxxx/xxxxx/xxx',params)
}
rem设置
/*rem设置*/ html{ font-size: calc(100vw/7.5); /*1rem=100px*/ }
js 设置 rem,如下。
(function (doc, win) { var docEl = doc.documentElement, // 手机旋转事件,大部分手机浏览器都支持 onorientationchange 如果不支持,可以使用原始的 resize resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize', recalc = function () { //clientWidth: 获取对象可见内容的宽度,不包括滚动条,不包括边框 var clientWidth = docEl.clientWidth; if (!clientWidth) return; docEl.style.fontSize = 100 * (clientWidth / 750) + 'px'; }; recalc(); if (!doc.addEventListener) return; //注册翻转事件 win.addEventListener(resizeEvt, recalc, false); })(document, window);
axios 的封装
刚开始我也是设置 axios 的 baseURL ,以及在拦截器里把数据用 qs 序列化。后来又改回来了。
最后有个需求是要把图片上传到七牛云(后面会讲),那么 axios 就不能在拦截器里设置了。
import axios from 'axios'
import qs from 'qs'
axios.defaults.timeout = 5000;
// axios.defaults.baseURL = process.env.API_ROOT; //填写域名
//http request 拦截器
axios.interceptors.request.use(
config => {
// config.data = qs.stringify(config.data);
config.headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
return config;
},
error => {
return Promise.reject(err);
}
);
//响应拦截器即异常处理
axios.interceptors.response.use(response => {
return response
}, err => {
if (err && err.response) {
switch (err.response.status) {
case 400:
console.log('错误请求')
break;
case 401:
console.log('未授权,请重新登录')
break;
case 403:
console.log('拒绝访问')
break;
case 404:
console.log('请求错误,未找到该资源')
break;
case 405:
console.log('请求方法未允许')
break;
case 408:
console.log('请求超时')
break;
case 500:
console.log('服务器端出错')
break;
case 501:
console.log('网络未实现')
break;
case 502:
console.log('网络错误')
break;
case 503:
console.log('服务不可用')
break;
case 504:
console.log('网络超时')
break;
case 505:
console.log('http版本不支持该请求')
break;
default:
console.log(`连接错误${err.response.status}`)
}
} else {
console.log('连接到服务器失败')
}
return Promise.resolve(err.response)
})
// 通用公用方法
export const req = (method, url, params) => {
return axios({
method: method,
url: url,
data: qs.stringify(params) ,
// traditional: true,
}).then(res => res.data);
};
底部菜单栏
mint-ui的底部菜单栏,我个人觉得用的不是很习惯,找了很多资料才勉强填上这个坑,如下:
<mt-tabbar class="bottom-tab" v-model="tabSelected"> <mt-tab-item id="home"> <span class="iconfont icon-zhuye"></span> <p>首页</p> </mt-tab-item> <mt-tab-item id="study"> <span class="iconfont icon-xianshanghuodong"></span> <p>学习</p> </mt-tab-item> <mt-tab-item id="ask"> <span class="iconfont icon-kefu"></span> <p>咨询</p> </mt-tab-item> <mt-tab-item id="user"> <span class="iconfont icon-wode"></span> <p>我的</p> </mt-tab-item> </mt-tabbar>
路由嵌套,底部 tabbar 是第一层组件,点击底部元素,可切换不同模块
{ path: '/bottomTab', component: bottomTab, children: [{ path: '/home', name: 'home', component: home, meta: { keepAlive: true, } }, { path: '/study', name: 'study', component: study, meta: { RequireLogin: true } }, ... }
最后我是没有设置默认选中,默认的是组件创建的时候的路由名称,然后在路由变化时,直接 watch 监控路由的名称,并作出相关操作
export default { data() { return { tabSelected: "", routerPath: "" }; }, created() { this.tabSelected = this.$route.path.slice(1); }, mounted() {}, beforeDestroy() {}, watch: { $route(to, from) { if ( to.name == "home" || to.name == "study" || to.name == "ask" || to.name == "user" ) { this.routerPath = this.$route.path; this.tabSelected = this.routerPath.slice(1); } }, tabSelected: function(val, oldVal) { this.$router.push({ name: val }); } }, methods: {}, computed: {} };
返回上一页
顶部返回上一页,有的需求是直接发个详情页的链接给别人,然后客户在点击返回的时候,是没有本站的浏览记录的。那么可能会退出到一个空白页,造成不必要的客户流失,我们的需求是让他去首页或者本站的其他页面,留住客户。
<mt-header :title="headTitle"> <mt-button icon="back" slot="left" @click="goBack">返回</mt-button> </mt-header>
浏览记录最少要有2条,否则去首页。我刚开始写的1,最后发现空白页也是记录。
methods: { goBack() { if (window.history.length <= 2) { this.$router.push({ path: "/" }); return false; } else { this.$router.back(); } } },
路由守卫
有些页面是需要登录了之后才能进的,这样的页面如果多了,就可以用路由守卫来判断
router.beforeEach((to, from, next) => { // 登录页、不需要登陆的和已登录的页面直接跳转 if (to.path == "/login" || !to.meta.RequireLogin || localStorage.getItem("user")) { next(); } else { next({ path: "/login", query: { redirect: to.fullPath } }) } })
路由守卫跳转过来的登录页,是带参的(目标页面的路径)在登录成功之后,就自动进入目标页面
if (r.Code == 0) { this.LOGIN(r.Data) if (this.$route.query.redirect) { this.$router.push({ path: this.$route.query.redirect }); } else { this.$router.push("/"); } }
列表页上拉加载,下拉刷新
课程列表页做了这个功能,待验证,使用 mt-loadmore
视频格式 m3u8
这个格式的视频还是挺多的,但是实现播放的话,就有点复杂了(要装两个插件,还一堆问题),折腾了两天,这速度算快还是慢呢。
import 'video.js/dist/video-js.css' import 'vue-video-player/src/custom-theme.css' import videojs from 'video.js'
//得手动绑定对象,不然找不到 window.videojs = videojs
//这里写成 import 还不行,必须得 require require('videojs-contrib-hls/dist/videojs-contrib-hls');
写元素的时候,可以不用写 source
<video id="video-wrap" class="video-js vjs-custom-skin vjs-big-play-centered"> <!-- <source src="http://xxx.m3u8" type="application/x-mpegURL" > --> </video>
如果多格式类型的视频,可能需要自动判断
mounted() {// 创建播放器 this.videoPlay = videojs('video-wrap', { playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度 autoplay: false, //如果true,浏览器准备好时开始播放 muted: false, //默认情况下将会消除任何音频 loop: false, //导致视频一结束就重新开始 preload: 'auto', //建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持) aspectRatio: '4:3', // 16:9 不会自动放中间 // fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。 // width:720, // height:540, notSupportedMessage: '此视频无法播放', controls: true, // sources: [{ // // type: "application/x-mpegURL", //video/mp4 // // type: "", //video/mp4 // // src: "http://37273.long-vod.cdn.aodiany210ad87667dd544439b28733e.m3u8", // }], // bigPlayButton: true, // textTrackDisplay: false, // posterImage: true, // errorDisplay: false, // controlBar: true }) }, watch: { onlineVideoVideoId(){ this.videoPlayUrl(this.onlineVideoInfo) } }, methods: { videoPlayUrl(info) { // 视频观看 // console.log(info); if (this.user) { if (info.bought == true) { // 获取课程视频 _StageItemPlayUrl({ courseId: this.$route.query.id, memberId: this.user.id, classId: info.videoId, }).then(res => { // console.log(res) if (res.Code === "0") { this.showVideoPlayer = true; this.changeVideo(res.Data.positiveUrl) } }).catch((err) => { console.log(err) }) } else { console.log('没有购买') this.$toast({ message: '请购买课程', position: 'bottom', duration: 2000 }); } } else { this.$router.push({ path: '/login', query: { redirect: to.fullPath } }) } }, changeVideo(vdSrc) { // 切换视频路径及类型 if (/\.m3u8$/.test(vdSrc)) { this.videoPlay.src({ src: vdSrc.replace("http://", "https://"), type: 'application/x-mpegURL' }) } else { this.videoPlay.src({ src: vdSrc, type: 'video/mp4' }) } this.videoPlay.load(); this.videoPlay.play(); //pause()暂停 销毁 dispose() }, }, beforeDestroy() { this.videoPlay.dispose(); console.log("video destroy"); }
在 build/webpack.base.conf.js 的 moudel 拿了要加上 noParse: [/videojs-contrib-hls/],不然可能会报错 t is not definded 之类的错误。
但是我在移动端没写上面这个操作,也播放成功了,PC端写了。现在不确定这段代码是否有必要。
module: { noParse: [/videojs-contrib-hls/], rules: [ { test: /\.vue$/, loader: 'vue-loader', options: vueLoaderConfig }, ...
mt-radio 的使用
需求是做一个单选的数据列表,但是 mt-radio 的 options 的数据结构是 label 和 value ,其中 label 是显示的名称, value 是值。
那么我们的数据结构就也得是 value 和 label 了,如果不是,就必须得手动转换。
Object.values(this.majorList).map(value => { value.value = value.id; value.label = value.majorName; // console.log(value) // console.log(this.majorList) });
自适应多层级目录
使用迭代,
调用:
<DetailMultiMenu v-for="(classItem,index) in this.classListData" :key="index" :item="classItem" @videoInfo="videoPlayUrl" ></DetailMultiMenu>
组件:
<template> <ul class="multi-menu"> <li v-if="item.stageName == ''"> <div class="class-title-wrap" @click="toggleItemList = !toggleItemList" :style="{marginLeft: 0.3*(item.level-1) +'rem'}" > <span> <i class="iconfont icon-caidan"></i> <span>{{item.classjName}}</span> </span> <i :class="[toggleItemList?'iconfont icon-xiangshang':'iconfont icon-xiangxia']"></i> </div> <ul v-for="(child,index) in item.sub" :key="index" v-show="toggleItemList"> <DetailMultiMenu v-if="child.stageName == ''" :item="child" :key="child.classjName"></DetailMultiMenu> <li v-else :key="child.id" :class="activeLi+index == child.id+index ? 'activeLi': ''" @click="videoInfo(child.id,child.isPay,child.id)" > <div class="class-item-wrap" :style="{marginLeft: 0.3*(child.level-1) +'rem'}"> <span class="class-title"> <i class="iconfont icon-zhibo11"></i> <span>{{child.classjName}}</span> </span> </div> </li> </ul> </li> <li v-else class="class-item-wrap"> <div> <i class="iconfont icon-zhibo11"></i> {{item.classjName}} </div> </li> </ul> </template> <script> import { // mapGetters, mapMutations // mapState } from "vuex"; export default { name: "DetailMultiMenu", data() { return { toggleItemList: false, activeLi: -1 }; }, props: { item: { type: Object, required: true } }, computed: {}, created() {}, mounted() {}, methods: { ...mapMutations([ "ONLINE_VIDEO_VIDEOID", "ONLINE_VIDEO_BOUGHT", "ONLINE_VIDEO_PLAY" ]), videoInfo(itemId, bought, videoId) { if (bought) { this.activeLi = itemId; } this.ONLINE_VIDEO_BOUGHT(bought); this.ONLINE_VIDEO_VIDEOID(videoId); // this.ONLINE_VIDEO_PLAY(true); // this.$emit("videoInfo",{bought, videoId} ); } }, watch: {}, components: {} }; </script> <style scoped> /* .activeLi { background: #26a2ff; } */ /* 课程目录 */ .multi-menu { font-size: 0.3rem; } .class-title-wrap { border-bottom: 1px solid #ddd; line-height: 1rem; height: 1rem; display: flex; justify-content: space-between; } .class-item-wrap { border-bottom: 1px solid #ddd; overflow: hidden; line-height: 1rem; height: 1rem; display: -webkit-box; /*! autoprefixer: off */ -webkit-box-orient: vertical; /* autoprefixer: on */ -webkit-line-clamp: 1; overflow: hidden; white-space: pre-line; font-size: 0.24rem; } </style>
行内切换 class 名
<i :class="[toggleItemList?'iconfont icon-xiangshang':'iconfont icon-xiangxia']"></i> <li v-else :key="child.id" :class="activeLi+index == child.id+index ? 'activeLi': ''" @click="videoInfo(child.id,child.isPay,child.id)" ></li>
行内样式自动计算
<div class="class-item-wrap" :style="{marginLeft: 0.3*(child.level-1) +'rem'}"></div>
七牛云上传图片
<input type="file" id="avatarUpload" ref="imgInput" accept="image/*" @change="PreviewImage" >
可以不用七牛云的插件,但要手动创建一个 formData,并且设置请求头
PreviewImage(event) { let file = event.target.files[0]; let formData = new FormData(); formData.append("file", file); formData.append("token", this.qiniutoke); this.$http({ url: "https://up-z2.qiniup.com", method: "POST", headers: { "Content-Type": "multipart/form-data" }, data: formData }) // _UploadQiniu({ // file:file, // token:this.qiniutoke // }) .then(res => { console.log(res); this.currentUser.handUrl = res.data.url + res.data.key; console.log(this.currentUser.handUrl); }) .catch(err => { console.log(err); }); }