《叩丁狼积分商城》PC端项目实战

项目介绍

《叩丁狼积分商城》是一个使用vue全家桶构建的PC端SPA商城,主要用于给学员将子级在叩丁狼的积分兑换成商品。

一、项目参考地址【熟悉】

真实项目参考地址:http://sc.wolfcode.cn/

实战项目参考地址:http://codesohigh.com/store-pc/

二、项目UI设计稿及图片资源【熟悉】

1、UI设计稿

链接:https://pan.baidu.com/s/11jI0eKuFNwxCzhYhEAHEGA 提取码:yyds

2、静态图片资源

链接:https://pan.baidu.com/s/1rBqQsxvuB2SpZOCfMs_kmA 提取码:9yos

3、iconfont链接

1、在项目中全局的css引入以下链接:

https://at.alicdn.com/t/font_2730880_ylrio3ahhx.css

2、具体图标名称:

图标名称图标类名

YDUI-复选框(选中)

icon-yduifuxuankuangxuanzhong

YDUI-复选框

icon-yduifuxuankuang

loading

icon-loading

toast-失败_画板 1

icon-toast-shibai_huaban

toast-警告

icon-toast-jinggao

toast _成功

icon-toast_chenggong

3、引用方式

<i class="iconfont icon-loading"></i>

三、PS安装【选装】

这里为电脑没有预装ps的同学提供ps安装包

链接:https://pan.baidu.com/s/1GX3PaRWNFMUY6wqF8NukQg 提取码:hle6

下载打开压缩包, 解压密码为:123456

四、蓝湖【重点】

蓝湖产品设计协作平台是一个服务于产品经理、设计师、工程师的在线协作平台,无缝连接产品、设计、研发流程,旨在降低沟通成本,缩短开发周期,提高工作效率,帮助企业建立科学的工作环境,提升企业的开发效率。

1、注册登录与插件

打开https://lanhuapp.com/,注册并登录,然后创建团队:

点击https://lanhuapp.com/ps?comeFrom=项目列表_右上下载蓝湖ps插件。

下载完成即可安装,安装后重启ps,如果看到:

就代表你已成功安装蓝湖插件。此时,顺便登录账号。

2、上传UI设计稿

首先,打开一张psd设计图,然后选择刚刚创建的团队与项目,选择 1倍像素 的web端设计稿:

点击 上传全部画板 即可。

后续每开发一个页面,可自行按照上述流程将需要用到的设计稿上传,无需一次性上传所有设计图。

五、项目创建【熟悉】

执行 vue create 项目名称 :

# 这里 store-pc 是我的项目名
vue create store-pc

按照没有eslint的配置,选择 vue 2lessvuexrouter

创建完成后,cd到项目中。

六、仓库创建【熟悉】

https://gitee.com/ 创建一个空白仓库:

点击 创建 即可。

由于我们已经有本地项目,直接在本地项目的命令行中执行:

git add .
git commit -m '项目创建'

# origin后面要改成你的仓库地址
git remote add origin git@gitee.com:codesohigh/store-pc.git

git push -u origin master

完成提交。刷新仓库页面,如果看到仓库已有文件:

表示你已提交成功。

七、默认样式【熟悉】

1、清空默认样式

清空默认样式我们使用 reset-css 。具体使用方法:

npm install reset-css

# 或者:
yarn add reset-css

然后在项目入口文件 main.js 中引入:

import 'reset-css';

2、定义默认样式

网页中有很多固定的颜色,我们可以抽离出来作为less公共变量。在 src 下,新建 total.less

// 公共样式变量
@blue: #0A328E;
@orange: #FF5E0F;
@black: #333333;

// 公有版心
.banxin{
    width: 1200px;
    margin-left: auto;
    margin-right: auto;
}

然后在需要使用这些变量的组件中的css最顶部引入:

@import "../total.less";

八、配置@指向src【了解】

1、方案一

  • 安装 Path Intellisense插件

  • 打开设置 - 首选项 - 搜索 Path Intellisense - 打开 settings.json ,添加:

"path-intellisense.mappings": {
     "@": "${workspaceRoot}/src"
 }
  • 在项目 package.json 所在同级目录下创建文件 jsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "allowSyntheticDefaultImports": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
    },
    "exclude": [
        "node_modules"
    ]
}
  • 重启vscode

2、方案二

安装 path

yarn add path
# 或者npm
npm i path -S

创建 vue.config.js

const path = require("path");
function resolve(dir) {
  return path.join(__dirname, dir);
}
 
