基于vue的黑马前端项目小兔鲜

目录

项目学习 

初始化项目 

建立项目 

引入elementplus

elementPlus主题设置 

配置axios

路由 

引入静态资源

自动导入scss变量

Layout页

组件结构快速搭建

 字体图标渲染

一级导航渲染 

吸顶导航交互实现 

Pinia优化重复请求

Home页 

分类实现

banner轮播图 

新鲜好物实现

人气推荐实现 

懒加载指令实现 

产品列表实现

GoodsItem组件封装 

一级分类页 

导航栏

分类Banner渲染 

导航激活 

分类列表渲染 

路由缓存问题 

业务逻辑的函数拆分 

二级分类页 

实现

定制路由的滚动行为

商品详情

路由配置

数据渲染

热榜区域 

适配热榜类型 

图片预览组件封装

sku插件

 登录页

表单

登录实现 

 登录失败的提示

 Pinia管理用户

Pinia持久化存储

登录和非登录状态

请求拦截器携带token

退出登录

token失效处理

购物车

本地加入购物车

头部购物车

购物车页面

订单页 

支付页 

会员中心 

基本页面

个人中心信息渲染

我的订单 

分页


项目学习 

视频:黑马程序员-小兔鲜

黑马资料:文档

vue调试:vue-devtools

DEV:HBuilder

初始化项目 

建立项目 

安装node

安装过node的需要查看node的版本是否大于或等于15,否则报错

 Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree.

windows下更新node需要重新安装,即覆盖原有的node。 

查看镜像地址 

npm config get registry

设置淘宝镜像地址 

npm config set registry https://registry.npm.taobao.org/ 

安装脚手架 

npm i -g @vue/cli

下载create-vue

npm install [email protected]

注意版本,或者安装最新版。

初始化项目

基于vue的黑马前端项目小兔鲜_第1张图片

安装依赖

npm install

配置别名路径

在根目录下新建jsconfig.json

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

引入elementplus

官网

// 引入插件
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'


export default defineConfig({
  plugins: [
    // 配置插件
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ]
})

vit.config.js






测试以下elementplus是否生效。

在app.vue的template下引入

Primary

 重启运行。

基于vue的黑马前端项目小兔鲜_第2张图片

测试完成。

elementPlus主题设置 

安装sass

npm i sass -D

样式文件

styles/element/index.scss

/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      // 主色
      'base': #27ba9b,
    ),
    'success': (
      // 成功色
      'base': #1dc779,
    ),
    'warning': (
      // 警告色
      'base': #ffb302,
    ),
    'danger': (
      // 危险色
      'base': #e26237,
    ),
    'error': (
      // 错误色
      'base': #cf4444,
    ),
  )
)

配置

vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
//elementplus按需导入
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: [
			//配置elementPlus采用sass样式配色系统
			ElementPlusResolver({importStyle:"sass"}),
		],
	}),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
      preprocessorOptions: {
        scss: {
          // 自动导入定制化样式文件进行样式覆盖
          additionalData: `
            @use "@/styles/element/index.scss" as *;
          `,
        }
      },
   }
})

测试

重启运行

基于vue的黑马前端项目小兔鲜_第3张图片

配置axios

安装axios

npm i axios

配置 

utils/http.js

import axios from 'axios'

// 创建axios实例
let http = axios.create({
  baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
  timeout: 5000
})

// axios请求拦截器
http.interceptors.request.use(config => {
  return config
}, e => Promise.reject(e))

// axios响应式拦截器
http.interceptors.response.use(res => res.data, e => {
  return Promise.reject(e)
})


export default http

 测试接口

api/testAPI.js下

import http from '@/utils/http'

export function getCategory () {
  return http({
    url: 'home/category/head'
  })
}

main.js下新增

//测试接口
import {getCategory} from '@/api/testAPI'
getCategory().then(res => {
	console.log(res)
})

重启运行

基于vue的黑马前端项目小兔鲜_第4张图片

路由 

views/Login/index.vue

views/Layout/index.vue

views/Home/index.vue

views/Category/index.vue

router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '@/views/login/index.vue'
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: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
		path: '/',
		component: Layout,
		children: [
			{
				path: '',
				component: Home
			},
			{
				path: 'category',
				component: Category
			}
		]
	},
	{
		path: '/login',
		component: Login
	}
  ]
})

