@es-plus/vue3 1.4.0

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.
Files changed (49) hide show
  1. package/README.md +1279 -0
  2. package/dist/es-plus.js +2922 -0
  3. package/dist/es-plus.js.map +1 -0
  4. package/dist/es-plus.umd.cjs +2 -0
  5. package/dist/es-plus.umd.cjs.map +1 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/resolver.cjs +101 -0
  8. package/dist/resolver.d.ts +25 -0
  9. package/dist/resolver.mjs +78 -0
  10. package/dist/src/components/es-crud-page/index.d.ts +4 -0
  11. package/dist/src/components/es-crud-page/src/es-crud-page.vue.d.ts +78 -0
  12. package/dist/src/components/es-crud-page/src/types.d.ts +180 -0
  13. package/dist/src/components/es-dialog/index.d.ts +3 -0
  14. package/dist/src/components/es-dialog/src/component.vue.d.ts +60 -0
  15. package/dist/src/components/es-dialog/src/use-dialog.d.ts +16 -0
  16. package/dist/src/components/es-form/index.d.ts +3 -0
  17. package/dist/src/components/es-form/src/es-form.vue.d.ts +83 -0
  18. package/dist/src/components/es-table/index.d.ts +3 -0
  19. package/dist/src/components/es-table/src/column-item.vue.d.ts +82 -0
  20. package/dist/src/components/es-table/src/component.vue.d.ts +154 -0
  21. package/dist/src/components/es-table/src/engines/types.d.ts +25 -0
  22. package/dist/src/components/es-table/src/engines/use-column-adapter.d.ts +44 -0
  23. package/dist/src/components/es-table/src/engines/use-virtual-selection.d.ts +13 -0
  24. package/dist/src/components/es-table/src/engines/use-virtual-sort.d.ts +24 -0
  25. package/dist/src/components/es-table/src/engines/virtual-engine.vue.d.ts +63 -0
  26. package/dist/src/components/es-table/src/table-btns.vue.d.ts +19 -0
  27. package/dist/src/components/svg-icon/index.d.ts +3 -0
  28. package/dist/src/components/svg-icon/src/svg-icon.vue.d.ts +17 -0
  29. package/dist/src/composables/use-form-inputs.d.ts +12 -0
  30. package/dist/src/composables/use-form-layout.d.ts +51 -0
  31. package/dist/src/composables/use-form-request.d.ts +18 -0
  32. package/dist/src/composables/use-table-resize.d.ts +19 -0
  33. package/dist/src/composables/use-table-selection.d.ts +16 -0
  34. package/dist/src/config.d.ts +10 -0
  35. package/dist/src/index.d.ts +17 -0
  36. package/dist/src/resolver.d.ts +44 -0
  37. package/dist/src/style.d.ts +0 -0
  38. package/dist/src/types/index.d.ts +174 -0
  39. package/dist/src/utils/shared.d.ts +17 -0
  40. package/dist/style.css +1 -0
  41. package/package.json +90 -0
  42. package/schemas/README.md +83 -0
  43. package/schemas/api-params.schema.json +36 -0
  44. package/schemas/btn-config.schema.json +77 -0
  45. package/schemas/dialog-options.schema.json +149 -0
  46. package/schemas/form-item.schema.json +146 -0
  47. package/schemas/index.schema.json +71 -0
  48. package/schemas/table-column.schema.json +118 -0
  49. package/schemas/table-options.schema.json +141 -0
