项目介绍
《叩丁狼积分商城》是一个使用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、具体图标名称:
icon-yduifuxuankuangxuanzhong
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 2
、less
、vuex
和 router
:
创建完成后,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
中引入:
2、定义默认样式
网页中有很多固定的颜色,我们可以抽离出来作为less公共变量。在 src
下,新建 total.less
:
复制 // 公共样式变量
@blue : #0A328E;
@orange : #FF5E0F;
@black : #333333;
// 公有版心
.banxin {
width : 1200 px ;
margin-left : auto ;
margin-right : auto ;
}
然后在需要使用这些变量的组件中的css最顶部引入:
复制 @import "../total.less" ;
八、配置@指向src【了解】
1、方案一
打开设置 - 首选项 - 搜索 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"
]
}
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、安装 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.html
的 head
中:
复制 < 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.js
与 wxlogin.css
:
复制 /* wxlogin.css */
.impowerBox .title , .impowerBox .info {
display : none ;
}
.impowerBox .qrcode {
margin-top : 20 px ;
}
复制 // 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:
此时要做判断,如果有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.vue
的 created
中:
复制 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、手机号校验
新建一个 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.js
、vue-socialmedia-share
或 vshare
。第一个插件对样式的重构性较低,第二个插件一般用于分享到外网,因此比较常用的是 vshare 。
使用方式:
在组件中引入:
复制 // 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
二十、设置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、项目技术点
使用vue-cli搭建项目,并结合蓝湖+PS进行页面切图,实现对设计稿的高保真还原;
使用axios进行数据请求,并对其进行request拦截封装;
登录采用手机+验证码、手机+密码及微信扫码登录,其中微信扫码登录结合环境变量,调用后端接口实现平台切换验证;
使用localStorage对用户信息和token进行存储;
使用vshare实现将项目分享到第三方社交平台(如:微信、QQ空间等);
使用原生JS在组件mounted中监听滚动,并实现向下滚动加载更多;
使用导航守卫对每个进入个人中心页的路由进行拦截,正则匹配路径后保证有token方能进入该路由;
给组件绑定key属性,通过修改key值来强制更新组件;