黑马程序员前端Vue3小兔鲜电商项目实战,vue3全家桶从入门到实战电商项目一套通关-飞行的贝克-稍后再看-哔哩哔哩视频 (bilibili.com)
全部视频标题
- Day1-01.Vue3小兔鲜先导课
- Day1-02.认识Vue3
- Day1-03.使用create-vue创建项目
- Day1-04.熟悉项目目录和关键文件
- Day1-05.组合式API入口-setup
- Day1-06.组合式API-reactive和ref函数
- Day1-07.组合式API-computed
- Day1-08.组合式API-watch-基本使用和立即执行
- Day1-09.组合式API-watch-深度侦听和精确侦听
- Day1-10.组合式API-生命周期函数
- Day1-11.组合式API下的父子通信-父传子
- Day1-12.组合式API下的父子通信-子传父
- Day1-13.组合式API-模版引用
- Day1-14.组合式API-provide和inject
- Day1-15.Vue3综合小案例
- Day1-16.补充作业-编辑功能作业
- Day2-01.Pinia-添加pinia到vue项目
- Day2-02.Pinia-counter基础使用
- Day2-03.Pinia-getters和异步action
- Day2-04.Pinia-storeToRefs和调试
- Day2-05.项目起步-项目初始化和git管理
- Day2-06.项目起步-别名路径联想设置
- Day2-07.项目起步-elementPlus自动按需导入配置
- Day2-08.项目起步-elementPlus主题色定制
- Day2-09.项目起步-axios基础配置
- Day2-10.项目起步-项目整体路由设计
- Day2-11.项目起步-静态资源引入和ErrorLen安装
- Day2-12.项目起步-scss文件的自动导入
- Day2-13.Layout-静态模版结构搭建
- Day2-14.Layout-字体图标引入
- Day2-15.Layout-一级导航渲染
- Day2-16.Layout-吸顶导航交互实现
- Day2-17.Layout-Pinia优化重复请求
- Day3-01.Home-整体结构拆分和分类实现
- Day3-02.Home-banner轮播图实现
- Day3-03.Home-面板组件封装
- Day3-04.Home-新鲜好物业务实现
- Day3-05.Home-图片懒加载指令实现
- Day3-06.Home-懒加载指令优化
- Day3-07.Home-Product产品列表实现
- Day3-08.Home-GoodsItem组件封装
- Day3-09.一级分类-整体认识和路由配置
- Day3-10.一级分类-面包屑导航渲染
- Day3-11.一级分类-轮播图功能实现
- Day3-12.一级分类-激活状态控制和分类列表渲染
- Day3-13.一级分类-解决路由缓存问题
- Day3-14.一级分类-使用逻辑函数拆分业务
- Day4-01.二级分类-整体认识和路由配置
- Day4-02.二级分类-面包屑导航实现
- Day4-03.二级分类-基础商品列表实现
- Day4-04.二级分类-列表筛选功能实现
- Day4-05.二级分类-列表无限加载实现
- Day4-06.二级分类-定制路由滚动行为
- Day4-07.详情页-整体认识和路由配置
- Day4-08.详情页-基础数据渲染
- Day4-09.详情页-热榜区-基础组件封装和数据渲染
- Day4-10.详情页-热榜区-适配不同title和数据列表
- Day4-11.详情页-图片预览组件-小图切换大图显示
- Day4-12.详情页-图片预览组件-放大镜-滑块跟随移动
- Day4-13.详情页-图片预览组件-放大镜-大图效果实现
- Day4-14.详情页-图片预览组件-props适配和整体总结
- Day4-15.详情页-SKU组件熟悉使用
- Day4-16.详情页-通用组件统一注册全局
- Day5-01.登录-整体认识和路由配置
- Day5-02.登录-表单校验实现
- Day5-03.登录-表单校验-自定义校验规则
- Day5-04.登录-表单校验-统一校验
- Day5-05.登录-基础功能实现
- Day5-06.登录-Pinia管理用户数据
- Day5-07.登录-Pinia用户数据持久化
- Day5-08.登录-登录和非登录状态下的模版适配
- Day5-09.登录-请求拦截器携带Token
- Day5-10.登录-退出登录功能实现
- Day5-11.登录-Token失效401拦截处理
- Day5-12.购物车-流程梳理和本地加入购物车实现
- Day5-13.购物车-本地-头部购物车列表渲染
- Day5-14.购物车-本地-头部购物车删除功能实现
- Day5-15.购物车-本地-头部购物车统计计算
- Day6-01.购物车-本地-列表购物车基础数据渲染
- Day6-02.购物车-本地-列表购物车单选功能
- Day6-03.购物车-本地-购物车列表全选功能
- Day6-04.购物车-本地-购物车列表统计数据实现
- Day6-05.购物车-接口-加入购物车
- Day6-06.购物车-接口-删除购物车
- Day6-07.退出登录-清空购物车数据
- Day6-08.购物车-合并本地购物车到服务器
- Day6-09.结算-路由配置和基础数据渲染
- 后面没做了
vue3 + vite + element-plus 快速入门教程(小兔鲜电商项目)
项目搭建
创建项目
npm create vue@lastest
配置@
jsconfig.json
{
"compilerOptions": {
"baseUrl": ".", // 设置相对根路径
"paths": {
"@/*": ["src/*"] // 将 @ 指向 src 目录
}
}
}
vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path"; // Node.js 的 path 模块
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver({ importStyle: "sass" })],
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"), // 将 @ 指向 src 目录
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: // 自动导入
`
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
},
},
},
});
// .vscode/settings.json
{
"path-intellisense.mappings": {
"@": "${workspaceFolder}/src" // 配置路径映射,@ 代表 src 目录
},
"typescript.preferences.importModuleSpecifier": "shortest"
}
安装依赖
以下是依赖项的详细表格说明:
依赖名称 | 分类 | 功能描述 |
---|---|---|
axios | 运行依赖 | 用于发送 HTTP 请求(如 GET、POST)与后端交互。 |
element-plus | 运行依赖 | Vue 3 的 UI 组件库,包含各种现代化组件(如表单、按钮、表格)。 |
pinia | 运行依赖 | Vue 3 的状态管理库,简单、轻量且功能强大。 |
pinia-plugin-persistedstate | 运行依赖 | Pinia 的持久化插件,用于将状态存储到 localStorage 或 sessionStorage 。 |
vue-router | 运行依赖 | Vue.js 官方路由库,支持路由管理与页面导航。 |
sass | 开发依赖 | CSS 预处理器,支持嵌套规则、变量、函数等特性。 |
unplugin-auto-import | 开发依赖 | 自动导入工具,无需手动导入 Vue、Router 等模块,提高开发效率。 |
unplugin-vue-components | 开发依赖 | 自动组件导入插件,自动解析 Vue 项目中使用的组件并引入对应代码。 |
vite | 开发依赖 | 现代化构建工具,支持快速开发、热更新和高效的生产环境构建。 |
vue-use | 开发依赖 | Vite 的 Vue 插件,用于支持 .vue 文件的解析和功能。 |
项目目录结构
VUE-RABBIT
├── .idea # 存储 jetbrain 的项目配置文件
├── .vscode # 存储 Visual Studio Code 编辑器的项目配置文件
├── node_modules # 包含通过 npm 或 yarn 安装的项目依赖包
├── public # 静态资源目录,通常存放不会被 Webpack 或 Vite 打包的文件,如 favicon
├── src # 项目的源码目录
│ ├── apis # 存放与后端交互的 API 封装文件
│ ├── assets # 存放静态资源,例如图片、字体等
│ ├── components # 可复用的 Vue 组件集合
│ ├── composables # 存放 Vue 3 的 Composition API 相关的逻辑封装文件
│ ├── directives # 定义自定义指令的文件夹
│ ├── router # 路由配置文件目录,用于管理页面导航
│ ├── stores # 状态管理文件夹,例如使用 Vuex 或 Pinia
│ ├── styles # 全局样式文件目录,存放 CSS 或 SCSS 文件
│ ├── utils # 实用工具函数和通用方法
│ ├── views # 页面组件目录,通常对应路由的视图组件
│ ├── App.vue # 根组件,Vue 应用的入口文件
│ ├── main.js # 应用的主入口文件,用于初始化 Vue 实例并挂载
│ └── style.css # 全局样式文件
├── .gitignore # Git 配置文件,定义需要忽略提交到版本控制的文件或目录
├── index.html # 应用的 HTML 模板文件,Vite 会在构建时注入脚本和样式
├── jsconfig.json # JavaScript 配置文件,支持路径别名和代码提示
├── package-lock.json # 锁定项目依赖的具体版本,确保一致性
├── package.json # 项目描述文件,定义依赖、脚本以及项目信息
├── README.md # 项目的说明文档,通常用于记录项目的简介和使用方法
└── vite.config.js # Vite 的配置文件,定义开发环境和构建的相关配置
如果按照每个页面维护自己的目录,则view
下面每个文件夹有自己的components
和composables
文件夹📂
引入
main.js
// 引入 Vue 的 createApp 方法用于创建应用实例
import { createApp } from "vue";
// 引入 Pinia,用于状态管理
import { createPinia } from "pinia";
// 引入全局样式文件
import "@/styles/common.scss";
// 引入主应用组件
import App from "./App.vue";
// 引入路由配置文件
import router from "@/router/index";
// 引入自定义指令插件(如图片懒加载)
import { lazyPlugin } from "@/directives";
// 引入全局组件插件
import { componentPlugin } from "@/components";
// 引入 Pinia 的持久化插件,用于持久化状态
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
// 创建 Vue 应用实例
const app = createApp(App);
// 创建 Pinia 实例并使用持久化插件
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// 注册 Pinia 到应用实例
app.use(pinia);
// 注册路由到应用实例
app.use(router);
// 注册自定义指令插件(如懒加载)
app.use(lazyPlugin);
// 注册全局组件插件
app.use(componentPlugin);
// 挂载应用到页面中的 #app 容器
app.mount("#app");
依赖技术
Element-Plus-UI
axios 使用
utils/http.js
:封装📦axios
// axios 基础的封装
import axios from "axios";
import { ElMessage } from 'element-plus';
import 'element-plus/theme-chalk/el-message.css'
import { useUserStore } from "@/stores/user";
import router from "@/router"; // 和.vue中不同,这是.js文件
const httpInstance = axios.create({
baseURL:'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 5000
})
// 拦截器
//请求拦截器
// 在登录状态下,才能请求api数据
httpInstance.interceptors.request.use(config => {
// 获取token
const userStore = useUserStore()
const token = userStore.userInfo.token
// 在请求头中拼接token
if(token){
config.headers.Authorization = `Bearer ${token}`
}
return config
},e=>Promise.reject(e))
// 响应式拦截器
httpInstance.interceptors.response.use(res => res.data,e => {
console.log(e);
// 错误处理
ElMessage({
type:'warning',
message:e?.response.data.message
})
// token 校验失败:token过期,错误
if(e?.response.status === 401){
const userStore = useUserStore()
userStore.clearUserInfo()
// 跳转到登录页面
router.push('/login')
}
return Promise.reject(e)
})
export default httpInstance
api\category.js
import httpInstance from "@/utils/http";
/**
* @description 获取分类 ,https:// category?id=1005000
* @param {*} id // `category/${id}`
* @returns
*/
export function getCategoryAPI(id){
return httpInstance({
url:'/category',
params:{
id
}
})
}
/**
* @description: 获取分类下的小分类
*
* @param id
*/
export const getCategoryFilterAPI = (id) => {
return httpInstance({
url:'/category/sub/filter',
params:{
id
}
})
}
/**
* @description: 获取导航数据
* @data {
categoryId: 1005000 ,
page: 1,
pageSize: 20,
sortField: 'publishTime' | 'orderNum' | 'evaluateNum'
}
* @return {*}
*/
export const getSubCategoryAPI = (data) => {
return httpInstance({
url:'/category/goods/temporary',
method:'POST',
data
})
}
/**
* 获取热榜商品
* @param {Number} id - 商品id
* @param {Number} type - 1代表24小时热销榜 2代表周热销榜
* @param {Number} limit - 获取个数
*/
export const getHotGoodsAPI = ({ id, type, limit = 3 }) => {
return httpInstance({
url:'/goods/hot',
params:{
id,
type,
limit
}
})
}
apis\layout.js
import httpInstance from "@/utils/http";
export function getCategoryAPI() {
return httpInstance({
url: "/home/category/head",
});
}
// 轮播图
export function getBannerAPI(param = {}){
const {distributionSite = '1'} = param
return httpInstance({
url:'/home/banner', //默认 method 为 get
params: {
distributionSite
}
})
}
/**
* @description: 获取新鲜好物
* @param {*}
* @return {Promise} A promise that resolves with the response from the API.
*/
export function getNewAPI(){
return httpInstance({
url:'/home/new',
})
}
export function getHotAPI(){
return httpInstance({
url:'/home/hot',
})
}
export function getGoodsAPI(){
return httpInstance({
url:'/home/goods'
})
}
调用这些API要求async function ; awati xxAPI()
路由 Vue-Router 使用
import {createWebHashHistory, createRouter} from "vue-router";
const routes = [
{
path: "/", // 一级路由首页显示布局 Layout
component: () => import("@/views/Layout/index.vue"),
children: [
{
// 二级路由 默认显示 Home 页面
path: "",
component: () => import("@/views/Home/index.vue"),
},
{
path: "category/:id",
component: () => import("@/views/Category/index.vue"),
},
{ // 二级分类模块
path: "category/sub/:id",
component: () => import("@/views/SubCategory/index.vue"),
},
{
path: "detail/:id",
component: () => import("@/views/Detail/index.vue")
}
],
},
{
path: "/login",
component: () => import("@/views/Login/index.vue"),
},
];
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return {top: 0}
},
});
export default router;
[!info] path参数
/
代表根路经下面的
无/
,类似“catagory/:id”
相对于父下面
先在views\Layout.vue
下面👇,划分布局
<template>
<LayoutFixed></LayoutFixed>
<NavLayout></NavLayout>
<HeaderLayout></HeaderLayout>
<!-- 二级路由:这里负责主要内容显示 -->
<RouterView></RouterView>
<FooterLayout></FooterLayout>
</template>
[!info] 什么
export default 其他文件 : import xxx from
export 其他文件: import { } from
import {useRoute} from "vue-router";
const route = useRoute()
<RouterLink :to="`/detail/${item.id}`" v-for="item in hotGoods" :key="item.id">
<img :src="item.picture" alt=""/>
<p class="name ellipsis">{{ item.name }}</p>
<p class="desc ellipsis">{{ item.desc }}</p>
<p class="price">¥{{ item.price }}</p>
</RouterLink>
views
├── Category
│ ├── composables
│ │ ├── useBanner.js
│ │ ├── useCategory.js
│ ├── index.vue
├── Detail
│ ├── components
│ │ ├── HotGoods.vue
│ ├── index.vue
├── Home
├── Layout
│ ├── components
│ ├── index.vue
├── Login
├── SubCategory
每个页面单独一个文件夹📂,每个页面维护自己
composables 存放Hook Hook
components 存放组件
route.params.id
是在基于 JavaScript 的前端框架(例如 Vue.js、React Router 等)中经常使用的一种表达方式,用于获取路由参数中的 id
值。
以下是它的含义和使用场景:
- 路由参数的获取
route
:通常表示当前路由对象,包含了与当前路径相关的所有信息。params
:表示路由路径中定义的动态参数。id
:表示动态参数的键,通常用来标识某个具体的值(例如某个用户的 ID、文章的 ID 等)。
- 常见使用场景
当定义一个带有动态参数的路由时,例如:
// Vue Router 路由配置
const routes = [
{
path: '/user/:id',
component: UserDetail,
},
];
在这个路由中,:id
是一个动态参数。
如果访问路径为 /user/123
,route.params.id
将会返回 123
。
Pinia 使用
stores\categoryStore.js
import { ref } from "vue";
import { defineStore } from 'pinia'
import { getCategoryAPI } from "@/apis/layout";
// 使用 pinia 全局管理category数据
export const useCategoryStore = defineStore("category", () => {
// state
const categoryList = ref([]);
//action 请求数据
const getCategory = async () => {
const res = await getCategoryAPI();
categoryList.value = res.result;
};
return { categoryList, getCategory };
});
import { useCategoryStore } from '@/stores/categoryStore';
const categoryStore = useCategoryStore();
// 使用 pinia 管理用户登陆信息
// pinia state 保存了用户数据,全局可用
// action 来登录逻辑,获取用户数据,存储数据到 state
import { defineStore } from "pinia";
import { ref } from "vue";
import { loginAPI } from "@/apis/user";
export const useUserStore = defineStore(
"user",
() => {
// state
const userInfo = ref({});
// action
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password });
userInfo.value = res.result;
};
// 处理退出的逻辑
const clearUserInfo = () => {
userInfo.value = {};
};
// 返回
return {
userInfo,
getUserInfo,
clearUserInfo
};
},
{ // 插件会自动持久化和删除loaclStorage中的数据
persist: true,
}
);
vue-use 和 自定义指令
directives\index.js
// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install(app, options) {
// 配置此应用
// 懒加载自定义指令的实现:当浏览器视口到达图片时候才发送网络图片请求
app.directive("img-lazy", {
mounted(el, binding) {
// el 是dom : img ; binding.value ; 属性 item.picture
const { stop } = useIntersectionObserver(
el, //观测的元素dom
([{ isIntersecting }]) => {
console.log(isIntersecting);
if (isIntersecting) {
el.src = binding.value;
stop() //当加载一次后,停掉 监听,减小性能损耗
}
}
);
},
});
},
};
使用自定义指令
<RouterLink to="/" class="goods-item">
<img v-img-lazy="good.picture" alt="" />
<p class="name ellipsis">{{ good.name }}</p>
<p class="desc ellipsis">{{ good.desc }}</p>
<p class="price">¥{{ good.price }}</p>
</RouterLink>
[Plugins | Vue.js (vuejs.org)](https://vuejs.org/guide/reusability/plugins.html
Hook 封装逻辑和数据
// hooks封装,轮播图逻辑和数据
import { getBannerAPI } from '@/apis/layout';
import {ref,onMounted} from "vue";
export function useBanner(){
// 获取 banner
const bannerList = ref([]);
const getBannerList = async () => {
const res = await getBannerAPI({ distributionSite: '2' });
bannerList.value = res.result;
}
onMounted(getBannerList)
return {bannerList}
}
import { onMounted, ref } from 'vue';
import { getCategoryAPI } from '@/apis/category';
import { useRoute,onBeforeRouteUpdate } from 'vue-router';
export function useCategory(){
const category = ref({})
const route = useRoute() // 当前路由,返回当前的路由地址
// 传进去当前分类的 id
const getCategory = async (id = route.params.id ) => {
const res = await getCategoryAPI(id)
category.value = res.result
}
onMounted(getCategory)
// 路由参数更新时重新获取分类数据
onBeforeRouteUpdate((to)=>{
getCategory(to.params.id)
})
return {
category
}
}
hook 向外提供轮播图的数据
外部使用
<script setup>
import {useBanner} from "@/views/Category/composables/useBanner.js";
const {bannerList} = useBanner()
</script>
<template>
<!-- 切换图片 -->
<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>
</template>