module.exports = {
  chainWebpack: config => {
    config.resolve.alias
      .set("@", resolve("src"))
      .set("assets", resolve("src/assets"))
      .set("components", resolve("src/components"))
      .set("base", resolve("baseConfig"))
      .set("public", resolve("public"));
  },
}

九、项目静态资源【了解】

将老师提供的 assets 文件夹放到项目的 src 下。

十、登录滑动拼图验证【了解】

插件参考:https://gitee.com/monoplasty/vue-monoplasty-slide-verify

1、安装插件

npm install --save vue-monoplasty-slide-verify
# yarn安装方式
yarn add vue-monoplasty-slide-verify

2、入口文件引入

import SlideVerify from 'vue-monoplasty-slide-verify' // 拼图验证码

Vue.use(SlideVerify)

3、组件引用

<template>
	<slide-verify :l="42" :r="20" :w="362" :h="140" @success="onSuccess" @fail="onFail" @refresh="onRefresh" :style="{ width: '100%' }" class="slide-box" ref="slideBlock" :slider-text="msg"></slide-verify>
</template>

<script>
export default {
  data() {
    return {
      msg: "向右滑动"
    };
  },
  methods: {
    // 拼图成功
    onSuccess(times) {
      let ms = (times / 1000).toFixed(1);
      this.msg = "login success, 耗时 " + ms + "s";
    },
    // 拼图失败
    onFail() {
      this.onRefresh(); // 重新刷新拼图
    },
    // 拼图刷新
    onRefresh() {
      this.msg = "再试一次";
    },
    // 点击登录按钮
    submitFn(formName) {
      if (this.msg == "再试一次" || this.msg == "向右滑动") {
        console.log("请滑动拼图");
      } else {
        console.log("开始登录");
      }
    },
  },
};
</script>

<style lang="less" scoped>
/deep/.slide-box {
    width: 100%;
    position: relative;
    box-sizing: border-box;
    canvas {
        position: absolute;
        left: 0;
        top: -140px;
        display: none;
        width: 100%;
        box-sizing: border-box;
    }
    .slide-verify-block{
        width: 85px;
        height: 136px;
    }
    .slide-verify-refresh-icon {
        top: -140px;
        display: none;
    }
    &:hover {
        canvas {
            display: block;
        }
        .slide-verify-refresh-icon {
            display: block;
        }
    }
}
</style>

十一、配置axios拦截器【重点】

本项目接口文档地址:积分商城PC【叩丁严选PC项目】 http://www.docway.net/project/1h9xcTeAZzV/share/1hZ5qEe9NVg 阅读密码:zhaowenxian

1、安装 axiosqs

yarn add axios qs

2、在 src 下创建 request>request.js+api.js

// request.js
import axios from "axios";

let instance = axios.create({
    baseURL: "http://192.168.113.249:8081/cms",
    timeout: 5000
});

// http request 拦截器
instance.interceptors.request.use(
  (config) => {
    if (config.url === "/wechatUsers/PCLogin") {
      config.headers["Content-Type"] = "application/x-www-form-urlencoded";
    }
    const token = sessionStorage.getItem("token");
    if (token) {
      // 判断是否存在token,如果存在的话,则每个http header都加上token
      config.headers["x-auth-token"] = token; //请求头加上token
    }
    return config;
  },
  (err) => {
    return Promise.reject(err);
  }
);

// http response 拦截器
instance.interceptors.response.use(
  (response) => {
    return response.data;
  },
  //接口错误状态处理,也就是说无响应时的处理
  (error) => {
    return Promise.reject(error.response.status); // 返回接口返回的错误信息
  }
);

export default instance

api.js

import request from './request'
import qs from 'qs'

// 首页精品推荐数据请求
export const JingpinApi = () => request.get('/products/recommend')

// 微信登录(这个接口必须用qs对数据进行格式化)
export const WeixinLoginApi = (params) => request.post(`/wechatUsers/PCLogin`, qs.stringify(params))

十二、Vuex与提示【重点】

vuex中可以定义三个状态:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // 提示的内容
    toastMsg: "",
    // 提示的状态
    toastStatus: false,
    // 提示的类型(success,danger,info)
    toastType: "success"
  },
  mutations: {
    // 打开提示
    openToast(state, payload){
      state.toastStatus = true;
      state.toastMsg = payload.msg;
      state.toastType = payload.type;
    },
    // 关闭提示
    closeToast(state){
      state.toastStatus = false;
    }
  },
  actions: {
    // 2秒后关闭提示
    closeToastFn(context){
      setTimeout(()=>{
        context.commit('closeToast');
      }, 2000)
    }
  }
})

