@tmagic/form 1.8.0-beta.2 → 1.8.0-beta.21

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 (91) hide show
  1. package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +120 -27
  2. package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +19 -3
  3. package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +30 -7
  4. package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +56 -14
  5. package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -2
  6. package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +51 -36
  7. package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +5 -3
  8. package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +1 -1
  9. package/dist/es/containers/FormLabel.vue_vue_type_script_setup_true_lang.js +1 -1
  10. package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +7 -6
  11. package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +42 -32
  12. package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +17 -11
  13. package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +1 -1
  14. package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +1 -1
  15. package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +14 -7
  17. package/dist/es/containers/table/SortColumn.vue_vue_type_script_setup_true_lang.js +1 -1
  18. package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +7 -5
  19. package/dist/es/containers/table/useTableColumns.js +5 -1
  20. package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +3 -2
  21. package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +6 -3
  22. package/dist/es/fields/Checkbox.vue_vue_type_script_setup_true_lang.js +5 -2
  23. package/dist/es/fields/CheckboxGroup.vue_vue_type_script_setup_true_lang.js +6 -3
  24. package/dist/es/fields/ColorPicker.vue_vue_type_script_setup_true_lang.js +5 -2
  25. package/dist/es/fields/Date.vue_vue_type_script_setup_true_lang.js +6 -3
  26. package/dist/es/fields/DateTime.vue_vue_type_script_setup_true_lang.js +6 -3
  27. package/dist/es/fields/Daterange.vue_vue_type_script_setup_true_lang.js +6 -3
  28. package/dist/es/fields/Display.vue_vue_type_script_setup_true_lang.js +6 -3
  29. package/dist/es/fields/DynamicField.vue_vue_type_script_setup_true_lang.js +5 -2
  30. package/dist/es/fields/Hidden.vue_vue_type_script_setup_true_lang.js +1 -1
  31. package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +30 -8
  32. package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +8 -3
  33. package/dist/es/fields/NumberRange.vue_vue_type_script_setup_true_lang.js +5 -2
  34. package/dist/es/fields/RadioGroup.vue_vue_type_script_setup_true_lang.js +21 -10
  35. package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +7 -3
  36. package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +5 -2
  37. package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +113 -78
  38. package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +5 -2
  39. package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +5 -2
  40. package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +7 -4
  41. package/dist/es/index.js +5 -4
  42. package/dist/es/plugin.js +4 -0
  43. package/dist/es/schema.js +12 -1
  44. package/dist/es/style.css +133 -11
  45. package/dist/es/submitForm.js +415 -56
  46. package/dist/es/utils/config.js +4 -1
  47. package/dist/es/utils/form.js +84 -9
  48. package/dist/es/utils/typeMatch.js +487 -0
  49. package/dist/style.css +133 -11
  50. package/dist/themes/magic-admin.css +509 -0
  51. package/dist/tmagic-form.umd.cjs +1773 -399
  52. package/package.json +5 -5
  53. package/src/Form.vue +163 -30
  54. package/src/FormBox.vue +11 -0
  55. package/src/FormDialog.vue +25 -3
  56. package/src/FormDrawer.vue +51 -12
  57. package/src/containers/Col.vue +1 -0
  58. package/src/containers/Container.vue +32 -11
  59. package/src/containers/Fieldset.vue +5 -2
  60. package/src/containers/GroupList.vue +1 -1
  61. package/src/containers/GroupListItem.vue +28 -22
  62. package/src/containers/Panel.vue +10 -3
  63. package/src/containers/table/ActionsColumn.vue +10 -4
  64. package/src/containers/table/Table.vue +1 -0
  65. package/src/containers/table/useTableColumns.ts +7 -3
  66. package/src/containers/table-group-list/TableGroupList.vue +7 -1
  67. package/src/fields/Link.vue +16 -3
  68. package/src/fields/Number.vue +2 -1
  69. package/src/fields/RadioGroup.vue +24 -6
  70. package/src/fields/Text.vue +43 -19
  71. package/src/index.ts +11 -0
  72. package/src/plugin.ts +12 -0
  73. package/src/schema.ts +13 -1
  74. package/src/submitForm.ts +699 -66
  75. package/src/theme/container.scss +3 -0
  76. package/src/theme/fieldset.scss +51 -0
  77. package/src/theme/form-dialog.scss +1 -1
  78. package/src/theme/form.scss +6 -0
  79. package/src/theme/group-list.scss +19 -8
  80. package/src/theme/index.scss +1 -0
  81. package/src/theme/link.scss +17 -0
  82. package/src/theme/panel.scss +1 -1
  83. package/src/theme/radio-group.scss +23 -0
  84. package/src/theme/table.scss +11 -1
  85. package/src/theme/tabs.scss +2 -2
  86. package/src/theme/text.scss +38 -0
  87. package/src/theme/themes/magic-admin/index.scss +106 -0
  88. package/src/utils/config.ts +7 -1
  89. package/src/utils/form.ts +144 -22
  90. package/src/utils/typeMatch.ts +870 -0
  91. package/types/index.d.ts +408 -21
