UNPKG

el-plus-crud

Version:

采用Vue3 + TS,封装的element-plus数据驱动表单、列表组件

501 lines (390 loc) 16.5 kB
--- 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 — Data-Driven 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. ## When to Activate - Using `<ElPlusForm>` component - Writing or modifying `IFormDesc` / `IFormDescItem` objects - Configuring form field types (input, select, upload, daterange, etc.) - Implementing field linking (inter-field dependencies, dynamic visibility) - Calling form expose methods: `submit` / `validate` / `reset` / `clear` / `getData` ## Core Concepts ### IFormDesc Structure Key = field name (matches backend field), value = `IFormDescItem`. ```typescript const formDesc: IFormDesc = { name: { type: 'input', label: '姓名', required: true }, phone: { type: 'input', label: '手机号', rules: 'phone' }, role: { type: 'select', label: '角色', options: roleList } } ``` ### 40+ Built-in Field Types | 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` | ### Dynamic Properties 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`). ### Multi-Column Layout Uses a 24-grid system via `el-row` / `el-col`. ```typescript // 2-column form <ElPlusForm :column="2" :formDesc="formDesc" /> // Field spanning 2 columns { type: 'textarea', label: '描述', colspan: 2 } ``` ### Submit Lifecycle ``` 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: ```html <ElPlusForm :formDesc="formDesc" v-model="formData" :requestFn="api.create" :updateFn="api.update" :beforeValidate="beforeValidate" :beforeRequest="beforeRequest" :success="onSuccess" /> ``` ### Data Transformation — Special Types 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` | ### Internal Properties (prefixed with `_`) 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 text ### Expose Methods ```typescript const 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 ``` ### Mixins System Each form sub-component receives these via mixins: - **getAttrs**: Merges `desc.attrs`, auto-generates `placeholder`, handles `multiple` - **getEvents**: Wraps events, injects `formData` and `rowIndex` into callbacks ### Display Typestag The `tag` type renders an `el-tag` for read-only status display. It does NOT support user inputthe value comes from `modelValue`. Two key properties control its behavior: #### tagTypeControls Tag Color 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'`. #### formatControls Display Text 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'` | #### attrsPass-through to el-tag Any `el-tag` native attribute can be set via `attrs`: ```typescript attrs: { effect: 'dark', round: true, size: 'small' } ``` #### tag in Table Columns 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) | ### isTable Mode When `isTable=true`, the form renders as a horizontal search bar (used in table toolbar): ```typescript // Auto-set: horizontal layout, fixed widths, no submit buttons <ElPlusForm isTable :formDesc="searchDesc" v-model="queryData" /> ``` ## Code Examples ### Basic Form ```typescript 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 }] } } ``` ```html <ElPlusForm v-model="formData" :formDesc="formDesc" :requestFn="api.createUser" /> ``` ### Multi-Column with Colspan ```html <ElPlusForm :column="2" v-model="formData" :formDesc="formDesc" /> ``` ```typescript const formDesc: IFormDesc = { name: { type: 'input', label: '姓名' }, phone: { type: 'input', label: '手机号' }, remark: { type: 'textarea', label: '备注', colspan: 2 } // spans full width } ``` ### Vertical Spanning with Rowspan When any visible field sets `rowspan`, that group switches to a CSS Grid layout so an item can span multiple rows. Other fields keep flowing around it. ```html <ElPlusForm :column="3" v-model="formData" :formDesc="formDesc" /> ``` ```typescript const formDesc: IFormDesc = { avatar: { type: 'image', label: '头像', rowspan: 2 }, // spans 2 rows name: { type: 'input', label: '姓名' }, phone: { type: 'input', label: '手机号' }, email: { type: 'input', label: '邮箱' }, address: { type: 'input', label: '地址' } } ``` Layout — `avatar` occupies the left column across 2 rows, the rest flow around it: ``` ┌──────┬──────┬──────┐ │ │ 姓名 │ 手机 │ │ 头像 ├──────┼──────┤ │ │ 邮箱 │ 地址 │ └──────┴──────┴──────┘ ``` **Limitations** - Only effective with `column >= 2`; on a single-column form and on mobile/narrow screens it degrades to normal stacking. - Ignored in `isTable` mode (inline search bar). - `maxShowRowIndex` (collapse first N rows) is not supported in Grid mode — all rows are shown. - `colspan` and `rowspan` can be combined on the same field. ### Dynamic Visibility and Linking ```typescript 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 } } ``` ### Options — Three Sources ```typescript 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' } } ``` ### Complete Submit Lifecycle ```html <ElPlusForm v-model="formData" :formDesc="formDesc" :requestFn="api.createUser" :updateFn="api.updateUser" :beforeValidate="handleBeforeValidate" :beforeRequest="handleBeforeRequest" :success="handleSuccess" :requestError="handleRequestError" /> ``` ```typescript 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 } ``` ### daterange Data Transformation ```typescript 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' } ``` ### Tag Display — Status Column ```typescript // === 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 ? '在线' : '离线' } ``` ### Custom Component Extension ```typescript // 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: '自定义选择器' } ``` ## Anti-Patterns ### FAIL: Use options to control tag display ```typescript // FAIL: tag does NOT use options — it ignores them completely { type: 'tag', label: '状态', options: [{ l: '启用', v: 1 }, { l: '禁用', v: 0 }] } ``` ### PASS: Use format for text, tagType for color ```typescript // tag uses format (display text) + tagType (color), not options { type: 'tag', label: '状态', tagType: (v) => v === 1 ? 'success' : 'danger', format: (v) => v === 1 ? '启用' : '禁用' } ``` ### FAIL: Manually set internal properties ```typescript // FAIL: _type, _vif etc. are computed internally formDesc.name._type = 'el-plus-form-input' formDesc.name._vif = true ``` ### PASS: Set declarative properties, let component handle internals ```typescript formDesc.name.type = 'input' formDesc.name.vif = (formData) => !!formData.showName ``` ### FAIL: Async operations in vif/disabled functions ```typescript // FAIL: vif/disabled are synchronous, called on every data change vif: async (formData) => { const result = await checkPermission(formData.id) return result } ``` ### PASS: Use reactive variable for async conditions ```typescript const showField = ref(false) checkPermission(data.id).then(r => showField.value = r) // vif can accept boolean directly formDesc.name.vif = showField ``` ### FAIL: Manually handle daterange splitting ```typescript // FAIL: don't manually process daterange values const data = formRef.value.getData() data.startTime = data.dateRange[0] data.endTime = data.dateRange[1] ``` ### PASS: Use startTimeKey/endTimeKey, component handles it automatically ```typescript { type: 'daterange', label: '时间范围', startTimeKey: 'startTime', endTimeKey: 'endTime' } // getData() already returns { startTime: ..., endTime: ... } ``` ## Best Practices 1. Keep `formDesc` key names aligned with backend field names — minimize mapping in `beforeRequest` 2. Use function form for `vif`/`disabled` to enable reactive field linking — component auto-enables deep watch 3. Use `descOrders` array to control field display order instead of relying on object key insertion order 4. In `isTable` mode, set fixed `width` on each field to prevent search bar overflow 5. Always configure `upload` in `ICRUDConfig` globally rather than per-field 6. For 3+ daterange components in one form, use `propPrefix` to avoid key collisions ## Related Skills - `el-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)