> For the complete documentation index, see [llms.txt](https://gb.akanote.cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gb.akanote.cn/vue/project.md).

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

## 项目介绍

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

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

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

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

[![image-20210813172527886](https://i.loli.net/2021/08/13/Fe2EwbSktsQxPTJ.png)](https://i.loli.net/2021/08/13/Fe2EwbSktsQxPTJ.png)

## 二、项目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>

[![image-20210813161428719](https://i.loli.net/2021/08/13/mB78V4kJdfhbUxt.png)](https://i.loli.net/2021/08/13/mB78V4kJdfhbUxt.png)

#### 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、引用方式

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

## 三、PS安装【选装】

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

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

下载打开压缩包， 解压密码为：123456

## 四、蓝湖【重点】

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

### 1、注册登录与插件

打开<https://lanhuapp.com/>，注册并登录，然后创建团队：

[![蓝湖创建团队](https://i.loli.net/2021/08/06/pPYLfQB4Gyz1NnC.png)](https://i.loli.net/2021/08/06/pPYLfQB4Gyz1NnC.png)

点击[https://lanhuapp.com/ps?comeFrom=项目列表\_右上](https://lanhuapp.com/ps?comeFrom=%E9%A1%B9%E7%9B%AE%E5%88%97%E8%A1%A8_%E5%8F%B3%E4%B8%8A)下载蓝湖ps插件。

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

[![image-20210806112816101](https://i.loli.net/2021/08/06/rUl8IBqcdZ6skya.png)](https://i.loli.net/2021/08/06/rUl8IBqcdZ6skya.png)

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

### 2、上传UI设计稿

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

[![image-20210806113117863](https://i.loli.net/2021/08/06/WugjXm3taEeSOrh.png)](https://i.loli.net/2021/08/06/WugjXm3taEeSOrh.png)

点击 `上传全部画板` 即可。

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

## 五、项目创建【熟悉】

执行 `vue create 项目名称` :

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

按照没有eslint的配置，选择 `vue 2`、`less` 、`vuex` 和 `router`：

[![image-20210806115331589](https://i.loli.net/2021/08/06/tJOwvQrmTV8ueA5.png)](https://i.loli.net/2021/08/06/tJOwvQrmTV8ueA5.png)

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

## 六、仓库创建【熟悉】

在 <https://gitee.com/> 创建一个空白仓库：

[![image-20210806115558468](https://i.loli.net/2021/08/06/bPxWOFR5JVHrQZY.png)](https://i.loli.net/2021/08/06/bPxWOFR5JVHrQZY.png)

点击 `创建` 即可。

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

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

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

git push -u origin master
```

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

[![image-20210806115958631](https://i.loli.net/2021/08/06/SVNWKajcUoXwh4I.png)](https://i.loli.net/2021/08/06/SVNWKajcUoXwh4I.png)

表示你已提交成功。

## 七、默认样式【熟悉】

### 1、清空默认样式

清空默认样式我们使用 [reset-css](https://www.npmjs.com/package/reset-css) 。具体使用方法：

```shell
npm install reset-css

# 或者：
yarn add reset-css
```

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

```js
import 'reset-css';
```

### 2、定义默认样式

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

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

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

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

```js
@import "../total.less";
```

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

### 1、方案一

* 安装 Path Intellisense插件
* 打开设置 - 首选项 - 搜索 `Path Intellisense` - 打开 `settings.json` ，添加：

```json
"path-intellisense.mappings": {
     "@": "${workspaceRoot}/src"
 }
```

* 在项目 `package.json` 所在同级目录下创建文件 `jsconfig.json`：

```json
{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "allowSyntheticDefaultImports": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
    },
    "exclude": [
        "node_modules"
    ]
}
```

* 重启vscode

### 2、方案二

安装 `path` ：

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

创建 `vue.config.js` ：

```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、安装插件

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

### 2、入口文件引入

```js
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、安装 `axios` 与 `qs`：

```shell
yarn add axios qs
```

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

```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

```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中可以定义三个状态：

```js
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，需要：

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

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

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

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

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

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

[![微信扫码登录流程图](https://tva1.sinaimg.cn/large/008i3skNgy1gtlydj66rnj61ea0u0dlj02.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gtlydj66rnj61ea0u0dlj02.jpg)

### 1、微信扫码布局与配置

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

[![扫码登录](https://i.loli.net/2021/08/09/5N7GZzTHCuWyXol.gif)](https://i.loli.net/2021/08/09/5N7GZzTHCuWyXol.gif)

在 `public/index.html` 的 `head` 中：

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

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

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

在 `api.js` 中：

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

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

```js
// 点击了微信扫码登录
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`：
>
> ```shell
> # .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` 中：
>
> ```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.js` 与 `wxlogin.css`：

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

.impowerBox .qrcode{
    margin-top: 20px;
}
```

```js
// 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'))
```

然后控制台运行：

```shell
cd src/utils/
node data-url.js
```

得到一段base64转码字符串：

```css
data:text/css;base64,LmltcG93ZXJCb3ggLnRpdGxlLCAuaW1wb3dlckJveCAuaW5mb3sNCiAgICBkaXNwbGF5OiBub25lOw0KfQ0KDQouaW1wb3dlckJveCAucXJjb2Rlew0KICAgIG1hcmdpbi10b3A6IDIwcHg7DQp9DQoNCg==
```

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

[![最终登录界面的二维码](https://i.loli.net/2021/08/09/8UrbNSjgucpLFWH.png)](https://i.loli.net/2021/08/09/8UrbNSjgucpLFWH.png)

### 2、扫码得到code做登录

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

```js
this.$route.query.code
```

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

```js
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.vue` 的 `created` 中：

```js
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` 文件：

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

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

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

### 2、验证码校验

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

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

### 3、检验是否完成拼图

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

### 4、登录请求

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

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

在判断拼图完成之后：

```js
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>

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

个人中心的界面：

[![个人中心-购物车](https://i.loli.net/2021/08/13/bOKrYcL5Zgkzvpt.png)](https://i.loli.net/2021/08/13/bOKrYcL5Zgkzvpt.png)

### 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、路由配置

```js
{
    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` 组件，修改个人中心的当前项判断：

```html
<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.js` 、`vue-socialmedia-share` 或 `vshare`。第一个插件对样式的重构性较低，第二个插件一般用于分享到外网，因此比较常用的是 [vshare](https://github.com/1006008051/vshare) 。

使用方式：

```shell
npm install vshare -S
```

在组件中引入：

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

components: {
    vshare
}
```

然后使用：

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

data中定义 `vshareConfig` ：

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

### 3、路由监听

当你路由更新，当页面没刷新时，需要监听路由：

```js
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` ，可以如下操作：

```js
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、重复点击相同路由报错

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

[![image-20210813115927502](https://i.loli.net/2021/08/13/mfvp9wxTPLoRIAY.png)](https://i.loli.net/2021/08/13/mfvp9wxTPLoRIAY.png)

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

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

```js
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` 中：

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

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

## 十九、图片懒加载

### 1、安装

```shell
yarn add vue-lazyload

# 或者
npm i vue-lazyload -S
```

### 2、全局引入与配置

```js
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` 中：

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

### 4、:src --> v-lazy

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

## 二十、设置title与favicon.ico

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

```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值来强制更新组件；
