前言

在完成基于 Vue3 和 TypeScript 的商城后台管理系统项目后,总结的学习笔记。项目原作者coderwhy

项目地址 在线演示

知识点

表格时间格式化

使用 Element-Plus 的表格组件时,用于规范用户管理的注册时间。将 formatUtcString 函数作为一个工具函数存放在utils 文件夹下,将其全局注册 registerProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'

dayjs.extend(utc)

const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'

export function formatUtcString(
utcString: string,
format: string = DATE_TIME_FORMAT
) {
return dayjs.utc(utcString).utcOffset(8).format(format)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { App } from 'vue'

import { formatUtcString } from '@/utils/date-format'

export default function registerProperties(app: App) {
app.config.globalProperties.$filters = {
foo() {
console.log('foo')
},
formatTime(value: string) {
return formatUtcString(value)
}
}
}

当使用表格组件时,借助列中的插槽进行 formatTime 处理

1
2
3
4
5
6
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>

表格组件的封装

  1. 把表头、表格的内容、表格页码跳转作为表格的基本元素封装起来(base-ui/table),代码结构如下

table的封装.jpg

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
<template>
<div class="hy-table">
<div class="header">
</div>

<el-table>
</el-table>

<div class="footer" v-if="showFooter">
</div>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
props: {
title: {
type: String,
default: ''
},
...
},
emits: ['selectionChange', 'update:page'],
setup(props, { emit }) {
const handleSelectionChange = (value: any) => {
emit('selectionChange', value)
}
...

return {
handleSelectionChange,
...
}
}
})
</script>

<style scoped lang="less">

</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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<template>
<div class="hy-table">
<div class="header">
<slot name="header">
<div class="title">{{ title }}</div>
<div class="handler">
<slot name="headerHandler"></slot>
</div>
</slot>
</div>
<el-table
:data="listData"
border
style="width: 100%"
@selection-change="handleSelectionChange"
v-bind="childrenProps"
>
<el-table-column
v-if="showSelectColumn"
type="selection"
align="center"
width="60"
></el-table-column>
<el-table-column
v-if="showIndexColumn"
type="index"
label="序号"
align="center"
width="80"
></el-table-column>
<template v-for="propItem in propList" :key="propItem.prop">
<el-table-column v-bind="propItem" align="center" show-overflow-tooltip>
<template #default="scope">
<slot :name="propItem.slotName" :row="scope.row">
{{ scope.row[propItem.prop] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
<div class="footer" v-if="showFooter">
<slot name="footer">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="page.currentPage"
:page-size="page.pageSize"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
:total="listCount"
>
</el-pagination>
</slot>
</div>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
props: {
title: {
type: String,
default: ''
},
listData: {
type: Array,
required: true
},
listCount: {
type: Number,
default: 0
},
propList: {
type: Array,
required: true
},
showIndexColumn: {
type: Boolean,
default: false
},
showSelectColumn: {
type: Boolean,
default: false
},
page: {
type: Object,
default: () => ({ currentPage: 0, pageSize: 10 })
},
childrenProps: {
type: Object,
default: () => ({})
},
showFooter: {
type: Boolean,
default: true
}
},
emits: ['selectionChange', 'update:page'],
setup(props, { emit }) {
const handleSelectionChange = (value: any) => {
emit('selectionChange', value)
}

const handleCurrentChange = (currentPage: number) => {
emit('update:page', { ...props.page, currentPage })
}

const handleSizeChange = (pageSize: number) => {
emit('update:page', { ...props.page, pageSize })
}

return {
handleSelectionChange,
handleCurrentChange,
handleSizeChange
}
}
})
</script>

<style scoped lang="less">
.header {
display: flex;
height: 45px;
padding: 0 5px;
justify-content: space-between;
align-items: center;

.title {
font-size: 20px;
font-weight: 700;
}

.handler {
align-items: center;
}
}

.footer {
margin-top: 15px;

.el-pagination {
text-align: right;
}
}
</style>

  1. 由于页面内容的重合(用户管理表、商品管理表、角色管理表、菜单管理表等),把页面内容的呈现作为第二层封装(components/page-content)。具体表现在 是否选中(index)、编号(id)、创建和编辑按钮等。代码结构如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<div class="page-content">
<hy-table
:listData="dataList"
:listCount="dataCount"
v-bind="contentTableConfig"
v-model:page="pageInfo"
>
<!-- 1.header中的插槽 -->
<template #headerHandler>
</template>

<!-- 2.列中的插槽 -->
<template #status="scope">
</template>

<template #createAt="scope">
</template>

<template #updateAt="scope">
</template>

<template #handler="scope">
<div class="handle-btns">
</div>
</template>

<!-- 在page-content中动态插入剩余的插槽 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</hy-table>
</div>
</template>

<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import { useStore } from '@/store'
import { usePermission } from '@/hooks/use-permission'

import HyTable from '@/base-ui/table'

