小兔鲜项目笔记

小兔鲜项目

别名联想

jsconfig.json文件

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  }
}

只做提示,在vite.config.js中做路径转换

element plus 按需导入

element plus 更改主题色

axios 基础配置

utilshttp.js

import axios from 'axios'

const httpInstance = axios.create({
    baseURL:'http://pcapi-xiaotuxian-front-devtest.itheima.net',
    timeout:5000
})
//请求拦截器
httpInstance.interceptors.request.use(config => {
    return config
}, e => Promise.reject(e))
//响应拦截器
httpInstance.interceptors.response.use(res => res.data, e => {
    return Promise.reject(e)
})
export default httpInstance

API做服务

import httpInstance from "@/utils/http";
export function getCtegory () {
    return httpInstance({
        url:'home/category/head'
    })
}
问题:需要的基地址不同?

axios.create()可以执行多次,生成新的实例

路由配置

一级路由:整体页面变化

二级路由:一级路由的内部切换

//不强制组件命名
rules:{
'vue/multi-word-component-names':0
}
问题:路由设计依据

内容切换方式

默认二级路由设置:

path配置空

静态资源初始化

图片资源:由UI提供,把images文件夹放到assets

样式资源:初始化时进行样式重置,用开源normalize.css或手写,把common.scss文件放到styles

自动导入色值变量

