Vue3入门 认识Vue3 1. Vue3组合式API体验 
通过 Counter 案例 体验Vue3新引入的组合式API
并且需要注意的是Vue3是向下兼容的,也就是说你可以在Vue3的项目中继续写Vue2的代码
 
<script> export default {   data(){     return {       count:0     }   },   methods:{     addCount(){       this.count++     }   } } </script> 
 
<script setup> import { ref } from 'vue' const count = ref(0) const addCount = ()=> count.value++ </script> 
 
特点:
代码量变少 
分散式维护变成集中式维护 
 
2. Vue3更多的优势 
使用create-vue搭建Vue3项目 1. 认识create-vue 
create-vue是Vue官方新的脚手架工具,底层切换到了 vite (下一代前端工具链),为开发提供极速响应
 
2. 使用create-vue创建项目 
前置条件 - 已安装16.0或更高版本的Node.js,
C:\Users\Xu>node -v v16.16.0
可以看到这里我是16.16版本,希望不会出现依赖冲突
 
执行如下命令,这一指令将会安装并执行 create-vue
 
我去,这看着比vue2页面高级多了
熟悉项目和关键文件 
组合式API - setup选项 1. setup选项的写法和执行时机 写法
<script>   export default {     setup(){            },     beforeCreate(){            }   } </script> 
 
执行时机
在beforeCreate钩子之前执行
 
2. setup中写代码的特点 
在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用
 
<script>   export default {     setup(){       const message = 'this is message'       const logMessage = ()=>{         console.log(message)       }       // 必须return才可以       return {         message,         logMessage       }     }   } </script> 
 
3. <script setup>语法糖 
script标签添加 setup标记,不需要再写导出语句,默认会添加导出语句
 
<script setup>   const message = 'this is message'   const logMessage = ()=>{     console.log(message)   } </script> 
 
需要注意的是。setup中的this已经不指向组件实例啦,指向的undefined。
组合式API - reactive和ref函数 1. reactive 
接受对象类型数据 的参数传入并返回一个响应式的对象
 
<script setup>  // 导入  import { reactive } from 'vue'  // 执行函数 传入参数 变量接收  const state = reactive({    msg:'this is msg'  })  const setSate = ()=>{    // 修改数据更新视图    state.msg = 'this is new msg'  } </script> <template>   {{ state.msg }}   <button @click="setState">change msg</button> </template> 
 
2. ref 
接收简单类型 或者对象类型 的数据传入并返回一个响应式的对象
 
<script setup>  // 导入  import { ref } from 'vue'  // 执行函数 传入参数 变量接收  const count = ref(0)  const setCount = ()=>{    // 修改数据更新视图必须加上.value    count.value++  } </script> <template>   <button @click="setCount">{{count}}</button> </template> 
 
3. reactive 对比 ref 
都是用来生成响应式数据 
不同点
reactive不能处理简单类型的数据 
ref参数类型支持更好,但是必须通过.value做访问修改 
ref函数内部的实现依赖于reactive函数 
 
 
在实际工作中的推荐
推荐使用ref函数,减少记忆负担,小兔鲜项目都使用ref  
 
 
 
组合式API - computed 
计算属性基本思想和Vue2保持一致,组合式API下的计算属性只是修改了API写法
 
下面这个计算属性会检测原始数据的变化,如果原始数据改变了,那么计算属性也会被调用!
组合式API - watch 
侦听一个或者多个数据的变化,数据变化时执行回调函数,两个额外参数
immediate控制立刻执行 
deep开启深度侦听 
 
 
1. 侦听单个数据 <script setup>   // 1. 导入watch   import { ref, watch } from 'vue'   const count = ref(0)   // 2. 调用watch 侦听变化   watch(count, (newValue, oldValue)=>{//一个是变化之前的老值,一个是变化之后的新值     console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)   }) </script> 
 
2. 侦听多个数据 
侦听多个数据,第一个参数可以改写成数组的写法
这两个数据任何一个变化都会引起函数的触发!
 
<script setup>   // 1. 导入watch   import { ref, watch } from 'vue'   const count = ref(0)   const name = ref('cp')   // 2. 调用watch 侦听变化   watch([count, name], ([newCount, newName],[oldCount,oldName])=>{     console.log(`count或者name变化了,[newCount, newName],[oldCount,oldName])   }) </script> 
 
在侦听器创建时立即触发回调(第一次就执行),响应式数据变化之后继续执行回调
 
<script setup>   // 1. 导入watch   import { ref, watch } from 'vue'   const count = ref(0)   // 2. 调用watch 侦听变化   watch(count, (newValue, oldValue)=>{     console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)   },{     immediate: true   }) </script> 
 
4. deep 
通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行,需要开启deep
需要注意的是deep有性能损耗,尽量不要使用deep
 
<script setup>   // 1. 导入watch   import { ref, watch } from 'vue'   const state = ref({ count: 0 })   // 2. 监听对象state   watch(state, ()=>{     console.log('数据变化了')   })   const changeStateByCount = ()=>{     // 直接修改不会引发回调执行     state.value.count++   } </script> 
 
添加deep属性就可以触发了
<script setup>   // 1. 导入watch   import { ref, watch } from 'vue'   const state = ref({ count: 0 })   // 2. 监听对象state 并开启deep   watch(state, ()=>{     console.log('数据变化了')   },{deep:true})   const changeStateByCount = ()=>{     // 此时修改可以触发回调     state.value.count++   } </script> 
 
5.精确侦听对象的某个属性 需求:在不开启deep的前提下,侦听age的变化,只有age变化时才执行回调
组合式API - 生命周期函数 1. 选项式对比组合式 
2. 生命周期函数基本使用 
导入生命周期函数 
执行生命周期函数,传入回调 
 
 
<scirpt setup> import { onMounted } from 'vue' onMounted(()=>{   // 自定义逻辑 }) </script> 
 
3. 执行多次 
生命周期函数执行多次的时候,会按照顺序依次执行
 
<scirpt setup> import { onMounted } from 'vue' onMounted(()=>{   // 自定义逻辑 }) onMounted(()=>{   // 自定义逻辑 }) </script> 
 
组合式API - 父子通信 1. 父传子 这里注意setup语法糖自动为我们注册了组件,所以我们不用在写components进行注册啦
基本思想
父组件中给子组件绑定属性 
子组件内部通过props选项接收数据 
 
 
2. 子传父 
基本思想
父组件中给子组件标签通过@绑定事件 
子组件内部通过 emit 方法触发事件 
 
 
组合式API - 模版引用 
概念:通过 ref标识 获取真实的 dom对象或者组件实例对象
 
1. 基本使用 
实现步骤:
调用ref函数生成一个ref对象 
通过ref标识绑定ref对象到标签 
 
 
 
之后这个dom就被绑定到h1Ref对象身上了,同理也可以得到组件实例对象
2. defineExpose 
默认情况下在 <script setup>语法糖下组件内部的属性和方法是不开放给父组件 (也就是说这个defineExpose是写在子组件里面的)访问的,可以通过defineExpose编译宏指定哪些属性和方法容许访问
 
说明:下面的代码将子组件里面的testMessage属性暴露出来,使之可以被父组件访问到
组合式API - provide和inject 1. 作用和场景 
顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信
 
2. 跨层传递普通数据 
实现步骤
顶层组件通过 provide 函数提供数据 
底层组件通过 inject 函数提供数据 
 
 
3. 跨层传递响应式数据 
在调用provide函数时,第二个参数设置为ref对象
 
4. 跨层传递方法 
顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件的数据
 
Pinia入门 什么是pinia Pinia 是 Vue 的专属状态管理库,可以实现跨组件或页面共享状态,是 vuex 状态管理工具的替代品,和 Vuex相比,具备以下优势
提供更加简单的API (去掉了 mutation ) 
提供符合组合式API风格的API (和 Vue3 新语法统一) 
去掉了modules的概念,每一个store都是一个独立的模块 
搭配 TypeScript 一起使用提供可靠的类型推断 
 
创建空Vue项目并安装Pinia 1. 创建空Vue项目  
2. 安装Pinia并注册  
import  { createPinia } from  'pinia' const  app = createApp (App )app.use (createPinia ()) app.use (router) app.mount ('#app' ) 
 
实现counter 
核心步骤:
定义store 
组件使用store 
 
 
1- 定义store
@/stores/counter.js
import  { defineStore } from  'pinia' import  { ref } from  'vue' export  const  useCounterStore = defineStore ('counter' , ()=> {     const  count = ref (0 )      const  increment  = ( )=>{     count.value ++   }      return  {     count,     increment   } }) 
 
2- 组件使用store
<script setup>   // 1. 导入use方法   import { useCounterStore } from '@/stores/counter'   // 2. 执行方法得到store, store里有数据和方法   const counterStore = useCounterStore() </script> <template> 	<button @click="counterStore.increment">     {{ counterStore.count }}   </button> </template> 
 
实现getters 
getters直接使用计算属性即可实现
 
const  count = ref (0 )const  doubleCount = computed (() =>  count.value  * 2 )
 
异步action 
思想:action函数既支持同步也支持异步,和在组件中发送网络请求写法保持一致 步骤:
store中定义action 
组件中触发action 
 
 
1- store中定义action
const  API_URL  = 'http://geek.itheima.net/v1_0/channels' export  const  useCounterStore = defineStore ('counter' , ()=> {     const  list = ref ([])      const  loadList  = async  ( )=>{     const  res = await  axios.get (API_URL )     list.value  = res.data .data .channels    }      return  {     list,     loadList   } }) 
 
2- 组件中调用action
<script setup> 	import { useCounterStore } from '@/stores/counter'   const counterStore = useCounterStore()   // 调用异步action   counterStore.loadList() </script> <template> 	<ul>     <li v-for="item in counterStore.list" :key="item.id">{{ item.name }}</li>   </ul> </template> 
 
storeToRefs保持响应式解构 
直接基于store进行解构赋值,响应式数据(state和getter)会丢失响应式特性。
但是我们还是想用解构赋值,那么我们就可以使用storeToRefs辅助保持响应式
 
<script setup>   import { storeToRefs } from 'pinia'   import { useCounterStore } from '@/stores/counter'   const counterStore = useCounterStore()   // 使用它storeToRefs包裹之后解构保持响应式,   // 注意,这里只能解构赋值数据,不管是对象类型还是基本数据类型都可以,但是不能解构赋值方法   const { count } = storeToRefs(counterStore)   // 如果想要解构赋值方法,使用这种方法解构就好啦   const { increment } = counterStore    </script> <template> 	<button @click="increment">     {{ count }}   </button> </template> 
 
项目起步 创建项目并整理目录  
src目录调整 
jsconfig.json配置别名路径 
配置别名路径可以在写代码时联想提示路径,也就是说,在编写代码的过程中,一旦输入@/,VSCode会立刻联想出src下的所有子目录和文件,统一文件路径访问不容易出错
不过我下载了插件,这个就不用配置了
 
那么如何进行配置呢?
在项目的根目录下新增jsconfig.json文件 
添加json格式的配置项,如下: 
 
{   "compilerOptions"  :  {      "baseUrl"  :  "./" ,      "paths"  :  {        "@/*" : [ "src/*" ]      }    }  } 
 
elementPlus引入 1. 安装elementPlus和自动导入插件 npm install element-plus --save npm install -D unplugin-vue-components unplugin-auto-import 
 
2. 配置自动按需导入 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 ()],     }),   ], }) 
 
3. 测试组件 <template>   <el-button type="primary">i am button</el-button> </template> 
 
定制elementPlus主题 1. 安装sass 
基于vite的项目默认不支持css预处理器,需要开发者单独安装
 
 
2. 准备定制化的样式文件 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,     ),   ) ) 
 
3. 自动导入配置 
这里自动导入需要深入到elementPlus的组件中,按照官方的配置文档来
自动导入定制化样式文件进行样式覆盖 
按需定制主题配置 (需要安装 unplugin-element-plus) 
 
 
vite.config.js
import  { fileURLToPath, URL  } from  "node:url" ;import  { defineConfig } from  "vite" ;import  vue from  "@vitejs/plugin-vue" ;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 : [     vue (),     AutoImport ({       resolvers : [ElementPlusResolver ()],     }),     Components ({       resolvers : [ElementPlusResolver ({ importStyle : "sass"  })],     }),   ],   resolve : {     alias : {       "@" : fileURLToPath (new  URL ("./src" , import .meta .url )),     },   },   css : {     preprocessorOptions : {       scss : {                  additionalData : `            @use "@/styles/element/index.scss" as *;         ` ,      },     },   }, }); 
 
axios安装并简单封装 1. 安装axios  
2. 基础配置 
官方文档地址:https://axios-http.com/zh/docs/intro  基础配置通常包括:
实例化 - baseURL + timeout 
拦截器 - 携带token 401拦截等 
 
 
utils/http.js
import  axios from  'axios' const  httpInstance = axios.create ({  baseURL : 'http://pcapi-xiaotuxian-front-devtest.itheima.net' ,   timeout : 5000  }) httpInstance.interceptors .request .use (config  =>  {   return  config }, e  =>  Promise .reject (e)) httpInstance.interceptors .response .use (res  =>  res.data , e  =>  {   return  Promise .reject (e) }) export  default  httpInstance
 
3. 封装请求函数 apis/testAPI.js
import  httpInstance from  '@/utils/http' export  function  getCategoryAPI  () {  return  httpInstance ({     url : 'home/category/head'    }) } 
 
4.进行测试 测试在哪进行都可以
import  { getCategoryAPI } from  "@/apis/testAPI.js" ;getCategoryAPI ().then ((res ) =>  {  console .log (res); }); 
 
添加不在强制组件重命名的配置 这个是对eslint的配置
.eslintrc.cjs
module .exports  = {  root : true ,   'extends' : [     'plugin:vue/vue3-essential' ,     'eslint:recommended'    ],   parserOptions : {     ecmaVersion : 'latest'    },      rules :{     'vue/multi-word-component-names' :0 ,   } } 
 
路由整体设计 路由设计原则:找页面的切换方式,如果是整体切换 ,则为一级路由,如果是在一级路由的内部进行的内容切换,则为二级路由
view/Login/index.vue
<template >   我是Login </template > 
 
views/Layout/index.vue
<template >   我是Layout </template > 
 
二级路由:如果是在一级路由的内部进行的内容切换,则为二级路由
views/Home/index.vue
<template >   我是home </template > 
 
views/Category/index.vue
<template >   我是Category </template > 
 
router/index.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" ;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;
 
添加一级路由出口
App.vue
<script setup></script> <template>   <!-- 添加一级路由出口 -->   <router-view></router-view> </template> <style scoped></style> 
 
添加二级路由出口
view/Layout/index.vue
<template>   <div>我是layout</div>   <!-- 添加二级路由出口 -->   <router-view /> </template> <script> export default {}; </script> <style></style> 
 
静态资源引入和Error Lens安装 1. 静态资源引入 
图片资源 - 把 images 文件夹放到 assets 目录下 
样式资源 - 把 common.scss 文件放到 styles 目录下,然后在main.js中引入 
 
2. Error Lens插件安装 error lens是一个试试提供错误警告信息的VSCode插件,方便开发
scss变量自动导入 为什么要自动导入?
在项目里一些组件共享的色值会以scss变量的方式统一放到一个名叫var.scss的文件中,正常组件中使用这些变量的时候,需要先导入var.scss文件。再使用内部的变量,比较繁琐,自动导入可以免去手动导入的步骤,直接使用内部的变量
styles/var.scss
$xtxColor: #27ba9b ; $helpColor: #e26237 ; $sucColor: #1dc779 ; $warnColor: #ffb302 ; $priceColor: #cf4444 ; 
 
vite.config.js
css:  {      preprocessorOptions:  {        scss:  {                   additionalData:  `           @use "@/styles/element/index.scss"  as *;           @use "@/styles/var.scss"  as *;         `,        }      }  } 
 