export default router

重启输入对应url可以看到不同页面。

引入静态资源

  1. 图片资源 - 把 images 文件夹放到 assets 目录下
  2. 样式资源 - 把 common.scss 文件放到 styles 目录下

链接:https://pan.baidu.com/s/15PoJhfpPDzf_WTsakO0jmg?pwd=3kgh 
提取码:3kgh

自动导入scss变量

var.scss

$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;

 修改main.js

css: {
    preprocessorOptions: {
      scss: {
        // 自动导入scss文件
        additionalData: `
          @use "@/styles/element/index.scss" as *;
          @use "@/styles/var.scss" as *;
        `,
      }
    }
}

测试 

修改App.vue






基于vue的黑马前端项目小兔鲜_第5张图片

测试成功,还原代码。

Layout页

组件结构快速搭建

基于vue的黑马前端项目小兔鲜_第6张图片

LayoutNav.vue






LayoutHeader.vue






LayoutFooter.vue



Layout/index.vue



重启项目

基于vue的黑马前端项目小兔鲜_第7张图片

 字体图标渲染

根目录下index.html引入

  

基于vue的黑马前端项目小兔鲜_第8张图片

一级导航渲染 

api/layout.js 

import httpInstance from '@/utils/http'

export function getCategoryAPI () {
  return httpInstance({
    url: '/home/category/head'
  })
}

LayoutHeader.vue





基于vue的黑马前端项目小兔鲜_第9张图片

吸顶导航交互实现 

实现吸顶交互

components/LayoutFixed.vue






基于vue的黑马前端项目小兔鲜_第10张图片

Pinia优化重复请求

两个导航栏会分别发送一个请求,可以利用pinia只发送一个请求。

stores/category.js

import { ref } from 'vue'
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/api/layout'
export const useCategoryStore = defineStore('category', () => {
  // 导航列表的数据管理
  // state 导航列表数据
  const categoryList = ref([])

  // action 获取导航数据的方法
  const getCategory = async () => {
    const res = await getCategoryAPI()
    categoryList.value = res.result
  }

  return {
    categoryList,
    getCategory
  }
})

 layout/index.vue



layout/components/LayoutFixed.vue






layout/components/LayoutHeader.vue






基于vue的黑马前端项目小兔鲜_第11张图片

 发现只有一次请求。

Home页 

在home/components下新建5个vue文件

  • HomeCategory


  • HomeBanner


  • HomeNew


  • HomeHot


  • HomeProduct


home/index.vue



基于vue的黑马前端项目小兔鲜_第12张图片

分类实现

home/components/HomeCategory.vue






基于vue的黑马前端项目小兔鲜_第13张图片

banner轮播图 

home/components/HomeBanner.vue









获取数据 

api/home.js

import  httpInstance  from '@/utils/http'
export function getBannerAPI (){
  return httpInstance({
    url:'home/banner'
  })
}

home/components/HomeBanner.vue





 轮播图就实现了。

新鲜好物实现

home/components/HomePanel.vue







api/home添加:

/**
 * @description: 获取新鲜好物
 * @param {*}
 * @return {*}
 */
export const getNewAPI = () => {
  return httpInstance({
    url:'/home/new'
  })
}

home/components/HomeNew.vue






人气推荐实现 

api/home添加:

/**
 * @description: 获取人气推荐
 * @param {*}
 * @return {*}
 */
export const getHotAPI = () => {
  return  httpInstance({
	  url:'/home/hot'
  })
}

home/components/HomeHot.vue





基于vue的黑马前端项目小兔鲜_第14张图片

懒加载指令实现 

directives/index.js

// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'

export const directivePlugin = {
  install (app) {
    // 懒加载指令逻辑
    app.directive('img-lazy', {
      mounted (el, binding) {
        // el: 指令绑定的那个元素 img
        // binding: binding.value  指令等于号后面绑定的表达式的值  图片url
        console.log(el, binding.value)
        useIntersectionObserver(
          el,
          ([{ isIntersecting }]) => {
            console.log(isIntersecting)
            if (isIntersecting) {
              // 进入视口区域
              el.src = binding.value
              stop()
            }
          },
        )
      }
    })
  }
}