创建一个 Toast.vue 组件:

<template>
  <transition name="slide">
    <div class="toast" v-if="$store.state.toastStatus">
      <i
        :class=" $store.state.toastType === 'success' ? 'iconfont icon-toast_chenggong' : $store.state.toastType === 'danger' ? 'iconfont icon-toast-shibai_huaban' : 'iconfont icon-toast-jinggao'"
        :style="{ color: $store.state.toastType === 'success' ? 'green' : $store.state.toastType === 'danger' ? 'red' : 'orange'}"
      ></i>
      <span>{{ $store.state.toastMsg }}</span>
    </div>
  </transition>
</template>
<script>
export default {
  data() {
    return {};
  },
};
</script>
<style lang="less" scoped>
@import "https://at.alicdn.com/t/font_2730880_lc6xeulwi7o.css";
.toast {
  position: absolute;
  left: 50%;
  z-index: 10;
  transform: translateX(-50%);
  padding: 10px 20px;
  max-width: 400px;
  display: flex;
  background: #fff;
  box-shadow: 0 0 10px #000;
  border-radius: 10px;
  font-size: 18px;
  box-sizing: border-box;
  .iconfont {
    margin-right: 10px;
    font-size: 18px;
  }
  span {
    flex: 1;
    line-height: 1.2;
  }
}

.slide-enter, .slide-leave-to{
  top: -500px;
}

.slide-enter-active, .slide-leave-active{
  transition: top 1s linear;
}

.slide-enter-to, .slide-leave{
  top: 10px;
}
</style>

在任何一个组件想要触发这个Toast,需要:

this.$store.commit("openToast", { msg: "你好世界", type: "success" });
setTimeout(() => {
    this.$store.dispatch("closeToastFn");
}, 1500)

但是每次都这么触发太麻烦,我们用一个 toastFn.js 文件封装一个函数:

// 打开与关闭toast
export function toastFn(_this, msg, type) {
    _this.$store.commit("openToast", { msg, type });
    setTimeout(() => {
        _this.$store.dispatch("closeToastFn");
    }, 1500)
}

如此,在需要调用的地方直接传参即可:

toastFn(this, "你好世界", "success");

十三、PC微信登录【重点】

1、微信扫码布局与配置

想要实现在登录框中可以扫码登录:

public/index.htmlhead 中:

<script src="https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>

在登录框中代码中添加一个div,这个div是用来存放微信二维码所在的iframe的:

<div id="weixin"></div>

api.js 中:

// 微信登录(这个接口必须用qs对数据进行格式化)
export const WeixinLoginApi = (params) => request.post(`/wechatUsers/PCLogin`, qs.stringify(params));

在切换到 微信扫码登录 的事件中:

// 点击了微信扫码登录
weixinClickFn() {
    ...
    let _this = this;
    new WxLogin({
        id: "weixin",
        appid: "wx67cfaf9e3ad31a0d",  // 这个appid要填死
        scope: "snsapi_login",
        // 扫码成功后重定向的接口
        redirect_uri: "https://sc.wolfcode.cn/cms/wechatUsers/shop/PC",
        // state填写编码后的url
        state: encodeURIComponent(window.btoa(process.env.VUE_APP_STATE_URL + _this.$route.path)),
        // 调用样式文件
        href: "",
    });
},

* 环境变量

上面的process.env.VUE_APP_STATE_URL是环境变量。书写方式:

在项目根目录新建 .env.prod.env.dev

# .env.dev
NODE_ENV=development
VUE_APP_BASE_URL=/
VUE_APP_STATE_URL=http://127.0.0.1:8080

# .env.prod
NODE_ENV = production
VUE_APP_BASE_URL = 'http://codesohigh.com/store-pc/#'
VUE_APP_STATE_URL = 'http://codesohigh.com/store-pc/#'

然后修改 package.json 中:

{
    "scripts": {
        "serve": "vue-cli-service serve --mode dev",
        "build": "vue-cli-service build --mode prod"
    },
}

重跑项目即可。

当你还没写href的时候,会发现iframe样式是无法改变的,因此我们需要借助 node+css 来实现css转base64:

src 下新建 utils 文件夹,并且在其中新建: data-url.jswxlogin.css

/* wxlogin.css */
.impowerBox .title, .impowerBox .info{
    display: none;
}

.impowerBox .qrcode{
    margin-top: 20px;
}
// data-url.js
var fs = require('fs');

