最终效果
安装依赖
npm i dayjs
格式化时间
代码实现
components/SUI/S-comMangeInfo.vue
<script lang="ts" setup>
import dayjs from "dayjs";
import type { DrawerProps } from "element-plus";
// 类型声明
type GenericObject = {
[key: string]: any;
};
type RecordObject = Record<string, any>;
const emit = defineEmits<{
handle_add: [];
handle_edit: [row: GenericObject];
getList_done: [data: RecordObject[], total: number];
}>();
// 父组件传参
const { Model, PageConfig, tableDataFormate } = defineProps<{
Model: {
[key: string]: GenericObject;
};
PageConfig: GenericObject;
tableDataFormate?: (row: RecordObject) => RecordObject;
}>();
// 父组件传参--二次加工
const propData = computed(() => {
let searchForm_formItemConfigList: GenericObject[] = [];
let form_customFormItemConfigList: GenericObject[] = [];
let defaultFormData: RecordObject = {};
let table_columnConfigList: GenericObject[] = [];
let top4_searchFieldList: string[] = [];
for (const [key, value] of Object.entries(Model)) {
if ("search" in value && value.search) {
searchForm_formItemConfigList.push({
prop: key,
...(value as object),
searchOrder: value.searchOrder || 99,
});
}
if (value.type === "custom") {
form_customFormItemConfigList.push({
prop: key,
...(value as object),
});
}
if (value.defaultValue) {
defaultFormData[key] = value.defaultValue;
}
if (!value.tableHide) {
table_columnConfigList.push({
prop: key,
...(value as object),
tableOrder: value.tableOrder || 999,
});
}
}
searchForm_formItemConfigList = searchForm_formItemConfigList.sort(
(a, b) => a.searchOrder - b.searchOrder
);
searchForm_formItemConfigList.forEach((item, index) => {
if (index < 4) {
top4_searchFieldList.push(item.prop);
if (item.type === "dateRange") {
top4_searchFieldList.push(item.prop + "_start");
top4_searchFieldList.push(item.prop + "_end");
}
}
});
table_columnConfigList = table_columnConfigList.sort(
(a, b) => a.tableOrder - b.tableOrder
);
return {
searchForm_formItemConfigList,
form_customFormItemConfigList,
defaultFormData,
table_columnConfigList,
top4_searchFieldList,
};
});
const direction = ref<DrawerProps["direction"]>("rtl");
const show_allSearch = ref(false);
const callbackMessage = ref({
show: false,
valid: true,
content: "",
});
// 当前操作状态
const action = ref("search");
// 操作状态字典
const actionDic: GenericObject = {
add: "新增",
edit: "修改",
detail: "详情",
};
const pageData = reactive<{
currentPage: number;
pageSize: number;
total: number;
searchformData: RecordObject;
tableData: RecordObject[];
loadData: boolean;
}>({
currentPage: 1,
pageSize: 10,
total: 0,
searchformData: {},
tableData: [],
loadData: false,
});
const { currentPage, pageSize, total, searchformData, tableData, loadData } =
toRefs(pageData);
// 排序配置
const sort = ref({
column: "createdAt",
direction: "desc",
});
// 日期选择组件配置
const shortcuts = [
{
text: "上周",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: "上个月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: "三个月前",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
];
// 搜索表单宽度样式
const formItemWidthClass = "w-260px!";
// 编辑表单数据
let formData: GenericObject = {};
// 是否添加了更多搜索条件
const hasMoreSearchCondition = computed(() => {
let result = false;
for (const key in pageData.searchformData) {
if (
!propData.value.top4_searchFieldList.includes(key) &&
(pageData.searchformData[key] || pageData.searchformData[key] === 0)
) {
result = true;
break;
}
}
return result;
});
// 自适配保存的API
const saveAPI = computed(() => {
if (action.value === "add") {
if (PageConfig.api && PageConfig.api.add) {
return PageConfig.api.add;
} else {
return `/${PageConfig.entity}/add`;
}
} else {
if (PageConfig.api && PageConfig.api.edit) {
return PageConfig.api.edit;
} else {
return `/${PageConfig.entity}/edit`;
}
}
});
// 获取分页表格数据
const getList = async () => {
loadData.value = true;
let searchformData_temp: RecordObject = JSON.parse(
JSON.stringify(pageData.searchformData)
);
for (const key in searchformData_temp) {
if (typeof searchformData_temp[key] === "object") {
delete searchformData_temp[key];
}
}
const res: GenericObject = await $fetch(`/api/${PageConfig.entity}/list`, {
query: {
orderBy: sort.value.column,
order: sort.value.direction,
currentPage: pageData.currentPage,
pageSize: pageData.pageSize,
...searchformData_temp,
},
});
if (res) {
tableData.value = res.data.map((row: RecordObject) => {
row.createdAt = dayjs(row!.createdAt).format("YYYY-MM-DD HH:mm");
row.updatedAt = dayjs(row!.updatedAt).format("YYYY-MM-DD HH:mm");
return tableDataFormate ? tableDataFormate(row) : row;
});
total.value = res.total;
}
loadData.value = false;
emit("getList_done", tableData.value, total.value);
};
// 按钮--取消(多条件搜索抽屉中)
function cancelMoreSearch() {
show_allSearch.value = false;
}
// 按钮--查询(多条件搜索抽屉中)
function confirmMoreSearch() {
getList();
show_allSearch.value = false;
}
// 切换分页-每页条数
const handleSizeChange = (val: number) => {
pageSize.value = val;
getList();
};
// 点击分页-页码
const handleCurrentChange = (val: number) => {
currentPage.value = val;
getList();
};
// 点击按钮-重置
const reset = () => {
pageData.currentPage = 1;
pageData.pageSize = 10;
pageData.searchformData = {};
getList();
};
// 点击按钮-新增
const handle_add = () => {
formData = JSON.parse(JSON.stringify(propData.value.defaultFormData));
action.value = "add";
emit("handle_add");
};
// 点击按钮-编辑
const handle_edit = (row: GenericObject) => {
formData = row;
action.value = "edit";
emit("handle_edit", row);
};
// 点击按钮-删除
const handle_del = async (id: string) => {
try {
await useFetch(`/api/${PageConfig.entity}/${id}`, {
method: "DELETE",
});
callbackMessage.value = {
show: true,
valid: true,
content: "操作成功",
};
getList();
} catch (e: any) {
callbackMessage.value = {
show: true,
valid: false,
content: e.data.message,
};
}
};
// 点击按钮-重置密码
const handle_resetPassword = async (id: string) => {
try {
await useFetch(`/api/user/${id}`, {
method: "PATCH",
query: {
handleType: "resetPassword",
},
});
callbackMessage.value = {
show: true,
valid: true,
content: "操作成功",
};
} catch (e: any) {
callbackMessage.value = {
show: true,
valid: false,
content: e.data.message,
};
}
};
// 点击按钮-详情
const handle_detail = (row: GenericObject) => {
formData = row;
action.value = "detail";
};
// 回调函数--保存成功
const saveOK = async () => {
setTimeout(() => {
action.value = "search";
getList();
}, 500);
};
const dateRangeChange = (newValue: string[] | null, prop: string) => {
if (newValue) {
pageData.searchformData[prop + "_start"] = newValue[0];
pageData.searchformData[prop + "_end"] = newValue[1];
} else {
delete pageData.searchformData[prop + "_start"];
delete pageData.searchformData[prop + "_end"];
}
};
// 处理 before-change 事件的方法
const beforeSwitchChange = async (
oldValue: boolean | number | string,
prop: string
) => {
if (PageConfig.entity === "user" && prop === "disabled") {
try {
// 弹出确认框
await ElMessageBox.confirm(
`确定${oldValue ? "启用" : "禁用"}吗?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
);
// 用户点击了确定,允许状态改变
return true;
} catch (error) {
// 用户点击了取消,阻止状态改变
return false;
}
} else {
return true;
}
};
const switchChange = async (
newValue: boolean | number | string,
id: string
) => {
try {
await useFetch(`/api/user/${id}`, {
method: "PATCH",
query: {
handleType: newValue ? "disable" : "enable",
},
});
callbackMessage.value = {
show: true,
valid: true,
content: "操作成功",
};
} catch (e: any) {
callbackMessage.value = {
show: true,
valid: false,
content: e.data.message,
};
}
};
onMounted(() => {
getList();
});
// 对外暴露的变量和方法
defineExpose({
saveOK,
getList,
});
</script>
<template>
<div class="relative">
<!-- 过渡动画--从右向左滑入 -->
<transition name="slide-in">
<div class="bg-white" v-if="['add', 'edit', 'detail'].includes(action)">
<el-page-header @back="action = 'search'">
<template #content>
<span class="text-large font-600 mr-3">
<span v-if="actionDic[action] !== `详情`">{{
actionDic[action]
}}</span>
<span>{{ PageConfig.entityName }}</span>
<span v-if="actionDic[action] === `详情`">{{
actionDic[action]
}}</span>
</span>
</template>
</el-page-header>
<S-form
:Model="Model"
class="m-4"
:PageConfig="PageConfig"
:action="action"
:disabled="action === 'detail'"
:cancel="
() => {
action = 'search';
}
"
:local_save="PageConfig.local_save"
:saveOK="saveOK"
:saveAPI="saveAPI"
v-model="formData"
>
<template
v-for="formItemConfig in propData.form_customFormItemConfigList"
:key="formItemConfig.prop"
#[formItemConfig.prop]
>
<slot :name="formItemConfig.prop" />
</template>
</S-form>
</div>
</transition>
<!-- 过渡动画--向中心缩小消失 -->
<transition name="shrink-to-center">
<div v-show="action === 'search'">
<!-- 搜索表单 -->
<el-form class="mt-2" :inline="true" :model="searchformData">
<el-row :span="24">
<template
v-for="(
formItemConfig, index
) in propData.searchForm_formItemConfigList"
>
<el-col
v-if="!formItemConfig.tableHide && index < 4"
:span="12"
:key="formItemConfig.prop"
class="text-center even:ml-[-60px]!"
>
<el-form-item :label="formItemConfig.label" :label-width="160">
<el-date-picker
v-if="formItemConfig.type === 'date'"
v-model="searchformData![formItemConfig.prop as string]"
type="date"
placeholder="选择日期"
v-bind="formItemConfig"
:class="formItemWidthClass"
/>
<el-date-picker
v-else-if="formItemConfig.type === 'dateRange'"
v-model="searchformData![formItemConfig.prop as string]"
type="daterange"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="shortcuts"
value-format="YYYY-MM-DD"
@change="dateRangeChange($event, formItemConfig.prop)"
:class="formItemWidthClass"
/>
<el-date-picker
v-else-if="formItemConfig.type === 'dateTimeRange'"
v-model="searchformData![formItemConfig.prop as string]"
type="datetimerange"
unlink-panels
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:shortcuts="shortcuts"
value-format="YYYY-MM-DD"
@change="dateRangeChange($event, formItemConfig.prop)"
:class="formItemWidthClass"
/>
<el-tree-select
v-else-if="formItemConfig.type === 'treeSelect'"
v-model="searchformData![formItemConfig.prop as string]"
:data="formItemConfig.treeData"
:render-after-expand="false"
placeholder="请选择"
:class="formItemWidthClass"
filterable
clearable
:node-key="formItemConfig.key"
default-expand-all
/>
<el-input
v-else
v-model="searchformData![formItemConfig.prop as string]"
v-bind="formItemConfig"
:class="formItemWidthClass"
clearable
/>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 查询按钮组 -->
<div class="flex justify-center relative">
<el-button type="primary" @click="getList">查询</el-button>
<el-button type="primary" @click="reset">重置</el-button>
<el-button
v-if="propData.searchForm_formItemConfigList.length > 4"
class="absolute right-4"
type="primary"
text
@click="show_allSearch = true"
>
<span class="text-red" v-if="hasMoreSearchCondition"
>(已添加)</span
>
更多搜索条件 >></el-button
>
</div>
<!-- 全部搜索条件抽屉 -->
<el-drawer v-model="show_allSearch" :direction="direction">
<template #header>
<h4 class="font-bold">全部搜索条件</h4>
</template>
<template #default>
<!-- 搜索表单 -->
<el-form :inline="true" :model="searchformData">
<el-row :span="24">
<template
v-for="formItemConfig in propData.searchForm_formItemConfigList"
>
<el-col
v-if="!formItemConfig.tableHide"
:span="24"
:key="formItemConfig.prop"
>
<el-form-item
:label="formItemConfig.label"
:label-width="100"
>
<el-date-picker
v-if="formItemConfig.type === 'date'"
v-model="searchformData![formItemConfig.prop as string]"
type="date"
placeholder="选择日期"
v-bind="formItemConfig"
:class="formItemWidthClass"
/>
<el-date-picker
v-else-if="formItemConfig.type === 'dateRange'"
v-model="searchformData![formItemConfig.prop as string]"
type="daterange"
unlink-panels
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="shortcuts"
value-format="YYYY-MM-DD"
@change="dateRangeChange($event, formItemConfig.prop)"
:class="formItemWidthClass"
/>
<el-tree-select
v-else-if="formItemConfig.type === 'treeSelect'"
v-model="searchformData![formItemConfig.prop as string]"
:data="formItemConfig.treeData"
:render-after-expand="false"
placeholder="请选择"
:class="formItemWidthClass"
filterable
clearable
:node-key="formItemConfig.key"
default-expand-all
/>
<el-input
v-else
v-model="searchformData![formItemConfig.prop as string]"
v-bind="formItemConfig"
:class="formItemWidthClass"
clearable
/>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</template>
<template #footer>
<div style="flex: auto">
<el-button @click="cancelMoreSearch">取消</el-button>
<el-button type="primary" @click="reset">重置</el-button>
<el-button type="primary" @click="confirmMoreSearch"
>查询</el-button
>
</div>
</template>
</el-drawer>
<!-- 新增按钮 -->
<div class="m-2 mb-4 flex">
<el-button
v-if="!PageConfig.disAdd"
v-permission="`${PageConfig.entity}:add`"
type="primary"
@click="handle_add"
>新增</el-button
>
</div>
<!-- 表格 -->
<el-table
height="360"
:data="tableData"
style="width: 96%"
empty-text="暂无数据"
v-loading="loadData"
>
<el-table-column
v-for="tableColumnConfig in propData.table_columnConfigList"
:key="tableColumnConfig.prop"
v-bind="tableColumnConfig"
:align="tableColumnConfig.align || 'center'"
show-overflow-tooltip
>
<template #default="scope">
<span v-if="tableColumnConfig.tableTransProp">
{{ scope.row[tableColumnConfig.tableTransProp] }}
</span>
<el-switch
v-if="tableColumnConfig.type === 'switch'"
v-model="scope.row[tableColumnConfig.prop]"
:before-change="
() =>
beforeSwitchChange(
scope.row[tableColumnConfig.prop],
tableColumnConfig.prop
)
"
@change="switchChange($event, scope.row._id)"
/>
<slot
v-if="tableColumnConfig.type === 'custom'"
:name="`table_${tableColumnConfig.prop}`"
:row="scope.row"
></slot>
</template>
</el-table-column>
<el-table-column
v-if="!PageConfig.hideHandle"
fixed="right"
label="操作"
min-width="120"
align="center"
>
<template #default="scope">
<el-button
v-if="
!PageConfig.hideDetailBtn &&
!(
PageConfig.entity === 'role' &&
scope.row.name === '超级管理员'
)
"
link
type="primary"
size="small"
@click="handle_detail(scope.row)"
v-permission="`${PageConfig.entity}:detail`"
>详情</el-button
>
<el-button
v-if="
!PageConfig.hideEditBtn &&
!(
PageConfig.entity === 'role' &&
scope.row.name === '超级管理员'
)
"
link
type="primary"
size="small"
@click="handle_edit(scope.row)"
v-permission="`${PageConfig.entity}:edit`"
>编辑</el-button
>
<el-popconfirm
title="确定删除吗?"
@confirm="handle_del(scope.row._id)"
>
<template #reference>
<el-button
v-if="
!PageConfig.hideDelBtn &&
!(
PageConfig.entity === 'role' &&
scope.row.name === '超级管理员'
)
"
link
type="danger"
size="small"
v-permission="`${PageConfig.entity}:del`"
>删除</el-button
>
</template>
</el-popconfirm>
<el-popconfirm
v-if="PageConfig.showResetPasswordBtn"
title="确定重置吗?"
@confirm="handle_resetPassword(scope.row._id)"
>
<template #reference>
<el-button
link
type="danger"
size="small"
v-permission="`${PageConfig.entity}:resetPassword`"
>重置密码</el-button
>
</template>
</el-popconfirm>
<slot name="myHandle" :row="scope.row" />
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20, 30]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
size="small"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</transition>
<S-msgWin :msg="callbackMessage" />
</div>
</template>
<style scoped>
/* 过渡动画 -- 从右向左滑入 */
.slide-in-enter-active {
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
0% {
transform: translateX(100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
/* 过渡动画 -- 向中心缩小消失 */
.shrink-to-center-enter-active {
animation: shrinkToCenter 0.5s reverse;
}
.shrink-to-center-leave-active {
animation: shrinkToCenter 0.5s;
}
@keyframes shrinkToCenter {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0);
opacity: 0;
}
}
</style>
依赖组件
S-form
S-msgWin
页面使用
<S-comMangeInfo :Model="Model" :PageConfig="PageConfig" />
核心传参 Model
配置语法(仅列出了部分核心字段)
// header-align 表头对齐方式 left center right
// search 是否显示搜索框
// tableHide 是否在表格中隐藏
// formHide 是否在表单中隐藏, "all" 时,表单中隐藏,数组时,如["edit", "detail"],则修改和详情不显示
// formDisable 是否在表单中禁用, 数组时,如["edit"],则修改时禁用
// formRules 表单验证规则
// searchFromRules 搜索表单验证规则
// unit 单位
// width 列表的列宽,数字
// defaultValue 表单的默认值
// searchOrder 搜索框的排序
// tableOrder 表格的排序
范例一 岗位管理
const Model: {
[key: string]: any;
} = {
name: {
label: "岗位名称",
search: true,
require: true,
},
EnglishName: {
label: "岗位英文",
search: true,
},
order: {
label: "岗位顺序",
type: "number",
},
desc: {
label: "岗位描述",
tableHide: true,
type: "textarea",
},
createdAt: {
label: "创建时间",
formHide: "all",
},
updatedAt: {
label: "修改时间",
formHide: "all",
},
};
范例二 用户管理
const Model: {
[key: string]: any;
} = {
createdAt: {
width: 150,
formHide: "all",
label: "注册日期",
type: "dateRange",
search: true,
searchOrder: 1,
tableOrder: 2,
},
avatar: {
group: "基本信息",
label: "头像",
type: "custom",
span: 24,
tableHide: true,
},
account: {
group: "账号信息",
label: "账号",
require: true,
formDisable: ["edit"],
search: true,
searchOrder: 3,
tableOrder: 1,
},
password: {
group: "账号信息",
label: "密码",
type: "password",
require: true,
tableHide: true,
formHide: ["edit", "detail"],
},
name: {
group: "基本信息",
label: "姓名",
search: true,
searchOrder: 4,
formRules: [{ min: 2, message: "姓名不能少于 2 个字符", trigger: "blur" }],
},
nickName: {
group: "基本信息",
label: "昵称",
search: true,
},
gender: {
group: "基本信息",
label: "性别",
type: "select",
options: [],
tableHide: true,
},
age: {
group: "基本信息",
label: "年龄",
type: "number",
min: 0,
max: 160,
// 单位
unit: "岁",
tableHide: true,
},
address: {
group: "基本信息",
label: "地址",
tableHide: true,
},
disabled: {
group: "账号信息",
label: "禁用",
type: "switch",
},
roles: {
group: "权限信息",
label: "角色",
type: "select",
options: [],
multSelect: true,
tableHide: true,
},
department: {
group: "权限信息",
label: "部门",
type: "treeSelect",
treeData: [],
// 树的键
key: "code",
search: true,
searchOrder: 2,
// 表格中显示的字段
tableTransProp: "department_desc",
},
positions: {
group: "权限信息",
label: "岗位",
type: "select",
options: [],
multSelect: true,
tableHide: true,
},
};
核心传参 PageConfig
范例一 岗位管理
const PageConfig = {
//被操作实体的名称
entity: "position",
entityName: "岗位",
};
范例二 用户管理
const PageConfig = {
//被操作实体的名称
entity: "user",
entityName: "用户",
// 表单是否分组
formGrouped: true,
// 未命名分组的默认名称
groupName_default: "其他信息",
api: {
//获取详情
detail: "/user/detail",
//新增
add: "/user/add",
//修改
edit: "/user/edit",
},
// 展示重置密码按钮
showResetPasswordBtn: true,
};