本文基于工作項(xiàng)目開發(fā),做的整理筆記
前段時(shí)間公司有的小伙伴剛開始學(xué)習(xí)vue,就直接著手用在新項(xiàng)目上,以項(xiàng)目實(shí)戰(zhàn)步步為營,不斷推進(jìn)vue的學(xué)習(xí)和使用。時(shí)間短,需求多,又是剛剛上手,遇到的坑和困難也真不少,感覺每天都在瘋狂地解決問題。說真的,每種技術(shù)的學(xué)習(xí)和使用,在實(shí)際項(xiàng)目的開發(fā)上得到了充分檢驗(yàn),個(gè)人能力也在快速的成長。
前一段時(shí)間,寫過文章“Vue教程--使用官方腳手架構(gòu)建實(shí)例”,主要是針對PC端,架構(gòu)而寫。當(dāng)初的目的,也是想做為一個(gè)入門的教程,但是根據(jù)反饋和自己后面的感受,發(fā)現(xiàn)并不是很好,并沒有做到真正的一步步上手。
今天決定專門針對Wap端去做這樣一個(gè)demo,整體架構(gòu)的搭建,并含有一些通用的功能。其中,部分知識點(diǎn)請回看前面那篇文章。對比來看,此篇應(yīng)該更為詳細(xì),步步為營。
前提條件:
你已經(jīng)了解vue的基礎(chǔ)知識,嘗試過使用vue-cli官方腳手架搭建項(xiàng)目。
編碼環(huán)境:
system:OS X EI Capitan 10.12.5
npm:5.4.2
node:v8.8.0
vue-cli:@lastest
相關(guān)技術(shù)棧:
vue2 + vuex + vue-router + webpack + ES6/7 + fetch/axios + sass + flex + svg
相關(guān)地址:
項(xiàng)目代碼github地址:https://github.com/YuxinChou/vue-wap-demo
項(xiàng)目在線地址:http://www.knowing365.com
(可用手機(jī)掃描下文中二維碼,或用chrome瀏覽器模擬手機(jī)訪問)
參考項(xiàng)目:https://github.com/bailicangdu/vue2-elm