// function to encode file data to base64 encoded string
function base64_encode(file) {
    // read binary data
    var bitmap = fs.readFileSync(file);
    // convert binary data to base64 encoded string
    return 'data:text/css;base64,'+new Buffer(bitmap).toString('base64');
}

console.log(base64_encode('./wxlogin.css'))

然后控制台运行:

cd src/utils/
node data-url.js

得到一段base64转码字符串:

data:text/css;base64,LmltcG93ZXJCb3ggLnRpdGxlLCAuaW1wb3dlckJveCAuaW5mb3sNCiAgICBkaXNwbGF5OiBub25lOw0KfQ0KDQouaW1wb3dlckJveCAucXJjb2Rlew0KICAgIG1hcmdpbi10b3A6IDIwcHg7DQp9DQoNCg==

然后粘贴到 href 中。最终效果:

2、扫码得到code做登录

扫码跳转页面后,我们来到 Header.vue 组件。在 Header.vue 组件的 created 生命周期里,由于在地址栏上可以得到code:

this.$route.query.code

此时要做判断,如果有code,则做请求获取token:

created() {
    let _this = this;
    if (this.$route.query.code) {
      // 存在code,说明扫码登录过,直接做请求
      WeixinLoginApi({
        code: this.$route.query.code,
      })
        .then((res) => {
          if (res.code === 0) {
            toastFn(_this, res.message, "success");
            localStorage.setItem("x-auth-token", res["x-auth-token"]); // 存储token
          } else {
            toastFn(_this, res.message, "danger");
          }
          this.$router.push(this.$route.path); // 清除参数code
          setTimeout(() => {
            this.$router.go(0); // 刷新当前页
          }, 2000);
        })
        .catch((err) => {
          console.log(err);
        });
    } else {
      // 没有code,说明可能未登录,也可能登录过已存好token,所以此时要判断token是否存在
      ...
    }
  },

但这样写有个问题,Header组件只会加载一次,如果用户退出登录,在别的页面登录,就无法正常登录了。

3、强制组件更新

由于具有以上描述的情况,我们希望只要路由的 query参数 发生变化,就强制更新一次 Header.vue 组件,这样就能重复触发它的 created,方法如下:

<!-- App.vue中 -->
<Header :key="componentKey" />

<script>
    export default {
        data() {
            return {
                componentKey: 0
            };
        },

        watch: {
            "$route.query": {
                handler(newVal, oldVal) {
                    let _this = this;
                    if (_this.$route.query.code) {
                        _this.componentKey++;
                    }
                },
                deep: true,
            },
        },
    }
</script>

十三、获取用户信息【重点】

Header.vuecreated 中:

created() {
    let _this = this;
    if (this.$route.query.code) {
      ...
    } else {
      // 没有code,说明可能未登录,也可能登录过已存好token,所以此时要判断token是否存在
      let token = localStorage.getItem("x-auth-token");
      if (token) {
        UserInfoApi().then((res) => {
          console.log(res);	// 获得用户信息
        });
      }
    }
  },

十四、手机号登录【重点】

手机号登录之前,需要判断三点:

  1. 手机号是否正确

  2. 验证码是否填写

  3. 滑块拼图是否拼接过

1、手机号校验

新建一个 validate.js 文件:

// 正则校验手机号
export function validateTelephone(value) {
    let reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/;
    return reg.test(value.trim());
}

在提交的事件中直接判断:

// 点击登录按钮
submitFn() {
    let result = validateTelephone(this.phoneNum);
    if (!result) {
        // 手机号错误
        toastFn(this, "请填写正确手机号", "danger");
        this.phoneRight = false;	// 控制手机输入框是否有红色边框提示,false表示有提示
        return;
    }
},

2、验证码校验

验证码无法校验填写对了没有,只能判断是否有填写:

// 点击登录按钮
submitFn() {
    let result = validateTelephone(this.phoneNum);
    // ...校验手机号的代码(省略)
    if (this.code.trim() === "") {
        // 验证码有误
        toastFn(this, "验证码有误", "danger");
        this.codeRight = false;	// 控制验证码输入框是否有红色边框提示,false表示有提示
        return;
    }
},

3、检验是否完成拼图

submitFn() {
    // ...手机与验证码校验代码(省略)
    this.phoneRight = true; // 重新复原手机号输入框状态
    this.codeRight = true; // 重新复原验证码输入框状态
    if (this.msg == "再试一次" || this.msg == "向右滑动") {
        // 拼图未完成
        toastFn(this, "请完成拼图", "info");
    } else {
        // 拼图完成
        console.log("开始登录");
    }
},

