Vue教程--Wap端项目搭建从0到1(详解2)

本文基于工作项目开发,做的整理笔记
前段时间公司有的小伙伴刚开始学习vue,就直接着手用在新项目上,以项目实战步步为营,不断推进vue的学习和使用。时间短,需求多,又是刚刚上手,遇到的坑和困难也真不少,感觉每天都在疯狂地解决问题。说真的,每种技术的学习和使用,在实际项目的开发上得到了充分检验,个人能力也在快速的成长。
前一段时间,写过文章“Vue教程--使用官方脚手架构建实例”,主要是针对PC端,架构而写。当初的目的,也是想做为一个入门的教程,但是根据反馈和自己后面的感受,发现并不是很好,并没有做到真正的一步步上手。
今天决定专门针对Wap端去做这样一个demo,整体架构的搭建,并含有一些通用的功能。其中,部分知识点请回看前面那篇文章。对比来看,此篇应该更为详细,步步为营。

前提条件:
你已经了解vue的基础知识,尝试过使用vue-cli官方脚手架搭建项目。

编码环境:
system:OS X EI Capitan 10.12.5
npm:5.4.2
node:v8.8.0
vue-cli:@lastest

相关技术栈:
vue2 + vuex + vue-router + webpack + ES6/7 + fetch/axios + sass + flex + svg

相关地址:
项目代码github地址:https://github.com/YuxinChou/vue-wap-demo
项目在线地址:http://www.knowing365.com
(可用手机扫描下文中二维码,或用chrome浏览器模拟手机访问)
参考项目:https://github.com/bailicangdu/vue2-elm

WAP端项目搭建从0到1.jpg

目录
| - 0.传送门
| - 1.安装
| - 2.项目说明
| - 3.项目搭建
  | - Step1. 初始化
  | - Step2. 母版页Layout
  | - Step3. 配置rem
  | - Step4. 配置sass
  | - Step5. 顶部导航header
  | - Step6. 引入iconfont
  | - Step7. 侧边菜单sidebar
  | - Step8. 底部导航footer
  | - Step9. 返回顶部backToTop(组件)
  | - Step10. 仓库存储store
  | - Step11. 侧边菜单状态保存
  | - Step12. 搜索栏searchBar(组件)
  | - Step13. 页面添加
  | - Step14. 弹窗提示(组件)
  | - ---------------------------------- 下内容为详解2
  | - Step15. 完善login页面(fetch请求数据)
  | - Step16. 合理引入svg
  | - Step17. 用axios实现请求(取代原生fetch)
  | - Step18. 登录状态存入仓库
  | - Step19. 滚动加载更多(组件)
  | - Step20. 回到指定位置(组件)
  | - Step21. 完善消息列表页面
  | - Step22. 顶部菜单改造(slot的使用)
  | - --------------------------------- 下内容为详解3
  | - Step23. 完善其他页面
  | - Step24. 权限检查
  | - Step25. 页面切换动画transition
  | - Step26. 轮播展示(swiper)
  | - Step26. 分享功能(vue-social-share)
  | - Step28. ...
| - 4.项目部署
  | - a)本地部署
  | - b)服务器部署
| - 5.后续

Step15. 完善login页面(fetch请求数据)

更新一下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>&nbsp;
          <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 = '请输入手机号/邮箱/用户名';
          return
        }else if(!this.passWord){
          this.showAlert = true;
          this.alertText = '请输入密码';
          return
        }
        //用户名登录
        console.log(this.userAccount);
        console.log(this.passWord);

        // 哈咯,各位阅读文章的小伙伴,
        // 登录接口由于业务调整已经暂停,
        // 做到这里,可以注释,暂时当请求返回成功处理。
        // 晚些更新接口后,会再次更新文章内容。
        // 造成不便,十分抱歉。

        // 暂时注释
        // let response = await accountLogin(this.userAccount, this.passWord);
        // 开启这个模拟登录成功
        let response = { retCode: "10000", msg: "请求成功", data: {}  };


        //如果返回的值不正确,则弹出提示框,返回的值正确则返回上一页
        if (response.retCode!="10000") {
          this.showAlert = true;
          this.alertText = response;        
        }else{
          // 缓存用户数据(等下处理)
          // ...
          // 跳转到消息列表页
          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文件,它是一个接口服务文件。我们现在创建,在src文件夹下创建service文件夹,及getData.js文件,代码如下:

/**********************************************/
/* src/service/getData.js                     */
/**********************************************/

import fetch from '../utils/fetch';

/**
 * 账号密码登录
 */
export const accountLogin = (email, password) => fetch('/loginController/login', { email, password }, 'POST');

接着我们创建../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 = ''; //数据拼接字符串
        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文件中添加一个项目配置变量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'
})

那么这个请求我们就做好了,试一试请求是否成功。

注意:这是一个这个项目demo中唯一的真实数据请求接口,其他类似请求如果要使用真实接口可参考这里。

Step16. 合理引入svg

login页面的代码里,我们放置了一大段svg绘制代码,不是很好,如果其他页面要用,可能还拿不到。这里,我们更好的是将整站的svg抽离成一个组件。

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实现请求(取代原生fetch)

有人可能说原生fetch对浏览器的支持情况不太好,可否换一个呢?那么我也可以用axios去实现请求。

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);
            });
    });
}

当然,要使用这种方式的话,我们还要安装axios模块,执行:

$ npm install axios --save

修改src/service/getData.js文件,使用axios进行请求,代码如下:

