35.Vue3实战之首页配置
前言
上一小节,我们开始了内容页部分的开发,分别使用了 ElCard
、ElPopover
等常用该组件。这两个组件在很多业务场景都非常的实用,同学们在开发后台管理系统的时候,尽量合理的利用好组件库给我提供的组件,减少自己编写组件,提高工作效率。
本小节将为同学们带来后台管理系统的重头戏,Table
、Upload
、Dialog
组件的使用,以及遇到多个类似页面的时候,如何通用一个组件。
本章节知识点
-
需要注册的组件:
ElTable
、ElUpload
、ElDialog
、ElPagination
。 -
弹窗组件的封装。
-
多页面公用同一个组件。
首页轮播图配置
首先打开 App.vue
添加轮播图的 Menu
,顺便把图表都更换为符合语义的图表:
template
...
<el-menu
background-color="#222832"
text-color="#fff"
:router="true"
:default-openeds="defaultOpen"
:default-active='currentPath'
>
<el-sub-menu index="1">
<template #title>
<span>Dashboard</span>
</template>
<el-menu-item-group>
<el-menu-item index="/"><el-icon><Odometer /></el-icon>首页</el-menu-item>
<el-menu-item index="/add"><el-icon><Plus /></el-icon>添加商品</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
<el-sub-menu index="2">
<template #title>
<span>首页配置</span>
</template>
<el-menu-item-group>
<el-menu-item index="/swiper"><el-icon><Picture /></el-icon>轮播图配置</el-menu-item>
</el-menu-item-group>
</el-sub-menu>
</el-menu>
...
script
<script setup>
...
const state = reactive({
showMenu: true,
defaultOpen: ['1', '2'],
currentPath: '/',
})
router.beforeEach((to, from, next) => {
if (to.path == '/login') {
// 如果路径是 /login 则正常执行
next()
} else {
// 如果不是 /login,判断是否有 token
if (!localGet('token')) {
// 如果没有,则跳至登录页面
next({ path: '/login' })
} else {
// 否则继续执行
next()
}
}
state.showMenu = !noMenu.includes(to.path)
state.currentPath = to.path
document.title = pathMap[to.name]
})
</script>
首先,上述 template
模板中,将 icon
都替换为符合标签语义的字符。
其次,给 el-menu
组件添加了两个属性,分别是 default-openeds
和 default-active
,前者代表默认打开的 el-sub-menu
索引(代码中默认赋值为全部打开),后者代表当前选中菜单项的高亮。
最后,在 script
逻辑中,通过路由监听函数的回调,设置当前选项高亮:
state.currentPath = to.path
上述所做的事情,只是让左侧的栏目显示出“轮播图配置”,而点击它之后,右侧的内容并没有发生变化。那是因为浏览器路径 /swiper
下,没能找到匹配的页面组件。
所以需要在 views
文件夹下新建页面组件 Swiper.vue
,如下所示:
<!--Swiper.vue-->
<template>
swiper
</template>
<script setup>
</script>
其次,在 router/index.js
下,新增路由配置项:
import Swiper from '@/views/Swiper.vue'
...
{
path: '/swiper',
name: 'swiper',
component: Swiper
}
...
最后,不要忘记每次新增页面需要增加一个头部的显示,打开 utils/index.js
,添加头部 pathMap
配置:
export const pathMap = {
index: '首页',
login: '登录',
add: '添加商品',
swiper: '轮播图配置',
}
此时重新启动项目,如下所示表示页面已经成功创建:
接下来准备在 Swiper.vue
文件内,制作页面,首先做如下修改:
template
<template>
<el-card class="swiper-container">
<el-table
:load="state.loading"
:data="state.tableData"
tooltip-effect="dark"
style="width: 100%"
>
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
label="轮播图"
width="200">
<template #default="scope">
<img style="width: 150px;height: 150px" :src="scope.row.carouselUrl" alt="轮播图">
</template>
</el-table-column>
<el-table-column
label="跳转链接"
>
<template #default="scope">
<a target="_blank" :href="scope.row.redirectUrl">{{ scope.row.redirectUrl }}</a>
</template>
</el-table-column>
<el-table-column
prop="carouselRank"
label="排序值"
width="120"
>
</el-table-column>
<el-table-column
prop="createTime"
label="添加时间"
width="200"
>
</el-table-column>
</el-table>
</el-card>
</template>
引入 el-card
作为外层包裹,在内部直接引入 el-table
。这里注意:load
属性用于数据加载之前的等待动画,但是我在之前的版本是使用的 v-loading
,后面官方好像是替换了这个属性的名称,导致我一直报错。
el-table-column
提供具名插槽,并且可以通过 #default="scope"
,拿到每一项的单独数据对象,可以在模板中进行使用,如 scope.row.carouselUrl
。
从这件事情可以看出,目前还处于
beta
版本的element-plus
还存在一些不稳定因素,代码随时会有优化的可能性。大家在使用的时候,遇到问题,及时查看文档。
script
<script setup>
import { onMounted, reactive, ref } from 'vue'
import axios from '@/utils/axios'
const state = reactive({
loading: false, // 控制加载动画
tableData: [], // 数据列表
currentPage: 1, // 当前页数
pageSize: 10, // 每页请求数
})
onMounted(() => {
getCarousels()
})
// 获取轮播图列表
const getCarousels = () => {
state.loading = true
axios.get('/carousels', {
params: {
pageNumber: state.currentPage,
pageSize: state.pageSize
}
}).then(res => {
state.tableData = res.list
state.loading = false
})
}
</script>
script
的逻辑也很直观,就是通过 axios.get
获取表格数据,赋值给 tableData
进行数据渲染。
显示效果如下图所示:
此时,需要在头部添加两个操作按钮:「增加」和「批量删除」,「增加」按钮的功能是点击后弹出表单,填写完之后调用生成轮播图配置接口,随后刷新列表。
接下来在 components
下新建 DialogAddSwiper.vue
组件,代码如下:
template
<!--DialogAddSwiper.vue-->
<template>
<el-dialog
:title="type == 'add' ? '添加轮播图' : '修改轮播图'"
v-model="state.visible"
width="400px"
>
<el-form :model="state.ruleForm" :rules="state.rules" ref="formRef" label-width="100px" class="good-form">
<el-form-item label="图片" prop="url">
<el-upload
class="avatar-uploader"
:action="state.uploadImgServer"
accept="jpg,jpeg,png"
:headers="{
token: state.token
}"
:show-file-list="false"
:before-upload="handleBeforeUpload"
:on-success="handleUrlSuccess"
>
<img style="width: 200px; height: 100px; border: 1px solid #e9e9e9;" v-if="state.ruleForm.url" :src="state.ruleForm.url" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
<el-form-item label="跳转链接" prop="link">
<el-input type="text" v-model="state.ruleForm.link"></el-input>
</el-form-item>
<el-form-item label="排序值" prop="sort">
<el-input type="number" v-model="state.ruleForm.sort"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="state.visible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</span>
</template>
</el-dialog>
</template>
从头分析一下代码,首先通过 type
变量控制是新增或是编辑,之后通过 visible
变量控制弹窗的显示隐藏,通过 ruleForm
和 rules
控制表单的数据和验证规则,el-upload
用于控制图片上传,el-upload
接受几个参数,参数释义如下:
-
action:上传接口,这边需要服务端提供。
-
accept:控制上传的文件后缀,目前这个参数并不生效,后面是通过
before-upload
来控制上传的文件。 -
headers:上传接口调用时,携带的请求头数据,项目中需要串 token 数据,因为这样才有权限调用上传接口,否则会报错。
-
on-success:成功回调方法,通常会在这里给变量赋值。
form
表单的使用和之前登录注册章节讲过的类似,这里不作赘述。
接下来是逻辑部分,代码如下:
script
<script setup>
import { reactive, ref } from 'vue'
import axios from '@/utils/axios'
// uploadImgServer 公用图片上传接口,我将其统一封装在 /utils/index.js 文件中
import { localGet, uploadImgServer } from '@/utils'
import { ElMessage } from 'element-plus'
const props = defineProps({
type: String, // add 为新增;edit 为编辑
reload: Function // table 刷新的方法
})
// formRef 用于表单验证控制
const formRef = ref(null)
const state = reactive({
uploadImgServer,
token: localGet('token') || '', // 用于调用上传图片接口是,放在请求头上的 token
visible: false, // 控制弹窗的显示隐藏
ruleForm: {
url: '',
link: '',
sort: ''
},
rules: {
url: [
{ required: 'true', message: '图片不能为空', trigger: ['change'] }
],
sort: [
{ required: 'true', message: '排序不能为空', trigger: ['change'] }
]
},
id: ''
})
// 获取详情
const getDetail = (id) => {
axios.get(`/carousels/${id}`).then(res => {
state.ruleForm = {
url: res.carouselUrl,
link: res.redirectUrl,
sort: res.carouselRank
}
})
}
// 上传之前,控制上传的文件。
const handleBeforeUpload = (file) => {
const sufix = file.name.split('.')[1] || ''
if (!['jpg', 'jpeg', 'png'].includes(sufix)) {
ElMessage.error('请上传 jpg、jpeg、png 格式的图片')
return false
}
}
// 上传图片
const handleUrlSuccess = (val) => {
state.ruleForm.url = val.data || ''
}
// 开启弹窗,此方法将在父组件,通过 ref 直接调用。
const open = (id) => {
state.visible = true
// 如果带着 id,则是编辑,否则为新增
if (id) {
state.id = id
getDetail(id)
} else {
state.ruleForm = {
url: '',
link: '',
sort: ''
}
}
}
// 关闭弹窗
const close = () => {
state.visible = false
}
// 提交表单方法
const submitForm = () => {
console.log(formRef.value.validate)
formRef.value.validate((valid) => {
// valid 为是否通过表单验证的变量
if (valid) {
if (props.type == 'add') {
// 增加用 axios.post
axios.post('/carousels', {
carouselUrl: state.ruleForm.url,
redirectUrl: state.ruleForm.link,
carouselRank: state.ruleForm.sort
}).then(() => {
ElMessage.success('添加成功')
state.visible = false
if (props.reload) props.reload()
})
} else {
// 编辑用 axios.put
axios.put('/carousels', {
carouselId: state.id,
carouselUrl: state.ruleForm.url,
redirectUrl: state.ruleForm.link,
carouselRank: state.ruleForm.sort
}).then(() => {
ElMessage.success('修改成功')
state.visible = false
if (props.reload) props.reload()
})
}
}
})
}
// 后续我们会在外面使用该组件内部的方法属性,通过 <script setup> 形式编写的组件,需通过 defineExpose 方法,将属性暴露出去。
defineExpose({ open, close })
</script>
详细的代码注释,都已经写在了上述代码中。
打开 utils/index.js
,添加 uploadImgServer
:
// 单张图片上传
export const uploadImgServer = 'http://backend-api-02.newbee.ltd/manage-api/v1/upload/file'
简单书写一下样式部分。
style
<style scoped>
.avatar-uploader {
width: 100px;
height: 100px;
color: #ddd;
font-size: 30px;
}
.avatar-uploader >>> .el-upload {
width: 100%;
text-align: center;
}
.avatar-uploader-icon {
display: block;
width: 100%;
height: 100%;
border: 1px solid #e9e9e9;
padding: 32px 17px;
}
</style>
弹窗组件的代码已经编写完成,接下来在 Swiper.vue
文件中引入并使用。
首先,在头部添加两个按钮,并且引入弹窗组件,代码如下:
<!--Swiper.vue-->
<template #header>
<el-card class="swiper-container">
<div class="header">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">增加</el-button>
<el-popconfirm
title="确定删除吗?"
confirmButtonText='确定'
cancelButtonText='取消'
@confirm="handleDelete"
>
<template #reference>
<el-button type="danger" size="small" icon="el-icon-delete">批量删除</el-button>
</template>
</el-popconfirm>
</div>
</el-card>
<DialogAddSwiper ref='addSwiper' :reload="getCarousels" :type="type" />
</template>
然后引入 DialogAddSwiper
组件,添加 handleAdd
方法,代码如下所示:
<script setup>
import DialogAddSwiper from '@/components/DialogAddSwiper.vue'
...
const addSwiper = ref(null)
const state = reactive({
...
type: 'add', // 操作类型
})
// 添加轮播项
const handleAdd = () => {
state.type = 'add'
addSwiper.value.open()
}
// 修改轮播图
const handleEdit = (id) => {
state.type = 'edit'
addSwiper.value.open(id)
}
</script>
上述模板中 ref='addSwiper'
、const addSwiper = ref(null)
的写法为 Vue3
对 red
的写法,一定要在 setup
函数中 return
给 template
。这样就可以通过 addSwiper
拿到 DialogAddSwiper
组件内部的方法,比如 addSwiper.value.open()
,通过获取弹窗组件内的 open
方法拿到内部的各个属性。
尝试在控制台打印一下 addSwiper
,便知道其中的原因,结果如下
如果想在 addSwiper 内得到上述图中的属性,需要在 DialogAddSwiper 组件中的 defineExpose 方法中传入上图中的各个属性。
上图中的变量都是在 DialogAddSwiper
组件内部声明的变量,可以轻易地在父组件中拿到,这样就可以控制弹窗内部的属性来控制弹窗的显示隐藏。
最后,外面需要通过如下指令,将 @vitejs/plugin-vue
升级到 2.3.3
版本,注意不要升级为最新版本,因为本教程使用的是 Vite 2
。
npm install @vitejs/plugin-vue@2.3.3 -D
重启项目后,效果如下所示:
温馨提示,上述代码中出现的
...
为之前写过的代码,这里省略掉,避免代码冗余。
接下来是「批量删除」的实现,el-table
组件为开发者们提供了原生选择属性,给 el-table
添加如下代码:
<!--Swiper.vue-->
...
<el-table
:load="state.loading"
ref="multipleTable"
:data="state.tableData"
tooltip-effect="dark"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="55">
</el-table-column>
...
</el-table>
<script>
import { ElMessage } from 'element-plus'
...
const state = reactive({
...
multipleSelection: [], // 选中项
})
// 选中之后的change方法,一旦选项有变化,就会触发该方法。
const handleSelectionChange = (val) => {
state.multipleSelection = val
}
// 批量删除
const handleDelete = () => {
if (!state.multipleSelection.length) {
ElMessage.error('请选择项')
return
}
axios.delete('/carousels', {
data: {
ids: state.multipleSelection.map(i => i.carouselId)
}
}).then(() => {
ElMessage.success('删除成功')
getCarousels()
})
}
</script>
最后,添加分页组件 el-pagination
,代码如下:
...
</el-table>
<el-pagination
background
layout="prev, pager, next"
:total="state.total"
:page-size="state.pageSize"
:current-page="state.currentPage"
@current-change="changePage"
/>
</el-card>
<script setup>
const state = reactive({
...
total: 0, // 总条数
})
// 获取轮播图列表
const getCarousels = () => {
state.loading = true
axios.get('/carousels', {
params: {
pageNumber: state.currentPage,
pageSize: state.pageSize
}
}).then(res => {
state.tableData = res.list
state.total = res.totalCount
state.currentPage = res.currPage
state.loading = false
})
}
const changePage = (val) => {
state.currentPage = val
getCarousels()
}
</script>
效果如下所示:
热销、新品、推荐页面制作
这三个页面的布局都是一样的,只不过请求接口的数据不一样罢了。实现这种需求的形式有两种。
-
通过
Tab
在统一组件内,切换不同的选项,从而替换展示的内容。 -
三个页面公用一个组件,通过路由监听变化,来判断不同的路径,对应不同的接口参数配置。
本项目中采用的是第二种方法,有兴趣的读者可以尝试改造成第一种方法。
新增页面的话,需要去 App.vue
添加菜单栏目,再去 router/index.js
添加组件路由配置,然后在 views
下新建组件。
这里,直接添加代码:
<!--App.vue-->
<el-submenu index="2">
<template #title>
<span>首页配置</span>
</template>
<el-menu-item-group>
<el-menu-item index="/swiper"><el-icon><Picture /></el-icon>轮播图配置</el-menu-item>
<el-menu-item index="/hot"><el-icon><StarFilled /></el-icon>热销商品配置</el-menu-item>
<el-menu-item index="/new"><el-icon><Sell /></el-icon>新品上线配置</el-menu-item>
<el-menu-item index="/recommend"><el-icon><ShoppingCart /></el-icon>为你推荐配置</el-menu-item>
</el-menu-item-group>
</el-submenu>
// router/index.js
{
path: '/hot',
name: 'hot',
component: IndexConfig
},
{
path: '/new',
name: 'new',
component: IndexConfig
},
{
path: '/recommend',
name: 'recommend',
component: IndexConfig
},
<!--IndexConfig.vue-->
<template>
<el-card class="index-container">
</el-card>
</template>
给模板添加一个路由监听方法:
<!--IndexConfig.vue-->
<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
// 监听路由变化
router.beforeEach((to) => {
console.log('to', to.name)
})
</script>
之后,打开浏览器,最终的显示效果如下图所示:
反复点击会发现一个问题:当你点击轮播图,再切回轮播图下的三个按钮的时候,会发生“上一次的 router
没有被销毁”的情况,然后又创建了一次 router
,导致执行了好几次 router.beforeEach
的回调方法,也直接导致我们若是在回调方法内直接根据路径的变化,请求不同的接口,一次性会有多个请求发出。
这里,直接查看源码,探索一下 beforeEach
的源码是怎么解释的,如下所示:
红框内的翻译大致是:返回一个函数,去消除注册的路由守卫。
很明显,代码需要修改:
<!--IndexConfig.vue-->
<script setup>
import { onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
// 监听路由变化
const unWatch = router.beforeEach((to) => {
console.log('to', to.name)
})
onUnmounted(() => {
unWatch()
})
</script>
当组件销毁的时候,路由守卫就会被销毁。
首先,实现「热销商品配置」的列表展示,在 IndexConfig.vue
文件下添加如下代码:
template
<template>
<el-card class="index-container">
<el-table
:load="state.loading"
:data="state.tableData"
tooltip-effect="dark"
style="width: 100%"
>
<el-table-column
prop="configName"
label="商品名称"
>
</el-table-column>
<el-table-column
label="跳转链接"
>
<template #default="scope">
<a target="_blank" :href="scope.row.redirectUrl">{{ scope.row.redirectUrl }}</a>
</template>
</el-table-column>
<el-table-column
prop="configRank"
label="排序值"
width="120"
>
</el-table-column>
<el-table-column
prop="goodsId"
label="商品编号"
width="200"
>
</el-table-column>
<el-table-column
prop="createTime"
label="添加时间"
width="200"
>
</el-table-column>
<el-table-column
label="操作"
width="100"
>
<template #default="scope">
<a style="cursor: pointer; margin-right: 10px" @click="handleEdit(scope.row.configId)">修改</a>
<el-popconfirm
title="确定删除吗?"
confirmButtonText='确定'
cancelButtonText='取消'
@confirm="handleDeleteOne(scope.row.configId)"
>
<template #reference>
<a style="cursor: pointer">删除</a>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!--总数超过一页,再展示分页器-->
<el-pagination
background
layout="prev, pager, next"
:total="state.total"
:page-size="state.pageSize"
:current-page="state.currentPage"
@current-change="changePage"
/>
</el-card>
</template>
script
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from '@/utils/axios'
// 首页配置类型参数
const configTypeMap = {
hot: 3,
new: 4,
recommend: 5
}
const router = useRouter()
const route = useRoute()
const state = reactive({
loading: false,
tableData: [], // 数据列表
total: 0, // 总条数
currentPage: 1, // 当前页
pageSize: 10, // 分页大小
type: 'add', // 操作类型
configType: 3 // 3-(首页)热销商品 4-(首页)新品上线 5-(首页)为你推荐
})
// 监听路由变化
router.beforeEach((to) => {
if (['hot', 'new', 'recommend'].includes(to.name)) {
// 通过 to.name 去匹配不同路径下,configType 参数也随之变化。
state.configType = configTypeMap[to.name]
state.currentPage = 1
getIndexConfig()
}
})
// 初始化
onMounted(() => {
state.configType = configTypeMap[route.name]
getIndexConfig()
})
// 首页热销商品列表
const getIndexConfig = () => {
state.loading = true
axios.get('/indexConfigs', {
params: {
pageNumber: state.currentPage,
pageSize: state.pageSize,
configType: state.configType
}
}).then(res => {
state.tableData = res.list
state.total = res.totalCount
state.currentPage = res.currPage
state.loading = false
})
}
const changePage = (val) => {
state.currentPage = val
getIndexConfig()
}
</script>
由于三个页面都是同一个接口 /indexConfigs
,只是根据 configType
参数的不同,返回相对应的值,下面是对应的值:
// 首页配置类型参数
const configTypeMap = {
hot: 3, // 热销
new: 4, // 新品
recommend: 5, // 推荐
}
之后去 src/utils/index.js
文件中,配置面包屑文字,代码如下所示:
export const pathMap = {
index: '首页',
login: '登录',
add: '添加商品',
swiper: '轮播图配置',
hot: '热销商品配置',
new: '新品上线配置',
recommend: '为你推荐配置',
}
展示效果如下图所示:
接下来为列表添加新增、修改弹窗组件 /components/DialogAddGood.vue
,
template
<!--DialogAddGood.vue-->
<template>
<el-dialog
:title="type == 'add' ? '添加商品' : '修改商品'"
v-model="state.visible"
width="400px"
>
<el-form :model="state.ruleForm" :rules="state.rules" ref="formRef" label-width="100px" class="good-form">
<el-form-item label="商品名称" prop="name">
<el-input type="text" v-model="state.ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="跳转链接" prop="link">
<el-input type="text" v-model="state.ruleForm.link"></el-input>
</el-form-item>
<el-form-item label="商品编号" prop="id">
<el-input type="number" min="0" v-model="state.ruleForm.id"></el-input>
</el-form-item>
<el-form-item label="排序值" prop="sort">
<el-input type="number" v-model="state.ruleForm.sort"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="state.visible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</span>
</template>
</el-dialog>
</template>
script
<script setup>
import { reactive, ref } from 'vue'
import axios from '@/utils/axios'
import { ElMessage } from 'element-plus'
const props = defineProps({
type: String,
configType: Number,
reload: Function
})
const formRef = ref(null)
const state = reactive({
visible: false,
ruleForm: {
name: '',
link: '',
id: '',
sort: ''
},
rules: {
name: [
{ required: 'true', message: '名称不能为空', trigger: ['change'] }
],
id: [
{ required: 'true', message: '编号不能为空', trigger: ['change'] }
],
sort: [
{ required: 'true', message: '排序不能为空', trigger: ['change'] }
]
},
id: ''
})
// 获取详情
const getDetail = (id) => {
axios.get(`/indexConfigs/${id}`).then(res => {
state.ruleForm = {
name: res.configName,
id: res.goodsId,
link: res.redirectUrl,
sort: res.configRank
}
})
}
// 开启弹窗
const open = (id) => {
state.visible = true
if (id) {
state.id = id
getDetail(id)
} else {
state.ruleForm = {
name: '',
id: '',
link: '',
sort: ''
}
}
}
// 关闭弹窗
const close = () => {
state.visible = false
}
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
if (state.ruleForm.id < 0 || state.ruleForm.id > 200) {
ElMessage.error('商品编号不能小于 0 或大于 200')
return
}
if (props.type == 'add') {
axios.post('/indexConfigs', {
configType: props.configType || 3,
configName: state.ruleForm.name,
redirectUrl: state.ruleForm.link,
goodsId: state.ruleForm.id,
configRank: state.ruleForm.sort
}).then(() => {
ElMessage.success('添加成功')
state.visible = false
if (props.reload) props.reload()
})
} else {
axios.put('/indexConfigs', {
configId: state.id,
configType: props.configType || 3,
configName: state.ruleForm.name,
redirectUrl: state.ruleForm.link,
goodsId: state.ruleForm.id,
configRank: state.ruleForm.sort
}).then(() => {
ElMessage.success('修改成功')
state.visible = false
if (props.reload) props.reload()
})
}
}
})
}
// 后续我们会在外面使用该组件内部的方法属性,通过 <script setup> 形式编写的组件,需通过 defineExpose 方法,将属性暴露出去。
defineExpose({ open, close })
</script>
这一步操作和轮播图的操作是一样的,不作赘述。
回到 IndexConfig.vue
,在头部添加新增、批量删除按钮,在底部引入 DialogAddGood.vue
弹窗组件,如下所示:
<template>
<el-card class="index-container">
<template #header>
<div class="header">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">增加</el-button>
<el-popconfirm
title="确定删除吗?"
confirmButtonText='确定'
cancelButtonText='取消'
@confirm="handleDelete"
>
<template #reference>
<el-button type="danger" size="small" icon="el-icon-delete">批量删除</el-button>
</template>
</el-popconfirm>
</div>
</template>
...
</el-card>
<DialogAddGood ref='addGood' :reload="getIndexConfig" :type="type" :configType="configType" />
</template>
添加逻辑部分:
<script setup>
...
import { ElMessage } from 'element-plus'
import DialogAddGood from '@/components/DialogAddGood.vue'
const state = reactive({
...
multipleSelection: [], // 选中项
})
...
// 添加商品
const handleAdd = () => {
state.type = 'add'
addGood.value.open()
}
// 修改商品
const handleEdit = (id) => {
state.type = 'edit'
addGood.value.open(id)
}
// 选择项
const handleSelectionChange = (val) => {
state.multipleSelection = val
}
// 删除
const handleDelete = () => {
if (!state.multipleSelection.length) {
ElMessage.error('请选择项')
return
}
axios.post('/indexConfigs/delete', {
ids: state.multipleSelection.map(i => i.configId)
}).then(() => {
ElMessage.success('删除成功')
getIndexConfig()
})
}
// 单个删除
const handleDeleteOne = (id) => {
axios.post('/indexConfigs/delete', {
ids: [id]
}).then(() => {
ElMessage.success('删除成功')
getIndexConfig()
})
}
</script>
最后重启项目,显示效果如下图所示:
总结
本章节篇幅较长,主要分析了轮播图 Table
制作,以及在数据相同的情况下,三个首页栏目的配置。主要知识点集中在弹窗组件的封装,路由监听事件的合理运用,以及页面销毁是,路由监听事件的销毁。这里需要注意,页面销毁时,一定要把当前页面的一些监听事件销毁,否则事件会一直存在执行栈内执行,后续会出现一些不可预知的 bug。
本章源码地址
文档最近更新时间:2022 年 9 月 20 日。