4、登录请求

完成了以上的所有校验,就可以点击登录,先配置 api.js

// 手机号登录
export const PhoneReginApi = (params) => request.post('/phoneRegin', qs.stringify(params))

在判断拼图完成之后:

PhoneReginApi({
    phone: _this.phoneNum,
    verifyCode: _this.code,
})
    .then((res) => {
    if (res.code === 0) {
        localStorage.setItem("x-auth-token", res["x-auth-token"]); // 存储token
        toastFn(_this, res.message, "success");
    } else {
        toastFn(_this, res.message, "danger");
    }
    setTimeout(()=>{
        this.$router.go(0); // 刷新当前页
    }, 2000)
})
    .catch((err) => {
    console.log(err);
});

十五、商品页滚动加载【熟悉】

请参考:https://www.npmjs.com/package/load-more-mczhao

十六、用户中心与购物车界面套用

个人中心的界面:

1、用户中心

新建 views/person/Person.vue

<template>
  <div class="person_page">
    <div class="person banxin">
      <bread-crumb title="个人中心">首页</bread-crumb>
      <main>
        <aside>
          <div
            class="avatar"
            :style="{ backgroundImage: `url(${userInfo.headImg})` }"
          ></div>
          <div class="name">{{ userInfo.nickName }} <span @click="loginOutFn">[退出]</span></div>
          <div class="title">
            <img
              src="../../assets/images/person/transaction.png"
              width="20"
              alt="交易管理"
            />
            交易管理
          </div>
          <ul class="list">
            <li :class="/\/person1/g.test($route.path) ? 'active' : ''">
              个人中心
            </li>
            <li :class="/\/person1/g.test($route.path) ? 'active' : ''">
              我的订单
            </li>
            <li :class="/\/cart/g.test($route.path) ? 'active' : ''">购物车</li>
            <li :class="/\/person1/g.test($route.path) ? 'active' : ''">
              消息通知
            </li>
            <li :class="/\/person1/g.test($route.path) ? 'active' : ''">
              积分明细
            </li>
            <li :class="/\/person1/g.test($route.path) ? 'active' : ''">
              积分攻略
            </li>
          </ul>
          <div class="title">
            <img
              src="../../assets/images/person/transaction.png"
              width="20"
              alt="交易管理"
            />
            个人信息管理
          </div>
          <ul class="list">
            <li>地址管理</li>
            <li>账号安全</li>
          </ul>
        </aside>
        <article><router-view></router-view></article>
      </main>
    </div>
  </div>
</template>
<script>
import Breadcrumb from "@/components/products/Breadcrumb.vue";
import {toastFn} from '@/utils/toastFn'
export default {
  data() {
    return {
      userInfo: JSON.parse(sessionStorage.getItem("userInfo")),
    };
  },
  components: {
    "bread-crumb": Breadcrumb,
  },
  methods: {
    loginOutFn(){
      localStorage.removeItem("x-auth-token");
      sessionStorage.clear();
      toastFn(this, "您已退出登录,即将返回首页", "success");
      setTimeout(()=>{
        this.$router.push('/home');
      }, 2000)
    }
  }
};
</script>
<style lang="less" scoped>
@import "../../total.less";
.person_page {
  background: #fff;
  main {
    border-top: 1px solid #e1e1e1;
    padding: 28px 0 48px;
    display: flex;
    justify-content: space-between;
    background: #fff;
    aside {
      width: 200px;
      height: 740px;
      background: #e7e7e7;
      margin-right: 62px;
      box-sizing: border-box;
      padding: 30px 18px 0;
      .avatar {
        width: 100px;
        height: 100px;
        margin: auto;
        background-size: 100% 100%;
        background-repeat: no-repeat;
      }
      .name {
        text-align: center;
        margin-top: 19px;
        margin-bottom: 43px;
        span {
          text-decoration: underline;
          color: #2a5df1;
        }
      }
      .title {
        font-size: 16px;
        color: #333333;
        display: flex;
        align-items: center;
        margin-bottom: 14px;
        img {
          margin-right: 6px;
        }
      }
      .list {
        li {
          margin-bottom: 17px;
          font-weight: 300;
          color: #666666;
          cursor: pointer;
          &.active {
            color: @blue;
            font-weight: bold;
            &::before {
              width: 2px;
              height: 14px;
              background: @blue;
              display: inline-block;
              content: "";
              margin-right: 10px;
            }
          }
        }
      }
    }
    article {
      flex: 1;
      padding: 20px 0 0 20px;
      box-sizing: border-box;
      background: #fff;
    }
  }
}
</style>

