文章目录
- 1.静态页面结构准备和动态渲染
- 2.搜索 - 历史记录管理
- 1. 写好基础静态页面,可以先往里面加一点假数据
- 2. 上面基本的渲染直接利用history渲染就可以了
- 3. 搜索历史基本渲染结束了,开始点击搜索添加历史
- 4. vant内用v-model=" ",可以快速拿到搜索框的值
- 5. 往历史记录里面追加,追加到最前面,要用到`onshift`
- 6. 清空数组,就是把它变成一个空数组,只需要在垃圾桶图标的地方注册一个点击事件,然后在methods中写方法
- 7. 完成搜索历史的持久化,往storage模块里封装方法就可以了
- 3.搜索列表 - 静态布局 & 渲染
- 1.现在这个手机是写死的,不管搜什么都是手机(基于搜索关键字渲染)
- 2.基于分类页进行渲染
- 4.商品详情 - 静态布局 & 渲染
- 1.图片部分
- 2.商品评价部分(获取接口)
- 5.加入购物车 - 唤起弹层
- 6.加入购物车 - 封装数字组件
- 7.加入购物车 - 判断token登录提示
- 1.封装接口 api/cart.js
- 2.页面中调用请求
- 3.请求拦截器中,统一携带 token
- 8.构建 vuex cart模块,获取数据存储
- 10. 购物车 - 封装 getters - 动态计算展示
- 11. 购物车 - 全选反选功能
- 12. 购物车 - 数字框修改数量
- 13. 购物车 - 编辑、删除、空购物车处理
- 14. 订单结算台
- 15. 个人中心 - 基本渲染
- 16. 个人中心 - 退出功能
- 17. 项目打包优化
- (1) 打包命令
- (2) 配置publicPath
- (3) 路由懒加载
1.静态页面结构准备和动态渲染
van-search是搜索框
van-swipe & van-swipe-item是轮播图
van-grid & van-grid-item是grid布局
2.搜索 - 历史记录管理
目标:构建搜索页面的静态布局,完成历史记录的管理
历史管理的需求:
1.搜索历史基本渲染(展示之前搜索过的标签
2.点击搜索(添加历史)
点击 搜索按钮 或 底下历史记录, 都能进行搜索
①若之前 没有 相同搜索关键字,则直接追加到最前面
②若之前 已有 相同搜索关键字, 将该原有关键字移除,再追加
3.清空历史:添加清空图标, 可以清空历史记录
4.持久化:搜索历史需要持久化,刷新历史不丢失
搜索部分在views/search/index.vue里面写
1. 写好基础静态页面,可以先往里面加一点假数据
<script>
export default {
name: 'SearchIndex',
data () {
return {
history: ['手机', '白酒', '电视']
}
}
}
</script>
2. 上面基本的渲染直接利用history渲染就可以了
<!-- 搜索历史 -->
<div class="search-history" v-if="history.length > 0">
<!-- 上面的 v-if 是,历史的长度大于0,有历史,才去渲染下面的东西 -->
<div class="title">
<span>最近搜索</span>
<van-icon name="delete-o" size="16" />
</div>
<div class="list">
<!-- 内容用v-for循环 -->
<div v-for ="item in history" :key="item" class="list-item" @click="$router.push('/searchlist')">{{ item }}</div>
</div>
</div>
3. 搜索历史基本渲染结束了,开始点击搜索添加历史
给搜索和最近搜索标签添加点击事件,
goSearch
,在下面添加方法
<div @click="goSearch">搜索</div>
<div v-for ="item in history" :key="item" class="list-item" @click="goSearch">{{ item }}</div>
methods: {
goSearch () {
console.log('进行了搜索')
}
}
4. vant内用v-model=" ",可以快速拿到搜索框的值
<van-search v-model="search" show-action placeholder="请输入搜索关键词" clearable>
<div @click="goSearch(search)">搜索</div> <!--把search传过去-->
<div v-for ="item in history" :key="item" class="list-item" @click="goSearch(item)">{{ item }}</div>
data () {
return {
search: '',
}
},
methods: {
goSearch (key) {
console.log('进行了搜索')
}
}
5. 往历史记录里面追加,追加到最前面,要用到onshift
methods: {
goSearch (key) {
// console.log('进行了搜索')
const index = this.history.indexOf(key) // indexOf的作用是用来查找当前这个key在history里的下标,如果将来真的找到了,便于删除
if (index !== -1) {
// 存在相同的项,将原有的关键字移除
// splice (从哪开始,删除几个,项1,项2)
this.history.splice(index, 1)
}
this.history.unshift(key)
}
}
6. 清空数组,就是把它变成一个空数组,只需要在垃圾桶图标的地方注册一个点击事件,然后在methods中写方法
<van-icon @click="clear" name="delete-o" size="16" />
clear () {
this.history = []
}
7. 完成搜索历史的持久化,往storage模块里封装方法就可以了
先在storage里面写
const HISTORY_KEY = 'zxy_history_list'
// 获取搜索历史
export const getHiatoryList = () => {
const result = localStorage.getItem(HISTORY_KEY)
return result ? JSON.parse(result) : []
}
// 设置搜索历史
export const setHiatoryList = (arr) => {
localStorage.setItem(HISTORY_KEY, JSON.stringify(arr))
}
然后历史记录应该优先从本地去读,直接调用(看有"👈"的行)
import { getHistoryList, setHistoryList } from '@/utils/storage'👈
export default {
name: 'SearchIndex',
data () {
return {
search: '',
history: getHistoryList()// 从本地读取👈
}
},
methods: {
goSearch (key) {
// console.log('进行了搜索')
const index = this.history.indexOf(key) // indexOf的作用是用来查找当前这个key在history里的下标,如果将来真的找到了,便于删除
if (index !== -1) {
// 存在相同的项,将原有的关键字移除
// splice (从哪开始,删除几个,项1,项2)
this.history.splice(index, 1)
}
this.history.unshift(key)
setHistoryList(this.history)// 本地存👈
// 跳转到搜索列表页
this.$router.push(`/searchlist?search=${key}`)👈
},
clear () {
this.history = []
setHistoryList([])👈
}
}
}
3.搜索列表 - 静态布局 & 渲染
1.现在这个手机是写死的,不管搜什么都是手机(基于搜索关键字渲染)
要去找接口文档
api/product.js
import requset from '@/utils/request'
// 获取搜索商品列表的数据
export const getProList = (obj) => {
const { categoryId, goodsName, page } = obj
return requset.get('/goods/list', {
params: {
categoryId,
goodsName,
page
}
})
}
计算属性,query拿地址栏参数
export default {
name: 'SearchIndex',
components: {
GoodsItem
},
computed: {
// 获取地址栏的搜索关键字
querySearch () {
return this.$route.query.search
}
}
}
在created里面发请求,拿数据然后渲染
data () {
return {
page: 1,
proList: []
}
},
async created () {
const { data: { list } } = await getProList({
goodsName: this.querySearch,
page: this.page
})
this.proList = list.data
}
list.vue把item传进去,用Goodsitem.vue解析
list.vue
<div class="goods-list">
<GoodsItem v-for="item in proList" :key="item.goods_id" :item="item"></GoodsItem>
</div>
2.基于分类页进行渲染
新建api/category.js
import request from '@/utils/request'
// 获取分类数据
export const getCategoryData = () => {
return request.get('/category/list')
}
list.vue
async created () {
const { data: { list } } = await getProList({
categoryId: this.$route.query.categoryId,👈
goodsName: this.querySearch,
page: this.page
})
this.proList = list.data
}
4.商品详情 - 静态布局 & 渲染
1.图片部分
product.js
// 获取商品详情数据
export const getProDetail = (goodsId) => {
return requset.get('/goods/detail', {
params: {
goodsId
}
})
}
prodetail/index.vue
<van-swipe :autoplay="3000" @change="onChange">
<van-swipe-item v-for="(image, index) in images" :key="index">
<img :src="image.external_url" />👈
</van-swipe-item>
<template #indicator>
<div class="custom-indicator">{{ current + 1 }} / {{ images.length }}</div>
</template>
</van-swipe>
<!-- 商品说明 -->
<div class="info">
<div class="title">
<div class="price">
<span class="now">¥{{ detail.goods_price_min }}</span>👈
<span class="oldprice">¥{{ detail.goods_price_max }}</span>👈
</div>
<div class="sellcount">已售 {{ detail.goods_sales }} 件</div>👈
</div>
<div class="msg text-ellipsis-2">
{{ detail.goods_name }}👈
</div>
data () {
return {
images: [],
current: 0,
detail: {}
}
},
computed: {
goodsId () {
return this.$route.params.id
}
},
created () {
this.getDetail()
},
methods: {
onChange (index) {
this.current = index
},
async getDetail () {
const { data: { detail } } = await getProDetail(this.goodsId)
this.detail = detail
this.images = detail.goods_images
console.log(this.images)
}
}
商品描述部分不能用{{ }},因为里面包含p标签
<!-- 商品描述 -->
<div class="desc" v-html="detail.content">
</div>
2.商品评价部分(获取接口)
product.js
// 获取商品评价
export const getProComments = (goodsId, limit) => {
return request.get('/comment/listRows', {
params: {
goodsId,
limit
}
})
}
index.vue
<!-- 商品评价 -->
<div class="comment">
<div class="comment-title">
<div class="left">商品评价 ({{ total }})</div>
<div class="right">查看更多 <van-icon name="arrow" /> </div>
</div>
<div class="comment-list">
<div class="comment-item" v-for="item in commentList" :key="item.comment_id">
<div class="top">
<img :src="item.user.avatar_url || defaultImg" alt="">
<div class="name">{{ item.user.nick_name }}</div>
<van-rate :size="16" :value="item.score / 2" color="#ffd21e" void-icon="star" void-color="#eee"/>
</div>
<div class="content">
{{ item.content }}
</div>
<div class="time">
{{ item.create_time }}
</div>
</div>
</div>
</div>
import defaultImg from '@/assets/default-avatar.png'
export default {
name: 'ProDetail',
data () {
return {
images: [],
current: 0,
detail: {},
total: 0, // 评价总数👈
commentList: [], // 评价列表👈
defaultImg👈
}
},
computed: {
goodsId () {
return this.$route.params.id
}
},
created () {
this.getDetail()
this.getComments()👈
},
methods: {
onChange (index) {
this.current = index
},
async getDetail () {
const { data: { detail } } = await getProDetail(this.goodsId)
this.detail = detail
this.images = detail.goods_images
console.log(this.images)
},
async getComments () {👈
const { data: { list, total } } = await getProComments(this.goodsId, 3)👈
this.commentList = list👈
this.total = total👈
}👈
}
}
</script>
5.加入购物车 - 唤起弹层
弹层用的是vant中的反馈组件
import { ActionSheet } from 'vant';
Vue.use(ActionSheet);
自定义面板
通过插槽可以自定义面板的展示内容,同时可以使用title属性展示标题栏
<van-action-sheet v-model="show" title="标题">
<div class="content">内容</div>
</van-action-sheet>
<style>
.content {
padding: 16px 16px 160px;
}
</style>
index.vue
<!-- 加入购物车的弹层 -->
<van-action-sheet v-model="showPannel" :title="mode === 'cart' ? '加入购物车' : '立刻购买'">
<div class="product">
<div class="product-title">
<div class="left">
<img :src="detail.goods_image" alt="">
</div>
<div class="right">
<div class="price">
<span>¥</span>
<span class="nowprice">{{ detail.goods_price_min }}</span>
</div>
<div class="count">
<span>库存</span>
<span>{{ detail.stock_total }}</span>
</div>
</div>
</div>
<div class="num-box">
<span>数量</span>
数字框占位
</div>
<!-- 有库存才显示提交按钮 -->
<div class="showbtn" v-if="detail.stock_total > 0">
<div class="btn" v-if="mode === 'cart'">加入购物车</div>
<div class="btn now" v-else>立刻购买</div>
</div>
<div class="btn-none" v-else>该商品已抢完</div>
</div>
</van-action-sheet>
6.加入购物车 - 封装数字组件
components/CountBox.vue
<template>
<div class="count-box">
<button @click="handleSub" class="minus">-</button>
<input :value="value" @change="handleChange" class="inp" type="text">
<button @click="handleAdd" class="add">+</button>
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 1
}
},
methods: {
handleSub () {
if (this.value <= 1) {
return
}
this.$emit('input', this.value - 1)
},
handleAdd () {
this.$emit('input', this.value + 1)
},
handleChange (e) {
// console.log(e.target.value)
const num = +e.target.value // 转数字处理 (1) 数字 (2) NaN
// 输入了不合法的文本 或 输入了负值,回退成原来的 value 值
if (isNaN(num) || num < 1) {
e.target.value = this.value
return
}
this.$emit('input', num)
}
}
}
</script>
<style lang="less" scoped>
.count-box {
width: 110px;
display: flex;
.add, .minus {
width: 30px;
height: 30px;
outline: none;
border: none;
background-color: #efefef;
}
.inp {
width: 40px;
height: 30px;
outline: none;
border: none;
margin: 0 5px;
background-color: #efefef;
text-align: center;
}
}
</style>
prodeatil/index.js
import CountBox from '@/components/CountBox.vue'👈
export default {
name: 'ProDetail',
components: {
CountBox
},
data () {
return {
images: [],
current: 0,
detail: {},
total: 0, // 评价总数
commentList: [], // 评价列表
defaultImg,
showPannel: false, // 控制弹层的显示隐藏
mode: 'cart', // 标记弹层状态
addCount: 1 // 数字框绑定的数据👈
}
},
7.加入购物车 - 判断token登录提示
1.封装接口 api/cart.js
// 加入购物车
export const addCart = (goodsId, goodsNum, goodsSkuId) => {
return request.post('/cart/add', {
goodsId,
goodsNum,
goodsSkuId
})
}
2.页面中调用请求
data () {
return {
cartTotal: 0
}
},
async addCart () {
...
const { data } = await addCart(this.goodsId, this.addCount, this.detail.skuList[0].goods_sku_id)
this.cartTotal = data.cartTotal
this.$toast('加入购物车成功')
this.showPannel = false
},
3.请求拦截器中,统一携带 token
// 自定义配置 - 请求/响应 拦截器
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
...
const token = store.getters.token
if (token) {
config.headers['Access-Token'] = token
config.headers.platform = 'H5'
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
8.构建 vuex cart模块,获取数据存储
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
新建 modules/cart.js 模块
export default {
namespaced: true,
state () {
return {
cartList: []
}
},
mutations: {
},
actions: {
},
getters: {
}
}
挂载到 store 上面
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({
getters: {
token: state => state.user.userInfo.token
},
modules: {
user,
cart
}
})
封装 API 接口 api/cart.js
// 获取购物车列表数据
export const getCartList = () => {
return request.get('/cart/list')
}
封装 action 和 mutation
mutations: {
setCartList (state, newList) {
state.cartList = newList
},
},
actions: {
async getCartAction (context) {
const { data } = await getCartList()
data.list.forEach(item => {
item.isChecked = true
})
context.commit('setCartList', data.list)
}
},
页面中 dispatch 调用
computed: {
isLogin () {
return this.$store.getters.token
}
},
created () {
if (this.isLogin) {
this.$store.dispatch('cart/getCartAction')
}
},
- 购物车 - mapState - 渲染购物车列表
将数据映射到页面
import { mapState } from 'vuex'
computed: {
...mapState('cart', ['cartList'])
}
动态渲染
<!-- 购物车列表 -->
<div class="cart-list">
<div class="cart-item" v-for="item in cartList" :key="item.goods_id">
<van-checkbox icon-size="18" :value="item.isChecked"></van-checkbox>
<div class="show" @click="$router.push(`/prodetail/${item.goods_id}`)">
<img :src="item.goods.goods_image" alt="">
</div>
<div class="info">
<span class="tit text-ellipsis-2">{{ item.goods.goods_name }}</span>
<span class="bottom">
<div class="price">¥ <span>{{ item.goods.goods_price_min }}</span></div>
<CountBox :value="item.goods_num"></CountBox>
</span>
</div>
</div>
</div>
10. 购物车 - 封装 getters - 动态计算展示
封装 getters:商品总数 / 选中的商品列表 / 选中的商品总数 / 选中的商品总价
getters: {
cartTotal (state) {
return state.cartList.reduce((sum, item, index) => sum + item.goods_num, 0)
},
selCartList (state) {
return state.cartList.filter(item => item.isChecked)
},
selCount (state, getters) {
return getters.selCartList.reduce((sum, item, index) => sum + item.goods_num, 0)
},
selPrice (state, getters) {
return getters.selCartList.reduce((sum, item, index) => {
return sum + item.goods_num * item.goods.goods_price_min
}, 0).toFixed(2)
}
}
页面中 mapGetters 映射使用
computed: {
...mapGetters('cart', ['cartTotal', 'selCount', 'selPrice']),
},
<!-- 购物车开头 -->
<div class="cart-title">
<span class="all">共<i>{{ cartTotal || 0 }}</i>件商品</span>
<span class="edit">
<van-icon name="edit" />
编辑
</span>
</div>
<div class="footer-fixed">
<div class="all-check">
<van-checkbox icon-size="18"></van-checkbox>
全选
</div>
<div class="all-total">
<div class="price">
<span>合计:</span>
<span>¥ <i class="totalPrice">{{ selPrice }}</i></span>
</div>
<div v-if="true" :class="{ disabled: selCount === 0 }" class="goPay">
结算({{ selCount }})
</div>
<div v-else :class="{ disabled: selCount === 0 }" class="delete">
删除({{ selCount }})
</div>
</div>
</div>
11. 购物车 - 全选反选功能
全选 getters
getters: {
isAllChecked (state) {
return state.cartList.every(item => item.isChecked)
}
}
...mapGetters('cart', ['isAllChecked']),
<div class="all-check">
<van-checkbox :value="isAllChecked" icon-size="18"></van-checkbox>
全选
</div>
点击小选,修改状态
<van-checkbox @click="toggleCheck(item.goods_id)" ...></van-checkbox>
toggleCheck (goodsId) {
this.$store.commit('cart/toggleCheck', goodsId)
},
mutations: {
toggleCheck (state, goodsId) {
const goods = state.cartList.find(item => item.goods_id === goodsId)
goods.isChecked = !goods.isChecked
},
}
点击全选,重置状态
<div @click="toggleAllCheck" class="all-check">
<van-checkbox :value="isAllChecked" icon-size="18"></van-checkbox>
全选
</div>
toggleAllCheck () {
this.$store.commit('cart/toggleAllCheck', !this.isAllChecked)
},
mutations: {
toggleAllCheck (state, flag) {
state.cartList.forEach(item => {
item.isChecked = flag
})
},
}
12. 购物车 - 数字框修改数量
封装 api 接口
// 更新购物车商品数量
export const changeCount = (goodsId, goodsNum, goodsSkuId) => {
return request.post('/cart/update', {
goodsId,
goodsNum,
goodsSkuId
})
}
页面中注册点击事件,传递数据
<CountBox :value="item.goods_num" @input="value => changeCount(value, item.goods_id, item.goods_sku_id)"></CountBox>
changeCount (value, goodsId, skuId) {
this.$store.dispatch('cart/changeCountAction', {
value,
goodsId,
skuId
})
},
提供 action 发送请求, commit mutation
mutations: {
changeCount (state, { goodsId, value }) {
const obj = state.cartList.find(item => item.goods_id === goodsId)
obj.goods_num = value
}
},
actions: {
async changeCountAction (context, obj) {
const { goodsId, value, skuId } = obj
context.commit('changeCount', {
goodsId,
value
})
await changeCount(goodsId, value, skuId)
},
}
13. 购物车 - 编辑、删除、空购物车处理
data 提供数据, 定义是否在编辑删除的状态
data () {
return {
isEdit: false
}
},
注册点击事件,修改状态
<span class="edit" @click="isEdit = !isEdit">
<van-icon name="edit" />
编辑
</span>
底下按钮根据状态变化
<div v-if="!isEdit" :class="{ disabled: selCount === 0 }" class="goPay">
去结算({{ selCount }})
</div>
<div v-else :class="{ disabled: selCount === 0 }" class="delete">删除</div>
监视编辑状态,动态控制复选框状态
watch: {
isEdit (value) {
if (value) {
this.$store.commit('cart/toggleAllCheck', false)
} else {
this.$store.commit('cart/toggleAllCheck', true)
}
}
}
购物车 - 删除功能完成
查看接口,封装 API ( 注意:此处 id 为获取回来的购物车数据的 id )
// 删除购物车
export const delSelect = (cartIds) => {
return request.post('/cart/clear', {
cartIds
})
}
注册删除点击事件
<div v-else :class="{ disabled: selCount === 0 }" @click="handleDel" class="delete">
删除({{ selCount }})
</div>
async handleDel () {
if (this.selCount === 0) return
await this.$store.dispatch('cart/delSelect')
this.isEdit = false
},
提供 actions
actions: {
// 删除购物车数据
async delSelect (context) {
const selCartList = context.getters.selCartList
const cartIds = selCartList.map(item => item.id)
await delSelect(cartIds)
Toast('删除成功')
// 重新拉取最新的购物车数据 (重新渲染)
context.dispatch('getCartAction')
}
},
购物车 - 空购物车处理
外面包个大盒子,添加 v-if 判断
<div class="cart-box" v-if="isLogin && cartList.length > 0">
<!-- 购物车开头 -->
<div class="cart-title">
...
</div>
<!-- 购物车列表 -->
<div class="cart-list">
...
</div>
<div class="footer-fixed">
...
</div>
</div>
<div class="empty-cart" v-else>
<img src="@/assets/empty.png" alt="">
<div class="tips">
您的购物车是空的, 快去逛逛吧
</div>
<div class="btn" @click="$router.push('/')">去逛逛</div>
</div>
14. 订单结算台
所谓的 “立即结算”,本质就是跳转到订单结算台,并且跳转的同时,需要携带上对应的订单参数。
而具体需要哪些参数,就需要基于 【订单结算台】 的需求来定。
(1) 静态布局
(2) 获取收货地址列表
- 封装获取地址的接口
import request from '@/utils/request'
// 获取地址列表
export const getAddressList = () => {
return request.get('/address/list')
}
- 页面中 - 调用获取地址
data () {
return {
addressList: []
}
},
computed: {
selectAddress () {
// 这里地址管理不是主线业务,直接获取默认第一条地址
return this.addressList[0]
}
},
async created () {
this.getAddressList()
},
methods: {
async getAddressList () {
const { data: { list } } = await getAddressList()
this.addressList = list
}
}
- 页面中 - 进行渲染
computed: {
longAddress () {
const region = this.selectAddress.region
return region.province + region.city + region.region + this.selectAddress.detail
}
},
<div class="info" v-if="selectAddress?.address_id">
<div class="info-content">
<span class="name">{{ selectAddress.name }}</span>
<span class="mobile">{{ selectAddress.phone }}</span>
</div>
<div class="info-address">
{{ longAddress }}
</div>
</div>
(3) 订单结算 - 封装通用接口
思路分析 : 这里的订单结算,有两种情况:
购物车结算,需要两个参数
① mode=“cart”
② cartIds=“cartId, cartId”
立即购买结算,需要三个参数
① mode=“buyNow”
② goodsId=“商品id”
③ goodsSkuId=“商品skuId”
都需要跳转时将参数传递过来
封装通用 API 接口 api/order
import request from '@/utils/request'
export const checkOrder = (mode, obj) => {
return request.get('/checkout/order', {
params: {
mode,
delivery: 0,
couponId: 0,
isUsePoints: 0,
...obj
}
})
}
15. 个人中心 - 基本渲染
1 封装获取个人信息 - API接口
import request from '@/utils/request'
// 获取个人信息
export const getUserInfoDetail = () => {
return request.get('/user/info')
}
2 调用接口,获取数据进行渲染
<template>
<div class="user">
<div class="head-page" v-if="isLogin">
<div class="head-img">
<img src="@/assets/default-avatar.png" alt="" />
</div>
<div class="info">
<div class="mobile">{{ detail.mobile }}</div>
<div class="vip">
<van-icon name="diamond-o" />
普通会员
</div>
</div>
</div>
<div v-else class="head-page" @click="$router.push('/login')">
<div class="head-img">
<img src="@/assets/default-avatar.png" alt="" />
</div>
<div class="info">
<div class="mobile">未登录</div>
<div class="words">点击登录账号</div>
</div>
</div>
<div class="my-asset">
<div class="asset-left">
<div class="asset-left-item">
<span>{{ detail.pay_money || 0 }}</span>
<span>账户余额</span>
</div>
<div class="asset-left-item">
<span>0</span>
<span>积分</span>
</div>
<div class="asset-left-item">
<span>0</span>
<span>优惠券</span>
</div>
</div>
<div class="asset-right">
<div class="asset-right-item">
<van-icon name="balance-pay" />
<span>我的钱包</span>
</div>
</div>
</div>
<div class="order-navbar">
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=all')">
<van-icon name="balance-list-o" />
<span>全部订单</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=payment')">
<van-icon name="clock-o" />
<span>待支付</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=delivery')">
<van-icon name="logistics" />
<span>待发货</span>
</div>
<div class="order-navbar-item" @click="$router.push('/myorder?dataType=received')">
<van-icon name="send-gift-o" />
<span>待收货</span>
</div>
</div>
<div class="service">
<div class="title">我的服务</div>
<div class="content">
<div class="content-item">
<van-icon name="records" />
<span>收货地址</span>
</div>
<div class="content-item">
<van-icon name="gift-o" />
<span>领券中心</span>
</div>
<div class="content-item">
<van-icon name="gift-card-o" />
<span>优惠券</span>
</div>
<div class="content-item">
<van-icon name="question-o" />
<span>我的帮助</span>
</div>
<div class="content-item">
<van-icon name="balance-o" />
<span>我的积分</span>
</div>
<div class="content-item">
<van-icon name="refund-o" />
<span>退换/售后</span>
</div>
</div>
</div>
<div class="logout-btn">
<button>退出登录</button>
</div>
</div>
</template>
<script>
import { getUserInfoDetail } from '@/api/user.js'
export default {
name: 'UserPage',
data () {
return {
detail: {}
}
},
created () {
if (this.isLogin) {
this.getUserInfoDetail()
}
},
computed: {
isLogin () {
return this.$store.getters.token
}
},
methods: {
async getUserInfoDetail () {
const { data: { userInfo } } = await getUserInfoDetail()
this.detail = userInfo
console.log(this.detail)
}
}
}
</script>
<style lang="less" scoped>
.user {
min-height: 100vh;
background-color: #f7f7f7;
padding-bottom: 50px;
}
.head-page {
height: 130px;
background: url("http://cba.itlike.com/public/mweb/static/background/user-header2.png");
background-size: cover;
display: flex;
align-items: center;
.head-img {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
margin: 0 10px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.info {
.mobile {
margin-bottom: 5px;
color: #c59a46;
font-size: 18px;
font-weight: bold;
}
.vip {
display: inline-block;
background-color: #3c3c3c;
padding: 3px 5px;
border-radius: 5px;
color: #e0d3b6;
font-size: 14px;
.van-icon {
font-weight: bold;
color: #ffb632;
}
}
}
.my-asset {
display: flex;
padding: 20px 0;
font-size: 14px;
background-color: #fff;
.asset-left {
display: flex;
justify-content: space-evenly;
flex: 3;
.asset-left-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
span:first-child {
margin-bottom: 5px;
color: #ff0000;
font-size: 16px;
}
}
}
.asset-right {
flex: 1;
.asset-right-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.van-icon {
font-size: 24px;
margin-bottom: 5px;
}
}
}
}
.order-navbar {
display: flex;
padding: 15px 0;
margin: 10px;
font-size: 14px;
background-color: #fff;
border-radius: 5px;
.order-navbar-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 25%;
.van-icon {
font-size: 24px;
margin-bottom: 5px;
}
}
}
.service {
font-size: 14px;
background-color: #fff;
border-radius: 5px;
margin: 10px;
.title {
height: 50px;
line-height: 50px;
padding: 0 15px;
font-size: 16px;
}
.content {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
font-size: 14px;
background-color: #fff;
border-radius: 5px;
.content-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 25%;
margin-bottom: 20px;
.van-icon {
font-size: 24px;
margin-bottom: 5px;
color: #ff3800;
}
}
}
}
.logout-btn {
button {
width: 60%;
margin: 10px auto;
display: block;
font-size: 13px;
color: #616161;
border-radius: 9px;
border: 1px solid #dcdcdc;
padding: 7px 0;
text-align: center;
background-color: #fafafa;
}
}
</style>
16. 个人中心 - 退出功能
1 注册点击事件
<button @click="logout">退出登录</button>
2 提供方法
methods: {
logout () {
this.$dialog.confirm({
title: '温馨提示',
message: '你确认要退出么?'
})
.then(() => {
this.$store.dispatch('user/logout')
})
.catch(() => {
})
}
}
actions: {
logout (context) {
context.commit('setUserInfo', {})
context.commit('cart/setCartList', [], { root: true })
}
},
17. 项目打包优化
vue脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线
参与上线的是 => 打包后的源代码
打包:
- 将多个文件压缩合并成一个文件
- 语法降级
- less sass ts 语法解析, 解析成css
- …
打包后,可以生成,浏览器能够直接运行的网页 => 就是需要上线的源码!
(1) 打包命令
vue脚手架工具已经提供了打包命令,直接使用即可。
yarn build
在项目的根目录会自动创建一个文件夹dist
,dist中的文件就是打包后的文件,只需要放到服务器中即可。
(2) 配置publicPath
module.exports = {
// 设置获取.js,.css文件时,是以相对地址为基准的。
// https://cli.vuejs.org/zh/config/#publicpath
publicPath: './'
}
(3) 路由懒加载
路由懒加载 & 异步组件, 不会一上来就将所有的组件都加载,而是访问到对应的路由了,才加载解析这个路由对应的所有组件
官网链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E4%BD%BF%E7%94%A8-webpack
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
const ProDetail = () => import('@/views/prodetail')
const Pay = () => import('@/views/pay')
const MyOrder = () => import('@/views/myorder')