# vue-rabbit **Repository Path**: su27sk/vue-rabbit ## Basic Information - **Project Name**: vue-rabbit - **Description**: 小兔鲜儿电商项目 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-10 - **Last Updated**: 2026-06-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue-rabbit 小兔鲜儿电商前端项目 ## 使用到的开发插件 ![工程化插件](./picture/工程化插件.png "工程化插件") ## 页面预览 ![Home页面](./picture/Home页面.png "Home页面") ![一级Category页面.png](./picture/一级Category页面.png "一级Category页面.png") ![二级Category页面.png](./picture/二级Category页面.png "二级Category页面.png") ![Detail页面.png](./picture/Detail页面.png "Detail页面.png") ![Login页面.png](./picture/Login页面.png "Login页面.png") ![头部购物车页面.png](./picture/头部购物车页面.png "头部购物车页面.png") ![CartList页面.png](./picture/CartList页面.png "CartList页面.png") ![订单页面.png](./picture/订单页面.png "订单页面.png") ![pay页面.png](./picture/pay页面.png "pay页面.png") ![PayBack页面.png](./picture/PayBack页面.png "PayBack页面.png") ![会员中心页面.png](./picture/会员中心页面.png "会员中心页面.png") ## 涉及到的技术 Vue3,Element-Plus,阿里icon图标库,封装axios,自定义scss样式(Element-Plus主题色定制) ``` # element-plus npm install element-plus --save # 按需导入 npm install -D unplugin-vue-components unplugin-auto-import # 样式 npm i sass -D # axios npm i axios # 阿里icon # Vue 官方风的工具库 npm i @vueuse/core # pinia持久化插件 npm i pinia-plugin-persistedstate # 时间格式化 npm i dayjs ``` 涉及到的配置文件脚手架:vite.config.js,eslint.config.js,styles里面引入element,common.scss和var.scss;assets引入图片。 ## Layout页面 ### 一级导航渲染 首先封装网络请求 ``` import httpInstance from '@/utils/http' export function getCategoryAPI () { return httpInstance({ url:'/home/category/head' }) } ``` 在LayoutHeader页面将数据渲染出来 ``` ``` ### 一级导航吸顶效果 核心逻辑:根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示 使用到了组件import { useScroll } from '@vueuse/core'; ``` ``` ### 使用Pinia存储一级导航数据 LayoutFixed和LayoutHeader组件都公用一套导航数据,用Pinia存储,避免重复调用 首先构建stores里面的category.js ``` import { ref } from 'vue' import { defineStore } from 'pinia' import { getCategoryAPI } from '@/apis/layout' export const useCategoryStore = defineStore('category', () => { const categortList = ref([]) const getCategory = async() => { const res = await getCategoryAPI() categortList.value = res.data.result } return { categortList, getCategory} }) ``` 然后让Layout的主页面挂载的时候调用getCategory的action保证categortList有数据,然后组件里面复用 ``` ``` ## Home页面 ### bannner轮播图 使用到了el-carousel组件作为轮播图 ``` ``` ### 插槽实现新鲜好物/人气推荐 首先创建一个HomePanel组件作为新鲜好物/人气推荐的数据容器,用到了props数据传递和默认插槽 ``` ``` 然后HomeNew和HomeHot组件传入数据 ``` ``` ### 图片懒加载 封装一个懒加载插件,自定义v-img-lazy指令 ``` import { useIntersectionObserver } from '@vueuse/core' export const lazyplugin = { install(app) { // 懒加载插件 // 自定义全局指令 app.directive('img-lazy',{ mounted(el,binding) { // el:指令绑定的那个元素 img // binding:指令后面跟的表达式 item.picture console.log(el,binding.value); const {stop} = useIntersectionObserver( el, ([{ isIntersecting}]) => { console.log(isIntersecting); if(isIntersecting){ // 是否进入视口区域 // 进入视口区域 el.src = binding.value // 图片加载完成以后结束视图监听 stop() } } ) } }) } } ``` 最后在main.js文件将懒加载插件挂载好 ``` // 引入懒加载插件 import {lazyplugin} from '@/directives' app.use(lazyplugin) // 挂载懒加载插件 ``` ## 一级Category页面 ### 设置默认路由跳转 设置动态路由路由传参 ``` { path:'category/:id', component:Category } ``` 给LayoutHeader和LayoutFixed设置跳转路由 ``` {{ item.name }} ``` ### 面包屑导航 需要用到组件el-breadcrumb ```
首页 {{ categoryData.name }}
``` ### 路由跳转激活状态控制 使用active-class这个指令解决,然后后面加上要显示激活状态的css样式class ``` {{ item.name }} ``` ### 响应式监听路由变化 解决url从/category/100跳转到/category/101时候只变化url页面不更新问题 使用watch监听的方法 ``` # 方法一 const getRouteId = () =>{ return route.params.id } // 监听分类id变化 watch(getRouteId ,(newId) => { if(newId){ getCategory(newId) } }, { immediate:true } ) ``` 使用onBeforeRouteUpdate解决 ``` import { useRoute,onBeforeRouteUpdate } from 'vue-router'; const categoryData = ref([]) const route = useRoute() const getCategory = async(id = route.params.id) => { const res = await getCategoryAPI(id) categoryData.value = res.data.result } // 路由参数变化时分类数据接口重新发送 onBeforeRouteUpdate((to)=>{ // to存储要跳转到的url getCategory(to.params.id) }) ``` ### 将业务代码封装 分类相关逻辑 ``` import {ref,onMounted} from 'vue' import { getCategoryAPI } from '@/apis/category' import { useRoute,onBeforeRouteUpdate } from 'vue-router'; export function useCategory() { const categoryData = ref([]) const route = useRoute() const getCategory = async(id = route.params.id) => { const res = await getCategoryAPI(id) categoryData.value = res.data.result } onMounted(()=>{ getCategory() }) // 路由参数变化时分类数据接口重新发送 onBeforeRouteUpdate((to)=>{ // to存储要跳转到的url getCategory(to.params.id) }) return { categoryData } } ``` 轮播图相关逻辑 ``` import {ref,onMounted} from 'vue' import { getBannerAPI } from '@/apis/home'; // 获取banner export function useBanner(){ const bannerList = ref([]) const getBanner = async() => { const res = await getBannerAPI({distrbutionSite:2}) console.log(res); bannerList.value = res.data.result } onMounted(()=>{ getBanner() }) return { bannerList } } ``` 最后导入业务逻辑 ``` ``` ## 二级Category页面 设置动态路由路由传参 ``` { path:'category/sub/:id', component: SubCategory } ``` 一级分类点击跳转 ``` ``` ### 重置筛选条件 ``` ``` ## Login页面 ### 验证规则 首先需要创建表单对象,使用 的:model相结合绑定,然后创建规则对象,使用 的:rules="rules"跟规则对象的具体字段相结合绑定,最后输入框的使用数据双向绑定v-model跟表单对象的具体字段相绑定。同时可以使用validator做自定义登录校验规则 ``` ``` ### 给请求拦截器添加token请求头 ``` import { useUserStore } from '@/stores/user' // 请求拦截器 httpInstance.interceptors.request.use(config =>{ // 1.从pinia获取token信息 const userStore = useUserStore() // 2.按照后端要求处理token信息 const token = userStore.userInfo.token if(token) { config.headers.Authorization = `Bearer ${token}` } return config },e => Promise.reject(e)) ``` ### 退出登录 el-popconfirm组件是一个确认的小弹窗二次确认,所以使用@confirm触发方法调用退出登录的方法 ``` ``` user.js也要创建一个清除用户信息的方法 ``` // 3.退出时清除用户信息 const clearUserInfo = () => { userInfo.value = {} } // 4.return出state和action return { userInfo, getUserInfo, clearUserInfo } ``` ### token无效401报错 ``` // 响应拦截器 httpInstance.interceptors.response.use(res => res, e =>{ // 1.从pinia获取token信息 const userStore = useUserStore() // 统一错误提示 ElMessage({ type:'warning', message: e.response.data.message }) if (e.response.status == 401) { // 2.清除用户信息 userStore.clearUserInfo() // 3.跳转登录路由 router.push('/login') } return Promise.reject(e) }) ``` ## HeaderCart页面 首先完成添加购物车操作 ``` // 封装购物车模块 import {defineStore} from 'pinia' import {computed, ref} from 'vue' export const useCartStore = defineStore('cart',()=>{ // 1.定义state const cartList = ref([]) // 2.定义action // 添加购物车数据 const addCart = (goods) => { // 添加购物车操作 // 判断有没有加入过购物车,加入过就数量加一,没有再添加 const item = cartList.value.find((item)=>{ return goods.skuId === item.skuId }) if(item) { item.count++ }else { cartList.value.push(goods) } } // 删除购物车数据 const delCart = (skuId) => { const idx = cartList.value.findIndex((item) => { return skuId === item.skuId }) cartList.value.splice(idx,1) } // 3.计算属性 // 1.总的数量 const allCount = computed(()=>{ return cartList.value.reduce((a,c)=> a + c.count,0) }) // 2.总的价格 const allPrice = computed(()=>{ return cartList.value.reduce((a,c)=> a + c.count * c.price ,0) }) return { cartList, addCart, delCart, allCount, allPrice } },{ persist:true // 开启持久化 }) ``` 商品Detail页面进行商品规格数量操作后添加购物车 ``` ``` ## CartList页面 ### 单选/多选功能 首先提供cartStore的action和getter ``` // 单选功能 const singleCheck = (skuId,selected) => { const item = cartList.value.find((item)=> { return item.id === skuId }) item.selected = selected } // 全选功能 const allCheck = (selected)=>{ cartList.value.forEach((item)=>{ return item.selected = selected }) } // 是否全选 const isAll = computed(()=>{ return cartList.value.every((item)=>{ return item.selected }) }) ``` 然后修改页面交互,注意如果用singeChange(i, i.selected) 这段表达式,直接算出两个参数的当前旧值,把这两个固定的值缓存下来;selected数据无法更新,所以要用箭头函数二次包裹(selected)=>singeChange(i,selected) ``` ``` ### 未登录的本地购物车和登陆后购物车的合并 cartStore ``` // 封装购物车模块 import {defineStore} from 'pinia' import {computed, ref} from 'vue' import { useUserStore } from './user' import { delCartAPI, findNewCartListAPI, insertCartAPI } from '@/apis/cart' export const useCartStore = defineStore('cart',()=>{ const useStore = useUserStore() const isLogin = computed(()=>{ return useStore.userInfo.token }) // 1.定义state const cartList = ref([]) // 2.定义action // 获取更新购物车列表 const updateNewList = async()=>{ const res = await findNewCartListAPI() cartList.value = res.data.result } // 添加购物车数据 const addCart = async(goods) => { const {skuId,count} = goods if (isLogin.value) { // 登录之后的逻辑 await insertCartAPI({skuId,count}) updateNewList() }else { // 添加购物车操作 // 判断有没有加入过购物车,加入过就数量加一,没有再添加 const item = cartList.value.find((item)=>{ return goods.skuId === item.skuId }) if(item) { item.count++ }else { cartList.value.push(goods) } } } // 删除购物车数据 const delCart = async(skuId) => { if(isLogin.value) { // 登录后删除购物车数据 await delCartAPI([skuId]) updateNewList() }else{ const idx = cartList.value.findIndex((item) => { return skuId === item.skuId }) cartList.value.splice(idx,1) } } // 清除购物车 const clearCart = ()=> { cartList.value = [] } // 单选功能 const singleCheck = (skuId,selected) => { const item = cartList.value.find((item)=> { return item.id === skuId }) item.selected = selected } // 全选功能 const allCheck = (selected)=>{ cartList.value.forEach((item)=>{ return item.selected = selected }) } // 3.计算属性 // 1.总的数量 const allCount = computed(()=>{ return cartList.value.reduce((a,c)=> a + c.count,0) }) // 2.总的价格 const allPrice = computed(()=>{ return cartList.value.reduce((a,c)=> a + c.count * c.price ,0) }) // 3.已勾选的数量 const selectedCount = computed(()=>{ return cartList.value.filter((item)=>{ return item.selected }).reduce((a,c)=>{ return a + c.count },0) }) // 4.已勾选的总价格 const selectedPrice = computed(()=>{ return cartList.value.filter((item)=>{ return item.selected }).reduce((a,c)=>{ return a + c.count * c.price },0) }) // 是否全选 const isAll = computed(()=>{ return cartList.value.every((item)=>{ return item.selected }) }) return { updateNewList, cartList, addCart, delCart, clearCart, singleCheck, allCheck, allCount, allPrice, isAll, selectedCount, selectedPrice } },{ persist:true // 开启持久化 }) ``` userStore ``` // 管理用户相关数据 import { defineStore } from "pinia"; import {ref} from 'vue' import { loginAPI } from "@/apis/user"; import { useCartStore } from "./cartStore"; import {mergeCartAPI} from '@/apis/cart' export const useUserStore = defineStore('user',()=>{ const cartStore = useCartStore() // 1.定义管理用户数据的state const userInfo = ref({}) // 2.定义获取接口数据的action函数 const getUserInfo = async({account,password}) => { const res = await loginAPI({account,password}) userInfo.value = res.data.result // 合并购物车操作 await mergeCartAPI(cartStore.cartList.map((item)=>{ return { skuId:item.skuId, selected:item.selected, count:item.count } })) cartStore.updateNewList() } // 3.退出时清除用户信息 const clearUserInfo = () => { userInfo.value = {} // 退出登录清除购物车数据 cartStore.clearCart() } // 4.return出state和action return { userInfo, getUserInfo, clearUserInfo } },{ persist:true // 开启持久化 }) ``` ## 订单页面 ### 打开切换地址弹窗 ``` ``` ## Pay页面 & PayBack页面 ### 支付流程 ![支付流程](./picture/支付流程.png "支付流程") 原本逻辑需要结合支付宝沙箱远端接口,逻辑如下 ``` ``` ## SKU组件详解 路径demo:src\components\SKU