vue 移动端项目总结(mint-ui)

时间:2024-12-20 16:36:38

回头看自己的代码,犹如鸡肋!!!里面有很多问题,建议大家不要看。我没时间整理╮(╯▽╰)╭

跨域解决方案

  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设置

  有多种方式,可以 js,也可以 css 设置。
  鉴于 H5 的浏览器都比较高级,可以使用一些最新的属性,这里先介绍 css 的写法。 
/*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);
});
}