es-plus-ui 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,942 +1,1040 @@
1
- # es-plus
2
-
3
- 基于 Vue 3 + Element Plus 的企业级中后台前端组件库,以配置化驱动为核心,大幅减少中后台 CRUD 页面开发代码量。
4
-
5
- [![npm version](https://img.shields.io/npm/v/es-plus.svg)](https://www.npmjs.com/package/es-plus)
6
- [![license](https://img.shields.io/npm/l/es-plus.svg)](https://www.npmjs.com/package/es-plus)
7
-
8
- ## 核心特性
9
-
10
- - **配置化开发** — JSON 配置生成复杂表单与表格,替代大量模板代码
11
- - **表单表格联动** — `triggerEvent` + `apiParams.model` 实现零事件代码查询
12
- - **编程式弹窗** — `useDialog` Hook 命令式调用,支持 JSX 渲染、嵌套弹窗
13
- - **自适应高度** — `ResizeObserver` 自动重算表格高度,表单展开/收起自动响应
14
- - **跨页选择** — `rowkey` + `cachePageSelection` 解决分页选择丢失痛点
15
- - **任意后端适配** — `configTableOut` + `qrcb` 配置化适配不同后端响应格式
16
- - **TypeScript** — 完整类型定义
17
- - **13 种表单类型** — Input、Select、datePicker、timePicker、Slider、ColorPicker、Transfer、Cascader、Radio、Checkbox、Switch、Rate、Upload
18
-
19
- ## 安装
20
-
21
- ```bash
22
- npm install es-plus element-plus @element-plus/icons-vue
23
- # 或
24
- yarn add es-plus element-plus @element-plus/icons-vue
25
- # 或
26
- pnpm add es-plus element-plus @element-plus/icons-vue
27
- ```
28
-
29
- 前置依赖:`vue ^3.2.0`、`element-plus ^2.2.0`、`@element-plus/icons-vue ^2.1.0`
30
-
31
- ## 快速上手
32
-
33
- ### 全局引入
34
-
35
- ```typescript
36
- import { createApp } from 'vue'
37
- import ElementPlus from 'element-plus'
38
- import 'element-plus/dist/index.css'
39
- import EsPlus from 'es-plus'
40
- import 'es-plus/dist/style.css'
41
- import App from './App.vue'
42
-
43
- const app = createApp(App)
44
- app.use(ElementPlus)
45
- app.use(EsPlus)
46
- app.mount('#app')
47
- ```
48
-
49
- ### 按需引入
50
-
51
- ```typescript
52
- import { EsForm, EsTable, useDialog } from 'es-plus'
53
- import 'es-plus/dist/style.css'
54
- ```
55
-
56
- ### 最小示例
57
-
58
- **EsForm**:
59
-
60
- ```vue
61
- <template>
62
- <es-form :model="form" :form-item-list="items" :config-btn="btns" />
63
- </template>
64
-
65
- <script setup>
66
- import { reactive } from 'vue'
67
- const form = reactive({ keyword: '', status: '' })
68
- const items = [
69
- { prop: 'keyword', label: '关键词', formtype: 'Input', span: 6, attrs: { clearable: true } },
70
- { prop: 'status', label: '状态', formtype: 'Select', span: 6, dataOptions: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }] }
71
- ]
72
- const btns = [
73
- { name: '查询', type: 'primary', key: 'query', triggerEvent: true },
74
- { name: '重置', key: 'reset', triggerEvent: true }
75
- ]
76
- </script>
77
- ```
78
-
79
- **EsTable**:
80
-
81
- ```vue
82
- <template>
83
- <es-table
84
- :columns="columns"
85
- :options="options"
86
- v-model:data-source="tableData"
87
- v-model:pagination="pagination"
88
- />
89
- </template>
90
-
91
- <script setup>
92
- import { ref } from 'vue'
93
- const tableData = ref([])
94
- const pagination = ref({ pageSize: 10, current: 1, total: 0 })
95
- const columns = [
96
- { prop: 'id', label: 'ID', width: 80 },
97
- { prop: 'name', label: '姓名' },
98
- { prop: 'status', label: '状态' }
99
- ]
100
- const mockRequest = async (params) => {
101
- const { formParams, ...rest } = params || {}
102
- const { pageIndex = 1, pageSize = 10 } = { ...formParams, ...rest }
103
- // 实际项目中替换为真实 API 调用
104
- return { data: [], total: 0, pageSize, pageIndex }
105
- }
106
- const options = {
107
- border: true,
108
- httpRequest: mockRequest,
109
- apiParams: { url: '/api/list', method: 'GET' },
110
- configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' },
111
- rowkey: 'id',
112
- heightType: 'height',
113
- tabHeight: 400
114
- }
115
- </script>
116
- ```
117
-
118
- **useDialog**:
119
-
120
- ```tsx
121
- import { useDialog } from 'es-plus'
122
- import EsForm from 'es-plus/components/es-form'
123
-
124
- const dialog = useDialog()
125
- function openAddDialog() {
126
- dialog({
127
- title: '新增',
128
- width: '600px',
129
- render: (h, { registerRef }) => (
130
- <EsForm
131
- ref={(el) => { if (el) registerRef('formRef', el) }}
132
- model={formData}
133
- formItemList={[{ prop: 'name', label: '名称', formtype: 'Input', span: 24 }]}
134
- />
135
- ),
136
- configBtn: [
137
- { name: '取消', click: (_, { close }) => close() },
138
- { name: '提交', type: 'primary', click: (_, { close, getRefs }) => {
139
- getRefs('formRef')?.validate().then(() => { close() })
140
- }}
141
- ]
142
- })
143
- }
144
- ```
145
-
146
- ---
147
-
148
- ## EsForm 表单组件
149
-
150
- 配置化驱动的表单组件,支持 13 种输入类型、动态显隐、异步数据加载、折叠展开等功能。
151
-
152
- ### Props
153
-
154
- | 属性 | 类型 | 默认值 | 说明 |
155
- |------|------|--------|------|
156
- | model | `Record<string, unknown>` | `{}` | 表单数据对象(必填) |
157
- | formItemList | `FormItemOption[]` | `[]` | 表单项配置数组(必填) |
158
- | layoutFormProps | `LayoutFormProps` | `{}` | 布局配置 |
159
- | configBtn | `BtnConfig[]` | `[]` | 按钮配置 |
160
- | renderBtn | `Function \| boolean` | `false` | 自定义按钮渲染函数 |
161
- | btnColSpanRow | `boolean` | `true` | 按钮是否独占一行 |
162
- | rules | `Record<string, unknown>` | `{}` | 验证规则(Element Plus 格式) |
163
- | fieldFieldOutput | `(defaults) => Record<string, string>` | — | API 响应字段映射 |
164
-
165
- ### FormItemOption 配置
166
-
167
- | 字段 | 类型 | 默认值 | 说明 |
168
- |------|------|--------|------|
169
- | prop | `string` | — | 字段名(必填) |
170
- | label | `string` | — | 标签文本(必填) |
171
- | formtype | `string` | | 输入组件类型(见下表) |
172
- | span | `number` | `6` | 栅格占列数(24 列布局) |
173
- | attrs | `Record<string, unknown>` | — | 透传给 Element Plus 组件的属性(placeholder、clearable 等) |
174
- | on | `Record<string, unknown>` | — | 事件监听(change、input 等) |
175
- | dataOptions | `Array<{ label, value }>` | — | 选项数据(Select/Radio/Checkbox/Cascader) |
176
- | isHidden | `(model, item, formProps) => boolean` | — | 条件隐藏函数,返回 `true` 时隐藏 |
177
- | render | `(h, model, ctx) => VNode \| string` | — | 自定义渲染函数 |
178
- | apiParams | `ApiParams` | — | 从接口加载选项数据 |
179
- | isInitRun | `boolean` | `true` | 是否在初始化时自动加载接口数据 |
180
- | callOptionListFormat | `(data) => unknown[]` | — | 将 API 响应转换为 dataOptions 格式 |
181
- | httpRequest | `(params) => Promise` | — | 自定义请求方法(覆盖全局配置) |
182
- | listenToCallBack | `Record<string, Function>` | — | 回调映射,`crtn` 用于选项格式转换 |
183
- | width | `number \| string` | — | 字段宽度 |
184
- | formItemOptions | `Record<string, unknown>` | — | el-form-item 附加属性(rules、labelWidth 等) |
185
- | components | `Record<string, unknown>` | — | 自定义组件映射 |
186
-
187
- ### formtype 类型
188
-
189
- | 类型 | 对应组件 | 常用配置 |
190
- |------|----------|----------|
191
- | `Input` | ElInput | `attrs: { placeholder, clearable, type: 'textarea' }` |
192
- | `Select` | ElSelect | `dataOptions: [{ label, value }]`, `attrs: { clearable, multiple }` |
193
- | `datePicker` | ElDatePicker | `attrs: { type: 'daterange/datetimerange', valueFormat }` |
194
- | `timePicker` | ElTimePicker | `attrs: { isRange }` |
195
- | `Slider` | ElSlider | `attrs: { min, max, step }` |
196
- | `ColorPicker` | ElColorPicker | `attrs: { showAlpha }` |
197
- | `Transfer` | ElTransfer | `dataOptions` |
198
- | `Cascader` | ElCascader | `dataOptions`(树形结构), `attrs: { props: { checkStrictly: true } }` |
199
- | `Radio` | ElRadioGroup | `dataOptions: [{ label, value }]` |
200
- | `Checkbox` | ElCheckboxGroup | `dataOptions: [{ label, value }]` |
201
- | `Switch` | ElSwitch | `attrs: { activeText, inactiveText }` |
202
- | `Rate` | ElRate | `attrs: { max, allowHalf }` |
203
- | `Upload` | ElUpload | `attrs: { action, listType, limit }` |
204
-
205
- ### LayoutFormProps 配置
206
-
207
- | 字段 | 类型 | 说明 |
208
- |------|------|------|
209
- | fromLayProps | `Object` | 表单级别属性 |
210
- | fromLayProps.labelWidth | `string` | 标签宽度,如 `'100px'` |
211
- | fromLayProps.minFoldRows | `number` | 折叠时显示的行数,`0` 不折叠 |
212
- | fromLayProps.isBtnHidden | `boolean` | 是否隐藏按钮区域 |
213
- | fromLayProps.btnColSpan | `number` | 按钮列占位宽度 |
214
- | fromLayProps.labelBtnWidth | `string \| number` | 按钮标签宽度 |
215
- | rowLayProps | `Object` | 行级别属性 |
216
- | rowLayProps.gutter | `number` | 栅格间距 |
217
- | setOptions | `boolean` | 是否启用设置下拉 |
218
-
219
- ### BtnConfig 配置
220
-
221
- | 字段 | 类型 | 说明 |
222
- |------|------|------|
223
- | name | `string` | 按钮文本 |
224
- | key | `string` | 按钮唯一标识 |
225
- | type | `string` | 按钮类型(primary/success/warning/danger/info) |
226
- | icon | `string` | 图标名称 |
227
- | size | `string` | 按钮尺寸 |
228
- | direction | `'left' \| 'right'` | 按钮位置 |
229
- | loading | `boolean` | 加载状态 |
230
- | disabled | `boolean \| () => boolean` | 禁用状态,支持函数形式 |
231
- | click | `(model, formRef, httpRequestInstance?) => void` | 点击回调 |
232
- | triggerEvent | `boolean` | `true` 时自动触发表格查询/表单重置 |
233
-
234
- > `triggerEvent: true` + `key: 'query'` → 自动调用父级 EsTable 的 `httpRequestInstance`;`triggerEvent: true` + `key: 'reset'` → 自动重置表单。
235
-
236
- ### Events
237
-
238
- | 事件 | 参数 | 说明 |
239
- |------|------|------|
240
- | confirm | `(formRef, model)` | 点击确认按钮时触发 |
241
- | reset | `(formRef, model)` | 点击重置按钮时触发 |
242
-
243
- ### Methods(通过 ref 调用)
244
-
245
- | 方法 | 说明 |
246
- |------|------|
247
- | `validate()` | 校验整个表单,返回 `Promise<boolean>` |
248
- | `resetFields()` | 重置所有字段 |
249
- | `clearValidate(props?)` | 清除校验状态 |
250
- | `validateField(props)` | 校验指定字段 |
251
- | `scrollToField(prop)` | 滚动到指定字段 |
252
- | `formItmeRequestInstance(propsList)` | 手动触发指定字段的 API 数据加载 |
253
- | `getFormRef()` | 获取底层 ElForm 实例 |
254
-
255
- ### 高级用法
256
-
257
- #### 条件隐藏
258
-
259
- ```typescript
260
- const items = [
261
- { prop: 'type', label: '类型', formtype: 'Select', span: 6,
262
- dataOptions: [{ label: '个人', value: 'personal' }, { label: '企业', value: 'company' }]
263
- },
264
- { prop: 'companyName', label: '企业名称', formtype: 'Input', span: 6,
265
- isHidden: (model) => model.type !== 'company' // 类型不是企业时隐藏
266
- }
267
- ]
268
- ```
269
-
270
- #### 异步数据加载
271
-
272
- ```typescript
273
- const items = [
274
- { prop: 'category', label: '分类', formtype: 'Select', span: 6,
275
- apiParams: { url: '/api/categories', method: 'GET' },
276
- callOptionListFormat: (data) => data.map(item => ({ label: item.name, value: item.id }))
277
- }
278
- ]
279
- ```
280
-
281
- #### 自定义渲染
282
-
283
- ```tsx
284
- const items = [
285
- { prop: 'amount', label: '金额', span: 6,
286
- render: (h, model) => <span style="color: red">¥{model.amount?.toLocaleString()}</span>
287
- }
288
- ]
289
- ```
290
-
291
- ---
292
-
293
- ## EsTable 表格组件
294
-
295
- 配置化驱动的数据表格,内置分页、远程数据、跨页选择、自适应高度等功能。
296
-
297
- ### Props
298
-
299
- | 属性 | 类型 | 默认值 | 说明 |
300
- |------|------|--------|------|
301
- | dataSource | `Record<string, unknown>[]` | `[]` | 表格数据(必填,支持 v-model) |
302
- | columns | `TableColumn[]` | `[]` | 列配置数组(必填) |
303
- | options | `TableOptions` | `{}` | 表格选项配置 |
304
- | pagination | `PaginationConfig` | — | 分页配置(支持 v-model) |
305
- | initTabHeight | `number` | `400` | 初始表格高度 |
306
- | showHeaderBar | `boolean` | `true` | 是否显示头部栏区域 |
307
- | headBarClass | `string \| Object` | | 头部栏样式类 |
308
-
309
- ### TableColumn 配置
310
-
311
- | 字段 | 类型 | 说明 |
312
- |------|------|------|
313
- | prop | `string` | 数据字段名 |
314
- | key | `string` | 列唯一标识 |
315
- | label | `string` | 列标题 |
316
- | width | `number \| string` | 列宽度 |
317
- | minWidth | `number \| string` | 最小列宽度 |
318
- | align | `string` | 对齐方式 |
319
- | fixed | `boolean \| string` | 固定列(`'left'` / `'right'`) |
320
- | formatter | `(row) => string` | 格式化函数 |
321
- | render | `(h, { row, value, index }) => VNode` | 自定义渲染函数 |
322
- | scopedSlots | `{ customRender: string }` | 插槽配置 |
323
- | groups | `TableColumn[]` | 多级表头子列 |
324
- | ellipsis | `boolean` | 文本溢出省略 |
325
- | hidCol | `boolean` | 是否隐藏该列 |
326
- | btns | `Array<{ name, type?, clickEvent? }>` | 行操作按钮 |
327
- | type | `string` | 特殊列类型(`'selection'`、`'expand'`、`'index'`) |
328
-
329
- #### render 函数
330
-
331
- ```tsx
332
- {
333
- prop: 'status', label: '状态', width: 100,
334
- render: (_, { row }) => {
335
- const map = { active: 'success', leave: 'warning', resigned: 'danger' }
336
- return <ElTag type={map[row.status]} size="small">{row.status}</ElTag>
337
- }
338
- }
339
- ```
340
-
341
- #### btns 行操作按钮
342
-
343
- ```typescript
344
- {
345
- prop: 'operate', label: '操作', width: 160,
346
- btns: [
347
- { name: '编辑', type: 'primary', clickEvent: (row) => openEditDialog(row) },
348
- { name: '删除', type: 'danger', clickEvent: (row) => handleDelete(row) }
349
- ]
350
- }
351
- ```
352
-
353
- #### 多级表头
354
-
355
- ```typescript
356
- {
357
- label: '基本信息',
358
- groups: [
359
- { prop: 'name', label: '姓名' },
360
- { prop: 'age', label: '年龄' },
361
- { label: '地址', groups: [
362
- { prop: 'province', label: '省份' },
363
- { prop: 'city', label: '城市' }
364
- ]}
365
- ]
366
- }
367
- ```
368
-
369
- ### TableOptions 配置
370
-
371
- | 字段 | 类型 | 默认值 | 说明 |
372
- |------|------|--------|------|
373
- | border | `boolean` | `false` | 是否显示边框 |
374
- | stripe | `boolean` | `false` | 是否显示斑马纹 |
375
- | size | `'large' \| 'default' \| 'small'` | `'small'` | 表格尺寸 |
376
- | headerCellStyle | `Record<string, unknown>` | `{ background: '#f5f7fa' }` | 表头样式 |
377
- | highlightCurrentRow | `boolean` | `true` | 高亮当前行 |
378
- | multiSelect | `boolean` | `false` | 启用多选列 |
379
- | expand | `boolean` | `false` | 启用展开行 |
380
- | snIndex | `boolean` | `false` | 显示序号列 |
381
- | loading | `boolean` | `false` | 加载状态 |
382
- | heightType | `'height' \| 'auto'` | — | 高度类型(`'height'` 推荐,`'auto'` 为 maxHeight) |
383
- | tabHeight | `number \| string` | — | 表格容器高度(配合 heightType 使用) |
384
- | cachePageSelection | `boolean` | `true` | 启用跨页选择缓存 |
385
- | rowkey | `string` | — | 行唯一标识字段名(跨页选择必填) |
386
- | isInitRun | `boolean` | — | 初始化时是否自动请求数据 |
387
- | httpRequest | `(params) => Promise` | — | 自定义请求方法 |
388
- | apiParams | `ApiParams` | — | API 请求配置 |
389
- | configTableOut | `Record<string, string>` | — | 响应字段映射 |
390
- | listenToCallBack | `Record<string, Function>` | — | 请求/响应回调管线 |
391
- | entryQuery | `Record<string, unknown>` | — | 默认查询参数 |
392
- | actionUrl | `string` | — | 请求地址(简写) |
393
-
394
- ### configTableOut 字段映射
395
-
396
- 映射后端响应字段到表格内部使用的字段:
397
-
398
- ```typescript
399
- configTableOut: {
400
- total: 'total', // 总数对应的字段名
401
- tableData: 'data', // 数据列表对应的字段名
402
- pageSize: 'pageSize', // 每页条数对应的字段名
403
- current: 'pageIndex' // 当前页码对应的字段名
404
- }
405
- ```
406
-
407
- > **注意**:`configTableOut` 的值应使用简单 key 名(如 `'total'`、`'list'`),不支持点号路径(如 `'result.pagination.total'`)。内部使用 `findValueByKey` 递归查找嵌套对象中的 key,无需写完整路径。
408
-
409
- ### listenToCallBack 回调管线
410
-
411
- ```typescript
412
- listenToCallBack: {
413
- // 请求前回调(Before Request CallBack)— 转换请求参数
414
- brcb: (params) => {
415
- return { ...params, timestamp: Date.now() }
416
- },
417
- // 请求后回调(Query Result CallBack)— 转换响应数据
418
- qrcb: (res) => {
419
- if (!res?.data) return res
420
- return {
421
- ...res,
422
- data: res.data.map(item => ({
423
- id: item.emp_id,
424
- name: item.emp_name // 后端蛇形字段转前端驼峰
425
- }))
426
- }
427
- }
428
- }
429
- ```
430
-
431
- ### PaginationConfig 配置
432
-
433
- | 字段 | 类型 | 说明 |
434
- |------|------|------|
435
- | pageSize | `number` | 每页条数 |
436
- | current | `number` | 当前页码 |
437
- | total | `number` | 总条数 |
438
- | pageSizes | `number[]` | 每页条数选项 |
439
- | size | `'large' \| 'default' \| 'small'` | 分页器尺寸 |
440
- | isSmall | `boolean` | 使用小型分页器 |
441
-
442
- ### Events
443
-
444
- | 事件 | 参数 | 说明 |
445
- |------|------|------|
446
- | update:dataSource | `(data)` | 数据更新 |
447
- | update:pagination | `(pagination)` | 分页更新 |
448
- | selection-change | `(rows)` | 选择变化(通过 fallthrough attrs 传递) |
449
- | pagination-current-change | `(current)` | 页码变化 |
450
- | size-change | `(pageSize)` | 每页条数变化 |
451
- | change-table-sort | `(sort)` | 排序变化 |
452
-
453
- ### Methods(通过 ref 调用)
454
-
455
- | 方法 | 说明 |
456
- |------|------|
457
- | `httpRequestInstance(model?)` | 手动触发表格数据请求,可传入额外查询参数 |
458
- | `getSelectionRows()` | 获取当前选中行(含跨页缓存) |
459
- | `clearSelection()` | 清除当前页选择 |
460
- | `clearAllSelection()` | 清除所有页面选择(含跨页缓存) |
461
- | `refresh()` | 强制重新计算表格布局(`doLayout`) |
462
-
463
- ### 高级用法
464
-
465
- #### 远程数据请求
466
-
467
- ```typescript
468
- const mockRequest = async (params) => {
469
- const { formParams, ...rest } = params || {}
470
- const { pageIndex = 1, pageSize = 10, ...filters } = { ...formParams, ...rest }
471
- const res = await fetch('/api/list')
472
- const data = await res.json()
473
- const total = data.length
474
- const start = (pageIndex - 1) * pageSize
475
- return { data: data.slice(start, start + pageSize), total, pageSize, pageIndex }
476
- }
477
-
478
- const options = {
479
- border: true,
480
- httpRequest: mockRequest,
481
- apiParams: { url: '/api/list', method: 'GET', model: queryModel },
482
- configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' },
483
- rowkey: 'id'
484
- }
485
- ```
486
-
487
- #### 跨页选择持久化
488
-
489
- ```vue
490
- <es-table
491
- ref="tableRef"
492
- :columns="columns"
493
- :options="{ rowkey: 'id', cachePageSelection: true, multiSelect: true }"
494
- @selection-change="onSelectionChange"
495
- />
496
-
497
- <script setup>
498
- const tableRef = ref(null)
499
- const selectedCount = ref(0)
500
-
501
- const onSelectionChange = (rows) => {
502
- selectedCount.value = rows?.length || 0
503
- }
504
-
505
- function getSelected() {
506
- return tableRef.value?.getSelectionRows() || []
507
- }
508
-
509
- function clearSelected() {
510
- tableRef.value?.clearAllSelection()
511
- }
512
- </script>
513
- ```
514
-
515
- #### 自适应高度
516
-
517
- ```typescript
518
- const options = {
519
- heightType: 'height', // 必须 'height',非 'auto'
520
- tabHeight: 400 // 容器高度,表格自动 = 容器 - 表单 - 分页
521
- }
522
- ```
523
-
524
- > 表单展开/收起时,`ResizeObserver` 自动触发高度重算,无需手动监听。
525
-
526
- #### 动态 options 需使用 `:key`
527
-
528
- > es-table 的 `httpRequest`、`configTableOut`、`listenToCallBack` 等选项在挂载后不可动态响应。如需切换,请使用 `:key` 强制重建:
529
-
530
- ```vue
531
- <es-table :key="activeFormat" :options="currentOptions" ... />
532
- ```
533
-
534
- ---
535
-
536
- ## EsDialog 弹窗组件
537
-
538
- 模板式增强弹窗,支持拖拽、全屏切换、自定义头尾等功能。
539
-
540
- ### Props
541
-
542
- | 属性 | 类型 | 默认值 | 说明 |
543
- |------|------|--------|------|
544
- | title | `string` | | 弹窗标题 |
545
- | visible | `boolean` | `false` | 显示状态(支持 v-model) |
546
- | width | `string \| number` | `'50%'` | 弹窗宽度 |
547
- | isDraggable | `boolean` | `false` | 是否可拖拽 |
548
- | fullscreen | `boolean` | `false` | 是否全屏 |
549
- | hiddenFullBtn | `boolean` | `false` | 隐藏全屏切换按钮 |
550
- | isHiddenFooter | `boolean` | `false` | 隐藏底部按钮区 |
551
- | maxHeight | `string \| number` | — | 内容区最大高度 |
552
- | appendTo | `string \| HTMLElement` | — | 挂载目标 |
553
- | confirmText | `string` | — | 确认按钮文本 |
554
- | cancelText | `string` | — | 取消按钮文本 |
555
- | configBtn | `BtnConfig[]` | `[]` | 底部按钮配置 |
556
- | render | `Function` | | 自定义内容渲染函数 |
557
- | renderHeader | `Function` | | 自定义头部渲染函数 |
558
- | renderFooter | `Function` | | 自定义底部渲染函数 |
559
-
560
- ### Events
561
-
562
- | 事件 | 说明 |
563
- |------|------|
564
- | update:visible | 显示状态变化 |
565
- | closed | 弹窗关闭后触发 |
566
- | submit | 点击确认时触发 |
567
-
568
- ---
569
-
570
- ## useDialog 编程式弹窗 Hook
571
-
572
- 命令式调用弹窗,支持 JSX 渲染、表单集成、嵌套弹窗等高级功能。
573
-
574
- ### 基本用法
575
-
576
- ```typescript
577
- import { useDialog } from 'es-plus'
578
-
579
- const dialog = useDialog()
580
-
581
- // 打开弹窗
582
- dialog({
583
- title: '提示',
584
- width: '500px',
585
- render: (h) => <div>弹窗内容</div>,
586
- configBtn: [
587
- { name: '确定', type: 'primary', click: (_, { close }) => close() }
588
- ]
589
- })
590
- ```
591
-
592
- ### DialogOptions 配置
593
-
594
- | 参数 | 类型 | 默认值 | 说明 |
595
- |------|------|--------|------|
596
- | title | `string` | — | 弹窗标题 |
597
- | width | `string \| number` | `'50%'` | 弹窗宽度 |
598
- | key | `string` | — | 唯一标识(相同 key 复用实例) |
599
- | height | `string \| number` | — | 弹窗高度 |
600
- | maxHeight | `string \| number` | — | 内容区最大高度 |
601
- | render | `(h, instance, components) => VNode` | — | 内容渲染函数 |
602
- | renderHeader | `(h, instance) => VNode` | — | 头部渲染函数 |
603
- | renderFooter | `(h, instance) => VNode` | — | 底部渲染函数 |
604
- | configBtn | `BtnConfig[]` | `[]` | 底部按钮配置 |
605
- | isDraggable | `boolean` | `false` | 是否可拖拽 |
606
- | fullscreen | `boolean` | `false` | 是否全屏 |
607
- | hiddenFullBtn | `boolean` | `false` | 隐藏全屏按钮 |
608
- | isHiddenFooter | `boolean` | `false` | 隐藏底部 |
609
- | center | `boolean` | — | 垂直居中 |
610
- | closeOnClickModal | `boolean` | `false` | 点击遮罩关闭 |
611
- | closeOnPressEscape | `boolean` | `false` | 按 ESC 关闭 |
612
- | showClose | `boolean` | `true` | 显示关闭按钮 |
613
- | destroyOnClose | `boolean` | — | 关闭时销毁 |
614
- | showDefaultButtons | `boolean` | — | 显示默认确定/取消按钮 |
615
- | loading | `boolean` | `false` | 加载状态 |
616
- | customClass | `string` | — | 自定义样式类 |
617
- | appendToBody | `boolean` | — | 挂载到 body |
618
- | appendTo | `string \| HTMLElement` | | 挂载目标 |
619
- | modal | `boolean` | — | 显示遮罩 |
620
- | lockScroll | `boolean` | — | 锁定滚动 |
621
- | onlyInstance | `boolean` | — | 单实例模式(复用同一弹窗) |
622
- | onSubmit | `(close) => void` | — | 提交回调 |
623
- | onClosed | `() => void` | — | 关闭回调 |
624
- | onOpen | `() => void` | — | 打开回调 |
625
-
626
- ### configBtn click 回调签名
627
-
628
- ```typescript
629
- {
630
- name: '提交',
631
- type: 'primary',
632
- click: (instance, { close, getRefs, dialogVm }) => {
633
- // instance: 渲染组件的内部实例
634
- // close(): 关闭弹窗
635
- // getRefs(name): 获取通过 registerRef 注册的引用
636
- // dialogVm: 弹窗组件实例
637
- }
638
- }
639
- ```
640
-
641
- ### registerRef + getRefs 模式
642
-
643
- render 中使用 `registerRef` 注册引用,在 configBtn click 中通过 `getRefs` 获取:
644
-
645
- ```tsx
646
- dialog({
647
- title: '编辑',
648
- render: (h, { registerRef }) => (
649
- <EsForm
650
- ref={(el) => { if (el) registerRef('formRef', el) }}
651
- model={formData}
652
- formItemList={formItems}
653
- />
654
- ),
655
- configBtn: [
656
- { name: '取消', click: (_, { close }) => close() },
657
- { name: '提交', type: 'primary', click: (_, { close, getRefs }) => {
658
- const formRef = getRefs('formRef')
659
- formRef?.validate().then(() => {
660
- // 提交逻辑...
661
- close()
662
- })
663
- }}
664
- ]
665
- })
666
- ```
667
-
668
- ### onlyInstance 模式
669
-
670
- ```typescript
671
- // 默认:每次调用创建新弹窗
672
- const dialog = useDialog()
673
-
674
- // 单实例模式:复用同一弹窗,后续调用更新内容
675
- const singleDialog = useDialog(null, { onlyInstance: true })
676
- ```
677
-
678
- ### 多实例独立弹窗
679
-
680
- ```typescript
681
- // 创建多个独立弹窗,可同时打开
682
- const dialog1 = useDialog()
683
- const dialog2 = useDialog()
684
-
685
- // 嵌套弹窗:父弹窗内打开子弹窗
686
- dialog1({
687
- title: '父弹窗',
688
- render: (h) => (
689
- <div>
690
- <ElButton onClick={() => dialog2({ title: '子弹窗', render: (h) => <div>嵌套内容</div> })}>
691
- 打开子弹窗
692
- </ElButton>
693
- </div>
694
- )
695
- })
696
- ```
697
-
698
- ---
699
-
700
- ## SvgIcon 图标组件
701
-
702
- 支持外部 URL 图标和 SVG Symbol Sprite 的图标组件。
703
-
704
- ### Props
705
-
706
- | 属性 | 类型 | 默认值 | 说明 |
707
- |------|------|--------|------|
708
- | iconClass | `string` | | 图标名称或外部 URL(必填) |
709
- | className | `string` | | 额外样式类 |
710
-
711
- ### 使用
712
-
713
- ```vue
714
- <!-- SVG Sprite 图标 -->
715
- <svg-icon icon-class="user" />
716
-
717
- <!-- 外部 URL 图标(自动检测 http/https 开头) -->
718
- <svg-icon icon-class="https://example.com/icon.svg" />
719
- ```
720
-
721
- ---
722
-
723
- ## TypeScript 类型
724
-
725
- es-plus 导出以下 TypeScript 接口,可直接导入使用:
726
-
727
- ```typescript
728
- import type {
729
- FormItemOption,
730
- BtnConfig,
731
- LayoutFormProps,
732
- TableColumn,
733
- TableOptions,
734
- PaginationConfig,
735
- DialogOptions,
736
- ApiParams,
737
- EsFormInstance,
738
- EsTableInstance
739
- } from 'es-plus'
740
- ```
741
-
742
- | 接口 | 说明 |
743
- |------|------|
744
- | `FormItemOption` | 表单项配置 |
745
- | `BtnConfig` | 按钮配置 |
746
- | `LayoutFormProps` | 表单布局配置 |
747
- | `TableColumn` | 表格列配置 |
748
- | `TableOptions` | 表格选项配置 |
749
- | `PaginationConfig` | 分页配置 |
750
- | `DialogOptions` | 弹窗选项配置 |
751
- | `ApiParams` | API 请求配置 |
752
- | `EsFormInstance` | EsForm 暴露的方法类型 |
753
- | `EsTableInstance` | EsTable 暴露的方法类型 |
754
-
755
- ---
756
-
757
- ## 表单+表格+弹窗联动
758
-
759
- es-plus 的核心优势在于 EsForm、EsTable、useDialog 三者的深度联动,实现配置即开发。
760
-
761
- ### 零代码查询
762
-
763
- 将 EsForm 放入 EsTable 的 default 插槽,按钮设置 `triggerEvent: true`,即可实现零事件代码的查询/重置:
764
-
765
- ```vue
766
- <es-table
767
- :columns="columns"
768
- :options="tableOptions"
769
- v-model:data-source="tableData"
770
- v-model:pagination="pagination"
771
- >
772
- <es-form
773
- :model="queryModel"
774
- :form-item-list="queryItems"
775
- :config-btn="queryBtns"
776
- />
777
- </es-table>
778
- ```
779
-
780
- ```typescript
781
- const queryModel = reactive({ keyword: '', status: '' })
782
- const queryItems = [
783
- { prop: 'keyword', label: '关键词', formtype: 'Input', span: 6 },
784
- { prop: 'status', label: '状态', formtype: 'Select', span: 6, dataOptions: [...] }
785
- ]
786
- const queryBtns = [
787
- { name: '查询', type: 'primary', key: 'query', triggerEvent: true },
788
- { name: '重置', key: 'reset', triggerEvent: true }
789
- ]
790
- const tableOptions = {
791
- httpRequest: mockRequest,
792
- apiParams: { url: '/api/list', method: 'GET', model: queryModel },
793
- configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' }
794
- }
795
- ```
796
-
797
- > `triggerEvent: true` + `key: 'query'` → EsForm 自动调用父级 EsTable 的 `httpRequestInstance`;`key: 'reset'` → 自动重置表单。
798
-
799
- ### CRUD 弹窗
800
-
801
- useDialog + JSX EsForm 实现增/编辑弹窗:
802
-
803
- ```tsx
804
- const dialog = useDialog()
805
-
806
- function openEditDialog(row) {
807
- const formData = reactive({ ...row })
808
- dialog({
809
- title: '编辑',
810
- width: '600px',
811
- render: (h, { registerRef }) => (
812
- <EsForm
813
- ref={(el) => { if (el) registerRef('formRef', el) }}
814
- model={formData}
815
- formItemList={[
816
- { prop: 'name', label: '名称', formtype: 'Input', span: 24 },
817
- { prop: 'status', label: '状态', formtype: 'Select', span: 24, dataOptions: [...] }
818
- ]}
819
- />
820
- ),
821
- configBtn: [
822
- { name: '取消', click: (_, { close }) => close() },
823
- { name: '保存', type: 'primary', click: (_, { close, getRefs }) => {
824
- getRefs('formRef')?.validate().then(() => {
825
- // 保存逻辑...
826
- close()
827
- tableRef.value?.httpRequestInstance() // 刷新表格
828
- })
829
- }}
830
- ]
831
- })
832
- }
833
- ```
834
-
835
- ### 弹窗内嵌套表格
836
-
837
- ```tsx
838
- dialog({
839
- title: '选择商品',
840
- width: '800px',
841
- render: (h, { registerRef }) => (
842
- <EsTable
843
- ref={(el) => { if (el) registerRef('tableRef', el) }}
844
- dataSource={productList}
845
- columns={productColumns}
846
- options={{ border: true, multiSelect: true, rowkey: 'id' }}
847
- @selection-change={(rows) => selectedRows = rows}
848
- />
849
- ),
850
- configBtn: [
851
- { name: '取消', click: (_, { close }) => close() },
852
- { name: '确定', type: 'primary', click: (_, { close, getRefs }) => {
853
- const selection = getRefs('tableRef')?.getSelectionRows() || []
854
- // 处理选中数据...
855
- close()
856
- }}
857
- ]
858
- })
859
- ```
860
-
861
- ---
862
-
863
- ## 常见问题
864
-
865
- ### CSS 未加载
866
-
867
- 确保引入了样式文件:`import 'es-plus/dist/style.css'`
868
-
869
- ### 图标不显示
870
-
871
- 确保安装了 `@element-plus/icons-vue`:`npm install @element-plus/icons-vue`
872
-
873
- ### 表格高度不自适应
874
-
875
- 1. 设置 `heightType: 'height'`(不是 `'auto'`)
876
- 2. 设置 `tabHeight` 为容器高度
877
- 3. 确保父容器有固定高度
878
-
879
- ### configTableOut 映射不生效
880
-
881
- 使用简单 key 名(如 `'total'`、`'list'`),不要使用点号路径(如 `'result.pagination.total'`)。内部 `findValueByKey` 会递归查找嵌套对象。
882
-
883
- ### 切换 options 无效
884
-
885
- es-table `httpRequest`、`configTableOut`、`listenToCallBack` 等选项在挂载后不可响应。使用 `:key` 强制重建:
886
-
887
- ```vue
888
- <es-table :key="activeFormat" :options="currentOptions" ... />
889
- ```
890
-
891
- ### httpRequest 参数格式
892
-
893
- es-table 传给 `httpRequest` 的参数格式为:
894
-
895
- ```typescript
896
- {
897
- url: string,
898
- method: string,
899
- headers: Record<string, string>,
900
- formParams: Record<string, unknown>, // 合并后的查询参数
901
- pageIndex: number,
902
- pageSize: number
903
- }
904
- ```
905
-
906
- mockRequest 中应使用此模式解构:
907
-
908
- ```typescript
909
- const mockRequest = async (params) => {
910
- const { formParams, ...rest } = params || {}
911
- const { pageIndex = 1, pageSize = 10, ...filters } = { ...formParams, ...rest }
912
- // ...
913
- }
914
- ```
915
-
916
- ### 选择变化不触发 computed 更新
917
-
918
- `getSelectionRows()` 在 computed 中不是响应式的。请使用 `@selection-change` 事件 + ref:
919
-
920
- ```typescript
921
- const selectedCount = ref(0)
922
- const handleSelectionChange = (rows) => {
923
- selectedCount.value = rows?.length || 0
924
- }
925
- ```
926
-
927
- ---
928
-
929
- ## 更新日志
930
-
931
- ### v1.0.0
932
-
933
- - 初始发布
934
- - EsForm 配置化表单组件
935
- - EsTable 配置化表格组件
936
- - EsDialog 增强弹窗组件
937
- - useDialog 编程式弹窗 Hook
938
- - SvgIcon 图标组件
939
-
940
- ## License
941
-
942
- MIT
1
+ # es-plus-ui
2
+
3
+ 基于 Vue 3 + Element Plus 的企业级中后台前端组件库,以配置化驱动为核心,大幅减少中后台 CRUD 页面开发代码量。
4
+
5
+ [![npm version](https://img.shields.io/npm/v/es-plus-ui.svg)](https://www.npmjs.com/package/es-plus-ui)
6
+ [![license](https://img.shields.io/npm/l/es-plus-ui.svg)](https://www.npmjs.com/package/es-plus-ui)
7
+
8
+ ## 核心特性
9
+
10
+ - **配置化开发** — JSON 配置生成复杂表单与表格,替代大量模板代码
11
+ - **表单表格联动** — `triggerEvent` + `apiParams.model` 实现零事件代码查询
12
+ - **编程式弹窗** — `useDialog` Hook 命令式调用,支持 JSX 渲染、嵌套弹窗
13
+ - **自适应高度** — `ResizeObserver` 自动重算表格高度,表单展开/收起自动响应
14
+ - **跨页选择** — `rowkey` + `cachePageSelection` 解决分页选择丢失痛点
15
+ - **任意后端适配** — `configTableOut` + `qrcb` 配置化适配不同后端响应格式
16
+ - **TypeScript** — 完整类型定义
17
+ - **13 种表单类型** — Input、Select、datePicker、timePicker、Slider、ColorPicker、Transfer、Cascader、Radio、Checkbox、Switch、Rate、Upload
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ npm install es-plus-ui element-plus @element-plus/icons-vue
23
+ # 或
24
+ yarn add es-plus-ui element-plus @element-plus/icons-vue
25
+ # 或
26
+ pnpm add es-plus-ui element-plus @element-plus/icons-vue
27
+ ```
28
+
29
+ 前置依赖:`vue ^3.2.0`、`element-plus ^2.2.0`、`@element-plus/icons-vue ^2.1.0`
30
+
31
+ ## 快速上手
32
+
33
+ ### 全局引入
34
+
35
+ ```typescript
36
+ import { createApp } from 'vue'
37
+ import ElementPlus from 'element-plus'
38
+ import 'element-plus/dist/index.css'
39
+ import EsPlus from 'es-plus-ui'
40
+ import 'es-plus-ui/dist/style.css'
41
+ import App from './App.vue'
42
+
43
+ const app = createApp(App)
44
+ app.use(ElementPlus)
45
+ app.use(EsPlus)
46
+ app.mount('#app')
47
+ ```
48
+
49
+ ### 全局配置
50
+
51
+ 通过 `app.use(EsPlus, options)` 第二个参数配置全局默认值,避免每个组件重复传入相同的请求方法、字段映射、分页布局等配置:
52
+
53
+ ```typescript
54
+ import axios from 'axios'
55
+
56
+ const app = createApp(App)
57
+
58
+ app.use(EsPlus, {
59
+ EsTable: {
60
+ methods: {
61
+ // 全局 HTTP 请求方法,所有 EsTable 共用
62
+ $httpRequest: async ({ url, formParams, headers, ...rest }) => {
63
+ const res = await axios({ url, method: rest.method || 'GET', headers, params: formParams, ...rest })
64
+ return res.data
65
+ },
66
+ // 分页布局配置
67
+ paginationLayout: () => ({
68
+ layout: 'total, sizes, prev, pager, next, jumper',
69
+ pageSizes: [10, 20, 50, 100],
70
+ isSmall: true,
71
+ background: true
72
+ }),
73
+ // API 响应字段映射(后端返回字段 组件内部字段)
74
+ configQueryFieldOutput: () => ({
75
+ total: 'total', // 后端总数字段名
76
+ pageSize: 'pageSize', // 后端每页条数字段名
77
+ current: 'pageIndex', // 后端当前页码字段名
78
+ tableData: 'data' // 后端数据列表字段名
79
+ })
80
+ }
81
+ },
82
+ EsForm: {
83
+ methods: {
84
+ // 全局 HTTP 请求方法,所有 EsForm 共用
85
+ $httpRequest: async ({ url, formParams, headers, ...rest }) => {
86
+ const res = await axios({ url, method: rest.method || 'GET', headers, params: formParams, ...rest })
87
+ return res.data
88
+ },
89
+ // API 响应字段映射(后端返回字段 → 组件内部字段)
90
+ fieldFieldOutput: () => ({
91
+ total: 'total', // 后端总数字段名
92
+ pageSize: 'pageSize', // 后端每页条数字段名
93
+ current: 'pageIndex', // 后端当前页码字段名
94
+ listData: 'data' // 后端选项列表字段名
95
+ })
96
+ }
97
+ }
98
+ })
99
+ ```
100
+
101
+ #### 配置项说明
102
+
103
+ | 组件 | 配置键 | 类型 | 说明 |
104
+ |------|--------|------|------|
105
+ | EsTable | `$httpRequest` | `(params) => Promise` | 全局请求方法,未传 `options.httpRequest` 时使用 |
106
+ | EsTable | `paginationLayout` | `() => PaginationLayoutConfig` | 分页布局配置(layout/pageSizes/isSmall/background) |
107
+ | EsTable | `configQueryFieldOutput` | `() => FieldMap` | API 响应字段映射,未传 `options.configTableOut` 时使用 |
108
+ | EsForm | `$httpRequest` | `(params) => Promise` | 全局请求方法,未传 `formItem.httpRequest` 时使用 |
109
+ | EsForm | `fieldFieldOutput` | `(defaults) => FieldMap` | API 响应字段映射,未传 `formItem.configFormOut` 时使用 |
110
+
111
+ > **优先级**:组件 props / 选项 > 全局配置 > 组件默认值。例如 `options.configTableOut` 优先于 `configQueryFieldOutput`。
112
+
113
+ #### paginationLayout 配置
114
+
115
+ ```typescript
116
+ paginationLayout: () => ({
117
+ layout: 'total, sizes, prev, pager, next, jumper', // Element Plus 分页布局字符串
118
+ pageSizes: [10, 20, 50, 100], // 每页条数选项
119
+ isSmall: true, // 是否使用小型分页器
120
+ background: true // 是否显示背景色
121
+ })
122
+ ```
123
+
124
+ #### fieldFieldOutput / configQueryFieldOutput 配置
125
+
126
+ 函数接收默认字段映射作为参数,返回自定义映射:
127
+
128
+ ```typescript
129
+ // 默认映射(不配置时的值)
130
+ {
131
+ total: 'records',
132
+ pageSize: 'pageSize',
133
+ current: 'pageNo',
134
+ listData: 'rows' // EsForm 用 listData
135
+ // tableData: 'rows' // EsTable 用 tableData
136
+ }
137
+
138
+ // 自定义示例:后端返回 { data: { list: [...], pagination: { total: 100 } } }
139
+ fieldFieldOutput: (defaults) => ({
140
+ total: 'total',
141
+ pageSize: 'pageSize',
142
+ current: 'pageNo',
143
+ listData: 'list'
144
+ })
145
+ ```
146
+
147
+ ### 按需引入
148
+
149
+ ```typescript
150
+ import { EsForm, EsTable, useDialog } from 'es-plus-ui'
151
+ import 'es-plus-ui/dist/style.css'
152
+ ```
153
+
154
+ ### 最小示例
155
+
156
+ **EsForm**:
157
+
158
+ ```vue
159
+ <template>
160
+ <es-form :model="form" :form-item-list="items" :config-btn="btns" />
161
+ </template>
162
+
163
+ <script setup>
164
+ import { reactive } from 'vue'
165
+ const form = reactive({ keyword: '', status: '' })
166
+ const items = [
167
+ { prop: 'keyword', label: '关键词', formtype: 'Input', span: 6, attrs: { clearable: true } },
168
+ { prop: 'status', label: '状态', formtype: 'Select', span: 6, dataOptions: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }] }
169
+ ]
170
+ const btns = [
171
+ { name: '查询', type: 'primary', key: 'query', triggerEvent: true },
172
+ { name: '重置', key: 'reset', triggerEvent: true }
173
+ ]
174
+ </script>
175
+ ```
176
+
177
+ **EsTable**:
178
+
179
+ ```vue
180
+ <template>
181
+ <es-table
182
+ :columns="columns"
183
+ :options="options"
184
+ v-model:data-source="tableData"
185
+ v-model:pagination="pagination"
186
+ />
187
+ </template>
188
+
189
+ <script setup>
190
+ import { ref } from 'vue'
191
+ const tableData = ref([])
192
+ const pagination = ref({ pageSize: 10, current: 1, total: 0 })
193
+ const columns = [
194
+ { prop: 'id', label: 'ID', width: 80 },
195
+ { prop: 'name', label: '姓名' },
196
+ { prop: 'status', label: '状态' }
197
+ ]
198
+ const mockRequest = async (params) => {
199
+ const { formParams, ...rest } = params || {}
200
+ const { pageIndex = 1, pageSize = 10 } = { ...formParams, ...rest }
201
+ // 实际项目中替换为真实 API 调用
202
+ return { data: [], total: 0, pageSize, pageIndex }
203
+ }
204
+ const options = {
205
+ border: true,
206
+ httpRequest: mockRequest,
207
+ apiParams: { url: '/api/list', method: 'GET' },
208
+ configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' },
209
+ rowkey: 'id',
210
+ heightType: 'height',
211
+ tabHeight: 400
212
+ }
213
+ </script>
214
+ ```
215
+
216
+ **useDialog**:
217
+
218
+ ```tsx
219
+ import { useDialog } from 'es-plus-ui'
220
+ import EsForm from 'es-plus-ui/components/es-form'
221
+
222
+ const dialog = useDialog()
223
+ function openAddDialog() {
224
+ dialog({
225
+ title: '新增',
226
+ width: '600px',
227
+ render: (h, { registerRef }) => (
228
+ <EsForm
229
+ ref={(el) => { if (el) registerRef('formRef', el) }}
230
+ model={formData}
231
+ formItemList={[{ prop: 'name', label: '名称', formtype: 'Input', span: 24 }]}
232
+ />
233
+ ),
234
+ configBtn: [
235
+ { name: '取消', click: (_, { close }) => close() },
236
+ { name: '提交', type: 'primary', click: (_, { close, getRefs }) => {
237
+ getRefs('formRef')?.validate().then(() => { close() })
238
+ }}
239
+ ]
240
+ })
241
+ }
242
+ ```
243
+
244
+ ---
245
+
246
+ ## EsForm 表单组件
247
+
248
+ 配置化驱动的表单组件,支持 13 种输入类型、动态显隐、异步数据加载、折叠展开等功能。
249
+
250
+ ### Props
251
+
252
+ | 属性 | 类型 | 默认值 | 说明 |
253
+ |------|------|--------|------|
254
+ | model | `Record<string, unknown>` | `{}` | 表单数据对象(必填) |
255
+ | formItemList | `FormItemOption[]` | `[]` | 表单项配置数组(必填) |
256
+ | layoutFormProps | `LayoutFormProps` | `{}` | 布局配置 |
257
+ | configBtn | `BtnConfig[]` | `[]` | 按钮配置 |
258
+ | renderBtn | `Function \| boolean` | `false` | 自定义按钮渲染函数 |
259
+ | btnColSpanRow | `boolean` | `true` | 按钮是否独占一行 |
260
+ | rules | `Record<string, unknown>` | `{}` | 验证规则(Element Plus 格式) |
261
+ | fieldFieldOutput | `(defaults) => Record<string, string>` | | API 响应字段映射 |
262
+
263
+ ### FormItemOption 配置
264
+
265
+ | 字段 | 类型 | 默认值 | 说明 |
266
+ |------|------|--------|------|
267
+ | prop | `string` | — | 字段名(必填) |
268
+ | label | `string` | — | 标签文本(必填) |
269
+ | formtype | `string` | — | 输入组件类型(见下表) |
270
+ | span | `number` | `6` | 栅格占列数(24 列布局) |
271
+ | attrs | `Record<string, unknown>` | — | 透传给 Element Plus 组件的属性(placeholder、clearable 等) |
272
+ | on | `Record<string, unknown>` | — | 事件监听(change、input 等) |
273
+ | dataOptions | `Array<{ label, value }>` | — | 选项数据(Select/Radio/Checkbox/Cascader) |
274
+ | isHidden | `(model, item, formProps) => boolean` | — | 条件隐藏函数,返回 `true` 时隐藏 |
275
+ | render | `(h, model, ctx) => VNode \| string` | — | 自定义渲染函数 |
276
+ | apiParams | `ApiParams` | | 从接口加载选项数据 |
277
+ | isInitRun | `boolean` | `true` | 是否在初始化时自动加载接口数据 |
278
+ | callOptionListFormat | `(data) => unknown[]` | — | 将 API 响应转换为 dataOptions 格式 |
279
+ | httpRequest | `(params) => Promise` | — | 自定义请求方法(覆盖全局配置) |
280
+ | listenToCallBack | `Record<string, Function>` | — | 回调映射,`crtn` 用于选项格式转换 |
281
+ | width | `number \| string` | — | 字段宽度 |
282
+ | formItemOptions | `Record<string, unknown>` | — | el-form-item 附加属性(rules、labelWidth 等) |
283
+ | components | `Record<string, unknown>` | — | 自定义组件映射 |
284
+
285
+ ### formtype 类型
286
+
287
+ | 类型 | 对应组件 | 常用配置 |
288
+ |------|----------|----------|
289
+ | `Input` | ElInput | `attrs: { placeholder, clearable, type: 'textarea' }` |
290
+ | `Select` | ElSelect | `dataOptions: [{ label, value }]`, `attrs: { clearable, multiple }` |
291
+ | `datePicker` | ElDatePicker | `attrs: { type: 'daterange/datetimerange', valueFormat }` |
292
+ | `timePicker` | ElTimePicker | `attrs: { isRange }` |
293
+ | `Slider` | ElSlider | `attrs: { min, max, step }` |
294
+ | `ColorPicker` | ElColorPicker | `attrs: { showAlpha }` |
295
+ | `Transfer` | ElTransfer | `dataOptions` |
296
+ | `Cascader` | ElCascader | `dataOptions`(树形结构), `attrs: { props: { checkStrictly: true } }` |
297
+ | `Radio` | ElRadioGroup | `dataOptions: [{ label, value }]` |
298
+ | `Checkbox` | ElCheckboxGroup | `dataOptions: [{ label, value }]` |
299
+ | `Switch` | ElSwitch | `attrs: { activeText, inactiveText }` |
300
+ | `Rate` | ElRate | `attrs: { max, allowHalf }` |
301
+ | `Upload` | ElUpload | `attrs: { action, listType, limit }` |
302
+
303
+ ### LayoutFormProps 配置
304
+
305
+ | 字段 | 类型 | 说明 |
306
+ |------|------|------|
307
+ | fromLayProps | `Object` | 表单级别属性 |
308
+ | fromLayProps.labelWidth | `string` | 标签宽度,如 `'100px'` |
309
+ | fromLayProps.minFoldRows | `number` | 折叠时显示的行数,`0` 不折叠 |
310
+ | fromLayProps.isBtnHidden | `boolean` | 是否隐藏按钮区域 |
311
+ | fromLayProps.btnColSpan | `number` | 按钮列占位宽度 |
312
+ | fromLayProps.labelBtnWidth | `string \| number` | 按钮标签宽度 |
313
+ | rowLayProps | `Object` | 行级别属性 |
314
+ | rowLayProps.gutter | `number` | 栅格间距 |
315
+ | setOptions | `boolean` | 是否启用设置下拉 |
316
+
317
+ ### BtnConfig 配置
318
+
319
+ | 字段 | 类型 | 说明 |
320
+ |------|------|------|
321
+ | name | `string` | 按钮文本 |
322
+ | key | `string` | 按钮唯一标识 |
323
+ | type | `string` | 按钮类型(primary/success/warning/danger/info) |
324
+ | icon | `string` | 图标名称 |
325
+ | size | `string` | 按钮尺寸 |
326
+ | direction | `'left' \| 'right'` | 按钮位置 |
327
+ | loading | `boolean` | 加载状态 |
328
+ | disabled | `boolean \| () => boolean` | 禁用状态,支持函数形式 |
329
+ | click | `(model, formRef, httpRequestInstance?) => void` | 点击回调 |
330
+ | triggerEvent | `boolean` | `true` 时自动触发表格查询/表单重置 |
331
+
332
+ > `triggerEvent: true` + `key: 'query'` → 自动调用父级 EsTable 的 `httpRequestInstance`;`triggerEvent: true` + `key: 'reset'` → 自动重置表单。
333
+
334
+ ### Events
335
+
336
+ | 事件 | 参数 | 说明 |
337
+ |------|------|------|
338
+ | confirm | `(formRef, model)` | 点击确认按钮时触发 |
339
+ | reset | `(formRef, model)` | 点击重置按钮时触发 |
340
+
341
+ ### Methods(通过 ref 调用)
342
+
343
+ | 方法 | 说明 |
344
+ |------|------|
345
+ | `validate()` | 校验整个表单,返回 `Promise<boolean>` |
346
+ | `resetFields()` | 重置所有字段 |
347
+ | `clearValidate(props?)` | 清除校验状态 |
348
+ | `validateField(props)` | 校验指定字段 |
349
+ | `scrollToField(prop)` | 滚动到指定字段 |
350
+ | `formItmeRequestInstance(propsList)` | 手动触发指定字段的 API 数据加载 |
351
+ | `getFormRef()` | 获取底层 ElForm 实例 |
352
+
353
+ ### 高级用法
354
+
355
+ #### 条件隐藏
356
+
357
+ ```typescript
358
+ const items = [
359
+ { prop: 'type', label: '类型', formtype: 'Select', span: 6,
360
+ dataOptions: [{ label: '个人', value: 'personal' }, { label: '企业', value: 'company' }]
361
+ },
362
+ { prop: 'companyName', label: '企业名称', formtype: 'Input', span: 6,
363
+ isHidden: (model) => model.type !== 'company' // 类型不是企业时隐藏
364
+ }
365
+ ]
366
+ ```
367
+
368
+ #### 异步数据加载
369
+
370
+ ```typescript
371
+ const items = [
372
+ { prop: 'category', label: '分类', formtype: 'Select', span: 6,
373
+ apiParams: { url: '/api/categories', method: 'GET' },
374
+ callOptionListFormat: (data) => data.map(item => ({ label: item.name, value: item.id }))
375
+ }
376
+ ]
377
+ ```
378
+
379
+ #### 自定义渲染
380
+
381
+ ```tsx
382
+ const items = [
383
+ { prop: 'amount', label: '金额', span: 6,
384
+ render: (h, model) => <span style="color: red">¥{model.amount?.toLocaleString()}</span>
385
+ }
386
+ ]
387
+ ```
388
+
389
+ ---
390
+
391
+ ## EsTable 表格组件
392
+
393
+ 配置化驱动的数据表格,内置分页、远程数据、跨页选择、自适应高度等功能。
394
+
395
+ ### Props
396
+
397
+ | 属性 | 类型 | 默认值 | 说明 |
398
+ |------|------|--------|------|
399
+ | dataSource | `Record<string, unknown>[]` | `[]` | 表格数据(必填,支持 v-model) |
400
+ | columns | `TableColumn[]` | `[]` | 列配置数组(必填) |
401
+ | options | `TableOptions` | `{}` | 表格选项配置 |
402
+ | pagination | `PaginationConfig` | — | 分页配置(支持 v-model) |
403
+ | initTabHeight | `number` | `400` | 初始表格高度 |
404
+ | showHeaderBar | `boolean` | `true` | 是否显示头部栏区域 |
405
+ | headBarClass | `string \| Object` | — | 头部栏样式类 |
406
+
407
+ ### TableColumn 配置
408
+
409
+ | 字段 | 类型 | 说明 |
410
+ |------|------|------|
411
+ | prop | `string` | 数据字段名 |
412
+ | key | `string` | 列唯一标识 |
413
+ | label | `string` | 列标题 |
414
+ | width | `number \| string` | 列宽度 |
415
+ | minWidth | `number \| string` | 最小列宽度 |
416
+ | align | `string` | 对齐方式 |
417
+ | fixed | `boolean \| string` | 固定列(`'left'` / `'right'`) |
418
+ | formatter | `(row) => string` | 格式化函数 |
419
+ | render | `(h, { row, value, index }) => VNode` | 自定义渲染函数 |
420
+ | scopedSlots | `{ customRender: string }` | 插槽配置 |
421
+ | groups | `TableColumn[]` | 多级表头子列 |
422
+ | ellipsis | `boolean` | 文本溢出省略 |
423
+ | hidCol | `boolean` | 是否隐藏该列 |
424
+ | btns | `Array<{ name, type?, clickEvent? }>` | 行操作按钮 |
425
+ | type | `string` | 特殊列类型(`'selection'`、`'expand'`、`'index'`) |
426
+
427
+ #### render 函数
428
+
429
+ ```tsx
430
+ {
431
+ prop: 'status', label: '状态', width: 100,
432
+ render: (_, { row }) => {
433
+ const map = { active: 'success', leave: 'warning', resigned: 'danger' }
434
+ return <ElTag type={map[row.status]} size="small">{row.status}</ElTag>
435
+ }
436
+ }
437
+ ```
438
+
439
+ #### btns 行操作按钮
440
+
441
+ ```typescript
442
+ {
443
+ prop: 'operate', label: '操作', width: 160,
444
+ btns: [
445
+ { name: '编辑', type: 'primary', clickEvent: (row) => openEditDialog(row) },
446
+ { name: '删除', type: 'danger', clickEvent: (row) => handleDelete(row) }
447
+ ]
448
+ }
449
+ ```
450
+
451
+ #### 多级表头
452
+
453
+ ```typescript
454
+ {
455
+ label: '基本信息',
456
+ groups: [
457
+ { prop: 'name', label: '姓名' },
458
+ { prop: 'age', label: '年龄' },
459
+ { label: '地址', groups: [
460
+ { prop: 'province', label: '省份' },
461
+ { prop: 'city', label: '城市' }
462
+ ]}
463
+ ]
464
+ }
465
+ ```
466
+
467
+ ### TableOptions 配置
468
+
469
+ | 字段 | 类型 | 默认值 | 说明 |
470
+ |------|------|--------|------|
471
+ | border | `boolean` | `false` | 是否显示边框 |
472
+ | stripe | `boolean` | `false` | 是否显示斑马纹 |
473
+ | size | `'large' \| 'default' \| 'small'` | `'small'` | 表格尺寸 |
474
+ | headerCellStyle | `Record<string, unknown>` | `{ background: '#f5f7fa' }` | 表头样式 |
475
+ | highlightCurrentRow | `boolean` | `true` | 高亮当前行 |
476
+ | multiSelect | `boolean` | `false` | 启用多选列 |
477
+ | expand | `boolean` | `false` | 启用展开行 |
478
+ | snIndex | `boolean` | `false` | 显示序号列 |
479
+ | loading | `boolean` | `false` | 加载状态 |
480
+ | heightType | `'height' \| 'auto'` | — | 高度类型(`'height'` 推荐,`'auto'` 为 maxHeight) |
481
+ | tabHeight | `number \| string` | | 表格容器高度(配合 heightType 使用) |
482
+ | cachePageSelection | `boolean` | `true` | 启用跨页选择缓存 |
483
+ | rowkey | `string` | — | 行唯一标识字段名(跨页选择必填) |
484
+ | isInitRun | `boolean` | — | 初始化时是否自动请求数据 |
485
+ | httpRequest | `(params) => Promise` | — | 自定义请求方法 |
486
+ | apiParams | `ApiParams` | — | API 请求配置 |
487
+ | configTableOut | `Record<string, string>` | — | 响应字段映射 |
488
+ | listenToCallBack | `Record<string, Function>` | — | 请求/响应回调管线 |
489
+ | entryQuery | `Record<string, unknown>` | — | 默认查询参数 |
490
+ | actionUrl | `string` | — | 请求地址(简写) |
491
+
492
+ ### configTableOut 字段映射
493
+
494
+ 映射后端响应字段到表格内部使用的字段:
495
+
496
+ ```typescript
497
+ configTableOut: {
498
+ total: 'total', // 总数对应的字段名
499
+ tableData: 'data', // 数据列表对应的字段名
500
+ pageSize: 'pageSize', // 每页条数对应的字段名
501
+ current: 'pageIndex' // 当前页码对应的字段名
502
+ }
503
+ ```
504
+
505
+ > **注意**:`configTableOut` 的值应使用简单 key 名(如 `'total'`、`'list'`),不支持点号路径(如 `'result.pagination.total'`)。内部使用 `findValueByKey` 递归查找嵌套对象中的 key,无需写完整路径。
506
+
507
+ ### listenToCallBack 回调管线
508
+
509
+ ```typescript
510
+ listenToCallBack: {
511
+ // 请求前回调(Before Request CallBack)— 转换请求参数
512
+ brcb: (params) => {
513
+ return { ...params, timestamp: Date.now() }
514
+ },
515
+ // 请求后回调(Query Result CallBack)— 转换响应数据
516
+ qrcb: (res) => {
517
+ if (!res?.data) return res
518
+ return {
519
+ ...res,
520
+ data: res.data.map(item => ({
521
+ id: item.emp_id,
522
+ name: item.emp_name // 后端蛇形字段转前端驼峰
523
+ }))
524
+ }
525
+ }
526
+ }
527
+ ```
528
+
529
+ ### PaginationConfig 配置
530
+
531
+ | 字段 | 类型 | 说明 |
532
+ |------|------|------|
533
+ | pageSize | `number` | 每页条数 |
534
+ | current | `number` | 当前页码 |
535
+ | total | `number` | 总条数 |
536
+ | pageSizes | `number[]` | 每页条数选项 |
537
+ | size | `'large' \| 'default' \| 'small'` | 分页器尺寸 |
538
+ | isSmall | `boolean` | 使用小型分页器 |
539
+
540
+ ### Events
541
+
542
+ | 事件 | 参数 | 说明 |
543
+ |------|------|------|
544
+ | update:dataSource | `(data)` | 数据更新 |
545
+ | update:pagination | `(pagination)` | 分页更新 |
546
+ | selection-change | `(rows)` | 选择变化(通过 fallthrough attrs 传递) |
547
+ | pagination-current-change | `(current)` | 页码变化 |
548
+ | size-change | `(pageSize)` | 每页条数变化 |
549
+ | change-table-sort | `(sort)` | 排序变化 |
550
+
551
+ ### Methods(通过 ref 调用)
552
+
553
+ | 方法 | 说明 |
554
+ |------|------|
555
+ | `httpRequestInstance(model?)` | 手动触发表格数据请求,可传入额外查询参数 |
556
+ | `getSelectionRows()` | 获取当前选中行(含跨页缓存) |
557
+ | `clearSelection()` | 清除当前页选择 |
558
+ | `clearAllSelection()` | 清除所有页面选择(含跨页缓存) |
559
+ | `refresh()` | 强制重新计算表格布局(`doLayout`) |
560
+
561
+ ### 高级用法
562
+
563
+ #### 远程数据请求
564
+
565
+ ```typescript
566
+ const mockRequest = async (params) => {
567
+ const { formParams, ...rest } = params || {}
568
+ const { pageIndex = 1, pageSize = 10, ...filters } = { ...formParams, ...rest }
569
+ const res = await fetch('/api/list')
570
+ const data = await res.json()
571
+ const total = data.length
572
+ const start = (pageIndex - 1) * pageSize
573
+ return { data: data.slice(start, start + pageSize), total, pageSize, pageIndex }
574
+ }
575
+
576
+ const options = {
577
+ border: true,
578
+ httpRequest: mockRequest,
579
+ apiParams: { url: '/api/list', method: 'GET', model: queryModel },
580
+ configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' },
581
+ rowkey: 'id'
582
+ }
583
+ ```
584
+
585
+ #### 跨页选择持久化
586
+
587
+ ```vue
588
+ <es-table
589
+ ref="tableRef"
590
+ :columns="columns"
591
+ :options="{ rowkey: 'id', cachePageSelection: true, multiSelect: true }"
592
+ @selection-change="onSelectionChange"
593
+ />
594
+
595
+ <script setup>
596
+ const tableRef = ref(null)
597
+ const selectedCount = ref(0)
598
+
599
+ const onSelectionChange = (rows) => {
600
+ selectedCount.value = rows?.length || 0
601
+ }
602
+
603
+ function getSelected() {
604
+ return tableRef.value?.getSelectionRows() || []
605
+ }
606
+
607
+ function clearSelected() {
608
+ tableRef.value?.clearAllSelection()
609
+ }
610
+ </script>
611
+ ```
612
+
613
+ #### 自适应高度
614
+
615
+ ```typescript
616
+ const options = {
617
+ heightType: 'height', // 必须 'height',非 'auto'
618
+ tabHeight: 400 // 容器高度,表格自动 = 容器 - 表单 - 分页
619
+ }
620
+ ```
621
+
622
+ > 表单展开/收起时,`ResizeObserver` 自动触发高度重算,无需手动监听。
623
+
624
+ #### 动态 options 需使用 `:key`
625
+
626
+ > es-table `httpRequest`、`configTableOut`、`listenToCallBack` 等选项在挂载后不可动态响应。如需切换,请使用 `:key` 强制重建:
627
+
628
+ ```vue
629
+ <es-table :key="activeFormat" :options="currentOptions" ... />
630
+ ```
631
+
632
+ ---
633
+
634
+ ## EsDialog 弹窗组件
635
+
636
+ 模板式增强弹窗,支持拖拽、全屏切换、自定义头尾等功能。
637
+
638
+ ### Props
639
+
640
+ | 属性 | 类型 | 默认值 | 说明 |
641
+ |------|------|--------|------|
642
+ | title | `string` | — | 弹窗标题 |
643
+ | visible | `boolean` | `false` | 显示状态(支持 v-model) |
644
+ | width | `string \| number` | `'50%'` | 弹窗宽度 |
645
+ | isDraggable | `boolean` | `false` | 是否可拖拽 |
646
+ | fullscreen | `boolean` | `false` | 是否全屏 |
647
+ | hiddenFullBtn | `boolean` | `false` | 隐藏全屏切换按钮 |
648
+ | isHiddenFooter | `boolean` | `false` | 隐藏底部按钮区 |
649
+ | maxHeight | `string \| number` | — | 内容区最大高度 |
650
+ | appendTo | `string \| HTMLElement` | — | 挂载目标 |
651
+ | confirmText | `string` | — | 确认按钮文本 |
652
+ | cancelText | `string` | — | 取消按钮文本 |
653
+ | configBtn | `BtnConfig[]` | `[]` | 底部按钮配置 |
654
+ | render | `Function` | — | 自定义内容渲染函数 |
655
+ | renderHeader | `Function` | — | 自定义头部渲染函数 |
656
+ | renderFooter | `Function` | | 自定义底部渲染函数 |
657
+
658
+ ### Events
659
+
660
+ | 事件 | 说明 |
661
+ |------|------|
662
+ | update:visible | 显示状态变化 |
663
+ | closed | 弹窗关闭后触发 |
664
+ | submit | 点击确认时触发 |
665
+
666
+ ---
667
+
668
+ ## useDialog 编程式弹窗 Hook
669
+
670
+ 命令式调用弹窗,支持 JSX 渲染、表单集成、嵌套弹窗等高级功能。
671
+
672
+ ### 基本用法
673
+
674
+ ```typescript
675
+ import { useDialog } from 'es-plus-ui'
676
+
677
+ const dialog = useDialog()
678
+
679
+ // 打开弹窗
680
+ dialog({
681
+ title: '提示',
682
+ width: '500px',
683
+ render: (h) => <div>弹窗内容</div>,
684
+ configBtn: [
685
+ { name: '确定', type: 'primary', click: (_, { close }) => close() }
686
+ ]
687
+ })
688
+ ```
689
+
690
+ ### DialogOptions 配置
691
+
692
+ | 参数 | 类型 | 默认值 | 说明 |
693
+ |------|------|--------|------|
694
+ | title | `string` | — | 弹窗标题 |
695
+ | width | `string \| number` | `'50%'` | 弹窗宽度 |
696
+ | key | `string` | — | 唯一标识(相同 key 复用实例) |
697
+ | height | `string \| number` | — | 弹窗高度 |
698
+ | maxHeight | `string \| number` | — | 内容区最大高度 |
699
+ | render | `(h, instance, components) => VNode` | — | 内容渲染函数 |
700
+ | renderHeader | `(h, instance) => VNode` | — | 头部渲染函数 |
701
+ | renderFooter | `(h, instance) => VNode` | — | 底部渲染函数 |
702
+ | configBtn | `BtnConfig[]` | `[]` | 底部按钮配置 |
703
+ | isDraggable | `boolean` | `false` | 是否可拖拽 |
704
+ | fullscreen | `boolean` | `false` | 是否全屏 |
705
+ | hiddenFullBtn | `boolean` | `false` | 隐藏全屏按钮 |
706
+ | isHiddenFooter | `boolean` | `false` | 隐藏底部 |
707
+ | center | `boolean` | — | 垂直居中 |
708
+ | closeOnClickModal | `boolean` | `false` | 点击遮罩关闭 |
709
+ | closeOnPressEscape | `boolean` | `false` | ESC 关闭 |
710
+ | showClose | `boolean` | `true` | 显示关闭按钮 |
711
+ | destroyOnClose | `boolean` | — | 关闭时销毁 |
712
+ | showDefaultButtons | `boolean` | — | 显示默认确定/取消按钮 |
713
+ | loading | `boolean` | `false` | 加载状态 |
714
+ | customClass | `string` | — | 自定义样式类 |
715
+ | appendToBody | `boolean` | — | 挂载到 body |
716
+ | appendTo | `string \| HTMLElement` | — | 挂载目标 |
717
+ | modal | `boolean` | | 显示遮罩 |
718
+ | lockScroll | `boolean` | — | 锁定滚动 |
719
+ | onlyInstance | `boolean` | — | 单实例模式(复用同一弹窗) |
720
+ | onSubmit | `(close) => void` | — | 提交回调 |
721
+ | onClosed | `() => void` | — | 关闭回调 |
722
+ | onOpen | `() => void` | — | 打开回调 |
723
+
724
+ ### configBtn click 回调签名
725
+
726
+ ```typescript
727
+ {
728
+ name: '提交',
729
+ type: 'primary',
730
+ click: (instance, { close, getRefs, dialogVm }) => {
731
+ // instance: 渲染组件的内部实例
732
+ // close(): 关闭弹窗
733
+ // getRefs(name): 获取通过 registerRef 注册的引用
734
+ // dialogVm: 弹窗组件实例
735
+ }
736
+ }
737
+ ```
738
+
739
+ ### registerRef + getRefs 模式
740
+
741
+ 在 render 中使用 `registerRef` 注册引用,在 configBtn 的 click 中通过 `getRefs` 获取:
742
+
743
+ ```tsx
744
+ dialog({
745
+ title: '编辑',
746
+ render: (h, { registerRef }) => (
747
+ <EsForm
748
+ ref={(el) => { if (el) registerRef('formRef', el) }}
749
+ model={formData}
750
+ formItemList={formItems}
751
+ />
752
+ ),
753
+ configBtn: [
754
+ { name: '取消', click: (_, { close }) => close() },
755
+ { name: '提交', type: 'primary', click: (_, { close, getRefs }) => {
756
+ const formRef = getRefs('formRef')
757
+ formRef?.validate().then(() => {
758
+ // 提交逻辑...
759
+ close()
760
+ })
761
+ }}
762
+ ]
763
+ })
764
+ ```
765
+
766
+ ### onlyInstance 模式
767
+
768
+ ```typescript
769
+ // 默认:每次调用创建新弹窗
770
+ const dialog = useDialog()
771
+
772
+ // 单实例模式:复用同一弹窗,后续调用更新内容
773
+ const singleDialog = useDialog(null, { onlyInstance: true })
774
+ ```
775
+
776
+ ### 多实例独立弹窗
777
+
778
+ ```typescript
779
+ // 创建多个独立弹窗,可同时打开
780
+ const dialog1 = useDialog()
781
+ const dialog2 = useDialog()
782
+
783
+ // 嵌套弹窗:父弹窗内打开子弹窗
784
+ dialog1({
785
+ title: '父弹窗',
786
+ render: (h) => (
787
+ <div>
788
+ <ElButton onClick={() => dialog2({ title: '子弹窗', render: (h) => <div>嵌套内容</div> })}>
789
+ 打开子弹窗
790
+ </ElButton>
791
+ </div>
792
+ )
793
+ })
794
+ ```
795
+
796
+ ---
797
+
798
+ ## SvgIcon 图标组件
799
+
800
+ 支持外部 URL 图标和 SVG Symbol Sprite 的图标组件。
801
+
802
+ ### Props
803
+
804
+ | 属性 | 类型 | 默认值 | 说明 |
805
+ |------|------|--------|------|
806
+ | iconClass | `string` | — | 图标名称或外部 URL(必填) |
807
+ | className | `string` | — | 额外样式类 |
808
+
809
+ ### 使用
810
+
811
+ ```vue
812
+ <!-- SVG Sprite 图标 -->
813
+ <svg-icon icon-class="user" />
814
+
815
+ <!-- 外部 URL 图标(自动检测 http/https 开头) -->
816
+ <svg-icon icon-class="https://example.com/icon.svg" />
817
+ ```
818
+
819
+ ---
820
+
821
+ ## TypeScript 类型
822
+
823
+ es-plus-ui 导出以下 TypeScript 接口,可直接导入使用:
824
+
825
+ ```typescript
826
+ import type {
827
+ FormItemOption,
828
+ BtnConfig,
829
+ LayoutFormProps,
830
+ TableColumn,
831
+ TableOptions,
832
+ PaginationConfig,
833
+ DialogOptions,
834
+ ApiParams,
835
+ EsFormInstance,
836
+ EsTableInstance
837
+ } from 'es-plus-ui'
838
+ ```
839
+
840
+ | 接口 | 说明 |
841
+ |------|------|
842
+ | `FormItemOption` | 表单项配置 |
843
+ | `BtnConfig` | 按钮配置 |
844
+ | `LayoutFormProps` | 表单布局配置 |
845
+ | `TableColumn` | 表格列配置 |
846
+ | `TableOptions` | 表格选项配置 |
847
+ | `PaginationConfig` | 分页配置 |
848
+ | `DialogOptions` | 弹窗选项配置 |
849
+ | `ApiParams` | API 请求配置 |
850
+ | `EsFormInstance` | EsForm 暴露的方法类型 |
851
+ | `EsTableInstance` | EsTable 暴露的方法类型 |
852
+
853
+ ---
854
+
855
+ ## 表单+表格+弹窗联动
856
+
857
+ es-plus-ui 的核心优势在于 EsForm、EsTable、useDialog 三者的深度联动,实现配置即开发。
858
+
859
+ ### 零代码查询
860
+
861
+ 将 EsForm 放入 EsTable 的 default 插槽,按钮设置 `triggerEvent: true`,即可实现零事件代码的查询/重置:
862
+
863
+ ```vue
864
+ <es-table
865
+ :columns="columns"
866
+ :options="tableOptions"
867
+ v-model:data-source="tableData"
868
+ v-model:pagination="pagination"
869
+ >
870
+ <es-form
871
+ :model="queryModel"
872
+ :form-item-list="queryItems"
873
+ :config-btn="queryBtns"
874
+ />
875
+ </es-table>
876
+ ```
877
+
878
+ ```typescript
879
+ const queryModel = reactive({ keyword: '', status: '' })
880
+ const queryItems = [
881
+ { prop: 'keyword', label: '关键词', formtype: 'Input', span: 6 },
882
+ { prop: 'status', label: '状态', formtype: 'Select', span: 6, dataOptions: [...] }
883
+ ]
884
+ const queryBtns = [
885
+ { name: '查询', type: 'primary', key: 'query', triggerEvent: true },
886
+ { name: '重置', key: 'reset', triggerEvent: true }
887
+ ]
888
+ const tableOptions = {
889
+ httpRequest: mockRequest,
890
+ apiParams: { url: '/api/list', method: 'GET', model: queryModel },
891
+ configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' }
892
+ }
893
+ ```
894
+
895
+ > `triggerEvent: true` + `key: 'query'` → EsForm 自动调用父级 EsTable 的 `httpRequestInstance`;`key: 'reset'` → 自动重置表单。
896
+
897
+ ### CRUD 弹窗
898
+
899
+ useDialog + JSX EsForm 实现增/编辑弹窗:
900
+
901
+ ```tsx
902
+ const dialog = useDialog()
903
+
904
+ function openEditDialog(row) {
905
+ const formData = reactive({ ...row })
906
+ dialog({
907
+ title: '编辑',
908
+ width: '600px',
909
+ render: (h, { registerRef }) => (
910
+ <EsForm
911
+ ref={(el) => { if (el) registerRef('formRef', el) }}
912
+ model={formData}
913
+ formItemList={[
914
+ { prop: 'name', label: '名称', formtype: 'Input', span: 24 },
915
+ { prop: 'status', label: '状态', formtype: 'Select', span: 24, dataOptions: [...] }
916
+ ]}
917
+ />
918
+ ),
919
+ configBtn: [
920
+ { name: '取消', click: (_, { close }) => close() },
921
+ { name: '保存', type: 'primary', click: (_, { close, getRefs }) => {
922
+ getRefs('formRef')?.validate().then(() => {
923
+ // 保存逻辑...
924
+ close()
925
+ tableRef.value?.httpRequestInstance() // 刷新表格
926
+ })
927
+ }}
928
+ ]
929
+ })
930
+ }
931
+ ```
932
+
933
+ ### 弹窗内嵌套表格
934
+
935
+ ```tsx
936
+ dialog({
937
+ title: '选择商品',
938
+ width: '800px',
939
+ render: (h, { registerRef }) => (
940
+ <EsTable
941
+ ref={(el) => { if (el) registerRef('tableRef', el) }}
942
+ dataSource={productList}
943
+ columns={productColumns}
944
+ options={{ border: true, multiSelect: true, rowkey: 'id' }}
945
+ @selection-change={(rows) => selectedRows = rows}
946
+ />
947
+ ),
948
+ configBtn: [
949
+ { name: '取消', click: (_, { close }) => close() },
950
+ { name: '确定', type: 'primary', click: (_, { close, getRefs }) => {
951
+ const selection = getRefs('tableRef')?.getSelectionRows() || []
952
+ // 处理选中数据...
953
+ close()
954
+ }}
955
+ ]
956
+ })
957
+ ```
958
+
959
+ ---
960
+
961
+ ## 常见问题
962
+
963
+ ### CSS 未加载
964
+
965
+ 确保引入了样式文件:`import 'es-plus-ui/dist/style.css'`
966
+
967
+ ### 图标不显示
968
+
969
+ 确保安装了 `@element-plus/icons-vue`:`npm install @element-plus/icons-vue`
970
+
971
+ ### 表格高度不自适应
972
+
973
+ 1. 设置 `heightType: 'height'`(不是 `'auto'`)
974
+ 2. 设置 `tabHeight` 为容器高度
975
+ 3. 确保父容器有固定高度
976
+
977
+ ### configTableOut 映射不生效
978
+
979
+ 使用简单 key 名(如 `'total'`、`'list'`),不要使用点号路径(如 `'result.pagination.total'`)。内部 `findValueByKey` 会递归查找嵌套对象。
980
+
981
+ ### 切换 options 无效
982
+
983
+ es-table 的 `httpRequest`、`configTableOut`、`listenToCallBack` 等选项在挂载后不可响应。使用 `:key` 强制重建:
984
+
985
+ ```vue
986
+ <es-table :key="activeFormat" :options="currentOptions" ... />
987
+ ```
988
+
989
+ ### httpRequest 参数格式
990
+
991
+ es-table 传给 `httpRequest` 的参数格式为:
992
+
993
+ ```typescript
994
+ {
995
+ url: string,
996
+ method: string,
997
+ headers: Record<string, string>,
998
+ formParams: Record<string, unknown>, // 合并后的查询参数
999
+ pageIndex: number,
1000
+ pageSize: number
1001
+ }
1002
+ ```
1003
+
1004
+ mockRequest 中应使用此模式解构:
1005
+
1006
+ ```typescript
1007
+ const mockRequest = async (params) => {
1008
+ const { formParams, ...rest } = params || {}
1009
+ const { pageIndex = 1, pageSize = 10, ...filters } = { ...formParams, ...rest }
1010
+ // ...
1011
+ }
1012
+ ```
1013
+
1014
+ ### 选择变化不触发 computed 更新
1015
+
1016
+ `getSelectionRows()` 在 computed 中不是响应式的。请使用 `@selection-change` 事件 + ref:
1017
+
1018
+ ```typescript
1019
+ const selectedCount = ref(0)
1020
+ const handleSelectionChange = (rows) => {
1021
+ selectedCount.value = rows?.length || 0
1022
+ }
1023
+ ```
1024
+
1025
+ ---
1026
+
1027
+ ## 更新日志
1028
+
1029
+ ### v1.0.0
1030
+
1031
+ - 初始发布
1032
+ - EsForm 配置化表单组件
1033
+ - EsTable 配置化表格组件
1034
+ - EsDialog 增强弹窗组件
1035
+ - useDialog 编程式弹窗 Hook
1036
+ - SvgIcon 图标组件
1037
+
1038
+ ## License
1039
+
1040
+ MIT