hy-app 0.2.18 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,533 +1,259 @@
1
- <template>
2
- <view class="hy-form">
3
- <view
4
- :class="['hy-form--item', borderBottom && 'hy-border__bottom']"
5
- v-for="item in columns"
6
- :key="item.field"
7
- >
8
- <view v-if="item.label" class="hy-form--item__label" :style="labelStyle">
9
- <text
10
- v-if="isRequired(item.rules)"
11
- style="color: red; font-size: 20px; line-height: 10px"
12
- >*</text
13
- >{{ item.label }} <text v-if="symbol">:</text>
14
- </view>
15
- <view class="hy-form--item__container">
16
- <view class="hy-form--item__container-content">
17
- <!-- 输入框 -->
18
- <view class="flex" v-if="isInput(item.type)">
19
- <HyInput
20
- v-model="formData[item.field]"
21
- :type="item.type"
22
- :disabled="item?.input?.disabled || input?.disabled"
23
- :disabledColor="
24
- item?.input?.disabledColor || input?.disabledColor
25
- "
26
- :maxlength="item?.input?.maxlength || input?.maxlength"
27
- :password="item?.input?.password"
28
- :clearable="item?.input?.clearable || input?.clearable"
29
- :readonly="item?.input?.readonly || input?.readonly"
30
- :placeholder="item?.input?.placeholder"
31
- :placeholderClass="
32
- item?.input?.placeholderClass || input?.placeholderClass
33
- "
34
- :placeholderStyle="
35
- item?.input?.placeholderStyle || input?.placeholderStyle
36
- "
37
- :showWordLimit="
38
- item?.input?.showWordLimit || input?.showWordLimit
39
- "
40
- :confirmType="item?.input?.confirmType || input?.confirmType"
41
- :confirmHold="item?.input?.confirmHold || input?.confirmHold"
42
- :holdKeyboard="item?.input?.holdKeyboard || input?.holdKeyboard"
43
- :focus="item?.input?.focus || input?.focus"
44
- :autoBlur="item?.input?.autoBlur || input?.autoBlur"
45
- :selectionStart="
46
- item?.input?.selectionStart || input?.selectionStart
47
- "
48
- :selectionEnd="item?.input?.selectionEnd || input?.selectionEnd"
49
- :adjustPosition="
50
- item?.input?.adjustPosition || input?.adjustPosition
51
- "
52
- :inputAlign="item?.input?.inputAlign || input?.inputAlign"
53
- :fontSize="item?.input?.fontSize || input?.fontSize"
54
- :color="item?.input?.color || input?.color"
55
- :prefixIcon="item?.input?.prefixIcon || input?.prefixIcon"
56
- :suffixIcon="item?.input?.suffixIcon || input?.suffixIcon"
57
- :formatter="item?.input?.formatter || input?.formatter"
58
- :shape="item?.input?.shape || input?.shape"
59
- :border="item?.input?.border || input?.border"
60
- :customStyle="errorStyle(!!errors[item.field])"
61
- @change="handleChange($event, item)"
62
- @blur="handleBlur($event, item)"
63
- @onPrefix="item?.input?.onPrefix"
64
- @onSuffix="item?.input?.onSuffix"
65
- ></HyInput>
66
- </view>
67
- <!-- 输入框 -->
68
-
69
- <!-- 上传 -->
70
- <template v-if="item.type === FormTypeEnum.UPLOAD">
71
- <HyUpload
72
- :fileList="formData[item.field]"
73
- :maxCount="item.maxCount"
74
- :disabled="item.disabled"
75
- />
76
- </template>
77
- <!-- 上传 -->
78
-
79
- <!-- 详情 -->
80
- <template v-if="item.type === FormTypeEnum.DETAIL">
81
- <view class="detail">
82
- {{ formData[item.field] }}
83
- </view>
84
- </template>
85
- <!-- 详情 -->
86
-
87
- <!-- 文本域 -->
88
- <view class="flex" v-if="item.type === FormTypeEnum.TEXTAREA">
89
- <HyTextarea
90
- v-model="formData[item.field]"
91
- :disabled="item?.textarea?.disabled || textarea?.disabled"
92
- :maxlength="item?.textarea?.maxlength || textarea?.maxlength"
93
- :placeholder="item?.textarea?.placeholder"
94
- :placeholderClass="
95
- item?.textarea?.placeholderClass || textarea?.placeholderClass
96
- "
97
- :placeholderStyle="
98
- item?.textarea?.placeholderStyle || textarea?.placeholderStyle
99
- "
100
- :holdKeyboard="
101
- item?.textarea?.holdKeyboard || textarea?.holdKeyboard
102
- "
103
- :focus="item?.textarea?.focus || textarea?.focus"
104
- :selectionStart="
105
- item?.textarea?.selectionStart || textarea?.selectionStart
106
- "
107
- :selectionEnd="
108
- item?.textarea?.selectionEnd || textarea?.selectionEnd
109
- "
110
- :adjustPosition="
111
- item?.textarea?.adjustPosition || textarea?.adjustPosition
112
- "
113
- :formatter="item?.textarea?.formatter || textarea?.formatter"
114
- :border="item?.textarea?.border || textarea?.border"
115
- :customStyle="errorStyle(!!errors[item.field])"
116
- :height="textarea?.height || item?.textarea?.height"
117
- @change="handleChange($event, item)"
118
- @blur="handleBlur($event, item)"
119
- ></HyTextarea>
120
- </view>
121
- <!-- 文本域 -->
122
-
123
- <!-- 复选框/单选框 -->
124
- <template v-if="item.type === FormTypeEnum.CHECK_BUTTON">
125
- <hy-check-button
126
- v-model="formData[item.field]"
127
- :columns="item.actions"
128
- :type="item?.checkButton?.type || checkButton?.type"
129
- :shape="item?.checkButton?.shape || checkButton?.shape"
130
- :size="item?.checkButton?.size || checkButton?.size"
131
- :col="item?.checkButton?.col || checkButton?.col"
132
- :gap="item?.checkButton?.gap || checkButton?.gap"
133
- :fieldNames="
134
- item?.checkButton?.fieldNames || checkButton?.fieldNames
135
- "
136
- :disabled="item?.checkButton?.size || checkButton?.disabled"
137
- :selectType="
138
- item?.checkButton?.selectType || checkButton?.selectType
139
- "
140
- ></hy-check-button>
141
- </template>
142
- <!-- 复选框/单选框 -->
143
-
144
- <!-- 单选框 -->
145
- <template v-if="item.type === FormTypeEnum.RADIO">
146
- <HyRadio
147
- v-model="formData[item.field]"
148
- :columns="item.actions"
149
- :fieldNames="item?.radio?.fieldNames || radio?.fieldNames"
150
- :shape="item?.radio?.shape || radio?.shape"
151
- :disabled="item?.radio?.disabled || radio?.disabled"
152
- :size="item?.radio?.size || radio?.size"
153
- :activeColor="item?.radio?.activeColor || radio?.activeColor"
154
- :inactiveColor="
155
- item?.radio?.inactiveColor || radio?.inactiveColor
156
- "
157
- :iconSize="item?.radio?.iconSize || radio?.iconSize"
158
- :iconColor="item?.radio?.iconColor || radio?.iconColor"
159
- :labelDisabled="item?.radio?.disabled || radio?.labelDisabled"
160
- :placement="item?.radio?.placement || radio?.placement"
161
- ></HyRadio>
162
- </template>
163
- <!-- 单选框 -->
164
-
165
- <!-- 开关 -->
166
- <template v-if="item.type === FormTypeEnum.SWITCH">
167
- <HySwitch
168
- v-model="formData[item.field]"
169
- :loading="item?.switchItem?.loading || switchItem?.loading"
170
- :disabled="item?.switchItem?.disabled || switchItem?.disabled"
171
- :size="item?.switchItem?.size || switchItem?.size"
172
- :activeColor="
173
- item?.switchItem?.activeColor || switchItem?.activeColor
174
- "
175
- :inactiveColor="
176
- item?.switchItem?.inactiveColor || switchItem?.inactiveColor
177
- "
178
- :activeValue="
179
- item?.switchItem?.activeValue || switchItem?.activeValue
180
- "
181
- :inactiveValue="
182
- item?.switchItem?.inactiveValue || switchItem?.inactiveValue
183
- "
184
- :activeIcon="
185
- item?.switchItem?.activeIcon || switchItem?.activeIcon
186
- "
187
- :inactiveIcon="
188
- item?.switchItem?.inactiveIcon || switchItem?.inactiveIcon
189
- "
190
- :space="item?.switchItem?.space || switchItem?.space"
191
- ></HySwitch>
192
- </template>
193
- <!-- 开关 -->
194
-
195
- <!-- 自定义选择器 -->
196
- <template v-if="item.type === FormTypeEnum.SELECT">
197
- <HyPicker
198
- v-model="formData[item.field]"
199
- :columns="item.select"
200
- has-input
201
- :separator="item?.picker?.separator || picker?.separator"
202
- :itemHeight="item?.picker?.itemHeight || picker?.itemHeight"
203
- :cancelText="item?.picker?.cancelText || picker?.cancelText"
204
- :confirmText="item?.picker?.confirmText || picker?.confirmText"
205
- :cancelColor="item?.picker?.cancelColor || picker?.cancelColor"
206
- :confirmColor="item?.picker?.confirmColor || picker?.confirmColor"
207
- :visibleItemCount="
208
- item?.picker?.visibleItemCount || picker?.visibleItemCount
209
- "
210
- :closeOnClickOverlay="
211
- item?.picker?.closeOnClickOverlay || picker?.closeOnClickOverlay
212
- "
213
- :title="item?.picker?.title || picker?.title"
214
- :showToolbar="item?.picker?.showToolbar || picker?.showToolbar"
215
- :customStyle="errorStyle(!!errors[item.field])"
216
- :input="{
217
- disabled:
218
- item?.picker?.input?.disabled || picker?.input?.disabled,
219
- placeholder: item.picker?.input?.placeholder,
220
- shape: item?.picker?.input?.shape || picker?.input?.shape,
221
- border: item?.picker?.input?.border || picker?.input?.border,
222
- customStyle: errorStyle(!!errors[item.field]),
223
- }"
224
- ></HyPicker>
225
- </template>
226
- <!-- 自定义选择器 -->
227
-
228
- <!-- 时间选择器 -->
229
- <template v-if="item.type === FormTypeEnum.DATE">
230
- <HyDatetimePicker
231
- v-model="formData[item.field]"
232
- has-input
233
- :mode="item.mode"
234
- :separator="item?.picker?.separator || picker?.separator"
235
- :itemHeight="item?.picker?.itemHeight || picker?.itemHeight"
236
- :cancelText="item?.picker?.cancelText || picker?.cancelText"
237
- :confirmText="item?.picker?.confirmText || picker?.confirmText"
238
- :cancelColor="item?.picker?.cancelColor || picker?.cancelColor"
239
- :confirmColor="item?.picker?.confirmColor || picker?.confirmColor"
240
- :visibleItemCount="
241
- item?.picker?.visibleItemCount || picker?.visibleItemCount
242
- "
243
- :closeOnClickOverlay="
244
- item?.picker?.closeOnClickOverlay || picker?.closeOnClickOverlay
245
- "
246
- :title="item?.picker?.title || picker?.title"
247
- :showToolbar="item?.picker?.showToolbar || picker?.showToolbar"
248
- :customStyle="errorStyle(!!errors[item.field])"
249
- :input="{
250
- disabled:
251
- item?.picker?.input?.disabled || picker?.input?.disabled,
252
- placeholder: item.picker?.input?.placeholder,
253
- shape: item?.picker?.input?.shape || picker?.input?.shape,
254
- border: item?.picker?.input?.border || picker?.input?.border,
255
- customStyle: errorStyle(!!errors[item.field]),
256
- }"
257
- ></HyDatetimePicker>
258
- </template>
259
- <!-- 时间选择器 -->
260
-
261
- <!-- 地址选择器 -->
262
- <template v-if="item.type === FormTypeEnum.ADDRESS">
263
- <HyAddressPicker
264
- v-model="formData[item.field]"
265
- has-input
266
- :separator="item?.picker?.separator || picker?.separator"
267
- :itemHeight="item?.picker?.itemHeight || picker?.itemHeight"
268
- :cancelText="item?.picker?.cancelText || picker?.cancelText"
269
- :confirmText="item?.picker?.confirmText || picker?.confirmText"
270
- :cancelColor="item?.picker?.cancelColor || picker?.cancelColor"
271
- :confirmColor="item?.picker?.confirmColor || picker?.confirmColor"
272
- :visibleItemCount="
273
- item?.picker?.visibleItemCount || picker?.visibleItemCount
274
- "
275
- :closeOnClickOverlay="
276
- item?.picker?.closeOnClickOverlay || picker?.closeOnClickOverlay
277
- "
278
- :title="item?.picker?.title || picker?.title"
279
- :showToolbar="item?.picker?.showToolbar || picker?.showToolbar"
280
- :customStyle="errorStyle(!!errors[item.field])"
281
- :input="{
282
- disabled:
283
- item?.picker?.input?.disabled || picker?.input?.disabled,
284
- placeholder: item.picker?.input?.placeholder,
285
- shape: item?.picker?.input?.shape || picker?.input?.shape,
286
- border: item?.picker?.input?.border || picker?.input?.border,
287
- customStyle: errorStyle(!!errors[item.field]),
288
- }"
289
- ></HyAddressPicker>
290
- </template>
291
- <!-- 地址选择器 -->
292
-
293
- <!-- 自定义插槽 -->
294
- <view class="flex" v-if="item.type === FormTypeEnum.CUSTOM">
295
- <slot
296
- :name="item.field"
297
- :record="item"
298
- :errorStyle="errorStyle(!!errors[item.field])"
299
- ></slot>
300
- </view>
301
- <!-- 自定义插槽 -->
302
- </view>
303
-
304
- <!-- 提示信息 -->
305
- <HyTransition :show="!!errors[item.field]" mode="slide-left">
306
- <view class="hy-form--item__container-warning">{{
307
- errors[item.field]
308
- }}</view>
309
- </HyTransition>
310
- <!-- 提示信息 -->
311
- </view>
312
- </view>
313
- </view>
314
- </template>
315
-
316
- <script lang="ts">
317
- export default {
318
- name: 'hy-form',
319
- options: {
320
- addGlobalClass: true,
321
- virtualHost: true,
322
- styleIsolation: 'shared'
323
- }
324
- }
325
- </script>
326
-
327
- <script setup lang="ts">
328
- import { computed, type CSSProperties, reactive, toRefs } from "vue";
329
- import type IProps from "./typing";
330
- import defaultProps from "./props";
331
- import { addUnit, error } from "../../utils";
332
- import type { FormColumnsType, RulesVo } from "../../typing";
333
- import { FormTypeEnum } from "../../typing";
334
-
335
- // 组件
336
- import HyInput from "../hy-input/hy-input.vue";
337
- import HyPicker from "../hy-picker/hy-picker.vue";
338
- import HyUpload from "../hy-upload/hy-upload.vue";
339
- import HyTextarea from "../hy-textarea/hy-textarea.vue";
340
- import HySwitch from "../hy-switch/hy-switch.vue";
341
- import HyRadio from "../hy-radio/hy-radio.vue";
342
- import HyDatetimePicker from "../hy-datetime-picker/hy-datetime-picker.vue";
343
- import HyAddressPicker from "../hy-address-picker/hy-address-picker.vue";
344
- import HyTransition from "../hy-transition/hy-transition.vue";
345
- import HyCheckButton from "../hy-check-button/hy-check-button.vue";
346
-
347
- const props = withDefaults(defineProps<IProps>(), defaultProps);
348
- const {
349
- formData,
350
- columns,
351
- labelWidth,
352
- labelAlign,
353
- labelPosition,
354
- borderBottom,
355
- } = toRefs(props);
356
- const emit = defineEmits(["click"]);
357
-
358
- const labelPos = labelPosition.value === "top" ? "column" : "row";
359
- const isInput = (type: FormTypeEnum) =>
360
- type === FormTypeEnum.TEXT ||
361
- type === FormTypeEnum.NUMBER ||
362
- type === FormTypeEnum.PASSWORD;
363
-
364
- /**
365
- * @description 错误输入框样式
366
- * */
367
- const errorStyle = computed(() => {
368
- return (err: boolean) => {
369
- const style: CSSProperties = {};
370
- if (err) {
371
- style.background = "#dd6161";
372
- }
373
-
374
- return style;
375
- };
376
- });
377
-
378
- const isRequired = computed(() => {
379
- return (temp: any) => {
380
- if (Array.isArray(temp)) {
381
- return temp.some((item) => item?.required);
382
- } else {
383
- return temp?.required;
384
- }
385
- };
386
- });
387
-
388
- const errors: AnyObject = reactive({});
389
-
390
- /**
391
- * @description 标题行内样式
392
- * */
393
- const labelStyle = computed(() => {
394
- return {
395
- textAlign: labelAlign.value,
396
- width: addUnit(labelWidth.value),
397
- };
398
- });
399
-
400
- /**
401
- * @description 错误信息校验
402
- * */
403
- const errorMsg = (rule: RulesVo, value: string): string => {
404
- // 手机正则
405
- const phoneExpression = /^1[3-9]\d{9}$/;
406
- // 邮箱正则
407
- const emitExpression = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
408
- // 复杂密码正则
409
- const passwordExpression =
410
- /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/;
411
- if (rule.required && !value) {
412
- return rule.message!;
413
- } else if (
414
- (rule?.min || rule?.max) &&
415
- (value.length < rule?.min! || value.length > rule?.max!)
416
- ) {
417
- return rule.message!;
418
- } else if (rule.type === "phone" && !phoneExpression.test(String(value))) {
419
- return rule.message || "请输入正确的手机号";
420
- } else if (rule.type === "email" && !emitExpression.test(String(value))) {
421
- return rule.message || "请输入正确的邮箱格式";
422
- } else if (
423
- rule.type === "password" &&
424
- !passwordExpression.test(String(value))
425
- ) {
426
- return (
427
- rule.message || "至少8个字符串,并且包含大、小写字母、数字和特殊符号"
428
- );
429
- } else if (rule?.validator && !rule.validator(rule, value)) {
430
- return rule.message || "";
431
- } else {
432
- return "";
433
- }
434
- };
435
-
436
- /**
437
- * @description 校验字段
438
- * */
439
- const validateField = (
440
- rules: RulesVo | RulesVo[] | undefined,
441
- value: string,
442
- field: string,
443
- event?: "blur" | "change",
444
- ) => {
445
- if (!rules) return;
446
-
447
- let errorMessage = "";
448
-
449
- if (Array.isArray(rules)) {
450
- // 遍历所有规则
451
- for (const index in rules) {
452
- // 判断是否有change事件或者blur事件
453
- if (event && !rules[index]?.trigger?.includes(event)) continue;
454
- errorMessage = errorMsg(rules[index], value);
455
- if (errorMessage) {
456
- errors[field] = errorMessage;
457
- return;
458
- }
459
- // 可以继续添加更多规则
460
- }
461
- } else {
462
- // 必填校验
463
- if (event && !rules?.trigger?.includes(event)) return;
464
- errorMessage = errorMsg(rules, value);
465
- }
466
-
467
- // 自定义校验规则
468
- // if (rules.custom) {
469
- // const customError = rules.custom(value);
470
- // if (customError) {
471
- // errorMessage = customError;
472
- // }
473
- // }
474
-
475
- errors[field] = errorMessage;
476
- };
477
-
478
- /**
479
- * @description 提交表单
480
- * */
481
- const handleSubmit = () => {
482
- return new Promise((resolve, reject) => {
483
- let isValid = true;
484
-
485
- // 校验所有字段
486
- props.columns.forEach((col) => {
487
- validateField(col.rules, formData.value[col.field], col.field);
488
- if (errors[col.field]) {
489
- isValid = false;
490
- }
491
- });
492
-
493
- if (isValid) {
494
- // alert("表单提交成功!");
495
- resolve(formData.value);
496
- } else {
497
- reject();
498
- error("表单校验失败,请检查输入!");
499
- }
500
- });
501
- };
502
-
503
- /**
504
- * @description 输入值触发
505
- * */
506
- const handleChange = (event: string, temp: FormColumnsType) => {
507
- if (isInput(temp.type) && temp?.input?.onChange) {
508
- temp.input.onChange(event, temp);
509
- }
510
- validateField(temp.rules, event, temp.field, "change");
511
- };
512
- /**
513
- * @description 输入值触发
514
- * */
515
- const handleBlur = (event: string, temp: FormColumnsType) => {
516
- if (isInput(temp.type) && temp?.input?.onBlur) {
517
- temp.input.onBlur(event, temp);
518
- }
519
- validateField(temp.rules, event, temp.field, "blur");
520
- };
521
-
522
- defineExpose({
523
- validateField,
524
- handleSubmit,
525
- });
526
- </script>
527
-
528
- <style lang="scss" scoped>
529
- @import "./index.scss";
530
- .hy-form--item {
531
- flex-direction: v-bind(labelPos);
532
- }
533
- </style>
1
+ <template>
2
+ <view class="hy-form">
3
+ <slot></slot>
4
+ </view>
5
+ </template>
6
+
7
+ <script lang="ts">
8
+ export default {
9
+ name: 'hy-form',
10
+ options: {
11
+ addGlobalClass: true,
12
+ virtualHost: true,
13
+ styleIsolation: 'shared',
14
+ },
15
+ }
16
+ </script>
17
+
18
+ <script setup lang="ts">
19
+ import { provide, reactive, ref, toRefs } from 'vue'
20
+ import type { PropType } from 'vue'
21
+ import type { FormItemRule } from './typing'
22
+ import { clearVal } from '../../utils'
23
+
24
+ /**
25
+ * 表单组件父组件,需要搭配hy-form-item
26
+ * @displayName hy-form
27
+ */
28
+ defineOptions({})
29
+
30
+ const props = defineProps({
31
+ /** 表单数据对象 */
32
+ model: Object as PropType<AnyObject>,
33
+ /** 表单校验规则 */
34
+ rules: Object as PropType<FormItemRule>,
35
+ /** 表单底部边框 */
36
+ border: {
37
+ type: Boolean,
38
+ default: false,
39
+ },
40
+ /** label标签的宽度,单位rpx */
41
+ labelWidth: {
42
+ type: String,
43
+ default: 'auto',
44
+ },
45
+ /**
46
+ * 标签位置
47
+ * @values left,top
48
+ * */
49
+ labelPosition: {
50
+ type: String,
51
+ default: 'left',
52
+ },
53
+ /**
54
+ * 标签位置
55
+ * @values left,center,right
56
+ * */
57
+ labelAlign: {
58
+ type: String,
59
+ default: 'left',
60
+ },
61
+ })
62
+ const { rules, border, labelWidth, labelAlign, labelPosition } = toRefs(props)
63
+
64
+ const emit = defineEmits<{
65
+ submit: [data: Record<string, any>]
66
+ validate: [valid: boolean, errors: Record<string, string>]
67
+ }>()
68
+
69
+ // 表单数据
70
+ const formData = reactive(props.model || {})
71
+ const formItems = ref<any[]>([])
72
+ const errors = reactive<Record<string, string>>({})
73
+
74
+ // 表单上下文
75
+ const formContext = {
76
+ formData,
77
+ errors,
78
+ rules: rules.value,
79
+ border: border.value,
80
+ labelWidth: labelWidth.value,
81
+ labelPosition: labelPosition.value,
82
+ labelAlign: labelAlign.value,
83
+ addFormItem: (item: any) => {
84
+ formItems.value.push(item)
85
+ },
86
+ removeFormItem: (item: any) => {
87
+ const index = formItems.value.indexOf(item)
88
+ if (index > -1) {
89
+ formItems.value.splice(index, 1)
90
+ }
91
+ },
92
+ validateField: (field: string, value: any, trigger?: 'blur' | 'change') => {
93
+ const fieldRules = props.rules?.[field]
94
+ if (!fieldRules) return true
95
+
96
+ const rules = Array.isArray(fieldRules) ? fieldRules : [fieldRules]
97
+ let isValid = true
98
+ let errorMessage = ''
99
+
100
+ for (const rule of rules) {
101
+ // 检查触发时机
102
+ if (
103
+ (trigger && !rule.trigger) ||
104
+ (trigger && rule.trigger && !rule.trigger.includes(trigger))
105
+ ) {
106
+ continue
107
+ }
108
+
109
+ // 必填校验
110
+ if (rule.required && (!value || value === '')) {
111
+ errorMessage = rule.message || `${field} 是必填项`
112
+ isValid = false
113
+ break
114
+ }
115
+
116
+ // 长度校验
117
+ if (rule.min && String(value).length < rule.min) {
118
+ errorMessage = rule.message || `${field} 长度不能少于 ${rule.min} 个字符`
119
+ isValid = false
120
+ break
121
+ }
122
+
123
+ if (rule.max && String(value).length > rule.max) {
124
+ errorMessage = rule.message || `${field} 长度不能超过 ${rule.max} 个字符`
125
+ isValid = false
126
+ break
127
+ }
128
+
129
+ // 类型校验
130
+ if (rule.type === 'phone') {
131
+ const phoneRegex = /^1[3-9]\d{9}$/
132
+ if (!phoneRegex.test(String(value))) {
133
+ errorMessage = rule.message || '请输入正确的手机号'
134
+ isValid = false
135
+ break
136
+ }
137
+ }
138
+
139
+ if (rule.type === 'email') {
140
+ const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/
141
+ if (!emailRegex.test(String(value))) {
142
+ errorMessage = rule.message || '请输入正确的邮箱格式'
143
+ isValid = false
144
+ break
145
+ }
146
+ }
147
+
148
+ if (rule.type === 'password') {
149
+ const passwordRegex =
150
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/
151
+ if (!passwordRegex.test(String(value))) {
152
+ errorMessage = rule.message || '密码至少8个字符,包含大小写字母、数字和特殊符号'
153
+ isValid = false
154
+ break
155
+ }
156
+ }
157
+
158
+ // 自定义校验
159
+ if (rule.validator) {
160
+ const result = rule.validator(value)
161
+ if (result === false || typeof result === 'string') {
162
+ errorMessage = typeof result === 'string' ? result : rule.message || `${field} 校验失败`
163
+ isValid = false
164
+ break
165
+ }
166
+ }
167
+ }
168
+
169
+ if (isValid) {
170
+ delete errors[field]
171
+ } else {
172
+ errors[field] = errorMessage
173
+ }
174
+
175
+ return isValid
176
+ },
177
+ setFieldValue: (field: string, value: any) => {
178
+ formData[field] = value
179
+ },
180
+ getFieldValue: (field: string) => {
181
+ return formData[field]
182
+ },
183
+ }
184
+
185
+ // 提供表单上下文给子组件
186
+ provide('formContext', formContext)
187
+
188
+ // 验证所有字段
189
+ const validate = () => {
190
+ return new Promise((resolve, reject) => {
191
+ let isValid = true
192
+ const allErrors: Record<string, string> = {}
193
+
194
+ formItems.value.forEach((item) => {
195
+ const field = item.props?.prop
196
+ if (field) {
197
+ const value = formData[field]
198
+ const fieldValid = formContext.validateField(field, value)
199
+ if (!fieldValid) {
200
+ isValid = false
201
+ allErrors[field] = errors[field]
202
+ }
203
+ }
204
+ })
205
+
206
+ emit('validate', isValid, allErrors)
207
+ if (isValid) {
208
+ resolve(isValid)
209
+ } else {
210
+ reject(allErrors)
211
+ }
212
+ })
213
+ }
214
+
215
+ // 重置表单
216
+ const resetFields = () => {
217
+ clearVal(formData)
218
+ Object.keys(errors).forEach((key) => {
219
+ delete errors[key]
220
+ })
221
+ }
222
+
223
+ // 清除验证
224
+ const clearValidate = (fields?: string[]) => {
225
+ if (fields) {
226
+ fields.forEach((field) => {
227
+ delete errors[field]
228
+ })
229
+ } else {
230
+ Object.keys(errors).forEach((key) => {
231
+ delete errors[key]
232
+ })
233
+ }
234
+ }
235
+
236
+ // 提交表单
237
+ const submit = async () => {
238
+ if (await validate()) {
239
+ emit('submit', { ...formData })
240
+ return formData
241
+ }
242
+ return false
243
+ }
244
+
245
+ defineExpose({
246
+ validate,
247
+ resetFields,
248
+ clearValidate,
249
+ submit,
250
+ formData,
251
+ errors,
252
+ })
253
+ </script>
254
+
255
+ <style lang="scss" scoped>
256
+ .hy-form {
257
+ width: 100%;
258
+ }
259
+ </style>