目录

35.Vue3实战之首页配置

前言

上一小节,我们开始了内容页部分的开发,分别使用了 ElCardElPopover 等常用该组件。这两个组件在很多业务场景都非常的实用,同学们在开发后台管理系统的时候,尽量合理的利用好组件库给我提供的组件,减少自己编写组件,提高工作效率。

本小节将为同学们带来后台管理系统的重头戏,TableUploadDialog 组件的使用,以及遇到多个类似页面的时候,如何通用一个组件。

本章节知识点

  • 需要注册的组件:ElTableElUploadElDialogElPagination

  • 弹窗组件的封装。

  • 多页面公用同一个组件。

首页轮播图配置

首先打开 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-openedsdefault-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: '轮播图配置',
}

此时重新启动项目,如下所示表示页面已经成功创建:

https://s.yezgea02.com/1618131263940/WeChat3dc4fd79ed4d9b11e76922b697f8006a.png

接下来准备在 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 进行数据渲染。

显示效果如下图所示:

https://s.yezgea02.com/1618145405832/WeChat8ca22ea195f286729acf0c9b983c55c0.png

此时,需要在头部添加两个操作按钮:「增加」和「批量删除」,「增加」按钮的功能是点击后弹出表单,填写完之后调用生成轮播图配置接口,随后刷新列表。

接下来在 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 变量控制弹窗的显示隐藏,通过 ruleFormrules 控制表单的数据和验证规则,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) 的写法为 Vue3red 的写法,一定要在 setup 函数中 returntemplate。这样就可以通过 addSwiper 拿到 DialogAddSwiper 组件内部的方法,比如 addSwiper.value.open(),通过获取弹窗组件内的 open 方法拿到内部的各个属性。

尝试在控制台打印一下 addSwiper,便知道其中的原因,结果如下

https://s.yezgea02.com/1618149014587/WeChatbba98db1d2f9ee088fc770b16f064cb6.png

如果想在 addSwiper 内得到上述图中的属性,需要在 DialogAddSwiper 组件中的 defineExpose 方法中传入上图中的各个属性。

上图中的变量都是在 DialogAddSwiper 组件内部声明的变量,可以轻易地在父组件中拿到,这样就可以控制弹窗内部的属性来控制弹窗的显示隐藏。

最后,外面需要通过如下指令,将 @vitejs/plugin-vue 升级到 2.3.3 版本,注意不要升级为最新版本,因为本教程使用的是 Vite 2

npm install @vitejs/plugin-vue@2.3.3 -D

重启项目后,效果如下所示:

https://s.yezgea02.com/1618149368768/Kapture%202021-04-11%20at%2021.55.58.gif

温馨提示,上述代码中出现的 ... 为之前写过的代码,这里省略掉,避免代码冗余。

接下来是「批量删除」的实现,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>

效果如下所示:

https://s.yezgea02.com/1618328276365/WeChat81d753132bf4487e57207c5564551686.png

热销、新品、推荐页面制作

这三个页面的布局都是一样的,只不过请求接口的数据不一样罢了。实现这种需求的形式有两种。

  • 通过 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>

之后,打开浏览器,最终的显示效果如下图所示:

https://s.yezgea02.com/1618153210241/Kapture%202021-04-11%20at%2023.00.03.gif

反复点击会发现一个问题:当你点击轮播图,再切回轮播图下的三个按钮的时候,会发生“上一次的 router 没有被销毁”的情况,然后又创建了一次 router,导致执行了好几次 router.beforeEach 的回调方法,也直接导致我们若是在回调方法内直接根据路径的变化,请求不同的接口,一次性会有多个请求发出。

这里,直接查看源码,探索一下 beforeEach 的源码是怎么解释的,如下所示:

https://s.yezgea02.com/1618327440553/WeChatf71b0104cd3ae0e64e7f6c408c5a6b3f.png

红框内的翻译大致是:返回一个函数,去消除注册的路由守卫。

很明显,代码需要修改:

<!--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: '为你推荐配置',
}

展示效果如下图所示:

https://s.yezgea02.com/1618364578965/Kapture%202021-04-14%20at%2009.42.48.gif

接下来为列表添加新增、修改弹窗组件 /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>

最后重启项目,显示效果如下图所示:

https://s.yezgea02.com/1618368089339/Kapture%202021-04-14%20at%2010.41.21.gif

总结

本章节篇幅较长,主要分析了轮播图 Table 制作,以及在数据相同的情况下,三个首页栏目的配置。主要知识点集中在弹窗组件的封装,路由监听事件的合理运用,以及页面销毁是,路由监听事件的销毁。这里需要注意,页面销毁时,一定要把当前页面的一些监听事件销毁,否则事件会一直存在执行栈内执行,后续会出现一些不可预知的 bug。

本章源码地址

点击下载

文档最近更新时间:2022 年 9 月 20 日。