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.
- package/components/TActionSheet/index.uvue +22 -6
- package/components/TAvatar/index.uvue +42 -28
- package/components/TCheckbox/index.uvue +7 -3
- package/components/TDateTimePicker/index.uvue +13 -6
- package/components/TDialog/index.uvue +16 -4
- package/components/TForm/index.uvue +131 -91
- package/components/TForm/type.uts +6 -6
- package/components/TGridItem/index.uvue +66 -42
- package/components/TImage/index.uvue +19 -15
- package/components/TInput/index.uvue +86 -58
- package/components/TList/index.uvue +5 -1
- package/components/TListItem/index.uvue +36 -16
- package/components/TLoading/index.uvue +9 -5
- package/components/TNavBar/index.uvue +15 -5
- package/components/TNoticeBar/index.uvue +15 -7
- package/components/TNumberInput/index.uvue +16 -7
- package/components/TPicker/index.uvue +13 -8
- package/components/TRadioButton/index.uvue +21 -4
- package/components/TRate/index.uvue +9 -2
- package/components/TSelect/index.uvue +81 -65
- package/components/TTextarea/index.uvue +25 -13
- package/components/Tabs/index.uvue +16 -4
- package/components/Tags/index.uvue +28 -16
- package/composables/i18n/types.uts +6 -10
- package/composables/useI18n.uts +1 -1
- package/composables/useModal.uts +5 -5
- package/composables/useTheme.uts +105 -124
- package/composables/useToast.uts +31 -31
- package/package.json +38 -38
|
@@ -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="
|
|
79
|
-
<text v-if="
|
|
80
|
-
<text v-if="
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
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="
|
|
121
|
-
class="t-avatar__image h-full w-full"
|
|
122
|
-
:src="src"
|
|
123
|
-
:mode="fit"
|
|
124
|
-
@error="handleError"
|
|
125
|
-
/>
|
|
126
|
-
<text v-else-if="
|
|
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: ${
|
|
86
|
+
styles.push(`border-color: ${isChecked.value ? props.activeColor : props.inactiveColor}`)
|
|
83
87
|
|
|
84
|
-
if (
|
|
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="
|
|
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):
|
|
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
|
|
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="
|
|
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="
|
|
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 =
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
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) :
|
|
79
|
-
const componentProps = getComponentProps(item)
|
|
80
|
-
const mergedProps:
|
|
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
|
|
95
|
-
const pickerValues = reactive
|
|
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 = () :
|
|
157
|
-
const activeErrors :
|
|
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
|
|
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<
|
|
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
|
-
|
|
266
|
-
|
|
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
|
|
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
|
|
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)">{{
|
|
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)">{{
|
|
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="
|
|
369
|
-
v-bind="getComponentProps(item)" @change="onRateChange($event, item)" />
|
|
370
|
-
|
|
371
|
-
<!-- 滑块 -->
|
|
372
|
-
<TSlider v-else-if="item.component === 'Slider'" :value="
|
|
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="
|
|
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
|
-
|
|
49
|
-
}
|
|
45
|
+
/** 是否显示值(用于 slider) */
|
|
46
|
+
showValue?: boolean
|
|
47
|
+
/** 方向(用于单选/多选组扩展透传) */
|
|
48
|
+
direction?: string
|
|
49
|
+
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* 表单字段配置
|