var.scss内定义变量

  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;
        @use "@/styles/var.scss" as *;`
      },
    },
  },

字体图标引入

字体图标采用的是阿里的字体图标库

  <link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">

吸顶导航

vueuse基于Vue组合式API的实用工具集

useScroll获取滚动距离

import { useScroll } from '@vueuse/core'
const { y } = useScroll(window)
<div class="app-header-sticky" :class="{show:y>78}" >

pinia优化

//父组件中
const categoryStore = useCategoryStore()
onMounted(()=>categoryStore.getCategory())
//子组件中
const categoryStore = useCategoryStore()

轮播图

使用element plus中的组件实现,从后端调用接口

面板组件封装

组件参数:props/插槽

props传纯文本 插槽传复杂模板

图片懒加载

数据早拿到,只是在进入视口之后才进行渲染,不会多次发送请求,因为有缓存

<img v-img-lazy="item.picture" />
1.自定义命令
2.判断图片是否进入视口(vueuse
3.测试图片监控是否生效
4.若图片进入视口,发送图片资源请求
5.测试图片资源是否发出
app.directive('img-lazy',{
    mounted(el,binding){
        //el:指令绑定的元素  img
        //binding:binding.value 后面绑定的表达式的值  图片url
        console.log(el,binding.value)
    }
})
<img v-img-lazy="item.picture" alt="">
//useIntersectionObserver函数
import { useIntersectionObserver } from '@vueuse/core'
useIntersectionObserver(
            el,
            ([{ isIntersecting }], observerElement) => {
                if(isIntersecting){
                    el.src = binding.value
                }
            },
          )

懒加载优化

问题1:上面的懒加载指令直接写到main.js入口文件里面,不可理(插件)

入口文件通常只做初始化的事情,不应包含逻辑代码,通过插件的方法把懒加载指令封装成插件,main.js只需要负责注册插件

main.js

import { lazyPlugin } from '@/directives/index'
app.use(lazyPlugin)

index.js

import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
    install(app){
        app.directive('img-lazy',{
            mounted(el,binding){
                //el:指令绑定的元素  img
                //binding:binding.value 后面绑定的表达式的值  图片url
                console.log(el,binding.value)
                useIntersectionObserver(
                    el,
                    ([{ isIntersecting }], observerElement) => {
                        if(isIntersecting){
                            el.src = binding.value
                        }
                    //   targetIsVisible.value = isIntersecting
                    },
                  )
            }
        })
    }
}
问题2:重复监听, useIntersectionObserver监听一直存在,有内存浪费,除非手动停止

stop()方法

const { stop } = useIntersectionObserver(
                    el,
                    ([{ isIntersecting }], observerElement) => {
                        if(isIntersecting){
                            console.log(isIntersecting)
                            el.src = binding.value
                            stop()
                        }
                    //   targetIsVisible.value = isIntersecting
                    },
                  )

GoodsItem 封装

数据对象设计为props参数,纯展示类组件,

defineProps({
    good:{
        type:Object,
        default:() => { }
    }
})

路由一级分类

//在router里:
{
          path:'category/:id',
          component:Category
}
//相应的跳转
<RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink>

面包屑导航渲染

const categoryData = ref({})
const route = useRoute()
const getCategory = async () => {
    const res = await getCategoryAPI(route.params.id)
    categoryData.value = res.result
}
import request from '@/utils/http'
export function getCategoryAPI(id) {
    return request({
        url:'/category',
        params:{
            id
        }
    })
}

轮播图 A P I 修改

存在home页和分类页的轮播图,通过distributionSite进行控制

export function getBanerAPI(params = {}) {
    const {distributionSite = '1'} = params
    return httpInstance({
        url:'/home/banner',
        params:{
            distributionSite
        }
    })
}
const res = await getBanerAPI({
        distributionSite:'2'
    })

激活状态显示

RouterLink有提供active-class可以激活,写激活样式

<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>

.active {
        color: $xtxColor;
        border-bottom: 1px solid $xtxColor;
      }

路由缓存问题

响应路由参数的变化:

使用带参数的路由时,当用户从/users/johnny导航到/users/jolyne时(即只有参数变化时),相同的组件实例被重复使用,因为两个路由都渲染同个组件,复用更加高效,但是,这时组件的生命周期钩子不会被调用

问题:一级分类的切换,组件实例复用,导致分类数据无法更新
  1. 让组件实例不复用,这时则强制销毁重建

  2. 监听路由变换,变化之后执行数据更新的操作

//方法一: :key,强制替换一个元素(组件),而不是复用它,但是这里banner和分类都会重新请求
<RouterView :key="$route.fullPath"/>
//方法二:监听路由变换  onBeforeRouteUpdate  只重新请求分类的数据
onBeforeRouteUpdate((to) => {
    getCategory(to.params.id)
})
const getCategory = async (id = route.params.id) => {
    const res = await getCategoryAPI(id)
    categoryData.value = res.result
}

逻辑函数拆分业务

步骤:
  1. 按照业务 声明以use打头的逻辑函数
  2. 把独立的业务逻辑封装到各个函数内部
  3. 函数内部把组件中需要用到的数据或者方法return出去
  4. 在组件中调用函数把数据或者发方法组合回来使用

eg. category ->composables->useBanner

import { getBanerAPI } from '@/apis/home'
import { ref,onMounted } from 'vue'
export function useBanner(){
    const bannerList = ref([])
    const getBanner = async () => {
    const res = await getBanerAPI({
        distributionSite:'2'
    })
    bannerList.value = res.result
    }
onMounted(()=>{getBanner()})
return {
    bannerList
}}

路由二级分类

<RouterLink :to="`/category/sub/${i.id}`">

二级分类商品列表

<div class="sub-container">
<el-tabs v-model="reqData.sortField">
    <el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
    <el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
    <el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
</el-tabs>
<div class="body">
	<!-- 商品列表-->
	<GoodsItem v-for="goods in goodlist" :good="goods" :key="goods.id" />
</div>
const goodlist = ref([])
const reqData = ref({
    categoryId: route.params.id,
    page: 1,
    pageSize: 20,
    sortField: 'publishTime'
})
const getGoodList = async () => {
      const res = await getSubCategoryAPI(reqData)
      goodlist.value = res.result.items

}
onMounted(() => getGoodList())
const tabChange = () => {
    reqData.value.page = 1
    getGoodList()
}
列表无限加载功能:

v-infinite-scroll监听是否触底,触底时页数(page)增加,新老数据做拼接渲染,结束监听

<div class="body" v-infinite-scroll="load" :infinite-scroll-disabled="disabled">
         <!-- 商品列表-->
         <GoodsItem v-for="goods in goodlist" :good="goods" :key="goods.id" />
</div>

const disabled = ref(false)
const load = async () => {
    reqData.value.page++
    const res = await getSubCategoryAPI(reqData.value)
    goodlist.value = [...goodlist.value,...res.result.items]  
    if(res.result.items.length === 0){
    disabled.value = true
}}
路由切换时自动滚动到顶部
  //router -> index.js
  scrollBehavior () {
    return {
      top:0
    }
  }

详情页

问题 :拿不到数据,goods一开始是{},{}.categories是undefined,拿不到数据

router 比 created 快,拼接 router 时,created 还没获取到 router 拼接的数据

<el-breadcrumb-item :to="{ path: `/category/${goods.categories[1].id}` }">{{goods.categories[1].name }}
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories[0].id}` }">{{goods.categories[0].name }}
</el-breadcrumb-item>
  1. 可选链的语法
  2. v-if手动控制渲染时机,保证数据存在才渲染
