前言
在完成基于 Vue3 和 TypeScript 的商城后台管理系统项目后,总结的学习笔记。项目原作者coderwhy
项目地址 在线演示
知识点
表格时间格式化
使用 Element-Plus 的表格组件时,用于规范用户管理的注册时间。将 formatUtcString
函数作为一个工具函数存放在utils
文件夹下,将其全局注册 registerProperties
。
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)
}
代码语言:javascript复制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
处理
<template #createAt="scope">
<span>{{ $filters.formatTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filters.formatTime(scope.row.updateAt) }}</span>
</template>
表格组件的封装
- 把表头、表格的内容、表格页码跳转作为表格的基本元素封装起来(base-ui/table),代码结构如下
<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>
完整代码如下
代码语言:javascript复制<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>
- 由于页面内容的重合(用户管理表、商品管理表、角色管理表、菜单管理表等),把页面内容的呈现作为第二层封装(components/page-content)。具体表现在 是否选中(index)、编号(id)、创建和编辑按钮等。代码结构如下
<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>
完整代码如下
代码语言:javascript复制<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>
- 在各种表格页面使用,以
user.vue
页面为例