一键导入
el-plus-crud-form
Data-driven form component with 40+ field types, dynamic visibility, multi-column layout, submit lifecycle hooks, and field-level linking
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Data-driven form component with 40+ field types, dynamic visibility, multi-column layout, submit lifecycle hooks, and field-level linking
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Configuration system and TypeScript types for el-plus-crud — ICRUDConfig, IFormDesc, ITableConfig, provide/inject dependency injection
Dialog wrapper for ElPlusForm and ElPlusFormGroup with auto open/close, submit handling, and table reload coordination
Multi-section grouped form with independent validation per group, shared submit lifecycle, and dynamic group visibility
Proven patterns and recipes for common CRUD scenarios — complete CRUD page, search-reset workflow, master-detail, custom components, and permission control
Vue 3 + Element Plus data-driven CRUD component library — entry skill for all components and configuration
Data-driven table component with multi-level headers, pagination, tabs, row selection, export, summary rows, and tree expansion
| name | el-plus-crud-form |
| description | Data-driven form component with 40+ field types, dynamic visibility, multi-column layout, submit lifecycle hooks, and field-level linking |
| version | 1.0.0 |
| origin | el-plus-crud |
| tags | ["vue3","element-plus","form","data-driven","dynamic-form"] |
ElPlusForm is the core form component. Declare form fields through an IFormDesc object, supporting 40+ built-in field types, multi-column layout, dynamic visibility (vif/disabled as functions), field linking, custom component extension, and a complete submit lifecycle.
<ElPlusForm> componentIFormDesc / IFormDescItem objectssubmit / validate / reset / clear / getDataKey = field name (matches backend field), value = IFormDescItem.
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名', required: true },
phone: { type: 'input', label: '手机号', rules: 'phone' },
role: { type: 'select', label: '角色', options: roleList }
}
| Category | Types |
|---|---|
| Input | input, number, nbinput, password, textarea, quickinput, percentinput |
| Selection | select, radio, checkbox, checkboxbutton, switch, slider, rate, transfer |
| Date/Time | date, datetime, daterange, datetimerange, time, timerange |
| Upload | upload, image, file, upbtn |
| Tree/Cascade | tree, tselect (tree-select), cascader, cascaderpanel |
| Display | text, tag, status, link, rate, color |
| Advanced | area, autocomplete, lkuser, formula |
| Action | btn, btns |
These properties accept functions for reactive behavior:
| Property | Function Signature | Description |
|---|---|---|
vif | (formData, fieldValue) => boolean | Show/hide field |
disabled | (formData, fieldValue) => boolean | Enable/disable field |
label | (formData, fieldValue) => string | Dynamic label |
attrs | (formData, fieldValue) => object | Dynamic attributes |
tip | (formData, fieldValue) => string | Dynamic hint text |
style | (formData, fieldValue) => object | Dynamic styles |
required | (formData) => boolean | Dynamic mandatory |
When any property is a function, the form automatically enables deep watching on modelValue (isOpenListen = true).
Uses a 24-grid system via el-row / el-col.
// 2-column form
<ElPlusForm :column="2" :formDesc="formDesc" />
// Field spanning 2 columns
{ type: 'textarea', label: '描述', colspan: 2 }
beforeValidate → validate → beforeRequest → requestFn/updateFn → success → requestError → requestEnd
beforeValidate(data): Return false to cancel. Can be async.beforeRequest(data): Return false to cancel, return object to replace data. Can be async.requestFn(data): Create request. If updateFn exists and data has idKey, calls updateFn instead.success({ response, formData, callBack }): Success callback. callBack() resets loading.requestError(error): Error handler.requestEnd(): Always called (finally block).These hooks are passed via v-bind (attrs), not as props:
<ElPlusForm
:formDesc="formDesc"
v-model="formData"
:requestFn="api.create"
:updateFn="api.update"
:beforeValidate="beforeValidate"
:beforeRequest="beforeRequest"
:success="onSuccess"
/>
The form automatically transforms data for special types during getData():
| Type | Transformation |
|---|---|
daterange | Splits into startTime/endTime (customizable via startTimeKey/endTimeKey) |
datetimerange | Same as daterange, preserves time component |
timerange | Splits into startTime/endTime with HH:mm:ss format |
area | Splits into provinceId/cityId/zoneId/streetId |
checkbox | Joins array with comma: [1,2,3] → '1,2,3' |
linkuser | Splits into userIds/deptIds/userNames/deptNames |
_)Do NOT set these manually — they are computed internally:
_type: Resolved component name (e.g., 'el-plus-form-input')_vif: Computed visibility_disabled: Computed disabled state_attrs: Merged attributes_label: Computed label_tip: Computed hint textconst formRef = ref()
formRef.value.submit() // Trigger submit lifecycle
formRef.value.validate() // Validate form (returns Promise)
formRef.value.getData() // Get transformed form data
formRef.value.reset() // Reset to initial values
formRef.value.clear() // Clear all data and validation
formRef.value.clearValid() // Clear validation only
formRef.value.refresh() // Re-compute dynamic properties
formRef.value.init() // Init child components
formRef.value.changeValidImg() // Refresh captcha image
Each form sub-component receives these via mixins:
desc.attrs, auto-generates placeholder, handles multipleformData and rowIndex into callbacksThe tag type renders an el-tag for read-only status display. It does NOT support user input — the value comes from modelValue. Two key properties control its behavior:
Determines the type attribute of el-tag (controls color theme).
| Value Type | Behavior | Example |
|---|---|---|
| Not set | Default 'primary' (blue) | — |
Function (value, formData, field) => string | Dynamic color based on value | tagType: (v) => v === 1 ? 'success' : 'danger' |
| String (format key) | Calls format[tagType](value, formData, field) | tagType: 'statusColor' |
Supported el-tag types: 'primary' | 'success' | 'warning' | 'danger' | 'info'
Rendering rule: if tagType is falsy or equals '--', falls back to 'primary'.
Determines the text shown inside the tag.
| Value Type | Behavior | Example |
|---|---|---|
| Not set | Shows raw modelValue, empty/null shows '-' | — |
Function (value, formData, field) => string | Dynamic text | format: (v) => v === 1 ? '启用' : '禁用' |
| String (format key) | Calls format[formatKey](value, formData, field) | format: 'statusLabel' |
Any el-tag native attribute can be set via attrs:
attrs: { effect: 'dark', round: true, size: 'small' }
When used in ITableConfig.column, tag also supports the color property for custom colors:
| color Value Type | Behavior |
|---|---|
| String | Fixed custom color: '#67C23A' |
Function (value, row, field) => string | Dynamic color per row |
Array ['#67C23A', '#F56C6C'] | Color per value index (multi-prop columns) |
When isTable=true, the form renders as a horizontal search bar (used in table toolbar):
// Auto-set: horizontal layout, fixed widths, no submit buttons
<ElPlusForm isTable :formDesc="searchDesc" v-model="queryData" />
const formData = ref({})
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名', required: true },
phone: { type: 'input', label: '手机号', rules: 'phone' },
email: { type: 'input', label: '邮箱', rules: 'email' },
role: { type: 'select', label: '角色', options: [{ l: '管理员', v: 1 }, { l: '用户', v: 2 }] }
}
<ElPlusForm v-model="formData" :formDesc="formDesc" :requestFn="api.createUser" />
<ElPlusForm :column="2" v-model="formData" :formDesc="formDesc" />
const formDesc: IFormDesc = {
name: { type: 'input', label: '姓名' },
phone: { type: 'input', label: '手机号' },
remark: { type: 'textarea', label: '备注', colspan: 2 } // spans full width
}
const formDesc: IFormDesc = {
isCompany: { type: 'switch', label: '是否企业' },
companyName: {
type: 'input', label: '企业名称',
vif: (formData) => !!formData.isCompany,
required: (formData) => !!formData.isCompany
},
creditCode: {
type: 'input', label: '信用代码',
vif: (formData) => !!formData.isCompany,
disabled: (formData) => !formData.isVerified
}
}
const formDesc: IFormDesc = {
// 1. Static array
status: { type: 'select', label: '状态', options: [{ l: '启用', v: 1 }, { l: '禁用', v: 0 }] },
// 2. Function returning array
roles: { type: 'select', label: '角色', options: () => roleStore.list },
// 3. String key → fetched from globalData
types: { type: 'select', label: '类型', options: 'typeOptions' }
}
<ElPlusForm
v-model="formData"
:formDesc="formDesc"
:requestFn="api.createUser"
:updateFn="api.updateUser"
:beforeValidate="handleBeforeValidate"
:beforeRequest="handleBeforeRequest"
:success="handleSuccess"
:requestError="handleRequestError"
/>
const handleBeforeValidate = (data: any) => {
// Return false to cancel submit
return true
}
const handleBeforeRequest = (data: any) => {
// Modify data before request, return false to cancel
data.timestamp = Date.now()
return data
}
const handleSuccess = ({ response, formData, callBack }: IFormBack) => {
ElMessage.success('保存成功')
callBack() // resets loading state
}
const formDesc: IFormDesc = {
dateRange: {
type: 'daterange',
label: '时间范围',
startTimeKey: 'startDate', // default: 'startTime'
endTimeKey: 'endDate', // default: 'endTime'
valueFormat: 'YYYY-MM-DD' // passed to time() formatter
}
}
// getData() returns: { startDate: '2024-01-01', endDate: '2024-01-31' }
// === In formDesc (form detail / isTable mode) ===
const formDesc: IFormDesc = {
status: {
type: 'tag',
label: '状态',
// tagType: function — dynamic color based on value
tagType: (value) => {
if (value === 1) return 'success' // green
if (value === 0) return 'danger' // red
if (value === 2) return 'warning' // yellow
return 'info' // gray
},
// format: function — dynamic display text
format: (value) => {
const map = { 0: '禁用', 1: '启用', 2: '待审核' }
return map[value] || '未知'
},
// attrs: pass-through to el-tag
attrs: { effect: 'dark', size: 'small' }
}
}
// === In tableConfig.column ===
const columns: Array<IColumnItem> = [
{
prop: 'status', label: '状态', type: 'tag',
tagType: (value) => value === 1 ? 'success' : 'danger',
format: (value) => value === 1 ? '启用' : '禁用'
}
]
// === Using global format functions ===
// main.ts
const formatObj = {
statusColor: (value) => ({ 0: 'danger', 1: 'success', 2: 'warning' }[value] || 'info'),
statusLabel: (value) => ({ 0: '禁用', 1: '启用', 2: '待审核' }[value] || '未知')
}
app.use(elPlusCrud, config, formatObj, globalDataObj)
// formDesc — reference by string key
{
type: 'tag', label: '状态',
tagType: 'statusColor', // calls format.statusColor(value, formData, field)
format: 'statusLabel' // calls format.statusLabel(value, formData, field)
}
// === Table column with custom color ===
{
prop: 'status', label: '状态', type: 'tag',
color: (value, row, field) => value === 1 ? '#67C23A' : '#F56C6C',
format: (value) => value === 1 ? '在线' : '离线'
}
// main.ts — register custom component type
app.use(elPlusCrud, {
form: { comList: ['custompicker'] } // registers el-plus-form-custompicker
})
// Create component file: ElPlusFormCustompicker.vue
// Use in formDesc:
{ type: 'custompicker', label: '自定义选择器' }
// FAIL: tag does NOT use options — it ignores them completely
{ type: 'tag', label: '状态', options: [{ l: '启用', v: 1 }, { l: '禁用', v: 0 }] }
// tag uses format (display text) + tagType (color), not options
{ type: 'tag', label: '状态', tagType: (v) => v === 1 ? 'success' : 'danger', format: (v) => v === 1 ? '启用' : '禁用' }
// FAIL: _type, _vif etc. are computed internally
formDesc.name._type = 'el-plus-form-input'
formDesc.name._vif = true
formDesc.name.type = 'input'
formDesc.name.vif = (formData) => !!formData.showName
// FAIL: vif/disabled are synchronous, called on every data change
vif: async (formData) => {
const result = await checkPermission(formData.id)
return result
}
const showField = ref(false)
checkPermission(data.id).then(r => showField.value = r)
// vif can accept boolean directly
formDesc.name.vif = showField
// FAIL: don't manually process daterange values
const data = formRef.value.getData()
data.startTime = data.dateRange[0]
data.endTime = data.dateRange[1]
{ type: 'daterange', label: '时间范围', startTimeKey: 'startTime', endTimeKey: 'endTime' }
// getData() already returns { startTime: ..., endTime: ... }
formDesc key names aligned with backend field names — minimize mapping in beforeRequestvif/disabled to enable reactive field linking — component auto-enables deep watchdescOrders array to control field display order instead of relying on object key insertion orderisTable mode, set fixed width on each field to prevent search bar overflowupload in ICRUDConfig globally rather than per-fieldpropPrefix to avoid key collisionsel-plus-crud-config — IFormDesc, IFormDescItem, IFormConfig type definitions (direct)el-plus-crud-validation — rules and required validation configuration (direct)el-plus-crud-table — table toolbar search bar uses ElPlusForm isTable mode (indirect)