后台管理系统通用页面抽离=>高阶组件+配置文件+hooks
目录结构
配置文件和通用页面组件
content.config.ts
const contentConfig = {
pageName: "role",
header: {
title: "角色列表",
btnText: "新建角色"
},
propsList: [
{ type: "selection", label: "选择", width: "80px" },
{ type: "index", label: "序号", width: "80px" },
{ type: "normal", prop: "name", label: "角色名称", width: "180px" },
{ type: "normal", prop: "intro", label: "角色权限", width: "180px" },
{ type: "timer", prop: "createAt", label: "创建时间" },
{ type: "timer", prop: "updateAt", label: "更新时间" },
{ type: "handler", label: "操作", width: "180px" }
]
};
export default contentConfig;
modal.config.ts
const modalConfig = {
pageName: "role",
header: {
newTitle: "新建角色",
editTitle: "编辑角色"
},
formItems: [
{
type: "input",
label: "角色名称",
prop: "name",
placeholder: "请输入角色名称"
},
{
type: "input",
label: "权限介绍",
prop: "intro",
placeholder: "请输入权限介绍"
},
{
type: "custom",
slotName: "menuList"
}
]
};
export default modalConfig;
search.config.ts
const searchConfig = {
formItems: [
{
type: 'input',
prop: 'name',
label: '角色名称',
placeholder: '请输入查询的角色名称'
},
{
type: 'input',
prop: 'intro',
label: '角色权限',
placeholder: '请输入查询的角色权限'
},
{
type: 'date-picker',
prop: 'createAt',
label: '创建时间'
}
]
}
export default searchConfig;
role.vue
<script setup lang="ts">
import PageSearch from "@/components/page-search/page-search.vue";
import PageModal from "@/components/page-modal/page-modal.vue";
import PageContent from "@/components/page-content/page-content.vue";
import searchConfig from "@/views/main/system/role/config/search.config";
import contentConfig from "@/views/main/system/role/config/content.config";
import modalConfig from "@/views/main/system/role/config/modal.config";
import usePageContent from "@/hooks/usePageContent";
import usePageModal from "@/hooks/usePageModal";
import useSystemStore from "@/stores/modules/main/system/system";
import { ref, useTemplateRef, nextTick } from "vue";
import { ElTree } from "element-plus";
import {mapMenuListToIds} from "@/utils/mapMenus";
/**
* 新增角色时,清空菜单列表
*/
const newCallback = () => {
nextTick(() => {
treeRef.value?.setCheckedKeys([]);
})
}
/**
* 编辑角色时,回显角色所拥有的菜单列表
* @param itemData 当前编辑的角色信息
*/
const editCallback = (itemData: any) => {
nextTick(() => {
const menuIds = mapMenuListToIds(itemData.menuList)
treeRef.value?.setCheckedKeys(menuIds);
})
}
const { contentRef, handleQueryClick, handleResetClick } = usePageContent();
const { modalRef, handleNewClick, handleEditClick } = usePageModal(newCallback, editCallback); // editCallback 必须在 usePageModal() 方法前初始化
const systemStore = useSystemStore();
const menuList = systemStore.menuList;
const treeRef = useTemplateRef<InstanceType<typeof ElTree>>("treeRef");
const treeInfo = ref({});
/**
* 选择某菜单节点的回调函数
* @param node 传递给 data 属性的数组中该节点所对应的对象
* @param checked 树目前的选中状态对象
*/
const handleElTreeCheck = (node: any, checked: any) => {
const menuList = [...checked.checkedKeys, ...checked.halfCheckedKeys];
treeInfo.value = { menuList };
};
</script>
<template>
<div class="role">
<page-search
:searchConfig="searchConfig"
:query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<page-content
ref="contentRef"
:content-config="contentConfig"
@new-data-click="handleNewClick"
@edit-data-click="handleEditClick"
/>
<page-modal ref="modalRef" :modal-config="modalConfig" :treeInfo="treeInfo">
<template #menuList>
<el-tree
ref="treeRef"
:data="menuList"
show-checkbox
node-key="id"
:props="{ children: 'children', label: 'name' }"
@check="handleElTreeCheck"
/>
</template>
</page-modal>
</div>
</template>
<style scoped></style>
高阶组件
page-search.vue
<template>
<div class="search">
<!-- 1.1.表单输入 -->
<el-form :model="searchForm" ref="formRef" label-width="120px" size="large">
<el-row :gutter="20">
<template v-for="item in searchConfig.formItems" :key="item.prop">
<el-col :span="8">
<el-form-item :label="item.label" :prop="item.prop">
<template v-if="item.type === 'input'">
<el-input v-model="searchForm[item.prop]" :placeholder="item.placeholder" />
</template>
<template v-if="item.type === 'date-picker'">
<el-date-picker
v-model="searchForm[item.prop]"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 1.2.搜索按钮 -->
<div class="btns">
<el-button size="large" icon="Refresh" @click="handleResetClick">重置</el-button>
<el-button size="large" icon="Search" type="primary" @click="handleQueryClick">
查询
</el-button>
</div>
</div>
</template>
<script setup lang="ts" name="page-search">
import type {ElForm} from 'element-plus'
import {reactive, ref} from 'vue'
const emit = defineEmits(['queryClick', 'resetClick'])
// 根据配置初始化表单数据
const {searchConfig} = defineProps(['searchConfig'])
const initialForm: any = {}
for (const item of searchConfig.formItems) {
initialForm[item.prop] = ""
}
// console.log('初始化表单数据', initialForm)
// 1.创建表单的数据
const searchForm = reactive(initialForm)
// 2.监听按钮的点击
const formRef = ref<InstanceType<typeof ElForm>>()
function handleResetClick() {
formRef.value?.resetFields()
emit('resetClick')
}
function handleQueryClick() {
emit('queryClick', searchForm)
}
</script>
<style scoped lang="less">
.search {
background-color: #fff;
padding: 20px;
border-radius: 5px;
.el-form-item {
padding: 20px 40px;
margin-bottom: 0;
}
}
.btns {
text-align: right;
padding: 0 50px 10px 0;
}
</style>
page-content.vue
- header
- propList
- 插槽(定制化)=> 作用域插槽
- pageName
<template>
<div class="content">
<div class="header">
<h3 class="title">{{ contentConfig?.header?.title ?? "数据列表" }}</h3>
<el-button v-if="isCreate" type="primary" @click="handleNewData">{{
contentConfig?.header?.btnText ?? "新建数据"
}}</el-button>
</div>
<div class="table">
<el-table
:data="pageList"
:border="true"
:row-key="contentConfig?.childrenTree?.rowKey"
style="width: 100%"
>
<template v-for="item in contentConfig.propsList" :key="item.prop">
<!-- <el-table-column align="center" :label="item.label" :prop="item.prop" :width="item.width ?? '150px'"></el-table-column>-->
<el-table-column
v-if="item.type === 'index' || item.type === 'selection'"
align="center"
v-bind="item"
/>
<el-table-column v-else-if="item.type === 'custom'" align="center" v-bind="item">
<template #default="scope">
<slot :name="item.slotName" v-bind="scope" :prop="item.prop" :leaderRange="10" />
</template>
</el-table-column>
<el-table-column v-else align="center" v-bind="item">
<template #default="scope">
<span v-if="item.type === 'timer'">{{ formatUTC(scope.row[item.prop]) }}</span>
<span v-else-if="item.type === 'handler'">
<el-button
v-if="isUpdate"
type="primary"
size="small"
icon="EditPen"
link
@click="handleEditClick(scope.row)"
>
编辑
</el-button>
<el-button
v-if="isDelete"
type="danger"
size="small"
icon="Delete"
link
@click="handleDeleteClick(scope.row.id)"
>
删除
</el-button>
</span>
<span v-else>{{ scope.row[item.prop] }}</span>
</template>
</el-table-column>
</template>
</el-table>
</div>
<div class="footer">
<el-pagination
v-model:currentPage="currentPage"
v-model:pageSize="pageSize"
:page-sizes="[10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
:total="pageTotalCount"
@update:currentPage="handleCurrentChange"
@update:pageSize="handlePageSizeChange"
/>
</div>
</div>
</template>
<script setup lang="ts" name="content">
import { storeToRefs } from "pinia";
import { ref } from "vue";
import useSystemStore from "@/stores/modules/main/system/system";
import { formatUTC } from "@/utils/format";
import usePermission from "@/hooks/usePermission";
const { contentConfig } = defineProps(["contentConfig"]);
const emit = defineEmits(["newDataClick", "editDataClick"]);
// 0.判断是否有增删改查的权限
const isCreate = usePermission(contentConfig.pageName, "create");
const isDelete = usePermission(contentConfig.pageName, "delete");
const isUpdate = usePermission(contentConfig.pageName, "update");
const isQuery = usePermission(contentConfig.pageName, "query");
// 1.请求数据
const systemStore = useSystemStore();
const currentPage = ref(1);
const pageSize = ref(10);
systemStore.$onAction(({ name, after }) => {
after(() => {
if (
name === "deletePageByIdAction" ||
name === "editPageDataAction" ||
name === "newPageDataAction"
) {
currentPage.value = 1;
}
})
});
function fetchPageListData(queryInfo: any = {}) {
// 0.判断是否具有查询权限
if (!isQuery) return;
// 1.获取offset和size
const size = pageSize.value;
const offset = (currentPage.value - 1) * size;
// 2.发生网络请求
systemStore.postPageListAction(contentConfig.pageName, { offset, size, ...queryInfo });
}
fetchPageListData();
// 2.展示数据
const { pageList, pageTotalCount } = storeToRefs(systemStore);
// 3.绑定分页数据
function handleCurrentChange() {
fetchPageListData();
}
function handlePageSizeChange(newPageSize: number) {
pageSize.value = newPageSize;
fetchPageListData();
}
function handleResetClick() {
currentPage.value = 1;
pageSize.value = 10;
fetchPageListData();
}
// 4.新建数据的处理
function handleNewData() {
emit("newDataClick");
}
// 5.删除和编辑操作
function handleDeleteClick(id: number) {
systemStore.deletePageByIdAction(contentConfig.pageName, id);
}
function handleEditClick(data: any) {
emit("editDataClick", data);
}
// 暴露函数
defineExpose({
fetchPageListData,
handleResetClick
});
</script>
<style scoped lang="less">
.content {
margin-top: 20px;
padding: 20px;
background-color: #fff;
.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;
}
}
.table {
:deep(.el-table__cell) {
padding: 14px 0;
}
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: 15px;
}
}
</style>
page-modal.vue
- header
- newTitle
- editTitle
- pageName
- formItems
<template>
<div class="modal">
<el-dialog v-model="dialogVisible" :title="modalConfig.header.newTitle" width="30%" center>
<div class="form">
<el-form :model="formData" label-width="80px" size="large">
<template v-for="item in modalConfig.formItems" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop">
<template v-if="item.type === 'input'">
<el-input v-model="formData[item.prop]" :placeholder="item.placeholder" />
</template>
<template v-if="item.type === 'password'">
<el-input
show-password
v-model="formData[item.prop]"
:placeholder="item.placeholder"
/>
</template>
<template v-if="item.type === 'select'">
<el-select
v-model="formData.parentId"
:placeholder="item.placeholder"
style="width: 100%"
>
<template v-for="value in item.options" :key="value.value">
<el-option :value="value.value" :label="value.label" />
</template>
</el-select>
</template>
<template v-if="item.type === 'date-picker'">
<el-date-picker
type="daterange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
v-model="formData[item.prop]"
/>
</template>
<template v-if="item.type === 'custom'">
<slot :name="item.slotName"></slot>
</template>
</el-form-item>
</template>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmClick">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="modal">
import { storeToRefs } from "pinia";
import { reactive, ref } from "vue";
import useSystemStore from "@/stores/modules/main/system/system";
const dialogVisible = ref(false);
const isEdit = ref(false);
const editData = ref();
// 定义数据绑定
const formData = reactive<any>({
name: "",
leader: "",
parentId: ""
});
const { modalConfig, treeInfo } = defineProps(["modalConfig", "treeInfo"]);
// 点击确定
const systemStore = useSystemStore();
const { departmentList } = storeToRefs(systemStore);
const initialData: any = {};
for (const item of modalConfig?.formItems) {
initialData[item.prop] = item.initialValue ?? "";
}
function handleConfirmClick() {
dialogVisible.value = false;
// 判断是否存在含树形菜单权限的formData
let treeFormData = { ...formData };
if (treeInfo) {
treeFormData = {
...treeFormData,
...treeInfo
};
}
console.log(treeFormData);
if (!isEdit.value) {
systemStore.newPageDataAction(modalConfig.pageName, treeFormData);
} else {
systemStore.editPageDataAction(modalConfig.pageName, editData.value.id, treeFormData);
}
}
// 新建或者编辑
function setDialogVisible(isNew: boolean = true, data: any = {}) {
dialogVisible.value = true;
isEdit.value = !isNew;
editData.value = data;
for (const key in formData) {
if (isNew) {
formData[key] = "";
} else {
formData[key] = data[key];
}
}
}
defineExpose({
setDialogVisible
});
</script>
<style scoped lang="less">
.form {
padding: 10px 30px;
}
</style>
hooks
usePageContent.ts
import { useTemplateRef } from "vue";
import PageContent from "@/components/page-content/page-content.vue";
function usePageContent() {
const contentRef = useTemplateRef<InstanceType<typeof PageContent>>("contentRef");
const handleQueryClick = (queryInfo: any) => {
contentRef.value?.fetchPageListData(queryInfo);
};
const handleResetClick = () => {
contentRef.value?.fetchPageListData();
};
return {
contentRef,
handleQueryClick,
handleResetClick
};
}
export default usePageContent;
usePageModal.ts
import { useTemplateRef } from "vue";
import PageModal from "@/components/page-modal/page-modal.vue";
function usePageModal(newCallback?: () => void, editCallback?: (itemData: any) => void) {
const modalRef = useTemplateRef<InstanceType<typeof PageModal>>("modalRef");
const handleNewClick = () => {
modalRef.value?.setDialogVisible(true);
if (newCallback) newCallback();
};
const handleEditClick = (itemData: any) => {
modalRef.value?.setDialogVisible(false, itemData);
if (editCallback) editCallback(itemData)
};
return {
modalRef,
handleNewClick,
handleEditClick
};
}
export default usePageModal;