2、购物车

新建 views/person/Cart.vue

<template>
  <div class="cart_page">
    <table>
      <thead>
        <tr>
          <th style="width: 8%">
            <i
              :class="
                totalSelect
                  ? 'iconfont icon-yduifuxuankuangxuanzhong'
                  : 'iconfont icon-yduifuxuankuang'
              "
            ></i>
          </th>
          <th style="width: 30%">礼品信息</th>
          <th>兑换分数</th>
          <th>数量</th>
          <th>小计 (鸡腿)</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>
            <i
              :class="
                oneSelect
                  ? 'iconfont icon-yduifuxuankuangxuanzhong'
                  : 'iconfont icon-yduifuxuankuang'
              "
            ></i>
          </td>
          <td>
            <section>
              <img
                width="84"
                src="http://sc.wolfcode.cn/upload/images/product_images/20200615/41ddc8c8-bd4b-4f5c-ae68-474f9ed18eb7.png"
                alt="列表图片"
              />
              <div class="info">
                <h5>叩丁狼定制T恤</h5>
                <p>颜色、版本:XL</p>
              </div>
            </section>
          </td>
          <td>5000鸡腿</td>
          <td>
            <div class="step">
              <span>-</span>
              <input type="text" disabled v-model="stepNum" />
              <span>+</span>
            </div>
          </td>
          <td>5000鸡腿</td>
          <td>
            <span class="del">删除</span>
          </td>
        </tr>
      </tbody>
    </table>
    <div class="total">总计:<span>0鸡腿</span></div>
    <div class="submit">提交</div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      stepNum: 1,
      // 全选
      totalSelect: false,
      // 单选
      oneSelect: true,
    };
  },
};
</script>
<style lang="less" scoped>
.cart_page {
  background: #fff;
  table {
    width: 100%;
    border: 1px solid #e6e6e6;
    box-sizing: border-box;
    color: #666;
    border-collapse: collapse;
    font-size: 14px;
    thead {
      background: #f2f2f2;
      th {
        padding: 19px 0;
        .iconfont {
          cursor: pointer;
        }
        .icon-yduifuxuankuangxuanzhong {
          color: #0a328e;
        }
      }
    }
    tbody {
      tr {
        td {
          vertical-align: middle;
          text-align: center;
          padding: 19px 0;
          table-layout: fixed; // td的宽度固定,不随内容变化
          .iconfont {
            cursor: pointer;
          }
          .icon-yduifuxuankuangxuanzhong {
            color: #0a328e;
          }
          section {
            padding-left: 20px;
            display: flex;
            box-sizing: border-box;
            img {
              margin-right: 12px;
            }
            .info {
              padding-top: 20px;
              flex: 1;
              overflow: hidden;
              box-sizing: border-box;
              text-align: left;
              h5 {
                overflow: hidden;
                color: #333;
                font-size: 18px;
                white-space: nowrap;
                text-overflow: ellipsis;
                margin-bottom: 20px;
              }
              p {
                color: #666;
                overflow: hidden;
                white-space: nowrap;
                text-overflow: ellipsis;
              }
            }
          }
          .step {
            width: 106px;
            height: 32px;
            margin: auto;
            span {
              float: left;
              width: 30px;
              height: 32px;
              display: block;
              border: solid 1px #d1d1d1;
              font-size: 20px;
              box-sizing: border-box;
              font-weight: normal;
              font-stretch: normal;
              line-height: 30px;
              letter-spacing: 0px;
              color: #999999;
              text-align: center;
              cursor: pointer;
              background: #fff;
            }
            input {
              box-sizing: border-box;
              width: 46px;
              height: 32px;
              float: left;
              text-align: center;
              font-size: 14px;
              line-height: 23px;
              letter-spacing: 0px;
              color: #666666;
              border: 0;
              border-top: 1px solid #d1d1d1;
              border-bottom: 1px solid #d1d1d1;
              background: #fff;
            }
          }
          .del {
            border: 1px solid #ececec;
            border-radius: 4px;
            padding: 5px 10px;
            cursor: pointer;
            &:hover {
              color: #fff;
              background: #0a328e;
            }
          }
        }
      }
    }
  }
  .total {
    padding: 30px 0;
    text-align: right;
    font-size: 22px;
    span {
      font-weight: bold;
      color: #fd604d;
    }
  }
  .submit {
    width: 175px;
    height: 40px;
    text-align: center;
    line-height: 40px;
    font-family: SourceHanSansSC-Light;
    font-size: 18px;
    font-weight: normal;
    font-stretch: normal;
    letter-spacing: 0px;
    color: #ffffff;
    cursor: pointer;
    background-color: #0a328e;
    float: right;
  }
}
</style>

