Vue3知识点汇总
创建项目
npm init vue@latest
// npm create vite@latest
Vue文件结构
<!-- 开关:经过语法糖的封装,容许在script中书写组合式API -->
<!-- setup在beforeCreate钩子之前自动执行 -->
<script setup><!-- 不再要求唯一根元素 -->
<template>
setup样例
<script setup>
const message = 'this is message'
</script><template><div>{{message}}</div>
</template>
语法糖,会自动编译成3.0的代码结构
https://play.vuejs.org/
在线编译组合式api代码
响应式数据
reactive():接受对象类型数据的参数传入并返回一个响应式的对象
ref():接受简单类型或者对象类型数据的参数传入并返回一个响应式的对象
ref
函数的内部实现依赖于reactive
函数
<script setup>
import { ref, reactive } from 'vue';
const state = reactive({count: 0
})const setCount = () => {state.count++
}const count2 = ref(0)
const setCount2 = () => {count2.value++
}
</script><template><button @click="setCount">{{ state.count }}</button><button @click="setCount2">{{ count2 }}</button>
</template>
computed计算属性函数
<script setup>
import { ref, 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>
- 计算属性中不应该有副作用
- 避免直接修改计算属性的值
watch函数
监听一个或者多个数据的变化,数据变化时执行回调函数
有2个参数:
immediate
在监听器创建时,立即触发回调
deep
深度监听:通过watch监听的ref对象默认是浅层监听,直接修改嵌套的对象属性不会触发回调执行
可以同时监听多个响应式数据的变化,不管哪个数据变化都需要执行回调
<script setup>
import { ref, watch } from 'vue';
const count = ref(0)
const name = ref('Tom')
/监听ref对象,不需要加.value
watch(count, (newValue, oldValue) => {console.log('count变更 新值:', newValue, '但值:', oldValue)
}, {immediate: true
})// 监听多个对象
// watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// console.log('count或者name变化了', [newCount, newName], [oldCount, oldName])
// })const setCount = () => {count.value++
}
</script><template>
<button @click="setCount">{{ count }}</button>
</template>
deep监听
<script setup>
import { ref, watch } from 'vue';
const state = ref({count: 0
})
// 通过watch监听的ref对象默认是浅层监听,需要开启deep
watch(state, (newValue, oldValue) => {console.log('state变更 新值:', newValue, '但值:', oldValue)
}, {deep: true
})// 精确监听某个属性
const info = ref({name: 'Tom',age: 18
})// deep有性能损耗,尽量不开启deep
watch(() => info.value.age, () => console.log('age发生了变化')
)const setCount = () => {state.value.count++
}
const setAge = () => {info.value.age = 30
}
</script><template>
<button @click="setCount">{{ state.count }}</button>
<button @click="setAge">age {{ info.age }}</button>
</template>
生命周期函数
组合式API - setup
onBeforeMount
onMounted
onBeforeUpdate
onUpdated
onBeforeUnmount
onUmmounted
<script setup>
import { onMounted } from 'vue';
onMounted(() => {console.log('组件挂载完毕onMounted执行了')
})
</script>
父子通信–父传子
- 父组件中给子组件中绑定属性
- 子组件内部通过
defineProps
接收参数
<script setup>
import { ref } from 'vue';
import SonCom from './son-com.vue'
const count = ref(100)
setTimeout(() => {count.value = 200
}, 3000)
</script><template>
<div>显示</div>
<SonCom message="father message" :count="count"></SonCom>
</template><script setup>
import { defineProps } from 'vue';
const props = defineProps({message: String,count: Number
})
console.log('props', props)
</script><template><div>子组件 {{ message }} - {{ count }}</div>
</template>
父子通信–子传父
- 父组件中给子组件标签通过
@
绑定事件 - 子组件内部通过
defineEmits
方法触发事件
<script setup>
import SonCom from './son-com.vue'
const getMessage = (msg) => {console.log(msg)
}
</script><template>
<div>显示</div>
<SonCom @get-message="getMessage"></SonCom>
</template><script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(["get-message"])
const sendMsg = () => {emit('get-message', 'This is son message')
}
</script><template><button @click="sendMsg">发送消息</button>
</template>
模板引用
通过ref
标识获取真实的dom对象或者组件实例对象
默认情况下,子组件在setup
语法糖中组件内部的属性和方法是不开放给父组件访问的,使用defineExpose
指定可允许访问的属性和方法
<script setup>
import { onMounted, ref } from 'vue';
import testCom from './test-com.vue'
const h1Ref = ref(null)
const comRef = ref(null)// 组件挂载完毕之后才能获取
onMounted(() => {console.log(h1Ref.value)console.log(comRef)
})
</script><template>
<h1 ref="h1Ref">绑定ref测试</h1>
<testCom ref="comRef"></testCom>
</template>//===============================<script setup>
import { ref } from 'vue';
const name = ref('test name')
const setName = () => {name.value = 'test new name'
}// 显示暴露组件内部的属性和方法
defineExpose({name, setName
})
</script><template>
<div>子组件div</div>
</template>
跨层通信provide/inject
顶层组件向任意底层组件传递数据或方法,实现跨层组件通信;可以传递响应式数据、方法
- 顶层组件provide提供数据
provide('key', data)
- 底层组件inject获取数据
const message = inject('key')
<script setup>
import { provide, ref } from 'vue';
import SonCom from './son-com.vue'
provide('data-key', 'This is parent message')// 响应式数据
const count = ref(0)
provide('count-key', count)
setTimeout(() => {count.value = 300
}, 3000)// 传递方法
const setCount = () => {count.value++
}
provide('setCount-key', setCount)
</script><template>
<div>顶层组件</div>
<SonCom></SonCom>
</template>//=======================<script setup>
import { inject } from 'vue';
const parentData = inject('data-key')
const countData = inject('count-key')
const setCount = inject('setCount-key')
</script><template><div>底层组件 {{ parentData }} - {{ countData }}</div><button @click="setCount">更新数据</button>
</template>
Element Plus列表、修改页面
http://git.itcast.cn/heimaqianduan/vue3-basic-project.git
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import Edit from './components/Edit.vue'
import { ElMessageBox } from 'element-plus';// TODO: 列表渲染
const list = ref([])
const getList = async () => {const res = await axios.get('/list')list.value = res.data
}// TODO: 删除功能
const onDelete = async (id) => {try {await ElMessageBox.confirm('确认删除这条数据吗?','删除提醒',{confirmButtonText: '确认',cancelButtonText: '取消',type: 'warning',})await axios.delete(`/del/${id}`)getList()} catch(err) {}
}// TODO: 编辑功能
const editRef = ref(null)
const onEdit = (row) => {editRef.value.openDialog(row)
}getList()
</script><template><div class="app"><el-table :data="list"><el-table-column label="ID" prop="id"></el-table-column><el-table-column label="姓名" prop="name" width="150"></el-table-column><el-table-column label="籍贯" prop="place"></el-table-column><el-table-column label="操作" width="150"><template #default="{row}"><el-button type="primary" link @click="onEdit(row)">编辑</el-button><el-button type="danger" link @click="onDelete(row.id)">删除</el-button></template></el-table-column></el-table></div><Edit ref="editRef" @on-update="getList"/>
</template><style scoped>
.app {width: 980px;margin: 100px auto 0;
}
</style>
/components/Edit.vue
<script setup>
// TODO: 编辑
import { ref, defineEmits } from 'vue'
import axios from 'axios'
// 弹框开关
const dialogVisible = ref(false)
const form = ref({id: '',name: '',place: ''
})// 打开编辑页面
const openDialog = (row) => {form.value.id = row.idform.value.name = row.nameform.value.place = row.placedialogVisible.value = true
}const emit = defineEmits(['on-update'])const onUpdate = async () => {await axios.patch(`/edit/${form.value.id}`, {name: form.value.name,place: form.value.place})dialogVisible.value = false// 父页面重新查询数据emit('on-update')
}// 暴露方法给父页面调用
defineExpose({openDialog
})
</script><template><el-dialog v-model="dialogVisible" title="编辑" width="400px"><el-form label-width="50px" :model="form"><el-form-item label="姓名"><el-input placeholder="请输入姓名" v-model="form.name" /></el-form-item><el-form-item label="籍贯"><el-input placeholder="请输入籍贯" v-model="form.place" /></el-form-item></el-form><template #footer><span class="dialog-footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="onUpdate">确认</el-button></span></template></el-dialog>
</template><style scoped>
.el-input {width: 290px;
}
</style>
Pinia状态管理库
npm init vue@latest //创建一个空项目
vue-pinia
npm install pinia
修改main.js
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')
添加文件 stores/counter.js
import { computed, ref } from 'vue'
import axios from 'axios'
import { defineStore } from 'pinia'const API_URL = 'http://geek.itheima.net/v1_0/channels'export const useCounterStore = defineStore('counter', () => {const count = ref(0) const increment = () => {count.value++}// Pinia中的getters直接使用computed函数进行模拟const doubleCount = computed(() => count.value * 2)// 定义异步actionconst list = ref([])const getList = async () => {const res = await axios.get(API_URL)list.value = res.data.data.channels}return { count, doubleCount, increment, list, getList }})
使用Pinia
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';
const counterStore = useCounterStore()
// 解构会导致响应式丢失
// const {count, doubleCount} = counterStore// storeToRefs可以辅助保持数据的响应式解构
// 保持数据响应式
const {count, doubleCount} = storeToRefs(counterStore)// 方法需要直接从原来的counterStore中解构赋值
const {increment} = counterStoreonMounted(() => {counterStore.getList()
})
</script><template><button @click="increment">{{ count }}</button><div>{{ doubleCount }}</div><ul><li v-for="item in counterStore.list" :key="item.id">{{ item.name }}</li></ul>
</template>
全局属性
在Vue3中使用 <script setup>
语法时,不能直接使用 this
来访问组件的实例属性。但是,如果你想在 <script setup>
组件中使用 app.config.globalProperties
设置的全局属性,可以通过属性注入或直接从应用实例访问它们。
以下是一个使用 <script setup>
语法的示例,展示如何在Vue3应用中使用 app.config.globalProperties
:
首先,在Vue3应用的入口文件(如 main.js 或 main.ts)中设置全局属性:
import { createApp } from 'vue';const app = createApp({})// 定义全局属性
app.config.globalProperties.$myGlobalProperty = 'Hello World';// 定义全局方法
app.config.globalProperties.myGlobalMethod = function() {console.log('This is a global method');
};// 挂载应用
app.mount('#app');
然后,创建一个使用 <script setup>
语法的组件,并在其中访问全局属性:
<template><div><p>{{ myGlobalProperty }}</p><!-- 如果全局属性是一个方法,可以这样调用: --><button @click="myGlobalMethod">Call Global Method</button></div>
</template><script setup>
import { getCurrentInstance } from 'vue';// 使用 getCurrentInstance 获取组件实例
const instance = getCurrentInstance();
const myGlobalProperty = instance.appContext.config.globalProperties.$myGlobalProperty;
const myGlobalMethod = instance.appContext.config.globalProperties.myGlobalMethod;
</script>
配置src路径别名
创建测试项目
npm init vue@latest
vue3-rabbit
选中Router/Pinia/ESLint
jsconfig.json配置别名路径,使用@自动路径提示
{"compilerOptions": {"paths": {"@/*": ["./src/*"]}},"exclude": ["node_modules", "dist"]
}
在vite.config.js文件中,alias配置会把@路径转换成真实的路径
Element Plus安装
#按需引入
npm install element-plus --save
npm install -D unplugin-vue-components unplugin-auto-import
修改vite.config.js
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()],}),],
})
主题定制
npm i sass -D
对Element Plus样式进行覆盖
添加文件 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,),)
)
配置Element Plus采用sass样式配色系统
修改文件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'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [// 配置ElementPlus采用sass样式配色系统 ElementPlusResolver({ importStyle: 'sass'})],})],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},css: {preprocessorOptions: {scss: {// 自动导入定制化样式文件进行样式覆盖additionalData: `@use "@/styles/element/index.scss" as *;`,}}}
})
axios基础配置
安装
npm i axios
添加工具类 utils/http.js
import axios from "axios";// 创建axios实例
const http = axios.create({baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',timeout: 5000
})// axios请求拦截器
http.interceptors.request.use(config => {return config
}, e => Promise.reject(e))// axios响应式拦截器
http.interceptors.response.use(res => res.data, e => {return Promise.reject(e)
})export default http
添加测试方法 api/testAPI.js
import http from "@/utils/http";export function getCategory() {return http({url: 'home/category/head'})
}
使用方法
import { getCategory } from './apis/testAPI'
getCategory().then((res) => console.log(res))
路由设计原则
找内容切换的区域,如果是页面整体切换,则为一级路由;
如果是在一级路由页的内部切换,则为二级路由
import { createRouter, createWebHistory } from 'vue-router'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',name: 'layout',component: () => import('../views/Layout/index.vue'),children: [{path: '',name: 'home',component: () => import('../views/Home/index.vue')}, {path: 'category',name: 'category',component: () => import('../views/Category/index.vue')}]}, {path: '/login',name: 'login',component: () => import('../views/Login/index.vue')}]
})export default router
scss文件中变量的自动导入
在项目中一般把颜色值以scss变量的方式放到var.scss文件中,在使用时需要先导入var.scss文件
修改vite.config.js
css: {preprocessorOptions: {scss: {// 自动导入定制化样式文件进行样式覆盖additionalData: `@use "@/styles/element/index.scss" as *;@use "@/styles/var.scss" as *;`,}}}
测试
<template><div class="test">test</div>
</template><style scoped lang="scss">
.test {color: $priceColor;
}
</style>
VS code安装插件Error Lens,语法错误提示
吸顶导航
准备吸顶导航组件,获取滚动距离,以滚动距离做判断条件控制组件盒子展示隐藏
https://vueuse.org/
常用的组合式api集合
#安装Vue3组合式api集合
npm i @vueuse/core
<script setup>
import { useScroll } from '@vueuse/core'
// 滚动时实时获取滚动条的位置
const { y } = useScroll(window)
</script>// css:在满足条件时,样式类自动生效
:class="{show: y > 78}"
element-plus 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>
图片懒加载
使用自定义组件实现
使用useIntersectionObserver
函数,监听绑定的dom对象是否进入可视窗口区域
// main.js
import { useIntersectionObserver } from '@vueuse/core'// 定义全局指令
app.directive('img-lazy', {mounted(el, binding) {// el:指令绑定的那个元素// binding:binding.value 指令等号后面绑定的表达式const { stop } = useIntersectionObserver(el,([{ isIntersecting }]) => {if (isIntersecting) {// 进入可视窗口区域el.src = binding.value// 停止监听stop()}},)}
})// 页面使用
<img v-img-lazy="item.picture" alt="" />
代码优化,抽取成插件
// directives/index.js
import { useIntersectionObserver } from '@vueuse/core'// 定义懒加载插件
export const lazyPlugin = {install(app) {app.directive('img-lazy', {mounted(el, binding) {// el:指令绑定的那个元素// binding:binding.value 指令等号后面绑定的表达式const { stop } = useIntersectionObserver(el,([{ isIntersecting }]) => {if (isIntersecting) {// 进入可视窗口区域el.src = binding.value// 停止监听stop()}},)}})}
}// main.js
import { lazyPlugin } from '@/directives'
app.use(lazyPlugin)
动态路由
<li v-for="item in categoryStore.categoryList" :key="item.id"><RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
</li>
获取参数
import {useRoute} from 'vue-router'
const route = useRoute()
console.log(route.params.id)
// console.log(route.query.id)
// route.params 用于访问当前路由的路由参数(route parameters),即路由路径中的动态参数部分
// route.query 用于访问当前路由的查询参数,通常用于传递额外的信息(获取到的路由参数是字符串)
激活路由链接中的样式
active-class配置选中路由的样式
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>.active {color: $xtxColor;border-bottom: 1px solid $xtxColor;
}
路由缓存问题
使用带有参数的路由时(/category/${item.id}
),相同的组件实例将被重复使用,
导致组件的生命周期钩子函数不会被调用
{path: 'category/:id',name: 'category',component: () => import('../views/Category/index.vue')}
解决方式
- 让组件实例不复用,强制销毁重建
// 添加key,破坏复用机制,强制销毁重建
<RouterView :key="$route.fullPath"/>
- 监听路由变化,变化之后执行数据更新操作
// useCategory.js
import {useRoute, onBeforeRouteUpdate} from 'vue-router'onBeforeRouteUpdate((to) => {// console.log('路由变化了', to)getCategory(to.params.id)
})
以下是useCategory.js完整代码
import { ref, onMounted } from "vue"
import { onBeforeRouteUpdate, useRoute } from 'vue-router';
import { getCategoryAPI } from '@/apis/category';export function useCategory() {const categoryData = ref({})const getCategory = async (id) => {const res = await getCategoryAPI(id)categoryData.value = res.result}// 解决组件复用生命周期钩子不调用的问题onBeforeRouteUpdate((to) => {// console.log('to', to)getCategory(to.params.id)})// 首次调用会用到const route = useRoute()onMounted(() => {getCategory(route.params.id)})return {categoryData}
}
面包屑导航
<el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/${categoryData.parentId}` }">{{ categoryData.parentName }}</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb>
Infinite Scroll无限滚动
在要实现滚动加载的列表上添加指令v-infinite-scroll
,可实现滚动到底部时自动执行加载方法
<div class="body" v-infinite-scroll="load" :infinite-scroll-disabled="disabled"><!-- 商品列表--><GoodsItem :good="good" v-for="good in goodsList" :key="good.id"/></div>// 下拉加载
const disabled = ref(false)
const load = async () => {reqData.value.page++const res = await getSubCategoryAPI(reqData.value)goodsList.value = [...goodsList.value, ...res.result.items]if (res.result.items.length < reqData.value.pageSize) {disabled.value = true}
}
tab排序方式切换
v-model绑定的是tab-pane对应的name属性值
<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>// 切换排序方式
const tabChange = () => {console.log('change', reqData.value.sortField)reqData.value.page = 1disabled.value = falsegetGoodsList(reqData.value)
}
定制路由滚动行为
在切换路由时,自动滚动到页面的顶部
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',name: 'layout',component: () => import('../views/Layout/index.vue')}, {path: '/login',name: 'login',component: () => import('../views/Login/index.vue')}], scrollBehavior() {return {top: 0}}
})export default router
激活某个样式
格式 :class="{ className: condition == true }"
<li v-for="(img, i) in imageList" :key="i" @mouseenter="enterHandler(i)" :class="{ active: activeIndex === i }"><img :src="img" alt="" />
</li>
图片放大镜功能
<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"
// ]defineProps({imageList: {type: Array,default: () => []}
})const activeIndex = ref(0)
const enterHandler = (i) => {activeIndex.value = i
} // 鼠标位置
const target = ref(null)
const top = ref(0)
const left = ref(0)
const positionX = ref(0)
const positionY = ref(0)
const { elementX, elementY, isOutside } = useMouseInElement(target)
watch([elementX, elementY, isOutside], () => {if (isOutside.value) return// 有效范围内控制滑块距离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 * 2positionY.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: activeIndex === i}"><img :src="img" alt="" /></li></ul><!-- 放大镜大图 --><div class="large" :style="[{backgroundImage: `url(${imageList[activeIndex]})`,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-positionbackground-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>
使用第三方组件的关注点
关注props
和emit
props
接收参数,emit
返回数据
配置全局组件插件
编写插件时引入组件
// 使用插件形式,把组件全局注册(components/index.js)
import ImageView from './ImageView/index.vue'
import Sku from './XtxSku/index.vue'
export const componentPlugin = {install(app) {// app.component('组件名字', 组件配置对象)app.component('XtxImageView', ImageView)app.component('XtxSku', Sku)}
}
注册全局插件
// main.js
import { componentPlugin } from './components'const app = createApp(App)app.use(componentPlugin)
Form表单
表单校验
<script setup>
import {ref} from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
// 1.表单对象
const form = ref({account: '',password: '',agree: true
})
// 2.规则对象
const rules = {account: [{required: true, message: '用户名不能为空', trigger: 'blur'}],password: [{required: true, message: '密码不能为空', trigger: 'blur'},{min: 6, max: 14, message: '密码长度为6-14个字符', trigger: 'blur'}], // 自定义规则agree: [{validator: (rule, value, callback) => {if (!value) {callback(new Error('请勾选协议'))} else {callback()}}}]
}
const router = useRouter()
// 3.表单校验
const formRef = ref(null)
const doLogin = () => {formRef.value.validate((valid) => {// 所有表单项校验通过才为trueif (valid) {const {account, password} = form.valueawait loginAPI({account,password})ElMessage({ type: 'success', message: '登录成功' })router.replace({ path: '/' })}})
}
</script><template><el-form label-position="right" label-width="60px" :model="form" :rules="rules" ref="formRef"status-icon><el-form-item label="账户" prop="account"><el-input v-model="form.account"/></el-form-item><el-form-item label="密码" prop="password"><el-input v-model="form.password"/></el-form-item><el-form-item label-width="22px" prop="agree"><el-checkbox size="large" v-model="form.agree">我已同意隐私条款和服务条款</el-checkbox></el-form-item><el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button></el-form>
</template>
用户数据存入Pinia
// userStore.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 {userInfo,getUserInfo}
})//登录时直接调用getUserInfo方法
import {useUserStore} from '@/stores/user'
const userStore = useUserStore()const doLogin = async () => {formRef.value.validate(async (valid) => {// 所有表单项校验通过才为trueif (valid) {const {account, password} = form.valueawait userStore.getUserInfo({account, password})ElMessage({ type: 'success', message: '登录成功' })router.replace({ path: '/' })}})
}
Pinia用户数据持久化
// 安装插件
npm i pinia-plugin-persistedstate// 注册插件main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'const app = createApp(App)const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
在定义defineStore
方法时,传入第3个参数:{ persist: true }
// userStore.js完整代码
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { loginAPI } from '@/apis/user';
import { useCartStore } from './cartStore';export const useUserStore = defineStore('user', () => {const cartStore = useCartStore()const userInfo = ref({})// 登录const getUserInfo = async ({account, password}) => {const res = await loginAPI({account, password})userInfo.value = res.result// 合并购物车await cartStore.mergeCart()// 查询最新购物车cartStore.updateNewList()}const clearUserInfo = () => {userInfo.value = {}// 退出时删除购物车cartStore.clearCart()}return { userInfo, getUserInfo, clearUserInfo }
}, { persist: true
})
Form确认框
退出登录
<script setup>
import { useRouter } from 'vue-router';
import {useUserStore} from '@/stores/user'
const userStore = useUserStore()
const router = useRouter()
const confirm = () => {userStore.clearUserInfo()router.push('/login')
}
</script><template><el-popconfirm title="确认退出吗?" @confirm="confirm" confirm-button-text="确认" cancel-button-text="取消"><template #reference><a href="javascript:;">退出登录</a></template></el-popconfirm>
</template>
常用的页面点击跳转方式
# 1. RouterLink<li class="home" v-for="item in categoryStore.categoryList" :key="item.id"><RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink></li># 2. $router
<el-button size="large" type="primary" @click="$router.push('/cartlist')">去购物车结算</el-button> # 3. router
import { useRouter } from 'vue-router';
const router = useRouter()
router.push('/login')# push跳转带参数
router.push({path: '/pay', query: {id: orderId}
})# 4. 超链接
<a href="javascript:;" @click="$router.push('/login')">请先登录</a>
单选全选框选中
把单选框值与pinia中的数据绑定
v-model
双向绑定指令不合适命令式的操作,使用独立的2个指令:model-value
和@change
// 默认change事件只有一个selected参数,使用匿名箭头函数是为了扩展传参
<el-checkbox :model-value="item.selected" @change="(selected) => changeHandler(item.skuId, selected)"/>// 单选回调
const changeHandler = (skuId, selected) => {// selected的值为true/false,即当前选中的状态console.log(skuId, selected)cartStore.singleCheck(skuId, selected)
}
全选功能
<el-checkbox :model-value="cartStore.isAll" @change="allCheck"/>// 是否全选
const isAll = computed(() => cartList.value.every((item) => item.selected))const allCheck = (selected) => {cartList.value.forEach(item => item.selected = selected)
}
cartStore.js
完整代码
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { useUserStore } from './userStore'
import { insertCartAPI, findNewCartListAPI, delCartAPI, mergeCartAPI } from '@/apis/cart'export const useCartStore = defineStore('cart', () => {const userStore = useUserStore()const isLogin = computed(() => userStore.userInfo.token)const cartList = ref([])const addCart = async (goods) => {const {skuId, count} = goodsif (isLogin.value) {await insertCartAPI({skuId, count})await updateNewList()} else {// 未登录,本地购物车const item = cartList.value.find(item => item.skuId === goods.skuId)if (item) {item.count = item.count + goods.count} else {cartList.value.push(goods)}}}const delCart = async (skuId) => {if (isLogin.value) {await delCartAPI([skuId])await updateNewList()} else {const index = cartList.value.findIndex(item => item.skuId === skuId)cartList.value.splice(index, 1)// const newList = cartList.value.filter(item => item.skuId !== skuId)// cartList.value = newList}}// 合并本地购物车,在登录时使用const mergeCart = async () => {if (cartList.value.length > 0) {await mergeCartAPI(cartList.value.map(item => {return {skuId: item.skuId,count: item.count,selected: item.selected}}))}}// 清除购物车const clearCart = () => {cartList.value = []}// 获取最新购物车列表const updateNewList = async () => {const res = await findNewCartListAPI()cartList.value = res.result}// 单选功能const singleCheck = (skuId, selected) => {const item = cartList.value.find(item => item.skuId === skuId)item.selected = selected}// 全选const allCheck = (selected) => {cartList.value.forEach(item => item.selected = selected)}// 是否全选 const isAll = computed(() => cartList.value.every(item => item.selected))// 总数量const allCount = computed(() => cartList.value.reduce((a, c) => a + c.count, 0))// 总价const allPrice = computed(() => cartList.value.reduce((a, c) => a + c.count * c.price, 0))// 已选择数量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))return { cartList, allCount, allPrice, selectedCount, selectedPrice, isAll,addCart, delCart, clearCart, mergeCart, singleCheck, allCheck, updateNewList }
}, { persist: true
})
弹窗组件
<el-dialog v-model="showDialog" title="切换收货地址" width="30%" center><div class="addressWrapper"><div class="text item" :class="{active : activeAddress.id === item.id }" @click="switchAddress(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 @click="showDialog = false">取消</el-button><el-button type="primary" @click="confirmAddress">确定</el-button></span></template></el-dialog>
倒计时组件
// useCountDown.js
// 倒计时
import { ref, computed, onUnmounted } from "vue";
import dayjs from "dayjs";
export const useCountDown = () => {let timer = nullconst time = ref(0)const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))// 传入倒计时的秒数const start = (currentTime) => {time.value = currentTimetimer = setInterval(() => {if (time.value > 0) {time.value --} else {timer && clearTimeout(timer)}}, 1000)}// 组件销毁时清除定时器onUnmounted(() => {timer && clearInterval(timer)})return {formatTime,start}
}
自定义列表空数据显示样式
<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>
</div>
tab页切换
<script setup>
import {ref, onMounted} from 'vue'
import {getUserOrder} from '@/apis/order'// 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 = ref([])
const params = ref({orderState: 0,page: 1,pageSize: 2
})
const getOrderList = async () => {const res = await getUserOrder(params.value)orderList.value = res.result.items
}const tabChange = (index) => {params.value.orderState = indexgetOrderList()
}onMounted(() => getOrderList())
</script><template><div class="order-container"><el-tabs @tab-change="tabChange"><!-- tab切换 --><el-tab-pane v-for="item in tabTypes" :key="item.name" :label="item.label" /></div><div>内容</div> </el-tabs></div>
</template>
列表分页
// 订单列表
const orderList = ref([])
const params = ref({orderState: 0,page: 1,pageSize: 2
})
const total = ref(0)
const getOrderList = async (params) => {const res = await getUserOrder(params) orderList.value = res.result.itemstotal.value = res.result.counts
} onMounted(() => getOrderList(params.value))
const tabChange = (type) => {params.value.page = 1params.value.orderState = typegetOrderList(params.value)
}const pageChange = (page) => {params.value.page = pagegetOrderList(params.value)
}<!-- 分页 -->
<div class="pagination-container"><el-pagination :total="total" :page-size="params.pageSize" @current-change="pageChange" background layout="prev, pager, next" />
</div><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;}}
}</style>
参考文档
https://www.bilibili.com/video/BV1Ac411K7EQ/?spm_id_from=333.999.0.0&vd_source=0311265fb5615f1fc596bb4c1dcdcd20
黑马程序员前端Vue3小兔鲜电商项目实战,vue3全家桶从入门到实战电商项目一套通关https://zsjie.blog.csdn.net/article/details/131323373
黑马程序员前端 Vue3 小兔鲜电商项目 https://www.yuque.com/fechaichai/trash-1cydvph9
Vue3小兔鲜新版文档https://apifox.com/apidoc/shared-fa9274ac-362e-4905-806b-6135df6aa90e
黑马前端api文档 https://play.vuejs.org/
在线编译组合式api代码https://vueuse.org/
vue官方提供的vue3组合式函数测试登录账号
xiaotuxian001
123456支付宝沙箱账号
scobys4865@sandbox.com
登录密码111111
支付密码111111 支付回调地址
http://127.0.0.1:5173/paycallback?payResult=true&orderId=1809449837100273665
相关文章:
Vue3知识点汇总
创建项目 npm init vuelatest // npm create vitelatestVue文件结构 <!-- 开关:经过语法糖的封装,容许在script中书写组合式API --> <!-- setup在beforeCreate钩子之前自动执行 --> <script setup><!-- 不再要求唯一根元素 -->…...
C++设计模式--单例模式
单例模式的学习笔记 单例模式是为了:在整个系统生命周期内,保证一个类只能产生一个实例,确保该类的唯一性 参见链接1,链接2 #include <iostream> #include <mutex>using namespace std;/*懒汉模式:只有在…...

数据驱动未来:构建下一代湖仓一体电商数据分析平台,引领实时商业智能革命
1.1 项目背景 本项目是一个创新的湖仓一体实时电商数据分析平台,旨在为电商平台提供深度的数据洞察和业务分析。技术层面,项目涵盖了从基础架构搭建到大数据技术组件的集成,采用了湖仓一体的设计理念,实现了数据仓库与数据湖的有…...
学习JavaScript第五天
文章目录 1.HTML DOM1.1 表单相关元素① form 元素② 文本输入框类和文本域(input 和 textarea)③ select 元素 1.2 表格相关元素① table 元素② tableRow 元素(tr 元素)③ tableCell 元素 (td 或 th) 1.3…...

pythonGame-实现简单的坦克大战
通过python简单复现坦克大战游戏。 使用到的库函数: import turtle import math import random import time 游戏源码: import turtle import math import random import time# 设置屏幕 screen turtle.Screen() screen.setup(800, 600) screen.tit…...
不太常见的asmnet诊断
asm侦听 [griddb1-[ASM1]-/home/grid]$ srvctl config asm ASM home: <CRS home> Password file: OCR/orapwASM Backup of Password file: OCRDG/orapwASM_backup ASM listener: LISTENER ASM instance count: 3 Cluster ASM listener: ASMNET1LSNR_ASM[rootdb1:/root]# …...

双指针-【3,4,5,6,7,8】
第三题:快乐数 . - 力扣(LeetCode). - 备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/happy-number/算法思想: 1.每个…...
react Vant中如何获取步进器的值
在React中使用Vant(一个轻量、可靠的移动端Vue组件库,虽然原生是为Vue设计的,但如果你在使用的是React版本的Vant,比如通过某些库或框架桥接Vue组件到React,或者是一个类似命名的React UI库),获…...
Windows下Git Bash乱码问题解决
Windows下Git Bash乱码问题解决 缘起 个人用的电脑是Mac OS,系统和终端编码都是UTF-8,但公司给配发的电脑是Windows,装上Git Bash在使用 git commit -m "中文"时会乱码 解决 确认有以下配置 # 输入 git config --global --lis…...

HTML5 + CSS3
HTML 基础 准备开发环境 1.vscode 使用 新建文件夹 ---> 左键拖入 vscode 中 2.安装插件 扩展 → 搜索插件 → 安装打开网页插件:open in browser汉化菜单插件:Chinese 3.缩放代码字号 放大,缩小:Ctrl 加号,减号 4.设…...

NFTScan | 07.22~07.28 NFT 市场热点汇总
欢迎来到由 NFT 基础设施 NFTScan 出品的 NFT 生态热点事件每周汇总。 周期:2024.07.22~ 2024.07.28 NFT Hot News 01/ 数据:NFT 系列 Liberty Cats 地板价突破 70000 MATIC 7 月 22 日,据 Magic Eden 数据,NFT 系列 Liberty C…...

探索分布式光伏运维系统的组成 需要几步呢?
前言 随着光伏发电的不断发展,对于光伏发电监控系统的需求也日益迫切,“互联网”时代,“互联网”的理念已经转化为科技生产的动力,促进了产业的升级发展,本文结合“互联网”技术提出了一种针对分散光伏发电站运行数据…...

做知识付费项目还能做吗?知识付费副业项目如何做?能挣多少钱?
hello,我是阿磊,一个20年的码农,6年前代码写不动了,转型专职做副业项目研究,为劳苦大众深度挖掘互联网副业项目,共同富裕。 现在做知识付费项目还能做吗? 互联网虚拟资源项目我一直在做,做了有…...

K210视觉识别模块学习笔记7:多线程多模型编程识别
今日开始学习K210视觉识别模块: 图形化操作函数 亚博智能 K210视觉识别模块...... 固件库: canmv_yahboom_v2.1.1.bin 训练网站: 嘉楠开发者社区 今日学习使用多线程、多模型来识别各种物体 这里先提前说一下本文这次测试实验的结果吧:结果是不太成…...

Go语言教程(一看就会)
全篇文章 7000 字左右, 建议阅读时长 1h 以上。 Go语言是一门开源的编程语言,目的在于降低构建简单、可靠、高效软件的门槛。Go平衡了底层系统语言的能力,以及在现代语言中所见到的高级特性。它是快速的、静态类型编译语言。 第一个GO程序…...

【Golang 面试 - 基础题】每日 5 题(十)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:http://t.csdnimg.cn/UWz06 📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞👍收藏…...
OD C卷 - 密码输入检测
密码输入检测 (100) 给定一个密码,‘<’ 表示删除前一个字符,输出最终得到的密码,并判断是否满足密码安全要求: 密码长度>8;至少包含一个大写字母;至少包含一个小写字母;至少…...

【每日一题】【逆推法 + 贪心】【数学】造数 河南萌新联赛2024第(一)场:河南农业大学 A题 C++
河南萌新联赛2024第(一)场:河南农业大学 A题 造数 题目描述 样例 #1 样例输入 #1 2样例输出 #1 1样例 #2 样例输入 #2 5样例输出 #2 3做题思路 本题可以用逆推法 将三种操作反过来变为 − 1 , − 2 , / 2 -1 , -2 , /2 −1,−2,/2 …...

刷题计划 day4 【双指针、快慢指针、环形链表】链表下
⚡刷题计划day4继续,可以点个免费的赞哦~ 下一期将会开启哈希表刷题专题,往期可看专栏,关注不迷路, 您的支持是我的最大动力🌹~ 目录 ⚡刷题计划day4继续,可以点个免费的赞哦~ 下一期将会开启哈希表刷题…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...

初探Service服务发现机制
1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能:服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...