el-crud-page 1.0.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.
package/src/form.vue ADDED
@@ -0,0 +1,659 @@
1
+ <script lang="jsx">
2
+ import { renderNode } from "./utils/vnode"
3
+ import { isObject, isString ,isFunction} from "./utils/index"
4
+ import valueHook from "./utils/value-hook"
5
+
6
+ // type FormItems = { // 表单项配置
7
+ // label: string; // 标签
8
+ // prop: string; // 字段名
9
+
10
+ // options?: { value: string; label: string; disabled?: boolean }[] || ()=>[]; // 字典数据
11
+ // rules?: any[]; // 验证规则
12
+
13
+ // children?: FormItems[];
14
+ // hidden?: boolean; // 是否隐藏
15
+ // help?: string; // 帮助信息
16
+
17
+ // span?: number; // 栅格占据的列数
18
+ // labelWidth?: string; // formitem 标签宽度
19
+
20
+ // component?: VNode;
21
+ // };
22
+ // type VNodeFunction = ({ scope,h }) => VNode | ({ scope,h }) =><div>{ scope }</div> | <div />;
23
+
24
+ // type VNodeObject = {
25
+ // name?: string | Component; // 组件名 | 组件引用
26
+ // options?: any[]; // 选项
27
+ // attrs?: any; // 属性
28
+ // on?: any; // 事件
29
+ // props?: any; // 属性
30
+ // [key: string]: any;
31
+ // };
32
+ // type VNode = string | VNodeFunction | VNodeObject;
33
+
34
+
35
+ export default {
36
+ props: {
37
+ // 表单配置项,可以从外部传入
38
+ formItems: {
39
+ type: Array,
40
+ default: () => []
41
+ },
42
+ // 初始表单数据
43
+ initialFormData: {
44
+ type: Object,
45
+ default: () => ({})
46
+ },
47
+ // 按钮配置
48
+ buttons: {
49
+ type: Object,
50
+ default: () => ({
51
+ submit: {
52
+ show: true,
53
+ text: '提交',
54
+ type: 'primary',
55
+ loading: false
56
+ },
57
+ cancel: {
58
+ show: true,
59
+ text: '取消',
60
+ type: 'default'
61
+ }
62
+ })
63
+ },
64
+ // 按钮位置
65
+ buttonPosition: {
66
+ type: String,
67
+ default: 'right', // center, left, right
68
+ validator: value => ['center', 'left', 'right'].includes(value)
69
+ },
70
+ // 是否显示按钮区域
71
+ showButtons: {
72
+ type: Boolean,
73
+ default: true
74
+ },
75
+ readonly: {
76
+ type: Boolean
77
+ },
78
+ labelWidth: {
79
+ type: String,
80
+ default: '80px'
81
+ },
82
+ column: {
83
+ type: Number,
84
+ default: 2
85
+ },
86
+ gutter: {
87
+ type: Number,
88
+ default: 10
89
+ },
90
+ size: {
91
+ type: String,
92
+ default: 'small'
93
+ },
94
+ // value: {
95
+ // type: Object,
96
+ // default: () => ({})
97
+ // },
98
+
99
+
100
+ },
101
+
102
+ data() {
103
+ return {
104
+ form: {},
105
+ // 如果没有传入formItems,则使用默认配置
106
+ items: [],
107
+ activeName: 'first',
108
+ submitLoading: false
109
+
110
+ }
111
+ },
112
+
113
+ computed: {
114
+ // 从items中提取所有字段的规则
115
+ formRules() {
116
+ const rules = {};
117
+
118
+ // 递归提取规则函数
119
+ const extractRules = (nodes) => {
120
+ if (!nodes || !nodes.length) return;
121
+
122
+ nodes.forEach(node => {
123
+ // 如果节点有children,递归处理
124
+ if (node.children && node.children.length) {
125
+ extractRules(node.children);
126
+ }
127
+
128
+ // 如果是表单字段且有规则,添加到规则对象
129
+ if (node.prop && node.rules) {
130
+ rules[node.prop] = node.rules;
131
+ }
132
+ });
133
+ };
134
+
135
+ // 对所有配置项提取规则
136
+ extractRules(this.items);
137
+
138
+ return rules;
139
+ },
140
+ // 按钮区域样式
141
+ buttonAreaStyle() {
142
+ return {
143
+ textAlign: this.buttonPosition,
144
+ marginTop: '20px'
145
+ };
146
+ },
147
+ // label宽度
148
+ labelStyle() {
149
+ return {
150
+ width: this.labelWidth,
151
+ textAlign: 'center'
152
+ };
153
+ },
154
+
155
+
156
+ },
157
+
158
+ watch: {
159
+ // 监听外部传入的表单数据变化
160
+ initialFormData: {
161
+ handler(val) {
162
+ this.form = JSON.parse(JSON.stringify(val));
163
+ },
164
+ immediate: true,
165
+ deep: true
166
+ },
167
+ // 监听外部传入的表单配置变化
168
+ formItems: {
169
+ handler(val) {
170
+ if (val && val.length) {
171
+ this.items = val;
172
+ }
173
+ },
174
+ immediate: true,
175
+ deep: true
176
+ },
177
+ // 监听外部传入的value变化
178
+ // value: {
179
+ // handler(val) {
180
+ // this.form = JSON.parse(JSON.stringify(val)) || {};
181
+ // },
182
+ // immediate: true,
183
+ // deep: true
184
+ // },
185
+ // v-model绑定的值变化时,更新表单数据
186
+ form: {
187
+ handler(val) {
188
+ this.$emit('input', val);
189
+ },
190
+ deep: true
191
+ }
192
+ },
193
+
194
+ methods: {
195
+ // 处理表单输入事件
196
+ handleInput(prop, value) {
197
+ this.$set(this.form, prop, value);
198
+ // 触发change事件,将变更通知给父组件
199
+ this.$emit('field-change', { prop, value, form: this.form });
200
+ },
201
+
202
+ // 递归渲染函数 - 根据节点类型渲染不同内容
203
+ renderNode(node, index) {
204
+ // 如果是只读模式,使用只读渲染函数
205
+ if (this.readonly) {
206
+ return this.renderReadOnlyNode(node, index);
207
+ }
208
+
209
+ // 如果是输入字段(没有type或没有children,则视为字段)
210
+ if (!node.type || (!node.children && node.prop)) {
211
+ return this.renderField(node, index);
212
+ }
213
+
214
+ // 根据节点类型渲染
215
+ switch (node.type) {
216
+ case 'tab':
217
+ return this.renderTab(node, index);
218
+ case 'group':
219
+ return this.renderGroup(node, index);
220
+ default:
221
+ // 默认当作字段渲染
222
+ return this.renderField(node, index);
223
+ }
224
+ },
225
+
226
+ // 只读模式 - 递归渲染函数
227
+ renderReadOnlyNode(node, index) {
228
+ if (!node.type || (!node.children && node.prop)) {
229
+ return this.renderReadOnlyField(node, index);
230
+ }
231
+ switch (node.type) {
232
+ case 'tab':
233
+ return this.renderReadOnlyTab(node, index);
234
+ case 'group':
235
+ return this.renderReadOnlyGroup(node, index);
236
+ default:
237
+ return this.renderReadOnlyField(node, index);
238
+ }
239
+ },
240
+
241
+ // 只读模式 - 渲染标签页
242
+ renderReadOnlyTab(tab, index) {
243
+ return (
244
+ <el-tab-pane
245
+ key={tab.name || index}
246
+ label={tab.label}
247
+ name={tab.name || index}
248
+ {...tab.props}
249
+ >
250
+ {this.renderReadOnlyChildren(tab.children)}
251
+ </el-tab-pane>
252
+ );
253
+ },
254
+
255
+ // 只读模式 - 渲染分组
256
+ renderReadOnlyGroup(group, index) {
257
+ return (
258
+ <div class={['el-form-group', group.className]} key={index} {...group.props}>
259
+ <el-descriptions
260
+ title={group.title}
261
+ border
262
+ column={this.column}
263
+ size={this.size}
264
+ labelStyle={this.labelStyle}
265
+ >
266
+ {this.renderReadOnlyDescriptionItems(group.children)}
267
+ </el-descriptions>
268
+ </div>
269
+ );
270
+ },
271
+
272
+ // 只读模式 - 渲染字段
273
+ renderReadOnlyField(field, index) {
274
+ // 如果字段被配置为隐藏,则不渲染
275
+ if (field.hidden) {
276
+ return null;
277
+ }
278
+
279
+ // 获取字段值
280
+ const value = this.form[field.prop];
281
+
282
+ // 处理显示值
283
+ let displayValue = value;
284
+
285
+ // 处理不同类型的字段显示值
286
+ if (field.options && Array.isArray(field.options)) {
287
+ const option = field.options.find(opt => opt.value === value);
288
+ if (option) {
289
+ displayValue = option.label;
290
+ }
291
+ }
292
+
293
+ // 如果处于顶层(不在group内),则需要包装在el-descriptions中
294
+ if (!field._inGroup) {
295
+ return (
296
+ <el-col span={24 / this.column || field.span || 12} key={field.prop || index}>
297
+ <el-descriptions labelStyle={this.labelStyle} border column={this.column} size={this.size}>
298
+ <el-descriptions-item label={field.label || ''}>
299
+ {displayValue || '-'}
300
+ </el-descriptions-item>
301
+ </el-descriptions>
302
+ </el-col>
303
+ );
304
+ }
305
+ // 在group内部的字段直接返回
306
+ return (
307
+ <el-descriptions-item label={field.label || ''}>
308
+ {displayValue || '-'}
309
+ </el-descriptions-item>
310
+ );
311
+ },
312
+
313
+ // 只读模式 - 渲染描述项
314
+ renderReadOnlyDescriptionItems(children) {
315
+ if (!children || !children.length) {
316
+ return null;
317
+ }
318
+
319
+ return children.map(child => {
320
+ // 标记字段在group内部
321
+ child._inGroup = true;
322
+ return this.renderReadOnlyField(child);
323
+ });
324
+ },
325
+
326
+ // 只读模式 - 渲染子节点
327
+ renderReadOnlyChildren(children) {
328
+ // 如果没有子节点,返回空
329
+ if (!children || !children.length) {
330
+ return null;
331
+ }
332
+
333
+ // 检查第一个子节点类型,判断是否需要包装
334
+ const firstChild = children[0];
335
+
336
+ // 如果第一个子节点是group,不需要额外包装
337
+ if (firstChild.type === 'group') {
338
+ return children.map((child, index) => this.renderReadOnlyNode(child, index));
339
+ }
340
+
341
+ // 如果子节点不是输入字段,也不是分组,则直接渲染
342
+ if (firstChild.type && firstChild.type !== 'group' && !firstChild.prop) {
343
+ return children.map((child, index) => this.renderReadOnlyNode(child, index));
344
+ }
345
+
346
+ // 如果是普通字段(非group),则包装在el-descriptions中
347
+ return (
348
+ <el-descriptions border column={this.column} size={this.size} labelStyle={this.labelStyle}>
349
+ {children.map((child, index) => {
350
+ child._inGroup = true;
351
+ return this.renderReadOnlyNode(child, index);
352
+ })}
353
+ </el-descriptions>
354
+ );
355
+ },
356
+
357
+ // 渲染标签页
358
+ renderTab(tab, index) {
359
+ return (
360
+ <el-tab-pane
361
+ key={tab.name || index}
362
+ label={tab.label}
363
+ name={tab.name || index}
364
+ {...tab.props}
365
+ >
366
+ {this.renderChildren(tab.children)}
367
+ </el-tab-pane>
368
+ );
369
+ },
370
+
371
+ // 渲染分组
372
+ renderGroup(group, index) {
373
+ return (
374
+ <div class={['el-form-group', group.className]} key={index} {...group.props}>
375
+ {group.title && <div class="el-form-group__title">{group.title}</div>}
376
+ <div>
377
+ {this.renderChildren(group.children)}
378
+ </div>
379
+ </div>
380
+ );
381
+ },
382
+
383
+ // 渲染各种表单控件
384
+ renderFormControl(field) {
385
+ let component = field.component;
386
+ if (isString(field.component)) {
387
+ component = {
388
+ name: field.component,
389
+ attrs: {
390
+ placeholder: field.placeholder || `请输入${field.label}`,
391
+ clearable: typeof field.clearable !== 'undefined' ? field.clearable : true,
392
+ }
393
+ }
394
+ }
395
+ if ( !isFunction(field.component)) {
396
+ if (field.component === 'el-select') {
397
+ component.attrs = {
398
+ ...component.attrs,
399
+ placeholder: field.placeholder || `请选择${field.label}`,
400
+ filterable: true,
401
+
402
+ }
403
+ }
404
+ }
405
+
406
+ if( field.hook?.bind?.length ){
407
+ let value = valueHook.bind(this.form[ field.prop ], field.hook?.bind , this.form)
408
+ this.$set(this.form, field.prop, value)
409
+ }
410
+ // component 渲染 { prop: this.column.prop, scope: scope }
411
+ return renderNode.call(this, component, {
412
+ prop: field.prop,
413
+ scope: this.form
414
+ });
415
+
416
+ },
417
+
418
+ // 渲染字段
419
+ renderField(field, index) {
420
+ // 如果字段被配置为隐藏,则不渲染
421
+ if (field.hidden) {
422
+ return null;
423
+ }
424
+ return (
425
+ <el-col span={24 / this.column || field.span || 12} key={field.prop || index}>
426
+ <el-form-item
427
+ label={field.label}
428
+ prop={field.prop}
429
+ rules={field.rules} // 直接使用字段自身的规则
430
+ labelWidth={field.labelWidth || this.labelWidth}
431
+ {...field.formItemProps}
432
+ >
433
+ {this.renderFormControl(field)}
434
+ {field.help && <div class="form-item-help">{field.help}</div>}
435
+ </el-form-item>
436
+ </el-col>
437
+ );
438
+ },
439
+
440
+ // 渲染子节点
441
+ renderChildren(children) {
442
+ // 如果没有子节点,返回空
443
+ if (!children || !children.length) {
444
+ return null;
445
+ }
446
+
447
+ // 检查第一个子节点类型,判断是否需要包装
448
+ const firstChild = children[0];
449
+
450
+ // 如果第一个子节点是group,不需要额外包装
451
+ if (firstChild.type === 'group') {
452
+ return children.map(this.renderNode);
453
+ }
454
+
455
+ // 如果子节点不是输入字段,也不是分组,则直接渲染
456
+ if (firstChild.type && firstChild.type !== 'group' && !firstChild.prop) {
457
+ return children.map(this.renderNode);
458
+ }
459
+
460
+ // 其他情况(子节点是输入字段)包装在row中
461
+ return <el-row gutter={this.gutter}>{children.map(this.renderNode)}</el-row>;
462
+ },
463
+
464
+ // 判断是否有标签页
465
+ hasTabs() {
466
+ return this.items && this.items.some(item => item.type === 'tab');
467
+ },
468
+
469
+ // 渲染按钮区域
470
+ renderButtons() {
471
+ if (!this.showButtons || this.readonly) return null;
472
+
473
+ return (
474
+ <div class="form-button-area" style={this.buttonAreaStyle}>
475
+ {this.buttons.cancel && this.buttons.cancel.show && (
476
+ <el-button
477
+ onClick={this.handleCancel}
478
+ type={this.buttons.cancel.type}
479
+ {...this.buttons.cancel.props}
480
+ >
481
+ {this.buttons.cancel.text}
482
+ </el-button>
483
+ )}
484
+ {this.buttons.submit && this.buttons.submit.show && (
485
+ <el-button
486
+ onClick={this.handleSubmit}
487
+ type={this.buttons.submit.type}
488
+ loading={this.submitLoading || (this.buttons.submit.loading)}
489
+ {...this.buttons.submit.props}
490
+ >
491
+ {this.buttons.submit.text}
492
+ </el-button>
493
+ )}
494
+ </div>
495
+ );
496
+ },
497
+
498
+ // 处理提交
499
+ async handleSubmit() {
500
+ try {
501
+ this.submitLoading = true;
502
+ // 表单验证
503
+ const formData = await this.validate();
504
+ // 发出提交事件
505
+ this.$emit('submit', formData);
506
+
507
+ // return formData;
508
+ } catch (error) {
509
+ this.$emit('submit-error', error);
510
+ return Promise.reject(error);
511
+ }
512
+ this.submitLoading = false;
513
+ },
514
+
515
+ // 处理取消
516
+ handleCancel() {
517
+ this.$emit('cancel');
518
+ },
519
+
520
+ // 表单验证方法
521
+ validate() {
522
+ return new Promise((resolve, reject) => {
523
+ this.$refs.form.validate(valid => {
524
+ if (valid) {
525
+ resolve(this.form);
526
+ } else {
527
+ reject(new Error('表单验证失败'));
528
+ }
529
+ });
530
+ });
531
+ },
532
+
533
+ // 重置表单
534
+ resetForm() {
535
+ this.form = JSON.parse(JSON.stringify(this.initialFormData)) || {};
536
+ this.$refs.form.resetFields();
537
+ this.$emit('form-reset', this.form);
538
+ },
539
+
540
+ // 获取表单数据
541
+ getFormData() {
542
+ return this.form;
543
+ }
544
+ },
545
+
546
+ render() {
547
+
548
+ if (this.readonly) {
549
+ // 只读模式渲染
550
+ return (
551
+ <div class="dynamic-form-container readonly">
552
+ {this.hasTabs() ? (
553
+ // 渲染标签页结构
554
+ <el-tabs v-on={this.$listeners} v-model={this.activeName}>
555
+ {this.items.map((item, index) => this.renderReadOnlyNode(item, index))}
556
+ </el-tabs>
557
+ ) : (
558
+ // 非标签页结构,直接渲染
559
+ this.renderReadOnlyChildren(this.items)
560
+ )}
561
+ </div>
562
+ );
563
+ }
564
+
565
+ const formProps = {
566
+ model: this.form,
567
+ rules: this.formRules,
568
+ ...this.$attrs
569
+ };
570
+ // 如果没有配置项,渲染空表单
571
+ if (!this.items || !this.items.length) {
572
+ return <div class="dynamic-form-container"><el-form ref="form" {...{ props: formProps }}></el-form></div>;
573
+ }
574
+
575
+ return (
576
+ <div class="dynamic-form-container">
577
+ <el-form ref="form" {...{ props: formProps }}>
578
+ {this.hasTabs() ? (
579
+ // 渲染标签页结构
580
+ <el-tabs v-on={this.$listeners} v-model={this.activeName}>
581
+ {this.items.map(this.renderNode)}
582
+ </el-tabs>
583
+ ) : (
584
+ // 非标签页结构,直接渲染
585
+ this.renderChildren(this.items)
586
+ )}
587
+
588
+ {/* 渲染按钮区域 */}
589
+ {this.renderButtons()}
590
+ </el-form>
591
+ </div>
592
+ );
593
+ }
594
+ }
595
+ </script>
596
+
597
+ <style scoped lang="scss">
598
+ .dynamic-form-container {
599
+ padding: 20px;
600
+
601
+ .el-form{
602
+
603
+
604
+ .el-form-item {
605
+
606
+ &::v-deep{
607
+ .el-input,
608
+ .el-select{
609
+ width: 100%;
610
+ }
611
+ .el-input__inner {
612
+ width: 100%;
613
+ }
614
+ }
615
+
616
+ }
617
+
618
+
619
+
620
+ }
621
+ }
622
+
623
+ .el-form-group {
624
+ margin-bottom: 20px;
625
+ /* padding: 15px; */
626
+ border-bottom: 1px solid #ebeef5;
627
+ }
628
+
629
+ .el-form-group__title {
630
+ font-weight: bold;
631
+ margin-bottom: 15px;
632
+ font-size: 14px;
633
+ color: #606266;
634
+ }
635
+
636
+ .form-button-area {
637
+ padding-top: 10px;
638
+ border-top: 1px solid #ebeef5;
639
+ }
640
+
641
+ .form-button-area .el-button+.el-button {
642
+ margin-left: 10px;
643
+ }
644
+
645
+ /* 只读模式样式 */
646
+ .dynamic-form-container.readonly {
647
+ .el-descriptions__body {
648
+ background-color: #fafafa;
649
+ }
650
+
651
+ .el-descriptions-item__label {
652
+ font-weight: bold;
653
+ }
654
+
655
+ .el-descriptions-item__content {
656
+ word-break: break-all;
657
+ }
658
+ }
659
+ </style>
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import Crud from './index.vue';
2
+ import CrudTable from './table.vue';
3
+ import CrudForm from './form.vue';
4
+ import QueryForm from "./queryForm.vue";
5
+ import "./style.scss";
6
+
7
+ export { Crud, CrudTable, CrudForm, QueryForm };