修改main.js

app.use(createPinia())
app.use(router)
app.use(directivePlugin)
app.mount('#app')

产品列表实现

api/home.js新增

/**
 * @description: 获取所有商品模块
 * @param {*}
 * @return {*}
 */
export const getGoodsAPI = () => {
  return httpInstance({
    url: '/home/goods'
  })
}

 home/components/HomeProduct.vue





这里没有用懒加载的形式,因为总有一些图片加载不出来,原因不明。 

基于vue的黑马前端项目小兔鲜_第15张图片

GoodsItem组件封装 

 home/components/GoodsItem.vue







 home/components/HomeProduct.vue修改

import GoodsItem from './GoodsItem.vue'

一级分类页 

导航栏

router/index.js修改

children: [
			{
				path: '',
				component: Home
			},
			{
				path: 'category/:id',
				component: Category
			}
		]

layout/components/LayoutHeader.vue修改

layout/components/LayoutFixed.vue修改

{{ item.name }}

api/category.vue

import httpInstance from '@/utils/http'

/**
 * @description: 获取分类数据
 * @param {*} id 分类id 
 * @return {*}
 */
export const findTopCategoryAPI = (id) => {
  return httpInstance({
    url:'/category',
    params:{
      id
    }
  })
}

views/category/index.vue







基于vue的黑马前端项目小兔鲜_第16张图片

分类Banner渲染 

api/home.js修改

export function getBannerAPI (params = {}) {
  // 默认为1 商品为2
  const { distributionSite = '1' } = params
  return httpInstance({
    url: '/home/banner',
    params: {
      distributionSite
    }
  })
}

category/index.vue 







基于vue的黑马前端项目小兔鲜_第17张图片

导航激活 

layout/components/LayoutHeader.vue修改

layout/components/LayoutFixed.vue修改

{{ item.name }}

分类列表渲染 

category/index.vue







基于vue的黑马前端项目小兔鲜_第18张图片

路由缓存问题 

banner与分类商品在刷新的时候都会请求一次,但banner是没必要刷新的,且导航栏需要刷新才能更新。

category/index.vue修改

import {useRoute,onBeforeRouteUpdate} from 'vue-router'

// 目标:路由参数变化的时候 可以把分类数据接口重新发送
    onBeforeRouteUpdate((to) => {
      // 存在问题:使用最新的路由参数请求最新的分类数据
      getCategory(to.params.id)
    })

基于vue的黑马前端项目小兔鲜_第19张图片

基于vue的黑马前端项目小兔鲜_第20张图片

业务逻辑的函数拆分 

将banner和分类抽象出来。

基于vue的黑马前端项目小兔鲜_第21张图片

useBanner.js

// 封装banner轮播图相关的业务代码
import { ref, onMounted } from 'vue'
import { getBannerAPI } from '@/api/home'

export function useBanner () {
  //获取banner
  const bannerList = ref([])
  const getBanner = async () => {
    const res = await getBannerAPI()
    console.log(res)
    bannerList.value = res.result
  }
  onMounted(() => getBanner())
  return {
    bannerList
  }
}

useCategory.js

// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { findTopCategoryAPI } from '@/api/category'
import { useRoute } from 'vue-router'
import { onBeforeRouteUpdate } from 'vue-router'

export function useCategory () {
  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)

  // 目标:路由参数变化的时候 可以把分类数据接口重新发送
  onBeforeRouteUpdate((to) => {
    // 存在问题:使用最新的路由参数请求最新的分类数据
    getCategory(to.params.id)
  })
  return {
    categoryData
  }
}

 index.vue修改

二级分类页 

实现

views下新建

index.vue







router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '@/views/login/index.vue'
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: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
		path: '/',
		component: Layout,
		children: [
			{
				path: '',
				component: Home
			},
			{
				path: 'category/:id',
				component: Category
			},
			{
			    path: 'category/sub/:id',
			    name: 'subCategory',
			    component: SubCategory
			},
		]
	},
	{
		path: '/login',
		component: Login
	}
  ]
})

export default router

category/index.vue修改

全部分类

  • {{ i.name }}

基于vue的黑马前端项目小兔鲜_第22张图片

api/category.js