目錄
| - 0.傳送門
| - 1.安裝
| - 2.項(xiàng)目說明
| - 3.項(xiàng)目搭建
??| - Step1. 初始化
??| - Step2. 母版頁Layout
??| - Step3. 配置rem
??| - Step4. 配置sass
??| - Step5. 頂部導(dǎo)航header
??| - Step6. 引入iconfont
??| - Step7. 側(cè)邊菜單sidebar
??| - Step8. 底部導(dǎo)航footer
??| - Step9. 返回頂部backToTop(組件)
??| - Step10. 倉庫存儲store
??| - Step11. 側(cè)邊菜單狀態(tài)保存
??| - Step12. 搜索欄searchBar(組件)
??| - Step13. 頁面添加
??| - Step14. 彈窗提示(組件)
??| - ---------------------------------- 下內(nèi)容為詳解2
??| - Step15. 完善login頁面(fetch請求數(shù)據(jù))
??| - Step16. 合理引入svg
??| - Step17. 用axios實(shí)現(xiàn)請求(取代原生fetch)
??| - Step18. 登錄狀態(tài)存入倉庫
??| - Step19. 滾動加載更多(組件)
??| - Step20. 回到指定位置(組件)
??| - Step21. 完善消息列表頁面
??| - Step22. 頂部菜單改造(slot的使用)
??| - --------------------------------- 下內(nèi)容為詳解3
??| - Step23. 完善其他頁面
??| - Step24. 權(quán)限檢查
??| - Step25. 頁面切換動畫transition
??| - Step26. 輪播展示(swiper)
??| - Step26. 分享功能(vue-social-share)
??| - Step28. ...
| - 4.項(xiàng)目部署
??| - a)本地部署
??| - b)服務(wù)器部署
| - 5.后續(xù)
Step15. 完善login頁面(fetch請求數(shù)據(jù))
更新一下login頁面,代碼如下:
/**********************************************/
/* src/page/login/login.vue */
/**********************************************/
<template>
<div class="container" :style="'height:'+wHeight+'px;'">
<div class="logo">
<svg class="qq">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#qq"></use>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0;visibility:hidden">
<defs>
<symbol viewBox="0 0 120 120" id="qq">
<g>
<g>
<path fill="#F9AE08" d="M72.2,97c-6.2-1.3-9.6-1.7-11.4-1.8c-0.2,0-0.4,0-0.6,0c-0.3,0-0.5,0-0.6,0c-1.8,0.1-5.2,0.5-11.4,1.8
c-12.5,2.6-20.6,7.6-18,9.3c2.6,1.7,8.2,1.4,8.2,1.4l20.5,0.1l0,0l1.4,0l1.4,0l0,0l20.5-0.1c0,0,5.6,0.2,8.2-1.4
C92.8,104.6,84.6,99.6,72.2,97z"/>
<path d="M90.7,52.8V40.6C90.7,23.7,77,10,60.1,10c-16.9,0-30.6,13.7-30.6,30.6v12.3C19,71.7,17.2,90.7,19.2,91.3
c1.1,0.4,5.3-4.9,8.7-9.5c2.3,14.4,14.7,25.4,29.7,25.4h4c15.4,0,28.2-11.6,29.9-26.6c3.5,4.8,8.3,11.1,9.6,10.7
C103,90.7,101.2,71.6,90.7,52.8z"/>
<path fill="#FFFFFF" d="M75.2,59.1H45.1c-5,0-9,4-9,9v13.5c0,12.5,10.1,22.5,22.6,22.5h3c12.5,0,22.6-10.1,22.6-22.5V68.1
C84.2,63.2,80.1,59.1,75.2,59.1z"/>
<path fill="#EA1C27" d="M90.7,52.7c0,0-12.2,4.3-30.2,4.3c-18,0-30.9-4.4-30.9-4.4l-3.1,6.3l-2,4.7c0,0,7.2,2.1,16.1,3.7v15.4h14
V69c2.1,0.2,4.3,0.2,6.4,0.2c16.6,0,34.8-5.9,34.8-5.9l-2-4.5L90.7,52.7z"/>
<g>
<ellipse fill="#FFFFFF" cx="51.8" cy="30.1" rx="5" ry="8"/>
<ellipse fill="#FFFFFF" cx="68.4" cy="30.1" rx="5" ry="8"/>
<ellipse cx="53.2" cy="30.7" rx="2.5" ry="3.5"/>
<g>
<path d="M65.1,31.9c-0.1,0-0.3,0-0.4-0.1c-0.4-0.2-0.5-0.7-0.2-1c1-1.6,3.8-3.9,7.6-1.5c0.4,0.2,0.5,0.7,0.2,1
c-0.2,0.4-0.7,0.5-1,0.2c-3.5-2.2-5.4,0.9-5.5,1.1C65.6,31.8,65.4,31.9,65.1,31.9z"/>
</g>
</g>
<path fill="#F9AE08" d="M60.3,40.2c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-4.6,0-18.2,1.3-18.2,3.8c0,2.5,10.9,6,18.2,6
c0.1,0,0.1,0,0.2,0c0.1,0,0.1,0,0.2,0c7.3,0,18.2-3.6,18.2-6C78.5,41.5,64.9,40.2,60.3,40.2z"/>
</g>
</g>
</symbol>
</defs>
</svg>
</div>
<form class="login_form">
<section class="input_container">
<input type="text" placeholder="郵箱" autocomplete="off" v-model.lazy="userAccount">
</section>
<section class="input_container">
<input v-if="!showPassword" type="password" placeholder="密碼" autocomplete="off" v-model="passWord">
<input v-else type="text" placeholder="密碼" autocomplete="off" v-model="passWord">
<div class="btn_switch" :class="{change_to_text: showPassword}">
<div class="btn_switch_circel" :class="{trans_to_right: showPassword}" @click="changePassWordType"></div>
<span>abc</span>
<span>***</span>
</div>
</section>
</form>
<div class="btn_block" @click="loginSubmit">登錄</div>
<div class="login_to clear">
<router-link class="left" to="/signup">還沒有賬號?立即注冊</router-link>
<router-link class="right" to="/forget">忘記密碼</router-link>
</div>
<alert-tip v-if="showAlert" :showHide="showAlert" @closeTip="closeTip" :alertText="alertText"></alert-tip>
</div>
</template>
<script>
import alertTip from '@/components/common/alertTip'
import {accountLogin} from '@/service/getData'
import { mapMutations } from 'vuex'
export default {
data() {
return {
wHeight: 0,
userAccount: "admin@fusio.com.cn",
passWord: "123456",
showPassword: false,
showAlert: false,
alertText: "",
}
},
components: {
alertTip,
},
mounted() {
this.wHeight = document.documentElement.clientHeight || document.body.clientHeight;
},
methods: {
changePassWordType() {
this.showPassword = !this.showPassword;
},
async loginSubmit() {
if (!this.userAccount) {
this.showAlert = true;
this.alertText = '請輸入手機(jī)號/郵箱/用戶名';
return
}else if(!this.passWord){
this.showAlert = true;
this.alertText = '請輸入密碼';
return
}
//用戶名登錄
console.log(this.userAccount);
console.log(this.passWord);
// 哈咯,各位閱讀文章的小伙伴,
// 登錄接口由于業(yè)務(wù)調(diào)整已經(jīng)暫停,
// 做到這里,可以注釋,暫時(shí)當(dāng)請求返回成功處理。
// 晚些更新接口后,會再次更新文章內(nèi)容。
// 造成不便,十分抱歉。
// 暫時(shí)注釋
// let response = await accountLogin(this.userAccount, this.passWord);
// 開啟這個(gè)模擬登錄成功
let response = { retCode: "10000", msg: "請求成功", data: {} };
//如果返回的值不正確,則彈出提示框,返回的值正確則返回上一頁
if (response.retCode!="10000") {
this.showAlert = true;
this.alertText = response;
}else{
// 緩存用戶數(shù)據(jù)(等下處理)
// ...
// 跳轉(zhuǎn)到消息列表頁
this.$router.push({ path: '/messages' });
}
},
closeTip() {
this.showAlert = false;
},
}
}
</script>
<style lang="scss" scoped>
.container {
padding: 1rem;
height: 100%;
text-align: center;
background-color: #a4e3ff;
.logo {
padding: 2rem 1rem 1rem;
span {
font-size: 1.4rem;
}
}
.login_form {
.input_container{
display: flex;
justify-content: space-between;
padding: .6rem .8rem;
background-color: #fff;
border-bottom: 1px solid #f1f1f1;
input{
font-size: 0.7rem;
color: #666;
width: 100%;
}
button{
font-size: 0.65rem;
color: #fff;
font-family: Helvetica Neue,Tahoma,Arial;
padding: .28rem .4rem;
border: 1px;
border-radius: 0.15rem;
}
.right_phone_number{
background-color: #4cd964;
}
}
}
.btn_switch{
background-color: #ccc;
display: flex;
justify-content: center;
width: 2.1rem;
height: 1rem;
padding: 0 0.2rem;
border: 1px;
border-radius: 0.5rem;
position: relative;
.btn_switch_circel{
transition: all .3s;
position: absolute;
top: -0.1rem;
left: -0.2rem;
z-index: 1;
width: 1.24rem;
height: 1.24rem;
box-shadow: 0 0.03rem 0.05rem 0 rgba(0,0,0,.1);
background-color: #5cacf9;
border-radius: 50%;
cursor: pointer;
}
.trans_to_right{
transform: translateX(1.4rem);
}
span{
font-size: 0.4rem;
line-height: 1rem;
color: #fff;
}
span:nth-of-type(2){
transform: translateY(0.08rem);
}
}
.btn_block {
margin: 1rem 0;
padding: 0.5rem 0;
font-size: 0.7rem;
color: #fff;
background-color: #4eaaff;
border: 1px;
border-radius: 0.15rem;
text-align: center;
}
.login_to {
a {
color: #666;
}
}
}
</style>
代碼中,我們引用了src/service/getData.js文件,它是一個(gè)接口服務(wù)文件。我們現(xiàn)在創(chuàng)建,在src文件夾下創(chuàng)建service文件夾,及getData.js文件,代碼如下:
/**********************************************/
/* src/service/getData.js */
/**********************************************/
import fetch from '../utils/fetch';
/**
* 賬號密碼登錄
*/
export const accountLogin = (email, password) => fetch('/loginController/login', { email, password }, 'POST');
接著我們創(chuàng)建../utils/fetch.js文件,代碼如下:
/**********************************************/
/* src/utils/fetch.js */
/**********************************************/
import { baseUrl } from '../../config/dev.env';
export default async(url = '', data = {}, type = 'GET', method = 'fetch') => {
type = type.toUpperCase();
url = baseUrl + url;
if (type == 'GET') {
let dataStr = ''; //數(shù)據(jù)拼接字符串
Object.keys(data).forEach(key => {
dataStr += key + '=' + data[key] + '&';
});
if (dataStr !== '') {
dataStr = dataStr.substr(0, dataStr.lastIndexOf('&'));
url = url + '?' + dataStr;
}
}
if (window.fetch && method == 'fetch') {
let requestConfig = {
method: type,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
cache: "force-cache"
}
if (type == 'POST') {
var params = "";
// 'Content-Type': 'application/json; charset=utf-8'
// Json串 JSON.stringify(data)
// params = JSON.stringify(data);
// 'Content-Type': 'multipart/form-data'
// formData拼接
// var formData = new FormData();
// for (var name in data) {
// formData.append(name, data[name]);
// }
// 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
// 字符串拼接 "email=aaa&password=xxxxx"
Object.keys(data).forEach(key => {
params += key + '=' + data[key] + '&';
});
if (params !== '') {
params = params.substr(0, params.lastIndexOf('&'));
}
Object.defineProperty(requestConfig, 'body', {
value: params
});
}
try {
const response = await fetch(url, requestConfig);
const responseJson = await response.json();
return responseJson
} catch (error) {
throw new Error(error)
}
} else {
// 非fetch
return new Promise((resolve, reject) => {
let requestObj;
if (window.XMLHttpRequest) {
requestObj = new XMLHttpRequest();
} else {
requestObj = new ActiveXObject;
}
let sendData = '';
if (type == 'POST') {
// Json串
// sendData = JSON.stringify(data);
// 字符串拼接
Object.keys(data).forEach(key => {
sendData += key + '=' + data[key] + '&';
});
if (sendData !== '') {
sendData = sendData.substr(0, sendData.lastIndexOf('&'));
}
}
requestObj.open(type, url, true);
requestObj.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
requestObj.send(sendData);
requestObj.onreadystatechange = () => {
if (requestObj.readyState == 4) {
if (requestObj.status == 200) {
let obj = requestObj.response
if (typeof obj !== 'object') {
obj = JSON.parse(obj);
}
resolve(obj)
} else {
reject(requestObj)
}
}
}
})
}
}
在config/dev.env.js文件中添加一個(gè)項(xiàng)目配置變量baseUrl,代碼如下:
/**********************************************/
/* config/dev.env.js */
/**********************************************/
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
baseUrl: 'http://52.80.21.233:8040'
})
那么這個(gè)請求我們就做好了,試一試請求是否成功。
注意:這是一個(gè)這個(gè)項(xiàng)目demo中唯一的真實(shí)數(shù)據(jù)請求接口,其他類似請求如果要使用真實(shí)接口可參考這里。
Step16. 合理引入svg
在login頁面的代碼里,我們放置了一大段svg繪制代碼,不是很好,如果其他頁面要用,可能還拿不到。這里,我們更好的是將整站的svg抽離成一個(gè)組件。
在src/components/common中添加svg.vue,代碼如下:
/**********************************************/
/* src/components/common/svg.vue */
/**********************************************/
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0;visibility:hidden">
<defs>
<symbol viewBox="0 0 120 120" id="qq">
<g>
<g>
<path fill="#F9AE08" d="M72.2,97c-6.2-1.3-9.6-1.7-11.4-1.8c-0.2,0-0.4,0-0.6,0c-0.3,0-0.5,0-0.6,0c-1.8,0.1-5.2,0.5-11.4,1.8
c-12.5,2.6-20.6,7.6-18,9.3c2.6,1.7,8.2,1.4,8.2,1.4l20.5,0.1l0,0l1.4,0l1.4,0l0,0l20.5-0.1c0,0,5.6,0.2,8.2-1.4
C92.8,104.6,84.6,99.6,72.2,97z"/>
<path d="M90.7,52.8V40.6C90.7,23.7,77,10,60.1,10c-16.9,0-30.6,13.7-30.6,30.6v12.3C19,71.7,17.2,90.7,19.2,91.3
c1.1,0.4,5.3-4.9,8.7-9.5c2.3,14.4,14.7,25.4,29.7,25.4h4c15.4,0,28.2-11.6,29.9-26.6c3.5,4.8,8.3,11.1,9.6,10.7
C103,90.7,101.2,71.6,90.7,52.8z"/>
<path fill="#FFFFFF" d="M75.2,59.1H45.1c-5,0-9,4-9,9v13.5c0,12.5,10.1,22.5,22.6,22.5h3c12.5,0,22.6-10.1,22.6-22.5V68.1
C84.2,63.2,80.1,59.1,75.2,59.1z"/>
<path fill="#EA1C27" d="M90.7,52.7c0,0-12.2,4.3-30.2,4.3c-18,0-30.9-4.4-30.9-4.4l-3.1,6.3l-2,4.7c0,0,7.2,2.1,16.1,3.7v15.4h14
V69c2.1,0.2,4.3,0.2,6.4,0.2c16.6,0,34.8-5.9,34.8-5.9l-2-4.5L90.7,52.7z"/>
<g>
<ellipse fill="#FFFFFF" cx="51.8" cy="30.1" rx="5" ry="8"/>
<ellipse fill="#FFFFFF" cx="68.4" cy="30.1" rx="5" ry="8"/>
<ellipse cx="53.2" cy="30.7" rx="2.5" ry="3.5"/>
<g>
<path d="M65.1,31.9c-0.1,0-0.3,0-0.4-0.1c-0.4-0.2-0.5-0.7-0.2-1c1-1.6,3.8-3.9,7.6-1.5c0.4,0.2,0.5,0.7,0.2,1
c-0.2,0.4-0.7,0.5-1,0.2c-3.5-2.2-5.4,0.9-5.5,1.1C65.6,31.8,65.4,31.9,65.1,31.9z"/>
</g>
</g>
<path fill="#F9AE08" d="M60.3,40.2c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-4.6,0-18.2,1.3-18.2,3.8c0,2.5,10.9,6,18.2,6
c0.1,0,0.1,0,0.2,0c0.1,0,0.1,0,0.2,0c7.3,0,18.2-3.6,18.2-6C78.5,41.5,64.9,40.2,60.3,40.2z"/>
</g>
</g>
</symbol>
</defs>
</svg>
</template>
<script>
export default {
}
</script>
<style lang="scss">
</style>
修改App.vue,將它引入,代碼如下:
/**********************************************/
/* src/App.vue */
/**********************************************/
<template>
<div id="app">
<router-view></router-view>
<!--所有用到的svg可以丟這里-->
<svg-icon></svg-icon>
</div>
</template>
<script>
import svgIcon from '@/components/common/svg'
export default {
name: 'app',
components: {
svgIcon
},
}
</script>
<style lang="scss">
</style>
接著,去掉login頁面那一段繪制代碼,也能看到效果的。
Step17. 用axios實(shí)現(xiàn)請求(取代原生fetch)
有人可能說原生fetch對瀏覽器的支持情況不太好,可否換一個(gè)呢?那么我也可以用axios去實(shí)現(xiàn)請求。
在src/utils中添加axios.js文件,代碼如下:
/****************************************** */
/* src/utils/axios.js */
/****************************************** */
import axios from 'axios';
import store from '../store';
import { baseUrl } from '../../config/dev.env';
export default (url = '', data = {}, type = 'GET', method = 'fetch') => {
return new Promise((resolve, reject) => {
const instance = axios.create({
baseURL: baseUrl,
timeout: 20000 //,
//headers: { 'authToken': store.getters.token } //如果后臺請求都需要攜帶的話
});
instance({
url: url,
method: type,
params: data
})
.then(response => {
const res = response.data;
// 50001:token已過期
// 50002:token非法
if (res.retCode === "50001" || res.retCode === "50002") {
router.push({ path: '/login' })
reject(res);
}
resolve(res);
})
.catch(error => {
router.push({ path: '/404', query: { error: error } });
reject(error);
});
});
}
當(dāng)然,要使用這種方式的話,我們還要安裝axios模塊,執(zhí)行:
$ npm install axios --save
修改src/service/getData.js文件,使用axios進(jìn)行請求,代碼如下:
/****************************************** */
/* src/service/getData.js */
/****************************************** */
// import fetch from '../utils/fetch';
import fetch from '../utils/axios';
/**
* 賬號密碼登錄
*/
export const accountLogin = (email, password) => fetch('/loginController/login', { email, password }, 'POST');
Step18. 登錄狀態(tài)存入倉庫
修改一下login頁面的代碼,代碼如下
/****************************************** */
/* src/page/login/login.vue */
/****************************************** */
// 其他地方不改
...
// 緩存用戶數(shù)據(jù)
let email = response.data.adminInfo.email;
let token = response.data.tokenModel.token;
this.$store.commit('SET_AUTH_INFO',[
{
email: email,
token: token
}
]);
...
需要在store的相關(guān)文件,添加SET_AUTH_INFO事件,修改如下:
/****************************************** */
/* src/store/modules/user.js */
/****************************************** */
import Cookies from 'js-cookie';
const app = {
state: {
email: '',
token: localStorage.getItem('token')
},
mutations: {
SET_AUTH_INFO: (state, info) => {
state.email = info.email;
state.token = info.token;
localStorage.token = info.token;
},
},
actions: {
}
};
export default app;
/****************************************** */
/* src/store/getters.js */
/****************************************** */
const getters = {
token: state => state.user.token,
email: state => state.user.email,
sidebar: state => state.app.sidebar,
scroll: state => state.app.scroll,
messages: state => state.common.messages,
contacts: state => state.common.contacts,
};
export default getters
/****************************************** */
/* src/store/index.js */
/****************************************** */
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';
import app from './modules/app';
import getters from './getters';
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
user,
app
},
getters
});
export default store
測試一下,查看是否正確。
Step19. 滾動加載更多(組件)
因?yàn)?code>messages頁面需要用到“滾動加載更多”這個(gè)功能,我們先看一下。
在src/components/common中添加mixin.js,里面的loadMore代碼如下:
/****************************************** */
/* src/components/common/mixin.js */
/****************************************** */
import { getStyle } from '../../utils/utils'
export const loadMore = {
directives: {
'load-more': {
bind: (el, binding) => {
let windowHeight = window.screen.height;
let height;
let setTop;
let paddingBottom;
let marginBottom;
let requestFram;
let oldScrollTop;
let scrollEl;
let heightEl;
let scrollType = el.attributes.type && el.attributes.type.value;
let scrollReduce = 2;
if (scrollType == 2) {
scrollEl = el;
heightEl = el.children[0];
} else {
scrollEl = document.body;
heightEl = el;
}
el.addEventListener('touchstart', () => {
height = heightEl.clientHeight;
if (scrollType == 2) {
height = height
}
setTop = el.offsetTop;
paddingBottom = getStyle(el, 'paddingBottom');
marginBottom = getStyle(el, 'marginBottom');
}, false)
el.addEventListener('touchmove', () => {
loadMore();
}, false)
el.addEventListener('touchend', () => {
oldScrollTop = scrollEl.scrollTop;
moveEnd();
}, false)
const moveEnd = () => {
requestFram = requestAnimationFrame(() => {
if (scrollEl.scrollTop != oldScrollTop) {
oldScrollTop = scrollEl.scrollTop;
moveEnd()
} else {
cancelAnimationFrame(requestFram);
height = heightEl.clientHeight;
loadMore();
}
})
}
const loadMore = () => {
if (scrollEl.scrollTop + windowHeight >= height + setTop + paddingBottom + marginBottom - scrollReduce) {
binding.value();
}
}
}
}
}
};
它引入了src/utils/utils.js文件的getStyle方法,我們現(xiàn)在添加一下,代碼如下:
/****************************************** */
/* src/utils/utils.js */
/****************************************** */
/**
* 獲取style樣式
*/
export const getStyle = (element, attr, NumberMode = 'int') => {
let target;
// scrollTop 獲取方式不同,沒有它不屬于style,而且只有document.body才能用
if (attr === 'scrollTop') {
target = element.scrollTop;
} else if (element.currentStyle) {
target = element.currentStyle[attr];
} else {
target = document.defaultView.getComputedStyle(element, null)[attr];
}
//在獲取 opactiy 時(shí)需要獲取小數(shù) parseFloat
return NumberMode == 'float' ? parseFloat(target) : parseInt(target);
}
/**
* 滾動到指定位置
*/
export const scrollPosition = scroll => {
let top = scroll[location.pathname];
if (top != undefined) {
setTimeout(function() {
document.documentElement.scrollTop = top;
document.body.scrollTop = top;
}, 100);
}
}
其中,scrollPosition方法為下面“回到指定位置”功能所需,我們先加著。
“滾動加載更多”的具體使用,稍后見“完善消息列表頁面”部分內(nèi)容。
Step20. 回到指定位置(組件)
我們先思考一下這個(gè)功能的核心問題,第1個(gè)就是頁面離開前要保存滾動位置,第2個(gè)就是回到頁面滾到指定位置。
針對第1個(gè)問題,我們可以監(jiān)聽滾動時(shí)間,實(shí)時(shí)保存這個(gè)值。這種較為容易實(shí)現(xiàn)。針對第2個(gè)問題就無需討論。
在Layout頁面下,添加滾動監(jiān)聽,如下:
/**********************************************/
/* src/components/Layout.vue */
/**********************************************/
...
// 僅插入這段代碼即可
mounted() {
window.onscroll = () => {
this.$store.dispatch('SetScroll');
};
}
...
修改src/store/modules/app.js,添加SetScroll相關(guān)代碼,如下:
/**********************************************/
/* src/store/modules/app.js */
/**********************************************/
import Cookies from 'js-cookie';
const app = {
state: {
sidebar: !+Cookies.get('sidebarStatus'),
scroll: {},
},
mutations: {
TOGGLE_SIDEBAR: state => {
if (state.sidebar) {
Cookies.set('sidebarStatus', 1);
} else {
Cookies.set('sidebarStatus', 0);
}
state.sidebar = !state.sidebar;
},
SET_SCROLL: (state, data) => {
state.scroll[data.name] = data.top;
}
},
actions: {
ToggleSideBar: ({ commit }) => {
commit('TOGGLE_SIDEBAR')
},
SetScroll: ({ commit, state }) => {
let nameValue = location.pathname;
let topValue = document.documentElement.scrollTop || document.body.scrollTop;
commit('SET_SCROLL', { name: nameValue, top: topValue });
},
}
};
export default app;
/**********************************************/
/* src/store/getters.js */
/**********************************************/
const getters = {
token: state => state.user.token,
email: state => state.user.email,
sidebar: state => state.app.sidebar,
scroll: state => state.app.scroll
};
export default getters
當(dāng)我們在某個(gè)頁面需要實(shí)現(xiàn)這樣的功能時(shí),在該頁面插入代碼:
import {scrollPosition} from '@/config/utils'
...
mounted(){
// 滾動到上一次位置
scrollPosition(this.$store.getters.scroll);
},
...
具體使用,可以看下面“完善消息列表頁面”內(nèi)容。
Step21. 完善消息列表頁面
修改messages頁面代碼,如下:
/**********************************************/
/* src/page/messages/messages.vue */
/**********************************************/
<template>
<div>
<!-- head -->
<head-top>
<span slot='head_text' class="head_text">消息</span>
<span slot='head_btn' class="head_btn" @click="handleHeadBtn"><i class="iconfont icon-tianjia"></i></span>
</head-top>
<!-- main -->
<div class="main_wrapper">
<div class="container" v-load-more="loaderMore">
<search-bar></search-bar>
<router-link to="/chat" class="item" v-for="(item, index) in data" :key="index">
<div class="item_image" :style="'background-image: url('+item.image+');'"></div>
<div class="item_info">
<div class="item_info_head">
<span class="name">{{item.name}}</span>
<span class="time">{{item.time}}</span>
</div>
<div class="item_info_content"><span>{{item.type}}</span> {{item.content}}</div>
</div>
</router-link>
<transition name="loading">
<div style="background-color: #fff; padding: 0.5rem; text-align: center; color: #999;" v-show="showLoading">Loading...</div>
</transition>
</div>
</div>
<!-- footer -->
<foot-menu :activeIndex="0"></foot-menu>
</div>
</template>
<script>
import headTop from '@/components/header/head'
import footMenu from '@/components/footer/footer'
import searchBar from '@/components/common/searchBar'
import {loadMore} from '@/components/common/mixin'
import {scrollPosition} from '@/utils/utils'
export default {
components: {
headTop,
footMenu,
searchBar
},
data () {
return {
data:[], // 列表數(shù)據(jù)
preventRepeatReuqest: false, //到達(dá)底部加載數(shù)據(jù),防止重復(fù)加載
showBackStatus: false, //顯示返回頂部按鈕
showLoading: true, //顯示加載動畫
touchend: false, //沒有更多數(shù)據(jù)
}
},
mixins: [loadMore],
mounted(){
this.initData();
// 滾動到上一次位置
scrollPosition(this.$store.getters.scroll);
},
methods: {
handleHeadBtn() {
console.log('####');
},
async initData(){
// 判斷之前是否請求過數(shù)據(jù)
if (this.$store.getters.messages.length == 0) {
// 請求數(shù)據(jù)
this.$store.dispatch('GetMessagesList').then(() => {
this.data = this.$store.getters.messages;
}).catch(err => {
console.log('error:'+err);
});
this.hideLoading();
} else {
// 直接取之前的數(shù)據(jù)
this.data = this.$store.getters.messages;
this.hideLoading();
}
},
async loaderMore(){
if (this.touchend) {
return
}
//防止重復(fù)請求
if (this.preventRepeatReuqest) {
return
}
this.showLoading = true;
this.preventRepeatReuqest = true;
// 觸發(fā)加載更多
this.$store.dispatch('GetMessagesList').then(() => {
this.data = this.$store.getters.messages;
}).catch(err => {
console.log('error:'+err);
});
this.hideLoading();
this.preventRepeatReuqest = false;
},
hideLoading(){
this.showLoading = false;
},
},
}
</script>
<style lang="scss" scoped>
.container {
margin-bottom: 1.95rem;
background-color: #f5f5f5;
}
.item {
display: flex;
flex-basis: 100%;
background-color: #fff;
.item_image {
display: flex;
margin: 0.25rem 0.5rem;
min-width: 2rem;
height: 2rem;
border-radius: 2rem;
background-size: cover;
background-position: 50%;
background-color: #d2d2d2;
border-bottom: 1px solid transparent;
}
.item_info {
display: flex;
flex-basis: 100%;
flex-direction: column;
padding: 0.25rem 0.5rem 0.25rem 0;
border-bottom: 1px solid #eee;
overflow: hidden;
.item_info_head {
padding-top: 0.2rem;
font-size: 0.5rem;
overflow: hidden;
color: #999;
> .name {
padding-top: 0.4rem;
color: #666;
font-size: 0.6rem;
font-weight: 600;
line-height: 1rem;
}
> .time {
float: right;
color: #999;
font-size: 0.5rem;
line-height: 1rem;
}
}
.item_info_content {
font-size: 0.5rem;
line-height: 1.4;
color: #666;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
span {
color: #f2a73b;
}
}
}
}
</style>
這個(gè)頁面的列表,我們使用了的倉庫存儲,所以我們還要更新一下src/store/modules/common.js文件,代碼如下:
/**********************************************/
/* src/store/modules/common.js */
/**********************************************/
const common = {
state: {
messages: [],
contacts: []
},
mutations: {
GET_MESSAGES_LIST: (state, list) => {
state.messages = [...state.messages, ...list];
},
GET_CONTACTS_LIST: (state, list) => {
state.contacts = [...state.contacts, ...list];
},
},
actions: {
// 獲取消息列表
GetMessagesList({ commit, state }) {
let list = [{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180518_0_KcXn.jpg",
name: "萬丈-Infinite",
time: "上午11:47",
type: "",
content: "+1"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20171012184532_0_xTaB.jpg",
name: "切圖者聯(lián)盟",
time: "上午11:47",
type: "",
content: "dav:世界上最可怕的不是孤獨(dú)終老,而是跟那個(gè)使你孤獨(dú)的人終老。"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
name: "群助手",
time: "上午11:40",
type: "[6個(gè)群有新消息]",
content: ""
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
name: "VR實(shí)驗(yàn)室",
time: "上午11:38",
type: "",
content: "收到"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181200_0_czDQ.jpg",
name: "我的電腦",
time: "上午11:36",
type: "",
content: "[圖片]IMG_8724.PNG"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182858_0_5sfc.jpg",
name: "QQ看點(diǎn)",
time: "上午11:30",
type: "",
content: "健身最女王:除了彭于晏,也就他的身材讓女人著迷!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171022233804_0_0QNg.jpg",
name: "D3.js",
time: "上午11:30",
type: "",
content: "w 加入本群"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182002_0_teEG.jpg",
name: "購物號",
time: "上午11:27",
type: "[新消息]",
content: "蘑菇街每日精選:秋冬好貨雙十一,全場49元封頂!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183756_0_i4am.jpg",
name: "夕陽",
time: "上午11:20",
type: "",
content: "知道"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183530_0_CyI7.jpg",
name: "小紅帽",
time: "上午11:19",
type: "",
content: "就這樣處理吧"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/300/20170929190950_0_THbY.jpg",
name: "京東商城",
time: "上午11:17",
type: "[新消息]",
content: "京東每日精選:好貨雙十一,全場99元封頂!"
}
];
commit('GET_MESSAGES_LIST', list);
},
// 獲取聯(lián)系人列表
GetContactsList({ commit, state }) {
let list = [{
name: "特別關(guān)心",
online: 2,
total: 2,
closed: true,
list: [{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180518_0_KcXn.jpg",
name: "萬丈-Infinite",
time: "上午11:47",
type: "",
content: "+1"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20171012184532_0_xTaB.jpg",
name: "切圖者聯(lián)盟",
time: "上午11:47",
type: "",
content: "dav:世界上最可怕的不是孤獨(dú)終老,而是跟那個(gè)使你孤獨(dú)的人終老。"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
name: "群助手",
time: "上午11:40",
type: "[6個(gè)群有新消息]",
content: ""
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
name: "VR實(shí)驗(yàn)室",
time: "上午11:38",
type: "",
content: "收到"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181200_0_czDQ.jpg",
name: "我的電腦",
time: "上午11:36",
type: "",
content: "[圖片]IMG_8724.PNG"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182858_0_5sfc.jpg",
name: "QQ看點(diǎn)",
time: "上午11:30",
type: "",
content: "健身最女王:除了彭于晏,也就他的身材讓女人著迷!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171022233804_0_0QNg.jpg",
name: "D3.js",
time: "上午11:30",
type: "",
content: "w 加入本群"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182002_0_teEG.jpg",
name: "購物號",
time: "上午11:27",
type: "[新消息]",
content: "蘑菇街每日精選:秋冬好貨雙十一,全場49元封頂!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183756_0_i4am.jpg",
name: "夕陽",
time: "上午11:20",
type: "",
content: "知道"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183530_0_CyI7.jpg",
name: "小紅帽",
time: "上午11:19",
type: "",
content: "就這樣處理吧"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/300/20170929190950_0_THbY.jpg",
name: "京東商城",
time: "上午11:17",
type: "[新消息]",
content: "京東每日精選:好貨雙十一,全場99元封頂!"
}
]
},
{
name: "我的好友",
online: 23,
total: 56,
closed: true,
list: [{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180518_0_KcXn.jpg",
name: "萬丈-Infinite",
time: "上午11:47",
type: "",
content: "+1"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20171012184532_0_xTaB.jpg",
name: "切圖者聯(lián)盟",
time: "上午11:47",
type: "",
content: "dav:世界上最可怕的不是孤獨(dú)終老,而是跟那個(gè)使你孤獨(dú)的人終老。"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
name: "群助手",
time: "上午11:40",
type: "[6個(gè)群有新消息]",
content: ""
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
name: "VR實(shí)驗(yàn)室",
time: "上午11:38",
type: "",
content: "收到"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181200_0_czDQ.jpg",
name: "我的電腦",
time: "上午11:36",
type: "",
content: "[圖片]IMG_8724.PNG"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182858_0_5sfc.jpg",
name: "QQ看點(diǎn)",
time: "上午11:30",
type: "",
content: "健身最女王:除了彭于晏,也就他的身材讓女人著迷!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171022233804_0_0QNg.jpg",
name: "D3.js",
time: "上午11:30",
type: "",
content: "w 加入本群"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182002_0_teEG.jpg",
name: "購物號",
time: "上午11:27",
type: "[新消息]",
content: "蘑菇街每日精選:秋冬好貨雙十一,全場49元封頂!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183756_0_i4am.jpg",
name: "夕陽",
time: "上午11:20",
type: "",
content: "知道"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183530_0_CyI7.jpg",
name: "小紅帽",
time: "上午11:19",
type: "",
content: "就這樣處理吧"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/300/20170929190950_0_THbY.jpg",
name: "京東商城",
time: "上午11:17",
type: "[新消息]",
content: "京東每日精選:好貨雙十一,全場99元封頂!"
}
]
},
{
name: "職場工作",
online: 123,
total: 239,
closed: true,
list: [{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180518_0_KcXn.jpg",
name: "萬丈-Infinite",
time: "上午11:47",
type: "",
content: "+1"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20171012184532_0_xTaB.jpg",
name: "切圖者聯(lián)盟",
time: "上午11:47",
type: "",
content: "dav:世界上最可怕的不是孤獨(dú)終老,而是跟那個(gè)使你孤獨(dú)的人終老。"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
name: "群助手",
time: "上午11:40",
type: "[6個(gè)群有新消息]",
content: ""
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
name: "VR實(shí)驗(yàn)室",
time: "上午11:38",
type: "",
content: "收到"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181200_0_czDQ.jpg",
name: "我的電腦",
time: "上午11:36",
type: "",
content: "[圖片]IMG_8724.PNG"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182858_0_5sfc.jpg",
name: "QQ看點(diǎn)",
time: "上午11:30",
type: "",
content: "健身最女王:除了彭于晏,也就他的身材讓女人著迷!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171022233804_0_0QNg.jpg",
name: "D3.js",
time: "上午11:30",
type: "",
content: "w 加入本群"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170919182002_0_teEG.jpg",
name: "購物號",
time: "上午11:27",
type: "[新消息]",
content: "蘑菇街每日精選:秋冬好貨雙十一,全場49元封頂!"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183756_0_i4am.jpg",
name: "夕陽",
time: "上午11:20",
type: "",
content: "知道"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/ori/20171012183530_0_CyI7.jpg",
name: "小紅帽",
time: "上午11:19",
type: "",
content: "就這樣處理吧"
},
{
image: "http://img.pingan.fusio.com.cn/materials/pic/300/20170929190950_0_THbY.jpg",
name: "京東商城",
time: "上午11:17",
type: "[新消息]",
content: "京東每日精選:好貨雙十一,全場99元封頂!"
}
]
}
];
commit('GET_CONTACTS_LIST', list);
},
}
};
export default common;
上面代碼,包含了messages頁面和contacts頁面的數(shù)據(jù)。
更新一下src/store/getters.js,代碼如下:
/**********************************************/
/* src/store/getters.js */
/**********************************************/
const getters = {
token: state => state.user.token,
email: state => state.user.email,
sidebar: state => state.app.sidebar,
scroll: state => state.app.scroll,
messages: state => state.common.messages,
contacts: state => state.common.contacts,
};
export default getters
現(xiàn)在我們可以去看一下messages頁面的效果,也試試“滾動加載更多”和“回到指定位置”功能。
Step22. 頂部菜單改造(slot的使用)
在消息列表頁面,我們引入header的時(shí)候,用了slot功能,這是一個(gè)非常有用的功能。詳情可以閱讀vue的API之slot使用。
我們通過分析幾個(gè)頁面的需求,需要用到幾個(gè)slot,所以header的代碼如下:
/**********************************************/
/* src/components/header/head.vue */
/**********************************************/
<template>
<header class='header'>
<span v-if="toggleBtn!=false" class="head_toggle" @click="toggleSideBar">
<img class="top_image" src="https://cn.bing.com/az/hprichbg/rb/GreatSaltLake_ZH-CN12553220159_1920x1080.jpg"/>
</span>
<slot name='head_text'>
<!-- <span class="head_text">LOGO</span> -->
</slot>
<slot name='head_subtext'>
<!-- <span class="head_subtext">iPhone在線 - WiFi</span> -->
</slot>
<slot name='head_btn'>
<!-- <span class="head_btn"><i class="iconfont icon-tianjia"></i></span> -->
</slot>
<slot name='head_back'>
<!-- <span class="head_back"><i class="iconfont icon-fanhui"></i>動態(tài)</span> -->
</slot>
<slot name='head_input'>
</slot>
</header>
</template>
<script>
export default {
props: ['toggleBtn', 'signinUp', 'headTitle', 'goBack'],
methods: {
toggleSideBar() {
this.$store.dispatch('ToggleSideBar');
},
},
}
</script>
<style lang="scss" scoped>
/*header*/
.header {
background-color: #3190e8;
background: -webkit-linear-gradient(right top, #61b8f8 , #5e8bf7); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient(bottom left, #61b8f8, #5e8bf7); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient(bottom left, #61b8f8, #5e8bf7); /* Firefox 3.6 - 15 */
background: linear-gradient(to bottom left, #61b8f8 , #5e8bf7); /* 標(biāo)準(zhǔn)的語法 */
position: fixed;
left: 0;
top: 0;
text-align: center;
width: 100%;
height: 1.95rem;
z-index: 10;
/*頭像菜單按鈕*/
.head_toggle {
position: absolute;
left:0.5rem;
img {
width: 1.5rem;
height: 1.5rem;
border-radius: 1rem;
margin-top: 0.2rem;
}
i {
line-height: 1.95rem;
font-size: 1rem;
}
}
/*文字*/
.head_text {
line-height: 1.95rem;
font-size: 0.7rem;
color: #fff;
display: inline-block;
&.subtext {
line-height: 1.5rem;
}
}
.head_subtext {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
text-align: center;
font-size: 0.3rem;
line-height: 1rem;
color: #fff;
}
/*右側(cè)按鈕*/
.head_btn {
position: absolute;
right:0.5rem;
line-height: 1.95rem;
font-size: 0.7rem;
color: #fff;
a {
color: #fff;
}
i {
line-height: 1.95rem;
font-size: 0.7rem;
color: #fff;
}
}
/*返回按鈕*/
.head_back {
position: absolute;
left:0.5rem;
line-height: 1.95rem;
font-size: 0.7rem;
color: #fff;
a {
color: #fff ;
}
i {
line-height: 1.95rem;
font-size: 0.7rem;
color: #fff;
}
}
.search {
padding: 0.5rem 2.4rem 0.5rem 0.5rem;
.search_content {
position: relative;
padding: 0 0.3rem;
text-align: left;
font-size: 0.5rem;
line-height: 2;
border-radius: 0.1rem;
color: #999;
background-color: #eee;
i {
position: absolute;
left: 0.3rem;
font-size: 0.5rem;
color: #999;
margin-right: 0.43em;
}
input {
background-color: transparent;
width: 100%;
padding-left: 0.8rem;
line-height: 1rem;
font-size: 0.5rem;
}
}
}
}
</style>
由于文章篇幅限制,請繼續(xù)閱讀Vue教程--Wap端項(xiàng)目搭建從0到1(詳解3)。
學(xué)習(xí)是一條漫漫長路,每天不求一大步,進(jìn)步一點(diǎn)點(diǎn)就是好的。