proformsmz
v0.3.6
Published
表单组件,函数式弹窗,可组合使用,也可单独使用表单组件
Maintainers
Readme
Prop-Form 表单组件
概述
ProForm 是一个基于 Element Plus 的高级表单组件,支持动态配置、多种数据源、丰富的事件处理和灵活的布局。
安装和引入
安装
pnpm add prop-form@latest --registry http://localhost:4873/引入
import { createApp } from 'vue'
import App from './App.vue'
import PropForm from 'prop-form'
import 'prop-form/dist/prop-form.css'
const app = createApp(App)
app.use(PropForm)
app.mount('#app')
// 按需引入
import { ProForm, useModalForm } from '@your-org/proform'基础用法
<template>
<ProForm
:options="formOptions"
v-model="formData"
@submit="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue';
const formData = ref({
username: '',
status: 1,
department: ''
});
const formOptions = {
form: {
labelWidth: '100px',
rules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
},
columns: [
{
el: 'input',
label: '用户名',
prop: 'username',
props: {
placeholder: '请输入用户名'
}
},
{
el: 'select',
label: '状态',
prop: 'status',
enum: [
{ value: 1, label: '启用' },
{ value: 0, label: '禁用' }
]
}
]
};
const handleSubmit = (data) => {
console.log('表单数据:', data);
};
</script>配置参数
ProForm Props
| 参数 | 类型 | 默认值 | 说明 |
| --- |-----------------|--------| --- |
| modelValue | Object | {} | 表单数据对象 (v-model) |
| data | Object | {} | 表单数据对象 (非响应式备用) |
| cols | number string | 4 | 网格布局列数 |
| options | Object | 必需 | 表单配置选项 |
ProFormOptions
| 属性 | 类型 | 说明
| --- |-----------|-----------------------|
|form | Object | Element Plus Form 配置 |
|columns | IColumn[] | 表单项配置数组 |
|emptyText| string | 空值显示文本 |
|dictConfig| Object | 全局字典配置 |
|on | Object| 全局事件处理器|
IColumn 表单项配置
| 属性 | 类型 | 说明 |
|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------|
| el| string | 元素类型: input, select, radio, checkbox, text, title, custom, button |
| label| string | 标签文本 |
| prop| string | 字段名称 |
| span| number ㇑ string | 跨越列数 | | |
| enum| string ㇑ array | 字典 | | | | object | function 枚举数据配置
|props| Object | Element UI 组件属性 |
|disabled| boolean | 是否禁用 |
|on| Object | 表单项事件处理器 |
|prefixSupplement| string | 值前缀补充 |
|suffixSupplement| string | 值后缀补充 |
|positiveNumber| boolean | 是否只允许正数 |
字典数据配置
- 本地字典(字符串)
{
el: 'select',
label: '用户状态',
prop: 'status',
enum: 'user_status' // 使用本地字典类型
}- 静态数据(数组)
{
el: 'select',
label: '性别',
prop: 'gender',
enum: [
{ value: 1, label: '男' },
{ value: 2, label: '女' }
]
}- API 远程获取(对象配置)
{
el: 'select',
label: '部门',
prop: 'department',
enum: {
api: '/api/departments/list',
params: { active: 1 },
transform: (data) => data.map(item => ({
value: item.id,
label: item.name
}))
}
}- 自定义函数获取
{
el: 'select',
label: '角色',
prop: 'role',
enum: async () => {
const response = await fetch('/api/roles');
const data = await response.json();
return data.map(role => ({
value: role.id,
label: role.name
}));
}
}事件处理
- 组件级别事件
<ProForm
:options="formOptions"
@change="handleChange"
@focus="handleFocus"
@blur="handleBlur"
@click="handleClick"
@validate="handleValidate"
@dict-loaded="handleDictLoaded"
/>- 配置级别事件
const formOptions = {
// ... 其他配置
on: {
change: (event) => {
console.log('表单变更:', event);
},
validate: (result) => {
console.log('验证结果:', result.valid);
}
}
};- 表单项级别事件
{
el: 'input',
label: '用户名',
prop: 'username',
on: {
change: (value, oldValue) => {
console.log('用户名变更:', value, oldValue);
},
focus: (event) => {
console.log('获得焦点');
}
}
}方法 API
通过 ref 调用组件方法:
<template>
<ProForm ref="proFormRef" :options="formOptions" />
<button @click="handleSubmit">提交</button>
</template>
<script setup>
import { ref } from 'vue';
const proFormRef = ref();
const handleSubmit = async () => {
try {
const formData = await proFormRef.value.submitForm();
console.log('提交成功:', formData);
} catch (error) {
console.error('提交失败:', error);
}
};
// 其他可用方法
const refreshDict = () => {
proFormRef.value.refreshDict('department');
};
const validateField = async () => {
const isValid = await proFormRef.value.validateField('username');
console.log('字段验证:', isValid);
};
</script>可用方法
| 方法名 | 说明 | 参数 | 返回值 |
|--------------------|-----------------------|------------------------------------------|-------------------|
| submitForm()| 提交表单验证| - | Promise<Object> |
| resetForm()| 重置表单| - | void |
| clearValidate()| 清除验证| props?: string ㇑ string[] | void |
| refreshDict()| 刷新字典| prop?: string | Promise<void> |
| validate()| 验证表单| props?: string ㇑ string[] | Promise<boolean> |
| validateField()| 验证字段| props: string ㇑ string[] | Promise<boolean> |
|getFormData()| 获取表单数据| - | Object |
|setFormData()| 设置表单数据| data: Object | void |
|resetToInitial()| 重置到初始值| - | void |
布局配置
网格布局
// 4列网格布局
<ProForm :cols="4" :options="formOptions" />
// 自定义列跨度
{
el: 'input',
label: '详细地址',
prop: 'address',
span: 2 // 跨越2列
}
// 标题行
{
el: 'title',
label: '基本信息',
span: 4 // 整行标题
}完整示例
<template>
<div>
<ProForm
ref="proFormRef"
:options="formOptions"
v-model="formData"
@change="handleFormChange"
@validate="handleFormValidate"
/>
<div class="actions">
<el-button @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="refreshDepartment">刷新部门</el-button>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const proFormRef = ref();
const formData = reactive({
name: '',
age: '',
gender: 1,
department: '',
status: 1,
description: ''
});
const formOptions = {
form: {
labelWidth: '120px',
rules: {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ pattern: /^\d+$/, message: '年龄必须为数字', trigger: 'blur' }
]
}
},
columns: [
{
el: 'title',
label: '基本信息',
span: 4
},
{
el: 'input',
label: '姓名',
prop: 'name',
props: {
placeholder: '请输入姓名',
maxlength: 20
},
on: {
change: (value) => {
console.log('姓名变更:', value);
}
}
},
{
el: 'input',
label: '年龄',
prop: 'age',
positiveNumber: true,
props: {
placeholder: '请输入年龄'
}
},
{
el: 'radio',
label: '性别',
prop: 'gender',
enum: [
{ value: 1, label: '男' },
{ value: 2, label: '女' }
]
},
{
el: 'title',
label: '工作信息',
span: 4
},
{
el: 'select',
label: '部门',
prop: 'department',
enum: {
api: '/api/departments',
params: { status: 1 },
transform: (data) => data.map(dept => ({
value: dept.id,
label: dept.name
}))
}
},
{
el: 'select',
label: '状态',
prop: 'status',
enum: 'user_status'
},
{
el: 'textarea',
label: '描述',
prop: 'description',
span: 2,
props: {
rows: 3,
placeholder: '请输入描述信息'
}
}
],
on: {
change: (event) => {
console.log('表单变更事件:', event);
}
}
};
const handleFormChange = (event) => {
console.log('变更:', event.prop, event.value);
};
const handleFormValidate = (result) => {
console.log('验证结果:', result.valid, result.fields);
};
const submitForm = async () => {
try {
const data = await proFormRef.value.submitForm();
console.log('表单数据:', data);
// 调用API提交数据
} catch (error) {
console.error('表单验证失败:', error);
}
};
const resetForm = () => {
proFormRef.value.resetForm();
};
const refreshDepartment = () => {
proFormRef.value.refreshDict('department');
};
</script>
<style scoped>
.actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
</style>类型使用
<template>
<ProForm
ref="proFormRef"
:options="formOptions"
v-model="formData"
@change="handleFormChange"
@validate="handleFormValidate"
/>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import type { ProFormExposed, IFormEvent, IValidateEvent } from 'prop-form/dist/types/prop-form';
interface FormData {
name: string;
age: number | null;
gender: number;
department: string;
status: number;
description: string;
}
const proFormRef = ref<ProFormExposed>();
const formData = reactive<FormData>({
name: '',
age: null,
gender: 1,
department: '',
status: 1,
description: ''
});
const formOptions = {
form: {
labelWidth: '120px',
rules: {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ pattern: /^\d+$/, message: '年龄必须为数字', trigger: 'blur' }
]
}
},
columns: [
{
el: 'input',
label: '姓名',
prop: 'name',
props: {
placeholder: '请输入姓名',
maxlength: 20
}
},
{
el: 'input',
label: '年龄',
prop: 'age',
positiveNumber: true
}
] as const
};
const handleFormChange = (event: IFormEvent): void => {
console.log('字段变更:', event.prop, event.value);
};
const handleFormValidate = (event: IValidateEvent): void => {
console.log('验证结果:', event.valid);
};
const submitForm = async (): Promise<void> => {
try {
const data = await proFormRef.value?.submitForm();
console.log('提交成功:', data);
} catch (error) {
console.error('提交失败:', error);
}
};
</script>高级功能
- 自定义渲染
{
el: 'custom',
label: '自定义区域',
prop: 'customField',
span: 2,
// 使用插槽自定义内容
// <template #item-customField>自定义内容</template>
} <PropForm class="flex-col" ref="proFormRef" :options="formConfig" :data="formData" :cols="2">
<template #item-qualificationsIamge>
<el-upload
ref="uploadRef2"
class="upload-demo flex-1"
action="#"
:auto-upload="false"
:on-change="onChangeFile2"
:on-remove="onRemove"
:on-preview="onPoreviewFile"
v-model:file-list="fileListModel.qualificationsIamge"
>
<template #trigger>
<el-button plain class="filterButton">
<svg class="icon" width="16" height="16" aria-hidden="true">
<use xlink:href="#icon-uploadIcon" />
</svg>
上传文件
</el-button>
</template>
</el-upload>
</template>
<template #item-Iamge>
<el-upload
ref="uploadRef"
action="#"
list-type="picture-card"
:auto-upload="false"
:file-list="fileLists"
accept=".jpg,.jpeg,.png,.gif"
:on-change="handleFileChange"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
</el-upload>
</template>
</PropForm>- 条件显示
// 根据其他字段值动态显示/隐藏
const shouldShowField = computed(() => {
return formData.value.type === 'special';
});- 动态验证规则
const dynamicRules = computed(() => {
return {
email: formData.value.receiveNewsletter ? [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
] : []
};
});函数式调用弹窗
与表单组件绑定,抛弃传统的书写标签,使用函数式调用弹窗,包括一些事件处理,符合思维逻辑。
- 引入和使用
import { useModalForm, createModalForm } from 'proform'
const openModalWithoutBoth = () => {
console.log('开始打开弹窗...')
const modal = useModalForm({
title: '弹窗',
width: '500px', // 明确设置宽度
formOptions: {
columns: [
{
el: 'input',
label: '名称',
prop: 'name',
props: {
placeholder: '请输入名称'
}
}
]
},
// defaultButtons: false
})
console.log('useModalForm 返回:', modal)
const closeFn = modal.open();
console.log('open() 返回:', closeFn)
}- 配置参数
| 参数 | 类型 | 默认值 | 说明 |
|----------------------------------------|-------------------------------|----------|---------------|
| title| string | '表单' | 弹窗标题 |
| width| string ㇑ number | '600px' | 弹窗宽度|
| formOptions| ProFormOptions | 必需 |表单配置 |
| formData| Record<string, any> | {} | 初始表单数据 |
| formProps| any| {} | 表单额外属性 |
| buttons | ModalButton[]| [] | 自定义按钮 |
| defaultButtons | boolean | true | 是否显示默认按钮 |
| showCancel| boolean| true | 是否显示取消按钮 |
| showConfirm| boolean| true | 是否显示确定按钮 |
| cancelText| string| '取消' | 取消按钮文字 |
| confirmText | string| '确定' | 确定按钮文字 |
| footerPosition| 'left' ㇑ 'center' ㇑ 'right' | 'right' | 按钮位置|
| beforeClose| () => Promise<boolean> ㇑ boolean -| 关闭前回调 |
- 事件处理
const modal = useModalForm({
title: '带事件处理的表单',
formOptions: {
columns: [
{ el: 'input', label: '测试', prop: 'test' }
]
},
// 确认事件
onConfirm: (data, formApi) => {
console.log('表单数据:', data)
// 调用 API 保存数据
return api.saveData(data).then(() => {
message.success('保存成功')
})
},
// 取消事件
onCancel: () => {
console.log('用户取消了操作')
},
// 按钮点击事件
onButtonClick: (button, data, formApi) => {
console.log('按钮点击:', button.key, data)
}
})高级用法
- 自定义按钮
const modal = useModalForm({
title: '自定义按钮表单',
defaultButtons: false, // 禁用默认按钮
buttons: [
{
key: 'preview',
text: '预览',
props: { type: 'info', icon: 'View' },
onClick: (data, formApi) => {
console.log('预览数据:', data)
}
},
{
key: 'save',
text: '保存草稿',
props: { type: 'warning' },
validate: false, // 不需要验证表单
onClick: (data, formApi) => {
return api.saveDraft(data)
}
},
{
key: 'submit',
text: '提交审核',
props: { type: 'primary' },
validate: true, // 需要验证表单
onClick: (data, formApi) => {
return api.submit(data)
}
}
]
})- 隐藏默认按钮
// 只显示确定按钮
const modal1 = useModalForm({
showCancel: false,
confirmText: '我知道了'
})
// 只显示取消按钮
const modal2 = useModalForm({
showConfirm: false,
cancelText: '关闭'
})
// 完全自定义按钮
const modal3 = useModalForm({
defaultButtons: false,
buttons: [
{ text: '自定义操作', props: { type: 'primary' } }
]
})- 表单验证和处理
const modal = useModalForm({
title: '带验证的表单',
formOptions: {
columns: [
{
el: 'input',
label: '手机号',
prop: 'phone',
rules: [
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
]
},
{
el: 'input',
label: '年龄',
prop: 'age',
positiveNumber: true, // 只允许正数
rules: [{ min: 18, max: 60, message: '年龄必须在18-60之间' }]
}
]
},
onConfirm: async (data, formApi) => {
// 手动验证
const isValid = await formApi.validate()
if (!isValid) {
throw new Error('表单验证失败')
}
// 获取表单数据
const formData = formApi.getFormData()
return api.submit(formData)
}
})- 动态表单数据
const modal = useModalForm({
title: '动态表单',
formOptions: {
columns: [
{
el: 'select',
label: '分类',
prop: 'category',
enum: async () => {
// 动态加载选项
const categories = await api.loadCategories()
return categories.map(item => ({
value: item.id,
label: item.name
}))
}
}
]
}
})
// 打开时传入动态数据
modal.open({ category: 'default' })ModalFormInstance 方法
| 方法 | 参数 | 返回值 | 说明 | |---------------------------|-------------------------|-------------------------|--------------| | open| data?: Record<string, any>| () => void| 打开弹窗,返回关闭函数 | | close| -| void | 关闭弹窗 | | getFormApi| - | ProFormExposed ㇑ undefined| 获取表单API实例 | |getFormData| - |Record<string, any>| 获取表单数据 |
插槽
多种上传类型示例
import { useModalForm } from '@your-org/proform'
import { h } from 'vue'
import { ElMessage } from 'element-plus'
const modal = useModalForm({
title: '多类型上传示例',
formOptions: {
columns: [
{
el: 'custom',
label: '资质文件',
prop: 'qualificationsIamge'
},
{
el: 'custom',
label: '图片上传',
prop: 'Iamge'
},
{
el: 'custom',
label: '其他文件',
prop: 'otherFiles'
}
]
},
slots: {
// 文件上传插槽
'item-qualificationsIamge': {
type: 'file',
buttonText: '上传资质文件',
buttonIcon: '#icon-certificate',
accept: '.pdf,.doc,.docx',
limit: 5,
onChange: (file, fieldName) => {
ElMessage.success(`资质文件 ${file.name} 上传成功`)
}
},
// 图片上传插槽
'item-Iamge': {
type: 'image',
listType: 'picture-card',
accept: '.jpg,.jpeg,.png,.gif',
limit: 10,
onPreview: (file, fieldName) => {
window.open(file.url, '_blank')
}
},
// 自定义上传插槽
'item-otherFiles': {
type: 'file',
buttonText: '选择其他文件',
multiple: true,
onChange: (file, fieldName) => {
console.log('文件变更:', file, fieldName)
}
},
// 完全自定义的插槽(非上传类型)
'custom-footer': () => h('div', { class: 'custom-footer' }, [
h('button', {
onClick: () => console.log('自定义操作'),
class: 'custom-btn'
}, '自定义按钮')
]),
// 另一个自定义插槽
'custom-header': () => h('div', { class: 'custom-header' }, [
h('h3', '自定义标题'),
h('p', '这是自定义头部内容')
])
}
})
// 打开弹窗
modal.open({
qualificationsIamge: [],
Iamge: [],
otherFiles: []
})更复杂的示例
import { useModalForm } from '@your-org/proform'
import { h } from 'vue'
const modal = useModalForm({
title: '企业信息登记',
formOptions: {
columns: [
{ el: 'input', label: '企业名称', prop: 'companyName' },
{ el: 'custom', label: '营业执照', prop: 'businessLicense' },
{ el: 'custom', label: '产品图片', prop: 'productImages' },
{ el: 'custom', label: '资质证书', prop: 'certificates' }
]
},
slots: {
// 营业执照上传
'item-businessLicense': {
type: 'image',
listType: 'picture-card',
accept: '.jpg,.jpeg,.png',
limit: 1,
buttonText: '上传营业执照',
onChange: (file, fieldName) => {
console.log('营业执照上传:', file)
}
},
// 产品图片上传
'item-productImages': {
type: 'image',
listType: 'picture',
multiple: true,
limit: 20,
buttonText: '上传产品图片',
buttonIcon: '#icon-image',
onChange: (file, fieldName) => {
console.log('产品图片上传:', file)
}
},
// 资质证书上传
'item-certificates': {
type: 'file',
accept: '.pdf,.jpg,.png',
limit: 10,
buttonText: '上传资质证书',
buttonProps: {
type: 'primary',
plain: false,
class: 'certificate-btn'
},
onChange: (file, fieldName) => {
console.log('资质证书上传:', file)
}
},
// 自定义底部按钮
'custom-operation': () => h('div', { class: 'custom-operations' }, [
h('button', {
class: 'btn-save-draft',
onClick: () => console.log('保存草稿')
}, '保存草稿'),
h('button', {
class: 'btn-submit',
onClick: () => console.log('提交审核')
}, '提交审核')
])
}
})
// 使用
modal.open({
companyName: '测试企业',
businessLicense: [],
productImages: [],
certificates: []
})动态配置插槽
// 动态生成插槽配置
function createDynamicSlots(fields) {
const slots = {}
fields.forEach(field => {
if (field.type === 'file') {
slots[`item-${field.name}`] = {
type: 'file',
buttonText: field.buttonText || `上传${field.label}`,
accept: field.accept,
limit: field.limit,
onChange: field.onChange
}
} else if (field.type === 'image') {
slots[`item-${field.name}`] = {
type: 'image',
listType: field.listType || 'picture-card',
buttonText: field.buttonText || `上传${field.label}`,
limit: field.limit,
onPreview: field.onPreview
}
}
})
return slots
}
// 使用动态配置
const dynamicFields = [
{ name: 'qualificationsIamge', type: 'file', label: '资质文件', accept: '.pdf', limit: 5 },
{ name: 'Iamge', type: 'image', label: '图片', listType: 'picture-card', limit: 10 }
]
const modal = useModalForm({
formOptions: {
columns: dynamicFields.map(field => ({
el: 'custom',
label: field.label,
prop: field.name
}))
},
slots: createDynamicSlots(dynamicFields)
})注意事项
- 请确保在项目中正确引入了
element-plus和vue。 - 性能优化:对于大型表单,建议使用 deep: false 监听选项
- 字典加载:远程字典加载时有 loading 状态显示
- 事件冒泡:事件会从表单项 → 配置 → 组件级别依次触发
常见问题
Q: 字典数据不显示怎么办? A: 检查字典配置是否正确,网络请求是否成功,控制台是否有错误信息。
Q: 表单验证不生效? A: 确保在 options.form.rules 中正确配置了验证规则。
Q: 如何动态更新表单选项? A: 直接修改 formOptions.columns 数组,组件会响应式更新。
Q: 如何获取表单实例? A: 使用 ref 获取组件实例,然后调用暴露的方法。
