tang-ui-x 1.3.2 → 1.3.3

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.
@@ -31,8 +31,24 @@ const emit = defineEmits<{
31
31
  close: []
32
32
  }>()
33
33
 
34
- // 使用 i18n
35
- const { $t } = useI18n()
34
+ // 使用 i18n
35
+ const { $t } = useI18n()
36
+
37
+ const hasTextValue = (value: string | null | undefined): boolean => {
38
+ return value !== undefined && value !== null && value !== ''
39
+ }
40
+
41
+ const hasTitle = computed(() => {
42
+ return hasTextValue(props.title)
43
+ })
44
+
45
+ const hasDescription = computed(() => {
46
+ return hasTextValue(props.description)
47
+ })
48
+
49
+ const showHeader = computed(() => {
50
+ return hasTitle.value || hasDescription.value
51
+ })
36
52
 
37
53
  // 优先使用用户传入的值,否则使用翻译
38
54
  const displayCancelText = computed(() => {
@@ -75,10 +91,10 @@ const handleClose = (): void => {
75
91
  >
76
92
  <view class="t-action-sheet bg-transparent p-0" :class="customClass" :style="customStyle">
77
93
  <!-- 头部:标题和描述 -->
78
- <view v-if="title || description" class="t-action-sheet__header border-b border-[#ebedf0] p-4 text-center">
79
- <text v-if="title" class="t-action-sheet__title mb-2 block text-base font-medium text-[#323233]">{{ title }}</text>
80
- <text v-if="description" class="t-action-sheet__description block text-sm text-[#969799]">{{ description }}</text>
81
- </view>
94
+ <view v-if="showHeader" class="t-action-sheet__header border-b border-[#ebedf0] p-4 text-center">
95
+ <text v-if="hasTitle" class="t-action-sheet__title mb-2 block text-base font-medium text-[#323233]">{{ title }}</text>
96
+ <text v-if="hasDescription" class="t-action-sheet__description block text-sm text-[#969799]">{{ description }}</text>
97
+ </view>
82
98
 
83
99
  <!-- 操作列表 -->
84
100
  <view class="t-action-sheet__actions max-h-[400px] overflow-y-auto">
@@ -24,8 +24,20 @@ const emit = defineEmits<{
24
24
  error: []
25
25
  }>()
26
26
 
27
- // 图片加载失败标记
28
- const hasLoadError = ref(false)
27
+ // 图片加载失败标记
28
+ const hasLoadError = ref(false)
29
+
30
+ const hasTextValue = (value: string | null | undefined): boolean => {
31
+ return value !== undefined && value !== null && value !== ''
32
+ }
33
+
34
+ const hasSrc = computed(() => {
35
+ return hasTextValue(props.src)
36
+ })
37
+
38
+ const hasAlt = computed(() => {
39
+ return hasTextValue(props.alt)
40
+ })
29
41
 
30
42
  /**
31
43
  * 计算头像大小
@@ -35,14 +47,16 @@ const avatarSize = computed(() => {
35
47
  return props.size
36
48
  }
37
49
 
38
- const sizeMap = {
39
- small: 32,
40
- medium: 40,
41
- large: 56
42
- }
43
-
44
- return sizeMap[props.size] || 40
45
- })
50
+ const sizeMap = {
51
+ small: 32,
52
+ medium: 40,
53
+ large: 56
54
+ }
55
+
56
+ if (props.size === 'small') return sizeMap.small
57
+ if (props.size === 'large') return sizeMap.large
58
+ return sizeMap.medium
59
+ })
46
60
 
47
61
  /**
48
62
  * 计算头像样式类
@@ -69,9 +83,9 @@ const avatarStyle = computed(() => {
69
83
  styles.push(`width: ${size}px`)
70
84
  styles.push(`height: ${size}px`)
71
85
 
72
- if (!props.src || hasLoadError.value) {
73
- styles.push(`background-color: ${props.bgColor}`)
74
- }
86
+ if (!hasSrc.value || hasLoadError.value) {
87
+ styles.push(`background-color: ${props.bgColor}`)
88
+ }
75
89
 
76
90
  return styles.join('; ')
77
91
  })
@@ -106,26 +120,26 @@ const handleError = (): void => {
106
120
  /**
107
121
  * 获取显示的文字(取首字符)
108
122
  */
109
- const displayText = computed(() => {
110
- if (props.alt) {
111
- return props.alt.charAt(0).toUpperCase()
112
- }
113
- return ''
123
+ const displayText = computed(() => {
124
+ if (hasAlt.value) {
125
+ return props.alt.charAt(0).toUpperCase()
126
+ }
127
+ return ''
114
128
  })
115
129
  </script>
116
130
 
117
131
  <template>
118
132
  <view :class="avatarClass" :style="avatarStyle" @click="handleClick">
119
- <image
120
- v-if="src && !hasLoadError"
121
- class="t-avatar__image h-full w-full"
122
- :src="src"
123
- :mode="fit"
124
- @error="handleError"
125
- />
126
- <text v-else-if="alt" class="t-avatar__text text-center font-medium" :style="textStyle">
127
- {{ displayText }}
128
- </text>
133
+ <image
134
+ v-if="hasSrc && !hasLoadError"
135
+ class="t-avatar__image h-full w-full"
136
+ :src="src"
137
+ :mode="fit"
138
+ @error="handleError"
139
+ />
140
+ <text v-else-if="hasAlt" class="t-avatar__text text-center font-medium" :style="textStyle">
141
+ {{ displayText }}
142
+ </text>
129
143
  <slot v-else></slot>
130
144
  </view>
131
145
  </template>
@@ -22,6 +22,10 @@ const emit = defineEmits<{
22
22
  click: [event: any]
23
23
  }>()
24
24
 
25
+ const isChecked = computed(() => {
26
+ return model.value === true
27
+ })
28
+
25
29
  const getCheckboxSize = (): number => {
26
30
  switch (props.size) {
27
31
  case 'small':
@@ -79,9 +83,9 @@ const checkboxStyle = computed(() => {
79
83
  styles.push(`height: var(--checkbox-size)`)
80
84
  styles.push(`border-width: var(--checkbox-border-width)`)
81
85
  styles.push('border-style: solid')
82
- styles.push(`border-color: ${model.value ? props.activeColor : props.inactiveColor}`)
86
+ styles.push(`border-color: ${isChecked.value ? props.activeColor : props.inactiveColor}`)
83
87
 
84
- if (model.value) {
88
+ if (isChecked.value) {
85
89
  styles.push(`background-color: ${props.activeColor}`)
86
90
  }
87
91
 
@@ -124,7 +128,7 @@ const handleTouchStart = () => {
124
128
  >
125
129
  <view :class="iconClass" :style="checkboxStyle">
126
130
  <!-- 选中状态的图标 -->
127
- <view v-if="model" class="flex items-center justify-center" :style="`width: var(--checkbox-icon-size); height: var(--checkbox-icon-size);`">
131
+ <view v-if="isChecked" class="flex items-center justify-center" :style="`width: var(--checkbox-icon-size); height: var(--checkbox-icon-size);`">
128
132
  <text class="font-bold text-white" :style="checkmarkStyle">✓</text>
129
133
  </view>
130
134
 
@@ -145,7 +145,14 @@ const getFirstDayOfMonth = (year: number, month: number): number => {
145
145
  }
146
146
 
147
147
  // 使用导出的日期项类型
148
- type DayItem = TDateTimePickerDayItem
148
+ type DayItem = TDateTimePickerDayItem
149
+ type DayClassMap = {
150
+ 't-datetime-picker__day--other': boolean
151
+ 't-datetime-picker__day--disabled': boolean
152
+ 't-datetime-picker__day--range-start': boolean
153
+ 't-datetime-picker__day--range-end': boolean
154
+ 't-datetime-picker__day--in-range': boolean
155
+ }
149
156
 
150
157
  // 将日期转换为时间戳(用于范围比较)
151
158
  const dateToTimestamp = (year: number, month: number, day: number): number => {
@@ -208,10 +215,10 @@ const getDayStyle = (item: DayItem): string => {
208
215
  }
209
216
 
210
217
  // 获取日期单元格类名
211
- const getDayClass = (item: DayItem): Record<string, boolean> => {
212
- return {
213
- 't-datetime-picker__day--other': !item.isCurrentMonth,
214
- 't-datetime-picker__day--disabled': item.isDisabled,
218
+ const getDayClass = (item: DayItem): DayClassMap => {
219
+ return {
220
+ 't-datetime-picker__day--other': !item.isCurrentMonth,
221
+ 't-datetime-picker__day--disabled': item.isDisabled,
215
222
  't-datetime-picker__day--range-start': item.isRangeStart === true,
216
223
  't-datetime-picker__day--range-end': item.isRangeEnd === true,
217
224
  't-datetime-picker__day--in-range': item.isInRange === true
@@ -654,7 +661,7 @@ watch(visible, (newVal: boolean) => {
654
661
  setValueByTimestamp(Date.now())
655
662
  }
656
663
  } else {
657
- setValueByTimestamp(selectedValue.value || Date.now())
664
+ setValueByTimestamp(selectedValue.value !== 0 ? selectedValue.value : Date.now())
658
665
  }
659
666
  // 根据模式设置初始面板
660
667
  if (props.mode === 'month') {
@@ -32,8 +32,20 @@ const emit = defineEmits<{
32
32
  close: []
33
33
  }>()
34
34
 
35
- // 使用 i18n
36
- const { $t } = useI18n()
35
+ // 使用 i18n
36
+ const { $t } = useI18n()
37
+
38
+ const hasTextValue = (value: string | null | undefined): boolean => {
39
+ return value !== undefined && value !== null && value !== ''
40
+ }
41
+
42
+ const hasTitle = computed(() => {
43
+ return hasTextValue(props.title)
44
+ })
45
+
46
+ const showHeader = computed(() => {
47
+ return hasTitle.value || props.showClose
48
+ })
37
49
 
38
50
  // 优先使用用户传入的值,否则使用翻译
39
51
  const displayConfirmText = computed(() => {
@@ -153,7 +165,7 @@ const handleClose = (): void => {
153
165
  >
154
166
  <view class="min-w-[280px] max-w-[90%] overflow-hidden rounded-lg bg-white" :style="dialogStyle">
155
167
  <!-- 头部 -->
156
- <view v-if="title || showClose" class="relative flex min-h-12 items-center justify-center border-b border-[#ebeef5] px-5 py-4">
168
+ <view v-if="showHeader" class="relative flex min-h-12 items-center justify-center border-b border-[#ebeef5] px-5 py-4">
157
169
  <!-- 关闭按钮(定位方式,不占用文档流) -->
158
170
  <view
159
171
  v-if="showClose"
@@ -167,7 +179,7 @@ const handleClose = (): void => {
167
179
  <!-- 标题(居中,支持插槽) -->
168
180
  <view class="relative flex w-full items-center justify-center">
169
181
  <slot name="title">
170
- <text v-if="title" class="text-lg font-semibold leading-[1.4] text-[#303133]">{{ title }}</text>
182
+ <text v-if="hasTitle" class="text-lg font-semibold leading-[1.4] text-[#303133]">{{ title }}</text>
171
183
  </slot>
172
184
  </view>
173
185
  </view>
@@ -12,7 +12,11 @@
12
12
  import type { FormOption, FormSchema, TFormProps, ComponentProps } from './type.uts'
13
13
  import { useI18n } from '../../composables/useI18n.uts'
14
14
 
15
- type FormModelValue = Record<string, any>
15
+ type FormModelValue = any
16
+ type FormSubmitResult = {
17
+ valid : boolean
18
+ values : any
19
+ }
16
20
 
17
21
  const props = withDefaults(defineProps<TFormProps>(), {
18
22
  labelWidth: '110rpx',
@@ -23,7 +27,7 @@
23
27
  const emit = defineEmits(['submit', 'reset'])
24
28
 
25
29
  const model = defineModel({ default: () => ({}) }) as Ref<FormModelValue>
26
- const errors = reactive<Record<string, string>>({})
30
+ const errors = reactive({}) as any
27
31
 
28
32
  // 使用 i18n
29
33
  const { $t } = useI18n()
@@ -35,37 +39,65 @@
35
39
  : $t('form.submitButton')
36
40
  })
37
41
 
38
- const resetButtonText = computed(() => {
39
- return props.resetText !== undefined && props.resetText !== ''
40
- ? props.resetText
41
- : $t('form.resetButton')
42
- })
43
-
44
- // 获取单个配置项的布局方向(单个配置优先)
45
- const getItemLayout = (item : FormSchema) : string => {
46
- return item.layout || props.layout
47
- }
48
-
49
- // 获取单个配置项的标签宽度(单个配置优先)
50
- const getItemLabelWidth = (item : FormSchema) : string => {
51
- return item.labelWidth || props.labelWidth
52
- }
53
-
54
- const handlePlaceholder = (item : FormSchema) : string => {
55
- if (item.componentProps?.placeholder) {
56
- return item.componentProps.placeholder
57
- }
58
- return $t('form.inputPlaceholder', { label: item.label })
59
- }
60
-
61
- const getSelectOptions = (item : FormSchema) => {
62
- const options = item.componentProps?.options as FormOption[] | undefined
63
- return options?.map(o => ({ label: o.label, value: o.value })) || []
64
- }
65
-
66
- const getPickerTextClass = (item : FormSchema) : string => {
67
- return model.value[item.field] ? 'text-[#333333]' : 'text-[#c0c4cc]'
68
- }
42
+ const resetButtonText = computed(() => {
43
+ return props.resetText !== undefined && props.resetText !== ''
44
+ ? props.resetText
45
+ : $t('form.resetButton')
46
+ })
47
+
48
+ const hasTextValue = (value : string | null | undefined) : boolean => {
49
+ return value !== undefined && value !== null && value !== ''
50
+ }
51
+
52
+ // 获取单个配置项的布局方向(单个配置优先)
53
+ const getItemLayout = (item : FormSchema) : string => {
54
+ return hasTextValue(item.layout) ? item.layout as string : props.layout
55
+ }
56
+
57
+ // 获取单个配置项的标签宽度(单个配置优先)
58
+ const getItemLabelWidth = (item : FormSchema) : string => {
59
+ return hasTextValue(item.labelWidth) ? item.labelWidth as string : props.labelWidth
60
+ }
61
+
62
+ const handlePlaceholder = (item : FormSchema) : string => {
63
+ const placeholder = item.componentProps?.placeholder
64
+ if (hasTextValue(placeholder)) {
65
+ return placeholder as string
66
+ }
67
+ return $t('form.inputPlaceholder', { label: item.label })
68
+ }
69
+
70
+ const getSelectOptions = (item : FormSchema) => {
71
+ const options = item.componentProps?.options as FormOption[] | undefined
72
+ if (options != null) {
73
+ return options.map(o => ({ label: o.label, value: o.value }))
74
+ }
75
+ return []
76
+ }
77
+
78
+ const getPickerTextClass = (item : FormSchema) : string => {
79
+ return isFieldEmpty(model.value[item.field]) ? 'text-[#c0c4cc]' : 'text-[#333333]'
80
+ }
81
+
82
+ const getInputType = (item : FormSchema) : string => {
83
+ const inputType = item.componentProps?.type
84
+ return hasTextValue(inputType) ? inputType as string : 'text'
85
+ }
86
+
87
+ const getSelectPlaceholder = (item : FormSchema) : string => {
88
+ const placeholder = item.componentProps?.placeholder
89
+ return hasTextValue(placeholder) ? placeholder as string : $t('form.selectPlaceholder')
90
+ }
91
+
92
+ const getPickerDisplayText = (item : FormSchema, placeholderKey : string) : string => {
93
+ const value = model.value[item.field]
94
+ return isFieldEmpty(value) ? $t(placeholderKey) : value as string
95
+ }
96
+
97
+ const getNumericFieldValue = (field : string) : number => {
98
+ const value = model.value[field]
99
+ return isFieldEmpty(value) ? 0 : value as number
100
+ }
69
101
 
70
102
  // 安全获取组件属性,避免 v-bind="" 编译问题
71
103
  const getComponentProps = (item : FormSchema) : ComponentProps => {
@@ -75,9 +107,9 @@
75
107
  return {} as ComponentProps
76
108
  }
77
109
 
78
- const getInputProps = (item : FormSchema, inputType : string) : Record<string, any> => {
79
- const componentProps = getComponentProps(item)
80
- const mergedProps: Record<string, any> = {
110
+ const getInputProps = (item : FormSchema, inputType : string) : any => {
111
+ const componentProps = getComponentProps(item)
112
+ const mergedProps: any = {
81
113
  ...componentProps,
82
114
  type: inputType,
83
115
  placeholder: handlePlaceholder(item)
@@ -91,8 +123,8 @@
91
123
  }
92
124
 
93
125
  // 日期时间选择器状态管理
94
- const pickerVisible = reactive<Record<string, boolean>>({})
95
- const pickerValues = reactive<Record<string, number>>({})
126
+ const pickerVisible = reactive({}) as any
127
+ const pickerValues = reactive({}) as any
96
128
 
97
129
  const openPicker = (field : string) => {
98
130
  pickerVisible[field] = true
@@ -153,8 +185,8 @@
153
185
  * 获取当前有效错误信息
154
186
  * @returns 当前存在错误文案的字段映射
155
187
  */
156
- const getActiveErrors = () : Record<string, string> => {
157
- const activeErrors : Record<string, string> = {}
188
+ const getActiveErrors = () : any => {
189
+ const activeErrors : any = {}
158
190
  Object.keys(errors).forEach((field : string) => {
159
191
  const message = errors[field]
160
192
  if (message !== '') {
@@ -175,14 +207,22 @@
175
207
  }
176
208
 
177
209
  // 抖动动画状态
178
- const shakeFields = reactive<Record<string, boolean>>({})
179
-
180
- const triggerShake = (field : string) => {
181
- shakeFields[field] = true
182
- setTimeout(() => {
183
- shakeFields[field] = false
184
- }, 500)
185
- }
210
+ const shakeFields = reactive({}) as any
211
+
212
+ const triggerShake = (field : string) => {
213
+ shakeFields[field] = true
214
+ setTimeout(() => {
215
+ shakeFields[field] = false
216
+ }, 500)
217
+ }
218
+
219
+ const hasFieldError = (field : string) : boolean => {
220
+ return errors[field] !== undefined && errors[field] !== ''
221
+ }
222
+
223
+ const isFieldShaking = (field : string) : boolean => {
224
+ return shakeFields[field] === true
225
+ }
186
226
 
187
227
  const onSwitchChange = (value : boolean, item : FormSchema) => {
188
228
  model.value[item.field] = value
@@ -199,7 +239,7 @@
199
239
  validateField(item)
200
240
  }
201
241
 
202
- const onFormSubmit = async (_e : UniFormSubmitEvent) : Promise<{ valid : boolean, values : any }> => {
242
+ const onFormSubmit = async (_e : UniFormSubmitEvent) : Promise<FormSubmitResult> => {
203
243
  clearErrors()
204
244
 
205
245
  let hasError = false
@@ -259,13 +299,13 @@
259
299
  <view class="p-4">
260
300
  <form @submit="onFormSubmit" @reset="onFormReset">
261
301
  <view
262
- v-for="(item, index) in schemas"
263
- class="mb-4 overflow-visible rounded-[12rpx] border-[2rpx] border-transparent p-2 transition-all duration-300"
264
- :class="[
265
- errors[item.field] ? 'border-[#ff4d4f] bg-[#fff1f0]' : '',
266
- shakeFields[item.field] ? 'animate-shake-soft' : ''
267
- ]"
268
- :key="index">
302
+ v-for="(item, index) in schemas"
303
+ class="mb-4 overflow-visible rounded-[12rpx] border-[2rpx] border-transparent p-2 transition-all duration-300"
304
+ :class="[
305
+ hasFieldError(item.field) ? 'border-[#ff4d4f] bg-[#fff1f0]' : '',
306
+ isFieldShaking(item.field) ? 'animate-shake-soft' : ''
307
+ ]"
308
+ :key="index">
269
309
  <view
270
310
  class="overflow-visible"
271
311
  :class="getItemLayout(item) === 'horizontal'
@@ -287,12 +327,12 @@
287
327
  <slot :name="item.field" :item="item" :value="model[item.field]"
288
328
  :error="errors[item.field]">
289
329
 
290
- <!-- 输入框 -->
291
- <TInput v-if="item.component === 'Input'"
292
- v-model="model[item.field]"
293
- v-bind="getInputProps(item, item.componentProps?.type || 'text')"
294
- @input="validateField(item)"
295
- @change="validateField(item)" />
330
+ <!-- 输入框 -->
331
+ <TInput v-if="item.component === 'Input'"
332
+ v-model="model[item.field]"
333
+ v-bind="getInputProps(item, getInputType(item))"
334
+ @input="validateField(item)"
335
+ @change="validateField(item)" />
296
336
 
297
337
  <!-- 数字输入 -->
298
338
  <TInput v-else-if="item.component === 'InputNumber'"
@@ -309,20 +349,20 @@
309
349
  @change="validateField(item)" />
310
350
 
311
351
  <!-- 下拉选择 -->
312
- <TSelect v-else-if="item.component === 'Select'"
313
- v-model="model[item.field]"
314
- :options="getSelectOptions(item)"
315
- :placeholder="item.componentProps?.placeholder || $t('form.selectPlaceholder')"
316
- v-bind="getComponentProps(item)"
317
- @change="validateField(item)" />
352
+ <TSelect v-else-if="item.component === 'Select'"
353
+ v-model="model[item.field]"
354
+ :options="getSelectOptions(item)"
355
+ :placeholder="getSelectPlaceholder(item)"
356
+ v-bind="getComponentProps(item)"
357
+ @change="validateField(item)" />
318
358
 
319
359
  <!-- 日期选择 -->
320
- <view
321
- v-else-if="item.component === 'Date'"
322
- class="flex w-full items-center rounded-[8rpx] border border-solid border-[#e0e0e0] bg-[#f5f5f5] px-3 text-[28rpx] transition-all duration-300 active:border-[#007aff] active:bg-white"
323
- @click="openPicker(item.field)">
324
- <text :class="getPickerTextClass(item)">{{ model[item.field] || $t('form.datePlaceholder') }}</text>
325
- </view>
360
+ <view
361
+ v-else-if="item.component === 'Date'"
362
+ class="flex w-full items-center rounded-[8rpx] border border-solid border-[#e0e0e0] bg-[#f5f5f5] px-3 text-[28rpx] transition-all duration-300 active:border-[#007aff] active:bg-white"
363
+ @click="openPicker(item.field)">
364
+ <text :class="getPickerTextClass(item)">{{ getPickerDisplayText(item, 'form.datePlaceholder') }}</text>
365
+ </view>
326
366
  <TDateTimePicker
327
367
  v-if="item.component === 'Date'"
328
368
  v-model="pickerVisible[item.field]"
@@ -334,12 +374,12 @@
334
374
  />
335
375
 
336
376
  <!-- 时间选择 -->
337
- <view
338
- v-else-if="item.component === 'Time'"
339
- class="flex h-[80rpx] w-full items-center rounded-[8rpx] border border-solid border-[#e0e0e0] bg-[#f5f5f5] px-3 text-[28rpx] transition-all duration-300 active:border-[#007aff] active:bg-white"
340
- @click="openPicker(item.field)">
341
- <text :class="getPickerTextClass(item)">{{ model[item.field] || $t('form.timePlaceholder') }}</text>
342
- </view>
377
+ <view
378
+ v-else-if="item.component === 'Time'"
379
+ class="flex h-[80rpx] w-full items-center rounded-[8rpx] border border-solid border-[#e0e0e0] bg-[#f5f5f5] px-3 text-[28rpx] transition-all duration-300 active:border-[#007aff] active:bg-white"
380
+ @click="openPicker(item.field)">
381
+ <text :class="getPickerTextClass(item)">{{ getPickerDisplayText(item, 'form.timePlaceholder') }}</text>
382
+ </view>
343
383
  <TDateTimePicker
344
384
  v-if="item.component === 'Time'"
345
385
  v-model="pickerVisible[item.field]"
@@ -364,21 +404,21 @@
364
404
  <TSwitch v-else-if="item.component === 'Switch'" :checked="model[item.field]"
365
405
  v-bind="getComponentProps(item)" @change="onSwitchChange($event, item)" />
366
406
 
367
- <!-- 评分 -->
368
- <TRate v-else-if="item.component === 'Rate'" :value="model[item.field] || 0"
369
- v-bind="getComponentProps(item)" @change="onRateChange($event, item)" />
370
-
371
- <!-- 滑块 -->
372
- <TSlider v-else-if="item.component === 'Slider'" :value="model[item.field] || 0"
373
- v-bind="getComponentProps(item)" @change="onSliderChange($event, item)" />
407
+ <!-- 评分 -->
408
+ <TRate v-else-if="item.component === 'Rate'" :value="getNumericFieldValue(item.field)"
409
+ v-bind="getComponentProps(item)" @change="onRateChange($event, item)" />
410
+
411
+ <!-- 滑块 -->
412
+ <TSlider v-else-if="item.component === 'Slider'" :value="getNumericFieldValue(item.field)"
413
+ v-bind="getComponentProps(item)" @change="onSliderChange($event, item)" />
374
414
  </slot>
375
415
  </view>
376
416
  </view>
377
417
 
378
- <!-- 错误提示 -->
379
- <view v-if="errors[item.field]" class="mt-2">
380
- <text class="text-sm text-red-500">{{ errors[item.field] }}</text>
381
- </view>
418
+ <!-- 错误提示 -->
419
+ <view v-if="hasFieldError(item.field)" class="mt-2">
420
+ <text class="text-sm text-red-500">{{ errors[item.field] }}</text>
421
+ </view>
382
422
  </view>
383
423
 
384
424
  <!-- 按钮区域 -->
@@ -9,7 +9,7 @@ export type FormOption = {
9
9
  /**
10
10
  * 组件通用属性
11
11
  */
12
- export type ComponentProps = {
12
+ export type ComponentProps = {
13
13
  /** 占位符 */
14
14
  placeholder?: string
15
15
  /** 是否禁用 */
@@ -42,11 +42,11 @@ export type ComponentProps = {
42
42
  color?: string
43
43
  /** 是否允许半星(用于 rate) */
44
44
  allowHalf?: boolean
45
- /** 是否显示值(用于 slider) */
46
- showValue?: boolean
47
- /** 其他扩展属性 */
48
- [key: string]: any
49
- }
45
+ /** 是否显示值(用于 slider) */
46
+ showValue?: boolean
47
+ /** 方向(用于单选/多选组扩展透传) */
48
+ direction?: string
49
+ }
50
50
 
51
51
  /**
52
52
  * 表单字段配置