@@ -22,4 +22,7 @@
22
22
  background: #f7dadd;
23
23
  }
24
24
  }
25
+ .m-form-title-extra {
26
+ margin-left: 8px;
27
+ }
25
28
  }
@@ -1,3 +1,54 @@
1
+ .m-container-fieldset {
2
+ &.no-border-fieldset {
3
+ &.fieldset-in-row {
4
+ > fieldset.m-fieldset {
5
+ legend {
6
+ left: 0;
7
+ }
8
+ }
9
+ }
10
+ > fieldset.m-fieldset {
11
+ border: 0;
12
+ legend {
13
+ position: relative;
14
+ left: -5px;
15
+ font-size: 16px;
16
+ font-weight: 500;
17
+ line-height: 24px;
18
+ padding: 0;
19
+ }
20
+ }
21
+ }
22
+ &.fieldset-in-card-fit {
23
+ border-bottom: 1px solid #e6e6e6;
24
+ > fieldset.m-fieldset {
25
+ padding: 16px 0 16px 0 !important;
26
+ border: 0;
27
+ margin-top: 0;
28
+ margin-bottom: 0;
29
+ legend {
30
+ position: relative;
31
+ top: 10px;
32
+ background-color: transparent;
33
+ }
34
+ }
35
+ &.fieldset-in-card-fit-last {
36
+ padding-bottom: 0 !important;
37
+ border-bottom: 0;
38
+ }
39
+ &.fieldset-in-card-fit-first {
40
+ padding-top: 0 !important;
41
+ }
42
+ &:last-child {
43
+ padding-bottom: 0 !important;
44
+ border-bottom: 0;
45
+ }
46
+ &:first {
47
+ padding-top: 0 !important;
48
+ }
49
+ }
50
+ }
51
+
1
52
  fieldset.m-fieldset {
2
53
  position: relative;
3
54
  border: 1px solid rgb(229, 229, 229);
@@ -7,7 +7,7 @@
7
7
  padding: 0 20px;
8
8
  }
9
9
 
10
- .el-table .m-form-item .el-form-item {
10
+ .el-table .m-form-item .el-form-item:not(.is-error) {
11
11
  margin-bottom: 0;
12
12
  }
13
13
  }
@@ -53,3 +53,9 @@
53
53
  margin-bottom: var(--td-comp-margin-xxl);
54
54
  }
55
55
  }
56
+
57
+ .m-form-container {
58
+ .m-form-container-expand {
59
+ text-align: center;
60
+ }
61
+ }
@@ -1,4 +1,7 @@
1
1
  .m-fields-group-list {
2
+ .m-fields-group-list-item.tmagic-design-card--flat:last-child {
3
+ border: 0;
4
+ }
2
5
  .el-button--text {
3
6
  padding: 0;
4
7
  margin-bottom: 7px;
@@ -19,20 +22,28 @@
19
22
  &:last-of-type {
20
23
  border-bottom: 0;
21
24
  }
22
- }
23
25
 
24
- .tmagic-design-card {
25
- .el-card__header {
26
- padding: 5px 20px;
27
- }
28
- .t-card__header {
29
- padding: 5px 0;
26
+ .m-fields-group-list-item-header {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 8px;
30
30
  }
31
31
  }
32
32
 
33
33
  .m-fields-group-list-footer {
34
- display: flex;
35
34
  justify-content: space-between;
36
35
  margin-top: 10px;
36
+ margin-bottom: 16px;
37
+ }
38
+ }
39
+
40
+ /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
41
+ .m-container-group-list {
42
+ &.outer-gorup_list {
43
+ > .m-fields-group-list {
44
+ > .m-fields-group-list-item {
45
+ margin-bottom: 10px;
46
+ }
47
+ }
37
48
  }
38
49
  }
@@ -13,3 +13,4 @@
13
13
  @use "./tabs.scss";
14
14
  @use "./number-range.scss";
15
15
  @use "./form-box.scss";
16
+ @use "./radio-group.scss";
@@ -1,3 +1,20 @@
1
1
  div.m-fields-link {
2
2
  width: fit-content;
3
3
  }
4
+ a.magic-admin-link {
5
+ width: calc(100%);
6
+ box-sizing: border-box;
7
+ border-radius: 4px;
8
+ border: 1px solid #e6e6e6;
9
+ background-color: #fafafa;
10
+ padding: 0 8px;
11
+ height: 28px;
12
+ display: inline-flex;
13
+ gap: 4px;
14
+ align-items: center;
15
+ text-decoration: none;
16
+
17
+ i {
18
+ margin-right: 4px;
19
+ }
20
+ }
@@ -4,7 +4,7 @@
4
4
  background: #f2f6fc;
5
5
  }