/****************************************** */
/*  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. 登录状态存入仓库

修改一下login页面的代码,代码如下

/****************************************** */
/*  src/page/login/login.vue                */
/****************************************** */

          // 其他地方不改
          ...
          // 缓存用户数据
          let email = response.data.adminInfo.email;
          let token = response.data.tokenModel.token;
          this.$store.commit('SET_AUTH_INFO',[
            {
              email: email,
              token: token
            }
          ]);
         ...

需要在store的相关文件,添加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. 滚动加载更多(组件)

因为messages页面需要用到“滚动加载更多”这个功能,我们先看一下。

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方法,我们现在添加一下,代码如下:

/****************************************** */
/*  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 时需要获取小数 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方法为下面“回到指定位置”功能所需,我们先加着。

“滚动加载更多”的具体使用,稍后见“完善消息列表页面”部分内容。

Step20. 回到指定位置(组件)

我们先思考一下这个功能的核心问题,第1个就是页面离开前要保存滚动位置,第2个就是回到页面滚到指定位置。

针对第1个问题,我们可以监听滚动时间,实时保存这个值。这种较为容易实现。针对第2个问题就无需讨论。

Layout页面下,添加滚动监听,如下:

/**********************************************/
/* src/components/Layout.vue                  */
/**********************************************/

...
   // 仅插入这段代码即可
   mounted() {
      window.onscroll = () => {
        this.$store.dispatch('SetScroll');
      };
    }
...

修改src/store/modules/app.js,添加SetScroll相关代码,如下:

/**********************************************/
/* 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

当我们在某个页面需要实现这样的功能时,在该页面插入代码:

    import {scrollPosition} from '@/config/utils'
    ...
    mounted(){
      // 滚动到上一次位置
      scrollPosition(this.$store.getters.scroll);
    },
    ...

具体使用,可以看下面“完善消息列表页面”内容。

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>&nbsp;{{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:[], // 列表数据
        preventRepeatReuqest: false, //到达底部加载数据,防止重复加载
        showBackStatus: false, //显示返回顶部按钮
        showLoading: true, //显示加载动画
        touchend: false, //没有更多数据
      }
    },
    mixins: [loadMore],
    mounted(){
      this.initData();
      // 滚动到上一次位置
      scrollPosition(this.$store.getters.scroll);
    },
    methods: {
      handleHeadBtn() {
        console.log('####');
      },
      async initData(){
        // 判断之前是否请求过数据
        if (this.$store.getters.messages.length == 0) {
          // 请求数据
          this.$store.dispatch('GetMessagesList').then(() => {
            this.data = this.$store.getters.messages;
          }).catch(err => {
            console.log('error:'+err);
          });
          this.hideLoading();
        } else {
          // 直接取之前的数据
          this.data = this.$store.getters.messages;
          this.hideLoading();
        }
      },

      async loaderMore(){
        if (this.touchend) {
          return
        }
        //防止重复请求
        if (this.preventRepeatReuqest) {
          return 
        }
        this.showLoading = true;
        this.preventRepeatReuqest = true;

        // 触发加载更多
        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>

这个页面的列表,我们使用了的仓库存储,所以我们还要更新一下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: "切图者联盟",
                    time: "上午11:47",
                    type: "",
                    content: "dav:世界上最可怕的不是孤独终老,而是跟那个使你孤独的人终老。"
                },
                {
                    image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
                    name: "群助手",
                    time: "上午11:40",
                    type: "[6个群有新消息]",
                    content: ""
                },
                {
                    image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
                    name: "VR实验室",
                    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看点",
                    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);
        },
        // 获取联系人列表
        GetContactsList({ commit, state }) {
            let list = [{
                    name: "特别关心",
                    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: "切图者联盟",
                            time: "上午11:47",
                            type: "",
                            content: "dav:世界上最可怕的不是孤独终老,而是跟那个使你孤独的人终老。"
                        },
                        {
                            image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
                            name: "群助手",
                            time: "上午11:40",
                            type: "[6个群有新消息]",
                            content: ""
                        },
                        {
                            image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
                            name: "VR实验室",
                            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看点",
                            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: "切图者联盟",
                            time: "上午11:47",
                            type: "",
                            content: "dav:世界上最可怕的不是孤独终老,而是跟那个使你孤独的人终老。"
                        },
                        {
                            image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
                            name: "群助手",
                            time: "上午11:40",
                            type: "[6个群有新消息]",
                            content: ""
                        },
                        {
                            image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
                            name: "VR实验室",
                            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看点",
                            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: "切图者联盟",
                            time: "上午11:47",
                            type: "",
                            content: "dav:世界上最可怕的不是孤独终老,而是跟那个使你孤独的人终老。"
                        },
                        {
                            image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170928180152_0_ldsQ.jpg",
                            name: "群助手",
                            time: "上午11:40",
                            type: "[6个群有新消息]",
                            content: ""
                        },
                        {
                            image: "http://img.pingan.fusio.com.cn/materials/pic/700/20170920181437_0_YqPR.jpg",
                            name: "VR实验室",
                            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看点",
                            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页面的数据。

更新一下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

现在我们可以去看一下messages页面的效果,也试试“滚动加载更多”和“回到指定位置”功能。

Step22. 顶部菜单改造(slot的使用)

在消息列表页面,我们引入header的时候,用了slot功能,这是一个非常有用的功能。详情可以阅读vue的API之slot使用

我们通过分析几个页面的需求,需要用到几个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>动态</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); /* 标准的语法 */
    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;
    }
    /*右侧按钮*/
    .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>

由于文章篇幅限制,请继续阅读Vue教程--Wap端项目搭建从0到1(详解3)


学习是一条漫漫长路,每天不求一大步,进步一点点就是好的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335