Vue3+ElementPlus+Pinia开发小兔鲜电商项目完整教程(附代码资料)主要内容讲述:认识Vue3,使用create-vue搭建Vue3项目1. Vue3组合式API体验,2. Vue3更多的优势,1. 认识create-vue,2. 使用create-vue创建项目,1. setup选项的写法和执行时机,2. setup中写代码的特点。什么是pinia,创建空Vue项目并安装Pinia1. 安装elementPlus和自动导入插件,2. 配置自动按需导入,3. 测试组件,1. 安装sass,2. 准备定制化的样式文件,3. 自动导入配置。静态结构搭建和分类实现,banner轮播图实现1. 整体结构创建,2. 分类实现,1. 熟悉组件,2. 获取数据渲染组件,1. 纯静态结构,2. 完整代码。静态结构搭建和路由配置,面包屑导航渲染1. 准备分类组件,2. 配置路由,3. 配置导航区域链接,1. 认识组件准备模版,2. 封装接口,3. 渲染面包屑导航。整体认识和路由配置,渲染基础数据1. 准备组件模版,2. 配置路由,3. 绑定模版测试跳转,1. 封装接口,2. 获取数据渲染模版,1. 渲染基础热榜数据。整体认识和路由配置,表单校验实现1. 准备模版,2. 配置路由跳转,1. 校验要求,2. 代码实现。本地购物车,接口购物车1. 添加购物车,2. 头部购物车,3. 列表购物车-基础内容渲染,4. 列表购物车-单选功能实现,5. 列表购物车-全选功能实现,6. 列表购物车-统计数据功能实现。路由配置和基础数据渲染,切换地址-打开弹框交互1. 准备组件模版,2. 配置路由,3. 封装接口,4. 渲染数据,1. 准备弹框模版,2. 控制弹框打开。![image.png](,基础数据渲染1. 准备接口,2. 获取数据渲染内容,1. 支付携带参数,2. 沙箱账号信息,1. 准备模版,2. 绑定路由。Sku组件封装1. 准备模版渲染规格数据,2. 选中和取消选中实现,3. 规格禁用功能实现,4. 产出Prop数据。
全套笔记资料代码移步: 前往gitee仓库查看
感兴趣的小伙伴可以自取哦,欢迎大家点赞转发~
全套教程部分目录:
部分文件图片:
静态结构搭建和路由配置
1. 准备分类组件
<script setup>
</script>
<template>
<div class='top-category'>
我是分类
</div>
</template>
2. 配置路由
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'layout',
component: Layout,
children: [
{
path: '',
name: 'home',
component: Home
},
{
path: 'category/:id',
name: 'category',
component: Category
}
]
},
{
path: '/login',
name: 'login',
component: Login
},
]
})
export default router
3. 配置导航区域链接
<li v-for="item in categoryStore.categoryList" :key="item.id">
<RouterLink active-class="active" :to="`/category/${item.id}`">
{{ item.name }}
</RouterLink>
</li>
面包屑导航渲染
1. 认识组件准备模版
<script setup>
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>居家</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.top-category {
h3 {
font-size: 28px;
color: #666;
font-weight: normal;
text-align: center;
line-height: 100px;
}
.sub-list {
margin-top: 20px;
background-color: #fff;
ul {
display: flex;
padding: 0 32px;
flex-wrap: wrap;
li {
width: 168px;
height: 160px;
a {
text-align: center;
display: block;
font-size: 16px;
img {
width: 100px;
height: 100px;
}
p {
line-height: 40px;
}
&:hover {
color: $xtxColor;
}
}
}
}
}
.ref-goods {
background-color: #fff;
margin-top: 20px;
position: relative;
.head {
.xtx-more {
position: absolute;
top: 20px;
right: 20px;
}
.tag {
text-align: center;
color: #999;
font-size: 20px;
position: relative;
top: -20px;
}
}
.body {
display: flex;
justify-content: space-around;
padding: 0 40px 30px;
}
}
.bread-container {
padding: 25px 0;
}
}
</style>
2. 封装接口
import request from '@/utils/request'
/**
* @description: 获取分类数据
* @param {*} id 分类id
* @return {*}
*/
export const getTopCategoryAPI = (id) => {
return request({
url:'/category',
params:{
id
}
})
}
3. 渲染面包屑导航
<script setup>
import { findTopCategoryAPI } from '@/apis/category'
const categoryData = ref({})
const route = useRoute()
const getCategory = async (id) => {
// 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route
const res = await findTopCategoryAPI(id)
categoryData.value = res.result
}
getCategory(route.params.id)
</script>
<template>
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
分类Banner渲染
1. 适配接口
export function getBannerAPI (params = {}) {
// 默认为1 商品为2
const { distributionSite = '1' } = params
return httpInstance({
url: '/home/banner',
params: {
distributionSite
}
})
}
2. 迁移首页Banner逻辑
<script setup>
// 部分代码省略
import { getBannerAPI } from '@/apis/home'
// 获取banner
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: '2'
})
console.log(res)
bannerList.value = res.result
}
onMounted(() => getBanner())
</script>
<template>
<div class="top-category">
<div class="container m-top-20">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 轮播图 -->
<div class="home-banner">
<el-carousel height="500px">
<el-carousel-item v-for="item in bannerList" :key="item.id">
<img :src="item.imgUrl" alt="">
</el-carousel-item>
</el-carousel>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
// 部分代码省略
.home-banner {
width: 1240px;
height: 500px;
margin: 0 auto;
img {
width: 100%;
height: 500px;
}
}
</style>
导航激活设置分类列表渲染
1. 导航激活状态设置
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
2. 分类数据模版
<div class="sub-list">
<h3>全部分类</h3>
<ul>
<li v-for="i in categoryData.children" :key="i.id">
<RouterLink to="/">
<img :src="i.picture" />
<p>{{ i.name }}</p>
</RouterLink>
</li>
</ul>
</div>
<div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
<div class="head">
<h3>- {{ item.name }}-</h3>
</div>
<div class="body">
<GoodsItem v-for="good in item.goods" :goods="good" :key="good.id" />
</div>
</div>
路由缓存问题解决
缓存问题:当路由path一样,参数不同的时候会选择直接复用路由对应的组件 解决方案: 1. 给 routerv-view 添加key属性,破坏缓存 2. 使用 onBeforeRouteUpdate钩子函数,做精确更新
基于业务逻辑的函数拆分
基本思想:把组件内独立的业务逻辑通过
useXXX
函数做封装处理,在组件中做组合使用
![image.png](
// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { getCategoryAPI } from '@/apis/category'
import { useRoute } from 'vue-router'
import { 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.result
}
onMounted(() => getCategory())
// 目标:路由参数变化的时候 可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 存在问题:使用最新的路由参数请求最新的分类数据
getCategory(to.params.id)
})
return {
categoryData
}
}
// 封装banner轮播图相关的业务代码
import { ref, onMounted } from 'vue'
import { getBannerAPI } from '@/apis/home'
export function useBanner () {
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: '2'
})
console.log(res)
bannerList.value = res.result
}
onMounted(() => getBanner())
return {
bannerList
}
}
<script setup>
import GoodsItem from '../Home/components/GoodsItem.vue'
import { useBanner } from './composables/useBanner'
import { useCategory } from './composables/useCategory'
const { bannerList } = useBanner()
const { categoryData } = useCategory()
</script>
本资源由 收集整理
整体业务认识和路由配置
1. 准备组件模版
<script setup>
</script>
<template>
<div class="container ">
<!-- 面包屑 -->
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/' }">居家
</el-breadcrumb-item>
<el-breadcrumb-item>居家生活用品</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="sub-container">
<el-tabs>
<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">
<!-- 商品列表-->
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.bread-container {
padding: 25px 0;
color: #666;
}
.sub-container {
padding: 20px 10px;
background-color: #fff;
.body {
display: flex;
flex-wrap: wrap;
padding: 0 10px;
}
.goods-item {
display: block;
width: 220px;
margin-right: 20px;
padding: 20px 30px;
text-align: center;
img {
width: 160px;
height: 160px;
}
p {
padding-top: 10px;
}
.name {
font-size: 16px;
}
.desc {
color: #999;
height: 29px;
}
.price {
color: $priceColor;
font-size: 20px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
}
</style>
2. 配置路由关系
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'
import SubCategory from '@/views/SubCategory/index.vue'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'layout',
component: Layout,
children: [
{
path: '',
name: 'home',
component: Home
},
{
path: 'category/:id',
name: 'category',
component: Category
},
{
path: 'category/sub/:id',
name: 'subCategory',
component: SubCategory
},
]
},
{
path: '/login',
name: 'login',
component: Login
},
]
})
export default router
3. 跳转配置
<div class="sub-list">
<h3>全部分类</h3>
<ul>
<li v-for="i in categoryData.children" :key="i.id">
<RouterLink :to="`/category/sub/${i.id}`">
<img :src="i.picture" />
<p>{{ i.name }}</p>
</RouterLink>
</li>
</ul>
</div>
面包屑导航实现
1. 准备接口
/**
* @description: 获取二级分类列表数据
* @param {*} id 分类id
* @return {*}
*/
export const getCategoryFilterAPI = (id) => {
return request({
url:'/category/sub/filter',
params:{
id
}
})
}
2. 获取数据渲染模版
<script setup>
import { getCategoryFilterAPI } from '@/apis/category'
// 获取面包屑导航数据
const filterData = ref({})
const getFilterData = async () => {
const res = await getCategoryFilterAPI(route.params.id)
filterData.value = res.result
}
getFilterData()
</script>
<template>
<div class="bread-container">
<el-breadcrumb separator=">">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: `/category/${filterData.parentId}` }">{{ filterData.parentName }}
</el-breadcrumb-item>
<el-breadcrumb-item>{{ filterData.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
分类基础列表实现
1. 准备接口
/**
* @description: 获取导航数据
* @data {
categoryId: 1005000 ,
page: 1,
pageSize: 20,
sortField: 'publishTime' | 'orderNum' | 'evaluateNum'
}
* @return {*}
*/
export const getSubCategoryAPI = (data) => {
return request({
url:'/category/goods/temporary',
method:'POST',
data
})
}
2. 获取数据列表
<script setup>
// 获取基础列表数据渲染
const goodList = ref([])
const reqData = ref({
categoryId: route.params.id,
page: 1,
pageSize: 20,
sortField: 'publishTime'
})
const getGoodList = async () => {
const res = await getSubCategoryAPI(reqData.value)
console.log(res)
goodList.value = res.result.items
}
onMounted(() => getGoodList())
</script>
列表筛选实现
思路:tab组件切换时修改reqData中的sortField字段,重新拉取接口列表
<script setup>
// tab切换回调
const tabChange = () => {
console.log('tab切换了', reqData.value.sortField)
reqData.value.page = 1
getGoodList()
}
</script>
<template>
<el-tabs v-model="reqData.sortField" @tab-change="tabChange">
<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>
</template>
无限加载实现
基础思路 1. 触底条件满足之后 page++,拉取下一页数据 2. 新老数据做数组拼接 3. 判断是否已经全部加载完毕,停止监听
// 加载更多
const disabled = ref(false)
const load = async () => {
console.log('加载更多数据咯')
// 获取下一页的数据
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
}
}