6
6
  a {
7
- color: #409eff;
7
+ color: var(--el-color-primary, #409eff);
8
8
  }
9
9
  }
10
10
 
@@ -0,0 +1,23 @@
1
+ // 单个 radio option 的内容容器:横向排列 icon + 文本,垂直居中对齐。
2
+ // 这样多行文本(`text: '内联布局\n inline'`)能跟左侧 icon 在视觉中线对齐,
3
+ // 不会出现 icon 顶到第一行的错位。
4
+ .m-fields-radio-group__option {
5
+ display: flex;
6
+ align-items: center;
7
+ }
8
+
9
+ .m-fields-radio-group__text {
10
+ // `inline-block` 关键:inline span 的 line-height 会被父级 inline formatting
11
+ // context(如 `.el-radio-button__inner` 的 strut)覆盖,外观高度量出来会比
12
+ // `line-height` 大;显式声明 `inline-block` 让 span 有独立的格式化上下文,
13
+ // 自身 line-height 才真正生效。
14
+ display: inline-block;
15
+ vertical-align: middle;
16
+ // `pre-line` 让 `\n` 渲染为换行,同时保留自动折行。
17
+ white-space: pre-line;
18
+ line-height: 20px;
19
+ }
20
+
21
+ .m-fields-radio-group__icon_with_text {
22
+ margin-right: 8px;
23
+ }
@@ -26,6 +26,16 @@
26
26
  }
27
27
  }
28
28
 
29
+ .el-form-item {
30
+ .m-fields-table {
31
+ .el-form-item {
32
+ &.is-error {
33
+ margin-bottom: 18px;
34
+ }
35
+ }
36
+ }
37
+ }
38
+
29
39
  .m-fields-table {
30
40
  width: 100%;
31
41
 
@@ -62,7 +72,7 @@
62
72
  }
63
73
  }
64
74
 