1. 可选链
<el-breadcrumb-item :to="{ path: `/category/${goods.categories?.[1].id}` }">{{goods.categories?.[1].name }}
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories?.[0].id}` }">{{goods.categories?.[0].name }}
</el-breadcrumb-item>
2. v-if手动控制渲染时机
  <div class="xtx-goods-page">
  <div class="container" v-if="goods.details">
  </div>
  </div>
思考:渲染模板时遇到对象的多层属性访问可能出现什么问题

出现undefined,解决方法:1. 可选链 2. v-if手动控制渲染

通过小图切换大图
 <!-- 左侧大图-->
    <div class="middle" ref="target">
      <img :src="imageList[activeIndex]" alt="" />
      <!-- 蒙层小滑块 -->
      <div class="l2ayer" :style="{ left: `0px`, top: `0px` }"></div>
    </div>
    <!-- 小图列表 -->
    <ul class="small">
      <li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{active:i===activeIndex}">
        <img :src="img" alt="" />
      </li>
    </ul>

const activeIndex = ref(0)
const enterhandler= (i) => {
    activeIndex.value = i
}
放大镜效果
<!-- 放大镜大图 -->
    <div class="large" :style="[
      {
        backgroundImage: `url(${imageList[activeIndex]})`,
        backgroundPositionX: `${positionX}px`,
        backgroundPositionY: `${positionY}px`,
      },
    ]" v-show="!isOutside"></div>
  </div>
  
const { elementX, elementY, isOutside } = useMouseInElement(target)

watch([elementX, elementY,isOutside],() => {
    if(isOutside.value) return
    if(elementX.value > 100 && elementX.value < 300){
        left.value = elementX.value - 100
    }
    if(elementY.value > 100 && elementY.value < 300){
        top.value = elementY.value - 100
    }
    if(elementX.value > 300) {left.value = 200}
    if(elementX.value < 100) {left.value = 0}
    if(elementY.value > 300) {top.value = 200}
    if(elementY.value < 100) {top.value = 0}

    positionX.value = -left.value * 2
    positionY.value = -top.value * 2
})
S K U 组件

sku存货单位(库存单元),会计学名词,库存管理中的最小可用单元,例:纺织品中表示规格、颜色、款式。

sku组件的作用:产出当前用户选择的商品规格,为购物车提供数据

问:实际工作中,遇到别人写的组件或第三方组件,首先看什么?
答:props和emit,props决定当前组件接收的数据,emit决定会产出的数据

通用组件统一注册全局

components下有很多其他通用型组件,在多个业务模块中共享,所以全局组件注册

插件化

components -> index.js

import ImageView from './ImageView/index.vue'
import Sku from './XtxSku/index.vue'
export const componentPlugin = {
    install(app){
        app.component('XtxImageView',ImageView)
        app.component('XtxSku',Sku)
    }
}

main.js

import { componentPlugin } from '@/components'
app.use(componentPlugin)

登录

<!--区分登录与非登录状态-->
<template v-if="true">...</template>
<template v-else>
<li><a href="javascript:;" @click="$router.push('/login')">请先登录</a></li>...
</template>
表单检验

过滤一些错误的请求提交,节省接口压力

el-form:绑定表单对象和规则对象

el-form-item:绑定使用的规则字段

el-input:双向绑定表单数据

<!--element plus组件内置表单校验功能-->
<!--准备表单对象并绑定-->
const form = ref({
  account:'',
  password:''
})
<el-form :model="form" label-position="right" label-width="60px" status-icon>
<!--准备规则对象并绑定-->
const rules = {
    account:[
        {required:true,message:'用户名不能为空',trigger:'blur'}
    ],
    password:[
        {required:true,message:'密码不能为空',trigger:'blur'},
        {min:6,max:14,message:'密码长度6-14',trigger:'blur'}
    ]
}
<el-form :model="form" :rules="rules" label-position="right" label-width="60px" status-icon>
<!--指定表单域的校验字段名-->
<el-form-item prop="account"  label="账户"><el-input/></el-form-item>
<el-form-item prop="password" label="密码"><el-input/></el-form-item>
<!--表单对象进行双向绑定v-model-->
<el-form-item prop="account"  label="账户"><el-input v-model="form.account"/></el-form-item>
<el-form-item prop="password" label="密码"><el-input v-model="form.password"/></el-form-item>
自定义校验规则
const form = ref({
  account:'',
  password:'',
  agree:true
})
agree:[
        {
            validator:(rule,value,callback) => {
                if(value==true){
                    callback()
                }else{
                    callback(new Error('请勾选协议'))
                }
            }
        }
    ]
<el-form-item prop="agree" label-width="22px">
<el-checkbox  size="large" v-model="form.agree">
 我已同意隐私条款和服务条款</el-checkbox></el-form-item>
统一校验,对整个表单进行
//获取实例对象
const formRef = ref(null)
const doLogin = ()=> {
    formRef.value.validate((valid)=>{
        //valid所有表单都通过校验,为true
        if(valid){
            //去登陆
        }
    })
}
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="60px" status-icon>
登录接口
export const loginAPI = ({ account, password }) => {
    return request({
        url:'/login',
        method:'POST',
        data:{
            account,
            password,
        }
    })
}

响应拦截器
//响应拦截器
httpInstance.interceptors.response.use(res => res.data, e => {
    //做错误提示
    ElMessage({
        type:'warning',
        message:e.response.data.message
    })
    return Promise.reject(e)
})
登录逻辑
import 'element-plus/theme-chalk/el-message.css'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const doLogin = () => {
  const { account, password } = form.value
  // 调用实例方法
  formRef.value.validate(async (valid) => {
    // valid: 所有表单都通过校验  才为true
    console.log(valid)
    // 以valid做为判断条件 如果通过校验才执行登录逻辑
    if (valid) {
      // TODO LOGIN
      await loginAPI({ account, password })
      // 1. 提示用户
      ElMessage({ type: 'success', message: '登录成功' })
      // 2. 跳转首页
      router.replace({ path: '/' })
    }
  })
}

p i n i a 管理数据

由于用户数据特殊性,很多组件都进行共享,使用pinia管理更方便

state + action 放到pinia中,组件只负责触发action函数

//stores -> user.js
import { defineStore } from 'pinia'
import { loginAPI } from '@/apis/user'
import { ref} from 'vue'
export const useUserStore = defineStore('user',()=>{
    const userInfo = ref({})
    const getUserInfo = async ({account,password}) => {
        const res = await loginAPI({account,password})
        userInfo.value = res.result
    }
    return {
        userInfo,
        getUserInfo
    }
})
用户数据的持久化
  • 用户数据中有个Token,标识当前用户是否登录,Token持续一段时间才会过期

  • pinia是基于内存存储,刷新就丢失,保持登陆状态就是刷新不丢失,需配合持久化存储

操作 state 时会自动把用户数据在本地localStorage也存一份,刷新时从localStorage中取

pinia-plugin-persistedstate插件

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

{
    persist: true,
},

登录与非登录的模板适配

<template v-if="userStore.userInfo.token"></template>
<template v-else></template>

v-if控制template条件渲染

请求拦截器携带Token

Token作为用户标识,多个接口都需要携带Token才能获得数据,为统一控制,采取请求拦截器携带的方案

//axios请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常Token会被注入到请求header中
//请求拦截器
httpInstance.interceptors.request.use(config => {
    const userStore = useUserStore()
    const token = userStore.userInfo.token
    if(token){
        config.headers.Authorization = `Bearer ${token}`
    }
    return config
}, e => Promise.reject(e))

退出登录

//清楚用户数据,跳转登录页
const confirm =() => {
  userStore.clearUserInfo()
  router.push('/login')
}
const clearUserInfo = () => {
        userInfo.value={}
}

<el-popconfirm @confirm="confirm" title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">

Token失效401拦截处理

Token有效性保持一定时间,一段时间不做任何操作,Token失效,再去请求接口,会出401错误

1.如何确定哪个接口出401错误,什么位置拦截2.检测到401后该干什么
响应拦截器清楚过期用户信息,跳转登录页
//响应拦截器
if(e.response.status === 401){
        userStore.clearUserInfo()
        router.push('/login')
}

购物车

非登录状态登录状态
本地购物车操作接口购物车操作
所有操作直接操作pinia中的本地购物车列表所有操作走接口,操作完毕后,获取购物车列表更新本地购物车列表

本地购物车

//pinia存储
export const useCartStore = defineStore('cart',() => {
    const cartList = ref([])
    const addCart = (goods) => {
        const item = cartList.value.find((item)=>goods.skuId === item.skuId)
        if(item){
            item.count++
        }else{
            cartList.value.push(goods)
        }
    }
    return{
        cartList,
        addCart
    }
},{
    persist: true,
})
const addCart = () =>{
  if(skuObj.skuId){
    console.log(cartStore.cartList)
    cartStore.addCart({
      id:goods.value.id,
      name:goods.value.name,
      picture:goods.value.mainPictures[0],
      price:goods.value.price,
      count:count.value,
      skuId:skuObj.skuId,
      attrsText:skuObj.specsText,
      selected:true
    })
  }else{
    ElMessage.warning('请选择规格')
  }
}
购物车渲染删除
const delCart = (skuId) => {
        const idx = 					cartList.value.findIndex((item)=>skuId===item.skuId)
        cartList.value.splice(idx,1)
}
购物车单选功能

把单选框的状态与pinia中store对应的状态保持同步

v-model 双向绑定不方便进行命令式的操作,后续还需要调用接口,因此退回到一般模式:model-value和@change的配合实现

<el-checkbox :model-value="i.selected" @change="(selected)=>singleCheck(i,selected)"/>
const singleCheck = (skuId,selected)=>{
        const item = cartList.value.find((item)=>item.skuId===skuId)
        item.selected = selected
    }
const singleCheck =(i,selected) =>{
  carStore.singleCheck(i.skuId,selected)
}
购物车全选功能

piniaisAll属性决定

<el-checkbox :model-value="cartStore.isAll" @change="allCheck" />
const allCheck = (selected) =>{
        cartList.value.forEach(item => item.selected =selected)
}
const allCheck = (selected) =>{
  cartStore.allCheck(selected)
}

接口购物车

。。。

合并购物车

const getUserInfo = async ({account,password}) => {
        const res = await loginAPI({account,password})
        userInfo.value = res.result
        await mergeCartAPI(cartStore.cartList.map(item=>{
            return{
                skuId:item.skuId,
                selected:item.selected,
                count:item.count
            }
        }))
        cartStore.updateNewList()
    }

class切换激活状态

 <div class="text item" :class="{active: activeAddress.id === item.id}" @click="switchAddress(item)" v-for="item in checkInfo.userAddresses" :key="item.id">
                <ul>
                    <li><span>收<i />货<i />人:</span>{{ item.receiver }} </li>
                    <li><span>联系方式:</span>{{ item.contact }}</li>
                    <li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li>
                </ul>
            </div>
const activeAddress = ref({})
const switchAddress = (item) =>{
    activeAddress.value = item
}

携带ID跳转支付页

const createOrder = async () => {
    const res = await createOrderAPI({
        deliveryTimeType: 1,
        payType: 1,
        payChannel: 1,
        buyerMessage: '',
        goods: checkInfo.value.goods.map(item => {
            return {
                skuId: item.skuId,
                count: item.count
            }
        }),
        addressId: curAddress.value.id
    })
    const orderId = res.result.id
    router.push({
        path: '/pay',
        query: {
            id: orderId
        }
    })
    cartStore.updateNewList()
}
export function createOrderAPI(data) {
    return request({
        url:'/member/order',
        method:'POST',
        data
    })
}

支付流程

前端(跳转支付地址,get请求,订单id+回跳地址url

->后端(根据支付协议请求支付宝,需要付钱的商品必要参数)

->第三方支付宝服务(响应支付结果)

->后端(跳转到回跳地址url,订单id+支付状态)

->前端

const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const backURL = 'http://127.0.0.1:5173/paycallback'
const redirectUrl = encodeURIComponent(backURL)
const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`
<a class="btn alipay" :href="payUrl"></a>

封装倒计时

编写函数框架,确定参数和返回值 -> 编写核心逻辑 -> 实现格式化

composables文件夹:通用的逻辑函数

import dayjs from 'dayjs'
import { ref, computed } from 'vue'
export const useCountDown = () => {
    let timer = null
    const time = ref(0)
    const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
    const start = (currentTime) => {
        time.value = currentTime
        timer = setInterval(() => {
            time.value--
        }, 1000);
    }
    //组件销毁时,清除定时器
    onUnmounted(() => {
        timer && clearInterval(timer)
    })
    return {
        formatTime,
        start
    }
}
//使用
import { useCountDown } from '@/composables/useCountDown'
const { formatTime,start } = useCountDown()

分页逻辑

const tabChange = (type) => {
    params.value.orderState = type
    getOrderList()
}
const pageChange = (page) =>{
    params.value.page = page
    getOrderList()
}
<!-- 分页 -->
<div class="pagination-container">
<el-pagination @current-change="pageChange" :total="total" :pagesize="params.pageSize" background layout="prev, pager, next" />
</div>
  • 32
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值