3、路由配置

{
    path: "/person",
    name: "Person",
    component: () => import(/* webpackChunkName: "person" */ "../views/person/Person.vue"),
    children: [
      {
        path: "cart",
        name: "Cart",
        component: () => import(/* webpackChunkName: "cart" */ "../views/person/Cart.vue"),
      },
    ],
},

4、导航修改

找到 Nav.vue 组件,修改个人中心的当前项判断:

<div class="link">
    <router-link :class="$route.path=='/home' ? 'active' : ''" to="/home">首页</router-link>
    <router-link :class="$route.path=='/products' ? 'active' : ''" to="/products">全部商品</router-link>
    <router-link :class="/\/person/g.test($route.path) ? 'active' : ''" to="/user">个人中心</router-link>
    ...
</div>

十七、开发小技巧

1、iconfont无法旋转?

iconfont是行内元素,无法添加 transform:rotate() 属性,需要把它转行内块才能旋转。

2、社交平台分享

当我们需要做到社交平台分享时,通常会用 iShare.jsvue-socialmedia-sharevshare。第一个插件对样式的重构性较低,第二个插件一般用于分享到外网,因此比较常用的是 vshare

使用方式:

npm install vshare -S

在组件中引入:

// ES6
import vshare from 'vshare'
//or require
var vshare = require('vshare')

components: {
    vshare
}

然后使用:

<vshare :vshareConfig="vshareConfig"></vshare>

data中定义 vshareConfig

data () {
    return {
        // 分享功能的配置
        vshareConfig: {
            // 此处放分享列表(ID)
            shareList: ["weixin", "qzone"],
            //此处放置分享按钮设置
            share: [{ bdSize: 24 }],
            //此处放置浮窗分享设置
            slide: false
        },
    }
}

3、路由监听

当你路由更新,当页面没刷新时,需要监听路由:

watch: {
    "$route.query.id": {
        handler(newVal, oldVal){
            if(newVal !== oldVal){
                this.$router.go(0);   // 刷新页面
            }
        }
    }
},

4、正则表达式替换文本

假设你得到的字符串为str,其中包含html标签,标签中有img,想把img里的 upload 字符串替换为 https://sc.wolfcode.cn/upload ,可以如下操作:

let newStr = str.replace(/upload/g, 'http://sc.wolfcode.cn/upload');

5、个人中心最基本界面

<template>
  <div class="person_page banxin">
    <bread-crumb title="个人中心">首页</bread-crumb>
    <main>
      <aside>
        <div class="avatar" :style="{ backgroundImage: `url(${userInfo.headImg})` }"></div>
        <div class="name">{{ userInfo.nickName }} <span>[退出]</span></div>
        <div class="title">
          <img src="../../assets/images/person/transaction.png" width="20" alt="交易管理" />
          交易管理
        </div>
        <ul class="list">
          <li :class="/\/person1/g.test($route.path) ? 'active' : ''">个人中心</li>
          <li :class="/\/person1/g.test($route.path) ? 'active' : ''">我的订单</li>
          <li :class="/\/cart/g.test($route.path) ? 'active' : ''">购物车</li>
          <li :class="/\/person1/g.test($route.path) ? 'active' : ''">消息通知</li>
          <li :class="/\/person1/g.test($route.path) ? 'active' : ''">积分明细</li>
          <li :class="/\/person1/g.test($route.path) ? 'active' : ''">积分攻略</li>
        </ul>
        <div class="title">
          <img src="../../assets/images/person/transaction.png" width="20" alt="交易管理" />
          个人信息管理
        </div>
        <ul class="list">
          <li>地址管理</li>
          <li>账号安全</li>
        </ul>
      </aside>
      <article><router-view></router-view></article>
    </main>
  </div>
</template>
<script>
import Breadcrumb from "@/components/products/Breadcrumb.vue";
export default {
  data() {
    return {
      userInfo: JSON.parse(sessionStorage.getItem("userInfo")),
    };
  },
  components: {
    "bread-crumb": Breadcrumb,
  },
};
</script>
<style lang="less" scoped>
@import "../../total.less";