65
- .el-form-item {
75
+ .el-form-item:not(.is-error) {
66
76
  margin-bottom: 0;
67
77
  }
68
78
 
@@ -5,8 +5,8 @@
5
5
  margin-right: auto;
6
6
  min-width: 50px;
7
7
  outline: none;
8
- border-color: #409eff;
9
- color: #409eff;
8
+ border-color: var(--el-color-primary, #409eff);
9
+ color: var(--el-color-primary, #409eff);
10
10
  width: 10px;
11
11
  &::before {
12
12
  content: "添加";
@@ -2,6 +2,30 @@
2
2
  display: flex;
3
3
  align-items: center;
4
4
  width: 100%;
5
+
6
+ // 纯文本(非输入框)形态:保留 prepend / append 槽位的横向布局。
7
+ // 配合 `TextConfig.static = true` 使用,常见于 ID 之类「只读 + 操作 icon」场景。
8
+ &--static {
9
+ gap: 6px;
10
+ min-height: 32px;
11
+
12
+ .m-fields-text__prepend {
13
+ color: var(--el-text-color-secondary, #909399);
14
+ }
15
+
16
+ .m-fields-text__value {
17
+ color: var(--el-text-color-regular, #606266);
18
+ overflow: hidden;
19
+ text-overflow: ellipsis;
20
+ white-space: nowrap;
21
+ }
22
+
23
+ .m-fields-text__icon {
24
+ width: 16px;
25
+ height: 16px;
26
+ cursor: pointer;
27
+ }
28
+ }
5
29
  }
6
30
 
7
31
  .m-form-validate__warning {
@@ -29,6 +53,20 @@
29
53
  }
30
54
  }
31
55
 
56
+ .m-container-text {
57
+ &.m-fields-title-container {
58
+ .tmagic-design-form-item {
59
+ .el-form-item__label {
60
+ color: #0f1113;
61
+ font-weight: 500;
62
+ font-size: 16px;
63
+ }
64
+ line-height: 24px;
65
+ margin-bottom: 16px;
66
+ }
67
+ }
68
+ }
69
+
32
70
  .tmagic-form-text-popper[data-popper-placement^="top"]
33
71
  > .tmagic-form-text-popper-arrow {
34
72
  bottom: -4px;
@@ -0,0 +1,106 @@
1
+ // magic-admin 主题:所有规则均以 `.m-form.m-form--magic-admin` 为前缀,
2
+ // 仅在 `<MForm theme="magic-admin" />` 时生效,避免污染默认样式。
3
+ @use "../../index.scss";
4
+
5
+ .m-form.m-form--magic-admin {
6
+ // 精确匹配"只挂 `m-form-container` + `m-container-fieldset` 这两个 class"的元素:
7
+ // `.a.b` 只能保证"包含"这两个 class,多余的 class 也会命中;
8
+ // 用 `[class="..."]` 做全量字符串等值匹配即可排除第三个 class 的情况。
9
+ // 由于 Vue/框架不保证 class 拼接顺序,两种顺序都列出以覆盖到位。
10
+ [class="m-form-container m-container-fieldset"],
11
+ [class="m-container-fieldset m-form-container"] {
12
+ margin-top: 15px;
13
+ }
14
+
15
+ .m-container-tab:not(.magic-right-panel-tabs-top):not(
16
+ .magic-form-dynamic-tab
17
+ ) {
18
+ > .tmagic-design-tabs:not(.el-tabs--border-card) {
19
+ > .el-tabs__content {
20
+ background-color: #fff;
21
+ padding: 16px 16px 0 16px;
22
+ border-radius: 4px;
23
+ }
24
+ }
25
+ }
26
+
27
+ .m-container-tab.magic-form-dynamic-tab {
28
+ }
29
+
30
+ .m-form-tip {
31
+ color: rgba(0, 0, 0, 0.55);
32
+ }
33
+
34
+ .tmagic-design-form-item {
35
+ &.show-after-diff {
36
+ background: #d4edbc;
37
+ }
38
+
39
+ &.show-before-diff {
40
+ background: #fad2cf;
41
+ }
42
+ }
43
+ .m-fields-radio-group__text {
44
+ // `pre-line` 让 `\n` 渲染为换行,同时保留自动折行;线高与默认按钮组高度对齐。
45
+ white-space: pre-line;
46
+ line-height: 20px;
47
+ }
48
+ .el-form-item__content {
49
+ .tmagic-design-radio-group {
50
+ .tmagic-design-radio-button {
51
+ height: 32px;
52
+ .el-radio-button__inner {
53
+ height: 32px;
54
+ display: flex;
55
+ padding-top: 0;
56
+ padding-bottom: 0;
57
+ align-items: center;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ .m-fields-data-source-field-select .tmagic-design-button {
63
+ margin-left: 8px;
64
+ span {
65
+ font-size: 16px;
66
+ }
67
+ }
68
+
69
+ .m-fields-group-list-item.tmagic-design-card--flat {
70
+ .el-card__body {
71
+ .m-fields-table-wrap {
72
+ .m-fields-table {
73
+ .el-table__header-wrapper {
74
+ thead {
75
+ height: 48px;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ .m-fields-group-list-item-header {
82
+ position: relative;
83
+ .delete-button {
84
+ position: absolute;
85
+ right: 0;
86
+ color: #0f1113;
87
+ }
88
+ }
89
+ }
90
+ .m-form-container {
91
+ .m-form-container-expand {
92
+ text-align: left;
93
+ }
94
+ }
95
+
96
+ .m-fields-group-list {
97
+ .el-table__empty-block {
98
+ .el-table__empty-text {
99
+ width: 100%;
100
+ background-color: rgba(0, 0, 0, 0.03);
101
+ margin-top: 8px;
102
+ border-radius: 4px;
103
+ }
104
+ }
105
+ }
106
+ }
@@ -17,11 +17,17 @@
17
17
  */
18
18
 
19
19
  import type { Component } from 'vue';
20
+ import { ref } from 'vue';
20
21
 
21
22
  let $MAGIC_FORM = {} as any;
23
+ // 用 ref 持有 flat 全局开关:const 引用本身不可变,符合 `import/no-mutable-exports`;
24
+ // 通过 `.value` 改值仍保持模块级响应式语义,与 editor/plugin.ts、design/index.ts 里
25
+ // 的同名变量写法对齐。
26
+ const isGlobalFlat = ref(false);
22
27
 
23
28
  const setConfig = (option: any): void => {
24
29
  $MAGIC_FORM = option;
30
+ isGlobalFlat.value = option.flat ?? false;
25
31
  };
26
32
 
27
33
  const getConfig = <T = unknown>(key: string): T => $MAGIC_FORM[key];
@@ -39,4 +45,4 @@ const getField = (tagName: string): Component | undefined => fieldRegistry.get(t
39
45
 
40
46
  const deleteField = (tagName: string): boolean => fieldRegistry.delete(tagName);
41
47
 
42
- export { deleteField, getConfig, getField, registerField, setConfig };
48
+ export { deleteField, getConfig, getField, isGlobalFlat, registerField, setConfig };
package/src/utils/form.ts CHANGED
@@ -16,11 +16,12 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { readonly } from 'vue';
19
+ import { ComputedRef, readonly } from 'vue';
20
20
  import dayjs from 'dayjs';
21
21
  import utc from 'dayjs/plugin/utc';
22
22
  import { cloneDeep } from 'lodash-es';
23
23
 
24
+ import { getDesignConfig } from '@tmagic/design';
24
25
  import { getValueByKeyPath } from '@tmagic/utils';
25
26
 
26
27
  import type {
@@ -39,6 +40,61 @@ import type {
39
40
  TypeFunction,
40
41
  } from '../schema';
41
42
 
43
+ import { createTypeMatchValidator } from './typeMatch';
44
+
45
+ type AsyncValidatorFn = (rule: any, value: any, callback: Function, source?: any, options?: any) => any;
46
+
47
+ const isTDesignAdapter = () => getDesignConfig('adapterType') === 'tdesign-vue-next';
48
+
49
+ /**
50
+ * 将 async-validator(Element Plus)风格的 validator 适配到当前 UI 库。
51
+ * TDesign 调用签名为 `(val) => boolean | CustomValidateObj | Promise`,无 callback。
52
+ */
53
+ export const adaptFormValidator = (validator: AsyncValidatorFn): AsyncValidatorFn => {
54
+ return (arg1: any, arg2?: any, arg3?: any, arg4?: any, arg5?: any) => {
55
+ if (!isTDesignAdapter()) {
56
+ return validator(arg1, arg2, arg3, arg4, arg5);
57
+ }
58
+
59
+ // TDesign: validator(val)
60
+ const value = arg1;
61
+ return new Promise((resolve) => {
62
+ let settled = false;
63
+ const callback = (error?: Error | string | (Error | string)[]) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ // async-validator 约定 callback 也可接收错误数组,TDesign 只能展示一条,取首条
67
+ const first = Array.isArray(error) ? error[0] : error;
68
+ if (first) {
69
+ resolve({
70
+ result: false,
71
+ message: typeof first === 'string' ? first : first.message,
72
+ });
73
+ } else {
74
+ resolve(true);
75
+ }
76
+ };
77
+
78
+ try {
79
+ // 异步 validator 会先返回 undefined,稍后再调 callback,这里不能当成 thenable 取值
80
+ const result = validator(undefined, value, callback);
81
+ if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
82
+ Promise.resolve(result).then(
83
+ () => {
84
+ if (!settled) callback();
85
+ },
86
+ (err) => {
87
+ callback(err instanceof Error ? err : new Error(String(err)));
88
+ },
89
+ );
90
+ }
91
+ } catch (e) {
92
+ callback(e instanceof Error ? e : new Error(String(e)));
93
+ }
94
+ });
95
+ };
96
+ };
97
+
42
98
  interface DefaultItem {
43
99
  defaultValue: any;
44
100
  type: string;
@@ -253,36 +309,54 @@ export const display = function (mForm: FormState | undefined, config: any, prop
253
309
  return true;
254
310
  };
255
311
 
256
- export const getRules = function (mForm: FormState | undefined, rules: Rule[] | Rule = [], props: any) {
257
- rules = cloneDeep(rules);
312
+ export const getRules = function (
313
+ mForm: FormState | undefined,
314
+ r: Rule[] | Rule = [],
315
+ props: any,
316
+ typeMatchValid?: ComputedRef<boolean>,
317
+ ) {
318
+ let rules = cloneDeep(r);
258
319
 
259
320
  if (typeof rules === 'object' && !Array.isArray(rules)) {
260
321
  rules = [rules];
261
322
  }
262
323
 
324
+ if (typeMatchValid?.value && !rules.some((r) => typeof r.typeMatch !== 'undefined')) {
325
+ rules.push({
326
+ typeMatch: true,
327
+ });
328
+ }
329
+
263
330
  return rules.map((item) => {
331
+ if (item.typeMatch) {
332
+ (item as any).validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
333
+ return item;
334
+ }
335
+
264
336
  if (typeof item.validator === 'function') {
265
337
  const fnc = item.validator;
266
338
 
267
- (item as any).validator = (rule: any, value: any, callback: Function, source: any, options: any) =>
268
- fnc(
269
- {
270
- rule,
271
- value: props.config.names ? props.model : value,
272
- callback,
273
- source,
274
- options,
275
- },
276
- {
277
- values: mForm?.initValues || {},
278
- model: props.model,
279
- parent: mForm?.parentValues || {},
280
- formValue: mForm?.values || props.model,
281
- prop: props.prop,
282
- config: props.config,
283
- },
284
- mForm,
285
- );
339
+ (item as any).validator = adaptFormValidator(
340
+ (rule: any, value: any, callback: Function, source: any, options: any) =>
341
+ fnc(
342
+ {
343
+ rule,
344
+ value: props.config.names ? props.model : value,
345
+ callback,
346
+ source,
347
+ options,
348
+ },
349
+ {
350
+ values: mForm?.initValues || {},
351
+ model: props.model,
352
+ parent: mForm?.parentValues || {},
353
+ formValue: mForm?.values || props.model,
354
+ prop: props.prop,
355
+ config: props.config,
356
+ },
357
+ mForm,
358
+ ),
359
+ );
286
360
  }
287
361
  return item;
288
362
  });
@@ -370,6 +444,54 @@ export const sortChange = (data: any[], { prop, order }: SortProp) => {
370
444
  }
371
445
  };
372
446
 
447
+ /**
448
+ * 将 extendState 返回的扩展字段合并进 formState。
449
+ *
450
+ * - data 描述符(普通字段)通过 `formState[key] = value` 写入,走 reactive proxy 的 set,
451
+ * 触发依赖通知;
452
+ * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
453
+ * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
454
+ *
455
+ * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
456
+ * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
457
+ * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
458
+ * post 等),命中这些 key 时统一跳过并告警。
459
+ *
460
+ * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
461
+ * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
462
+ * `TypeError: 'set' on proxy: trap returned falsish`。
463
+ */
464
+ export const applyExtendState = (
465
+ formState: FormState,
466
+ state: Record<string, any> | null | undefined,
467
+ reservedKeys?: Set<string | symbol>,
468
+ ): void => {
469
+ if (!state) return;
470
+
471
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
472
+ if (reservedKeys?.has(key)) {
473
+ continue;
474
+ }
475
+
476
+ if (!('value' in descriptor)) {
477
+ descriptor.configurable = true;
478
+ Object.defineProperty(formState, key, descriptor);
479
+ continue;
480
+ }
481
+
482
+ const targetDescriptor = Object.getOwnPropertyDescriptor(formState, key);
483
+ if (targetDescriptor && !('value' in targetDescriptor) && typeof targetDescriptor.set !== 'function') {
484
+ console.warn(
485
+ `[MForm] extendState: "${key}" is a read-only field derived from props and cannot be assigned a plain value. ` +
486
+ 'Return it as a getter accessor if you really need to override it.',
487
+ );
488
+ continue;
489
+ }
490
+
491
+ (formState as any)[key] = (state as any)[key];
492
+ }
493
+ };
494
+
373
495
  export const createObjectProp = (prop: string, key: string, name?: string | number) => {
374
496
  if (prop === '') {
375
497
  return key;