package/README.md ADDED
@@ -0,0 +1,1279 @@
1
+ # @es-plus/vue3
2
+
3
+ 基于 Vue 3 + Element Plus 的企业级中后台 CRUD 组件库 — 配置化驱动表单、表格、弹窗全链路联动
4
+
5
+ [![npm version](https://img.shields.io/npm/v/%40es-plus%2Fvue3.svg)](https://www.npmjs.com/package/@es-plus/vue3)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40es-plus%2Fvue3.svg)](https://www.npmjs.com/package/@es-plus/vue3)
7
+ [![license](https://img.shields.io/npm/l/%40es-plus%2Fvue3.svg)](https://www.npmjs.com/package/@es-plus/vue3)
8
+ [![github stars](https://img.shields.io/github/stars/liujiaao/es-plus?style=social)](https://github.com/liujiaao/es-plus)
9
+
10
+ **[在线文档](https://liujiaao.github.io/es-plus/)** · **[Playground](https://liujiaao.github.io/es-plus/#/playground)** · **[GitHub](https://github.com/liujiaao/es-plus)** · **[更新日志](https://github.com/liujiaao/es-plus/releases)** · **[Vue 2 版本](https://www.npmjs.com/package/@es-plus/vue2)**
11
+
12
+ > **v1.4.0 起重命名**:原包 [`es-plus-ui`](https://www.npmjs.com/package/es-plus-ui) 已重命名为 `@es-plus/vue3`,与 Vue 2 版本 [`@es-plus/vue2`](https://www.npmjs.com/package/@es-plus/vue2) 在同一 scope 下统一管理。`es-plus-ui@1.4.0+` 现在是仅做 re-export 的 stub 包并已 `npm deprecate` —— 现有项目仍能继续使用,但建议升级到 `@es-plus/vue3`。详见 [迁移指南](https://github.com/liujiaao/es-plus/blob/master/docs/migrate-v1.4.md)。
13
+
14
+ ## 核心特性
15
+
16
+ - **配置化开发** — JSON 配置生成复杂表单与表格,替代大量模板代码
17
+ - **表单↔表格↔弹窗联动** — 查询/重置/分页全自动,零事件代码
18
+ - **编程式弹窗** — `useDialog` Hook 命令式调用,支持 JSX 渲染、嵌套弹窗
19
+ - **自适应高度** — `ResizeObserver` 自动重算表格高度,表单展开/收起自动响应
20
+ - **跨页选择** — `rowkey` + `cachePageSelection` 解决分页选择丢失痛点
21
+ - **任意后端适配** — `configTableOut` + `qrcb` 配置化适配不同后端响应格式
22
+ - **权限控制** — `permissionValue` 声明式按钮权限,无需 v-if
23
+ - **国际化** — `labelKey` + 自定义翻译函数,兼容任意 i18n 方案
24
+ - **TypeScript** — 完整类型定义,11 个核心接口可导入
25
+ - **AI 原生支持** — 配套 [@es-plus/mcp-server](https://www.npmjs.com/package/@es-plus/mcp-server) 和 [@es-plus/cli](https://www.npmjs.com/package/@es-plus/cli)
26
+ - **13 种表单类型** — Input、Select、datePicker、timePicker、Slider、ColorPicker、Transfer、Cascader、Radio、Checkbox、Switch、Rate、Upload
27
+
28
+ ## 为什么选择 @es-plus/vue3?
29
+
30
+ > 同样的 CRUD 页面,传统写法 ~200 行,@es-plus/vue3 ~20 行
31
+
32
+ | 痛点 | 传统 Element Plus | @es-plus/vue3 |
33
+ |------|-------------------|------------|
34
+ | 表单字段 | 每个字段 5-8 行 `el-form-item` + `v-model` | 一行 `{ prop, label, formtype }` |
35
+ | 查询/重置 | 手动 `@click` + `resetFields()` | `triggerEvent: true` 自动处理 |
36
+ | 分页请求 | 手动绑定 `current-page` + `size-change` 事件 | 分页切换自动请求 |
37
+ | 表单↔表格 | 手动传参、手动刷新 | 自动发现、自动合并参数 |
38
+ | 弹窗管理 | 模板声明 + `visible` 状态 | `useDialog()` 编程式调用 |
39
+ | 选择丢失 | 分页后选中清空 | 跨页选择缓存 + 去重 |
40
+ | 后端适配 | 每个接口手动映射字段 | `configTableOut` 统一映射 |
41
+
42
+ ## 安装
43
+
44
+ ```bash
45
+ npm install @es-plus/vue3 element-plus @element-plus/icons-vue
46
+ # 或
47
+ yarn add @es-plus/vue3 element-plus @element-plus/icons-vue
48
+ # 或
49
+ pnpm add @es-plus/vue3 element-plus @element-plus/icons-vue
50
+ ```
51
+
52
+ 前置依赖:`vue ^3.2.0`、`element-plus ^2.2.0`、`@element-plus/icons-vue ^2.1.0`
53
+
54
+ ## 快速上手
55
+
56
+ ### 全局引入
57
+
58
+ ```typescript
59
+ import { createApp } from 'vue'
60
+ import ElementPlus from 'element-plus'
61
+ import 'element-plus/dist/index.css'
62
+ import EsPlus from '@es-plus/vue3'
63
+ import '@es-plus/vue3/dist/style.css'
64
+ import App from './App.vue'
65
+
66
+ const app = createApp(App)
67
+ app.use(ElementPlus)
68
+ app.use(EsPlus)
69
+ app.mount('#app')
70
+ ```
71
+
72
+ ### 全局配置
73
+
74
+ 通过 `app.use(EsPlus, options)` 第二个参数配置全局默认值,避免每个组件重复传入相同的请求方法、字段映射、分页布局等配置:
75
+
76
+ ```typescript
77
+ import axios from 'axios'
78
+
79
+ const app = createApp(App)
80
+
81
+ app.use(EsPlus, {
82
+ EsTable: {
83
+ methods: {
84
+ // 全局 HTTP 请求方法,所有 EsTable 共用
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
+ // 分页布局配置
90
+ paginationLayout: () => ({
91
+ layout: 'total, sizes, prev, pager, next, jumper',
92
+ pageSizes: [10, 20, 50, 100],
93
+ isSmall: true,
94
+ background: true
95
+ }),
96
+ // API 响应字段映射(后端返回字段 → 组件内部字段)
97
+ configQueryFieldOutput: () => ({
98
+ total: 'total', // 后端总数字段名
99
+ pageSize: 'pageSize', // 后端每页条数字段名
100
+ current: 'pageIndex', // 后端当前页码字段名
101
+ tableData: 'data' // 后端数据列表字段名
102
+ })
103
+ }
104
+ },
105
+ EsForm: {
106
+ methods: {
107
+ // 全局 HTTP 请求方法,所有 EsForm 共用
108
+ $httpRequest: async ({ url, formParams, headers, ...rest }) => {
109
+ const res = await axios({ url, method: rest.method || 'GET', headers, params: formParams, ...rest })
110
+ return res.data
111
+ },
112
+ // API 响应字段映射(后端返回字段 → 组件内部字段)
113
+ fieldFieldOutput: () => ({
114
+ total: 'total', // 后端总数字段名
115
+ pageSize: 'pageSize', // 后端每页条数字段名
116
+ current: 'pageIndex', // 后端当前页码字段名
117
+ listData: 'data' // 后端选项列表字段名
118
+ })
119
+ }
120
+ }
121
+ })
122
+ ```
123
+
124
+ #### 配置项说明
125
+
126
+ | 组件 | 配置键 | 类型 | 说明 |
127
+ |------|--------|------|------|
128
+ | EsTable | `$httpRequest` | `(params) => Promise` | 全局请求方法,未传 `options.httpRequest` 时使用 |
129
+ | EsTable | `paginationLayout` | `() => PaginationLayoutConfig` | 分页布局配置(layout/pageSizes/isSmall/background) |
130
+ | EsTable | `configQueryFieldOutput` | `() => FieldMap` | API 响应字段映射,未传 `options.configTableOut` 时使用 |
131
+ | EsForm | `$httpRequest` | `(params) => Promise` | 全局请求方法,未传 `formItem.httpRequest` 时使用 |
132
+ | EsForm | `fieldFieldOutput` | `(defaults) => FieldMap` | API 响应字段映射,未传 `formItem.configFormOut` 时使用 |
133
+
134
+ > **优先级**:组件 props / 选项 > 全局配置 > 组件默认值。例如 `options.configTableOut` 优先于 `configQueryFieldOutput`。
135
+
136
+ #### paginationLayout 配置
137
+
138
+ ```typescript
139
+ paginationLayout: () => ({
140
+ layout: 'total, sizes, prev, pager, next, jumper', // Element Plus 分页布局字符串
141
+ pageSizes: [10, 20, 50, 100], // 每页条数选项
142
+ isSmall: true, // 是否使用小型分页器
143
+ background: true // 是否显示背景色
144
+ })
145
+ ```
146
+
147
+ #### fieldFieldOutput / configQueryFieldOutput 配置
148
+
149
+ 函数接收默认字段映射作为参数,返回自定义映射:
150
+
151
+ ```typescript
152
+ // 默认映射(不配置时的值)
153
+ {
154
+ total: 'records',
155
+ pageSize: 'pageSize',
156
+ current: 'pageNo',
157
+ listData: 'rows' // EsForm 用 listData
158
+ // tableData: 'rows' // EsTable 用 tableData
159
+ }
160
+
161
+ // 自定义示例:后端返回 { data: { list: [...], pagination: { total: 100 } } }
162
+ fieldFieldOutput: (defaults) => ({
163
+ total: 'total',
164
+ pageSize: 'pageSize',
165
+ current: 'pageNo',
166
+ listData: 'list'
167
+ })
168
+ ```
169
+
170
+ ### 按需引入
171
+
172
+ ```typescript
173
+ import { EsForm, EsTable, useDialog } from '@es-plus/vue3'
174
+ import '@es-plus/vue3/dist/style.css'
175
+ ```
176
+
177
+ ### 最小示例
178
+
179
+ **EsForm**:
180
+
181
+ ```vue
182
+ <template>
183
+ <es-form :model="form" :form-item-list="items" :config-btn="btns" />
184
+ </template>
185
+
186
+ <script setup>
187
+ import { reactive } from 'vue'
188
+ const form = reactive({ keyword: '', status: '' })
189
+ const items = [
190
+ { prop: 'keyword', label: '关键词', formtype: 'Input', span: 6, attrs: { clearable: true } },
191
+ { prop: 'status', label: '状态', formtype: 'Select', span: 6, dataOptions: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }] }
192
+ ]
193
+ const btns = [
194
+ { name: '查询', type: 'primary', key: 'query', triggerEvent: true },
195
+ { name: '重置', key: 'rest', triggerEvent: true }
196
+ ]
197
+ </script>
198
+ ```
199
+
200
+ **EsTable**:
201
+
202
+ ```vue
203
+ <template>
204
+ <es-table
205
+ :columns="columns"
206
+ :options="options"
207
+ v-model:data-source="tableData"
208
+ v-model:pagination="pagination"
209
+ />
210
+ </template>
211
+
212
+ <script setup>
213
+ import { ref } from 'vue'
214
+ const tableData = ref([])
215
+ const pagination = ref({ pageSize: 10, current: 1, total: 0 })
216
+ const columns = [
217
+ { prop: 'id', label: 'ID', width: 80 },
218
+ { prop: 'name', label: '姓名' },
219
+ { prop: 'status', label: '状态' }
220
+ ]
221
+ const mockRequest = async (params) => {
222
+ const { formParams, ...rest } = params || {}
223
+ const { pageIndex = 1, pageSize = 10 } = { ...formParams, ...rest }
224
+ // 实际项目中替换为真实 API 调用
225
+ return { data: [], total: 0, pageSize, pageIndex }
226
+ }
227
+ const options = {
228
+ border: true,
229
+ httpRequest: mockRequest,
230
+ apiParams: { url: '/api/list', method: 'GET' },
231
+ configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' },
232
+ rowkey: 'id',
233
+ heightType: 'height',
234
+ tabHeight: 400
235
+ }
236
+ </script>
237
+ ```
238
+
239
+ **useDialog**:
240
+
241
+ ```tsx
242
+ import { useDialog } from '@es-plus/vue3'
243
+ import EsForm from '@es-plus/vue3/components/es-form'
244
+
245
+ const dialog = useDialog()
246
+ function openAddDialog() {
247
+ dialog({
248
+ title: '新增',
249
+ width: '600px',
250
+ render: (h, { registerRef }) => (
251
+ <EsForm
252
+ ref={(el) => { if (el) registerRef('formRef', el) }}
253
+ model={formData}
254
+ formItemList={[{ prop: 'name', label: '名称', formtype: 'Input', span: 24 }]}
255
+ />
256
+ ),
257
+ configBtn: [
258
+ { name: '取消', click: (_, { close }) => close() },
259
+ { name: '提交', type: 'primary', click: (_, { close, getRefs }) => {
260
+ getRefs('formRef')?.validate().then(() => { close() })
261
+ }}
262
+ ]
263
+ })
264
+ }
265
+ ```
266
+
267
+ ---
268
+
269
+ ## EsForm 表单组件
270
+
271
+ 配置化驱动的表单组件,支持 13 种输入类型、动态显隐、异步数据加载、折叠展开等功能。
272
+
273
+ ### Props
274
+
275
+ | 属性 | 类型 | 默认值 | 说明 |
276
+ |------|------|--------|------|
277
+ | model | `Record<string, unknown>` | `{}` | 表单数据对象(必填) |
278
+ | formItemList | `FormItemOption[]` | `[]` | 表单项配置数组(必填) |
279
+ | layoutFormProps | `LayoutFormProps` | `{}` | 布局配置 |
280
+ | configBtn | `BtnConfig[]` | `[]` | 按钮配置 |
281
+ | renderBtn | `Function \| boolean` | `false` | 自定义按钮渲染函数 |
282
+ | btnColSpanRow | `boolean` | `true` | 按钮是否独占一行 |
283
+ | rules | `Record<string, unknown>` | `{}` | 验证规则(Element Plus 格式) |
284
+ | fieldFieldOutput | `(defaults) => Record<string, string>` | — | API 响应字段映射 |
285
+
286
+ ### FormItemOption 配置
287
+
288
+ | 字段 | 类型 | 默认值 | 说明 |
289
+ |------|------|--------|------|
290
+ | prop | `string` | — | 字段名(必填) |
291
+ | label | `string` | — | 标签文本(必填) |
292
+ | formtype | `string` | — | 输入组件类型(见下表) |
293
+ | span | `number` | `6` | 栅格占列数(24 列布局) |
294
+ | attrs | `Record<string, unknown>` | — | 透传给 Element Plus 组件的属性(placeholder、clearable 等) |
295
+ | on | `Record<string, unknown>` | — | 事件监听(change、input 等) |
296
+ | dataOptions | `Array<{ label, value }>` | — | 选项数据(Select/Radio/Checkbox/Cascader) |
297
+ | isHidden | `(model, item, formProps) => boolean` | — | 条件隐藏函数,返回 `true` 时隐藏 |
298
+ | render | `(h, model, ctx) => VNode \| string` | — | 自定义渲染函数 |
299
+ | apiParams | `ApiParams` | — | 从接口加载选项数据 |
300
+ | isInitRun | `boolean` | `true` | 是否在初始化时自动加载接口数据 |
301
+ | callOptionListFormat | `(data) => unknown[]` | — | 将 API 响应转换为 dataOptions 格式 |
302
+ | httpRequest | `(params) => Promise` | — | 自定义请求方法(覆盖全局配置) |
303
+ | listenToCallBack | `Record<string, Function>` | — | 回调映射,`crtn` 用于选项格式转换 |
304
+ | width | `number \| string` | — | 字段宽度 |
305
+ | formItemOptions | `Record<string, unknown>` | — | el-form-item 附加属性(rules、labelWidth 等) |
306
+ | components | `Record<string, unknown>` | — | 自定义组件映射 |
307
+
308
+ ### formtype 类型
309
+
310
+ | 类型 | 对应组件 | 常用配置 |
311
+ |------|----------|----------|
312
+ | `Input` | ElInput | `attrs: { placeholder, clearable, type: 'textarea' }` |
313
+ | `Select` | ElSelect | `dataOptions: [{ label, value }]`, `attrs: { clearable, multiple }` |
314
+ | `datePicker` | ElDatePicker | `attrs: { type: 'daterange/datetimerange', valueFormat }` |
315
+ | `timePicker` | ElTimePicker | `attrs: { isRange }` |
316
+ | `Slider` | ElSlider | `attrs: { min, max, step }` |
317
+ | `ColorPicker` | ElColorPicker | `attrs: { showAlpha }` |
318
+ | `Transfer` | ElTransfer | `dataOptions` |
319
+ | `Cascader` | ElCascader | `dataOptions`(树形结构), `attrs: { props: { checkStrictly: true } }` |
320
+ | `Radio` | ElRadioGroup | `dataOptions: [{ label, value }]` |
321
+ | `Checkbox` | ElCheckboxGroup | `dataOptions: [{ label, value }]` |
322
+ | `Switch` | ElSwitch | `attrs: { activeText, inactiveText }` |
323
+ | `Rate` | ElRate | `attrs: { max, allowHalf }` |
324
+ | `Upload` | ElUpload | `attrs: { action, listType, limit }` |
325
+
326
+ ### LayoutFormProps 配置
327
+
328
+ | 字段 | 类型 | 说明 |
329
+ |------|------|------|
330
+ | fromLayProps | `Object` | 表单级别属性 |
331
+ | fromLayProps.labelWidth | `string` | 标签宽度,如 `'100px'` |
332
+ | fromLayProps.minFoldRows | `number` | 折叠时显示的行数,`0` 不折叠 |
333
+ | fromLayProps.isBtnHidden | `boolean` | 是否隐藏按钮区域 |
334
+ | fromLayProps.btnColSpan | `number` | 按钮列占位宽度 |
335
+ | fromLayProps.labelBtnWidth | `string \| number` | 按钮标签宽度 |
336
+ | rowLayProps | `Object` | 行级别属性 |
337
+ | rowLayProps.gutter | `number` | 栅格间距 |
338
+ | setOptions | `boolean` | 是否启用设置下拉 |
339
+
340
+ ### BtnConfig 配置
341
+
342
+ | 字段 | 类型 | 说明 |
343
+ |------|------|------|
344
+ | name | `string` | 按钮文本 |
345
+ | key | `string` | 按钮唯一标识 |
346
+ | type | `string` | 按钮类型(primary/success/warning/danger/info) |
347
+ | icon | `string` | 图标名称 |
348
+ | size | `string` | 按钮尺寸 |
349
+ | direction | `'left' \| 'right'` | 按钮位置 |
350
+ | loading | `boolean` | 加载状态 |
351
+ | disabled | `boolean \| () => boolean` | 禁用状态,支持函数形式 |
352
+ | click | `(model, formRef, httpRequestInstance?) => void` | 点击回调 |
353
+ | triggerEvent | `boolean` | `true` 时自动触发表格查询/表单重置 |
354
+
355
+ > `triggerEvent: true` + `key: 'query'` → 自动调用父级 EsTable 的 `httpRequestInstance`;`triggerEvent: true` + `key: 'rest'` → 自动重置表单。
356
+
357
+ ### Events
358
+
359
+ | 事件 | 参数 | 说明 |
360
+ |------|------|------|
361
+ | confirm | `(formRef, model)` | 点击确认按钮时触发 |
362
+ | reset | `(formRef, model)` | 点击重置按钮时触发 |
363
+
364
+ ### Methods(通过 ref 调用)
365
+
366
+ | 方法 | 说明 |
367
+ |------|------|
368
+ | `validate()` | 校验整个表单,返回 `Promise<boolean>` |
369
+ | `resetFields()` | 重置所有字段 |
370
+ | `clearValidate(props?)` | 清除校验状态 |
371
+ | `validateField(props)` | 校验指定字段 |
372
+ | `scrollToField(prop)` | 滚动到指定字段 |
373
+ | `formItmeRequestInstance(propsList)` | 手动触发指定字段的 API 数据加载 |
374
+ | `getFormRef()` | 获取底层 ElForm 实例 |
375
+
376
+ ### 高级用法
377
+
378
+ #### 条件隐藏
379
+
380
+ ```typescript
381
+ const items = [
382
+ { prop: 'type', label: '类型', formtype: 'Select', span: 6,
383
+ dataOptions: [{ label: '个人', value: 'personal' }, { label: '企业', value: 'company' }]
384
+ },
385
+ { prop: 'companyName', label: '企业名称', formtype: 'Input', span: 6,
386
+ isHidden: (model) => model.type !== 'company' // 类型不是企业时隐藏
387
+ }
388
+ ]
389
+ ```
390
+
391
+ #### 异步数据加载
392
+
393
+ ```typescript
394
+ const items = [
395
+ { prop: 'category', label: '分类', formtype: 'Select', span: 6,
396
+ apiParams: { url: '/api/categories', method: 'GET' },
397
+ callOptionListFormat: (data) => data.map(item => ({ label: item.name, value: item.id }))
398
+ }
399
+ ]
400
+ ```
401
+
402
+ #### 自定义渲染
403
+
404
+ ```tsx
405
+ const items = [
406
+ { prop: 'amount', label: '金额', span: 6,
407
+ render: (h, model) => <span style="color: red">¥{model.amount?.toLocaleString()}</span>
408
+ }
409
+ ]
410
+ ```
411
+
412
+ ---
413
+
414
+ ## EsTable 表格组件
415
+
416
+ 配置化驱动的数据表格,内置分页、远程数据、跨页选择、自适应高度等功能。
417
+
418
+ ### Props
419
+
420
+ | 属性 | 类型 | 默认值 | 说明 |
421
+ |------|------|--------|------|
422
+ | dataSource | `Record<string, unknown>[]` | `[]` | 表格数据(必填,支持 v-model) |
423
+ | columns | `TableColumn[]` | `[]` | 列配置数组(必填) |
424
+ | options | `TableOptions` | `{}` | 表格选项配置 |
425
+ | pagination | `PaginationConfig` | — | 分页配置(支持 v-model) |
426
+ | initTabHeight | `number` | `400` | 初始表格高度 |
427
+ | showHeaderBar | `boolean` | `true` | 是否显示头部栏区域 |
428
+ | headBarClass | `string \| Object` | — | 头部栏样式类 |
429
+
430
+ ### TableColumn 配置
431
+
432
+ | 字段 | 类型 | 说明 |
433
+ |------|------|------|
434
+ | prop | `string` | 数据字段名 |
435
+ | key | `string` | 列唯一标识 |
436
+ | label | `string` | 列标题 |
437
+ | width | `number \| string` | 列宽度 |
438
+ | minWidth | `number \| string` | 最小列宽度 |
439
+ | align | `string` | 对齐方式 |
440
+ | fixed | `boolean \| string` | 固定列(`'left'` / `'right'`) |
441
+ | formatter | `(row) => string` | 格式化函数 |
442
+ | render | `(h, { row, value, index }) => VNode` | 自定义渲染函数 |
443
+ | scopedSlots | `{ customRender: string }` | 插槽配置 |
444
+ | groups | `TableColumn[]` | 多级表头子列 |
445
+ | ellipsis | `boolean` | 文本溢出省略 |
446
+ | hidCol | `boolean` | 是否隐藏该列 |
447
+ | btns | `Array<{ name, type?, clickEvent? }>` | 行操作按钮 |
448
+ | type | `string` | 特殊列类型(`'selection'`、`'expand'`、`'index'`) |
449
+
450
+ #### render 函数
451
+
452
+ ```tsx
453
+ {
454
+ prop: 'status', label: '状态', width: 100,
455
+ render: (_, { row }) => {
456
+ const map = { active: 'success', leave: 'warning', resigned: 'danger' }
457
+ return <ElTag type={map[row.status]} size="small">{row.status}</ElTag>
458
+ }
459
+ }
460
+ ```
461
+
462
+ #### btns 行操作按钮
463
+
464
+ ```typescript
465
+ {
466
+ prop: 'operate', label: '操作', width: 160,
467
+ btns: [
468
+ { name: '编辑', type: 'primary', clickEvent: (row) => openEditDialog(row) },
469
+ { name: '删除', type: 'danger', clickEvent: (row) => handleDelete(row) }
470
+ ]
471
+ }
472
+ ```
473
+
474
+ #### 多级表头
475
+
476
+ ```typescript
477
+ {
478
+ label: '基本信息',
479
+ groups: [
480
+ { prop: 'name', label: '姓名' },
481
+ { prop: 'age', label: '年龄' },
482
+ { label: '地址', groups: [
483
+ { prop: 'province', label: '省份' },
484
+ { prop: 'city', label: '城市' }
485
+ ]}
486
+ ]
487
+ }
488
+ ```
489
+
490
+ ### TableOptions 配置
491
+
492
+ | 字段 | 类型 | 默认值 | 说明 |
493
+ |------|------|--------|------|
494
+ | border | `boolean` | `false` | 是否显示边框 |
495
+ | stripe | `boolean` | `false` | 是否显示斑马纹 |
496
+ | size | `'large' \| 'default' \| 'small'` | `'small'` | 表格尺寸 |
497
+ | headerCellStyle | `Record<string, unknown>` | `{ background: '#f5f7fa' }` | 表头样式 |
498
+ | highlightCurrentRow | `boolean` | `true` | 高亮当前行 |
499
+ | multiSelect | `boolean` | `false` | 启用多选列 |
500
+ | expand | `boolean` | `false` | 启用展开行 |
501
+ | snIndex | `boolean` | `false` | 显示序号列 |
502
+ | loading | `boolean` | `false` | 加载状态 |
503
+ | heightType | `'height' \| 'auto'` | — | 高度类型(`'height'` 推荐,`'auto'` 为 maxHeight) |
504
+ | tabHeight | `number \| string` | — | 表格容器高度(配合 heightType 使用) |
505
+ | cachePageSelection | `boolean` | `true` | 启用跨页选择缓存 |
506
+ | rowkey | `string` | — | 行唯一标识字段名(跨页选择必填) |
507
+ | isInitRun | `boolean` | — | 初始化时是否自动请求数据 |
508
+ | httpRequest | `(params) => Promise` | — | 自定义请求方法 |
509
+ | apiParams | `ApiParams` | — | API 请求配置 |
510
+ | configTableOut | `Record<string, string>` | — | 响应字段映射 |
511
+ | listenToCallBack | `Record<string, Function>` | — | 请求/响应回调管线 |
512
+ | entryQuery | `Record<string, unknown>` | — | 默认查询参数 |
513
+ | actionUrl | `string` | — | 请求地址(简写) |
514
+
515
+ ### configTableOut 字段映射
516
+
517
+ 映射后端响应字段到表格内部使用的字段:
518
+
519
+ ```typescript
520
+ configTableOut: {
521
+ total: 'total', // 总数对应的字段名
522
+ tableData: 'data', // 数据列表对应的字段名
523
+ pageSize: 'pageSize', // 每页条数对应的字段名
524
+ current: 'pageIndex' // 当前页码对应的字段名
525
+ }
526
+ ```
527
+
528
+ > **注意**:`configTableOut` 的值应使用简单 key 名(如 `'total'`、`'list'`),不支持点号路径(如 `'result.pagination.total'`)。内部使用 `findValueByKey` 递归查找嵌套对象中的 key,无需写完整路径。
529
+
530
+ ### listenToCallBack 回调管线
531
+
532
+ ```typescript
533
+ listenToCallBack: {
534
+ // 请求前回调(Before Request CallBack)— 转换请求参数
535
+ brcb: (params) => {
536
+ return { ...params, timestamp: Date.now() }
537
+ },
538
+ // 请求后回调(Query Result CallBack)— 转换响应数据
539
+ qrcb: (res) => {
540
+ if (!res?.data) return res
541
+ return {
542
+ ...res,
543
+ data: res.data.map(item => ({
544
+ id: item.emp_id,
545
+ name: item.emp_name // 后端蛇形字段转前端驼峰
546
+ }))
547
+ }
548
+ }
549
+ }
550
+ ```
551
+
552
+ ### PaginationConfig 配置
553
+
554
+ | 字段 | 类型 | 说明 |
555
+ |------|------|------|
556
+ | pageSize | `number` | 每页条数 |
557
+ | current | `number` | 当前页码 |
558
+ | total | `number` | 总条数 |
559
+ | pageSizes | `number[]` | 每页条数选项 |
560
+ | size | `'large' \| 'default' \| 'small'` | 分页器尺寸 |
561
+ | isSmall | `boolean` | 使用小型分页器 |
562
+
563
+ ### Events
564
+
565
+ | 事件 | 参数 | 说明 |
566
+ |------|------|------|
567
+ | update:dataSource | `(data)` | 数据更新 |
568
+ | update:pagination | `(pagination)` | 分页更新 |
569
+ | selection-change | `(rows)` | 选择变化(通过 fallthrough attrs 传递) |
570
+ | pagination-current-change | `(current)` | 页码变化 |
571
+ | size-change | `(pageSize)` | 每页条数变化 |
572
+ | change-table-sort | `(sort)` | 排序变化 |
573
+
574
+ ### Methods(通过 ref 调用)
575
+
576
+ | 方法 | 说明 |
577
+ |------|------|
578
+ | `httpRequestInstance(model?)` | 手动触发表格数据请求,可传入额外查询参数 |
579
+ | `getSelectionRows()` | 获取当前选中行(含跨页缓存) |
580
+ | `clearSelection()` | 清除当前页选择 |
581
+ | `clearAllSelection()` | 清除所有页面选择(含跨页缓存) |
582
+ | `refresh()` | 强制重新计算表格布局(`doLayout`) |
583
+
584
+ ### 高级用法
585
+
586
+ #### 远程数据请求
587
+
588
+ ```typescript
589
+ const mockRequest = async (params) => {
590
+ const { formParams, ...rest } = params || {}
591
+ const { pageIndex = 1, pageSize = 10, ...filters } = { ...formParams, ...rest }
592
+ const res = await fetch('/api/list')
593
+ const data = await res.json()
594
+ const total = data.length
595
+ const start = (pageIndex - 1) * pageSize
596
+ return { data: data.slice(start, start + pageSize), total, pageSize, pageIndex }
597
+ }
598
+
599
+ const options = {
600
+ border: true,
601
+ httpRequest: mockRequest,
602
+ apiParams: { url: '/api/list', method: 'GET', model: queryModel },
603
+ configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' },
604
+ rowkey: 'id'
605
+ }
606
+ ```
607
+
608
+ #### 跨页选择持久化
609
+
610
+ ```vue
611
+ <es-table
612
+ ref="tableRef"
613
+ :columns="columns"
614
+ :options="{ rowkey: 'id', cachePageSelection: true, multiSelect: true }"
615
+ @selection-change="onSelectionChange"
616
+ />
617
+
618
+ <script setup>
619
+ const tableRef = ref(null)
620
+ const selectedCount = ref(0)
621
+
622
+ const onSelectionChange = (rows) => {
623
+ selectedCount.value = rows?.length || 0
624
+ }
625
+
626
+ function getSelected() {
627
+ return tableRef.value?.getSelectionRows() || []
628
+ }
629
+
630
+ function clearSelected() {
631
+ tableRef.value?.clearAllSelection()
632
+ }
633
+ </script>
634
+ ```
635
+
636
+ #### 自适应高度
637
+
638
+ ```typescript
639
+ const options = {
640
+ heightType: 'height', // 必须 'height',非 'auto'
641
+ tabHeight: 400 // 容器高度,表格自动 = 容器 - 表单 - 分页
642
+ }
643
+ ```
644
+
645
+ > 表单展开/收起时,`ResizeObserver` 自动触发高度重算,无需手动监听。
646
+
647
+ #### 动态 options 需使用 `:key`
648
+
649
+ > es-table 的 `httpRequest`、`configTableOut`、`listenToCallBack` 等选项在挂载后不可动态响应。如需切换,请使用 `:key` 强制重建:
650
+
651
+ ```vue
652
+ <es-table :key="activeFormat" :options="currentOptions" ... />
653
+ ```
654
+
655
+ ---
656
+
657
+ ## EsDialog 弹窗组件
658
+
659
+ 模板式增强弹窗,支持拖拽、全屏切换、自定义头尾等功能。
660
+
661
+ ### Props
662
+
663
+ | 属性 | 类型 | 默认值 | 说明 |
664
+ |------|------|--------|------|
665
+ | title | `string` | — | 弹窗标题 |
666
+ | visible | `boolean` | `false` | 显示状态(支持 v-model) |
667
+ | width | `string \| number` | `'50%'` | 弹窗宽度 |
668
+ | isDraggable | `boolean` | `false` | 是否可拖拽 |
669
+ | fullscreen | `boolean` | `false` | 是否全屏 |
670
+ | hiddenFullBtn | `boolean` | `false` | 隐藏全屏切换按钮 |
671
+ | isHiddenFooter | `boolean` | `false` | 隐藏底部按钮区 |
672
+ | maxHeight | `string \| number` | — | 内容区最大高度 |
673
+ | appendTo | `string \| HTMLElement` | — | 挂载目标 |
674
+ | confirmText | `string` | — | 确认按钮文本 |
675
+ | cancelText | `string` | — | 取消按钮文本 |
676
+ | configBtn | `BtnConfig[]` | `[]` | 底部按钮配置 |
677
+ | render | `Function` | — | 自定义内容渲染函数 |
678
+ | renderHeader | `Function` | — | 自定义头部渲染函数 |
679
+ | renderFooter | `Function` | — | 自定义底部渲染函数 |
680
+
681
+ ### Events
682
+
683
+ | 事件 | 说明 |
684
+ |------|------|
685
+ | update:visible | 显示状态变化 |
686
+ | closed | 弹窗关闭后触发 |
687
+ | submit | 点击确认时触发 |
688
+
689
+ ---
690
+
691
+ ## useDialog 编程式弹窗 Hook
692
+
693
+ 命令式调用弹窗,支持 JSX 渲染、表单集成、嵌套弹窗等高级功能。
694
+
695
+ ### 基本用法
696
+
697
+ ```typescript
698
+ import { useDialog } from '@es-plus/vue3'
699
+
700
+ const dialog = useDialog()
701
+
702
+ // 打开弹窗
703
+ dialog({
704
+ title: '提示',
705
+ width: '500px',
706
+ render: (h) => <div>弹窗内容</div>,
707
+ configBtn: [
708
+ { name: '确定', type: 'primary', click: (_, { close }) => close() }
709
+ ]
710
+ })
711
+ ```
712
+
713
+ ### DialogOptions 配置
714
+
715
+ | 参数 | 类型 | 默认值 | 说明 |
716
+ |------|------|--------|------|
717
+ | title | `string` | — | 弹窗标题 |
718
+ | width | `string \| number` | `'50%'` | 弹窗宽度 |
719
+ | key | `string` | — | 唯一标识(相同 key 复用实例) |
720
+ | height | `string \| number` | — | 弹窗高度 |
721
+ | maxHeight | `string \| number` | — | 内容区最大高度 |
722
+ | render | `(h, instance, components) => VNode` | — | 内容渲染函数 |
723
+ | renderHeader | `(h, instance) => VNode` | — | 头部渲染函数 |
724
+ | renderFooter | `(h, instance) => VNode` | — | 底部渲染函数 |
725
+ | configBtn | `BtnConfig[]` | `[]` | 底部按钮配置 |
726
+ | isDraggable | `boolean` | `false` | 是否可拖拽 |
727
+ | fullscreen | `boolean` | `false` | 是否全屏 |
728
+ | hiddenFullBtn | `boolean` | `false` | 隐藏全屏按钮 |
729
+ | isHiddenFooter | `boolean` | `false` | 隐藏底部 |
730
+ | center | `boolean` | — | 垂直居中 |
731
+ | closeOnClickModal | `boolean` | `false` | 点击遮罩关闭 |
732
+ | closeOnPressEscape | `boolean` | `false` | 按 ESC 关闭 |
733
+ | showClose | `boolean` | `true` | 显示关闭按钮 |
734
+ | destroyOnClose | `boolean` | — | 关闭时销毁 |
735
+ | showDefaultButtons | `boolean` | — | 显示默认确定/取消按钮 |
736
+ | loading | `boolean` | `false` | 加载状态 |
737
+ | customClass | `string` | — | 自定义样式类 |
738
+ | appendToBody | `boolean` | — | 挂载到 body |
739
+ | appendTo | `string \| HTMLElement` | — | 挂载目标 |
740
+ | modal | `boolean` | — | 显示遮罩 |
741
+ | lockScroll | `boolean` | — | 锁定滚动 |
742
+ | onlyInstance | `boolean` | — | 单实例模式(复用同一弹窗) |
743
+ | onSubmit | `(close) => void` | — | 提交回调 |
744
+ | onClosed | `() => void` | — | 关闭回调 |
745
+ | onOpen | `() => void` | — | 打开回调 |
746
+
747
+ ### configBtn click 回调签名
748
+
749
+ ```typescript
750
+ {
751
+ name: '提交',
752
+ type: 'primary',
753
+ click: (instance, { close, getRefs, dialogVm }) => {
754
+ // instance: 渲染组件的内部实例
755
+ // close(): 关闭弹窗
756
+ // getRefs(name): 获取通过 registerRef 注册的引用
757
+ // dialogVm: 弹窗组件实例
758
+ }
759
+ }
760
+ ```
761
+
762
+ ### registerRef + getRefs 模式
763
+
764
+ 在 render 中使用 `registerRef` 注册引用,在 configBtn 的 click 中通过 `getRefs` 获取:
765
+
766
+ ```tsx
767
+ dialog({
768
+ title: '编辑',
769
+ render: (h, { registerRef }) => (
770
+ <EsForm
771
+ ref={(el) => { if (el) registerRef('formRef', el) }}
772
+ model={formData}
773
+ formItemList={formItems}
774
+ />
775
+ ),
776
+ configBtn: [
777
+ { name: '取消', click: (_, { close }) => close() },
778
+ { name: '提交', type: 'primary', click: (_, { close, getRefs }) => {
779
+ const formRef = getRefs('formRef')
780
+ formRef?.validate().then(() => {
781
+ // 提交逻辑...
782
+ close()
783
+ })
784
+ }}
785
+ ]
786
+ })
787
+ ```
788
+
789
+ ### onlyInstance 模式
790
+
791
+ ```typescript
792
+ // 默认:每次调用创建新弹窗
793
+ const dialog = useDialog()
794
+
795
+ // 单实例模式:复用同一弹窗,后续调用更新内容
796
+ const singleDialog = useDialog(null, { onlyInstance: true })
797
+ ```
798
+
799
+ ### 多实例独立弹窗
800
+
801
+ ```typescript
802
+ // 创建多个独立弹窗,可同时打开
803
+ const dialog1 = useDialog()
804
+ const dialog2 = useDialog()
805
+
806
+ // 嵌套弹窗:父弹窗内打开子弹窗
807
+ dialog1({
808
+ title: '父弹窗',
809
+ render: (h) => (
810
+ <div>
811
+ <ElButton onClick={() => dialog2({ title: '子弹窗', render: (h) => <div>嵌套内容</div> })}>
812
+ 打开子弹窗
813
+ </ElButton>
814
+ </div>
815
+ )
816
+ })
817
+ ```
818
+
819
+ ---
820
+
821
+ ## SvgIcon 图标组件
822
+
823
+ 支持外部 URL 图标和 SVG Symbol Sprite 的图标组件。
824
+
825
+ ### Props
826
+
827
+ | 属性 | 类型 | 默认值 | 说明 |
828
+ |------|------|--------|------|
829
+ | iconClass | `string` | — | 图标名称或外部 URL(必填) |
830
+ | className | `string` | — | 额外样式类 |
831
+
832
+ ### 使用
833
+
834
+ ```vue
835
+ <!-- SVG Sprite 图标 -->
836
+ <svg-icon icon-class="user" />
837
+
838
+ <!-- 外部 URL 图标(自动检测 http/https 开头) -->
839
+ <svg-icon icon-class="https://example.com/icon.svg" />
840
+ ```
841
+
842
+ ---
843
+
844
+ ## TypeScript 类型
845
+
846
+ @es-plus/vue3 导出以下 TypeScript 接口,可直接导入使用:
847
+
848
+ ```typescript
849
+ import type {
850
+ FormItemOption,
851
+ BtnConfig,
852
+ LayoutFormProps,
853
+ TableColumn,
854
+ TableOptions,
855
+ PaginationConfig,
856
+ DialogOptions,
857
+ ApiParams,
858
+ EsFormInstance,
859
+ EsTableInstance
860
+ } from '@es-plus/vue3'
861
+ ```
862
+
863
+ | 接口 | 说明 |
864
+ |------|------|
865
+ | `FormItemOption` | 表单项配置 |
866
+ | `BtnConfig` | 按钮配置 |
867
+ | `LayoutFormProps` | 表单布局配置 |
868
+ | `TableColumn` | 表格列配置 |
869
+ | `TableOptions` | 表格选项配置 |
870
+ | `PaginationConfig` | 分页配置 |
871
+ | `DialogOptions` | 弹窗选项配置 |
872
+ | `ApiParams` | API 请求配置 |
873
+ | `EsFormInstance` | EsForm 暴露的方法类型 |
874
+ | `EsTableInstance` | EsTable 暴露的方法类型 |
875
+
876
+ ---
877
+
878
+ ## 表单+表格+弹窗联动
879
+
880
+ @es-plus/vue3 的核心优势在于 EsForm、EsTable、useDialog 三者的深度联动,实现配置即开发。
881
+
882
+ ### 零代码查询
883
+
884
+ 将 EsForm 放入 EsTable 的 default 插槽,按钮设置 `triggerEvent: true`,即可实现零事件代码的查询/重置:
885
+
886
+ ```vue
887
+ <es-table
888
+ :columns="columns"
889
+ :options="tableOptions"
890
+ v-model:data-source="tableData"
891
+ v-model:pagination="pagination"
892
+ >
893
+ <es-form
894
+ :model="queryModel"
895
+ :form-item-list="queryItems"
896
+ :config-btn="queryBtns"
897
+ />
898
+ </es-table>
899
+ ```
900
+
901
+ ```typescript
902
+ const queryModel = reactive({ keyword: '', status: '' })
903
+ const queryItems = [
904
+ { prop: 'keyword', label: '关键词', formtype: 'Input', span: 6 },
905
+ { prop: 'status', label: '状态', formtype: 'Select', span: 6, dataOptions: [...] }
906
+ ]
907
+ const queryBtns = [
908
+ { name: '查询', type: 'primary', key: 'query', triggerEvent: true },
909
+ { name: '重置', key: 'rest', triggerEvent: true }
910
+ ]
911
+ const tableOptions = {
912
+ httpRequest: mockRequest,
913
+ apiParams: { url: '/api/list', method: 'GET', model: queryModel },
914
+ configTableOut: { total: 'total', tableData: 'data', pageSize: 'pageSize', current: 'pageIndex' }
915
+ }
916
+ ```
917
+
918
+ > `triggerEvent: true` + `key: 'query'` → EsForm 自动调用父级 EsTable 的 `httpRequestInstance`;`key: 'rest'` → 自动重置表单。
919
+
920
+ ### CRUD 弹窗
921
+
922
+ useDialog + JSX EsForm 实现增/编辑弹窗:
923
+
924
+ ```tsx
925
+ const dialog = useDialog()
926
+
927
+ function openEditDialog(row) {
928
+ const formData = reactive({ ...row })
929
+ dialog({
930
+ title: '编辑',
931
+ width: '600px',
932
+ render: (h, { registerRef }) => (
933
+ <EsForm
934
+ ref={(el) => { if (el) registerRef('formRef', el) }}
935
+ model={formData}
936
+ formItemList={[
937
+ { prop: 'name', label: '名称', formtype: 'Input', span: 24 },
938
+ { prop: 'status', label: '状态', formtype: 'Select', span: 24, dataOptions: [...] }
939
+ ]}
940
+ />
941
+ ),
942
+ configBtn: [
943
+ { name: '取消', click: (_, { close }) => close() },
944
+ { name: '保存', type: 'primary', click: (_, { close, getRefs }) => {
945
+ getRefs('formRef')?.validate().then(() => {
946
+ // 保存逻辑...
947
+ close()
948
+ tableRef.value?.httpRequestInstance() // 刷新表格
949
+ })
950
+ }}
951
+ ]
952
+ })
953
+ }
954
+ ```
955
+
956
+ ### 弹窗内嵌套表格
957
+
958
+ ```tsx
959
+ dialog({
960
+ title: '选择商品',
961
+ width: '800px',
962
+ render: (h, { registerRef }) => (
963
+ <EsTable
964
+ ref={(el) => { if (el) registerRef('tableRef', el) }}
965
+ dataSource={productList}
966
+ columns={productColumns}
967
+ options={{ border: true, multiSelect: true, rowkey: 'id' }}
968
+ @selection-change={(rows) => selectedRows = rows}
969
+ />
970
+ ),
971
+ configBtn: [
972
+ { name: '取消', click: (_, { close }) => close() },
973
+ { name: '确定', type: 'primary', click: (_, { close, getRefs }) => {
974
+ const selection = getRefs('tableRef')?.getSelectionRows() || []
975
+ // 处理选中数据...
976
+ close()
977
+ }}
978
+ ]
979
+ })
980
+ ```
981
+
982
+ ---
983
+
984
+ ## 常见问题
985
+
986
+ ### CSS 未加载
987
+
988
+ 确保引入了样式文件:`import '@es-plus/vue3/dist/style.css'`
989
+
990
+ ### 图标不显示
991
+
992
+ 确保安装了 `@element-plus/icons-vue`:`npm install @element-plus/icons-vue`
993
+
994
+ ### 表格高度不自适应
995
+
996
+ 1. 设置 `heightType: 'height'`(不是 `'auto'`)
997
+ 2. 设置 `tabHeight` 为容器高度
998
+ 3. 确保父容器有固定高度
999
+
1000
+ ### configTableOut 映射不生效
1001
+
1002
+ 使用简单 key 名(如 `'total'`、`'list'`),不要使用点号路径(如 `'result.pagination.total'`)。内部 `findValueByKey` 会递归查找嵌套对象。
1003
+
1004
+ ### 切换 options 无效
1005
+
1006
+ es-table 的 `httpRequest`、`configTableOut`、`listenToCallBack` 等选项在挂载后不可响应。使用 `:key` 强制重建:
1007
+
1008
+ ```vue
1009
+ <es-table :key="activeFormat" :options="currentOptions" ... />
1010
+ ```
1011
+
1012
+ ### httpRequest 参数格式
1013
+
1014
+ es-table 传给 `httpRequest` 的参数格式为:
1015
+
1016
+ ```typescript
1017
+ {
1018
+ url: string,
1019
+ method: string,
1020
+ headers: Record<string, string>,
1021
+ formParams: Record<string, unknown>, // 合并后的查询参数
1022
+ pageIndex: number,
1023
+ pageSize: number
1024
+ }
1025
+ ```
1026
+
1027
+ mockRequest 中应使用此模式解构:
1028
+
1029
+ ```typescript
1030
+ const mockRequest = async (params) => {
1031
+ const { formParams, ...rest } = params || {}
1032
+ const { pageIndex = 1, pageSize = 10, ...filters } = { ...formParams, ...rest }
1033
+ // ...
1034
+ }
1035
+ ```
1036
+
1037
+ ### 选择变化不触发 computed 更新
1038
+
1039
+ `getSelectionRows()` 在 computed 中不是响应式的。请使用 `@selection-change` 事件 + ref:
1040
+
1041
+ ```typescript
1042
+ const selectedCount = ref(0)
1043
+ const handleSelectionChange = (rows) => {
1044
+ selectedCount.value = rows?.length || 0
1045
+ }
1046
+ ```
1047
+
1048
+ ---
1049
+
1050
+ ## 权限控制
1051
+
1052
+ 安装时配置权限函数,按钮自动按权限显隐,无需 v-if:
1053
+
1054
+ ```typescript
1055
+ app.use(ESPlus, {
1056
+ permission: (value) => userPermissions.includes(value)
1057
+ })
1058
+ ```
1059
+
1060
+ 在任意 `BtnConfig` 中声明 `permissionValue`:
1061
+
1062
+ ```typescript
1063
+ const btns = [
1064
+ { name: '新增', type: 'primary', permissionValue: 'user:add', click: () => add() },
1065
+ { name: '删除', type: 'danger', permissionValue: 'user:delete', click: (row) => del(row) }
1066
+ ]
1067
+ // 用户无 'user:delete' 权限时,删除按钮自动隐藏
1068
+ ```
1069
+
1070
+ 适用于 EsForm `configBtn`、EsTable `configBtn`、表格列 `btns`、useDialog `configBtn`。
1071
+
1072
+ ---
1073
+
1074
+ ## 国际化(i18n)
1075
+
1076
+ 安装时配置翻译函数,兼容任意 i18n 库:
1077
+
1078
+ ```typescript
1079
+ app.use(ESPlus, {
1080
+ t: (key) => i18n.global.t(key)
1081
+ })
1082
+ ```
1083
+
1084
+ 表单项和表格列使用 `labelKey` 字段:
1085
+
1086
+ ```typescript
1087
+ const formItems = [
1088
+ { prop: 'name', label: '姓名', labelKey: 'form.name', formtype: 'Input' }
1089
+ ]
1090
+ const columns = [
1091
+ { prop: 'name', label: '姓名', labelKey: 'table.name' }
1092
+ ]
1093
+ // 有 labelKey 且配置了 t 函数时,使用 t(labelKey);否则回退到 label
1094
+ ```
1095
+
1096
+ ---
1097
+
1098
+ ## EsCrudPage — 一键 CRUD 页面
1099
+
1100
+ 传入 Schema 即可生成完整的查询表单 + 数据表格 + 多弹窗交互页面:
1101
+
1102
+ ### 基础用法(旧模式,仍可用)
1103
+
1104
+ ```vue
1105
+ <template>
1106
+ <es-crud-page :schema="schema" :http-request="fetchList" />
1107
+ </template>
1108
+
1109
+ <script setup>
1110
+ const schema = {
1111
+ formItems: [
1112
+ { prop: 'name', label: '姓名', formtype: 'Input', span: 6 },
1113
+ { prop: 'status', label: '状态', formtype: 'Select', span: 6,
1114
+ dataOptions: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }] }
1115
+ ],
1116
+ columns: [
1117
+ { prop: 'name', label: '姓名' },
1118
+ { prop: 'status', label: '状态' }
1119
+ ],
1120
+ actions: ['add', 'edit', 'delete']
1121
+ }
1122
+ </script>
1123
+ ```
1124
+
1125
+ ### 多弹窗模式(v1.4+ 推荐)
1126
+
1127
+ 显式声明按钮和多个独立弹窗,通过 `dialogKey` 绑定:
1128
+
1129
+ ```vue
1130
+ <template>
1131
+ <es-crud-page ref="crudRef" :schema="schema" @dialog-confirm="handleConfirm" @btn-click="handleBtn" />
1132
+ </template>
1133
+
1134
+ <script setup lang="ts">
1135
+ import type { CrudPageSchema } from '@es-plus/vue3'
1136
+
1137
+ const schema: CrudPageSchema = {
1138
+ formItems: [
1139
+ { prop: 'name', label: '姓名', formtype: 'Input' },
1140
+ { prop: 'status', label: '状态', formtype: 'Select',
1141
+ dataOptions: [{ label: '启用', value: 1 }, { label: '禁用', value: 0 }] }
1142
+ ],
1143
+ columns: [
1144
+ { prop: 'name', label: '姓名' },
1145
+ { prop: 'status', label: '状态' }
1146
+ ],
1147
+ tableOptions: { border: true, apiParams: { url: '/api/users' } },
1148
+ toolbarBtns: [
1149
+ { name: '新增', type: 'primary', icon: 'Plus', dialogKey: 'add' },
1150
+ { name: '导出', icon: 'Download', actionType: 'export' },
1151
+ ],
1152
+ operationColumn: {
1153
+ width: 200,
1154
+ btns: [
1155
+ { name: '编辑', type: 'primary', dialogKey: 'edit' },
1156
+ { name: '删除', type: 'danger', confirm: '确定删除吗?' },
1157
+ ]
1158
+ },
1159
+ dialogs: {
1160
+ add: { title: '新增', width: '600px', formItems: [
1161
+ { prop: 'name', label: '姓名', formtype: 'Input', formItemOptions: { rules: [{ required: true, message: '请输入' }] } },
1162
+ ]},
1163
+ edit: { title: '编辑', width: '600px', formItems: [
1164
+ { prop: 'name', label: '姓名', formtype: 'Input' },
1165
+ ]},
1166
+ }
1167
+ }
1168
+ </script>
1169
+ ```
1170
+
1171
+ ### configureEsPlus — 模块级全局配置
1172
+
1173
+ 自动导入模式下无需 `app.use`,直接调用 `configureEsPlus`:
1174
+
1175
+ ```typescript
1176
+ import { configureEsPlus } from '@es-plus/vue3'
1177
+
1178
+ configureEsPlus({
1179
+ permission: (value) => userStore.permissions.includes(value),
1180
+ EsTable: { methods: { $httpRequest: (p) => axios(p).then(r => r.data) } },
1181
+ EsForm: { $httpRequest: (p) => axios(p).then(r => r.data) }
1182
+ })
1183
+ ```
1184
+
1185
+ ### TypeScript 类型
1186
+
1187
+ ```typescript
1188
+ import type {
1189
+ CrudPageSchema, CrudBtnConfig, OperationColumnConfig,
1190
+ RowBtnConfig, CrudDialogConfig, DialogRenderContext, DialogActionContext
1191
+ } from '@es-plus/vue3'
1192
+ ```
1193
+
1194
+ ---
1195
+
1196
+ ## AI 工具链
1197
+
1198
+ ES-Plus 配套两个官方 AI 工具,支持自然语言生成完整 CRUD 页面:
1199
+
1200
+ ### @es-plus/mcp-server
1201
+
1202
+ 让 Claude Code、Cursor 等 AI 编码工具直接调用 CRUD 生成能力:
1203
+
1204
+ ```bash
1205
+ claude mcp add es-plus -- npx -y @es-plus/mcp-server
1206
+ ```
1207
+
1208
+ 在 AI 对话中直接说"生成一个用户管理页面",AI 自动调用 MCP Server 生成完整 .vue 文件。
1209
+
1210
+ 详见:[@es-plus/mcp-server](https://www.npmjs.com/package/@es-plus/mcp-server)
1211
+
1212
+ ### @es-plus/cli
1213
+
1214
+ 终端生成 CRUD 页面、校验配置、生成脚手架:
1215
+
1216
+ ```bash
1217
+ npx @es-plus/cli create user-management
1218
+ npx @es-plus/cli validate ./config.json --schema form-item
1219
+ npx @es-plus/cli scaffold dashboard --features query,table,dialog
1220
+ ```
1221
+
1222
+ 详见:[@es-plus/cli](https://www.npmjs.com/package/@es-plus/cli)
1223
+
1224
+ ---
1225
+
1226
+ ## 更新日志
1227
+
1228
+ ### v1.4.0
1229
+
1230
+ - **包重命名**:`es-plus-ui` → `@es-plus/vue3`,与同期发布的 `@es-plus/vue2` 形成统一 scope。旧包名 `es-plus-ui@1.4.0+` 是仅做 re-export 的 stub,并已 npm deprecate
1231
+ - **`@es-plus/vue2` 0.x 同步发布**:Vue 2 + Element UI 渲染器,使用同一份 schema 配置
1232
+ - **`@es-plus/core` 1.0**:抽出框架无关核心层,由 `@es-plus/vue3` 与 `@es-plus/vue2` 共同消费
1233
+ - **EsCrudPage 多弹窗架构**:支持 `toolbarBtns`、`operationColumn`、`dialogs` 显式声明
1234
+ - 按钮通过 `dialogKey` 声明式绑定弹窗,支持多个独立弹窗
1235
+ - 弹窗支持 `formItems`(表单模式)和 `render`(自定义渲染模式)
1236
+ - 新增事件:`dialog-confirm`、`dialog-cancel`、`dialog-open`
1237
+ - Expose 扩展:`openDialog(key, row?)`、`closeDialog(key)`
1238
+ - **configureEsPlus()**:模块级单例配置,解决自动导入模式配置丢失问题
1239
+ - 所有组件 inject 添加 `getGlobalConfig()` fallback
1240
+ - 完全向后兼容:旧 `actions` + `dialogFormItems` 配置自动转换
1241
+
1242
+ ### v1.3.3
1243
+
1244
+ - 修复全部 35 个 TypeScript 编译错误,`vue-tsc --noEmit` 零错误通过
1245
+ - 新增 `tsconfig.build.json` 分离生产构建和测试的类型检查
1246
+ - `useDialog` 添加函数重载,精确区分 `DialogCallableWithDestroy` 和 `DialogCallable` 类型
1247
+ - `TableOptions` 类型补全:新增 `configBtn`、`leftText`、`height` 属性声明
1248
+ - `heightType` 支持 `'maxHeight'` 选项
1249
+ - 单元测试全面覆盖:82 → 254 测试用例(11 个测试文件)
1250
+ - `vue-tsc` 升级至 v3.3.1(兼容 TypeScript 5.9)
1251
+
1252
+ ### v1.3.2
1253
+
1254
+ - 新增 JSON Schema 文件,支持 AI 工具链集成
1255
+ - 配置 changesets 自动版本管理
1256
+
1257
+ ### v1.2.0
1258
+
1259
+ - 导出全部 TypeScript 类型定义(FormItemOption、TableColumn 等 11 个核心接口)
1260
+ - 新增 EsCrudPage 一键 CRUD 页面组件
1261
+ - 新增权限控制(permissionValue 声明式按钮权限)
1262
+ - 新增国际化支持(labelKey + 自定义翻译函数)
1263
+ - 新增 @es-plus/mcp-server AI 编码工具集成
1264
+ - 新增 @es-plus/cli 命令行工具
1265
+ - 修复 EsForm 按钮模板嵌套问题
1266
+ - 修复 EsDialog v-if/v-for 优先级问题
1267
+
1268
+ ### v1.0.0
1269
+
1270
+ - 初始发布
1271
+ - EsForm 配置化表单组件
1272
+ - EsTable 配置化表格组件
1273
+ - EsDialog 增强弹窗组件
1274
+ - useDialog 编程式弹窗 Hook
1275
+ - SvgIcon 图标组件
1276
+
1277
+ ## License
1278
+
1279
+ MIT