测试:
<template>   <div class="test">我是layout</div>   <!-- 添加二级路由出口 -->   <router-view /> </template> <script> export default {}; </script> <!-- 这里注意要加lang --> <style scoped lang="scss"> .test {   // 直接在这里使用就可以了   color: $priceColor; } </style> 
 
Layout页 
组件结构快速搭建 view/Latout/components/LayoutNav.vue
<script setup> </script> <template>   <nav class="app-topnav">     <div class="container">       <ul>         <template v-if="true">           <li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li>           <li>             <el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">               <template #reference>                 <a href="javascript:;">退出登录</a>               </template>             </el-popconfirm>           </li>           <li><a href="javascript:;">我的订单</a></li>           <li><a href="javascript:;">会员中心</a></li>         </template>         <template v-else>           <li><a href="javascript:;">请先登录</a></li>           <li><a href="javascript:;">帮助中心</a></li>           <li><a href="javascript:;">关于我们</a></li>         </template>       </ul>     </div>   </nav> </template> <style scoped lang="scss"> .app-topnav {   background: #333;   ul {     display: flex;     height: 53px;     justify-content: flex-end;     align-items: center;     li {       a {         padding: 0 15px;         color: #cdcdcd;         line-height: 1;         display: inline-block;         i {           font-size: 14px;           margin-right: 2px;         }         &:hover {           color: $xtxColor;         }       }       ~li {         a {           border-left: 2px solid #666;         }       }     }   } } </style> 
 
view/Latout/components/LayoutHeader.vue
<script setup> </script> <template>   <header class='app-header'>     <div class="container">       <h1 class="logo">         <RouterLink to="/">小兔鲜</RouterLink>       </h1>       <ul class="app-header-nav">         <li class="home">           <RouterLink to="/">首页</RouterLink>         </li>         <li> <RouterLink to="/">居家</RouterLink> </li>         <li> <RouterLink to="/">美食</RouterLink> </li>         <li> <RouterLink to="/">服饰</RouterLink> </li>       </ul>       <div class="search">         <i class="iconfont icon-search"></i>         <input type="text" placeholder="搜一搜">       </div>       <!-- 头部购物车 -->            </div>   </header> </template> <style scoped lang='scss'> .app-header {   background: #fff;   .container {     display: flex;     align-items: center;   }   .logo {     width: 200px;     a {       display: block;       height: 132px;       width: 100%;       text-indent: -9999px;       background: url('@/assets/images/logo.png') no-repeat center 18px / contain;     }   }   .app-header-nav {     width: 820px;     display: flex;     padding-left: 40px;     position: relative;     z-index: 998;        li {       margin-right: 40px;       width: 38px;       text-align: center;          a {         font-size: 16px;         line-height: 32px;         height: 32px;         display: inline-block;            &:hover {           color: $xtxColor;           border-bottom: 1px solid $xtxColor;         }       }          .active {         color: $xtxColor;         border-bottom: 1px solid $xtxColor;       }     }   }   .search {     width: 170px;     height: 32px;     position: relative;     border-bottom: 1px solid #e7e7e7;     line-height: 32px;     .icon-search {       font-size: 18px;       margin-left: 5px;     }     input {       width: 140px;       padding-left: 5px;       color: #666;     }   }   .cart {     width: 50px;     .curr {       height: 32px;       line-height: 32px;       text-align: center;       position: relative;       display: block;       .icon-cart {         font-size: 22px;       }       em {         font-style: normal;         position: absolute;         right: 0;         top: 0;         padding: 1px 6px;         line-height: 1;         background: $helpColor;         color: #fff;         font-size: 12px;         border-radius: 10px;         font-family: Arial;       }     }   } } </style> 
 
view/Latout/components/LayoutFooter.vue
<template>   <footer class="app_footer">     <!-- 联系我们 -->     <div class="contact">       <div class="container">         <dl>           <dt>客户服务</dt>           <dd><i class="iconfont icon-kefu"></i> 在线客服</dd>           <dd><i class="iconfont icon-question"></i> 问题反馈</dd>         </dl>         <dl>           <dt>关注我们</dt>           <dd><i class="iconfont icon-weixin"></i> 公众号</dd>           <dd><i class="iconfont icon-weibo"></i> 微博</dd>         </dl>         <dl>           <dt>下载APP</dt>           <dd class="qrcode"><img src="@/assets/images/qrcode.jpg" /></dd>           <dd class="download">             <span>扫描二维码</span>             <span>立马下载APP</span>             <a href="javascript:;">下载页面</a>           </dd>         </dl>         <dl>           <dt>服务热线</dt>           <dd class="hotline">400-0000-000 <small>周一至周日 8:00-18:00</small></dd>         </dl>       </div>     </div>     <!-- 其它 -->     <div class="extra">       <div class="container">         <div class="slogan">           <a href="javascript:;">             <i class="iconfont icon-footer01"></i>             <span>价格亲民</span>           </a>           <a href="javascript:;">             <i class="iconfont icon-footer02"></i>             <span>物流快捷</span>           </a>           <a href="javascript:;">             <i class="iconfont icon-footer03"></i>             <span>品质新鲜</span>           </a>         </div>         <!-- 版权信息 -->         <div class="copyright">           <p>             <a href="javascript:;">关于我们</a>             <a href="javascript:;">帮助中心</a>             <a href="javascript:;">售后服务</a>             <a href="javascript:;">配送与验收</a>             <a href="javascript:;">商务合作</a>             <a href="javascript:;">搜索推荐</a>             <a href="javascript:;">友情链接</a>           </p>           <p>CopyRight © 小兔鲜儿</p>         </div>       </div>     </div>   </footer> </template> <style scoped lang='scss'> .app_footer {   overflow: hidden;   background-color: #f5f5f5;   padding-top: 20px;   .contact {     background: #fff;     .container {       padding: 60px 0 40px 25px;       display: flex;     }     dl {       height: 190px;       text-align: center;       padding: 0 72px;       border-right: 1px solid #f2f2f2;       color: #999;       &:first-child {         padding-left: 0;       }       &:last-child {         border-right: none;         padding-right: 0;       }     }     dt {       line-height: 1;       font-size: 18px;     }     dd {       margin: 36px 12px 0 0;       float: left;       width: 92px;       height: 92px;       padding-top: 10px;       border: 1px solid #ededed;       .iconfont {         font-size: 36px;         display: block;         color: #666;       }       &:hover {         .iconfont {           color: $xtxColor;         }       }       &:last-child {         margin-right: 0;       }     }     .qrcode {       width: 92px;       height: 92px;       padding: 7px;       border: 1px solid #ededed;     }     .download {       padding-top: 5px;       font-size: 14px;       width: auto;       height: auto;       border: none;       span {         display: block;       }       a {         display: block;         line-height: 1;         padding: 10px 25px;         margin-top: 5px;         color: #fff;         border-radius: 2px;         background-color: $xtxColor;       }     }     .hotline {       padding-top: 20px;       font-size: 22px;       color: #666;       width: auto;       height: auto;       border: none;       small {         display: block;         font-size: 15px;         color: #999;       }     }   }   .extra {     background-color: #333;   }   .slogan {     height: 178px;     line-height: 58px;     padding: 60px 100px;     border-bottom: 1px solid #434343;     display: flex;     justify-content: space-between;     a {       height: 58px;       line-height: 58px;       color: #fff;       font-size: 28px;       i {         font-size: 50px;         vertical-align: middle;         margin-right: 10px;         font-weight: 100;       }       span {         vertical-align: middle;         text-shadow: 0 0 1px #333;       }     }   }   .copyright {     height: 170px;     padding-top: 40px;     text-align: center;     color: #999;     font-size: 15px;     p {       line-height: 1;       margin-bottom: 20px;     }     a {       color: #999;       line-height: 1;       padding: 0 10px;       border-right: 1px solid #999;       &:last-child {         border-right: none;       }     }   } } </style> 
 
view/Latout/index.vue
<script setup> import LayoutNav from './components/LayoutNav.vue' import LayoutHeader from './components/LayoutHeader.vue' import LayoutFooter from './components/LayoutFooter.vue' </script> <template>   <LayoutNav />   <LayoutHeader />   <RouterView />   <LayoutFooter /> </template> 
 
字体图标渲染 
字体图标采用的是阿里的字体图标库,样式文件已经准备好,在 index.html文件中引入即可
 
<link  rel ="stylesheet"  href ="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css" > 
 
效果:
一级导航渲染 要把上面的导航信息根据后端传过来的数据进行渲染
实现步骤 
封装接口函数 
调用接口函数 
v-for渲染模版 
 
代码落地 
apis/layout.js
import  httpInstance from  '@/utils/http' export  function  getCategoryAPI  () {  return  httpInstance ({     url : '/home/category/head'    }) } 
 
views/components/LayoutHeader.vue
<script setup> // 引入api import { getCategoryAPI } from "@/apis/layout"; import { onMounted, ref } from "vue"; const categoryList = ref([]); const getCategory = async () => {   const res = await getCategoryAPI();   // 赋值   categoryList.value = res.result; }; onMounted(() => getCategory()); </script> <template>   <header class="app-header">     <div class="container">       <h1 class="logo">         <RouterLink to="/">小兔鲜</RouterLink>       </h1>       <ul class="app-header-nav">         <!-- 遍历list中的数据,渲染列表 -->         <li class="home" v-for="item in categoryList" :key="item.id">           <RouterLink to="/">{{ item.name }}</RouterLink>         </li>       </ul>       <div class="search">         <i class="iconfont icon-search"></i>         <input type="text" placeholder="搜一搜" />       </div>       <!-- 头部购物车 -->     </div>   </header> </template> 
 
吸顶导航交互实现 需求:浏览器在上下滚动的过程中,如果距离顶部的滚动距离大于78px,吸顶导航就显示,如果小于78px就隐藏 
1. 准备组件静态结构 view/Layout/components/LayoutFixed.vue
<script setup> </script> <template>   <div class="app-header-sticky show">     <div class="container">       <RouterLink class="logo" to="/" />       <!-- 导航区域 -->       <ul class="app-header-nav ">         <li class="home">           <RouterLink to="/">首页</RouterLink>         </li>         <li>           <RouterLink to="/">居家</RouterLink>         </li>         <li>           <RouterLink to="/">美食</RouterLink>         </li>         <li>           <RouterLink to="/">服饰</RouterLink>         </li>         <li>           <RouterLink to="/">母婴</RouterLink>         </li>         <li>           <RouterLink to="/">个护</RouterLink>         </li>         <li>           <RouterLink to="/">严选</RouterLink>         </li>         <li>           <RouterLink to="/">数码</RouterLink>         </li>         <li>           <RouterLink to="/">运动</RouterLink>         </li>         <li>           <RouterLink to="/">杂项</RouterLink>         </li>       </ul>       <div class="right">         <RouterLink to="/">品牌</RouterLink>         <RouterLink to="/">专题</RouterLink>       </div>     </div>   </div> </template> <style scoped lang='scss'> .app-header-sticky {   width: 100%;   height: 80px;   position: fixed;   left: 0;   top: 0;   z-index: 999;   background-color: #fff;   border-bottom: 1px solid #e4e4e4;   // 此处为关键样式!!!   // 状态一:往上平移自身高度 + 完全透明   transform: translateY(-100%);   opacity: 0;   // 状态二:移除平移 + 完全不透明   &.show {     transition: all 0.3s linear;     transform: none;     opacity: 1;   }   .container {     display: flex;     align-items: center;   }   .logo {     width: 200px;     height: 80px;     background: url("@/assets/images/logo.png") no-repeat right 2px;     background-size: 160px auto;   }   .right {     width: 220px;     display: flex;     text-align: center;     padding-left: 40px;     border-left: 2px solid $xtxColor;     a {       width: 38px;       margin-right: 40px;       font-size: 16px;       line-height: 1;       &:hover {         color: $xtxColor;       }     }   } } .app-header-nav {   width: 820px;   display: flex;   padding-left: 40px;   position: relative;   z-index: 998;   li {     margin-right: 40px;     width: 38px;     text-align: center;     a {       font-size: 16px;       line-height: 32px;       height: 32px;       display: inline-block;       &:hover {         color: $xtxColor;         border-bottom: 1px solid $xtxColor;       }     }     .active {       color: $xtxColor;       border-bottom: 1px solid $xtxColor;     }   } } </style> 
 
然后引入到
2. 获取滚动距离 安装vueuse
npm i @vueuse/core
用vueuse的函数来实现获取滚动的距离
3. 以滚动距离做判断条件控制组件盒子显示隐藏 
核心逻辑:根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示
 
view/Layout/components/LayoutFixed.vue
<script setup> // vueUse import { useScroll } from '@vueuse/core' const { y } = useScroll(window) </script> <template>   <!-- 以滚动距离做判断条件控制组件盒子显示隐藏 -->   <div class="app-header-sticky" :class="{ show: y > 78 }">     <!-- 省略部分代码 -->   </div> </template> 
 
Pinia优化重复请求 
store/category.js
import  { ref } from  'vue' import  { defineStore } from  'pinia' import  { getCategoryAPI } from  '@/apis/layout' export  const  useCategoryStore = defineStore ('category' , () =>  {        const  categoryList = ref ([])      const  getCategory  = async  ( ) => {     const  res = await  getCategoryAPI ()     categoryList.value  = res.result    }   return  {     categoryList,     getCategory   } }) 
 
在index页面中调用action初始化数据
然后就是在两个组件中使用了
另一个组件同理
Home页 静态结构搭建和分类实现 1. 整体结构创建 
1- 按照结构新增五个组件,准备最简单的模版,分别在Home模块的入口组件中引入
HomeCategory  轮播图左侧分类 
HomeBanner 轮播图 
HomeNew 新鲜好物 
HomeHot 人气推荐 
HomeProduct 产品列表 
 
<script setup> </script> <template>   <div> HomeCategory </div> </template> 
 
剩下的四个省略
2- Home模块入口组件中引入并渲染
views/Home/index.vue
<script setup> import HomeCategory from './components/HomeCategory.vue' import HomeBanner from './components/HomeBanner.vue' import HomeNew from './components/HomeNew.vue' import HomeHot from './components/HomeHot.vue' import homeProduct from './components/HomeProduct.vue' </script> <template>   <div class="container">     <HomeCategory />     <HomeBanner />   </div>   <HomeNew />   <HomeHot />   <homeProduct /> </template> 
 
2. 分类实现 1- 准备详细模版
HomeCategory.vue
<script setup> </script> <template>   <div class="home-category">     <ul class="menu">       <li v-for="item in 9" :key="item">         <RouterLink to="/">居家</RouterLink>         <RouterLink v-for="i in 2" :key="i" to="/">南北干货</RouterLink>         <!-- 弹层layer位置 -->         <div class="layer">           <h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>           <ul>             <li v-for="i in 5" :key="i">               <RouterLink to="/">                 <img alt="" />                 <div class="info">                   <p class="name ellipsis-2">                     男士外套                   </p>                   <p class="desc ellipsis">男士外套,冬季必选</p>                   <p class="price"><i>¥</i>200.00</p>                 </div>               </RouterLink>             </li>           </ul>         </div>       </li>     </ul>   </div> </template> <style scoped lang='scss'> .home-category {   width: 250px;   height: 500px;   background: rgba(0, 0, 0, 0.8);   position: relative;   z-index: 99;   .menu {     li {       padding-left: 40px;       height: 55px;       line-height: 55px;       &:hover {         background: $xtxColor;       }       a {         margin-right: 4px;         color: #fff;         &:first-child {           font-size: 16px;         }       }       .layer {         width: 990px;         height: 500px;         background: rgba(255, 255, 255, 0.8);         position: absolute;         left: 250px;         top: 0;         display: none;         padding: 0 15px;         h4 {           font-size: 20px;           font-weight: normal;           line-height: 80px;           small {             font-size: 16px;             color: #666;           }         }         ul {           display: flex;           flex-wrap: wrap;           li {             width: 310px;             height: 120px;             margin-right: 15px;             margin-bottom: 15px;             border: 1px solid #eee;             border-radius: 4px;             background: #fff;             &:nth-child(3n) {               margin-right: 0;             }             a {               display: flex;               width: 100%;               height: 100%;               align-items: center;               padding: 10px;               &:hover {                 background: #e3f9f4;               }               img {                 width: 95px;                 height: 95px;               }               .info {                 padding-left: 10px;                 line-height: 24px;                 overflow: hidden;                 .name {                   font-size: 16px;                   color: #666;                 }                 .desc {                   color: #999;                 }                 .price {                   font-size: 22px;                   color: $priceColor;                   i {                     font-size: 16px;                   }                 }               }             }           }         }       }       // 关键样式  hover状态下的layer盒子变成block       &:hover {         .layer {           display: block;         }       }     }   } } </style> 
 
2- 完成代码
HomeCategory.vue
<script setup> import { useCategoryStore } from "@/stores/category"; const categoryStore = useCategoryStore(); </script> <template>   <div class="home-category">     <ul class="menu">       <li v-for="item in categoryStore.categoryList" :key="item.id">         <!-- 一级标题 -->         <RouterLink to="/">{{ item.name }}</RouterLink>         <!-- 二级标题,但是只需要显示两个就可以了 -->         <RouterLink v-for="i in item.children.slice(0, 2)" :key="i" to="/">{{           i.name         }}</RouterLink>         <!-- 弹层layer位置 -->         <div class="layer">           <h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>           <ul>             <!-- 遍历goods信息 -->             <li v-for="i in item.goods" :key="i.id">               <RouterLink to="/">                 <img :src="i.picture" alt="" />                 <div class="info">                   <p class="name ellipsis-2">                     {{ i.name }}                   </p>                   <p class="desc ellipsis">{{ i.desc }}</p>                   <p class="price"><i>¥</i>{{ i.price }}</p>                 </div>               </RouterLink>             </li>           </ul>         </div>       </li>     </ul>   </div> </template> 
 
banner轮播图实现 1. 熟悉组件 这里直接使用elment-ui的组件进行完成
src\views\Home\components\HomeBanner.vue
<script setup> </script> <template>   <div class="home-banner">     <el-carousel height="500px">       <el-carousel-item v-for="item in 4" :key="item">         <img src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/6d202d8e-bb47-4f92-9523-f32ab65754f4.jpg" alt="">       </el-carousel-item>     </el-carousel>   </div> </template> <style scoped lang='scss'> .home-banner {   width: 1240px;   height: 500px;   position: absolute;   left: 0;   top: 0;   z-index: 98;   img {     width: 100%;     height: 500px;   } } </style> 
 
2. 获取数据渲染组件 1- 封装接口
src\apis\home.js
import  httpInstance from  "@/utils/http" ;export  function  getBannerAPI ( ) {  return  httpInstance ({     url : "home/banner" ,   }); } 
 
2- 获取数据渲染模版
src\views\Home\components\HomeBanner.vue
<script setup> import { getBannerAPI } from '@/apis/home' import { onMounted, ref } from 'vue' const bannerList = ref([]) const getBanner = async () => {   const res = await getBannerAPI()   console.log(res)   bannerList.value = res.result } onMounted(() => getBanner()) </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> 
 
面板组件封装 
并且我们把需要改变的部分可以抽象成组件参数(插槽/props)
1. 纯静态结构 HomePanel.vue
<script setup> </script> <template>   <div class="home-panel">     <div class="container">       <div class="head">          <!-- 主标题和副标题 -->         <h3>           新鲜好物<small>新鲜出炉 品质靠谱</small>         </h3>       </div>       <!-- 主体内容区域 -->       <div> 主体内容 </div>     </div>   </div> </template> <style scoped lang='scss'> .home-panel {   background-color: #fff;   .head {     padding: 40px 0;     display: flex;     align-items: flex-end;     h3 {       flex: 1;       font-size: 32px;       font-weight: normal;       margin-left: 6px;       height: 35px;       line-height: 35px;       small {         font-size: 16px;         color: #999;         margin-left: 20px;       }     }   } } </style> 
 
2. 完整代码 HomePanel.vue
<script setup> defineProps({   title: {     type: String,     default: ''   },   subTitle: {     type: String,     default: ''   } }) </script> <template>   <div class="home-panel">     <div class="container">       <div class="head">         <!-- 主标题和副标题 -->         <h3>           {{ title }}             <small>{{ subTitle }}</small>         </h3>       </div>       <!-- 主体内容区域 -->       <slot name="main" />     </div>   </div> </template> <style scoped lang='scss'> .home-panel {   background-color: #fff;   .head {     padding: 40px 0;     display: flex;     align-items: flex-end;     h3 {       flex: 1;       font-size: 32px;       font-weight: normal;       margin-left: 6px;       height: 35px;       line-height: 35px;       small {         font-size: 16px;         color: #999;         margin-left: 20px;       }     }   } } </style> 
 
新鲜好物实现 1. 准备模版 src\views\Home\components\HomeNew.vue
<script setup> </script> <template>   <div></div>   <!-- 下面是插槽主体内容模版   <ul class="goods-list">     <li v-for="item in newList" :key="item.id">       <RouterLink to="/">         <img :src="item.picture" alt="" />         <p class="name">{{ item.name }}</p>         <p class="price">¥{{ item.price }}</p>       </RouterLink>     </li>   </ul>   --> </template> <style scoped lang='scss'> .goods-list {   display: flex;   justify-content: space-between;   height: 406px;   li {     width: 306px;     height: 406px;     background: #f0f9f4;     transition: all .5s;     &:hover {       transform: translate3d(0, -3px, 0);       box-shadow: 0 3px 8px rgb(0 0 0 / 20%);     }     img {       width: 306px;       height: 306px;     }     p {       font-size: 22px;       padding-top: 12px;       text-align: center;       text-overflow: ellipsis;       overflow: hidden;       white-space: nowrap;     }     .price {       color: $priceColor;     }   } } </style> 
 
2. 封装接口 home.js
export  const  findNewAPI  = ( ) => {  return  httpInstance ({     url :'/home/new'    }) } 
 
3. 获取数据渲染模版 src\views\Home\components\HomeNew.vue
<script setup> import HomePanel from "./HomePanel.vue"; import { findNewAPI } from "@/apis/home"; import { ref } from "vue"; const newList = ref([]); const getNewList = async () => {   const res = await findNewAPI();   newList.value = res.result; }; getNewList(); </script> <template>   <HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱">     <template #main>       <ul class="goods-list">         <li v-for="item in newList" :key="item.id">           <RouterLink :to="`/detail/${item.id}`">             <img :src="item.picture" alt="" />             <p class="name">{{ item.name }}</p>             <p class="price">¥{{ item.price }}</p>           </RouterLink>         </li>       </ul>     </template>   </HomePanel> </template> <style scoped lang="scss"> .goods-list {   display: flex;   justify-content: space-between;   height: 406px;   li {     width: 306px;     height: 406px;     background: #f0f9f4;     transition: all 0.5s;     &:hover {       transform: translate3d(0, -3px, 0);       box-shadow: 0 3px 8px rgb(0 0 0 / 20%);     }     img {       width: 306px;       height: 306px;     }     p {       font-size: 22px;       padding-top: 12px;       text-align: center;       text-overflow: ellipsis;       overflow: hidden;       white-space: nowrap;     }     .price {       color: $priceColor;     }   } } </style> 
 
人气推荐实现 1. 封装接口 src\apis\home.js
export  const  getHotAPI  = ( ) => {  return   httpInstance ('home/hot' , 'get' , {}) } 
 
2. 获取数据渲染模版 src\views\Home\components\HomeHot.vue
<script setup> import HomePanel from "./HomePanel.vue"; import { getHotAPI } from "@/apis/home"; import { ref } from "vue"; const hotList = ref([]); const getHotList = async () => {   const res = await getHotAPI();   hotList.value = res.result; }; getHotList(); </script> <template>   <HomePanel title="人气推荐" sub-title="人气爆款 不容错过">     <template #main>       <ul class="goods-list">         <li v-for="item in hotList" :key="item.id">           <RouterLink to="/">             <img :src="item.picture" alt="" />             <p class="name">{{ item.title }}</p>             <p class="desc">{{ item.alt }}</p>           </RouterLink>         </li>       </ul>     </template>   </HomePanel> </template> <style scoped lang="scss"> .goods-list {   display: flex;   justify-content: space-between;   height: 426px;   li {     width: 306px;     height: 406px;     transition: all 0.5s;     &:hover {       transform: translate3d(0, -3px, 0);       box-shadow: 0 3px 8px rgb(0 0 0 / 20%);     }     img {       width: 306px;       height: 306px;     }     p {       font-size: 22px;       padding-top: 12px;       text-align: center;     }     .desc {       color: #999;       font-size: 18px;     }   } } </style> 
 
懒加载指令实现 1. 封装全局指令 import  { useIntersectionObserver } from  "@vueuse/core" ;export  const  lazyPlugin = {  install (app ) {          app.directive ("img-lazy" , {       mounted (el, binding ) {                           console .log (el, binding.value );         const  { stop } = useIntersectionObserver (el, ([{ isIntersecting }] ) =>  {           console .log (isIntersecting);           if  (isIntersecting) {                          el.src  = binding.value ;                          stop ();           }         });       },     });   }, }; 
 
2. 注册全局指令 import  { lazyPlugin } from  "@/directives" ;app.use (lazyPlugin); 
 
3.修改HomeHot <template>   <HomePanel title="人气推荐" sub-title="人气爆款 不容错过">     <template #main>       <ul class="goods-list">         <li v-for="item in hotList" :key="item.id">           <RouterLink to="/">             <!-- 这里使用了懒加载 -->             <img :src="item.picture" alt="" />             <p class="name">{{ item.title }}</p>             <p class="desc">{{ item.alt }}</p>           </RouterLink>         </li>       </ul>     </template>   </HomePanel> </template> 
 
Product产品列表实现 1. 基础数据渲染 1- 准备静态模版
HomeProduct.vue
<script setup> import HomePanel from './HomePanel.vue' </script> <template>   <div class="home-product">     <!-- <HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">       <div class="box">         <RouterLink class="cover" to="/">           <img :src="cate.picture" />           <strong class="label">             <span>{{ cate.name }}馆</span>             <span>{{ cate.saleInfo }}</span>           </strong>         </RouterLink>         <ul class="goods-list">           <li v-for="good in cate.goods" :key="good.id">             <RouterLink to="/" class="goods-item">               <img :src="good.picture" alt="" />               <p class="name ellipsis">{{ good.name }}</p>               <p class="desc ellipsis">{{ good.desc }}</p>               <p class="price">¥{{ good.price }}</p>             </RouterLink>           </li>         </ul>       </div>     </HomePanel> -->   </div> </template> <style scoped lang='scss'> .home-product {   background: #fff;   margin-top: 20px;   .sub {     margin-bottom: 2px;     a {       padding: 2px 12px;       font-size: 16px;       border-radius: 4px;       &:hover {         background: $xtxColor;         color: #fff;       }       &:last-child {         margin-right: 80px;       }     }   }   .box {     display: flex;     .cover {       width: 240px;       height: 610px;       margin-right: 10px;       position: relative;       img {         width: 100%;         height: 100%;       }       .label {         width: 188px;         height: 66px;         display: flex;         font-size: 18px;         color: #fff;         line-height: 66px;         font-weight: normal;         position: absolute;         left: 0;         top: 50%;         transform: translate3d(0, -50%, 0);         span {           text-align: center;           &:first-child {             width: 76px;             background: rgba(0, 0, 0, 0.9);           }           &:last-child {             flex: 1;             background: rgba(0, 0, 0, 0.7);           }         }       }     }     .goods-list {       width: 990px;       display: flex;       flex-wrap: wrap;       li {         width: 240px;         height: 300px;         margin-right: 10px;         margin-bottom: 10px;         &:nth-last-child(-n + 4) {           margin-bottom: 0;         }         &:nth-child(4n) {           margin-right: 0;         }       }     }     .goods-item {       display: block;       width: 220px;       padding: 20px 30px;       text-align: center;       transition: all .5s;       &:hover {         transform: translate3d(0, -3px, 0);         box-shadow: 0 3px 8px rgb(0 0 0 / 20%);       }       img {         width: 160px;         height: 160px;       }       p {         padding-top: 10px;       }       .name {         font-size: 16px;       }       .desc {         color: #999;         height: 29px;       }       .price {         color: $priceColor;         font-size: 20px;       }     }   } } </style> 
 
2- 封装接口
src\apis\home.js
export  const  getGoodsAPI  = ( ) => {  return  httpInstance ({     url : '/home/goods'    }) } 
 
3- 获取并渲染数据
<script setup> import HomePanel from "./HomePanel.vue"; import { getGoodsAPI } from "@/apis/home"; import { ref, onMounted } from "vue"; const goodsProduct = ref([]); const getGoods = async () => {   const { result } = await getGoodsAPI();   goodsProduct.value = result; }; onMounted(() => getGoods()); </script> <template>   <div class="home-product">     <HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">       <template #main>         <div class="box">           <RouterLink class="cover" to="/">             <img :src="cate.picture" />             <strong class="label">               <span>{{ cate.name }}馆</span>               <span>{{ cate.saleInfo }}</span>             </strong>           </RouterLink>           <ul class="goods-list">             <li v-for="goods in cate.goods" :key="goods.id">               <RouterLink to="/" class="goods-item">                 <img :src="goods.picture" alt="" />                 <p class="name ellipsis">{{ goods.name }}</p>                 <p class="desc ellipsis">{{ goods.desc }}</p>                 <p class="price">¥{{ goods.price }}</p>               </RouterLink>             </li>           </ul>         </div>       </template>     </HomePanel>   </div> </template> 
 
2. 图片懒加载 <div  class ="home-product" >   <HomePanel  :title ="cate.name"  v-for ="cate in goodsProduct"  :key ="cate.id" >      <div  class ="box" >        <RouterLink  class ="cover"  to ="/" >                   <img  v-img-lazy ="cate.picture"  />        </RouterLink >        <ul  class ="goods-list" >          <li  v-for ="goods in cate.goods"  :key ="goods.id" >            <RouterLink  to ="/"  class ="goods-item" >                            <img  v-img-lazy ="goods.picture"  alt =""  />            </RouterLink >          </li >        </ul >      </div >    </HomePanel >  </div > 
 
GoodsItem组件封装 由于下面这个组件经常用,所以我们可以给他封装成一个组件
1. 封装组件 src\views\Home\components\GoodsItem.vue
<script setup> defineProps({   goods: {     type: Object,     default: () => { }   } }) </script> <template>   <RouterLink to="/" class="goods-item">     <img :src="goods.picture" alt="" />     <p class="name ellipsis">{{ goods.name }}</p>     <p class="desc ellipsis">{{ goods.desc }}</p>     <p class="price">¥{{ goods.price }}</p>   </RouterLink> </template> <style scoped lang="scss"> .goods-item {   display: block;   width: 220px;   padding: 20px 30px;   text-align: center;   transition: all .5s;   &:hover {     transform: translate3d(0, -3px, 0);     box-shadow: 0 3px 8px rgb(0 0 0 / 20%);   }   img {     width: 160px;     height: 160px;   }   p {     padding-top: 10px;   }   .name {     font-size: 16px;   }   .desc {     color: #999;     height: 29px;   }   .price {     color: $priceColor;     font-size: 20px;   } } </style> 
 
2. 使用组件 src\views\Home\components\GoodsItem.vue
下面就不写导入组件了
<ul class="goods-list">     <li v-for="goods in cate.goods" :key="goods.id">         <GoodsItem :goods="goods" />     </li> </ul> 
 
一级分类页 静态结构搭建和路由配置 1. 准备分类组件 src\views\Category\index.vue
<script setup> </script> <template>   <div class='top-category'>     我是分类   </div> </template> 
 
2. 配置路由 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" ;const  router = createRouter ({  history : createWebHistory (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. 配置导航区域链接 LayoutHeader.vue和LayoutFixed.vue
<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. 封装接口 category.js
import  request from  "@/utils/http.js" ;export  const  getTopCategoryAPI  = (id ) => {  return  request ({     url : "/category" ,     params : {       id,     },   }); }; 
 
3. 渲染面包屑导航 <script setup> import { ref, onMounted } from "vue"; import { useRoute } from "vue-router"; import { getTopCategoryAPI } from "@/apis/category"; const categoryData = ref({}); // 这里可以获取路由的参数,这里还没引入,注意一下 const route = useRoute(); const getCategory = async () => {   // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route   const res = await getTopCategoryAPI(route.params.id);   categoryData.value = res.result; }; onMounted(() => {   console.log("这里是category");   getCategory(); }); </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. 适配接口 apis/home.js
export  function  getBannerAPI (params = {} ) {     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. 导航激活状态设置 其实就是如果点击导航,然后点击的这个导航就会一直高亮的效果
src\views\Layout\components\LayoutHeader.vue
<!-- 这个active-class="active"的value是一个需要激活的class样式,样式写在了下面 --> <RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink> 
 
2. 分类数据模版 
src\views\Category\index.vue
<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 > 
 
路由缓存问题解决 
解决方案:
让组件实例不再复用,强制销毁重建 
监听路由变化,变化之后执行数据更新操作 
 
 
具体实现:
第一种方法 给 routerv-view 添加key属性,强制不添加缓存,破坏缓存,所以这个方法性能会比较差
第二种方法 使用 onBeforeRouteUpdate钩子函数,做精确更新
src\views\Category\index.vue
<script setup> import { ref, onMounted } from "vue"; import { getBannerAPI } from "@/apis/home"; import { useRoute, onBeforeRouteUpdate } from "vue-router"; import { getTopCategoryAPI } from "@/apis/category"; import GoodsItem from "../Home/components/GoodsItem.vue"; const categoryData = ref({}); // 这里可以获取路由的参数,这里还没引入,注意一下 const route = useRoute(); // 这里是给id付了一个初始值,是一种高级的写法,如果 const getCategory = async (id = route.params.id) => {   // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route   const res = await getTopCategoryAPI(id);   categoryData.value = res.result; }; // 获取banner const bannerList = ref([]); const getBanner = async () => {   const res = await getBannerAPI({     distributionSite: "2",   });   console.log(res);   bannerList.value = res.result; }; onMounted(() => {   getBanner();   console.log("这里是category");   getCategory(); }); // 这里的to对象里面有跳转到指定网址的参数和信息 onBeforeRouteUpdate((to) => {   console.log("hahha路由变化了");   getCategory(to.params.id); }); </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 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>     </div>   </div> </template> <style scoped lang="scss"> // 部分代码省略 .home-banner {   width: 1240px;   height: 500px;   margin: 0 auto;   img {     width: 100%;     height: 500px;   } } .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> 
 
基于业务逻辑的函数拆分 
基本思想:把组件内独立的业务逻辑通过 useXXX 函数做封装处理,在组件中做组合使用
 
实现步骤:
按照业务 声明以use打头的逻辑函数 
把独立的业务逻辑封装到各个函数的内部 
函数内部把组件中需要用到的数据或者方法return出去 
在组件中调用函数然后把数据或者方法解构赋值进行使用 
 
src\views\Category\composables\useBanner.js
import  { onMounted, ref } from  "vue" ;import  { getTopCategoryAPI } 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  getTopCategoryAPI (id);     categoryData.value  = res.result ;   };   onMounted (() =>  getCategory ());      onBeforeRouteUpdate ((to ) =>  {          getCategory (to.params .id );   });   return  {     categoryData,   }; } 
 
src\views\Category\composables\useBanner.js
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   } } 
 
src\views\Category\index.vue
<script setup> import { useBanner } from './composables/useBanner' import { useCategory } from './composables/useCategory' const { bannerList } = useBanner() const { categoryData } = useCategory() </script> 
 
二级分类 整体业务认识和路由配置 
1. 准备组件模版 src\views\SubCategory\index.vue
<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. 配置路由关系 src\router\index.js
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. 跳转配置 src\views\Category\index.vue
<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. 准备接口 src\apis\category.js
export  const  getCategoryFilterAPI  = (id ) => {  return  request ({     url :'/category/sub/filter' ,     params :{       id     }   }) } 
 
2. 获取数据渲染模版 src\views\SubCategory\index.vue
<script setup> import { ref, onMounted } from "vue"; import { getCategoryFilterAPI } from "@/apis/category"; import { useRoute } from "vue-router"; // 获取面包屑导航数据 const route = useRoute(); const filterData = ref({}); const getFilterData = async () => {   const res = await getCategoryFilterAPI(route.params.id);   filterData.value = res.result; }; onMounted(() => {   getFilterData(); }); </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: `/category/${filterData.parentId}` }"           >{{ filterData.parentName }}         </el-breadcrumb-item>         <el-breadcrumb-item>{{ filterData.name }}</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> 
 
分类基础列表实现 
这里我们先实现第一步,也就是得到基础的列表
1. 准备接口 @/apis/category.js
}  export  const  getSubCategoryAPI  = (data ) => {  return  request ({     url :'/category/goods/temporary' ,     method :'POST' ,     data   }) } 
 
2. 获取数据列表 src\views\SubCategory\index.vue
<script setup> import GoodsItem from "../Home/components/GoodsItem.vue"; import { getSubCategoryAPI } from "@/apis/category.js"; // 获取基础列表数据渲染 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>       <div class="body">         <!-- 商品列表-->         <GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id" />       </div> 
 
列表筛选实现 
思路:tab组件切换时修改reqData中的sortField字段,重新拉取接口列表
 
src\views\SubCategory\index.vue
<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> 
 
无限加载实现 
基础思路
触底条件满足之后 page++,拉取下一页数据 
新老数据做数组拼接 
判断是否已经全部加载完毕,停止监听 
 
 
src\views\SubCategory\index.vue
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    } } 
 
在页面上监听load方法,并且设置如果到底的话不再加载新数据
<div  class ="body"  v-infinite-scroll ="load"  :infinite-scroll-disabled ="disabled" >          <GoodsItem  v-for ="goods in goodList"  :goods ="goods"  :key ="goods.id"  />  </div > 
 
定制路由行为 定制路由行为可以让我们在切换不同路由的时候,可以自动滚动到页面的任何一个部分(比如说顶部),如果不配置默认会停留到原先的位置
配置
新鲜好物商品详情 整体认识和路由配置 
1. 准备组件模版 src\views\Detail\index.vue
<script setup> </script> <template>   <div class="xtx-goods-page">     <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 :to="{ path: '/' }">跑步鞋           </el-breadcrumb-item>           <el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item>         </el-breadcrumb>       </div>       <!-- 商品信息 -->       <div class="info-container">         <div>           <div class="goods-info">             <div class="media">               <!-- 图片预览区 -->               <!-- 统计数量 -->               <ul class="goods-sales">                 <li>                   <p>销量人气</p>                   <p> 100+ </p>                   <p><i class="iconfont icon-task-filling"></i>销量人气</p>                 </li>                 <li>                   <p>商品评价</p>                   <p>200+</p>                   <p><i class="iconfont icon-comment-filling"></i>查看评价</p>                 </li>                 <li>                   <p>收藏人气</p>                   <p>300+</p>                   <p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>                 </li>                 <li>                   <p>品牌信息</p>                   <p>400+</p>                   <p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>                 </li>               </ul>             </div>             <div class="spec">               <!-- 商品信息区 -->               <p class="g-name"> 抓绒保暖,毛毛虫儿童鞋 </p>               <p class="g-desc">好穿 </p>               <p class="g-price">                 <span>200</span>                 <span> 100</span>               </p>               <div class="g-service">                 <dl>                   <dt>促销</dt>                   <dd>12月好物放送,App领券购买直降120元</dd>                 </dl>                 <dl>                   <dt>服务</dt>                   <dd>                     <span>无忧退货</span>                     <span>快速退款</span>                     <span>免费包邮</span>                     <a href="javascript:;">了解详情</a>                   </dd>                 </dl>               </div>               <!-- sku组件 -->               <!-- 数据组件 -->               <!-- 按钮组件 -->               <div>                 <el-button size="large" class="btn">                   加入购物车                 </el-button>               </div>             </div>           </div>           <div class="goods-footer">             <div class="goods-article">               <!-- 商品详情 -->               <div class="goods-tabs">                 <nav>                   <a>商品详情</a>                 </nav>                 <div class="goods-detail">                   <!-- 属性 -->                   <ul class="attrs">                     <li v-for="item in 3" :key="item.value">                       <span class="dt">白色</span>                       <span class="dd">纯棉</span>                     </li>                   </ul>                   <!-- 图片 -->                 </div>               </div>             </div>             <!-- 24热榜+专题推荐 -->             <div class="goods-aside">             </div>           </div>         </div>       </div>     </div>   </div> </template> <style scoped lang='scss'> .xtx-goods-page {   .goods-info {     min-height: 600px;     background: #fff;     display: flex;     .media {       width: 580px;       height: 600px;       padding: 30px 50px;     }     .spec {       flex: 1;       padding: 30px 30px 30px 0;     }   }   .goods-footer {     display: flex;     margin-top: 20px;     .goods-article {       width: 940px;       margin-right: 20px;     }     .goods-aside {       width: 280px;       min-height: 1000px;     }   }   .goods-tabs {     min-height: 600px;     background: #fff;   }   .goods-warn {     min-height: 600px;     background: #fff;     margin-top: 20px;   }   .number-box {     display: flex;     align-items: center;     .label {       width: 60px;       color: #999;       padding-left: 10px;     }   }   .g-name {     font-size: 22px;   }   .g-desc {     color: #999;     margin-top: 10px;   }   .g-price {     margin-top: 10px;     span {       &::before {         content: "¥";         font-size: 14px;       }       &:first-child {         color: $priceColor;         margin-right: 10px;         font-size: 22px;       }       &:last-child {         color: #999;         text-decoration: line-through;         font-size: 16px;       }     }   }   .g-service {     background: #f5f5f5;     width: 500px;     padding: 20px 10px 0 10px;     margin-top: 10px;     dl {       padding-bottom: 20px;       display: flex;       align-items: center;       dt {         width: 50px;         color: #999;       }       dd {         color: #666;         &:last-child {           span {             margin-right: 10px;             &::before {               content: "•";               color: $xtxColor;               margin-right: 2px;             }           }           a {             color: $xtxColor;           }         }       }     }   }   .goods-sales {     display: flex;     width: 400px;     align-items: center;     text-align: center;     height: 140px;     li {       flex: 1;       position: relative;       ~li::after {         position: absolute;         top: 10px;         left: 0;         height: 60px;         border-left: 1px solid #e4e4e4;         content: "";       }       p {         &:first-child {           color: #999;         }         &:nth-child(2) {           color: $priceColor;           margin-top: 10px;         }         &:last-child {           color: #666;           margin-top: 10px;           i {             color: $xtxColor;             font-size: 14px;             margin-right: 2px;           }           &:hover {             color: $xtxColor;             cursor: pointer;           }         }       }     }   } } .goods-tabs {   min-height: 600px;   background: #fff;   nav {     height: 70px;     line-height: 70px;     display: flex;     border-bottom: 1px solid #f5f5f5;     a {       padding: 0 40px;       font-size: 18px;       position: relative;       >span {         color: $priceColor;         font-size: 16px;         margin-left: 10px;       }     }   } } .goods-detail {   padding: 40px;   .attrs {     display: flex;     flex-wrap: wrap;     margin-bottom: 30px;     li {       display: flex;       margin-bottom: 10px;       width: 50%;       .dt {         width: 100px;         color: #999;       }       .dd {         flex: 1;         color: #666;       }     }   }   >img {     width: 100%;   } } .btn {   margin-top: 20px; } .bread-container {   padding: 25px 0; } </style> 
 
2. 配置路由 src\router\index.js
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' ,           component : SubCategory          },         {           path : 'detail/:id' ,           component : Detail          }       ]     },     {       path : '/login' ,       component : Login      }   ],      scrollBehavior () {     return  {       top : 0      }   } }) 
 
3. 绑定模版测试跳转 src\views\Home\components\HomeNew.vue
<RouterLink  :to ="`/detail/${item.id}`" >   <img  :src ="item.picture"  alt =""  />    <p  class ="name" > {{ item.name }}</p >    <p  class ="price" > ¥ {{ item.price }}</p >  </RouterLink > 
 
渲染基础数据 1. 封装接口 src\apis\category.js
import  request from  '@/utils/http' export  const  getDetail  = (id ) => {  return  request ({     url : '/goods' ,     params : {       id     }   }) } 
 
2. 获取数据渲染模版 src\views\Detail\index.vue
<script setup> import { getDetail } from '@/apis/detail' import { onMounted, ref } from 'vue' import { useRoute } from 'vue-router' const goods = ref({}) const route = useRoute() const getGoods = async () => {   const res = await getDetail(route.params.id)   goods.value = res.result } onMounted(() => getGoods()) </script> <template>   <div class="xtx-goods-page">     <div class="container" v-if="goods.details">       <div class="bread-container">         <el-breadcrumb separator=">">           <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>           <!--              错误原因:goods一开始{}  {}.categories -> undefined  -> undefined[1]             1. 可选链的语法?.              2. v-if手动控制渲染时机 保证只有数据存在才渲染            -->           <el-breadcrumb-item :to="{ path: `/category/${goods.categories[1].id}` }">{{ goods.categories[1].name }}           </el-breadcrumb-item>           <el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories[0].id}` }">{{             goods.categories[0].name           }}           </el-breadcrumb-item>           <el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item>         </el-breadcrumb>       </div>       <!-- 商品信息 -->       <div class="info-container">         <div>           <div class="goods-info">             <div class="media">               <!-- 图片预览区 -->               <!-- 统计数量 -->               <ul class="goods-sales">                 <li>                   <p>销量人气</p>                   <p> {{ goods.salesCount }}+ </p>                   <p><i class="iconfont icon-task-filling"></i>销量人气</p>                 </li>                 <li>                   <p>商品评价</p>                   <p>{{ goods.commentCount }}+</p>                   <p><i class="iconfont icon-comment-filling"></i>查看评价</p>                 </li>                 <li>                   <p>收藏人气</p>                   <p>{{ goods.collectCount }}+</p>                   <p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>                 </li>                 <li>                   <p>品牌信息</p>                   <p>{{ goods.brand.name }}</p>                   <p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>                 </li>               </ul>             </div>             <div class="spec">               <!-- 商品信息区 -->               <p class="g-name"> {{ goods.name }} </p>               <p class="g-desc">{{ goods.desc }} </p>               <p class="g-price">                 <span>{{ goods.oldPrice }}</span>                 <span> {{ goods.price }}</span>               </p>               <div class="g-service">                 <dl>                   <dt>促销</dt>                   <dd>12月好物放送,App领券购买直降120元</dd>                 </dl>                 <dl>                   <dt>服务</dt>                   <dd>                     <span>无忧退货</span>                     <span>快速退款</span>                     <span>免费包邮</span>                     <a href="javascript:;">了解详情</a>                   </dd>                 </dl>               </div>               <!-- sku组件 -->               <!-- 数据组件 -->               <!-- 按钮组件 -->               <div>                 <el-button size="large" class="btn">                   加入购物车                 </el-button>               </div>             </div>           </div>           <div class="goods-footer">             <div class="goods-article">               <!-- 商品详情 -->               <div class="goods-tabs">                 <nav>                   <a>商品详情</a>                 </nav>                 <div class="goods-detail">                   <!-- 属性 -->                   <ul class="attrs">                     <li v-for="item in goods.details.properties" :key="item.value">                       <span class="dt">{{ item.name }}</span>                       <span class="dd">{{ item.value }}</span>                     </li>                   </ul>                   <!-- 图片 -->                   <img v-for="img in goods.details.pictures" :src="img" :key="img" alt="">                 </div>               </div>             </div>             <!-- 24热榜+专题推荐 -->             <div class="goods-aside">             </div>           </div>         </div>       </div>     </div>   </div> </template> 
 
热榜区域 
1. 渲染基础热榜数据 1- 准备模版
src\views\Detail\index.vue
<script setup> </script> <template>   <div class="goods-hot">     <h3>周日榜单</h3>     <!-- 商品区块 -->     <RouterLink to="/" class="goods-item" v-for="item in 3" :key="item.id">       <img :src="item.picture" alt="" />       <p class="name ellipsis">一双男鞋</p>       <p class="desc ellipsis">一双好穿的男鞋</p>       <p class="price">¥200.00</p>     </RouterLink>   </div> </template> <style scoped lang="scss"> .goods-hot {   h3 {     height: 70px;     background: $helpColor;     color: #fff;     font-size: 18px;     line-height: 70px;     padding-left: 25px;     margin-bottom: 10px;     font-weight: normal;   }   .goods-item {     display: block;     padding: 20px 30px;     text-align: center;     background: #fff;     img {       width: 160px;       height: 160px;     }     p {       padding-top: 10px;     }     .name {       font-size: 16px;     }     .desc {       color: #999;       height: 29px;     }     .price {       color: $priceColor;       font-size: 20px;     }   } } </style> 
 
2- 封装接口
src\apis\detail.js
export  const  fetchHotGoodsAPI  = ({ id, type, limit = 3  } ) => {  return  request ({     url :'/goods/hot' ,     params :{       id,        type,        limit     }   }) } 
 
3- 获取基础数据渲染模版
<script setup> import { ref } from 'vue' import { getHotGoodsAPI } from '@/apis/detail' import { useRoute } from 'vue-router' const goodList = ref([]) const route = useRoute() const getHotList = async () => {   const res = await getHotGoodsAPI({     id: route.params.id,     type: 1   })   goodList.value = res.result } getHotList() </script> <template>   <div class="goods-hot">     <h3> 24小时热榜 </h3>     <!-- 商品区块 -->     <RouterLink :to="`/detail/${item.id}`" class="goods-item" v-for="item in goodList" :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>   </div> </template> 
 
2. 适配热榜类型 1- 设计props参数type
src\views\Detail\components\DetailHot.vue
const  props = defineProps ({  type : {     type : Number ,      default : 1    } })  const  res = await  fetchHotGoodsAPI ({     id : route.params .id ,     type : props.type  }) 
 
2- 使用组件传入不同的type
src\views\Detail\index.vue
<!-- 24小时热榜 --> <DetailHot :type="1" /> <!-- 周热榜 --> <DetailHot :type="2" /> 
 
3. 适配热榜title src\views\Detail\components\DetailHot.vue
const  TITLEMAP  = {  1 : '24小时热榜' ,   2 : '周热榜' ,  } const  title = computed (() =>  TITLEMAP [props.type ])
 
在页面中使用
src\views\Detail\components\DetailHot.vue
 
4.汇总热榜代码 src\views\Detail\components\DetailHot.vue
<script setup> import { ref, computed } from "vue"; import { fetchHotGoodsAPI } from "@/apis/detail"; import { useRoute } from "vue-router"; const goodList = ref([]); const route = useRoute(); // type适配不同类型热榜数据 const props = defineProps({   type: {     type: Number, // 1代表24小时热销榜 2代表周热销榜 3代表总热销榜 可以使用type去适配title和数据列表     default: 1,   }, }); const getHotList = async () => {   const res = await fetchHotGoodsAPI({     id: route.params.id,     // 这里传入type类型,是为了传参用来区别周榜和日榜     type: props.type,   });   goodList.value = res.result; }; getHotList(); const TITLEMAP = {   1: "24小时热榜",   2: "周热榜", }; const title = computed(() => TITLEMAP[props.type]); </script> <template>   <div class="goods-hot">     <h3>{{ title }}</h3>     <!-- 商品区块 -->     <RouterLink       :to="`/detail/${item.id}`"       class="goods-item"       v-for="item in goodList"       :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>   </div> </template> <style scoped lang="scss"> .goods-hot {   h3 {     height: 70px;     background: $helpColor;     color: #fff;     font-size: 18px;     line-height: 70px;     padding-left: 25px;     margin-bottom: 10px;     font-weight: normal;   }   .goods-item {     display: block;     padding: 20px 30px;     text-align: center;     background: #fff;     img {       width: 160px;       height: 160px;     }     p {       padding-top: 10px;     }     .name {       font-size: 16px;     }     .desc {       color: #999;       height: 29px;     }     .price {       color: $priceColor;       font-size: 20px;     }   } } </style> 
 
图片预览组件封装 
1. 小图切换大图显示 1- 准备模版
src\components\ImageView.vue
<script setup> // 图片列表 const imageList = [   "https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png",   "https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg",   "https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg",   "https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg",   "https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg" ] </script> <template>   <div class="goods-image">     <!-- 左侧大图-->     <div class="middle" ref="target">       <img :src="imageList[0]" alt="" />       <!-- 蒙层小滑块 -->       <div class="layer" :style="{ left: `0px`, top: `0px` }"></div>     </div>     <!-- 小图列表 -->     <ul class="small">       <li v-for="(img, i) in imageList" :key="i">         <img :src="img" alt="" />       </li>     </ul>     <!-- 放大镜大图 -->     <div class="large" :style="[       {         backgroundImage: `url(${imageList[0]})`,         backgroundPositionX: `0px`,         backgroundPositionY: `0px`,       },     ]" v-show="false"></div>   </div> </template> <style scoped lang="scss"> .goods-image {   width: 480px;   height: 400px;   position: relative;   display: flex;   .middle {     width: 400px;     height: 400px;     background: #f5f5f5;   }   .large {     position: absolute;     top: 0;     left: 412px;     width: 400px;     height: 400px;     z-index: 500;     box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);     background-repeat: no-repeat;     // 背景图:盒子的大小 = 2:1  将来控制背景图的移动来实现放大的效果查看 background-position     background-size: 800px 800px;     background-color: #f8f8f8;   }   .layer {     width: 200px;     height: 200px;     background: rgba(0, 0, 0, 0.2);     // 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来     left: 0;     top: 0;     position: absolute;   }   .small {     width: 80px;     li {       width: 68px;       height: 68px;       margin-left: 12px;       margin-bottom: 15px;       cursor: pointer;       &:hover,       &.active {         border: 2px solid $xtxColor;       }     }   } } </style> 
 
2- 实现逻辑
src\components\ImageView.vue
<script setup> import { ref } from 'vue' // 实现鼠标移入交互 const curIndex = ref(0) const mouseEnterFn = (i) => curIndex.value = i </script> <template>   <div class="goods-image">     <!-- 小图列表 -->     <ul class="small">       <li v-for="(img, i) in imageList"          :key="i"          @mouseenter="mouseEnterFn(i)"          :class="{ active: i === curIndex }">         <img :src="img" alt="" />       </li>     </ul>   </div> </template> 
 
2. 放大镜效果实现 功能拆解:
左侧滑块跟随鼠标移动(这里用到了vueuese) 
右侧大图放大效果实现 
鼠标移入控制滑块和大图显示隐藏 
 
<script setup> import { ref, watch } from 'vue' import { useMouseInElement } from '@vueuse/core' // 图片列表 const imageList = [   "https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png",   "https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg",   "https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg",   "https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg",   "https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg" ] // 1.小图切换大图显示 const activeIndex = ref(0) const enterhandler = (i) => {   activeIndex.value = i } // 2. 获取鼠标相对位置 const target = ref(null) const { elementX, elementY, isOutside } = useMouseInElement(target) // 3. 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top) const left = ref(0) const top = ref(0) const positionX = ref(0) const positionY = ref(0) watch([elementX, elementY, isOutside], () => {   console.log('xy变化了')   // 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑   if (isOutside.value) return   console.log('后续逻辑执行了')   // 有效范围内控制滑块距离   // 横向   if (elementX.value > 100 && elementX.value < 300) {     left.value = elementX.value - 100   }   // 纵向   if (elementY.value > 100 && elementY.value < 300) {     top.value = elementY.value - 100   }   // 处理边界   if (elementX.value > 300) { left.value = 200 }   if (elementX.value < 100) { left.value = 0 }   if (elementY.value > 300) { top.value = 200 }   if (elementY.value < 100) { top.value = 0 }   // 控制大图的显示   positionX.value = -left.value * 2   positionY.value = -top.value * 2 }) </script> <template>   <div class="goods-image">     <!-- 左侧大图-->     <div class="middle" ref="target">       <img :src="imageList[activeIndex]" alt="" />       <!-- 蒙层小滑块 -->       <div class="layer" v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }"></div>     </div>     <!-- 小图列表 -->     <ul class="small">       <li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{ active: i === activeIndex }">         <img :src="img" alt="" />       </li>     </ul>     <!-- 放大镜大图 -->     <div class="large" :style="[       {         backgroundImage: `url(${imageList[0]})`,         backgroundPositionX: `${positionX}px`,         backgroundPositionY: `${positionY}px`,       },     ]" v-show="!isOutside"></div>   </div> </template> <style scoped lang="scss"> .goods-image {   width: 480px;   height: 400px;   position: relative;   display: flex;   .middle {     width: 400px;     height: 400px;     background: #f5f5f5;   }   .large {     position: absolute;     top: 0;     left: 412px;     width: 400px;     height: 400px;     z-index: 500;     box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);     background-repeat: no-repeat;     // 背景图:盒子的大小 = 2:1  将来控制背景图的移动来实现放大的效果查看 background-position     background-size: 800px 800px;     background-color: #f8f8f8;   }   .layer {     width: 200px;     height: 200px;     background: rgba(0, 0, 0, 0.2);     // 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来     left: 0;     top: 0;     position: absolute;   }   .small {     width: 80px;     li {       width: 68px;       height: 68px;       margin-left: 12px;       margin-bottom: 15px;       cursor: pointer;       &:hover,       &.active {         border: 2px solid $xtxColor;       }     }   } } </style> 
 
SKU组件熟悉 全局组件统一插件化 存货单位,也翻译为库存单元,是一个会计学名词,定义为库存管理中的最小可用单元,例如纺织品中一个SKU通常表示规格、颜色、款式,而在连锁零售门店中有时称单品为一个SKU
1. 插件化开发 import  ImageView  from  './ImageView/index.vue' import  Sku  from  './XtxSku/index.vue' export  const  componentPlugin = {  install (app) {          app.component ('XtxImageView' , ImageView )     app.component ('XtxSku' , Sku )   } } 
 
2. 插件注册 // 引入全局组件插件 import { componentPlugin } from '@/components' app.use(componentPlugin)  
 
登录页 整体认识和路由配置 1. 准备模版 src\views\Login\index.vue
<script setup> </script> <template>   <div>     <header class="login-header">       <div class="container m-top-20">         <h1 class="logo">           <RouterLink to="/">小兔鲜</RouterLink>         </h1>         <RouterLink class="entry" to="/">           进入网站首页           <i class="iconfont icon-angle-right"></i>           <i class="iconfont icon-angle-right"></i>         </RouterLink>       </div>     </header>     <section class="login-section">       <div class="wrapper">         <nav>           <a href="javascript:;">账户登录</a>         </nav>         <div class="account-box">           <div class="form">             <el-form label-position="right" label-width="60px"               status-icon>               <el-form-item  label="账户">                 <el-input/>               </el-form-item>               <el-form-item label="密码">                 <el-input/>               </el-form-item>               <el-form-item label-width="22px">                 <el-checkbox  size="large">                   我已同意隐私条款和服务条款                 </el-checkbox>               </el-form-item>               <el-button size="large" class="subBtn">点击登录</el-button>             </el-form>           </div>         </div>       </div>     </section>     <footer class="login-footer">       <div class="container">         <p>           <a href="javascript:;">关于我们</a>           <a href="javascript:;">帮助中心</a>           <a href="javascript:;">售后服务</a>           <a href="javascript:;">配送与验收</a>           <a href="javascript:;">商务合作</a>           <a href="javascript:;">搜索推荐</a>           <a href="javascript:;">友情链接</a>         </p>         <p>CopyRight © 小兔鲜儿</p>       </div>     </footer>   </div> </template> <style scoped lang='scss'> .login-header {   background: #fff;   border-bottom: 1px solid #e4e4e4;   .container {     display: flex;     align-items: flex-end;     justify-content: space-between;   }   .logo {     width: 200px;     a {       display: block;       height: 132px;       width: 100%;       text-indent: -9999px;       background: url("@/assets/images/logo.png") no-repeat center 18px / contain;     }   }   .sub {     flex: 1;     font-size: 24px;     font-weight: normal;     margin-bottom: 38px;     margin-left: 20px;     color: #666;   }   .entry {     width: 120px;     margin-bottom: 38px;     font-size: 16px;     i {       font-size: 14px;       color: $xtxColor;       letter-spacing: -5px;     }   } } .login-section {   background: url('@/assets/images/login-bg.png') no-repeat center / cover;   height: 488px;   position: relative;   .wrapper {     width: 380px;     background: #fff;     position: absolute;     left: 50%;     top: 54px;     transform: translate3d(100px, 0, 0);     box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);     nav {       font-size: 14px;       height: 55px;       margin-bottom: 20px;       border-bottom: 1px solid #f5f5f5;       display: flex;       padding: 0 40px;       text-align: right;       align-items: center;       a {         flex: 1;         line-height: 1;         display: inline-block;         font-size: 18px;         position: relative;         text-align: center;       }     }   } } .login-footer {   padding: 30px 0 50px;   background: #fff;   p {     text-align: center;     color: #999;     padding-top: 20px;     a {       line-height: 1;       padding: 0 10px;       color: #999;       display: inline-block;       ~a {         border-left: 1px solid #ccc;       }     }   } } .account-box {   .toggle {     padding: 15px 40px;     text-align: right;     a {       color: $xtxColor;       i {         font-size: 14px;       }     }   }   .form {     padding: 0 20px 20px 20px;     &-item {       margin-bottom: 28px;       .input {         position: relative;         height: 36px;         >i {           width: 34px;           height: 34px;           background: #cfcdcd;           color: #fff;           position: absolute;           left: 1px;           top: 1px;           text-align: center;           line-height: 34px;           font-size: 18px;         }         input {           padding-left: 44px;           border: 1px solid #cfcdcd;           height: 36px;           line-height: 36px;           width: 100%;           &.error {             border-color: $priceColor;           }           &.active,           &:focus {             border-color: $xtxColor;           }         }         .code {           position: absolute;           right: 1px;           top: 1px;           text-align: center;           line-height: 34px;           font-size: 14px;           background: #f5f5f5;           color: #666;           width: 90px;           height: 34px;           cursor: pointer;         }       }       >.error {         position: absolute;         font-size: 12px;         line-height: 28px;         color: $priceColor;         i {           font-size: 14px;           margin-right: 2px;         }       }     }     .agree {       a {         color: #069;       }     }     .btn {       display: block;       width: 100%;       height: 40px;       color: #fff;       text-align: center;       line-height: 40px;       background: $xtxColor;       &.disabled {         background: #cfcdcd;       }     }   }   .action {     padding: 20px 40px;     display: flex;     justify-content: space-between;     align-items: center;     .url {       a {         color: #999;         margin-left: 10px;       }     }   } } .subBtn {   background: $xtxColor;   width: 100%;   color: #fff; } </style> 
 
2. 配置路由跳转 src\views\Layout\components\LayoutNav.vue
<li><a href="javascript:;" @click="router.push('/login')">请先登录</a></li> 
 
表单校验实现 
1. 校验要求 
用户名:不能为空,字段名为 account 密码:不能为空且为6-14个字符,字段名为 password 同意协议:必选,字段名为 agree
 
2. 代码实现 
这里用到了自定义校验规则
<script setup> import { ref } from 'vue' // 表单数据对象 const userInfo = ref({   account: '1311111111',   password: '123456',   agree: true }) // 规则数据对象 const rules = {   account: [     { required: true, message: '用户名不能为空' }   ],   password: [     { required: true, message: '密码不能为空' },     { min: 6, max: 24, message: '密码长度要求6-14个字符' }   ],   agree: [     {       validator: (rule, val, callback) => {         return val ? callback() : new Error('请先同意协议')       }     }   ] } </script> <template>     <div class="form">       <el-form ref="formRef" :model="userInfo" :rules="rules" status-icon>         <el-form-item prop="account" label="账户">           <el-input v-model="userInfo.account" />         </el-form-item>         <el-form-item prop="password" label="密码">           <el-input v-model="userInfo.password" />         </el-form-item>         <el-form-item prop="agree" label-width="22px">           <el-checkbox v-model="userInfo.agree" size="large">             我已同意隐私条款和服务条款           </el-checkbox>         </el-form-item>         <el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button>       </el-form>     </div> </template> 
 
登录基础业务实现 
基础思想
调用登录接口获取用户信息 
提示用户当前是否成功 
跳转到首页 
 
 
封装接口
src\apis\user.js
import  request from  "@/utils/http" ;export  const  loginAPI  = ({ account, password } ) => {  return  request ({     url : "/login" ,     method : "POST" ,     data : {       account,       password,     },   }); }; 
 
src\views\Login\index.vue
import  { ElMessage  } from  "element-plus" ;import  "element-plus/theme-chalk/el-message.css" ;import  { useRouter } from  "vue-router" ;import  { loginAPI } from  "@/apis/user.js" ;const  router = useRouter ();const  formRef = ref (null );const  doLogin  = ( ) => {  const  { account, password } = userInfo.value ;      formRef.value .validate (async  (valid) => {          console .log (valid);          if  (valid) {              await  loginAPI ({ account, password });                     ElMessage ({ type : "success" , message : "登录成功"  });              router.replace ({ path : "/"  });     }   }); }; 
 
这里其实我有点不理解,这里如果登录失败就不会执行后面的语句,然后就会执行拦截器里面的语句了
src\utils\http.js
httpInstance.interceptors .response .use (   (res ) =>  res.data ,   (e ) =>  {               ElMessage ({       type : "warning" ,       message : e.response .data .message ,     });     return  Promise .reject (e);   } ); 
 
Pinia管理用户数据 
基本思想:Pinia负责用户数据相关的state和action,组件中只负责触发action函数并传递参数
 
src\stores\user.js
import  { defineStore } from  'pinia' import  { ref } from  'vue' import  { loginAPI } from  '@/apis/user' export  const  useUserStore = defineStore ('user' , () =>  {     const  userInfo = ref ({})      const  getUserInfo  = async  ({ account, password } ) => {     const  res = await  loginAPI ({ account, password })     userInfo.value  = res.result    }      return  {     getUserInfo   } }, {   persist : true , }) 
 
使用pinia替换原来的api形式
src\views\Login\index.vue
import  { ElMessage  } from  "element-plus" ;import  "element-plus/theme-chalk/el-message.css" ;import  { useRouter } from  "vue-router" ;import  { useUserStore } from  "@/stores/user.js" ;const  router = useRouter ();const  userStore = useUserStore ();const  formRef = ref (null );const  doLogin  = ( ) => {  const  { account, password } = userInfo.value ;      formRef.value .validate (async  (valid) => {          console .log (valid);          if  (valid) {              await  userStore.getUserInfo ({ account, password });                     ElMessage ({ type : "success" , message : "登录成功"  });              router.replace ({ path : "/"  });     }   }); }; 
 
持久化用户数据 
这个pinia的插件也是把token存储到localstorage中。只是更方便一些
请求拦截器携带token 
基础思想:很多接口如果想要获取数据必须要带着有效的Token信息才可以,拦截器中做一次,用到axios实例的其他都可以拿到
 
httpInstance.interceptors .request .use (config  =>  {      const  userStore = useUserStore ()      const  token = userStore.userInfo .token    if  (token) {     config.headers .Authorization  = `Bearer ${token} `    }   return  config }, e  =>  Promise .reject (e)) 
 
退出登录实现 
基础思想:
清除用户信息 
跳转到登录页 
 
 
1- 新增清除用户信息action
 const  clearUserInfo  = ( ) => {    userInfo.value  = {}  } 
 
2- 组件中执行业务逻辑
<script setup> import { useUserStore } from '@/stores/userStore' import { useRouter } from 'vue-router' const userStore = useUserStore() const router = useRouter() const confirm = () => {   console.log('用户要退出登录了')   // 退出登录业务逻辑实现   // 1.清除用户信息 触发action   userStore.clearUserInfo()   // 2.跳转到登录页   router.push('/login') } </script> 
 
购物车 
本地购物车 1. 添加购物车 
基础思想:如果已经添加过相同的商品,就在其数量count上加一,如果没有添加过,就直接push到购物车列表中
 
import  { defineStore } from  'pinia' import  { ref } from  'vue' export  const  useCartStore = defineStore ('cart' , () =>  {     const  cartList = ref ([])      const  addCart  = (goods ) => {     console .log ('添加' , goods)                         const  item = cartList.value .find ((item ) =>  goods.skuId  === item.skuId )     if  (item) {              item.count ++     } else  {              cartList.value .push (goods)     }   }   return  {     cartList,     addCart   } }, {   persist : true , }) 
 
2. 头部购物车 2.1. 头部购物车组件模版 <script setup> </script> <template>   <div class="cart">     <a class="curr" href="javascript:;">       <i class="iconfont icon-cart"></i><em>2</em>     </a>     <div class="layer">       <div class="list">         <!--         <div class="item" v-for="i in cartList" :key="i">           <RouterLink to="">             <img :src="i.picture" alt="" />             <div class="center">               <p class="name ellipsis-2">                 {{ i.name }}               </p>               <p class="attr ellipsis">{{ i.attrsText }}</p>             </div>             <div class="right">               <p class="price">¥{{ i.price }}</p>               <p class="count">x{{ i.count }}</p>             </div>           </RouterLink>           <i class="iconfont icon-close-new" @click="store.delCart(i.skuId)"></i>         </div>         -->       </div>       <div class="foot">         <div class="total">           <p>共 10 件商品</p>           <p>¥ 100.00 </p>         </div>         <el-button size="large" type="primary" >去购物车结算</el-button>       </div>     </div> </div> </template> <style scoped lang="scss"> .cart {   width: 50px;   position: relative;   z-index: 600;   .curr {     height: 32px;     line-height: 32px;     text-align: center;     position: relative;     display: block;     .icon-cart {       font-size: 22px;     }     em {       font-style: normal;       position: absolute;       right: 0;       top: 0;       padding: 1px 6px;       line-height: 1;       background: $helpColor;       color: #fff;       font-size: 12px;       border-radius: 10px;       font-family: Arial;     }   }   &:hover {     .layer {       opacity: 1;       transform: none;     }   }   .layer {     opacity: 0;     transition: all 0.4s 0.2s;     transform: translateY(-200px) scale(1, 0);     width: 400px;     height: 400px;     position: absolute;     top: 50px;     right: 0;     box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);     background: #fff;     border-radius: 4px;     padding-top: 10px;     &::before {       content: "";       position: absolute;       right: 14px;       top: -10px;       width: 20px;       height: 20px;       background: #fff;       transform: scale(0.6, 1) rotate(45deg);       box-shadow: -3px -3px 5px rgba(0, 0, 0, 0.1);     }     .foot {       position: absolute;       left: 0;       bottom: 0;       height: 70px;       width: 100%;       padding: 10px;       display: flex;       justify-content: space-between;       background: #f8f8f8;       align-items: center;       .total {         padding-left: 10px;         color: #999;         p {           &:last-child {             font-size: 18px;             color: $priceColor;           }         }       }     }   }   .list {     height: 310px;     overflow: auto;     padding: 0 10px;     &::-webkit-scrollbar {       width: 10px;       height: 10px;     }     &::-webkit-scrollbar-track {       background: #f8f8f8;       border-radius: 2px;     }     &::-webkit-scrollbar-thumb {       background: #eee;       border-radius: 10px;     }     &::-webkit-scrollbar-thumb:hover {       background: #ccc;     }     .item {       border-bottom: 1px solid #f5f5f5;       padding: 10px 0;       position: relative;       i {         position: absolute;         bottom: 38px;         right: 0;         opacity: 0;         color: #666;         transition: all 0.5s;       }       &:hover {         i {           opacity: 1;           cursor: pointer;         }       }       a {         display: flex;         align-items: center;         img {           height: 80px;           width: 80px;         }         .center {           padding: 0 10px;           width: 200px;           .name {             font-size: 16px;           }           .attr {             color: #999;             padding-top: 5px;           }         }         .right {           width: 100px;           padding-right: 20px;           text-align: center;           .price {             font-size: 16px;             color: $priceColor;           }           .count {             color: #999;             margin-top: 5px;             font-size: 16px;           }         }       }     }   } } </style> 
 
2.2 渲染头部购物车数据 <script setup> import { useCartStore } from '@/stores/cartStore' const cartStore = useCartStore() </script> <template>   <div class="cart">     <a class="curr" href="javascript:;">       <i class="iconfont icon-cart"></i><em>{{ cartStore.cartList.length }}</em>     </a>     <div class="layer">       <div class="list">         <div class="item" v-for="i in cartStore.cartList" :key="i">           <RouterLink to="">             <img :src="i.picture" alt="" />             <div class="center">               <p class="name ellipsis-2">                 {{ i.name }}               </p>               <p class="attr ellipsis">{{ i.attrsText }}</p>             </div>             <div class="right">               <p class="price">¥{{ i.price }}</p>               <p class="count">x{{ i.count }}</p>             </div>           </RouterLink>           <i class="iconfont icon-close-new" @click="cartStore.delCart(i.skuId)"></i>         </div>       </div>       <div class="foot">         <div class="total">           <p>共 {{ cartStore.allCount }} 件商品</p>           <p>¥ {{ cartStore.allPrice.toFixed(2) }} </p>         </div>         <el-button size="large" type="primary" @click="$router.push('/cartlist')">去购物车结算</el-button>       </div>     </div> </div> </template> 
 
2.3 删除功能实现 1- 添加删除action函数
const  delCart  = async  (skuId ) => {                   const  idx = cartList.value .findIndex ((item ) =>  skuId === item.skuId )     cartList.value .splice (idx, 1 ) } 
 
2- 组件触发action函数并传递参数
<i class="iconfont icon-close-new" @click="cartStore.delCart(i.skuId)"></i> 
 
3. 列表购物车-基础内容渲染 3.1. 准备模版 <script setup> const cartList = [] </script> <template>   <div class="xtx-cart-page">     <div class="container m-top-20">       <div class="cart">         <table>           <thead>             <tr>               <th width="120">                 <el-checkbox/>               </th>               <th width="400">商品信息</th>               <th width="220">单价</th>               <th width="180">数量</th>               <th width="180">小计</th>               <th width="140">操作</th>             </tr>           </thead>           <!-- 商品列表 -->           <tbody>             <tr v-for="i in cartList" :key="i.id">               <td>                 <el-checkbox />               </td>               <td>                 <div class="goods">                   <RouterLink to="/"><img :src="i.picture" alt="" /></RouterLink>                   <div>                     <p class="name ellipsis">                       {{ i.name }}                     </p>                   </div>                 </div>               </td>               <td class="tc">                 <p>¥{{ i.price }}</p>               </td>               <td class="tc">                 <el-input-number v-model="i.count" />               </td>               <td class="tc">                 <p class="f16 red">¥{{ (i.price * i.count).toFixed(2) }}</p>               </td>               <td class="tc">                 <p>                   <el-popconfirm title="确认删除吗?" confirm-button-text="确认" cancel-button-text="取消" @confirm="delCart(i)">                     <template #reference>                       <a href="javascript:;">删除</a>                     </template>                   </el-popconfirm>                 </p>               </td>             </tr>             <tr v-if="cartList.length === 0">               <td colspan="6">                 <div class="cart-none">                   <el-empty description="购物车列表为空">                     <el-button type="primary">随便逛逛</el-button>                   </el-empty>                 </div>               </td>             </tr>           </tbody>         </table>       </div>       <!-- 操作栏 -->       <div class="action">         <div class="batch">           共 10 件商品,已选择 2 件,商品合计:           <span class="red">¥ 200.00 </span>         </div>         <div class="total">           <el-button size="large" type="primary" >下单结算</el-button>         </div>       </div>     </div>   </div> </template> <style scoped lang="scss"> .xtx-cart-page {   margin-top: 20px;   .cart {     background: #fff;     color: #666;     table {       border-spacing: 0;       border-collapse: collapse;       line-height: 24px;       th,       td {         padding: 10px;         border-bottom: 1px solid #f5f5f5;         &:first-child {           text-align: left;           padding-left: 30px;           color: #999;         }       }       th {         font-size: 16px;         font-weight: normal;         line-height: 50px;       }     }   }   .cart-none {     text-align: center;     padding: 120px 0;     background: #fff;     p {       color: #999;       padding: 20px 0;     }   }   .tc {     text-align: center;     a {       color: $xtxColor;     }     .xtx-numbox {       margin: 0 auto;       width: 120px;     }   }   .red {     color: $priceColor;   }   .green {     color: $xtxColor;   }   .f16 {     font-size: 16px;   }   .goods {     display: flex;     align-items: center;     img {       width: 100px;       height: 100px;     }     >div {       width: 280px;       font-size: 16px;       padding-left: 10px;       .attr {         font-size: 14px;         color: #999;       }     }   }   .action {     display: flex;     background: #fff;     margin-top: 20px;     height: 80px;     align-items: center;     font-size: 16px;     justify-content: space-between;     padding: 0 30px;     .xtx-checkbox {       color: #999;     }     .batch {       a {         margin-left: 20px;       }     }     .red {       font-size: 18px;       margin-right: 20px;       font-weight: bold;     }   }   .tit {     color: #666;     font-size: 16px;     font-weight: normal;     line-height: 50px;   } } </style> 
 
3.2. 绑定路由 import  CartList  from  '@/views/CartList/index.vue' {   path : 'cartlist' ,   component : CartList  } 
 
3.3. 渲染列表 <script setup> import { useCartStore } from '@/stores/cartStore' const cartStore = useCartStore() </script> <template>   <div class="xtx-cart-page">     <div class="container m-top-20">       <div class="cart">         <table>           <thead>             <tr>               <th width="120">                 <el-checkbox />               </th>               <th width="400">商品信息</th>               <th width="220">单价</th>               <th width="180">数量</th>               <th width="180">小计</th>               <th width="140">操作</th>             </tr>           </thead>           <!-- 商品列表 -->           <tbody>             <tr v-for="i in cartStore.cartList" :key="i.id">               <td>                 <!-- 单选框 -->                 <el-checkbox/>               </td>               <td>                 <div class="goods">                   <RouterLink to="/"><img :src="i.picture" alt="" /></RouterLink>                   <div>                     <p class="name ellipsis">                       {{ i.name }}                     </p>                   </div>                 </div>               </td>               <td class="tc">                 <p>¥{{ i.price }}</p>               </td>               <td class="tc">                 <el-input-number v-model="i.count" />               </td>               <td class="tc">                 <p class="f16 red">¥{{ (i.price * i.count).toFixed(2) }}</p>               </td>               <td class="tc">                 <p>                   <el-popconfirm title="确认删除吗?" confirm-button-text="确认" cancel-button-text="取消" @confirm="delCart(i)">                     <template #reference>                       <a href="javascript:;">删除</a>                     </template>                   </el-popconfirm>                 </p>               </td>             </tr>             <tr v-if="cartStore.cartList.length === 0">               <td colspan="6">                 <div class="cart-none">                   <el-empty description="购物车列表为空">                     <el-button type="primary">随便逛逛</el-button>                   </el-empty>                 </div>               </td>             </tr>           </tbody>         </table>       </div>       <!-- 操作栏 -->       <div class="action">         <div class="batch">           共 10 件商品,已选择 2 件,商品合计:           <span class="red">¥ 200.00 </span>         </div>         <div class="total">           <el-button size="large" type="primary" >下单结算</el-button>         </div>       </div>     </div>   </div> </template> 
 
4. 列表购物车-单选功能实现 
基本思想:通过skuId找到要进行单选操作的商品,把控制是否选中的selected字段修改为当前单选框的状态
 
1- 添加单选action
const  singleCheck  = (skuId, selected ) => {     const  item = cartList.value .find ((item ) =>  item.skuId  === skuId)   item.selected  = selected } 
 
2- 触发action函数
<script setup> // 单选回调 const singleCheck = (i, selected) => {   console.log(i, selected)   // store cartList 数组 无法知道要修改谁的选中状态?   // 除了selected补充一个用来筛选的参数 - skuId   cartStore.singleCheck(i.skuId, selected) } </script>    <template>   <td>     <!-- 单选框 -->     <el-checkbox :model-value="i.selected" @change="(selected) => singleCheck(i, selected)" />   </td> </template> 
 
5. 列表购物车-全选功能实现 
基础思想:
全选状态决定单选框状态 - 遍历cartList把每一项的selected都设置为何全选框状态一致 
单选框状态决定全选状态 - 只有所有单选框的selected都为true, 全选框才为true 
 
 
1- store中定义action和计算属性
const  allCheck  = (selected ) => {     cartList.value .forEach (item  =>  item.selected  = selected) } const  isAll = computed (() =>  cartList.value .every ((item ) =>  item.selected ))
 
2- 组件中触发aciton和使用计算属性
<script setup> const allCheck = (selected) => {   cartStore.allCheck(selected) } </script>    <template>   <!-- 全选框 -->   <el-checkbox :model-value="cartStore.isAll" @change="allCheck" /> </template> 
 
6. 列表购物车-统计数据功能实现 const  selectedCount = computed (() =>  cartList.value .filter (item  =>  item.selected ).reduce ((a, c ) =>  a + c.count , 0 ))const  selectedPrice = computed (() =>  cartList.value .filter (item  =>  item.selected ).reduce ((a, c ) =>  a + c.count  * c.price , 0 ))
 
接口购物车 1. 加入购物车 1-接口封装
export  const  insertCartAPI  = ({ skuId, count } ) => {  return  request ({     url : '/member/cart' ,     method : 'POST' ,     data : {       skuId,       count     }   }) } 
 
2- action中适配登录和非登录
import  { defineStore } from  'pinia' import  { useUserStore } from  './userStore' import  { insertCartAPI } from  '@/apis/cart' export  const  useCartStore = defineStore ('cart' , () =>  {  const  userStore = useUserStore ()   const  isLogin = computed (() =>  userStore.userInfo .token )   const  addCart  = async  (goods ) => {     const  { skuId, count } = goods          if  (isLogin.value ) {              await  insertCartAPI ({ skuId, count })       updateNewList ()     } else  {              const  item = cartList.value .find ((item ) =>  goods.skuId  === item.skuId )       if  (item) {                  item.count ++       } else  {                  cartList.value .push (goods)       }     }   } }, {   persist : true , }) 
 
2. 删除购物车 1- 封装接口
export  const  delCartAPI  = (ids ) => {  return  request ({     url : '/member/cart' ,     method : 'DELETE' ,     data : {       ids     }   }) } 
 
2- action中适配登录和非登录
const  delCart  = async  (skuId ) => {  if  (isLogin.value ) {          await  delCartAPI ([skuId])     updateNewList ()   } else  {                    const  idx = cartList.value .findIndex ((item ) =>  skuId === item.skuId )     cartList.value .splice (idx, 1 )   } } 
 
订单页 路由配置和基础数据渲染 1. 准备组件模版 <script setup> const checkInfo = {}  // 订单对象 const curAddress = {}  // 地址对象 </script> <template>   <div class="xtx-pay-checkout-page">     <div class="container">       <div class="wrapper">         <!-- 收货地址 -->         <h3 class="box-title">收货地址</h3>         <div class="box-body">           <div class="address">             <div class="text">               <div class="none" v-if="!curAddress">您需要先添加收货地址才可提交订单。</div>               <ul v-else>                 <li><span>收<i />货<i />人:</span>{{ curAddress.receiver }}</li>                 <li><span>联系方式:</span>{{ curAddress.contact }}</li>                 <li><span>收货地址:</span>{{ curAddress.fullLocation }} {{ curAddress.address }}</li>               </ul>             </div>             <div class="action">               <el-button size="large" @click="toggleFlag = true">切换地址</el-button>               <el-button size="large" @click="addFlag = true">添加地址</el-button>             </div>           </div>         </div>         <!-- 商品信息 -->         <h3 class="box-title">商品信息</h3>         <div class="box-body">           <table class="goods">             <thead>               <tr>                 <th width="520">商品信息</th>                 <th width="170">单价</th>                 <th width="170">数量</th>                 <th width="170">小计</th>                 <th width="170">实付</th>               </tr>             </thead>             <tbody>               <tr v-for="i in checkInfo.goods" :key="i.id">                 <td>                   <a href="javascript:;" class="info">                     <img :src="i.picture" alt="">                     <div class="right">                       <p>{{ i.name }}</p>                       <p>{{ i.attrsText }}</p>                     </div>                   </a>                 </td>                 <td>¥{{ i.price }}</td>                 <td>{{ i.price }}</td>                 <td>¥{{ i.totalPrice }}</td>                 <td>¥{{ i.totalPayPrice }}</td>               </tr>             </tbody>           </table>         </div>         <!-- 配送时间 -->         <h3 class="box-title">配送时间</h3>         <div class="box-body">           <a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a>           <a class="my-btn" href="javascript:;">工作日送货:周一至周五</a>           <a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a>         </div>         <!-- 支付方式 -->         <h3 class="box-title">支付方式</h3>         <div class="box-body">           <a class="my-btn active" href="javascript:;">在线支付</a>           <a class="my-btn" href="javascript:;">货到付款</a>           <span style="color:#999">货到付款需付5元手续费</span>         </div>         <!-- 金额明细 -->         <h3 class="box-title">金额明细</h3>         <div class="box-body">           <div class="total">             <dl>               <dt>商品件数:</dt>               <dd>{{ checkInfo.summary?.goodsCount }}件</dd>             </dl>             <dl>               <dt>商品总价:</dt>               <dd>¥{{ checkInfo.summary?.totalPrice.toFixed(2) }}</dd>             </dl>             <dl>               <dt>运<i></i>费:</dt>               <dd>¥{{ checkInfo.summary?.postFee.toFixed(2) }}</dd>             </dl>             <dl>               <dt>应付总额:</dt>               <dd class="price">{{ checkInfo.summary?.totalPayPrice.toFixed(2) }}</dd>             </dl>           </div>         </div>         <!-- 提交订单 -->         <div class="submit">           <el-button type="primary" size="large" >提交订单</el-button>         </div>       </div>     </div>   </div>   <!-- 切换地址 -->   <!-- 添加地址 --> </template> <style scoped lang="scss"> .xtx-pay-checkout-page {   margin-top: 20px;   .wrapper {     background: #fff;     padding: 0 20px;     .box-title {       font-size: 16px;       font-weight: normal;       padding-left: 10px;       line-height: 70px;       border-bottom: 1px solid #f5f5f5;     }     .box-body {       padding: 20px 0;     }   } } .address {   border: 1px solid #f5f5f5;   display: flex;   align-items: center;   .text {     flex: 1;     min-height: 90px;     display: flex;     align-items: center;     .none {       line-height: 90px;       color: #999;       text-align: center;       width: 100%;     }     >ul {       flex: 1;       padding: 20px;       li {         line-height: 30px;         span {           color: #999;           margin-right: 5px;           >i {             width: 0.5em;             display: inline-block;           }         }       }     }     >a {       color: $xtxColor;       width: 160px;       text-align: center;       height: 90px;       line-height: 90px;       border-right: 1px solid #f5f5f5;     }   }   .action {     width: 420px;     text-align: center;     .btn {       width: 140px;       height: 46px;       line-height: 44px;       font-size: 14px;       &:first-child {         margin-right: 10px;       }     }   } } .goods {   width: 100%;   border-collapse: collapse;   border-spacing: 0;   .info {     display: flex;     text-align: left;     img {       width: 70px;       height: 70px;       margin-right: 20px;     }     .right {       line-height: 24px;       p {         &:last-child {           color: #999;         }       }     }   }   tr {     th {       background: #f5f5f5;       font-weight: normal;     }     td,     th {       text-align: center;       padding: 20px;       border-bottom: 1px solid #f5f5f5;       &:first-child {         border-left: 1px solid #f5f5f5;       }       &:last-child {         border-right: 1px solid #f5f5f5;       }     }   } } .my-btn {   width: 228px;   height: 50px;   border: 1px solid #e4e4e4;   text-align: center;   line-height: 48px;   margin-right: 25px;   color: #666666;   display: inline-block;   &.active,   &:hover {     border-color: $xtxColor;   } } .total {   dl {     display: flex;     justify-content: flex-end;     line-height: 50px;     dt {       i {         display: inline-block;         width: 2em;       }     }     dd {       width: 240px;       text-align: right;       padding-right: 70px;       &.price {         font-size: 20px;         color: $priceColor;       }     }   } } .submit {   text-align: right;   padding: 60px;   border-top: 1px solid #f5f5f5; } .addressWrapper {   max-height: 500px;   overflow-y: auto; } .text {   flex: 1;   min-height: 90px;   display: flex;   align-items: center;   &.item {     border: 1px solid #f5f5f5;     margin-bottom: 10px;     cursor: pointer;     &.active,     &:hover {       border-color: $xtxColor;       background: lighten($xtxColor, 50%);     }     >ul {       padding: 10px;       font-size: 14px;       line-height: 30px;     }   } } </style> 
 
2. 配置路由 3. 封装接口 import  request from  '@/utils/request' export  const  getCheckoutInfoAPI  = ( ) => {  return  request ({     url :'/member/order/pre'    }) } 
 
4. 渲染数据 切换地址-打开弹框交互 1. 准备弹框模版 <el-dialog  title ="切换收货地址"  width ="30%"  center >   <div  class ="addressWrapper" >      <div  class ="text item"  v-for ="item in checkInfo.userAddresses"   :key ="item.id" >        <ul >        <li > <span > 收<i  /> 货<i  /> 人:</span > {{ item.receiver }} </li >        <li > <span > 联系方式:</span > {{ item.contact }}</li >        <li > <span > 收货地址:</span > {{ item.fullLocation + item.address }}</li >        </ul >      </div >    </div >    <template  #footer >      <span  class ="dialog-footer" >        <el-button > 取消</el-button >        <el-button  type ="primary" > 确定</el-button >      </span >    </template >  </el-dialog > 
 
2. 控制弹框打开 const showDialog = ref(false) <el-button size="large" @click="showDialog = true">切换地址</el-button> <el-dialog v-model="showDialog" title="切换收货地址" width="30%" center>     <!-- 省略 --> </el-dialog> 
 
切换地址-地址切换交互 
基础思想:记录当前点击项,通过动态class判断当前div是否有激活类名
 
<script setup> // 切换地址 const activeAddress = ref({}) const switchAddress = (item) => {   activeAddress.value = item } </script> <template> <div class="text item"    :class="{ active: activeAddress.id === item.id }"    @click="switchAddress(item)"   :key="item.id">   <!-- 省略... --> </div> </template> 
 
创建订单生成订单ID 1. 准备支付页组件并绑定路由 <script setup> const payInfo = {} </script> <template>   <div class="xtx-pay-page">     <div class="container">       <!-- 付款信息 -->       <div class="pay-info">         <span class="icon iconfont icon-queren2"></span>         <div class="tip">           <p>订单提交成功!请尽快完成支付。</p>           <p>支付还剩 <span>24分30秒</span>, 超时后将取消订单</p>         </div>         <div class="amount">           <span>应付总额:</span>           <span>¥{{ payInfo.payMoney?.toFixed(2) }}</span>         </div>       </div>       <!-- 付款方式 -->       <div class="pay-type">         <p class="head">选择以下支付方式付款</p>         <div class="item">           <p>支付平台</p>           <a class="btn wx" href="javascript:;"></a>           <a class="btn alipay" :href="payUrl"></a>         </div>         <div class="item">           <p>支付方式</p>           <a class="btn" href="javascript:;">招商银行</a>           <a class="btn" href="javascript:;">工商银行</a>           <a class="btn" href="javascript:;">建设银行</a>           <a class="btn" href="javascript:;">农业银行</a>           <a class="btn" href="javascript:;">交通银行</a>         </div>       </div>     </div>   </div> </template> <style scoped lang="scss"> .xtx-pay-page {   margin-top: 20px; } .pay-info {   background: #fff;   display: flex;   align-items: center;   height: 240px;   padding: 0 80px;   .icon {     font-size: 80px;     color: #1dc779;   }   .tip {     padding-left: 10px;     flex: 1;     p {       &:first-child {         font-size: 20px;         margin-bottom: 5px;       }       &:last-child {         color: #999;         font-size: 16px;       }     }   }   .amount {     span {       &:first-child {         font-size: 16px;         color: #999;       }       &:last-child {         color: $priceColor;         font-size: 20px;       }     }   } } .pay-type {   margin-top: 20px;   background-color: #fff;   padding-bottom: 70px;   p {     line-height: 70px;     height: 70px;     padding-left: 30px;     font-size: 16px;     &.head {       border-bottom: 1px solid #f5f5f5;     }   }   .btn {     width: 150px;     height: 50px;     border: 1px solid #e4e4e4;     text-align: center;     line-height: 48px;     margin-left: 30px;     color: #666666;     display: inline-block;     &.active,     &:hover {       border-color: $xtxColor;     }     &.alipay {       background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7b6b02396368c9314528c0bbd85a2e06.png) no-repeat center / contain;     }     &.wx {       background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/c66f98cff8649bd5ba722c2e8067c6ca.jpg) no-repeat center / contain;     }   } } </style> 
 
2. 准备生成订单接口 export  const  createOrderAPI  = (data ) => {  return  request ({     url : '/member/order' ,     method : 'POST' ,     data   }) } 
 
3. 调用接口携带id跳转路由 <script setup> import { createOrderAPI } from '@/apis/checkout' // 创建订单 const createOrder = async () => {   const res = await createOrderAPI({     deliveryTimeType: 1,     payType: 1,     payChannel: 1,     buyerMessage: '',     goods: checkInfo.value.goods.map(item => {       return {         skuId: item.skuId,         count: item.count       }     }),     addressId: curAddress.value.id   })   const orderId = res.result.id   router.push({     path: '/pay',     query: {       id: orderId     }   }) } </script> <template>     <!-- 提交订单 -->     <div class="submit">       <el-button @click="createOrder" type="primary" size="large">提交订单</el-button>     </div> </template> 
 
支付页 基础数据渲染 1. 准备接口 import  request from  '@/utils/http' export  const  getOrderAPI  = (id ) => {  return  request ({     url : `/member/order/${id} `    }) } 
 
2. 获取数据渲染内容 <script setup> import { getOrderAPI } from '@/apis/pay' import { onMounted, ref } from 'vue' import { useRoute } from 'vue-router' // 获取订单数据 const route = useRoute() const payInfo = ref({}) const getPayInfo = async () => {   const res = await getOrderAPI(route.query.id)   payInfo.value = res.result } onMounted(() => getPayInfo()) </script> <template>   <div class="xtx-pay-page">     <div class="container">       <!-- 付款信息 -->       <div class="pay-info">         <span class="icon iconfont icon-queren2"></span>         <div class="tip">           <p>订单提交成功!请尽快完成支付。</p>           <p>支付还剩 <span>{{ formatTime }}</span>, 超时后将取消订单</p>         </div>         <div class="amount">           <span>应付总额:</span>           <span>¥{{ payInfo.payMoney?.toFixed(2) }}</span>         </div>       </div>       <!-- 付款方式 -->       <div class="pay-type">         <p class="head">选择以下支付方式付款</p>         <div class="item">           <p>支付平台</p>           <a class="btn wx" href="javascript:;"></a>           <a class="btn alipay" :href="payUrl"></a>         </div>         <div class="item">           <p>支付方式</p>           <a class="btn" href="javascript:;">招商银行</a>           <a class="btn" href="javascript:;">工商银行</a>           <a class="btn" href="javascript:;">建设银行</a>           <a class="btn" href="javascript:;">农业银行</a>           <a class="btn" href="javascript:;">交通银行</a>         </div>       </div>     </div>   </div> </template> 
 
支付功能实现 
1. 支付携带参数 const  baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/' const  backURL = 'http://127.0.0.1:5173/paycallback' const  redirectUrl = encodeURIComponent (backURL)const  payUrl = `${baseURL} pay/aliPay?orderId=${route.query.id} &redirect=${redirectUrl} ` 
 
2. 支付宝沙箱账号信息 
支付结果页展示 1. 准备模版 <script setup> </script> <template>   <div class="xtx-pay-page">     <div class="container">       <!-- 支付结果 -->       <div class="pay-result">         <span class="iconfont icon-queren2 green"></span>         <span class="iconfont icon-shanchu red"></span>         <p class="tit">支付成功</p>         <p class="tip">我们将尽快为您发货,收货期间请保持手机畅通</p>         <p>支付方式:<span>支付宝</span></p>         <p>支付金额:<span>¥200.00</span></p>         <div class="btn">           <el-button type="primary" style="margin-right:20px">查看订单</el-button>           <el-button>进入首页</el-button>         </div>         <p class="alert">           <span class="iconfont icon-tip"></span>           温馨提示:小兔鲜儿不会以订单异常、系统升级为由要求您点击任何网址链接进行退款操作,保护资产、谨慎操作。         </p>       </div>     </div>   </div> </template> <style scoped lang="scss"> .pay-result {   padding: 100px 0;   background: #fff;   text-align: center;   margin-top: 20px;   >.iconfont {     font-size: 100px;   }   .green {     color: #1dc779;   }   .red {     color: $priceColor;   }   .tit {     font-size: 24px;   }   .tip {     color: #999;   }   p {     line-height: 40px;     font-size: 16px;   }   .btn {     margin-top: 50px;   }   .alert {     font-size: 12px;     color: #999;     margin-top: 50px;   } } </style> 
 
2. 绑定路由 {   path : 'paycallback' ,    component : PayBack  }, 
 
3. 渲染数据 <script setup> import { getOrderAPI } from '@/apis/pay' import { onMounted, ref } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const orderInfo = ref({}) const getOrderInfo = async () => {   const res = await getOrderAPI(route.query.orderId)   orderInfo.value = res.result } onMounted(() => getOrderInfo()) </script> <template>   <div class="xtx-pay-page">     <div class="container">       <!-- 支付结果 -->       <div class="pay-result">         <!-- 路由参数获取到的是字符串而不是布尔值 -->         <span class="iconfont icon-queren2 green" v-if="$route.query.payResult === 'true'"></span>         <span class="iconfont icon-shanchu red" v-else></span>         <p class="tit">支付{{ $route.query.payResult === 'true' ? '成功' : '失败' }}</p>         <p class="tip">我们将尽快为您发货,收货期间请保持手机畅通</p>         <p>支付方式:<span>支付宝</span></p>         <p>支付金额:<span>¥{{ orderInfo.payMoney?.toFixed(2) }}</span></p>         <div class="btn">           <el-button type="primary" style="margin-right:20px">查看订单</el-button>           <el-button>进入首页</el-button>         </div>         <p class="alert">           <span class="iconfont icon-tip"></span>           温馨提示:小兔鲜儿不会以订单异常、系统升级为由要求您点击任何网址链接进行退款操作,保护资产、谨慎操作。         </p>       </div>     </div>   </div> </template> 
 
倒计时逻辑函数封装 import  { computed, onUnmounted, ref } from  'vue' import  dayjs from  'dayjs' export  const  useCountDown  = ( ) => {     let  timer = null    const  time = ref (0 )      const  formatTime = computed (() =>  dayjs.unix (time.value ).format ('mm分ss秒' ))      const  start  = (currentTime ) => {               time.value  = currentTime     timer = setInterval (() =>  {       time.value --     }, 1000 )   }      onUnmounted (() =>  {     timer && clearInterval (timer)   })   return  {     formatTime,     start   } } 
 
会员中心 路由配置 1. 准备路由模版 <script setup> </script> <template>   <div class="container">     <div class="xtx-member-aside">       <div class="user-manage">         <h4>我的账户</h4>         <div class="links">           <RouterLink to="/member/user">个人中心</RouterLink>         </div>         <h4>交易管理</h4>         <div class="links">           <RouterLink to="/member/order">我的订单</RouterLink>         </div>       </div>     </div>     <div class="article">       <!-- 三级路由的挂载点 -->       <!-- <RouterView /> -->     </div>   </div> </template> <style scoped lang="scss"> .container {   display: flex;   padding-top: 20px;   .xtx-member-aside {     width: 220px;     margin-right: 20px;     border-radius: 2px;     background-color: #fff;     .user-manage {       background-color: #fff;       h4 {         font-size: 18px;         font-weight: 400;         padding: 20px 52px 5px;         border-top: 1px solid #f6f6f6;       }       .links {         padding: 0 52px 10px;       }       a {         display: block;         line-height: 1;         padding: 15px 0;         font-size: 14px;         color: #666;         position: relative;         &:hover {           color: $xtxColor;         }         &.active,         &.router-link-exact-active {           color: $xtxColor;           &:before {             display: block;           }         }         &:before {           content: '';           display: none;           width: 6px;           height: 6px;           border-radius: 50%;           position: absolute;           top: 19px;           left: -16px;           background-color: $xtxColor;         }       }     }   }   .article {     width: 1000px;     background-color: #fff;   } } </style> 
 
2. 配置路由 import  Member  from  '@/views/Member/index.vue' {     path : '/member' ,     component : Member , } 
 
3. 准备个人信息和我的订单路由组件 <script setup> const userStore = {} </script> <template>   <div class="home-overview">     <!-- 用户信息 -->     <div class="user-meta">       <div class="avatar">         <img :src="userStore.userInfo?.avatar" />       </div>       <h4>{{ userStore.userInfo?.account }}</h4>     </div>     <div class="item">       <a href="javascript:;">         <span class="iconfont icon-hy"></span>         <p>会员中心</p>       </a>       <a href="javascript:;">         <span class="iconfont icon-aq"></span>         <p>安全设置</p>       </a>       <a href="javascript:;">         <span class="iconfont icon-dw"></span>         <p>地址管理</p>       </a>     </div>   </div>   <div class="like-container">     <div class="home-panel">       <div class="header">         <h4 data-v-bcb266e0="">猜你喜欢</h4>       </div>       <div class="goods-list">         <!-- <GoodsItem v-for="good in likeList" :key="good.id" :good="good" /> -->       </div>     </div>   </div> </template> <style scoped lang="scss"> .home-overview {   height: 132px;   background: url(@/assets/images/center-bg.png) no-repeat center / cover;   display: flex;   .user-meta {     flex: 1;     display: flex;     align-items: center;     .avatar {       width: 85px;       height: 85px;       border-radius: 50%;       overflow: hidden;       margin-left: 60px;       img {         width: 100%;         height: 100%;       }     }     h4 {       padding-left: 26px;       font-size: 18px;       font-weight: normal;       color: white;     }   }   .item {     flex: 1;     display: flex;     align-items: center;     justify-content: space-around;     &:first-child {       border-right: 1px solid #f4f4f4;     }     a {       color: white;       font-size: 16px;       text-align: center;       .iconfont {         font-size: 32px;       }       p {         line-height: 32px;       }     }   } } .like-container {   margin-top: 20px;   border-radius: 4px;   background-color: #fff; } .home-panel {   background-color: #fff;   padding: 0 20px;   margin-top: 20px;   height: 400px;   .header {     height: 66px;     border-bottom: 1px solid #f5f5f5;     padding: 18px 0;     display: flex;     justify-content: space-between;     align-items: baseline;     h4 {       font-size: 22px;       font-weight: 400;     }   }   .goods-list {     display: flex;     justify-content: space-around;   } } </style> 
 
<script setup> // tab列表 const tabTypes = [   { name: "all", label: "全部订单" },   { name: "unpay", label: "待付款" },   { name: "deliver", label: "待发货" },   { name: "receive", label: "待收货" },   { name: "comment", label: "待评价" },   { name: "complete", label: "已完成" },   { name: "cancel", label: "已取消" } ] // 订单列表 const orderList = [] </script> <template>   <div class="order-container">     <el-tabs>       <!-- tab切换 -->       <el-tab-pane v-for="item in tabTypes" :key="item.name" :label="item.label" />       <div class="main-container">         <div class="holder-container" v-if="orderList.length === 0">           <el-empty description="暂无订单数据" />         </div>         <div v-else>           <!-- 订单列表 -->           <div class="order-item" v-for="order in orderList" :key="order.id">             <div class="head">               <span>下单时间:{{ order.createTime }}</span>               <span>订单编号:{{ order.id }}</span>               <!-- 未付款,倒计时时间还有 -->               <span class="down-time" v-if="order.orderState === 1">                 <i class="iconfont icon-down-time"></i>                 <b>付款截止: {{order.countdown}}</b>               </span>             </div>             <div class="body">               <div class="column goods">                 <ul>                   <li v-for="item in order.skus" :key="item.id">                     <a class="image" href="javascript:;">                       <img :src="item.image" alt="" />                     </a>                     <div class="info">                       <p class="name ellipsis-2">                         {{ item.name }}                       </p>                       <p class="attr ellipsis">                         <span>{{ item.attrsText }}</span>                       </p>                     </div>                     <div class="price">¥{{ item.realPay?.toFixed(2) }}</div>                     <div class="count">x{{ item.quantity }}</div>                   </li>                 </ul>               </div>               <div class="column state">                 <p>{{ order.orderState }}</p>                 <p v-if="order.orderState === 3">                   <a href="javascript:;" class="green">查看物流</a>                 </p>                 <p v-if="order.orderState === 4">                   <a href="javascript:;" class="green">评价商品</a>                 </p>                 <p v-if="order.orderState === 5">                   <a href="javascript:;" class="green">查看评价</a>                 </p>               </div>               <div class="column amount">                 <p class="red">¥{{ order.payMoney?.toFixed(2) }}</p>                 <p>(含运费:¥{{ order.postFee?.toFixed(2) }})</p>                 <p>在线支付</p>               </div>               <div class="column action">                 <el-button  v-if="order.orderState === 1" type="primary"                   size="small">                   立即付款                 </el-button>                 <el-button v-if="order.orderState === 3" type="primary" size="small">                   确认收货                 </el-button>                 <p><a href="javascript:;">查看详情</a></p>                 <p v-if="[2, 3, 4, 5].includes(order.orderState)">                   <a href="javascript:;">再次购买</a>                 </p>                 <p v-if="[4, 5].includes(order.orderState)">                   <a href="javascript:;">申请售后</a>                 </p>                 <p v-if="order.orderState === 1"><a href="javascript:;">取消订单</a></p>               </div>             </div>           </div>           <!-- 分页 -->           <div class="pagination-container">             <el-pagination background layout="prev, pager, next" />           </div>         </div>       </div>     </el-tabs>   </div> </template> <style scoped lang="scss"> .order-container {   padding: 10px 20px;   .pagination-container {     display: flex;     justify-content: center;   }   .main-container {     min-height: 500px;     .holder-container {       min-height: 500px;       display: flex;       justify-content: center;       align-items: center;     }   } } .order-item {   margin-bottom: 20px;   border: 1px solid #f5f5f5;   .head {     height: 50px;     line-height: 50px;     background: #f5f5f5;     padding: 0 20px;     overflow: hidden;     span {       margin-right: 20px;       &.down-time {         margin-right: 0;         float: right;         i {           vertical-align: middle;           margin-right: 3px;         }         b {           vertical-align: middle;           font-weight: normal;         }       }     }     .del {       margin-right: 0;       float: right;       color: #999;     }   }   .body {     display: flex;     align-items: stretch;     .column {       border-left: 1px solid #f5f5f5;       text-align: center;       padding: 20px;       >p {         padding-top: 10px;       }       &:first-child {         border-left: none;       }       &.goods {         flex: 1;         padding: 0;         align-self: center;         ul {           li {             border-bottom: 1px solid #f5f5f5;             padding: 10px;             display: flex;             &:last-child {               border-bottom: none;             }             .image {               width: 70px;               height: 70px;               border: 1px solid #f5f5f5;             }             .info {               width: 220px;               text-align: left;               padding: 0 10px;               p {                 margin-bottom: 5px;                 &.name {                   height: 38px;                 }                 &.attr {                   color: #999;                   font-size: 12px;                   span {                     margin-right: 5px;                   }                 }               }             }             .price {               width: 100px;             }             .count {               width: 80px;             }           }         }       }       &.state {         width: 120px;         .green {           color: $xtxColor;         }       }       &.amount {         width: 200px;         .red {           color: $priceColor;         }       }       &.action {         width: 140px;         a {           display: block;           &:hover {             color: $xtxColor;           }         }       }     }   } } </style> 
 
4. 配置三级路由 import  MemberInfo  from  '@/views/Member/components/UserInfo.vue' import  MemberOrder  from  '@/views/Member/components/UserOrder.vue' {   path : '/member' ,   component : Member ,   children : [     {       path : '' ,       component : MemberInfo      },     {       path : 'order' ,       component : MemberOrder      }   ] } 
 
个人中心信息渲染 1. 使用Pinia数据渲染个人信息 <script setup> // 导入userStore import { useUserStore } from '@/stores/userStore' const userStore = useUserStore() </script> <template>   <!-- 用户信息 -->   <div class="user-meta">     <div class="avatar">       <img :src="userStore.userInfo?.avatar" />     </div>     <h4>{{ userStore.userInfo?.account }}</h4>   </div> </template> 
 
2. 封装猜你喜欢接口 export  const  getLikeListAPI  = ({ limit = 4  } ) => {  return  request ({     url :'/goods/relevant' ,     params : {       limit      }   }) } 
 
3. 渲染猜你喜欢数据 <script setup> import { onMounted, ref } from 'vue' // 导入GoodsItem组件 import GoodsItem from '@/views/Home/components/GoodsItem.vue' // 获取猜你喜欢列表 const likeList = ref([]) const getLikeList = async () => {   const res = await getLikeListAPI({ limit: 4 })   likeList.value = res.result } onMounted(() => getLikeList()) </script> <template>    <div class="goods-list">       <GoodsItem v-for="good in likeList" :key="good.id" :goods="good" />     </div> </template> 
 
我的订单 1. 基础列表渲染 export  const  getUserOrder  = (params ) => {  return  request ({     url :'/member/order' ,     method :'GET' ,     params   }) } 
 
<script setup> import { getUserOrder } from '@/apis/order' import { onMounted, ref } from 'vue' // 获取订单列表 const orderList = ref([]) const params = ref({   orderState: 0,   page: 1,   pageSize: 2 }) const getOrderList = async () => {   const res = await getUserOrder(params.value)   orderList.value = res.result.items   total.value = res.result.counts } onMounted(() => getOrderList()) </script> 
 
2. tab切换实现 <script setup> // tab列表 const tabTypes = [   { name: "all", label: "全部订单" },   { name: "unpay", label: "待付款" },   { name: "deliver", label: "待发货" },   { name: "receive", label: "待收货" },   { name: "comment", label: "待评价" },   { name: "complete", label: "已完成" },   { name: "cancel", label: "已取消" } ] // tab切换 const tabChange = (type) => {   params.value.orderState = type   getOrderList() } </script> <template>   <el-tabs @tab-change="tabChange">     <!-- 省略... -->   </el-tabs> </template> 
 
3. 分页逻实现 
页数 = 总条数 / 每页条数
 
<script setup> // 补充总条数 const total = ref(0) const getOrderList = async () => {   const res = await getUserOrder(params.value)   // 存入总条数   total.value = res.result.counts } // 页数切换 const pageChange = (page) => {   params.value.page = page   getOrderList() } </script> <template>    <el-pagination       :total="total"       @current-change="pageChange"       :page-size="params.pageSize"       background      layout="prev, pager, next" /> </template> 
 
细节优化 1. 默认三级路由设置 {   path : 'member' ,   component : Member ,   children : [     {       path : '' ,        component : UserInfo      },     {       path : 'order' ,       component : UserOrder      }   ] } 
 
2. 订单状态显示适配 <script setup>   // 创建格式化函数   const fomartPayState = (payState) => {     const stateMap = {       1: '待付款',       2: '待发货',       3: '待收货',       4: '待评价',       5: '已完成',       6: '已取消'     }     return stateMap[payState]   } </script> <template>   <!-- 调用函数适配显示 -->   <p>{{ fomartPayState(order.orderState)}}</p> </template> 
 
拓展部分 Sku组件封装 1. 准备模版渲染规格数据 
使用Vite快速创建一个Vue项目,在项目中添加请求插件axios,然后新增一个SKU组件,在根组件中把它渲染出来,下面是规格内容的基础模板
 
<script setup> import { onMounted, ref } from 'vue' import axios from 'axios' // 商品数据 const goods = ref({}) const getGoods = async () => {   // 1135076  初始化就有无库存的规格   // 1369155859933827074 更新之后有无库存项(蓝色-20cm-中国)   const res = await axios.get('http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074')   goods.value = res.data.result } onMounted(() => getGoods()) </script> <template>   <div class="goods-sku">     <dl v-for="item in goods.specs" :key="item.id">       <dt>{{ item.name }}</dt>       <dd>         <template v-for="val in item.values" :key="val.name">           <!-- 图片类型规格 -->           <img v-if="val.picture" :src="val.picture" :title="val.name">           <!-- 文字类型规格 -->           <span v-else>{{ val.name }}</span>         </template>       </dd>     </dl>   </div> </template> <style scoped lang="scss"> @mixin sku-state-mixin {   border: 1px solid #e4e4e4;   margin-right: 10px;   cursor: pointer;   &.selected {     border-color: #27ba9b;   }   &.disabled {     opacity: 0.6;     border-style: dashed;     cursor: not-allowed;   } } .goods-sku {   padding-left: 10px;   padding-top: 20px;   dl {     display: flex;     padding-bottom: 20px;     align-items: center;     dt {       width: 50px;       color: #999;     }     dd {       flex: 1;       color: #666;       >img {         width: 50px;         height: 50px;         margin-bottom: 4px;         @include sku-state-mixin;       }       >span {         display: inline-block;         height: 30px;         line-height: 28px;         padding: 0 20px;         margin-bottom: 4px;         @include sku-state-mixin;       }     }   } } </style> 
 
2. 选中和取消选中实现 
基本思路:
每一个规格按钮都拥有自己的选中状态数据-selected,true为选中,false为取消选中 
配合动态class,把选中状态selected作为判断条件,true让active类名显示,false让active类名不显示 
点击的是未选中,把同一个规格的其他取消选中,当前点击项选中;点击的是已选中,直接取消 
 
 
<script setup> // 省略代码 // 选中和取消选中实现 const changeSku = (item, val) => {   // 点击的是未选中,把同一个规格的其他取消选中,当前点击项选中,点击的是已选中,直接取消   if (val.selected) {     val.selected = false   } else {     item.values.forEach(valItem => valItem.selected = false)     val.selected = true   } } </script> <template>   <div class="goods-sku">     <dl v-for="item in goods.specs" :key="item.id">       <dt>{{ item.name }}</dt>       <dd>         <template v-for="val in item.values" :key="val.name">           <img v-if="val.picture"              @click="changeSku(item, val)"              :class="{ selected: val.selected }"              :src="val.picture"             :title="val.name">           <span v-else              @click="changeSku(val)"              :class="{ selected: val.selected }">{{ val.name }}</span>         </template>       </dd>     </dl>   </div> </template> 
 
3. 规格禁用功能实现 整体思路 分析 
生成路径字典 幂集算法
export  default  function  bwPowerSet  (originalSet) {  const  subSets = []            const  numberOfCombinations = 2  ** originalSet.length                for  (let  combinationIndex = 0 ; combinationIndex < numberOfCombinations; combinationIndex += 1 ) {     const  subSet = []     for  (let  setElementIndex = 0 ; setElementIndex < originalSet.length ; setElementIndex += 1 ) {              if  (combinationIndex & (1  << setElementIndex)) {         subSet.push (originalSet[setElementIndex])       }     }          subSets.push (subSet)   }   return  subSets } 
 
const  getPathMap  = (goods ) => {  const  pathMap = {}      const  effectiveSkus = goods.skus .filter (sku  =>  sku.inventory  > 0 )      effectiveSkus.forEach (sku  =>  {          const  selectedValArr = sku.specs .map (val  =>  val.valueName )          const  valueArrPowerSet = powerSet (selectedValArr)               valueArrPowerSet.forEach (arr  =>  {              const  key = arr.join ('-' )              if  (pathMap[key]) {         pathMap[key].push (sku.id )       } else  {         pathMap[key] = [sku.id ]       }     })   })   return  pathMap } let  pathMap = {}const  getGoods  = async  ( ) => {        const  res = await  axios.get ('http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1135076' )   goods.value  = res.data .result    pathMap = getPathMap (goods.value )      initDisabledState (goods.value .specs , pathMap) } 
 
根据路径字典设置初始化状态 
思路:判断规格的name属性是否能在有效路径字典中找到,如果找不到就禁用
 
const  initDisabledState  = (specs, pathMap ) => {     specs.forEach (item  =>  {     item.values .forEach (val  =>  {              val.disabled  = !pathMap[val.name ]     })   }) } let  patchMap = {}const  getGoods  = async  ( ) => {        const  res = await  axios.get ('http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1135076' )   goods.value  = res.data .result    pathMap = getPathMap (goods.value )      initDisabledState (goods.value .specs , pathMap) } <img :class ="{ selected: val.selected, disabled: val.disabled }" /> <span  :class ="{ selected: val.selected, disabled: val.disabled }" > {{val.name }}</span > 
 
根据路径字典设置组合状态 
思路:
根据当前选中规格,生成顺序规格数组 =>  [‘黑色’, undefined, undefined ] 
遍历每一个规格按钮 
 
如何规格按钮已经选中,忽略判断 如果规格按钮未选中,拿着按钮的name值按顺序套入匹配数组对应的位置,最后过滤掉没有值的选项,通过-进行拼接成字符串key, 去路径字典中查找,没有找到则把当前规格按钮禁用
 
const  getSelectedValues  = (specs ) => {  const  arr = []   specs.forEach (spec  =>  {     const  selectedVal = spec.values .find (value  =>  value.selected )     arr.push (selectedVal ? selectedVal.name  : undefined )   })   return  arr } const  updateDisabledState  = (specs, pathMap ) => {     specs.forEach ((item, i ) =>  {     const  selectedValues = getSelectedValues (specs)     item.values .forEach (val  =>  {       if  (val.selected ) return        const  _seletedValues = [...selectedValues]       _seletedValues[i] = val.name        const  key = _seletedValues.filter (value  =>  value).join ('*' )              val.disabled  = !pathMap[key]     })   })   } 
 
4. 产出Prop数据 const  changeSku  = (item, val ) => {        const  index = getSelectedValues (goods.value .specs ).findIndex (item  =>  item === undefined )   if  (index > -1 ) {     console .log ('找到了,信息不完整' )   } else  {     console .log ('没有找到,信息完整,可以产出' )          const  key = getSelectedValues (goods.value .specs ).join ('*' )     const  skuIds = pathMap[key]     console .log (skuIds)          const  skuObj = goods.value .skus .find (item  =>  item.id  === skuIds[0 ])     console .log ('sku对象为' , skuObj)   } }