动态表格处理方案
# 动态表格渲染方案之文章排名业务实现
文章排名 核心的内容是围绕着表格处理来进行的。
核心业务:
- 文章排名切换
- 动态表格渲染
辅助功能:
- 文章排名页面展示
- 文章详情页面展示
# 辅助业务:文章排名页面渲染
点击查看
整个 文章排名 的页面渲染分成三个部分:
- 顶部的动态展示区域
- 中间的
table
列表展示区域 - 底部的分页展示区域
那么在这一小节中,我们先去渲染第 2、3 两部分:
创建
api/article
文件定义数据获取接口import request from '@/utils/request' /** * 获取列表数据 */ export const getArticleList = data => { return request({ url: '/article/list', params: data }) }
1
2
3
4
5
6
7
8
9
10
11
12在
article-ranking
中获取对应数据<script setup> import { ref, onActivated } from 'vue' import { getArticleList } from '@/api/article' import { watchSwitchLang } from '@/utils/i18n' // 数据相关 const tableData = ref([]) const total = ref(0) const page = ref(1) const size = ref(10) // 获取数据的方法 const getListData = async () => { const result = await getArticleList({ page: page.value, size: size.value }) tableData.value = result.list total.value = result.total } getListData() // 监听语言切换 watchSwitchLang(getListData) // 处理数据不重新加载的问题 onActivated(getListData) </script>
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根据数据渲染视图
<template> <div class="article-ranking-container"> <el-card> <el-table ref="tableRef" :data="tableData" border> <el-table-column :label="$t('msg.article.ranking')" prop="ranking" ></el-table-column> <el-table-column :label="$t('msg.article.title')" prop="title" ></el-table-column> <el-table-column :label="$t('msg.article.author')" prop="author" ></el-table-column> <el-table-column :label="$t('msg.article.publicDate')" prop="publicDate" > </el-table-column> <el-table-column :label="$t('msg.article.desc')" prop="desc" ></el-table-column> <el-table-column :label="$t('msg.article.action')"> <el-button type="primary" size="mini" @click="onShowClick(row)">{{ $t('msg.article.show') }}</el-button> <el-button type="danger" size="mini" @click="onRemoveClick(row)">{{ $t('msg.article.remove') }}</el-button> </el-table-column> </el-table> <el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="page" :page-sizes="[5, 10, 50, 100, 200]" :page-size="size" layout="total, sizes, prev, pager, next, jumper" :total="total" > </el-pagination> </el-card> </div> </template> <script setup> ... /** * size 改变触发 */ const handleSizeChange = currentSize => { size.value = currentSize getListData() } /** * 页码改变触发 */ const handleCurrentChange = currentPage => { page.value = currentPage getListData() } </script> <style lang="scss" scoped> .article-ranking-container { .header { margin-bottom: 20px; .dynamic-box { display: flex; align-items: center; .title { margin-right: 20px; font-size: 14px; font-weight: bold; } } } ::v-deep .el-table__row { cursor: pointer; } .pagination { margin-top: 20px; text-align: center; } } </style>
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
# 相对时间与时间国际化处理
在 发布时间 列中,我们希望展示相对时间,并且希望相对时间具备国际化的能力。那么我们就去需要到 filters
中对 dayjs
进行处理
定义相对时间的处理方法
... import rt from 'dayjs/plugin/relativeTime' ... // 加载相对时间插件 dayjs.extend(rt) function relativeTime(val) { if (!isNaN(val)) { val = parseInt(val) } // 当前时间 相对于 传入时间 return dayjs().to(dayjs(val)) } export default app => { app.config.globalProperties.$filters = { ... relativeTime } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22在
article-ranking
中使用相对时间<el-table-column :label="$t('msg.article.publicDate')"> <template #default="{row}"> {{ $filters.relativeTime(row.publicDate) }} </template> </el-table-column>
1
2
3
4
5接下来来处理国际化内容
... // 语言包 import 'dayjs/locale/zh-cn' import store from '@/store' ... function relativeTime(val) { ... return dayjs() .locale(store.getters.language === 'zh' ? 'zh-cn' : 'en') .to(dayjs(val)) }
1
2
3
4
5
6
7
8
9
10
11
12
# 动态表格原理与实现分析
根据列的勾选,动态展示表格中的列
- 展示可勾选的列
- 动态展示表格的列
展示可勾选的列:
可勾选的列通过 el-checkbox
来进行渲染。
动态展示表格的列:
依赖于数据,通过 v-for
渲染 el-table-column
实现步骤:
- 构建列数据(核心)
- 根据数据,通过
el-checkbox
渲染可勾选的列 - 根据数据,通过
v-for
动态渲染el-table-column
# 方案落地:动态列数据构建
因为我们要在 article-ranking
中处理多个业务,如果我们把所有的业务处理都写到 article-ranking
中,那么对应的组件就过于复杂了,所以说我们把所有的 动态列表 相关的业务放入到 article-ranking/dynamic
文件夹中
创建
article-ranking/dynamic/DynamicData
文件,用来指定初始的 列数据import i18n from '@/i18n' const t = i18n.global.t export default () => [ { label: t('msg.article.ranking'), prop: 'ranking' }, { label: t('msg.article.title'), prop: 'title' }, { label: t('msg.article.author'), prop: 'author' }, { label: t('msg.article.publicDate'), prop: 'publicDate' }, { label: t('msg.article.desc'), prop: 'desc' }, { label: t('msg.article.action'), prop: '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创建
article-ranking/dynamic/index
文件,对外暴露出- 动态列数据
- 被勾选的动态列数据
- table 的列数据
import getDynamicData from './DynamicData' import { watchSwitchLang } from '@/utils/i18n' import { watch, ref } from 'vue' // 暴露出动态列数据 export const dynamicData = ref(getDynamicData()) // 监听 语言变化 watchSwitchLang(() => { // 重新获取国际化的值 dynamicData.value = getDynamicData() // 重新处理被勾选的列数据 initSelectDynamicLabel() }) // 创建被勾选的动态列数据 export const selectDynamicLabel = ref([]) // 默认全部勾选 const initSelectDynamicLabel = () => { selectDynamicLabel.value = dynamicData.value.map(item => item.label) } initSelectDynamicLabel() // 声明 table 的列数据 export const tableColumns = ref([]) // 监听选中项的变化,根据选中项动态改变 table 列数据的值 watch( selectDynamicLabel, val => { tableColumns.value = [] // 遍历选中项 const selectData = dynamicData.value.filter(item => { // 被勾选的动态列数据中有无包括动态列数据中的数据 return val.includes(item.label) }) tableColumns.value.push(...selectData) }, { immediate: true } )
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
# 方案落地:实现动态表格能力
在
article-ranking
中渲染 动态表格的check
导入动态表格的
check
数据import { dynamicData, selectDynamicLabel } from './dynamic'
1完成动态表格的
check
渲染<el-card class="header"> <div class="dynamic-box"> <span class="title">{{ $t('msg.article.dynamicTitle') }}</span> <el-checkbox-group v-model="selectDynamicLabel"> <el-checkbox v-for="(item, index) in dynamicData" :label="item.label" :key="index" >{{ item.label }}</el-checkbox > </el-checkbox-group> </div> </el-card>
1
2
3
4
5
6
7
8
9
10
11
12
13导入动态列数据
import { ... tableColumns } from './dynamic'
1完成动态列渲染
<el-table ref="tableRef" :data="tableData" border> <el-table-column v-for="(item, index) in tableColumns" :key="index" :prop="item.prop" :label="item.label" > <template #default="{ row }" v-if="item.prop === 'publicDate'"> {{ $filters.relativeTime(row.publicDate) }} </template> <template #default="{ row }" v-else-if="item.prop === 'action'"> <el-button type="primary" size="mini" @click="onShowClick(row)">{{ $t('msg.article.show') }}</el-button> <el-button type="danger" size="mini" @click="onRemoveClick(row)">{{ $t('msg.article.remove') }}</el-button> </template> </el-table-column> </el-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 动态表格实现总结
把动态表格拆开来去看,主要就是分成了两部分:
- 展示可勾选的列
- 动态展示表格的列
那么对于这两部分而言,核心的就是 数据。
# 拖拽排序原理与实现分析
具体业务:
- 鼠标在某一行中按下
- 移动鼠标位置
- 产生对应的替换样式
- 鼠标抬起,表格行顺序发生变化
功能核心:监听鼠标事件,完成对应的 UI 视图处理
具体来说:
- 监听鼠标的按下事件
- 监听鼠标的移动事件
- 生成对应的
UI
样式 - 监听鼠标的抬起事件
实现方案:
- 利用 sortablejs (opens new window) 实现表格拖拽功能
- 在拖拽完成后,调用接口完成排序
# 方案落地:实现表格拖拽功能
下载 sortablejs
npm i [email protected]
1创建
article-ranking/sortable/index
文件,完成sortable
初始化import { ref } from 'vue' import Sortable from 'sortablejs' // 排序相关 export const tableRef = ref(null) /** * 初始化排序 */ export const initSortable = (tableData, cb) => { // 设置拖拽效果 // 取出table中要拖拽的元素 const el = tableRef.value.$el.querySelectorAll( '.el-table__body-wrapper > table > tbody' )[0] // 1. 要拖拽的元素 // 2. 配置对象 Sortable.create(el, { // 拖拽时类名 ghostClass: 'sortable-ghost', // 拖拽结束的回调方法 onEnd(event) {} }) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24在
article-ranking
中导入tableRef, initSortable
,并完成初始化import { tableRef, initSortable } from './sortable' // 表格拖拽相关 onMounted(() => { // 传递Ref响应式 initSortable(tableData, getListData) })
1
2
3
4
5
6
7指定拖拽时的样式
::v-deep .sortable-ghost { opacity: 0.6; color: #fff !important; background: #304156 !important; }
1
2
3
4
5
# 方案落地:完成拖拽后的排序
完成拖拽后的排序主要是在 拖拽结束的回调方法 中进行。
我们需要在 拖拽结束的回调方法中调用对应的服务端接口完成持久化的排序
在
api/article
中定义排序接口/** * 修改排序 */ export const articleSort = data => { return request({ url: '/article/sort', method: 'POST', data }) }
1
2
3
4
5
6
7
8
9
10在拖拽结束的回调方法中调用接口
// 拖拽结束的回调方法 async onEnd(event) { const { newIndex, oldIndex } = event // 修改数据 await articleSort({ initRanking: tableData.value[oldIndex].ranking, finalRanking: tableData.value[newIndex].ranking }) ElMessage.success({ message: i18n.global.t('msg.article.sortSuccess'), type: 'success' }) // 直接重新获取数据无法刷新 table!! tableData.value = [] // 重新获取数据 cb && cb() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 拖拽排序方案总结
整个拖拽排序的功能我们围绕着 sortablejs (opens new window) 来去进行实现。
# 辅助业务:文章删除
定义删除接口
/** * 删除文章 */ export const deleteArticle = articleId => { return request({ url: `/article/delete/${articleId}` }) }
1
2
3
4
5
6
7
8为删除按钮添加点击事件
<el-button type="danger" size="mini" @click="onRemoveClick(row)">{{ $t('msg.article.remove') }}</el-button>
1
2
3处理删除操作
// 删除用户 const i18n = useI18n() const onRemoveClick = row => { ElMessageBox.confirm( i18n.t('msg.article.dialogTitle1') + row.title + i18n.t('msg.article.dialogTitle2'), { type: 'warning' } ).then(async () => { await deleteArticle(row._id) ElMessage.success(i18n.t('msg.article.removeSuccess')) // 重新渲染数据 getListData() }) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 辅助业务:文章详情展示
文章详情中包含一个 编辑 按钮,用于对文章的编辑功能。与 创建文章 配合,达到相辅相成的目的。
在
api/article
中定义获取文章详情接口/** * 获取文章详情 */ export const articleDetail = (articleId) => { return request({ url: `/article/${articleId}` }) }
1
2
3
4
5
6
7
8在
article-detail
中获取文章详情数据<script setup> import { ref } from 'vue' import { useRoute } from 'vue-router' import { articleDetail } from '@/api/article' // 获取数据 const route = useRoute() const articleId = route.params.id const detail = ref({}) const getArticleDetail = async () => { detail.value = await articleDetail(articleId) } getArticleDetail() </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14在
article-detail
中,根据数据渲染视图<template> <div class="article-detail-container"> <h2 class="title">{{ detail.title }}</h2> <div class="header"> <span class="author" >{{ $t('msg.article.author') }}:{{ detail.author }}</span > <span class="time" >{{ $t('msg.article.publicDate') }}:{{ $filters.relativeTime(detail.publicDate) }}</span > <el-button type="text" class="edit" @click="onEditClick">{{ $t('msg.article.edit') }}</el-button> </div> <div class="content" v-html="detail.content"></div> </div> </template> ... <style lang="scss" scoped> .article-detail-container { .title { font-size: 22px; text-align: center; padding: 12px 0; } .header { padding: 26px 0; .author { font-size: 14px; color: #555666; margin-right: 20px; } .time { font-size: 14px; color: #999aaa; margin-right: 20px; } .edit { float: right; } } .content { font-size: 14px; padding: 20px 0; border-top: 1px solid #d4d4d4; } } </style>
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在
article-ranking/index.vue
点击进入详情页面/** * 查看按钮点击事件 */ const router = useRouter() const onShowClick = row => { router.push(`/article/${row._id}`) //article-detail页面的路径是/article/:id }
1
2
3
4
5
6
7
# 总结
- 文章排名切换
- 动态表格渲染