@tmagic/form 1.8.0-manmanyu.9 → 1.8.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/dist/es/Form.vue_vue_type_script_setup_true_lang.js +88 -107
- package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +21 -5
- package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +21 -5
- package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +46 -12
- package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +44 -47
- package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/containers/FormLabel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +59 -22
- package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +118 -100
- package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +8 -3
- package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +8 -1
- package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/table/SortColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +14 -10
- package/dist/es/containers/table/useTableColumns.js +10 -15
- package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +72 -79
- package/dist/es/containers/table-group-list/useAdd.js +26 -16
- package/dist/es/containers/table-group-list/useScrollLastItemIntoView.js +72 -0
- package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/Checkbox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/CheckboxGroup/Index.js +5 -0
- package/dist/es/fields/{CheckboxGroup.vue_vue_type_script_setup_true_lang.js → CheckboxGroup/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
- package/dist/es/fields/CheckboxGroup/effect.js +8 -0
- package/dist/es/fields/ColorPicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Date/Index.js +5 -0
- package/dist/es/fields/{Date.vue_vue_type_script_setup_true_lang.js → Date/Index.vue_vue_type_script_setup_true_lang.js} +5 -7
- package/dist/es/fields/Date/effect.js +10 -0
- package/dist/es/fields/DateTime/Index.js +5 -0
- package/dist/es/fields/{DateTime.vue_vue_type_script_setup_true_lang.js → DateTime/Index.vue_vue_type_script_setup_true_lang.js} +5 -9
- package/dist/es/fields/DateTime/effect.js +15 -0
- package/dist/es/fields/Daterange.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/Display/Index.js +5 -0
- package/dist/es/fields/{Display.vue_vue_type_script_setup_true_lang.js → Display/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
- package/dist/es/fields/Display/effect.js +8 -0
- package/dist/es/fields/DynamicField/Index.js +5 -0
- package/dist/es/fields/{DynamicField.vue_vue_type_script_setup_true_lang.js → DynamicField/Index.vue_vue_type_script_setup_true_lang.js} +11 -15
- package/dist/es/fields/DynamicField/effect.js +40 -0
- package/dist/es/fields/Hidden.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/fields/NumberRange/Index.js +5 -0
- package/dist/es/fields/{NumberRange.vue_vue_type_script_setup_true_lang.js → NumberRange/Index.vue_vue_type_script_setup_true_lang.js} +5 -6
- package/dist/es/fields/NumberRange/effect.js +8 -0
- package/dist/es/fields/RadioGroup.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +13 -10
- package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/headless.js +13 -0
- package/dist/es/index.js +19 -12
- package/dist/es/plugin.js +57 -39
- package/dist/es/schema.js +11 -2
- package/dist/es/style.css +123 -3
- package/dist/es/submitForm.js +270 -72
- package/dist/es/utils/builtInFields.js +49 -0
- package/dist/es/utils/collectFields.js +407 -0
- package/dist/es/utils/config.js +4 -8
- package/dist/es/utils/fieldInnerConfig.js +42 -0
- package/dist/es/utils/fieldValueEffects.js +80 -0
- package/dist/es/utils/form.js +154 -19
- package/dist/es/utils/formStateProxy.js +111 -0
- package/dist/es/utils/registerField.js +158 -0
- package/dist/es/utils/submitHeadless.js +54 -0
- package/dist/es/utils/tableGroupList.js +104 -0
- package/dist/es/utils/typeMatch.js +532 -0
- package/dist/es/utils/validateError.js +57 -0
- package/dist/es/utils/validateValues.js +115 -0
- package/dist/style.css +123 -3
- package/dist/themes/magic-admin.css +576 -14
- package/dist/tmagic-form-headless.umd.cjs +4515 -0
- package/dist/tmagic-form.umd.cjs +3444 -983
- package/package.json +16 -10
- package/src/Form.vue +113 -129
- package/src/FormBox.vue +16 -4
- package/src/FormDialog.vue +16 -5
- package/src/FormDrawer.vue +41 -13
- package/src/containers/Col.vue +3 -0
- package/src/containers/Container.vue +40 -45
- package/src/containers/Fieldset.vue +5 -0
- package/src/containers/FlexLayout.vue +2 -0
- package/src/containers/GroupList.vue +64 -13
- package/src/containers/GroupListItem.vue +88 -81
- package/src/containers/Panel.vue +3 -0
- package/src/containers/Row.vue +2 -0
- package/src/containers/Step.vue +2 -0
- package/src/containers/Tabs.vue +13 -0
- package/src/containers/table/ActionsColumn.vue +1 -1
- package/src/containers/table/Table.vue +5 -3
- package/src/containers/table/usePagination.ts +1 -1
- package/src/containers/table/useSortable.ts +1 -1
- package/src/containers/table/useTableColumns.ts +15 -22
- package/src/containers/table-group-list/TableGroupList.vue +46 -72
- package/src/containers/table-group-list/useAdd.ts +40 -25
- package/src/containers/table-group-list/useScrollLastItemIntoView.ts +94 -0
- package/src/fields/{CheckboxGroup.vue → CheckboxGroup/Index.vue} +3 -8
- package/src/fields/CheckboxGroup/effect.ts +27 -0
- package/src/fields/{Date.vue → Date/Index.vue} +2 -5
- package/src/fields/Date/effect.ts +29 -0
- package/src/fields/{DateTime.vue → DateTime/Index.vue} +2 -16
- package/src/fields/DateTime/effect.ts +36 -0
- package/src/fields/{Display.vue → Display/Index.vue} +3 -7
- package/src/fields/Display/effect.ts +28 -0
- package/src/fields/{DynamicField.vue → DynamicField/Index.vue} +11 -11
- package/src/fields/DynamicField/effect.ts +74 -0
- package/src/fields/Link.vue +3 -4
- package/src/fields/Number.vue +2 -1
- package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
- package/src/fields/NumberRange/effect.ts +27 -0
- package/src/fields/RadioGroup.vue +13 -1
- package/src/fields/Select.vue +13 -4
- package/src/headless.ts +78 -0
- package/src/index.ts +46 -9
- package/src/plugin.ts +68 -37
- package/src/schema.ts +13 -2
- package/src/submitForm.ts +410 -131
- package/src/theme/fieldset.scss +1 -0
- package/src/theme/form-box.scss +5 -0
- package/src/theme/form-dialog.scss +1 -1
- package/src/theme/group-list.scss +143 -2
- package/src/theme/table.scss +11 -1
- package/src/theme/themes/magic-admin/index.scss +27 -16
- package/src/utils/builtInFields.ts +69 -0
- package/src/utils/collectFields.ts +561 -0
- package/src/utils/config.ts +7 -15
- package/src/utils/fieldInnerConfig.ts +127 -0
- package/src/utils/fieldValueEffects.ts +140 -0
- package/src/utils/form.ts +230 -30
- package/src/utils/formStateProxy.ts +149 -0
- package/src/utils/registerField.ts +308 -0
- package/src/utils/submitHeadless.ts +206 -0
- package/src/utils/tableGroupList.ts +147 -0
- package/src/utils/typeMatch.ts +948 -0
- package/src/utils/validateError.ts +92 -0
- package/src/utils/validateValues.ts +177 -0
- package/types/headless.d.ts +682 -0
- package/types/index.d.ts +900 -180
- package/dist/es/_virtual/_rolldown/runtime.js +0 -27
- package/dist/es/fields/CheckboxGroup.js +0 -5
- package/dist/es/fields/Date.js +0 -5
- package/dist/es/fields/DateTime.js +0 -5
- package/dist/es/fields/Display.js +0 -5
- package/dist/es/fields/DynamicField.js +0 -5
- package/dist/es/fields/NumberRange.js +0 -5
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.0
|
|
2
|
+
"version": "1.8.0",
|
|
3
3
|
"name": "@tmagic/form",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"import": "./dist/es/index.js",
|
|
18
18
|
"require": "./dist/tmagic-form.umd.cjs"
|
|
19
19
|
},
|
|
20
|
+
"./headless": {
|
|
21
|
+
"types": "./types/headless.d.ts",
|
|
22
|
+
"import": "./dist/es/headless.js",
|
|
23
|
+
"require": "./dist/tmagic-form-headless.umd.cjs"
|
|
24
|
+
},
|
|
20
25
|
"./dist/style.css": {
|
|
21
26
|
"import": "./dist/style.css",
|
|
22
27
|
"require": "./dist/style.css"
|
|
@@ -40,21 +45,22 @@
|
|
|
40
45
|
"dependencies": {
|
|
41
46
|
"@element-plus/icons-vue": "^2.3.2",
|
|
42
47
|
"@popperjs/core": "^2.11.8",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
48
|
+
"async-validator": "^4.2.5",
|
|
49
|
+
"dayjs": "^1.11.23",
|
|
50
|
+
"lodash-es": "^4.18.1",
|
|
51
|
+
"sortablejs": "^1.15.7"
|
|
46
52
|
},
|
|
47
53
|
"devDependencies": {
|
|
48
|
-
"@types/lodash-es": "^4.17.
|
|
54
|
+
"@types/lodash-es": "^4.17.12",
|
|
49
55
|
"@types/sortablejs": "^1.15.9",
|
|
50
|
-
"@vue/test-utils": "^2.
|
|
56
|
+
"@vue/test-utils": "^2.5.1"
|
|
51
57
|
},
|
|
52
58
|
"peerDependencies": {
|
|
53
|
-
"vue": "^3.5.34",
|
|
54
59
|
"typescript": "^6.0.3",
|
|
55
|
-
"
|
|
56
|
-
"@tmagic/
|
|
57
|
-
"@tmagic/form-schema": "1.8.0
|
|
60
|
+
"vue": "^3.5.43",
|
|
61
|
+
"@tmagic/design": "1.8.0",
|
|
62
|
+
"@tmagic/form-schema": "1.8.0",
|
|
63
|
+
"@tmagic/utils": "1.8.0"
|
|
58
64
|
},
|
|
59
65
|
"peerDependenciesMeta": {
|
|
60
66
|
"typescript": {
|
package/src/Form.vue
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
:last-values="lastValuesProcessed"
|
|
24
24
|
:is-compare="isCompare"
|
|
25
25
|
:label-width="item.labelWidth || labelWidth"
|
|
26
|
+
:label-position="item.labelPosition || labelPosition"
|
|
26
27
|
:step-active="stepActive"
|
|
27
28
|
:size="size"
|
|
28
29
|
@change="changeHandler"
|
|
@@ -36,25 +37,26 @@
|
|
|
36
37
|
</template>
|
|
37
38
|
|
|
38
39
|
<script setup lang="ts">
|
|
39
|
-
import { computed, inject, provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch
|
|
40
|
-
import { cloneDeep,
|
|
40
|
+
import { computed, inject, nextTick, provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch } from 'vue';
|
|
41
|
+
import { cloneDeep, isEqualWith } from 'lodash-es';
|
|
41
42
|
|
|
42
43
|
import { M_THEME_KEY, TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
|
|
43
44
|
import { setValueByKeyPath } from '@tmagic/utils';
|
|
44
45
|
|
|
45
46
|
import Container from './containers/Container.vue';
|
|
46
|
-
import {
|
|
47
|
-
import { initValue } from './utils/form';
|
|
47
|
+
import { applyMountValueEffects } from './utils/collectFields';
|
|
48
|
+
import { createFormStateBase, createFormStateProxy, initValue, mergeFormContexts } from './utils/form';
|
|
49
|
+
import { formatValidateError as formatError, getTextByName as findTextByName } from './utils/validateError';
|
|
48
50
|
import type {
|
|
49
51
|
ChangeRecord,
|
|
50
52
|
ContainerChangeEventData,
|
|
51
53
|
FormConfig,
|
|
54
|
+
FormContext,
|
|
52
55
|
FormSlots,
|
|
53
56
|
FormState,
|
|
54
57
|
FormValue,
|
|
55
|
-
ValidateError,
|
|
56
58
|
} from './schema';
|
|
57
|
-
import { FORM_DIFF_CONFIG_KEY } from './schema';
|
|
59
|
+
import { FORM_CONTEXT_KEY, FORM_DIFF_CONFIG_KEY, FORM_TYPE_MATCH_VALID_KEY } from './schema';
|
|
58
60
|
|
|
59
61
|
defineOptions({
|
|
60
62
|
name: 'MForm',
|
|
@@ -74,16 +76,37 @@ const props = withDefaults(
|
|
|
74
76
|
isCompare?: boolean;
|
|
75
77
|
parentValues?: Record<string, any>;
|
|
76
78
|
labelWidth?: string;
|
|
79
|
+
/** 是否开启类型匹配校验 */
|
|
80
|
+
typeMatchValid?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* 初始化(`config` / `initValues` 就绪)后是否立即执行一次表单校验。
|
|
83
|
+
*
|
|
84
|
+
* - `false`(默认):不自动校验,避免打开表单时就展示错误态;
|
|
85
|
+
* - `true`:初始化完成后在 `nextTick` 中调用 `validate()`。
|
|
86
|
+
*/
|
|
87
|
+
validateOnInit?: boolean;
|
|
77
88
|
disabled?: boolean;
|
|
78
89
|
height?: string;
|
|
79
90
|
stepActive?: string | number;
|
|
80
91
|
size?: 'small' | 'default' | 'large';
|
|
81
92
|
inline?: boolean;
|
|
82
|
-
labelPosition?:
|
|
93
|
+
labelPosition?: 'top' | 'left' | 'right';
|
|
83
94
|
keyProp?: string;
|
|
84
95
|
popperClass?: string;
|
|
85
96
|
preventSubmitDefault?: boolean;
|
|
86
|
-
|
|
97
|
+
/**
|
|
98
|
+
* 表单校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
|
|
99
|
+
*
|
|
100
|
+
* - `true`(默认):错误提示形如 `字段文案 -> 错误信息`,找不到 text 时回退为字段 name;
|
|
101
|
+
* - `false`:跳过查找,直接使用字段 name 作为错误提示前缀(形如 `字段name -> 错误信息`)。
|
|
102
|
+
*/
|
|
103
|
+
useFieldTextInError?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* 宿主业务上下文。也可由祖先 `provide(FORM_CONTEXT_KEY)` 下发,本 prop 覆盖祖先的同名字段。
|
|
106
|
+
*
|
|
107
|
+
* 配置回调通过 `mForm.xxx` 读取,由 formState 的读穿 Proxy 落到这里。
|
|
108
|
+
*/
|
|
109
|
+
context?: FormContext;
|
|
87
110
|
/**
|
|
88
111
|
* 自定义"是否展示对比内容"的判断函数(仅在 `isCompare === true` 时生效)。
|
|
89
112
|
*
|
|
@@ -134,18 +157,22 @@ const props = withDefaults(
|
|
|
134
157
|
inline: false,
|
|
135
158
|
labelPosition: 'right',
|
|
136
159
|
keyProp: '__key',
|
|
160
|
+
useFieldTextInError: true,
|
|
161
|
+
validateOnInit: false,
|
|
137
162
|
},
|
|
138
163
|
);
|
|
139
164
|
|
|
140
165
|
const emit = defineEmits(['change', 'error', 'field-input', 'field-change', 'update:stepActive']);
|
|
141
166
|
|
|
167
|
+
provide(
|
|
168
|
+
FORM_TYPE_MATCH_VALID_KEY,
|
|
169
|
+
computed(() => props.typeMatchValid),
|
|
170
|
+
);
|
|
171
|
+
|
|
142
172
|
const tMagicFormRef = useTemplateRef('tMagicForm');
|
|
143
173
|
const initialized = ref(false);
|
|
144
174
|
const values = ref<FormValue>({});
|
|
145
175
|
const lastValuesProcessed = ref<FormValue>({});
|
|
146
|
-
const fields = new Map<string, any>();
|
|
147
|
-
|
|
148
|
-
const requestFuc = getConfig('request') as Function;
|
|
149
176
|
|
|
150
177
|
/**
|
|
151
178
|
* 当前表单生效的主题名称:
|
|
@@ -178,18 +205,14 @@ const themeClass = computed(() => (effectiveTheme.value ? `m-theme--${effectiveT
|
|
|
178
205
|
* 2. `values` / `lastValuesProcessed` 是 ref,Vue 的 `reactive` 会自动解包,因此每次
|
|
179
206
|
* 访问 `formState.values` / `formState.lastValuesProcessed` 也都是当前 ref 值。
|
|
180
207
|
*
|
|
181
|
-
* 3.
|
|
182
|
-
*
|
|
183
|
-
* set,触发依赖通知;`extendState` 同步段读到的响应式数据变化时会自动重跑,
|
|
184
|
-
* 把最新值刷进 formState。
|
|
185
|
-
* - accessor 描述符(`{ get stage() { return ... } }`)按原样写入,调用方可以控制
|
|
186
|
-
* 读时求值,每次读取都会重新执行 getter。
|
|
208
|
+
* 3. 宿主业务上下文不再 merge 进 coreState,而是单独放在 `contextRef` 上,由读穿
|
|
209
|
+
* Proxy 在 miss 时落到 context。核心字段结构性优先,`mForm.xxx` 永久兼容旧读法。
|
|
187
210
|
*
|
|
188
211
|
* 4. `popperClass` 会自动拼接 `themeClass`:调用方传入的 `popperClass` + 当前主题
|
|
189
212
|
* 修饰类(含祖先 `<MEditor>` provide 的主题)。这样所有透传到 Element Plus 弹层
|
|
190
213
|
* `popper-class` 的字段(Select / DateTime / Cascader 等)能自带主题作用域。
|
|
191
214
|
*/
|
|
192
|
-
const
|
|
215
|
+
const coreState: FormState = reactive<FormState>({
|
|
193
216
|
get keyProp() {
|
|
194
217
|
return props.keyProp;
|
|
195
218
|
},
|
|
@@ -218,72 +241,21 @@ const formState: FormState = reactive<FormState>({
|
|
|
218
241
|
values,
|
|
219
242
|
lastValuesProcessed,
|
|
220
243
|
$emit: emit as (_event: string, ..._args: any[]) => void,
|
|
221
|
-
|
|
222
|
-
setField: (prop: string, field: any) => fields.set(prop, field),
|
|
223
|
-
getField: (prop: string) => fields.get(prop),
|
|
224
|
-
deleteField: (prop: string) => fields.delete(prop),
|
|
225
|
-
$messageBox: tMagicMessageBox,
|
|
226
|
-
$message: tMagicMessage,
|
|
227
|
-
post: (options: any) => {
|
|
228
|
-
if (requestFuc) {
|
|
229
|
-
return requestFuc({
|
|
230
|
-
method: 'POST',
|
|
231
|
-
...options,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
},
|
|
244
|
+
...createFormStateBase({ $message: tMagicMessage, $messageBox: tMagicMessageBox }),
|
|
235
245
|
});
|
|
236
246
|
|
|
247
|
+
const ancestorContext = inject(FORM_CONTEXT_KEY, undefined);
|
|
248
|
+
|
|
237
249
|
/**
|
|
238
|
-
* `
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* extendState: (formState) => ({
|
|
242
|
-
* username: store.username, // 同步读 store,会被跟踪
|
|
243
|
-
* env: store.env,
|
|
244
|
-
* })
|
|
245
|
-
*
|
|
246
|
-
* 当 `store.username` 变化时,整个 effect 重跑,新值会被刷进 `formState`。
|
|
247
|
-
*
|
|
248
|
-
* prop 派生字段(initValues / config / ...)已经在上方用 getter 定义,
|
|
249
|
-
* 这里不再重复同步;因此 `props.initValues` 这类高频变化也不会再触发
|
|
250
|
-
* `extendState` 重跑(旧版的性能问题修复点)。
|
|
251
|
-
*
|
|
252
|
-
* 实现细节:
|
|
253
|
-
* - data 描述符:通过 `formState[key] = value` 走 reactive proxy 的 set,
|
|
254
|
-
* 触发依赖通知;与旧版「逐项赋值」语义完全等价。
|
|
255
|
-
* - accessor 描述符(`{ get stage() {...} }`)按原样写入 formState,调用方
|
|
256
|
-
* 可以自行控制读时求值;强制 `configurable: true` 以便下一次重跑可再 define。
|
|
250
|
+
* 宿主业务上下文:`props.context` 覆盖祖先注入的同名字段。
|
|
251
|
+
* 嵌套表单(Link / FormBox / FormDialog)通过下面的 provide 自动继承。
|
|
257
252
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
let stale = false;
|
|
263
|
-
onCleanup(() => {
|
|
264
|
-
stale = true;
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
let state: Record<string, any> = {};
|
|
268
|
-
try {
|
|
269
|
-
state = (await extendState(formState)) || {};
|
|
270
|
-
} catch (e) {
|
|
271
|
-
console.error('[MForm] extendState failed:', e);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
if (stale) return;
|
|
275
|
-
|
|
276
|
-
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
|
277
|
-
if ('value' in descriptor) {
|
|
278
|
-
(formState as any)[key] = (state as any)[key];
|
|
279
|
-
} else {
|
|
280
|
-
descriptor.configurable = true;
|
|
281
|
-
Object.defineProperty(formState, key, descriptor);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
});
|
|
253
|
+
const contextRef = computed<FormContext>(() => mergeFormContexts(ancestorContext?.value, props.context));
|
|
254
|
+
|
|
255
|
+
const formState: FormState = createFormStateProxy(coreState, () => contextRef.value);
|
|
285
256
|
|
|
286
257
|
provide('mForm', formState);
|
|
258
|
+
provide(FORM_CONTEXT_KEY, contextRef);
|
|
287
259
|
|
|
288
260
|
/**
|
|
289
261
|
* 把生效主题(自身或祖先)再 provide 出去,供 form 子树内含 `Teleport` 的组件
|
|
@@ -291,7 +263,7 @@ provide('mForm', formState);
|
|
|
291
263
|
* 详见 `@tmagic/design/theme.ts`。
|
|
292
264
|
*/
|
|
293
265
|
provide(M_THEME_KEY, effectiveTheme);
|
|
294
|
-
|
|
266
|
+
provide('formInline', props.inline);
|
|
295
267
|
// 对比相关配置单独通过 provide 下发,所有层级的 Container 通过 inject 获取,无需逐层透传 prop。
|
|
296
268
|
// 用 getter 对象保证读取时回到最新的 props 值,维持响应式。
|
|
297
269
|
provide(FORM_DIFF_CONFIG_KEY, {
|
|
@@ -305,12 +277,23 @@ provide(FORM_DIFF_CONFIG_KEY, {
|
|
|
305
277
|
|
|
306
278
|
const changeRecords = shallowRef<ChangeRecord[]>([]);
|
|
307
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 两份配置的结构是否一致;函数一律视为相等。
|
|
282
|
+
*
|
|
283
|
+
* 宿主(如编辑器属性面板)往往在每次节点更新后整份重新生成配置,其中的
|
|
284
|
+
* `display` / `options` / `onChange` 都是新闭包,深比较必然判不等。若据此把 `initialized`
|
|
285
|
+
* 置 false,整棵表单会卸载重挂,滚动位置、展开态、输入焦点全部丢失。
|
|
286
|
+
* 配置是响应式 prop,闭包换了照样生效,只有结构变化(增删字段、换组件类型)才需要重挂。
|
|
287
|
+
*/
|
|
288
|
+
const isSameConfigShape = (config: unknown, preConfig: unknown) =>
|
|
289
|
+
isEqualWith(config, preConfig, (a, b) => (typeof a === 'function' && typeof b === 'function' ? true : undefined));
|
|
290
|
+
|
|
308
291
|
watch(
|
|
309
292
|
[() => props.config, () => props.initValues],
|
|
310
|
-
([config], [preConfig]) => {
|
|
293
|
+
async ([config], [preConfig]) => {
|
|
311
294
|
changeRecords.value = [];
|
|
312
295
|
|
|
313
|
-
if (!
|
|
296
|
+
if (!isSameConfigShape(toRaw(config), toRaw(preConfig))) {
|
|
314
297
|
initialized.value = false;
|
|
315
298
|
}
|
|
316
299
|
|
|
@@ -319,8 +302,17 @@ watch(
|
|
|
319
302
|
config: props.config,
|
|
320
303
|
}).then((value) => {
|
|
321
304
|
values.value = value;
|
|
305
|
+
// 字段的值初始化写入统一在这里执行,字段组件自身不再改写 model。
|
|
306
|
+
// 必须放在赋值之后:effect 与动态 type / display 回调读到的 formValue 才是这一轮的值。
|
|
307
|
+
applyMountValueEffects(formState, props.config, values.value);
|
|
322
308
|
// 非对比模式,初始化完成
|
|
323
309
|
initialized.value = !props.isCompare;
|
|
310
|
+
|
|
311
|
+
if (props.validateOnInit) {
|
|
312
|
+
nextTick(() => {
|
|
313
|
+
tMagicFormRef.value?.validate();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
324
316
|
});
|
|
325
317
|
|
|
326
318
|
if (props.isCompare) {
|
|
@@ -330,6 +322,8 @@ watch(
|
|
|
330
322
|
config: props.config,
|
|
331
323
|
}).then((value) => {
|
|
332
324
|
lastValuesProcessed.value = value;
|
|
325
|
+
// 对比模式下待对比的那份值同样要规整,否则会与当前值比出「格式差异」这种假差异
|
|
326
|
+
applyMountValueEffects(formState, props.config, lastValuesProcessed.value);
|
|
333
327
|
initialized.value = true;
|
|
334
328
|
});
|
|
335
329
|
}
|
|
@@ -361,45 +355,17 @@ const submitHandler = (e: SubmitEvent) => {
|
|
|
361
355
|
}
|
|
362
356
|
};
|
|
363
357
|
|
|
358
|
+
const getTextByName = (name: string, config: FormConfig = props.config): string | undefined =>
|
|
359
|
+
findTextByName(name, config);
|
|
360
|
+
|
|
364
361
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
362
|
+
* 将校验返回的 invalidFields 汇总为可读的错误文案(多条以 `<br>` 拼接)。
|
|
363
|
+
*
|
|
364
|
+
* 实现收口在 `utils/validateError`,供渲染式校验(本组件的 `submitForm` / `validate`)与
|
|
365
|
+
* 无渲染校验(`validateValues`)共用,保证两条链路产出的错误文案格式完全一致。
|
|
369
366
|
*/
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const nameParts = name.split('.');
|
|
374
|
-
|
|
375
|
-
const findInConfig = (configs: FormConfig, parts: string[]): string | undefined => {
|
|
376
|
-
if (parts.length === 0) return undefined;
|
|
377
|
-
|
|
378
|
-
const [currentPart, ...remainingParts] = parts;
|
|
379
|
-
|
|
380
|
-
for (const item of configs) {
|
|
381
|
-
if (item.name === currentPart) {
|
|
382
|
-
if (remainingParts.length === 0) {
|
|
383
|
-
return typeof item.text === 'string' ? item.text : undefined;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
387
|
-
const result = findInConfig(item.items, remainingParts);
|
|
388
|
-
if (result !== undefined) return result;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if ('items' in item && Array.isArray(item.items)) {
|
|
393
|
-
const result = findInConfig(item.items, parts);
|
|
394
|
-
if (result !== undefined) return result;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return undefined;
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
return findInConfig(config, nameParts);
|
|
402
|
-
};
|
|
367
|
+
const formatValidateError = (invalidFields: Record<string, any>): string =>
|
|
368
|
+
formatError(invalidFields, { config: props.config, useFieldTextInError: props.useFieldTextInError });
|
|
403
369
|
|
|
404
370
|
defineExpose({
|
|
405
371
|
values,
|
|
@@ -428,18 +394,36 @@ defineExpose({
|
|
|
428
394
|
} catch (invalidFields: any) {
|
|
429
395
|
emit('error', invalidFields);
|
|
430
396
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
(ValidateError as ValidateError[]).forEach(({ field, message }) => {
|
|
435
|
-
const name = field || prop;
|
|
436
|
-
const text = getTextByName(name, props.config) || name;
|
|
437
|
-
|
|
438
|
-
error.push(`${text} -> ${message}`);
|
|
439
|
-
});
|
|
440
|
-
});
|
|
397
|
+
throw new Error(formatValidateError(invalidFields));
|
|
398
|
+
}
|
|
399
|
+
},
|
|
441
400
|
|
|
442
|
-
|
|
401
|
+
/**
|
|
402
|
+
* 校验:对表单当前值执行校验,返回汇总后的错误文案。
|
|
403
|
+
*
|
|
404
|
+
* 与 `submitForm` 的区别:
|
|
405
|
+
* - 校验失败时不抛异常、不触发 `error` 事件,而是以返回值形式给出错误文案;
|
|
406
|
+
* - 不重置 `changeRecords`,不改变提交语义,仅用于「探测」当前配置是否合法。
|
|
407
|
+
*
|
|
408
|
+
* 注意:本方法只改变「校验结果的返回方式」,并不负责「不污染页面表单状态」——
|
|
409
|
+
* 若需对一份独立的「配置 + 值」做完全不影响页面上已渲染表单的校验,请使用 `validateForm`
|
|
410
|
+
* (内部会新建一个隐藏的 MForm 实例,通过 `initValues` 传入待校验值,用完即卸载)。
|
|
411
|
+
*
|
|
412
|
+
* 典型用途:作为 `validateForm` 内部复用的校验实现;也可在已渲染的表单实例上主动调用,
|
|
413
|
+
* 根据返回的错误文案自行决定后续处理(如记录节点错误状态)。
|
|
414
|
+
*
|
|
415
|
+
* @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
|
|
416
|
+
*/
|
|
417
|
+
validate: async (): Promise<string> => {
|
|
418
|
+
try {
|
|
419
|
+
const result = await tMagicFormRef.value?.validate();
|
|
420
|
+
// tdesign 通过返回值返回校验结果,element-plus 通过 throw error
|
|
421
|
+
if (result !== true) {
|
|
422
|
+
throw result;
|
|
423
|
+
}
|
|
424
|
+
return '';
|
|
425
|
+
} catch (invalidFields: any) {
|
|
426
|
+
return formatValidateError(invalidFields);
|
|
443
427
|
}
|
|
444
428
|
},
|
|
445
429
|
|
package/src/FormBox.vue
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
:label-position="labelPosition"
|
|
14
14
|
:inline="inline"
|
|
15
15
|
:prevent-submit-default="preventSubmitDefault"
|
|
16
|
-
:
|
|
16
|
+
:use-field-text-in-error="useFieldTextInError"
|
|
17
|
+
:context="context"
|
|
18
|
+
:type-match-valid="typeMatchValid"
|
|
19
|
+
:validate-on-init="validateOnInit"
|
|
17
20
|
@change="changeHandler"
|
|
18
21
|
></Form>
|
|
19
22
|
<slot></slot>
|
|
@@ -41,7 +44,7 @@ import { computed, ref, watchEffect } from 'vue';
|
|
|
41
44
|
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
|
|
42
45
|
|
|
43
46
|
import Form from './Form.vue';
|
|
44
|
-
import type { ContainerChangeEventData, FormConfig,
|
|
47
|
+
import type { ContainerChangeEventData, FormConfig, FormContext, FormValue } from './schema';
|
|
45
48
|
|
|
46
49
|
defineOptions({
|
|
47
50
|
name: 'MFormBox',
|
|
@@ -55,18 +58,27 @@ const props = withDefaults(
|
|
|
55
58
|
width?: number;
|
|
56
59
|
height?: number;
|
|
57
60
|
labelWidth?: string;
|
|
61
|
+
/** 是否开启类型匹配校验 */
|
|
62
|
+
typeMatchValid?: boolean;
|
|
63
|
+
/** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false`) */
|
|
64
|
+
validateOnInit?: boolean;
|
|
58
65
|
disabled?: boolean;
|
|
59
66
|
size?: 'small' | 'default' | 'large';
|
|
60
67
|
confirmText?: string;
|
|
61
68
|
inline?: boolean;
|
|
62
|
-
labelPosition?:
|
|
69
|
+
labelPosition?: 'top' | 'left' | 'right';
|
|
63
70
|
preventSubmitDefault?: boolean;
|
|
64
|
-
|
|
71
|
+
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
|
|
72
|
+
useFieldTextInError?: boolean;
|
|
73
|
+
/** 透传给内部 `MForm` 的宿主业务上下文 */
|
|
74
|
+
context?: FormContext;
|
|
65
75
|
}>(),
|
|
66
76
|
{
|
|
67
77
|
config: () => [],
|
|
68
78
|
values: () => ({}),
|
|
69
79
|
confirmText: '确定',
|
|
80
|
+
useFieldTextInError: true,
|
|
81
|
+
validateOnInit: false,
|
|
70
82
|
},
|
|
71
83
|
);
|
|
72
84
|
|
package/src/FormDialog.vue
CHANGED
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
:label-position="labelPosition"
|
|
32
32
|
:inline="inline"
|
|
33
33
|
:prevent-submit-default="preventSubmitDefault"
|
|
34
|
-
:
|
|
34
|
+
:use-field-text-in-error="useFieldTextInError"
|
|
35
|
+
:type-match-valid="typeMatchValid"
|
|
36
|
+
:validate-on-init="validateOnInit"
|
|
37
|
+
:context="context"
|
|
35
38
|
:theme="effectiveTheme"
|
|
36
39
|
@change="changeHandler"
|
|
37
40
|
></Form>
|
|
@@ -70,7 +73,7 @@ import { computed, inject, provide, ref } from 'vue';
|
|
|
70
73
|
import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
|
|
71
74
|
|
|
72
75
|
import Form from './Form.vue';
|
|
73
|
-
import { ContainerChangeEventData, FormConfig,
|
|
76
|
+
import { ContainerChangeEventData, FormConfig, FormContext, FormValue, StepConfig } from './schema';
|
|
74
77
|
|
|
75
78
|
defineOptions({
|
|
76
79
|
name: 'MFormDialog',
|
|
@@ -83,11 +86,15 @@ const props = withDefaults(
|
|
|
83
86
|
parentValues?: Object;
|
|
84
87
|
width?: string | number;
|
|
85
88
|
labelWidth?: string;
|
|
89
|
+
/** 是否开启类型匹配校验 */
|
|
90
|
+
typeMatchValid?: boolean;
|
|
91
|
+
/** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false`) */
|
|
92
|
+
validateOnInit?: boolean;
|
|
86
93
|
fullscreen?: boolean;
|
|
87
94
|
disabled?: boolean;
|
|
88
95
|
title?: string;
|
|
89
96
|
inline?: boolean;
|
|
90
|
-
labelPosition?:
|
|
97
|
+
labelPosition?: 'top' | 'left' | 'right';
|
|
91
98
|
zIndex?: number;
|
|
92
99
|
size?: 'small' | 'default' | 'large';
|
|
93
100
|
confirmText?: string;
|
|
@@ -97,8 +104,10 @@ const props = withDefaults(
|
|
|
97
104
|
destroyOnClose?: boolean;
|
|
98
105
|
showClose?: boolean;
|
|
99
106
|
showCancel?: boolean;
|
|
100
|
-
/** 透传给内部 `MForm
|
|
101
|
-
|
|
107
|
+
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
|
|
108
|
+
useFieldTextInError?: boolean;
|
|
109
|
+
/** 透传给内部 `MForm` 的宿主业务上下文 */
|
|
110
|
+
context?: FormContext;
|
|
102
111
|
/**
|
|
103
112
|
* 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
|
|
104
113
|
* 计算结果会再次 `provide` 出去,使得 Dialog 被 Teleport 到 body 后,内部子树
|
|
@@ -115,6 +124,8 @@ const props = withDefaults(
|
|
|
115
124
|
destroyOnClose: false,
|
|
116
125
|
showClose: true,
|
|
117
126
|
showCancel: true,
|
|
127
|
+
useFieldTextInError: true,
|
|
128
|
+
validateOnInit: false,
|
|
118
129
|
},
|
|
119
130
|
);
|
|
120
131
|
|
package/src/FormDrawer.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<TMagicDrawer
|
|
3
3
|
:class="['m-form-drawer', effectiveTheme ? `m-theme--${effectiveTheme}` : '']"
|
|
4
4
|
ref="drawer"
|
|
5
|
-
v-model="
|
|
5
|
+
v-model="dialogVisible"
|
|
6
6
|
:title="title"
|
|
7
7
|
:close-on-press-escape="closeOnPressEscape"
|
|
8
8
|
:append-to-body="true"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
@close="closeHandler"
|
|
17
17
|
@closed="closedHandler"
|
|
18
18
|
>
|
|
19
|
-
<div v-if="
|
|
19
|
+
<div v-if="dialogVisible" ref="drawerBody" class="m-drawer-body">
|
|
20
20
|
<Form
|
|
21
21
|
ref="form"
|
|
22
22
|
:size="size"
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
:label-position="labelPosition"
|
|
29
29
|
:inline="inline"
|
|
30
30
|
:prevent-submit-default="preventSubmitDefault"
|
|
31
|
-
:
|
|
31
|
+
:use-field-text-in-error="useFieldTextInError"
|
|
32
|
+
:type-match-valid="typeMatchValid"
|
|
33
|
+
:validate-on-init="validateOnInit"
|
|
34
|
+
:context="context"
|
|
32
35
|
:theme="effectiveTheme"
|
|
33
36
|
@change="changeHandler"
|
|
34
37
|
></Form>
|
|
@@ -61,10 +64,10 @@ import { computed, inject, provide, ref, watchEffect } from 'vue';
|
|
|
61
64
|
import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
|
|
62
65
|
|
|
63
66
|
import Form from './Form.vue';
|
|
64
|
-
import type { ContainerChangeEventData, FormConfig,
|
|
67
|
+
import type { ContainerChangeEventData, FormConfig, FormContext, FormValue } from './schema';
|
|
65
68
|
|
|
66
69
|
defineOptions({
|
|
67
|
-
name: '
|
|
70
|
+
name: 'MFormDrawer',
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
const props = withDefaults(
|
|
@@ -74,6 +77,10 @@ const props = withDefaults(
|
|
|
74
77
|
parentValues?: Object;
|
|
75
78
|
width?: string | number;
|
|
76
79
|
labelWidth?: string;
|
|
80
|
+
/** 是否开启类型匹配校验 */
|
|
81
|
+
typeMatchValid?: boolean;
|
|
82
|
+
/** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false`) */
|
|
83
|
+
validateOnInit?: boolean;
|
|
77
84
|
disabled?: boolean;
|
|
78
85
|
closeOnPressEscape?: boolean;
|
|
79
86
|
title?: string;
|
|
@@ -81,12 +88,14 @@ const props = withDefaults(
|
|
|
81
88
|
size?: 'small' | 'default' | 'large';
|
|
82
89
|
confirmText?: string;
|
|
83
90
|
inline?: boolean;
|
|
84
|
-
labelPosition?:
|
|
91
|
+
labelPosition?: 'top' | 'left' | 'right';
|
|
85
92
|
preventSubmitDefault?: boolean;
|
|
93
|
+
/** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
|
|
94
|
+
useFieldTextInError?: boolean;
|
|
86
95
|
/** 关闭前的回调,会暂停 Drawer 的关闭; done 是个 function type 接受一个 boolean 参数, 执行 done 使用 true 参数或不提供参数将会终止关闭 */
|
|
87
96
|
beforeClose?: (_done: (_cancel?: boolean) => void) => void;
|
|
88
|
-
/** 透传给内部 `MForm
|
|
89
|
-
|
|
97
|
+
/** 透传给内部 `MForm` 的宿主业务上下文 */
|
|
98
|
+
context?: FormContext;
|
|
90
99
|
/**
|
|
91
100
|
* 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
|
|
92
101
|
* 计算结果会再次 `provide` 出去,使得 Drawer 被 Teleport 到 body 后,内部子树
|
|
@@ -99,6 +108,8 @@ const props = withDefaults(
|
|
|
99
108
|
config: () => [],
|
|
100
109
|
values: () => ({}),
|
|
101
110
|
confirmText: '确定',
|
|
111
|
+
useFieldTextInError: true,
|
|
112
|
+
validateOnInit: false,
|
|
102
113
|
},
|
|
103
114
|
);
|
|
104
115
|
|
|
@@ -111,7 +122,7 @@ const emit = defineEmits(['close', 'closed', 'submit', 'error', 'change', 'open'
|
|
|
111
122
|
const drawer = ref<InstanceType<typeof TMagicDrawer>>();
|
|
112
123
|
const form = ref<InstanceType<typeof Form>>();
|
|
113
124
|
const drawerBody = ref<HTMLDivElement>();
|
|
114
|
-
const
|
|
125
|
+
const dialogVisible = ref(false);
|
|
115
126
|
const saveFetch = ref(false);
|
|
116
127
|
const bodyHeight = ref(0);
|
|
117
128
|
|
|
@@ -152,25 +163,42 @@ const closedHandler = () => {
|
|
|
152
163
|
};
|
|
153
164
|
|
|
154
165
|
const show = () => {
|
|
155
|
-
|
|
166
|
+
dialogVisible.value = true;
|
|
156
167
|
};
|
|
157
168
|
|
|
158
169
|
const hide = () => {
|
|
159
|
-
|
|
170
|
+
dialogVisible.value = false;
|
|
171
|
+
};
|
|
172
|
+
const close = () => {
|
|
173
|
+
dialogVisible.value = false;
|
|
174
|
+
};
|
|
175
|
+
const cancel = () => {
|
|
176
|
+
hide();
|
|
160
177
|
};
|
|
161
|
-
|
|
162
178
|
/** 用于关闭 Drawer, 该方法会调用传入的 before-close 方法 */
|
|
163
179
|
const handleClose = () => {
|
|
164
180
|
drawer.value?.handleClose();
|
|
165
181
|
};
|
|
166
182
|
|
|
183
|
+
const save = async () => {
|
|
184
|
+
try {
|
|
185
|
+
const changeRecords = [...(form.value?.changeRecords || [])];
|
|
186
|
+
const values = await form.value?.submitForm();
|
|
187
|
+
emit('submit', values, { changeRecords });
|
|
188
|
+
} catch (e) {
|
|
189
|
+
emit('error', e);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
167
193
|
defineExpose({
|
|
168
194
|
form,
|
|
169
195
|
saveFetch,
|
|
170
196
|
bodyHeight,
|
|
171
|
-
|
|
197
|
+
close,
|
|
172
198
|
show,
|
|
173
199
|
hide,
|
|
200
|
+
save,
|
|
201
|
+
cancel,
|
|
174
202
|
handleClose,
|
|
175
203
|
});
|
|
176
204
|
</script>
|