vue3电商项目 1.vue3初学 创建vue3工程
关键文件内容介绍 1 2 3 createApp (App ).mount ('#app' )
1 2 <!--相当于开关:容许script书写组合式API--> <script setUp>
模板template不再要求唯一根元素
组合式API介绍 setup在beforeCreate之前执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> export default { setup() { console.log('setup') }, beforeCreate() { console.log('beforeCreate') } } </script> <template> <div> this is a div </div> </template>
setup函数定义的数据必须return后才可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <script> export default { setup() { //console.log('setup') const message='this is message' const logMessag=()=>{ console.log(message) } return { message, logMessag } }, beforeCreate() { //console.log('beforeCreate') } } </script> <template> <div> {{ message }} <button @click="logMessag">log</button> </div> </template>
语法糖
1 2 3 4 5 6 7 8 9 10 11 12 13 <script setup> const message = 'this is message' const logMessag = () => { console.log(message) } </script> <template> <div> {{ message }} <button @click="logMessag">log</button> </div> </template>
组合式API-reactive和ref函数 reactive函数 (得到一个响应式的对象): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script setup> //导入函数 import { reactive } from 'vue'; //执行函数 传入一个对象类型的参数 const state =reactive({ count:0 }) const setCount =() =>{ state.count++ } </script> <template> <div> <button @click="setCount">{{ state.count }}</button> </div> </template>
ref() 函数 (实际工作更推荐) 接受简单类型或者对象类型的数据传入并返回一个响应式的对象(ref内部依赖于reactive函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script setup> //导入函数 import { ref } from 'vue'; //执行函数 const count =ref(0) const setCount =()=>{ // 脚本区域修改ref产生的响应式对象数据必须通过 .value属性 count.value++ } </script> <template> <div> <button @click="setCount">{{ count }}</button> </div> </template>
computed计算属性函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <script setup> import { ref } from 'vue'; import { computed } from 'vue'; const list=ref([1,2,3,4,5,6,7,8]) //执行函数 const computedList =computed(()=>{ return list.value.filter(item => item>2) }) //定时器 setTimeout (()=>{ list.value.push(9,10); },3000) </script> <template> <div> 原始响应式数组 -{{ list }} </div> <div> 计算属性数组 -{{ computedList }} </div> </template>
注意:
计算属性中不应该有副作用(异步请求,修改dom)
避免直接修改计算属性的值
watch函数 监听变化值,如果该变化值改变,则执行对应的监听回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <script setup> import { ref, watch } from 'vue'; const count=ref(0) const name=ref('cp') const setCount =() =>{ count.value++ } const changeName=()=>{ name.value='pc' } //watch监听单个数据源 watch(count,(newVal,oldVal)=>{ console.log(newVal,oldVal) }) //watch监听多个数据源 watch([count,name],([newCount,oldCount],[newName,oldName])=>{ console.log([newCount,oldCount],[newName,oldName]) }) </script> <template> <div> <button @click="setCount">+{{ count }}</button> </div> <div> <button @click="changeName">{{ name }}</button> </div> </template>
在监听器创建时立即触发回调,响应式数据变化之后继续执行回调
语法格式:
1 2 3 4 5 watch (count,(newVal,oldVal )=> { console .log (newVal,oldVal) },{ immediate :true })
deep(性能消耗大,尽量不要开启) 默认修改嵌套属性的对象属性不会出发回调执行,需要开启deep选项
(如果不加deep,回调函数不会执行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <script setup> import { ref, watch } from 'vue'; const state=ref({count:0}) const setCount =() =>{ state.value.count++ } watch(state,()=>{ console.log("state change") },{ deep:true }) </script> <template> <div> <button @click="setCount">+{{ state.count }}</button> </div> </template>
精确监听 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <script setup> import { ref, watch } from 'vue'; const state =ref({ name:'guojia', age:28 }) const changeName=()=>{ state.value.name='guojiajia' } const changeAge=()=>{ state.value.age=20 } watch( ()=>state.value.age, ()=>{ console.log('age change') } ) </script> <template> <div> {{ state.name }} </div> <div> {{ state.age }} </div> <div> <button @click="changeName">修改名字</button> <button @click="changeAge">修改年龄</button> </div> </template>
生命周期函数
可以执行多次,且会依次执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script setup> //引入 import { onMounted } from 'vue'; //执行函数,执行回调 onMounted (()=>{ console.log('mounted挂载完毕') }) </script> <template> <div> </div> </template>
父子通信 父传子 父代码(静态数据和响应数据都可以传递)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script setup> import { ref } from 'vue'; import SonCom from './son.vue' const count=ref(100) setTimeout(()=>{ count.value++ },3000) </script> <template> <div class="father"> <h2>father</h2> <SonCom :count="count" message="father message"/> </div> </template>
子代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script setup> // 接收数据 defineProps({ message:String, count: Number }) </script> <template> <div class="son"> <h3>son</h3> <Div> {{ message }} {{ count }} </Div> </div> </template> <script scoped> </script>
子传父 子代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script setup> // 宏函数 const emit=defineEmits(['getMessage']) const sendMsg=()=>{ emit('getMessage',"this is a son msg") } </script> <template> <button @click="sendMsg">sendMsg</button> </template> <script scoped> </script>
父代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script setup> import { ref } from 'vue'; import SonCom from './son.vue' const getMessage =(msg)=>{ console.log(msg) } </script> <template> <div class="father"> <h2>father</h2> <SonCom @get-message="getMessage"/> </div> </template>
模板引用 组件内部的属性和方法是不公开给父组件的,可通过defineExpose编译宏指定那些属性和方法允许访问
父
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script setup> import { onMounted, ref } from 'vue'; import TestCom from './TestCom.vue'; //调用ref函数 ->ref对象 const h1Ref=ref(null) const comRef=ref(null) //组件挂在完毕之后才能获取 onMounted(()=>{ console.log(h1Ref.value,comRef.value) }) </script> <template> <h1 ref="h1Ref">我是dom标签h1</h1> <TestCom ref="comRef"></TestCom> </template>
子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script setup> import { ref } from 'vue'; const name=ref('test name') const setName=()=>{ name.value='test new value' } defineExpose({ name }) </script> <template> <div> 我是test组件 </div> </template>
provide和inject 用于跨层组件通信(谁的数据谁负责修改)
通用步骤:
顶层组件通过provi函数提供数据
底层组件通过inject函数获取数据
顶层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <script setup> import middle from './middle.vue'; import bottom from './bottom.vue'; import { provide, ref } from 'vue'; //顶层组件提供数据 provide('data-key','this is top data') //传递响应式数据 const count=ref(0) provide('count-key',count) //传递方法 const setCount=()=>{ count.value++ } provide('setCount-key',setCount) </script> <template> <div> 顶层组件 <middle/> <bottom/> </div> </template>
中间代码
1 2 3 4 5 6 7 8 <script setup> </script> <template> <div> 中间组件 </div> </template>
底层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <script setup> import { inject } from 'vue'; const data=inject("data-key") //接受响应式数据 const count =inject("count-key") //接受方法 const setCount=inject("setCount-key") </script> <template> <div> 底层组件 <div> 来自顶层:{{ data }} </div> <div> 来自顶层响应式数据:{{ count }} </div> <div> <button @click="setCount">count++</button> </div> </div> </template>
2.Pinia(状态管理库) 安装
使用(在入口函数中引入)
1 2 3 4 5 6 7 8 9 10 11 12 import './assets/main.css' import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const pinia = createPinia ()const app=createApp (App )app.use (pinia) app.mount ('#app' )
计数器案例使用 详细教程定义一个 Store | Pinia 中文文档 (web3doc.top)
创建一个store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { defineStore } from 'pinia' import { ref } from 'vue' export const useCounterStore=defineStore ('counter' ,()=> { const count =ref (0 ) const increment =( )=>{ count.value ++ } return { count, increment } })
组件使用store
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script setup> //导入 import { useCounterStore } from './stores/counter'; //执行方法得到store实例对象 const counterStore= useCounterStore() console.log(counterStore) </script> <template> <button @click="counterStore.increment"> {{ counterStore.count }}</button> </template>
getter(在store中计算属性) 在刚刚定义store加入
1 2 const doubleCount=computed (()=> count.value *2 )
在组件间使用即可获取(随着count的变化而变化)
action(异步) 发送异步请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { defineStore } from 'pinia' import { computed, ref } from 'vue' import axios, { Axios } from 'axios' const API_URL ='http://geek.itheima.net/v1_0/channels' export const useCounterStore=defineStore ('counter' ,()=> { const count =ref (0 ) const increment =( )=>{ count.value ++ } const doubleCount=computed (()=> count.value *2 ) const list =ref ([]) const getList =async ( )=>{ const res= await axios.get (API_URL ) list.value =res.data .data .channels } return { count, doubleCount, increment, list, getList } })
组件渲染数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <script setup> //导入 import { onMounted } from 'vue'; import { useCounterStore } from './stores/counter'; //执行方法得到store实例对象 const counterStore= useCounterStore() console.log(counterStore) onMounted (()=>{ counterStore.getList() }) </script> <template> <button @click="counterStore.increment"> {{ counterStore.count }}</button> <div> {{ counterStore.doubleCount }} </div> <ul> <li v-for="item in counterStore.list" :key="item.id">{{ item.name }}</li> </ul> </template>
storeToRefs 直接基于store进行解构赋值,响应式数据(state和getter)会丢失响应式特性,使用storeToRefs辅助保持响应式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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>
3.初始化项目
![]po1.png)
创建项目目录
apis api接口文件夹
composables 组合函数文件夹
directives 全局指令文件夹
styles 全局样式文件夹
utils 工具函数文件夹
jsconfig.json配置别名路径
配置别名路径可以在写代码时联想提示路径
1 2 3 4 5 6 7 8 { "compilerOptions" : { "baseUrl" : "./" , "paths" : { "@/*" : [ "src/*" ] } } }
element-plus 按需引入 过程参考:快速开始 | Element Plus (element-plus.org)
主题定制 安装scss
准备定制样式文件(styles\element\index.css)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @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, ), ) )
自动导入配置
这里自动导入需要深入到elementPlus的组件中,按照官方的配置文档来
自动导入定制化样式文件进行样式覆盖
按需定制主题配置 (需要安装 unplugin-element-plus)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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' import ElementPlus from 'unplugin-element-plus/vite' export default defineConfig ({ plugins : [ vue (), AutoImport ({ resolvers : [ElementPlusResolver ()], }), Components ({ resolvers :[ ElementPlusResolver ({importStyle :"sass" }) ] }), ElementPlus ({ useSource : true , }), ], resolve : { alias : { '@' : fileURLToPath (new URL ('./src' , import .meta .url )) } }, css : { preprocessorOptions : { scss : { additionalData : ` @use "@/styles/element/index.scss" as *; ` , } } } })
axios配置 安装
配置基础实例(统一接口配置) 在utils目录创建http.js基础的封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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;
在apis文件夹下测试 1 2 3 4 5 6 7 import httpInstance from "@/utils/http" function getCategory ( ){ httpInstance ({ url :'home/category/head' }) }
在入口文件测试
1 2 3 4 5 6 7 import httpInstance from "@/utils/http" export function getCategory ( ){ return httpInstance ({ url :'home/category/head' }) }
路由整体设计 路由设计原则:找页面的切换方式,如果是整体切换,则为一级路由,如果是在一级路由的内部进行的内容切换,则为二级路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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
注意写路由出口(默认二级路由只需要把二级路由path路径置为空即可)
组件文件如下:
静态资源引入和Error Lens安装 1. 静态资源引入
图片资源 - 把 images 文件夹放到 assets 目录下
样式资源 - 把 common.scss 文件放到 styles 目录下
2. Error Lens插件安装 vscode搜索安装即可
3.scss变量自动导入 1 2 3 4 5 $xtxColor: #27ba9b ; $helpColor: #e26237 ; $sucColor: #1dc779 ; $warnColor: #ffb302 ; $priceColor: #cf4444 ;
1 2 3 4 5 6 7 8 9 10 11 css: { preprocessorOptions: { scss: { additionalData: ` @use "@/styles/element/index.scss" as *; @use "@/styles/var.scss" as *; `, } } }
静态模板结构搭建 layout静态页面 index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <script setup> import layoutNav from './components/layoutNav.vue'; import layoutHeader from './components/layoutHeader.vue'; import layoutFooter from './components/layoutFooter.vue'; </script> <template> <layoutNav></layoutNav> <layoutHeader></layoutHeader> <RouterView></RouterView> <layoutFooter></layoutFooter> </template>
layoutFooter.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 <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:#27ba9b;; } } &: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: #27ba9b;; } } .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>
layoutHeader.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 <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: #27ba9b;; border-bottom: 1px solid #27ba9b;; } } .active { color: #27ba9b;; border-bottom: 1px solid #27ba9b;; } } } .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: #e26237; color: #fff; font-size: 12px; border-radius: 10px; font-family: Arial; } } } } </style>
layoutNav.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <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: #27ba9b;; } } ~li { a { border-left: 2px solid #666; } } } } } </style>
字体图标渲染
字体图标采用的是阿里的字体图标库,样式文件已经准备好,在 index.html
文件中引入即可
1 <link rel ="stylesheet" href ="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css" >
一级导航渲染 实现步骤
封装接口函数
调用接口函数
v-for渲染模版
封装api
1 2 3 4 5 6 7 import http from "@/tools/axios" ;export function getCategory ( ){ return http ({ url :'/home/category/head' }) }
调用接口函数
1 2 3 4 5 6 7 8 9 10 11 12 import { getCategory } from '@/apis/layout' ;import { onMounted, ref } from 'vue' ;const categoryList=ref ([])const getCategoryAPI =async ( )=>{ const res=await getCategory (); categoryList.value =res.result ; } onMounted (()=> { getCategoryAPI (); })
v-for渲染
1 2 3 4 5 <ul class ="app-header-nav" > <li class ="home" v-for ="item in categoryList" :key ="item.id" > <RouterLink to ="/" > {{ item.name }}</RouterLink > </li > </ul >
吸顶导航交互实现 1. 准备组件静态结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 <script setup> </script> <template> <div class="app-header-sticky"> <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. 渲染基础数据 3. 实现吸顶交互 安装vueUser
核心逻辑:根据滚动距离判断当前show类名是否显示,大于78显示,小于78,不显示
1 2 3 4 5 6 7 8 9 10 11 12 <script setup> import LayoutHeaderUl from './LayoutHeaderUl.vue' // vueUse import { useScroll } from '@vueuse/core' const { y } = useScroll(window) </script> <template> <div class="app-header-sticky" :class="{ show: y > 78 }"> <!-- 省略部分代码 --> </div> </template>
Pinia优化重复请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { defineStore } from "pinia" ;import { getCategory } from '@/apis/layout' ;import { ref } from 'vue' ;export const useCategoryStore = defineStore ('category' , () => { const categoryList = ref ([]) const getCategoryAPI = async ( ) => { const res = await getCategory (); categoryList.value = res.result ; } return { categoryList, getCategoryAPI } })
在父组件导入
1 2 3 4 5 6 7 8 9 10 import { useCategoryStore } from '@/store/category' ;import { onMounted } from 'vue' ;const categoryStore=useCategoryStore ();onMounted (()=> { categoryStore.getCategoryAPI (); })
4.项目开始 home页结构搭建和分类实现 1. 整体结构创建
1- 按照结构新增五个组件,准备最简单的模版,分别在Home模块的入口组件中引入
HomeCategory
HomeBanner
HomeNew
HomeHot
HomeProduct
1 2 3 4 5 6 <script setup> </script> <template> <div> HomeCategory </div> </template>
2- Home模块入口组件中引入并渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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- 准备详细模版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 <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- 完成代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <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> <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. 熟悉组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <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- 封装接口
1 2 3 4 5 6 7 8 9 10 11 import httpInstance from '@/utils/http' function getBannerAPI (){ return request ({ url :'home/banner' }) }
2- 获取数据渲染模版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <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>
面板组件封装 1. 纯静态结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <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. 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <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. 准备模版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <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. 封装接口 1 2 3 4 5 6 7 8 9 10 export const findNewAPI = ( ) => { return httpInstance ({ url :'/home/new' }) }
3. 获取数据渲染模版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <script setup> import HomePanel from './HomePanel.vue' import { getNewAPI } from '@/apis/home' import { ref } from 'vue' const newList = ref([]) const getNewList = async () => { const res = await getNewAPI() 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>
人气推荐实现 1. 封装接口 1 2 3 4 5 6 7 8 export const getHotAPI = ( ) => { return httpInstance ('home/hot' , 'get' , {}) }
2. 获取数据渲染模版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <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="人气爆款 不容错过"> <ul class="goods-list"> <li v-for="item in hotList" :key="item.id"> <RouterLink to="/"> <img v-img-lazy="item.picture" alt=""> <p class="name">{{ item.title }}</p> <p class="desc">{{ item.alt }}</p> </RouterLink> </li> </ul> </HomePanel> </template> <style scoped lang='scss'> .goods-list { display: flex; justify-content: space-between; height: 426px; li { width: 306px; height: 406px; 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; } .desc { color: #999; font-size: 18px; } } } </style>
懒加载指令实现 1. 封装全局指令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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. 注册全局指令 1 2 3 import { directivePlugin } from '@/directives' app.use (directivePlugin)
Product产品列表实现 1. 基础数据渲染 1- 准备静态模版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 <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- 封装接口 1 2 3 4 5 6 7 8 9 10 export const getGoodsAPI = ( ) => { return httpInstance ({ url : '/home/goods' }) }
3- 获取并渲染数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <script setup> import HomePanel from './HomePanel.vue' import { getGoodsAPI } from '@/apis/home' import { ref } 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"> <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="good.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> </HomePanel> </div> </template>
4. 图片懒加载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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. 封装组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <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. 使用组件 1 2 3 4 5 <ul class="goods-list"> <li v-for="goods in cate.goods" :key="item.id"> <GoodsItem :goods="goods" /> </li> </ul>
静态结构搭建和路由配置 1. 准备分类组件 1 2 3 4 5 6 7 8 9 <script setup> </script> <template> <div class='top-category'> 我是分类 </div> </template>
2. 配置路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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' 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 : '/login' , name : 'login' , component : Login }, ] }) export default router
3. 配置导航区域链接 1 2 3 4 5 <li v-for ="item in categoryStore.categoryList" :key ="item.id" > <RouterLink active-class ="active" :to ="`/category/${item.id}`" > {{ item.name }} </RouterLink > </li >
面包屑导航渲染 1. 认识组件准备模版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 <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. 封装接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import request from '@/utils/request' export const getTopCategoryAPI = (id ) => { return request ({ url :'/category' , params :{ id } }) }
3. 渲染面包屑导航 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script setup> import { findTopCategoryAPI } from '@/apis/category' const categoryData = ref({}) const route = useRoute() const getCategory = async (id) => { // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route const res = await findTopCategoryAPI(id) categoryData.value = res.result } getCategory(route.params.id) </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. 适配接口 1 2 3 4 5 6 7 8 9 10 export function getBannerAPI (params = {}) { const { distributionSite = '1' } = params return httpInstance ({ url : '/home/banner' , params : { distributionSite } }) }
2. 迁移首页Banner逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <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. 导航激活状态设置 1 <RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
2. 分类数据模版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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 >
路由缓存问题解决
缓存问题:当路由path一样,参数不同的时候会选择直接复用路由对应的组件 解决方案:
给 routerv-view 添加key属性,破坏缓存
使用 onBeforeRouteUpdate钩子函数,做精确更新
基于业务逻辑的函数拆分
基本思想:把组件内独立的业务逻辑通过 useXXX
函数做封装处理,在组件中做组合使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { onMounted, ref } from 'vue' import { getCategoryAPI } 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 getCategoryAPI (id) categoryData.value = res.result } onMounted (() => getCategory ()) onBeforeRouteUpdate ((to ) => { getCategory (to.params .id ) }) return { categoryData } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 } }
1 2 3 4 5 6 7 8 9 <script setup> import GoodsItem from '../Home/components/GoodsItem.vue' import { useBanner } from './composables/useBanner' import { useCategory } from './composables/useCategory' const { bannerList } = useBanner() const { categoryData } = useCategory() </script>