van-search是搜索框
van-swipe & van-swipe-item是轮播图
van-grid & van-grid-item是grid布局
目标:构建搜索页面的静态布局,完成历史记录的管理
历史管理的需求:
1.搜索历史基本渲染(展示之前搜索过的标签
2.点击搜索(添加历史)
点击 搜索按钮 或 底下历史记录, 都能进行搜索
①若之前 没有 相同搜索关键字,则直接追加到最前面
②若之前 已有 相同搜索关键字, 将该原有关键字移除,再追加
3.清空历史:添加清空图标, 可以清空历史记录
4.持久化:搜索历史需要持久化,刷新历史不丢失
搜索部分在views/search/index.vue里面写
最近搜索
{{ item }}
搜索
{{ item }}
methods: {
goSearch () {
console.log('进行了搜索')
}
}
搜索
{{ item }}
data () {
return {
search: '',
}
},
methods: {
goSearch (key) {
console.log('进行了搜索')
}
}
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)
}
}
clear () {
this.history = []
}
先在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([])
}
}
}
要去找接口文档
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
新建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
}
product.js
// 获取商品详情数据
export const getProDetail = (goodsId) => {
return requset.get('/goods/detail', {
params: {
goodsId
}
})
}
prodetail/index.vue
{{ current + 1 }} / {{ images.length }}
¥{{ detail.goods_price_min }}
¥{{ detail.goods_price_max }}
已售 {{ detail.goods_sales }} 件
{{ detail.goods_name }}
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标签
2.商品评价部分(获取接口)
product.js
// 获取商品评价
export const getProComments = (goodsId, limit) => {
return request.get('/comment/listRows', {
params: {
goodsId,
limit
}
})
}
index.vue
商品评价 ({{ total }})
查看更多
{{ item.user.nick_name }}
{{ item.content }}
{{ item.create_time }}
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
}
}
}
5.加入购物车 - 唤起弹层
import { ActionSheet } from 'vant';
Vue.use(ActionSheet);
自定义面板
通过插槽可以自定义面板的展示内容,同时可以使用title属性展示标题栏
内容
index.vue
¥
{{ detail.goods_price_min }}
库存
{{ detail.stock_total }}
数量
数字框占位
加入购物车
立刻购买
该商品已抢完
6.加入购物车 - 封装数字组件
components/CountBox.vue
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'])
}
动态渲染
{{ item.goods.goods_name }}
¥ {{ item.goods.goods_price_min }}
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']),
},
共{{ cartTotal || 0 }}件商品
编辑
11. 购物车 - 全选反选功能
全选 getters
getters: {
isAllChecked (state) {
return state.cartList.every(item => item.isChecked)
}
}
...mapGetters('cart', ['isAllChecked']),
全选
点击小选,修改状态
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
},
}
点击全选,重置状态
全选
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
})
}
页面中注册点击事件,传递数据
changeCount(value, item.goods_id, item.goods_sku_id)">
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
}
},
注册点击事件,修改状态
编辑
底下按钮根据状态变化
去结算({{ selCount }})
删除
监视编辑状态,动态控制复选框状态
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
})
}
注册删除点击事件
删除({{ selCount }})
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 判断
...
...
您的购物车是空的, 快去逛逛吧
去逛逛
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
}
},
{{ selectAddress.name }}
{{ selectAddress.phone }}
{{ longAddress }}
(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 调用接口,获取数据进行渲染
{{ detail.mobile }}
普通会员
未登录
点击登录账号
{{ detail.pay_money || 0 }}
账户余额
0
积分
0
优惠券
我的钱包
我的服务
收货地址
领券中心
优惠券
我的帮助
我的积分
退换/售后
16. 个人中心 - 退出功能
1 注册点击事件
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')