# 《叩丁狼积分商城》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值来强制更新组件；


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gb.akanote.cn/vue/project.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
