feat: pageModal组件,添加二级弹窗与使用案例

This commit is contained in:
超凡
2025-04-21 22:12:58 +08:00
parent 9e47f25520
commit 4b1614952b
7 changed files with 331 additions and 229 deletions

View File

@@ -503,19 +503,24 @@ function handleDelete(id?: number | string) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(function () {
if (props.contentConfig.deleteAction) {
props.contentConfig.deleteAction(ids).then(() => {
ElMessage.success("删除成功");
removeIds.value = [];
//清空选中项
tableRef.value?.clearSelection();
handleRefresh(true);
});
} else {
ElMessage.error("未配置deleteAction");
}
});
})
.then(function () {
if (props.contentConfig.deleteAction) {
props.contentConfig
.deleteAction(ids)
.then(() => {
ElMessage.success("删除成功");
removeIds.value = [];
//清空选中项
tableRef.value?.clearSelection();
handleRefresh(true);
})
.catch(() => {});
} else {
ElMessage.error("未配置deleteAction");
}
})
.catch(() => {});
}
// 导出表单
@@ -777,7 +782,14 @@ function handleToolbar(name: string) {
// 操作列
function handleOperate(data: IOperateData) {
emit("operateClick", data);
switch (data.name) {
case "delete":
props.contentConfig?.deleteAction && handleDelete(data.row[pk]);
break;
default:
emit("operateClick", data);
break;
}
}
// 属性修改

View File

@@ -1,147 +1,151 @@
<template>
<!-- drawer -->
<template v-if="modalConfig.component === 'drawer'">
<el-drawer
v-model="modalVisible"
v-bind="{ destroyOnClose: true, ...modalConfig.drawer }"
@close="handleClose"
>
<!-- 表单 -->
<el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
<el-row :gutter="20">
<template v-for="item in formItems" :key="item.prop">
<el-col v-show="!item.hidden" v-bind="item.col">
<el-form-item :label="item.label" :prop="item.prop">
<!-- Label -->
<template #label>
<span>
{{ item?.label || "" }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
<span v-if="modalConfig.colon" class="ml-0.5">:</span>
</span>
</template>
<!-- components -->
<template v-if="item.type === 'custom'">
<slot
:name="item.slotName ?? item.prop"
:prop="item.prop"
:form-data="formData"
:attrs="item.attrs"
style="width: 100%"
></slot>
</template>
<component
:is="componentMap.get(item.type)"
v-else
v-model.trim="formData[item.prop]"
v-bind="{ style: { width: '100%' }, ...item.attrs }"
>
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
<component
:is="childrenMap.get(item.type)"
v-for="opt in item.options"
:label="opt.label"
:value="opt.value"
></component>
<div>
<!-- drawer -->
<template v-if="modalConfig.component === 'drawer'">
<el-drawer
v-model="modalVisible"
v-bind="{ destroyOnClose: true, ...modalConfig.drawer }"
@close="handleClose"
>
<!-- 表单 -->
<el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
<el-row :gutter="20">
<template v-for="item in formItems" :key="item.prop">
<el-col v-show="!item.hidden" v-bind="item.col">
<el-form-item :label="item.label" :prop="item.prop">
<!-- Label -->
<template #label>
<span>
{{ item?.label || "" }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
<span v-if="modalConfig.colon" class="ml-0.5">:</span>
</span>
</template>
<template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
<slot :name="item.slotName" />
<!-- components -->
<template v-if="item.type === 'custom'">
<slot
:name="item.slotName ?? item.prop"
:prop="item.prop"
:form-data="formData"
:attrs="item.attrs"
></slot>
</template>
</component>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 弹窗底部操作按钮 -->
<template #footer>
<el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleClose">{{ !formDisable ? " " : "关闭" }}</el-button>
</template>
</el-drawer>
</template>
<!-- dialog -->
<template v-else>
<el-dialog
v-model="modalVisible"
v-bind="{ destroyOnClose: true, appendToBody: true, ...modalConfig.dialog }"
@close="handleClose"
>
<!-- 表单 -->
<el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
<el-row :gutter="20">
<template v-for="item in formItems" :key="item.prop">
<el-col v-show="!item.hidden" v-bind="item.col">
<el-form-item :label="item.label" :prop="item.prop">
<!-- Label -->
<template #label>
<span class="flex-y-center">
{{ item?.label || "" }}
<el-tooltip v-if="item?.tips">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
<span v-if="modalConfig.colon" class="ml-0.5">:</span>
</span>
</template>
<component
:is="componentMap.get(item.type)"
v-else
v-model.trim="formData[item.prop]"
v-bind="{ style: { width: '100%' }, ...item.attrs }"
>
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
<component
:is="childrenMap.get(item.type)"
v-for="opt in item.options"
:label="opt.label"
:value="opt.value"
></component>
</template>
<!-- components -->
<template v-if="item.type === 'custom'">
<slot
:name="item.slotName ?? item.prop"
:prop="item.prop"
:form-data="formData"
:attrs="item.attrs"
></slot>
</template>
<component
:is="componentMap.get(item.type)"
v-else
v-model.trim="formData[item.prop]"
v-bind="{ style: { width: '100%' }, ...item.attrs }"
>
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
<component
:is="childrenMap.get(item.type)"
v-for="opt in item.options"
:label="opt.label"
:value="opt.value"
>
<slot v-if="item.slotName" :name="item.slotName"></slot>
</component>
<template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
<slot :name="item.slotName" />
</template>
</component>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 弹窗底部操作按钮 -->
<template #footer>
<el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleClose">{{ !formDisable ? " " : "关闭" }}</el-button>
</template>
</el-drawer>
</template>
<!-- dialog -->
<template v-else>
<el-dialog
v-model="modalVisible"
v-bind="{ destroyOnClose: true, ...modalConfig.dialog }"
@close="handleClose"
>
<!-- 表单 -->
<el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
<el-row :gutter="20">
<template v-for="item in formItems" :key="item.prop">
<el-col v-show="!item.hidden" v-bind="item.col">
<el-form-item :label="item.label" :prop="item.prop">
<!-- Label -->
<template #label>
<span>
{{ item?.label || "" }}
<el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
<span v-if="modalConfig.colon" class="ml-0.5">:</span>
</span>
</template>
</component>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 弹窗底部操作按钮 -->
<template #footer>
<el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleClose">{{ !formDisable ? " " : "关闭" }}</el-button>
</template>
</el-dialog>
</template>
<!-- components -->
<template v-if="item.type === 'custom'">
<slot
:name="item.slotName ?? item.prop"
:prop="item.prop"
:form-data="formData"
:attrs="item.attrs"
></slot>
</template>
<component
:is="componentMap.get(item.type)"
v-else
v-model.trim="formData[item.prop]"
v-bind="{ style: { width: '100%' }, ...item.attrs }"
>
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
<component
:is="childrenMap.get(item.type)"
v-for="opt in item.options"
:label="opt.label"
:value="opt.value"
></component>
</template>
<template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
<slot :name="item.slotName" />
</template>
</component>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
<!-- 弹窗底部操作按钮 -->
<template #footer>
<el-button v-if="!formDisable" type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleClose">{{ !formDisable ? " " : "关闭" }}</el-button>
</template>
</el-dialog>
</template>
</div>
</template>
<script setup lang="ts">
import { useThrottleFn } from "@vueuse/core";
import type { FormInstance, FormRules } from "element-plus";
import type { IModalConfig, IObject } from "./types";
import type { IComponentType, IModalConfig, IObject } from "./types";
import InputTag from "@/components/InputTag/index.vue";
import IconSelect from "@/components/IconSelect/index.vue";
defineSlots<{ [key: string]: (_args: any) => any }>();
// 定义接收的属性
const props = defineProps<{ modalConfig: IModalConfig }>();
// 自定义事件
const emit = defineEmits<{ submitClick: [] }>();
const emit = defineEmits<{ submitClick: []; customSubmit: [queryParams: IObject] }>();
// 组件映射表
/* eslint-disable */
const componentMap = new Map([
const componentMap = new Map<IComponentType, any>([
// @ts-ignore
["input", markRaw(ElInput)], // @ts-ignore
["select", markRaw(ElSelect)], // @ts-ignore
@@ -160,7 +164,7 @@ const componentMap = new Map([
["icon-select", markRaw(IconSelect)], // @ts-ignore"
["custom", ""],
]);
const childrenMap = new Map([
const childrenMap = new Map<IComponentType, any>([
// @ts-ignore
["select", markRaw(ElOption)], // @ts-ignore
["radio", markRaw(ElRadio)], // @ts-ignore"
@@ -168,13 +172,13 @@ const childrenMap = new Map([
]);
/* eslint-enable */
const pk = props.modalConfig.pk ?? "id";
const modalVisible = ref(false);
const formRef = ref<FormInstance>();
const formItems = reactive(props.modalConfig.formItems ?? []);
const formData = reactive<IObject>({});
const formRules: FormRules = {};
const formDisable = ref(false);
const pk = props.modalConfig.pk ?? "id"; // 主键名,用于表单数据处理
const modalVisible = ref(false); // 弹窗显示状态
const formRef = ref<FormInstance>(); // 表单实例
const formItems = reactive(props.modalConfig.formItems ?? []); // 表单配置项
const formData = reactive<IObject>({}); // 表单数据
const formRules: FormRules = {}; // 表单验证规则
const formDisable = ref(false); // 表单禁用状态
// 获取tooltip提示框属性
const getTooltipProps = (tips: string | IObject) => {
@@ -204,17 +208,16 @@ const handleSubmit = useThrottleFn(() => {
props.modalConfig.beforeSubmit(formData);
}
if (!props.modalConfig?.formAction) {
ElMessage.error("未配置exportAction");
emit("customSubmit", formData);
handleClose();
return;
}
props.modalConfig.formAction(formData).then(() => {
let msg = "操作成功";
if (props.modalConfig.component === "drawer") {
msg = `${props.modalConfig.drawer?.title}成功`;
ElMessage.success(`${props.modalConfig.drawer?.title}成功`);
} else {
msg = `${props.modalConfig.dialog?.title}成功`;
ElMessage.success(`${props.modalConfig.dialog?.title}成功`);
}
ElMessage.success(msg);
emit("submitClick");
handleClose();
});

View File

@@ -13,11 +13,11 @@ export type PageFormInstance = InstanceType<typeof PageForm>;
export type IObject = Record<string, any>;
type DataComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag";
type DateComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag";
type InputComponent = "input" | "select" | "input-number" | "cascader" | "tree-select";
type OtherComponent = "text" | "radio" | "checkbox" | "switch" | "icon-select" | "custom";
export type ISearchComponent = DataComponent | InputComponent;
export type IComponentType = DataComponent | InputComponent | OtherComponent;
export type ISearchComponent = DateComponent | InputComponent;
export type IComponentType = DateComponent | InputComponent | OtherComponent;
type ToolbarLeft = "add" | "delete" | "import" | "export";
type ToolbarRight = "refresh" | "filter" | "imports" | "exports" | "search";
@@ -44,26 +44,7 @@ export interface ISearchConfig {
// 标签冒号(默认false)
colon?: boolean;
// 表单项(默认:[])
formItems?: Array<{
// 组件类型(如input,select等)
type?: ISearchComponent;
// 标签文本
label?: string;
// 标签提示
tips?: string | IObject;
// 键名
prop: string;
// 组件属性(input-tag组件支持join,btnText,size属性)
attrs?: IObject;
// 初始值
initialValue?: any;
// 可选项(适用于select组件)
options?: Array<{ label: string; value: any }>;
// 组件事件
events?: Record<string, (...args: any) => void>;
// 初始化数据函数扩展
initFn?: (formItem: IObject) => void;
}>;
formItems?: IFormItems;
// 是否开启展开和收缩(默认true)
isExpandable?: boolean;
// 默认展示的表单项数量(默认3)
@@ -199,36 +180,36 @@ export interface IModalConfig<T = any> {
// 提交之前处理
beforeSubmit?: (data: T) => void;
// 提交的网络请求函数(需返回promise)
formAction: (data: T) => Promise<any>;
formAction?: (data: T) => Promise<any>;
}
export type IForm = Partial<Omit<FormProps, "model" | "rules">>;
// 表单项
export type IFormItems = Array<{
// 组件类型(如input,select,radio,custom等默认input)
// 组件类型(如input,select,radio,custom等)
type: IComponentType;
// 标签提示
tips?: string | IObject;
// 标签文本
label: string;
// 键名
prop: string;
// 组件属性
attrs?: IObject;
// 组件可选项(适用于select,radio,checkbox组件)
// 组件可选项(适用于select,radio,checkbox组件)
options?: Array<{
label: string;
value: any;
disabled?: boolean;
[key: string]: any;
}>;
// 插槽名(适用于组件类型为custom)
slotName?: string;
// 标签文本
label: string;
// 标签提示
tips?: string | IObject;
// 键名
prop: string;
// 验证规则
rules?: FormItemRule[];
// 初始值
initialValue?: any;
// 插槽名(适用于自定义组件设置类型为custom)
slotName?: string;
// 是否隐藏
hidden?: boolean;
// layout组件Col属性

View File

@@ -18,23 +18,50 @@ function usePage() {
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
}
// 新增
function handleAddClick() {
//显示添加表单
addModalRef.value?.setModalVisible();
function handleAddClick(RefImpl?: Ref<PageContentInstance>) {
if (RefImpl) {
RefImpl?.value.setModalVisible();
RefImpl?.value.handleDisabled(false);
} else {
addModalRef.value?.setModalVisible();
addModalRef.value?.handleDisabled(false);
}
}
// 编辑
async function handleEditClick(row: IObject, callback?: (result?: IObject) => IObject) {
editModalRef.value?.setModalVisible();
editModalRef.value?.handleDisabled(false);
let from = await (callback?.(row) ?? Promise.resolve(row));
editModalRef.value?.setFormData(from ? from : row);
async function handleEditClick(
row: IObject,
callback?: (result?: IObject) => IObject,
RefImpl?: Ref<PageContentInstance>
) {
if (RefImpl) {
RefImpl.value?.setModalVisible();
RefImpl.value?.handleDisabled(false);
let from = await (callback?.(row) ?? Promise.resolve(row));
RefImpl.value?.setFormData(from ? from : row);
} else {
editModalRef.value?.setModalVisible();
editModalRef.value?.handleDisabled(false);
let from = await (callback?.(row) ?? Promise.resolve(row));
editModalRef.value?.setFormData(from ? from : row);
}
}
// 编辑
async function handleViewClick(row: IObject, callback?: (result?: IObject) => IObject) {
editModalRef.value?.setModalVisible();
editModalRef.value?.handleDisabled(true);
let from = await (callback?.(row) ?? Promise.resolve(row));
editModalRef.value?.setFormData(from ? from : row);
// 查看
async function handleViewClick(
row: IObject,
callback?: (result?: IObject) => IObject,
RefImpl?: Ref<PageContentInstance>
) {
if (RefImpl) {
RefImpl.value?.setModalVisible();
RefImpl.value?.handleDisabled(true);
let from = await (callback?.(row) ?? Promise.resolve(row));
RefImpl.value?.setFormData(from ? from : row);
} else {
editModalRef.value?.setModalVisible();
editModalRef.value?.handleDisabled(true);
let from = await (callback?.(row) ?? Promise.resolve(row));
editModalRef.value?.setFormData(from ? from : row);
}
}
// 表单提交
function handleSubmitClick() {

View File

@@ -1,5 +1,7 @@
import UserAPI, { type UserForm } from "@/api/system/user.api";
import type { IModalConfig } from "@/components/CURD/types";
import DeptAPI from "@/api/system/dept.api";
import RoleAPI from "@/api/system/role.api";
const modalConfig: IModalConfig<UserForm> = {
permPrefix: "sys:user",
@@ -45,7 +47,7 @@ const modalConfig: IModalConfig<UserForm> = {
{
label: "所属部门",
prop: "deptId",
rules: [{ required: true, message: "所属部门不能为空", trigger: "blur" }],
rules: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
type: "tree-select",
attrs: {
placeholder: "请选择所属部门",
@@ -54,17 +56,22 @@ const modalConfig: IModalConfig<UserForm> = {
"check-strictly": true,
"render-after-expand": false,
},
async initFn(formItem) {
// 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
formItem.attrs.data = await DeptAPI.getOptions();
},
},
{
type: "custom",
label: "性别",
prop: "gender",
initialValue: 1,
attrs: { style: { width: "100%" } },
},
{
label: "角色",
prop: "roleIds",
rules: [{ required: true, message: "用户角色不能为空", trigger: "blur" }],
rules: [{ required: true, message: "用户角色不能为空", trigger: "change" }],
type: "select",
attrs: {
placeholder: "请选择",
@@ -72,6 +79,9 @@ const modalConfig: IModalConfig<UserForm> = {
},
options: [],
initialValue: [],
async initFn(formItem) {
formItem.options = await RoleAPI.getOptions();
},
},
{
type: "input",
@@ -115,6 +125,12 @@ const modalConfig: IModalConfig<UserForm> = {
],
initialValue: 1,
},
{
type: "custom",
label: "二级弹窗",
prop: "openModal",
slotName: "openModal",
},
],
};

View File

@@ -0,0 +1,59 @@
import { type UserForm } from "@/api/system/user.api";
import type { IModalConfig } from "@/components/CURD/types";
import DeptAPI from "@/api/system/dept.api";
const modalConfig: IModalConfig<UserForm> = {
colon: true,
dialog: {
title: "二级弹窗",
width: 500,
draggable: true,
},
form: {
labelWidth: "auto",
labelPosition: "top",
},
formItems: [
{
label: "用户名",
prop: "username",
rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
type: "input",
attrs: { placeholder: "请输入" },
},
{
label: "用户昵称",
prop: "nickname",
rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
type: "input",
attrs: { placeholder: "请输入" },
},
{
label: "所属部门",
prop: "deptId",
rules: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
type: "tree-select",
attrs: {
placeholder: "请选择",
data: [],
filterable: true,
"check-strictly": true,
"render-after-expand": false,
},
async initFn(formItem) {
// 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
formItem.attrs.data = await DeptAPI.getOptions();
},
},
{
type: "custom",
label: "性别",
prop: "gender",
initialValue: 1,
attrs: { style: { width: "100%" } },
},
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -57,7 +57,17 @@
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<Dict v-model="scope.formData[scope.prop]" code="gender" />
<Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template>
<template #openModal>
<el-button type="primary" @click="openSecondModal">打开二级弹窗</el-button>
</template>
</page-modal>
<!-- 二级弹窗 -->
<page-modal ref="addModalRef2" :modal-config="addModalConfig2" @custom-click="secondSubmit">
<template #gender="scope">
<Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template>
</page-modal>
@@ -104,9 +114,10 @@
<script setup lang="ts">
import UserAPI from "@/api/system/user.api";
import type { IObject, IOperateData } from "@/components/CURD/types";
import type { IObject, IOperateData, PageContentInstance } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";
import addModalConfig2 from "./config/add2";
import contentConfig from "./config/content";
import contentConfig2 from "./config/content2";
import editModalConfig from "./config/edit";
@@ -121,7 +132,7 @@ const {
editModalRef,
handleQueryClick,
handleResetClick,
// handleAddClick,
handleAddClick,
handleEditClick,
handleViewClick,
handleSubmitClick,
@@ -130,12 +141,8 @@ const {
handleFilterChange,
} = usePage();
// 新增
async function handleAddClick() {
addModalRef.value?.setModalVisible();
// 加载下拉数据源,建议在初始化配置项 initFn 中加载,避免多次请求
// addModalConfig.formItems[2]!.attrs!.data = await DeptAPI.getOptions();
}
const addModalRef2 = ref();
// 其他工具栏
function handleToolbarClick(name: string) {
console.log(name);
@@ -162,25 +169,14 @@ const handleOperateClick = (data: IObject) => {
ElMessageBox.prompt("请输入用户「" + data.row.username + "」的新密码", "重置密码", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(({ value }) => {
if (!value || value.length < 6) {
ElMessage.warning("密码至少需要6位字符请重新输入");
return false;
}
UserAPI.resetPassword(data.row.id, value).then(() => {
ElMessage.success("密码重置成功,新密码是:" + value);
});
});
} else if (data.name === "delete") {
ElMessageBox.confirm("确认删除?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(function () {
UserAPI.deleteByIds(data.row.id).then(() => {
ElMessage.success("删除成功");
contentRef.value?.handleRefresh(true); // 刷新列表
.then(({ value }) => {
if (!value || value.length < 6) {
ElMessage.warning("密码至少需要6位字符请重新输入");
return false;
}
UserAPI.resetPassword(data.row.id, value).then(() => {
ElMessage.success("密码重置成功,新密码是:" + value);
});
})
.catch(() => {});
@@ -198,6 +194,14 @@ const handleOperateClick2 = (data: IOperateData) => {
}
};
// 打开二级弹窗
const openSecondModal = () => {
handleAddClick(addModalRef2 as Ref<PageContentInstance>);
};
const secondSubmit = () => {
ElMessage.success("二级弹窗提交成功");
};
// 切换示例
const isA = ref(true);
</script>