main {
  border-top: 1px solid #e1e1e1;
  padding: 28px 0 48px;
  display: flex;
  justify-content: space-between;
  aside {
    width: 200px;
    height: 740px;
    background: #e7e7e7;
    margin-right: 62px;
    box-sizing: border-box;
    padding: 30px 18px 0;
    .avatar {
      width: 100px;
      height: 100px;
      margin: auto;
      background-size: 100% 100%;
      background-repeat: no-repeat;
    }
    .name {
      text-align: center;
      margin-top: 19px;
      margin-bottom: 43px;
      span {
        text-decoration: underline;
        color: #2a5df1;
      }
    }
    .title {
      font-size: 16px;
      color: #333333;
      display: flex;
      align-items: center;
      margin-bottom: 14px;
      img {
        margin-right: 6px;
      }
    }
    .list {
      li {
        margin-bottom: 17px;
        font-weight: 300;
        color: #666666;
        &.active {
          color: @blue;
          font-weight: bold;
          &::before {
            width: 2px;
            height: 14px;
            background: @blue;
            display: inline-block;
            content: "";
            margin-right: 10px;
          }
        }
      }
    }
  }
  article {
    flex: 1;
  }
}
</style>

6、重复点击相同路由报错

当我们重复点击相同的路由,就会有以下报错:

这是路由升级导致的vue版本过低报错。解决方案:

在路由文件中加这一段代码:

const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}

十八、项目打包

1、将路由中的 mode: history 注释

2、在 vue.config.js 中:

module.exports = {
  publicPath: "./"
}

3、如果使用了懒加载,记得将img_loading.gif图片放到相应的位置

十九、图片懒加载

1、安装

yarn add vue-lazyload

# 或者
npm i vue-lazyload -S

2、全局引入与配置

import VueLazyload from 'vue-lazyload'

// 配置项
Vue.use(VueLazyload, {
    preLoad: 1.3,
    // error: 'dist/error.png',
    // 这里注意,不能写相对路径,因此打包上线也需要修改这个地址
    loading: 'http://codesohigh.com/img_loading.gif', 
    attempt: 1
})

3、设定loading大小

App.vue 中:

img[lazy="loading"] {
  display: block;
  width: 30% !important;
  height: 30% !important;
  margin: 0 auto;
}

4、:src --> v-lazy

<img v-lazy="item...">

二十、设置title与favicon.ico

当我们完成项目后,想要在webpack修改 index.html 的title标签,可以在 vue.config.js 中:

module.exports = {
  chainWebpack: (config) => {
    config.plugin("html").tap((args) => {
      args[0].title = "叩丁狼积分商城";
      return args;
    });
  },
  publicPath: "./",
};

如果想改favicon.ico,可以在网上扒一个你想要的,然后替换 public 下的favicon.ico即可。

二十一、作业

【Level-1】完成可达到做网页设计师的水平

day01-day02:仓库新建一个dev分支,首页、全部商品页、个人中心页搭建

【Level-2】完成可达到项目逻辑度最高的水平

day03-day04:独立完成微信扫码登录及手机号登录

【Level-3】完成可达到中级前端工程师的水平

day05-day06:独立完成购物车全选与步进器功能、商品详情页社交平台分享功能

【Level-4】完成可达到高级前端工程师的水平

在day07之前完成项目,项目完成过程中请教同学或老师的次数小于3次,并能在老师讲授每个模块前已自主完成该模块,那么,你就是前端大佬了!

二十二、项目总结

1、项目介绍

《叩丁严选》是一个由vue-cli搭建的PC端SPA商城,该商城主要涉及登录注册、商品列表、商品详情、个人中心、购物车及商品检索等主体功能。该项目主要用于平台用户参与积分兑换商品,是一个中大型的PC端商城项目。

2、项目技术点

  1. 使用vue-cli搭建项目,并结合蓝湖+PS进行页面切图,实现对设计稿的高保真还原;

  2. 使用axios进行数据请求,并对其进行request拦截封装;

  3. 登录采用手机+验证码、手机+密码及微信扫码登录,其中微信扫码登录结合环境变量,调用后端接口实现平台切换验证;

  4. 使用localStorage对用户信息和token进行存储;

  5. 使用vshare实现将项目分享到第三方社交平台(如:微信、QQ空间等);

  6. 使用原生JS在组件mounted中监听滚动,并实现向下滚动加载更多;

  7. 使用导航守卫对每个进入个人中心页的路由进行拦截,正则匹配路径后保证有token方能进入该路由;

  8. 使用路由降级等方式解决进入重复路由报错的问题;

  9. 使用路由监听解决路由跳转而页面不跳转的问题;

  10. 给组件绑定key属性,通过修改key值来强制更新组件;

最后更新于