export default defineComponent({
components: {
HyTable
},
props: {
},
emits: ['newBtnClick', 'editBtnClick'],
setup(props, { emit }) {
const store = useStore()

// 0.获取操作的权限
const isCreate = usePermission(props.pageName, 'create')
const isUpdate = usePermission(props.pageName, 'update')
const isDelete = usePermission(props.pageName, 'delete')
const isQuery = usePermission(props.pageName, 'query')

// 1.双向绑定pageInfo
const pageInfo = ref({ currentPage: 1, pageSize: 10 })
watch(pageInfo, () => getPageData())

// 2.发送网络请求
const getPageData = (queryInfo: any = {}) => {
}
getPageData()

// 3.从vuex中获取数据
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
const dataCount = computed(() =>
store.getters[`system/pageListCount`](props.pageName)
)

// 4.获取其他的动态插槽名称
const otherPropSlots = props.contentTableConfig?.propList.filter(

)

// 5.删除/编辑/新建操作
const handleDeleteClick = (item: any) => {

}
const handleNewClick = () => {
emit('newBtnClick')
}
const handleEditClick = (item: any) => {
emit('editBtnClick', item)
}

return {
dataList,
getPageData,
dataCount,
pageInfo,
otherPropSlots,
isCreate,
isUpdate,
isDelete,
handleDeleteClick,
handleNewClick,
handleEditClick
}
}
})
</script>

<style scoped>
</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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<template>
<div class="page-content">
<hy-table
:listData="dataList"
:listCount="dataCount"
v-bind="contentTableConfig"
v-model:page="pageInfo"
>
<!-- 1.header中的插槽 -->
<template #headerHandler>
<el-button
v-if="isCreate"
type="primary"
size="medium"
@click="handleNewClick"
>
新建用户
</el-button>
</template>

<!-- 2.列中的插槽 -->
<template #status="scope">
<el-button
plain
size="mini"
:type="scope.row.enable ? 'success' : 'danger'"
>
{{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</template>
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
<template #handler="scope">
<div class="handle-btns">
<el-button
v-if="isUpdate"
icon="el-icon-edit"
size="mini"
type="text"
@click="handleEditClick(scope.row)"
>
编辑
</el-button>
<el-button
v-if="isDelete"
icon="el-icon-delete"
size="mini"
type="text"
@click="handleDeleteClick(scope.row)"
>删除</el-button
>
</div>
</template>

<!-- 在page-content中动态插入剩余的插槽 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</hy-table>
</div>
</template>

<script lang="ts">
import { defineComponent, computed, ref, watch } from 'vue'
import { useStore } from '@/store'
import { usePermission } from '@/hooks/use-permission'

import HyTable from '@/base-ui/table'

export default defineComponent({
components: {
HyTable
},
props: {
contentTableConfig: {
type: Object,
require: true
},
pageName: {
type: String,
required: true
}
},
emits: ['newBtnClick', 'editBtnClick'],
setup(props, { emit }) {
const store = useStore()

// 0.获取操作的权限
const isCreate = usePermission(props.pageName, 'create')
const isUpdate = usePermission(props.pageName, 'update')
const isDelete = usePermission(props.pageName, 'delete')
const isQuery = usePermission(props.pageName, 'query')

// 1.双向绑定pageInfo
const pageInfo = ref({ currentPage: 1, pageSize: 10 })
watch(pageInfo, () => getPageData())

// 2.发送网络请求
const getPageData = (queryInfo: any = {}) => {
if (!isQuery) return
store.dispatch('system/getPageListAction', {
pageName: props.pageName,
queryInfo: {
offset: (pageInfo.value.currentPage - 1) * pageInfo.value.pageSize,
size: pageInfo.value.pageSize,
...queryInfo
}
})
}
getPageData()

// 3.从vuex中获取数据
const dataList = computed(() =>
store.getters[`system/pageListData`](props.pageName)
)
const dataCount = computed(() =>
store.getters[`system/pageListCount`](props.pageName)
)

// 4.获取其他的动态插槽名称
const otherPropSlots = props.contentTableConfig?.propList.filter(
(item: any) => {
if (item.slotName === 'status') return false
if (item.slotName === 'createAt') return false
if (item.slotName === 'updateAt') return false
if (item.slotName === 'handler') return false
return true
}
)

// 5.删除/编辑/新建操作
const handleDeleteClick = (item: any) => {
console.log(item)
store.dispatch('system/deletePageDataAction', {
pageName: props.pageName,
id: item.id
})
}
const handleNewClick = () => {
emit('newBtnClick')
}
const handleEditClick = (item: any) => {
emit('editBtnClick', item)
}

return {
dataList,
getPageData,
dataCount,
pageInfo,
otherPropSlots,
isCreate,
isUpdate,
isDelete,
handleDeleteClick,
handleNewClick,
handleEditClick
}
}
})
</script>

<style scoped>
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
  1. 在各种表格页面使用,以 user.vue 页面为例

user页面的使用.jpg