/**
 * @description: 获取二级分类列表数据
 * @param {*} id 分类id 
 * @return {*}
 */

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
  })
}

 subCategory/index.vue





基于vue的黑马前端项目小兔鲜_第23张图片

subCategory/index.vue修改 

无限加载。

定制路由的滚动行为

自动回到顶部。

router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '@/views/login/index.vue'
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: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
		path: '/',
		component: Layout,
		children: [
			{
				path: '',
				component: Home
			},
			{
				path: 'category/:id',
				component: Category
			},
			{
			    path: 'category/sub/:id',
			    name: 'subCategory',
			    component: SubCategory
			},
		]
	},
	{
		path: '/login',
		component: Login
	}
  ],
  //路由滚动行为
  scrollBehavior() {
	  return {
		  top: 0
	  }
  }
})

export default router

商品详情

路由配置

views/detail/index.vue






router/index.js中children中添加

{
		path: 'detail/:id',
		component: Detail
}

home/components/HomeNew.vue修改

  • {{ item.name }}

    ¥{{ item.price }}

  • 基于vue的黑马前端项目小兔鲜_第24张图片

    数据渲染

    detail/index.vue

    
    
    
    
    
    

    基于vue的黑马前端项目小兔鲜_第25张图片

    热榜区域 

    api/detail.js添加

    export const fetchHotGoodsAPI = ({ id, type, limit = 3 }) => {
      return httpInstance({
        url:'/goods/hot',
        params:{
          id, 
          type, 
          limit
        }
      })
    }

    DetailHot.vue

    
    
    
    
    
    
    
    

    detail/index.vue

    
    
    
    
    
    

    基于vue的黑马前端项目小兔鲜_第26张图片

    适配热榜类型 

    detail/components/DeatinHot.vue修改

    
    
    
                

    detail/index.vue

    
                

    图片预览组件封装

    src/components/imgView/index.vue

    
    
    
    
    
    

    detail/index.vue修改

    import ImageView from '@/components/imageView/index.vue'
    
    
    

     基于vue的黑马前端项目小兔鲜_第27张图片

    sku插件

    基于vue的黑马前端项目小兔鲜_第28张图片

     XtxSku

    链接:https://pan.baidu.com/s/1KQ5-OFMoD7bpL8DJjsmsIA?pwd=3kgh 
    提取码:3kgh

    detain/index.vue修改 

    import XtxSku from '@/components/XtxSku/index.vue'
    
    
    
    

     登录页

    表单

    login/index.vue

    
    
    
    
    

    登录实现 

    账号:cdshi0006

    密码:123456

    login/index.vue修改

     

     登录失败的提示

    utils/http.js修改

    import 'element-plus/theme-chalk/el-message.css'
    import { ElMessage } from 'element-plus'
    
    // axios响应式拦截器
    http.interceptors.response.use(res=>res.data,e => {
    	ElMessage({
    		type: 'warning',
    		message: e.response.data.msg
    	})
    	console.log(e)
    	return Promise.reject(e)
    })

     Pinia管理用户

    stores/user.js

    // 管理用户数据相关
    
    import { defineStore } from 'pinia'
    import { ref } from 'vue'
    import { loginAPI } from '@/api/user'
    
    export const useUserStore = defineStore('user', () => {
      // 1. 定义管理用户数据的state
      const userInfo = ref({})
      // 2. 定义获取接口数据的action函数
      const getUserInfo = async ({ account, password }) => {
        const res = await loginAPI({ account, password })
        userInfo.value = res.result
      }
      // 3. 以对象的格式把state和action return
      return {
        userInfo,
    	getUserInfo
      }
    }, {
      persist: true,
    })

    基于vue的黑马前端项目小兔鲜_第29张图片

    Pinia持久化存储

    需要使用插件,自动本地存储,官网

    下载pinia-plugin-persistedstate

    npm i pinia-plugin-persistedstate

    main.js修改

    import { createPinia } from 'pinia'
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    
    const pinia = createPinia()
    pinia.use(piniaPluginPersistedstate)

     login/index.vue修改

    export const useUserStore = defineStore('user', () => {
      //......
    }, {
      persist: true,
    })

     基于vue的黑马前端项目小兔鲜_第30张图片

    登录和非登录状态

    LayoutNav.vue修改