vue3 + vite + element-plus 快速入门教程(小兔鲜电商项目)

黑马程序员前端Vue3小兔鲜电商项目实战,vue3全家桶从入门到实战电商项目一套通关-飞行的贝克-稍后再看-哔哩哔哩视频 (bilibili.com)

全部视频标题

  1. Day1-01.Vue3小兔鲜先导课
  2. Day1-02.认识Vue3
  3. Day1-03.使用create-vue创建项目
  4. Day1-04.熟悉项目目录和关键文件
  5. Day1-05.组合式API入口-setup
  6. Day1-06.组合式API-reactive和ref函数
  7. Day1-07.组合式API-computed
  8. Day1-08.组合式API-watch-基本使用和立即执行
  9. Day1-09.组合式API-watch-深度侦听和精确侦听
  10. Day1-10.组合式API-生命周期函数
  11. Day1-11.组合式API下的父子通信-父传子
  12. Day1-12.组合式API下的父子通信-子传父
  13. Day1-13.组合式API-模版引用
  14. Day1-14.组合式API-provide和inject
  15. Day1-15.Vue3综合小案例
  16. Day1-16.补充作业-编辑功能作业
  17. Day2-01.Pinia-添加pinia到vue项目
  18. Day2-02.Pinia-counter基础使用
  19. Day2-03.Pinia-getters和异步action
  20. Day2-04.Pinia-storeToRefs和调试
  21. Day2-05.项目起步-项目初始化和git管理
  22. Day2-06.项目起步-别名路径联想设置
  23. Day2-07.项目起步-elementPlus自动按需导入配置
  24. Day2-08.项目起步-elementPlus主题色定制
  25. Day2-09.项目起步-axios基础配置
  26. Day2-10.项目起步-项目整体路由设计
  27. Day2-11.项目起步-静态资源引入和ErrorLen安装
  28. Day2-12.项目起步-scss文件的自动导入
  29. Day2-13.Layout-静态模版结构搭建
  30. Day2-14.Layout-字体图标引入
  31. Day2-15.Layout-一级导航渲染
  32. Day2-16.Layout-吸顶导航交互实现
  33. Day2-17.Layout-Pinia优化重复请求
  34. Day3-01.Home-整体结构拆分和分类实现
  35. Day3-02.Home-banner轮播图实现
  36. Day3-03.Home-面板组件封装
  37. Day3-04.Home-新鲜好物业务实现
  38. Day3-05.Home-图片懒加载指令实现
  39. Day3-06.Home-懒加载指令优化
  40. Day3-07.Home-Product产品列表实现
  41. Day3-08.Home-GoodsItem组件封装
  42. Day3-09.一级分类-整体认识和路由配置
  43. Day3-10.一级分类-面包屑导航渲染
  44. Day3-11.一级分类-轮播图功能实现
  45. Day3-12.一级分类-激活状态控制和分类列表渲染
  46. Day3-13.一级分类-解决路由缓存问题
  47. Day3-14.一级分类-使用逻辑函数拆分业务
  48. Day4-01.二级分类-整体认识和路由配置
  49. Day4-02.二级分类-面包屑导航实现
  50. Day4-03.二级分类-基础商品列表实现
  51. Day4-04.二级分类-列表筛选功能实现
  52. Day4-05.二级分类-列表无限加载实现
  53. Day4-06.二级分类-定制路由滚动行为
  54. Day4-07.详情页-整体认识和路由配置
  55. Day4-08.详情页-基础数据渲染
  56. Day4-09.详情页-热榜区-基础组件封装和数据渲染
  57. Day4-10.详情页-热榜区-适配不同title和数据列表
  58. Day4-11.详情页-图片预览组件-小图切换大图显示
  59. Day4-12.详情页-图片预览组件-放大镜-滑块跟随移动
  60. Day4-13.详情页-图片预览组件-放大镜-大图效果实现
  61. Day4-14.详情页-图片预览组件-props适配和整体总结
  62. Day4-15.详情页-SKU组件熟悉使用
  63. Day4-16.详情页-通用组件统一注册全局
  64. Day5-01.登录-整体认识和路由配置
  65. Day5-02.登录-表单校验实现
  66. Day5-03.登录-表单校验-自定义校验规则
  67. Day5-04.登录-表单校验-统一校验
  68. Day5-05.登录-基础功能实现
  69. Day5-06.登录-Pinia管理用户数据
  70. Day5-07.登录-Pinia用户数据持久化
  71. Day5-08.登录-登录和非登录状态下的模版适配
  72. Day5-09.登录-请求拦截器携带Token
  73. Day5-10.登录-退出登录功能实现
  74. Day5-11.登录-Token失效401拦截处理
  75. Day5-12.购物车-流程梳理和本地加入购物车实现
  76. Day5-13.购物车-本地-头部购物车列表渲染
  77. Day5-14.购物车-本地-头部购物车删除功能实现
  78. Day5-15.购物车-本地-头部购物车统计计算
  79. Day6-01.购物车-本地-列表购物车基础数据渲染
  80. Day6-02.购物车-本地-列表购物车单选功能
  81. Day6-03.购物车-本地-购物车列表全选功能
  82. Day6-04.购物车-本地-购物车列表统计数据实现
  83. Day6-05.购物车-接口-加入购物车
  84. Day6-06.购物车-接口-删除购物车
  85. Day6-07.退出登录-清空购物车数据
  86. Day6-08.购物车-合并本地购物车到服务器
  87. Day6-09.结算-路由配置和基础数据渲染
  88. 后面没做了

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 的持久化插件,用于将状态存储到 localStoragesessionStorage
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下面每个文件夹有自己的componentscomposables文件夹📂

引入

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">&yen;{{ 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 值。

以下是它的含义和使用场景:

  1. 路由参数的获取
  • route:通常表示当前路由对象,包含了与当前路径相关的所有信息。
  • params:表示路由路径中定义的动态参数。
  • id:表示动态参数的键,通常用来标识某个具体的值(例如某个用户的 ID、文章的 ID 等)。
  1. 常见使用场景

当定义一个带有动态参数的路由时,例如:

// Vue Router 路由配置
const routes = [
  {
    path: '/user/:id',
    component: UserDetail,
  },
];

在这个路由中,:id 是一个动态参数。

如果访问路径为 /user/123route.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">&yen;{{ 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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值