# vue-rabbit **Repository Path**: pikapi_box/vue-rabbit ## Basic Information - **Project Name**: vue-rabbit - **Description**: 基于Vue3的商城项目——小兔鲜儿 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2025-07-03 - **Last Updated**: 2025-07-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue-rabbit # 前端项目上线 ## 项目打包说明 - 我们开发用的脚手架其实就是一个**微型服务器**,用于:支撑开发环境、运行代理服务器等。 - 打包完的文件中不存在:`.vue`、`.jsx`、`.less` 等文件,而是:`html`、`css`、`js`等。 - 打包后的文件,不再借助脚手架运行,而是需要部署到服务器上运行。 - 打包前,请务必梳理好前端项目的`ajax`封装(请求前缀、代理规则等) ## 项目部署 - 本地服务器部署(在局域网内可用访问) - nginx服务器部署 - 云服务器部署 参考:https://www.yuque.com/tianyu-coder/openshare/shka6xog7fbezhad # jsconfig.json配置别名路径 配置别名路径可以在写代码时联想提示路径 ```json { "compilerOptions" : { "baseUrl" : "./", "paths" : { "@/*":["src/*"] } } } ``` # elementPlus引入 小兔鲜ui项目设计到的组件 - 通用型组件(ElementPlus) - 如:Dialog模态框 - 业务定制化组件(自己手写) - 如:商品热榜组件 ## 安装elementPlus和自动导入插件 参考官网:https://element-plus.org/zh-CN/guide/quickstart.html 安装elementPlus按需引入和自动导入插件 首先你需要安装`unplugin-vue-components` 和 `unplugin-auto-import`这两款插件 ```shell npm install element-plus --save npm install -D unplugin-vue-components unplugin-auto-import ``` 说明: - -D 表示只在开发环境中安装 ## 配置自动按需导入 然后把下列代码插入到你的 `Vite` 或 `Webpack` 的配置文件中 ```ts // vite.config.ts import { defineConfig } from 'vite' 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()], }), ], }) ``` ## 测试组件 ```vue ``` # elementPlus主题色定制 ![1721280554212](./assets/elementPlus主题色定制.png) 如何定制? 参考官网:https://element-plus.org/zh-CN/guide/theming.html ![1721280757018](./assets/elementPlus主题色定制2.png) ## 1. 安装sass 基于vite的项目默认不支持css预处理器,需要开发者单独安装 ```shell npm i sass -D ``` ## 2. 准备定制化的样式文件 styles/element/index.scss ```javascript /* 只需要重写你需要的即可 */ @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, ), ) ); ``` ## 3. 自动导入配置 这里自动导入需要深入到elementPlus的组件中,按照官方的配置文档来 1. 配置elementPlus采用sass样式配色系统 2. 自动导入定制化样式文件进行样式覆盖 ```javascript 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: [ // 1.配置elementPlus采用sass样式配色系统 ElementPlusResolver({importStyle:"sass"}) ], }), ], resolve: { // 实际的路径转换 @ => src alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, css: { preprocessorOptions: { scss: { // 2.自动导入定制化样式文件进行样式覆盖 additionalData: ` @use "@/styles/element/index.scss" as *; `, } } } }) ``` ## 4.验证定制主题色生效 ![1721281802511](./assets/elementPlus主题色定制3.png) # axios安装并简单封装 ![1721282281645](./assets/axios基础配置.png) ## 1. 安装axios ```shell npm install axios --save ``` ## 2. 基础配置 官方文档地址: 基础配置通常包括: 1. 实例化 - baseURL + timeout 2. 拦截器 - 携带token 401拦截等
文件名: utils/http.ts
```js import axios from 'axios' // 创建axios实例 const instance = axios.create({ baseURL: `http://pcapi-xiaotuxian-front-devtest.itheima.net`, timeout: 5000 }); // axios请求拦截器 instance.interceptors.request.use(config => { return config; }, e => Promise.reject(e)) // axios响应拦截器 instance.interceptors.response.use(res => res.data, e => { return Promise.reject(e) }) export default instance ``` ## 3. 封装请求函数并测试
文件名: apps/testAPI.js
```js import instance from '@/utils/http'; export function getCategoryAPI() { return instance({ url: 'home/category/head' }) } ``` # 路由整体设计 **路由设计原则:**找页面的切换方式 - 如果是整体切换,则为一级路由 - 如果是在一级路由的内部进行的内容切换,则为二级路由 ## 一级路由 ![1721296364351](./assets/路由设计1.png)
文件名: views/Login/index.vue
```vue ```
文件名: views/Layout/index.vue
```vue ```
一级路由: router/index.js
``` routes: [ { name: 'layout', path: '/', component: Layout }, { name: 'login', path: '/login', component: Login } ] ``` ## 二级路由 ![1721296980089](./assets/路由设计2.png)
文件名: views/Home/index.vue
```vue ```
文件名: views/Category/index.vue
```vue ``` 二级路由: router/index.js ```js { name: 'layout', path: '/', component: Layout, children: [ { path: '', component: Home }, { path: 'category', component: Category } ] } ``` # 静态资源引入和Error Lens安装 图片资源和样式资源说明: 1. 图片资源通常由 UI设计师 提供,常见的图片格式有png、svg等,都是由UI切图交给前端 2. 样式资源通常是指项目初始化的时候进行样式的重置,常见的比如开业的 normalize.css 或者自己手写 ## 1. 静态资源引入 我们打算按照如下方式进行: 1. 图片资源 - 把 images 文件夹放到 assets 目录下 2. 样式资源 - 把 common.scss 文件放到 styles 目录下 详情参考git提交记录 ## 2. Error Lens插件安装 Error Lens是一个时时提供错误警告的信息的VScode插件,减低开发难度 ![](./assets/error-lens.png) # scss变量自动导入 ![1721299971663](./assets/scss变量自动导入1.png) 自动导入配置步骤: 1)新增一个 `var.scss `文件,存入色值变量 2)通过 `vite.config.js` 配置自动导入文件
文件名: styles/var.scss
```scss $xtxColor: #27ba9b; $helpColor: #e26237; $sucColor: #1dc779; $warnColor: #ffb302; $priceColor: #cf4444; ```
文件名: vite.config.js
```js css: { preprocessorOptions: { scss: { // 自动导入scss文件 additionalData: ` @use "@/styles/element/index.scss" as *; @use "@/styles/var.scss" as *; `, } } } ``` # 补充eslint配置
文件名: .eslintrc.cjs
``` /* eslint-env node */ require('@rushstack/eslint-patch/modern-module-resolution') module.exports = { root: true, 'extends': [ 'plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript' ], parserOptions: { ecmaVersion: 'latest' }, rules: { 'vue/multi-word-component-names': 0, // 不再强制要求组件命名 }, } ``` # Layout页 ## Layout页组件结构快速搭建 ![1721300761591](./assets/Layout模块静态模板搭建1.png) 详情参照:**feat: layout页面静态结构结构搭建** ## 字体图标引入 ![1721303278749](./assets/layout字体图标引入.png) 阿里的字体图标库支持多种引入方式,小兔鲜项目采用的是**font-class**引用的方式 在 `index.html`文件中引入即可 ```html ``` 更多的引用方式参考官网:https://www.iconfont.cn/help/detail?spm=a313x.home_index.i3.28.58a33a815XEfoF&helptype=code **font-class**引用方式,图标的使用步骤如下: - 第一步:拷贝项目下面生成的fontclass代码: ```js //at.alicdn.com/t/font_8d5l8fzk5b87iudi.css ``` - 第二步:挑选相应图标并获取类名,应用于页面: ```css ``` ## 一级导航渲染 ![](./assets/layout一级导航.png) **功能描述**:使用后端接口渲染一级路由导航 **实现步骤** 1. 封装接口函数 2. 调用接口函数 3. v-for渲染模版 **代码如下**
文件名: apis/layout.js
```js import instance from '@/utils/http' export function getCategoryAPI() { return instance({ url: 'home/category/head' }) } ```
文件名: views/components/LayoutHeader.vue
```vue ``` ## 吸顶导航交互实现 **功能描述**:要求浏览器在上下滚动的过程中,如果距离顶部的距离大于78px,导航吸顶显示,小于78px隐藏 **实现步骤** ![1721312845148](./assets/吸顶导航交互.png) ### 1. 准备组件静态结构
文件名: views/components/LayoutFixed.vue
```vue ``` ### 2. 渲染基础数据 将组件放置在合适的位置
文件名: views/index.vue
```vue ``` ### 3. 实现吸顶交互 核心逻辑:根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示 ```vue ``` 要使用useScroll需要安装vueuse,命令如下 ```shell npm i @vueuse/core ``` ## Pinia优化重复请求 ![1721314171067](./assets/1721314171067.png) 如何优化? ![1721314466608](./assets/1721314466608.png) 代码如下: ```js import {defineStore} from "pinia"; import {ref} from "vue"; import {getCategoryAPI} from "@/apis/layout"; // 官方推荐使用hooks的那种命名规范 /** * 参数一:id (建议与文件名一致) * 参数二:options 配置对象 */ // 组合式写法 export const useCategoryStore = defineStore('category', () => { // 导航列表数据 // categoryList就是state const categoryList = ref([]) // getCategory函数就相当于action const getCategory = async () => { const res = await getCategoryAPI() categoryList.value = res.result; } return {categoryList, getCategory} } ) ``` 接着在之前调用 getCategoryAPI() 的地方进行修改,详情参考**feat: pina优化导航数据** # Home页 ## 整体结构搭建和分类实现 ### 1. 整体结构创建 ![](./assets/Home页整体结构.png) 1.按照结构新增五个组件,准备最简单的模版,分别在Home模块的入口组件中引入 - HomeCategory - HomeBanner - HomeNew - HomeHot - HomeProduct 2.Home模块入口组件中引入并渲染 ```vue ``` ### 2. 分类实现 ![1721318127877](./assets/分类实现.png) 1.准备详细模版 ```vue ``` 2.使用pinia中的数据进行渲染,完整代码如下 ```vue ``` ## banner轮播图实现 ![1721319097317](D:\video\workspace\vue-project\vue-rabbit\assets\banner轮播图.png) ### 1. 准备静态组件
文件名: views/Home/components/HomeBanner.vue
```vue ``` ### 2. 获取接口数据渲染组件 #### 1)封装接口
文件名: apis/home.js
```js import instance from '@/utils/http' /** * @description: 获取banner图 */ export function getBannerAPI() { return instance({ url: 'home/banner' }) } ``` #### 2)获取接口数据渲染模版
文件名: views/Home/components/HomeBanner.vue
```vue ``` ## 面板组件封装 ![1721320347110](./assets/面版组件封装.png) ### 组件封装核心思路 把可复用的的结构只写一次,把**可能发生变化的部分抽象成组件参数(props / 插槽)** ![1721320520565](./assets/面版组件封装2.png) 观察发现有三个地方不同: - 主标题 - 副标题 - 主体内容(图片及其下面的文字内容) ### 实现步骤分析 - 1)先不做任何的抽象,准备静态模板 - 2)抽象可变的部分 - 主标题 和 副标题 是**纯文本**,可以抽象成**prop**传入 - 主体内容是复杂的**模板**,抽象成**插槽**传入 ### 纯静态结构
文件名: views/Home/components/HomePanel.vue
```vue ``` ### 完整代码
文件名: views/Home/components/HomePanel.vue
```vue ``` ## 新鲜好物实现 实现思路: - 1)准备模板(HomePanel组件) - 2)定制props - 3)定制插槽内容(接口 + 渲染模板) ### 1. 准备模版
文件名: views/Home/components/HomeNew.vue
```vue ``` ### 2. 封装接口
文件名: apis/home.js
```js /** * @description: 获取新鲜好物 */ export const findNewAPI = () => { return instance({ url: '/home/new' }) } ``` ### 3. 获取数据渲染模版 调用接口获取数据,将数据渲染到插槽位置 完整代码如下 ```vue ``` ## 人气推荐实现 实现思路: - 1)准备模板(HomePanel组件) - 2)定制props - 3)定制插槽内容(接口 + 渲染模板) ### 1. 封装接口
文件名: apis/home.js
```js /** * @description: 获取新鲜好物 */ export const findNewAPI = () => { return instance({ url: '/home/new' }) } ``` ### 2. 获取数据渲染模版 调用接口获取数据,将数据渲染到插槽位置 完整代码如下
文件名: views/Home/components/HomeHot.vue
```vue ``` ## 懒加载指令优化 **场景:** ​ 电商网站的首页通常会很长,用户不一定能访问到**页面靠下面的图片**,这类图片通过懒加载优化手段可以做到**只有进入视口区域才发送图片请求** **指令用法:** ```html ``` 在图片img上绑定指令,该图片只有在正式进入到视口区域时才会发送图片的网络请求 **实现思路和步骤:** ![1721457458804](./assets/懒加载指令优化1.png) vue自定义指令官网:https://cn.vuejs.org/guide/reusability/custom-directives.html#custom-directives ### 1. 封装全局指令
文件名: directives/index.js
```js // 定义懒加载插件 import type {App} from "vue"; import {useIntersectionObserver} from "@vueuse/core"; export const lazyPlugin = { install: (app: App): void => { // 懒加载指令逻辑 app.directive('img-lazy', { mounted(el, binding) { // el 指定绑定哪个元素 img // binding binding.value 指令等于后后面绑定的表达式的值 url // console.log(el, binding.value) const {stop} = useIntersectionObserver( el, ([{isIntersecting}]) => { // console.log(isIntersecting) if (isIntersecting) { // 进入视口区域 el.src = binding.value stop() // 在图片第一次加载后,停止监听视口,避免内存浪费 } }, ) } }) } } ``` ### 2. 注册全局指令
文件名: main.js
```js // 引入懒加载指令插件并注册 import {lazyPlugin} from '@/directives' app.use(lazyPlugin) ``` ### 3. 按需使用 在需要使用自定义指令的地方使用即可,详情参照gti提交记录**feat: 图片懒加载优化** ## Product产品列表实现 ![1721459585318](./assets/product产品列表实现.png) ### 1. 准备静态模版
文件名: views/Home/components/HomeProduct.vue
```vue ``` ### 2. 封装接口
文件名: apis/home.js
```js // 获取所有商品模块 export const getGoodsAPI = () => instance.get('/home/goods') ``` ### 3. 获取并渲染数据
文件名: views/Home/components/HomeProduct.vue
```vue ``` ### 4. 图片懒加载
文件名: views/Home/components/HomeProduct.vue
```vue ``` ## GoodsItem组件封装 ![1721460836072](./assets/封装goodsItem组件.png) 核心思想:把要显示是数据设计为props参数,传入什么数据对象就显示什么数据 ### 1. 封装组件
文件名: views/Home/components/GoodsItem.vue
```vue ``` ### 2. 使用组件
文件名: views/Home/components/HomeProduct.vue
```html ``` # 一级分类页 ## 静态结构搭建和路由配置 ### 1. 准备分类组件
文件名: views/Category/index.vue
```vue ``` ### 2. 配置路由 ```js routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, { name: 'category', path: 'category/:id', component: Category } ] }, { name: 'login', path: '/login', component: Login } ] ``` ### 3. 配置导航区域链接
文件名: views/components/LayoutHeader.vue
```html ```
文件名: views/components/LayoutFixed.vue
```html ``` ## 面包屑导航渲染 ![1721489318877](./assets/面包屑导航.png) **核心思路:** - 准备组件模板 - 封装接口函数 - 调用接口获取数据(使用路由参数) - 渲染模板 ### 1. 认识组件准备模版
文件名: views/Category/index.vue
```vue ``` ### 2. 封装接口
文件名: apis/category.js
```js import instance from '@/utils/http' // 获取-二级分类列表 export const findTopCategoryAPI = (id) => { return instance({ url: '/category', params: { id } }) } ``` ### 3. 渲染面包屑导航
文件名: views/Category/index.vue
```vue ``` ## 分类Banner渲染 分类轮播图Banner的实现:分类轮播图和首页轮播图的区别只有一个,**接口参数不同**,其余逻辑完全一致 ![1721490937081](./assets/分类轮播图.png) **核心思路:** - 改造前面写过的接口(适配参数) - 迁移首页轮播图实现 ### 1. 适配接口
文件名: apis/home.js
```js /** * @description: 获取banner图 */ export function getBannerAPI(params = {}) { // 1为首页,2为分类商品页 默认是1 const {distributionSite = '1'} = params return instance({ url: 'home/banner', params: { distributionSite } }) } ``` ### 2. 迁移首页Banner逻辑
文件名: views/Category/components/CategoryBanner.vue
```vue ``` ### 3.渲染分类轮播图
文件名: views/Category/index.vue
```vue ``` ## 导航激活设置分类列表渲染 ### 导航激活状态设置 当我们选择某个分类时,它就会高亮显示,如下: ![1721492344348](./assets/激活状态显示.png) **实现思路:** ​ RouterLink组件默认支持激活样式显示的类名,只需要给**active-class属性设置对应的类名**即可
文件名: views/layout/LayoutHeader.vue
```vue {{ item.name }} ```
文件名: views/layout/LayoutFixed.vue
```vue {{ item.name }} ``` ### 分类数据模版 **核心思路:** ​ 分类的数据一级在面包屑实现的时候获取到了,只需要通过v-for遍历出来即可
文件名: views/Category/index.vue
```vue

全部分类

- {{ item.name }}-

``` ## 路由缓存问题解决 ![1721493608245](./assets/路由缓存问题.png) **缓存问题:** ​ 当路由path一样,参数不同的时候会选择直接复用路由对应的组件 **解决思路:** - 1、让组件实例不复用,强制销毁(vue3叫卸载)重建 - 2、监听路由变化,变化之后执行数据更新操作 **解决方案:** - 给 routerv-view 添加key属性,破坏缓存 - 使用 onBeforeRouteUpdate钩子函数,做精确更新 ![1721494006647](./assets/路由缓存问题2.png) ![1721494088689](./assets/路由缓存问题3.png) ## 基于业务逻辑的函数拆分 Hook **基本思想:** ​ 把组件内独立的业务逻辑通过 `useXXX` 函数做封装处理,在组件中做组合使用 ![](./assets/基于业务逻辑的函数拆分.png) 实现步骤: - 1、按照业务声明以`use`打头的逻辑函数 - 2、把**独立的业务逻辑**封装到各个函数内部 - 3、函数内部把组件中需要用到的数据或者方法**return出去** - 4、在**组件中调用函数**把数据或者方法组合回来使用 ![1721495472734](./assets/基于业务逻辑的函数拆分2.png) # 二级分类页 ## 整体业务认识和路由配置 ### 整体业务认识 ![1721536249468](./assets/二级分类页整体业务认识.png) ### 路由配置 ![1721536325587](./assets/二级分类页路由配置.png) **核心思路:** - 创建路由组件 - 配置路由关系 - 修改模板实现路由跳转 #### 1. 准备组件模版
文件名: views/subCategory/index.vue
```vue ``` #### 2. 配置路由关系
文件名: router/index.vue
```js import { createRouter, createWebHistory } from 'vue-router' 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), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, { name: 'category', path: 'category/:id', component: Category }, { name:'subCategory', path: '/category/sub/:id', component: SubCategory }, ] }, { name: 'login', path: '/login', component: Login } ] }) export default router ``` #### 3. 路由跳转配置
文件名: views/Category/index.vue
```vue

全部分类

``` ## 面包屑导航实现 ![1721538358460](./assets/二级分类页-面包屑导航.png) **核心思路:** - 封装接口 - 调用接口渲染模板 ### 1. 封装接口
文件名: apis/category.js
```js // 获取二级分类列表数据 export const getCategoryFilterAPI = (id) => { return instance({ url: '/category/sub/filter', params: { id } }) } ``` ### 2. 获取数据渲染模版
文件名: views/subCategory/index.vue
```vue ``` ## 分类列表实现 ![1721539962118](./assets/二级分类页-分类基础列表.png) 上述图片中需要实现如下功能: - 实现基础列表渲染 - 添加筛选功能 - 无限加载功能 ### 分类基础列表实现 **核心思路:** - 封装接口 - 获取数据渲染模板 #### 1. 封装接口
文件名: apis/category.js
```js /** * @description: 获取导航数据 * @data { * categoryId: 1005000 , * page: 1, * pageSize: 20, * sortField: 'publishTime' | 'orderNum' | 'evaluateNum' } * @return {*} */ export const getSubCategoryAPI = (data) => { return instance({ url: '/category/goods/temporary', method: 'POST', data }) } ``` #### 2. 获取数据列表并渲染
文件名: views/subCategory/index.vue
```vue ``` ### 列表筛选实现 ![1721543178322](assets/二级分类页-分类列表筛选.png) **核心逻辑:** - 点击tab,**切换筛选条件参数sortFiled**,重新发送列表请求 **核心思路:** - 获取激活项数据 - 使用新参数发送请求重新渲染列表数据
文件名: views/subCategory/index.vue
tab组件切换时修改reqData中的sortField字段,重新拉取接口列表 ```vue ``` ### 列表无限加载实现 ![1721544371759](assets/二级分类页-分类列表无限加载.png) **核心逻辑:** ​ 使用elementPlus提供的 `v-infinite-scroll`指令**监听是否满足触底条件**,满足加载条件时让**页数参数加一获取下一页数据,做新老数据拼接渲染** 核心思路: - 配置 `v-infinite-scroll`指令 - 触底条件满足之后**page++**获取下一页数据 - 老数据和新数据做拼接 - 判断是否已经全部加载完毕,加载完毕结束监听 elementPlus官网无线滚动用法:https://element-plus.org/zh-CN/component/infinite-scroll.html#infinite-scroll-%E6%97%A0%E9%99%90%E6%BB%9A%E5%8A%A8
文件名: views/subCategory/index.vue
```vue ``` ### 定制路由滚动行为(优化路由) **定制路由行为解决什么问题?** - 在不同路由切换的时候,可以**自动滚动到页面的顶部**,而不是停留在原先的位置 如何配置优化呢? - vue-router 支持 `scrollBehavior`配置项,可以指定路由切换时的滚动位置 ```js scrollBehavior (){ return { top: 0 } } ``` # 商品详情 ## 整体认识和路由配置 ### 整体认识 ![1721551539095](assets/商品详情-整体认识.png) ![1721552242573](assets/商品详情-整体认识2.png) ### 路由配置 **核心思路:** - 创建详情组件 - 绑定路由关系 - 绑定模板测试跳转 #### 1. 准备组件模版
文件名: views/Detail/index.vue
```vue ``` #### 2. 配置路由规则
文件名: router/index.vue
```js const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, { name: 'category', path: 'category/:id', component: Category }, { name:'subCategory', path: '/category/sub/:id', component: SubCategory }, { name:'detail', path: '/detail/:id', component: ProductDetail } ] }, { name: 'login', path: '/login', component: Login } ], scrollBehavior (){ return { top: 0 } } }) export default router ``` #### 3. 绑定模版测试跳转
文件名: views/home/components/HomeNew.vue
```vue

{{ item.name }}

¥{{ item.price }}

``` ## 渲染基础数据 ![1721560312265](assets/商品详情-基础数据渲染.png) **核心思路:** - 封装接口 - 调用接口获取数据 - 渲染模板 ### 1. 封装接口
文件名: apis/detail.js
```js import request from '@/utils/http' // 获取商品详情 export const getDetailAPI = (id: number) => { return request({ url: '/goods', params: { id } }) } ``` ### 2. 获取数据渲染模版
文件名: views/Detail/index.vue
```vue ``` ### 3.小结 ![1721562221831](assets/商品详情-基础数据渲染小结.png) ## 热榜区域 ![1721564109107](assets/商品详情-热榜区域.png) **核心思路:** - 24小时热榜 与 周热榜 大体结构一致,可以先封装一个 Hot热榜组件 - 获取数据渲染模板(下面以24小时热榜为例) - 适配不同的标题title - 适配不同的列表内容 ### 渲染基础热榜数据(24小时热榜为例) #### 1 准备模版
文件名: view/Detail/components/DetailHot.vue
```vue ``` #### 2 封装接口
文件名: apis/detail.js
```js /** * 获取热榜商品 * @param {Number} id - 商品id * @param {Number} type - 1代表24小时热销榜 2代表周热销榜 * @param {Number} limit - 获取个数 */ export const getHotGoodsAPI = ({ id, type, limit = 3 }) => { return request({ url: '/goods/hot', params: { id, type, limit } }) } ``` #### 3 获取基础数据渲染模版
文件名: view/Detail/components/DetailHot.vue
```vue ``` ### 适配热榜title #### 设计props参数type
文件名: view/Detail/components/DetailHot.vue
```js // 设计props参数,适配不同的title和数据 const props = defineProps({ hotType: { type: Number, // 1代表24小时热销榜 2代表周热销榜 }, }); // 适配title // 1代表24小时热销榜 2代表周热销榜 const TYPEMAP = { 1: "24小时热榜", 2: "周热榜", }; const title = computed(() => TYPEMAP[props.hotType]); ``` ### 适配热榜类型 #### 使用组件传入不同的type
文件名: views/Detail/index.vue
```vue ```
文件名: view/Detail/components/DetailHot.vue
```js const getHotGoods = async () => { const res = await getHotGoodsAPI({ id: route.params.id, type: props.hotType, }); // console.log(res.result); hotList.value = res.result; }; ``` ## 图片预览组件封装 ![1721571374884](assets/商品详情-图片预览.png) ### 1. 小图切换大图显示 ![1721571527801](assets/商品详情-图片预览2.png) **核心思路:** - 准备组件静态模板 - 为小图榜单事件,记录当前激活的下标值 - 通过下标切换大图显示 - 通过下表实现激活状态显示 #### 1)准备模版
文件名:components/ImageView/index.vue
```vue ``` #### 2)实现逻辑 **实现:** - 为小图榜单事件,记录当前激活的下标值 - 通过下标切换大图显示 - 通过下表实现激活状态显示
文件名:components/ImageView/index.vue
```vue ``` ### 2. 放大镜效果实现 ![](assets/商品详情-图片预览3.png) **核心思路:** - 左侧滑块跟随鼠标移动 - 右侧大图放大效果实现 - 鼠标移入滑块和大图显示隐藏 #### 1)左侧滑块跟随鼠标移动 ![1721635540263](assets/商品详情-图片预览4.png) ![1721637110078](assets/商品详情-图片预览5.png) 参考vueuse官网usemouseinelement用法:https://vueuse.org/core/useMouseInElement/#usemouseinelement
文件名:components/ImageView/index.vue
```vue ``` #### 2)右侧大图放大效果实现 ![1721637364012](assets/商品详情-图片预览6.png)
文件名:components/ImageView/index.vue
```vue ``` #### 3)鼠标移入控制显隐 ![1721637763676](assets/商品详情-图片预览7.png)
文件名:components/ImageView/index.vue
```vue ``` ### 3. 组件props的适配 ![1721638895090](assets/商品详情-图片预览8.png)
文件名:components/ImageView/index.vue
```js // props适配图片列表 defineProps({ imageList: { type: Array, default: () => [], }, }); ```
文件名:views/Detail/index.vue
```html ``` ### 4. 总结 ![1721639088685](assets/商品详情-图片预览9.png) ## SKU组件熟悉 ![1721639202885](assets/sku组件.png) ![1721639918265](assets/sku组件2.png)
文件名:views/Detail/index.vue
```vue ``` ## 通用组件统一全局注册(插件化) **为什么要进行优化(全局注册)?** ​ 在components目录下可能还会有很多其他通用型组件,有可能在多个业务模块中共享,所以统一进行全局组件注册比较好 **注册思路** - 把components目录下的所有组件进行全局注册 - 是`mian.js`中注册插件 ### 1. 插件化开发
文件名:components/index.js
```js // 把components中的所组件都进行全局化注册 // 通过插件的方式 import ImageView from './ImageView/index.vue' import XtxSku from './ImageView/index.vue' export const componentPlugin = { install(app) { // app.component('组件名字',组件配置对象) app.component('XtxImageView', ImageView) app.component('XtxSku', XtxSku) } } ``` ### 2. 插件注册
文件名:main.js
```js // 引入全局组件插件 import { componentPlugin } from '@/components' app.use(componentPlugin) ``` # 登录页 ## 整体认识和路由配置 ### 整体认识 ![1721644211968](assets/登录页-整体认识.png) ### 路由配置 #### 1. 准备模板
文件名:views/Login/index.vue
```vue ``` #### 2. 配置路由规则
文件名:router/index.js
```js routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, { name: 'category', path: 'category/:id', component: Category }, { name:'subCategory', path: '/category/sub/:id', component: SubCategory }, { name:'detail', path: '/detail/:id', component: ProductDetail } ] }, { name: 'login', path: '/login', component: Login } ] ``` #### 3. 配置路由跳转
文件名:views/Layout/components/LayoutNav.vue
```vue ``` ## 表单校验实现 ![1721645389731](assets/登录页-表单校验1.png) ![1721645473603](assets/登录页-表单校验2.png) 参考elementPlus官网:https://element-plus.org/zh-CN/component/form.html#form-%E8%A1%A8%E5%8D%95 **表单校验核心步骤:** - 1、**按照接口字段**准备表单对象并绑定 - 2、**按照产品要求**准备规则对象并绑定 - 3、指定表单域的**校验字段名** - 4、把表单对象进行**双向绑定** ### 1. 校验要求 - 用户名:不能为空,字段名为 account - 密码:不能为空且为6-14个字符,字段名为 password - 同意协议:必选,字段名为 agree ### 2. 代码实现
文件名:views/Login/index.vue
```vue ``` ### 自定义校验规则 ![1721650963507](assets/登录页-表单校验3.png) 我们项目的需求: ​ 我们的校验逻辑是:如果勾选了协议框,通过校验;如果没有勾选,不通过校验
文件名:views/Login/index.vue
```vue ``` ### 整个表单的内容校验 ![1721705215705](assets/登录页-表单校验4.png) **核心思路:** - 获取form组件实例 - 调用实例方法
文件名:views/Login/index.vue
```vue ``` ## 登录基础业务实现 ![1721706361805](assets/登录页-基础业务实现1.png) **基础思想** - 调用登录接口获取用户信息 - 提示用户当前是否成功 - 跳转到首页 ### 1. 封装接口
文件名:apis/user.js
```js // 封装所有与用户相关的接口 import request from '@/utils/http' export const loginAPI = ({ account, password }) => { return request({ url: '/login', method: 'POST', data: { account, password } }) } ``` ### 2. 实现主逻辑
文件名:views/Login/index.vue
```vue ``` ### 3. 拦截器统一处理错误信息
文件名:utils/http.js
```js // 按需导入的方式需要手动导入样式 import { ElMessage } from 'element-plus' import 'element-plus/theme-chalk/el-message.css' // axios响应拦截器 instance.interceptors.response.use(res => res.data, e => { // 错误提示统一处理 ElMessage({ type: 'warning', message: e.response.data.message }) return Promise.reject(e) }) ``` ## Pinia管理用户数据 ![1721714650115](assets/登录页-pina管理用户数据.png) ![1721714725984](assets/登录页-pina管理用户数据2.png) ### 1.定义useUserStore
文件名:stores/user.js
```js import { defineStore } from "pinia"; import { ref } from "vue"; import { loginAPI } from '@/apis/user' // 官方推荐使用hooks的那种命名规范 /** * 参数一:id (建议与文件名一致) * 参数二:options 配置对象 */ // 组合式写法 export const useUserStore = defineStore('user', () => { // 1.定义管理用户数据的state const userInfo = ref({}) // 2.定义获取接口数据的action函数 const getUserInfo = async ({ account, password }) => { const res = await loginAPI({ account, password }) // console.log(res) userInfo.value = res.result } // 3.以对象的形式把state和action返回 return { userInfo, getUserInfo } }) ``` ### 2. 登录组件中使用
文件名:views/Login/index.vue
```vue ``` ### 3. Pinia用户数据持久化 ![1721716253961](assets/登录页-pina管理用户数据3.png) `pinia-plugin-persistedstate`参考: https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/ 1、安装插件 ```shell npm i pinia-plugin-persistedstate ``` 2、将插件添加到 pinia 实例上
文件名:main.js
```js import {createPinia} from 'pinia' // 将插件添加到 pinia 实例上 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const app = createApp(App) const pinia = createPinia() pinia.use(piniaPluginPersistedstate) ``` 3、使用 创建 Store 时,将 `persist` 选项设置为 `true`。
文件名:stores/user.js
```js import { defineStore } from "pinia"; import { ref } from "vue"; import { loginAPI } from '@/apis/user' // 组合式写法 export const useUserStore = defineStore('user', () => { // 1.定义管理用户数据的state const userInfo = ref({}) // 2.定义获取接口数据的action函数 const getUserInfo = async ({ account, password }) => { const res = await loginAPI({ account, password }) // console.log(res) userInfo.value = res.result } // 3.以对象的形式把state和action返回 return { userInfo, getUserInfo } }, { persist: true // store持久化配置 } ) ``` ## 登录与非登录状态的模板匹配 ![1721717259630](assets/登录页-登录与非登录.png) ![1721717655642](assets/登录页-登录与非登录2.png)
文件名:views/Layout/components/LayoutNav.vue
```vue ``` ## 请求拦截器携带token ![1721717922487](assets/登录页-请求拦截器携带token.png)
文件名:utils/http.js
```js import { useUserStore } from '@/stores/user'; // axios请求拦截器 instance.interceptors.request.use(config => { // 1.从pinia中获取token const userStore = useUserStore() const token = userStore.userInfo.token // 2.按照后端的要求进行token拼接处理 if (token) { config.headers.Authorization = `Bearer ${token}` } return config; }, e => Promise.reject(e)) ``` ## 退出登录实现 ![1721721592877](assets/登录页-退出登录.png) **核心思路:** - 点击退出登录弹出确认框 - 点击确认按钮实现退出登录逻辑 - 退出登录逻辑包括 - 1、清理当前用户信息(store、localStorage) - 2、跳转到登录页面 el-popconfirm 组件的使用参考elementPlus官方:
文件名:views/Layout/components/LayoutNav.vue
```vue ```
文件名:stores/user.js
```js // 组合式写法 export const useUserStore = defineStore('user', () => { // 1.定义管理用户数据的state const userInfo = ref({}) // 2.定义获取接口数据的action函数 const getUserInfo = async ({ account, password }) => { const res = await loginAPI({ account, password }) // console.log(res) userInfo.value = res.result } // 清除用户信息:会同步清空local Storage(由pinia-plugin-persistedstate插件实现) const clearUserInfo = () => { userInfo.value = {} } // 3.以对象的形式把state和action返回 return { userInfo, getUserInfo, clearUserInfo } }, { persist: true // store持久化配置 } ) ``` ## Token失效401实现 ![1721723504678](assets/登录页-token失效401.png)
文件名:utils/http.js
```js import { useUserStore } from '@/stores/user'; import router from '@/router'; // axios响应拦截器 instance.interceptors.response.use(res => res.data, e => { // 错误提示统一处理 ElMessage({ type: 'warning', message: e.response.data.message }) // 401 token失效处理 // 2.跳转到登录页 // 1.清除本地用户数据(pinia与local storage) if (e.response.status === 401) { const userStore = useUserStore() userStore.clearUserInfo() router.replace('/login') } return Promise.reject(e) }) ``` # 购物车 ![1721725268479](assets/购物车1.png) ## 本地购物车 ### 加入购物车实现 ![1721726137996](assets/购物车-本地购物车1.png)
文件名:stores/cart.js
```js // 封装购物车模块 import { defineStore } from 'pinia' import { ref } from 'vue' export const useCartStore = defineStore('cart', () => { // 定义state const cartList = ref([]) // 定义action // 加入购物车 const addCart = (good) => { console.log('加入购物车action执行') // 添加入购物车 // 已添加过:count + 1 // 没有添加过:直接push // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过 const item = cartList.value.find((item) => good.skuId === item.skuId) if (item) { // 找到啦 item.count++ } else { // 没找到 cartList.value.push(good) } } return { cartList, addCart } }, { persist: true // store持久化配置 } ) ```
文件名:views/Detail/index.vue
```vue ``` ### 头部购物车 #### 列表渲染-消息框渲染 ![1721729286054](assets/购物车-头部购物车.png) ##### 1. 头部购物车组件模版
文件名:views/Layout/components/HeaderCart.vue
```vue ``` ##### 2. 放置组件
文件名:views/Layout/components/LayoutHeader.vue
```vue ``` ##### 3. 渲染头部购物车数据
文件名:views/Layout/components/HeaderCart.vue
```vue ``` #### 头部购物车删除实现 ![1721730923315](assets/购物车-头部购物车2.png)
文件名:stores/cart.js
```js export const useCartStore = defineStore('cart', () => { // 定义state const cartList = ref([]) // 删除购物车 const delCart = (skuId) => { console.log('删除购物车action执行') // 方法一:找到要删除项的下标值 splice // 方法二:使用数组的过滤方法 filter const idx = cartList.value.findIndex((item) => item.skuId === skuId) cartList.value.splice(idx, 1) } return { cartList, addCart, delCart } } ) ```
文件名:views/Layout/components/HeaderCart.vue
```vue ``` #### 头部购物车统计计算 ![1721732734947](assets/购物车-头部购物车3.png)
文件名:stores/cart.js
```js export const useCartStore = defineStore('cart', () => { // 定义state const cartList = ref([]) // getters 计算属性 // 1.总数量 所有项的 count 之和 const allCount = computed(() => { return cartList.value.reduce((x, y) => x + y.count, 0) }) // 2.总价格 所有项的 count * price const allPrice = computed(() => { return cartList.value.reduce((x, y) => x + y.count * y.price, 0) }) return { cartList, allCount, allPrice, addCart, delCart } } ) ```
文件名:views/Layout/components/HeaderCart.vue
```vue ``` ### 列表购物车 #### 基础内容渲染 ![1721732976031](assets/购物车-列表购物车1.png) **核心思路:** - 准备模板 - 定制路由规则 - 绑定路由 - 渲染基础列表 ##### 准备模板
文件名:views/CartList/index.vue
```vue ``` ##### 定制路由规则
文件名:router/index.js
```js import CartList from '@/views/CartList/index.vue' routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, { name: 'cartList', path: 'cartList', component: CartList } ] }, ] ``` ##### 绑定路由
文件名:views/Layout/components/HeaderCart.vue
```vue 去购物车结算 ``` ##### 渲染基础列表
文件名:views/CartList/index.vue
```vue ``` #### 单选功能 ![1721733322700](assets/购物车-列表购物车2.png) 补充: ​ 组件标签上的`v-model`的本质:`:moldeValue` + `update:modelValue`事件
文件名:stores/cart.js
```js export const useCartStore = defineStore('cart', () => { // 定义state const cartList = ref([]) // 单选功能 const singleCheck = (skuId, selected) => { // 根据skuId找到要修改的项,修改其selected const item = cartList.value.find((item) => item.skuId === skuId) item.selected = selected }; return { cartList, allCount, allPrice, addCart, delCart, singleCheck } } ) ```
文件名:views/CartList/index.vue
```vue ``` #### 全选功能 ![1721733716080](assets/购物车-列表购物车3.png)
文件名:stores/cart.js
```js export const useCartStore = defineStore('cart', () => { // 定义state const cartList = ref([]) // 全选功能 const allCheck = (selected) => { // 把cartList中所有项的selected都设置为当前的全选框状态 cartList.value.forEach(item => item.selected = selected) }; // 是否全选 const isAll = computed(() => { return cartList.value.every((item) => item.selected) }) return { cartList, allCount, allPrice, isAll, addCart, delCart, singleCheck, allCheck } ) ```
文件名:views/CartList/index.vue
```vue ``` #### 统计计算实现 ![1721734000357](assets/购物车-列表购物车4.png)
文件名:stores/cart.js
```js export const useCartStore = defineStore('cart', () => { // 定义state const cartList = ref([]) // 3.选中数量 const selectedCount = computed(() => { return cartList.value .filter(item => item.selected) .reduce((x, y) => x + y.count, 0) }) // 4.选中商品总价格 const selectedPrice = computed(() => { return cartList.value .filter(item => item.selected) .reduce((x, y) => x + y.count * y.price, 0) }) return { cartList, allCount, allPrice, isAll, selectedCount, selectedPrice, addCart, delCart, singleCheck, allCheck } } ) ```
文件名:views/CartList/index.vue
```vue ``` ## 接口购物车 ![1721725268479](assets/购物车1.png) ### 加入购物车(优化本地购物车逻辑) ![1721798746101](assets/购物车-接口购物车1.png) #### 1.封装购物车接口
文件名:apis/cart.js
```js // 封装购物车相关接口 import request from '@/utils/http' // 加入购物车 export const insertCartAPI = ({ skuId, count }) => { return request({ url: '/member/cart', method: 'POST', data: { skuId, count } }) } // 获取最新购物车列表 export const findNewCartListAPI = () => { return request({ url: '/member/cart', }) } ``` #### 2.加入购物车实现
文件名:stores/cart.js
```js // 在cartStore中使用userStore import { useUserStore } from './user' import { insertCartAPI, findNewCartListAPI } from '@/apis/cart' export const useCartStore = defineStore('cart', () => { const userStore = useUserStore() const isLogin = computed(() => userStore.userInfo.token) // 定义state const cartList = ref([]) // 定义action // 加入购物车 const addCart = async (good) => { // console.log('加入购物车action执行') if (isLogin) { // 登录之后的加入购物车逻辑 const { skuId, count } = good await insertCartAPI({ skuId, count }) const res = await findNewCartListAPI() cartList.value = res.result } else { // 本地购物车添加入购物车逻辑 // 已添加过:count + 1 // 没有添加过:直接push // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过 const item = cartList.value.find((item) => good.skuId === item.skuId) if (item) { // 找到啦 item.count++ } else { // 没找到 cartList.value.push(good) } } } } ) ``` ### 删除购物车 ![1721804291811](assets/购物车-接口购物车2.png) #### 1.封装购物车接口
文件名:apis/cart.js
```js // 删除购物车 export const delCartAPI = (ids) => { return request({ url: '/member/cart', method: 'DELETE', data: { ids } }) } ``` #### 2.加入购物车实现
文件名:stores/cart.js
```js import { insertCartAPI, findNewCartListAPI, delCartAPI } from '@/apis/cart' // 删除购物车 const delCart = async (skuId) => { // console.log('删除购物车action执行') if (isLogin) { await delCartAPI([skuId]) // 获取最新购物车 const res = await findNewCartListAPI() cartList.value = res.result } else { // 方法一:找到要删除项的下标值 splice // 方法二:使用数组的过滤方法 filter const idx = cartList.value.findIndex((item) => item.skuId === skuId) cartList.value.splice(idx, 1) } } ``` ### 清空购物车 ![1721805324566](assets/购物车-接口购物车3.png)
文件名:stores/cart.js
```js export const useCartStore = defineStore('cart', () => { // 清除购物车 const clearCart = () => { cartList.value = [] } } ) ```
文件名:stores/user.js
```js // 在userStore中使用CartStore import { useCartStore } from "@/stores/cart"; // 组合式写法 export const useUserStore = defineStore('user', () => { const cartStore = useCartStore() // 清除用户信息:会同步清空local Storage(由pinia-plugin-persistedstate插件实现) const clearUserInfo = () => { userInfo.value = {} // 清空用户购物车 cartStore.clearCart() } } ) ``` ## 合并本地购物车到服务器 ![1721805867231](assets/购物车2.png) ### 1.封装接口
文件名:apis/cart.js
```js // 合并购物车 export const mergeCartAPI = (data) => { return request({ url: '/member/cart/merge', method: 'POST', data }) } ``` ### 2.合并本地购物车到服务器实现 在用户登录时,进行合并购物车操作 ```js 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 }) // console.log(res) userInfo.value = res.result // 合并购物车操作 const data = cartStore.cartList.map(item => { return { skuId: item.skuId, selected: item.selected, count: item.count } }) await mergeCartAPI(data) // 获取最新购物车列表 cartStore.updateNewCartList() } } ) ``` # 订单页 结算页 ## 路由配置和基础数据渲染 ### 路由配置 ![1721810709877](assets/订单页-路由配置.png) #### 1. 准备组件模版
文件名:views/Checkout/index.vue
```vue ``` #### 2. 配置路由规则
文件名:router/index.js
```js const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, { name: 'category', path: 'category/:id', component: Category }, { name:'subCategory', path: '/category/sub/:id', component: SubCategory }, { name:'detail', path: '/detail/:id', component: ProductDetail }, { name: 'cartList', path: 'cartList', component: CartList }, { name: 'checkout', path: 'checkout', component: Checkout } ] }, { // 登录页 name: 'login', path: '/login', component: Login } ] }) ``` #### 3. 配置路由跳转
文件名:views/CartList/index.vue
```vue 下单结算 ``` ### 基础数据渲染 ![1721811468590](assets/订单页-基础数据渲染.png) #### 1. 封装接口
文件名:apis/checkout.js
```js import request from '@/utils/http' // 获取详情 export const getCheckInfoAPI = () => { return request({ url: '/member/order/pre' }) } ``` #### 2. 获取数据
文件名:views/Checkout/index.vue
```vue ``` #### 3. 默认地址和商品列表渲染
文件名:views/Checkout/index.vue
```vue ``` ## 切换地址 ![1721812867070](assets/订单页-地址切换.png) ### 打开弹框交互 #### 1. 准备弹框模版
文件名:views/Checkout/index.vue
```vue
  • 人:{{ item.receiver }}
  • 联系方式:{{ item.contact }}
  • 收货地址:{{ item.fullLocation + item.address }}
``` #### 2. 控制弹框打开
文件名:views/Checkout/index.vue
```vue ``` ### 地址切换交互 ![1721813725208](assets/订单页-地址切换2.png)
文件名:views/Checkout/index.vue
```vue ``` #### 总结 ![1721814465939](assets/订单页-地址切换3.png) ## 创建订单生成订单ID ![1721814969537](assets/订单页-创建订单1.png) ### 准备支付页组件并绑定路由 #### 1. 支付页组件
文件名:views/Pay/index.vue
```vue ``` #### 2. 配置路由规则
文件名:router/index.js
```js import Pay from '@/views/Pay/index.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ { // '' 默认二级路由 path: '', component: Home }, // 支付页 { name: 'pay', path: 'pay', component: Pay } ] }, ] }) ``` ### 封装生成订单接口
文件名:apis/checkout.js
```js // 创建订单 export const createOrderAPI = (data) => { return request({ url: '/member/order', method: 'POST', data }) } ``` ### 提交订单(调用接口携带id跳转路由) 点击`提交订单`按钮会做如下两件事情: - 1、调用生成订单接口,得到订单id - 2、携带订单id,完成路由跳转
文件名:views/Checkout/index.vue
```vue ``` ### 更新购物车
文件名:views/Checkout/index.vue
```vue ``` # 支付页 ## 基础数据渲染 ![1721817005279](assets/支付页-基础数据渲染.png) **核心思路:** - 封装获取订单详情的接口 - 获取关键数据并渲染 ### 1. 封装接口
文件名:apis/pay.js
```js import request from '@/utils/http' export const getOrderAPI = (id) => { return request({ url: `/member/order/${id}` }) } ``` ### 2. 获取数据渲染内容
文件名:views/Pay/index.vue
```vue ``` ## 支付功能实现 ![1721817595268](assets/支付页-支付功能.png) ### 1. 支付携带参数
文件名:views/Pay/index.vue
```vue ``` ### 2. 支付宝沙箱账号信息 | 账号 | fukuvb7569@sandbox.com | | -------- | ---------------------- | | 登录密码 | 111111 | | 支付密码 | 111111 | ## 支付结果页展示 ![1721818252722](assets/支付页-支付结果展示.png) ### 1.准备模板组件
文件名:views/Pay/components/PayBack.vue
```vue ``` ### 2、配置路由规则
文件名:router/index.js
```js import PayBack from '@/views/Pay/components/PayBack.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ // 支付页 { name: 'pay', path: 'pay', component: Pay }, // 支付结果跳转页 { name: 'payBack', path: 'paycallback', component: PayBack } ] }, ] }) ``` ### 3、根据支付结果适配支付状态
文件名:views/Pay/components/PayBack.vue
```vue ``` ### 4、获取订单数据渲染支付信息
文件名:views/Pay/components/PayBack.vue
```vue ``` ## 封装倒计时函数 ![1721819573517](assets/支付页-封装倒计时函数.png) **核心思路:** - 编写函数框架,确认函数的入参、返回值 - 编写核心倒计时逻辑实现基础倒计时功能 - 实现格式化
文件名:composables/useCountDown.js
```js // 封装倒计时逻辑函数 import { computed, onUnmounted, ref } from "vue" import dayjs from "dayjs" // npm i dayjs export const useCountDown = () => { let timer = null // 1.响应式数据 const time = ref(0) // 格式化时间 xx分xx秒 const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒')) // 2.开启倒计时函数 const start = (currentTime) => { // 开始倒计时核心逻辑 // 逻辑: 每隔1s减1 time.value = currentTime // 定时器 timer = setInterval(() => { time.value-- }, 1000) } // 组件卸载时清除定时器 onUnmounted(() => { timer && clearInterval(timer) }) return { formatTime, start } } ``` # 会员中心 ## 整体功能梳理 ![1721821830529](assets/会员中心1.png) ## 路由配置 ![1721821972075](assets/会员中心-路由配置1.png) ### 1. 准备个人中心组件模版
文件名:views/Member/index.vue
```vue ``` ### 2. 配置二级路由规则
文件名:router/index.js
```js import Member from '@/views/Member/index.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ // 会员中心 个人中心 { name: 'member', path: 'member', component: Member } ] }, }) ``` ### 3. 准备个人中心组件、我的订单组件模板
文件名:views/Member/components/UserInfo.vue
```vue ```
文件名:views/Member/components/UserOrder.vue
```vue ``` ### 4. 配置三级路由规则
文件名:router/index.js
```js import MemberInfo from '@/views/Member/components/UserInfo.vue' import MemberOrder from '@/views/Member/components/UserOrder.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ // 会员中心 个人中心 { name: 'member', path: 'member', component: Member, children: [ { path: '', component: MemberInfo }, { path: 'order', component: MemberOrder } ] } ] } ] }) ``` ## 个人中心信息渲染 ![1721822940038](assets/会员中心-个人中心1.png) ### 1. 使用Pinia数据渲染个人信息
文件名:views/Member/components/UserInfo.vue
```vue ``` ### 2. 封装猜你喜欢接口
文件名:apis/user.js
```js export const getLikeListAPI = ({ limit = 4 }) => { return request({ url: '/goods/relevant', params: { limit } }) } ``` ### 3. 渲染猜你喜欢数据
文件名:views/Member/components/UserInfo.vue
```vue ``` ## 我的订单 ### 1. 基础列表渲染 ![1721823552632](assets/会员中心-我的订单1.png)
文件名:apis/order.js
```js import request from '@/utils/http' /* params: { orderState:0, page:1, pageSize:2 } */ export const getUserOrder = (params) => { return request({ url: '/member/order', method: 'GET', params }) } ```
文件名:views/Member/components/UserOrder.vue
```vue ``` ### 2. tab切换实现 ![1721824077863](assets/会员中心-我的订单2.png)
文件名:views/Member/components/UserOrder.vue
```vue ``` ### 3. 分页逻辑实现 ![1721824458898](assets/会员中心-我的订单3.png)
文件名:views/Member/components/UserOrder.vue
```vue ``` ## 细节优化 ### 默认三级路由设置 ![1721825108793](assets/会员中心-细节优化1.png)
文件名:router/index.js
```js import MemberInfo from '@/views/Member/components/UserInfo.vue' import MemberOrder from '@/views/Member/components/UserOrder.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 路由规则:path和component对应关系 routes: [ { name: 'layout', path: '/', component: Layout, children: [ // 会员中心 个人中心 { name: 'member', path: 'member', component: Member, children: [ { path: '', // 默认 component: MemberInfo }, { path: 'order', component: MemberOrder } ] } ] } ] }) ```
文件名:views/Member/index.vue
```vue ``` ### 订单状态显示适配 ![1721825292714](assets/会员中心-细节优化2.png)
文件名:views/Member/components/UserOrder.vue
```vue ```