tang-ui-x 1.0.6 → 1.1.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.
@@ -0,0 +1,392 @@
1
+ <script setup lang="uts">
2
+ import TRadioButton from '../TRadioButton/index.uvue'
3
+ import TCheckbox from '../TCheckbox/index.uvue'
4
+ import TSwitch from '../TSwitch/index.uvue'
5
+ import TRate from '../TRate/index.uvue'
6
+ import TSlider from '../TSlider/index.uvue'
7
+ import type { FormOption, FormSchema, TFormProps, ComponentProps } from './type.uts'
8
+
9
+ const props = withDefaults(defineProps<TFormProps>(), {
10
+ labelWidth: '160rpx',
11
+ hideButtons: false,
12
+ })
13
+
14
+ const emit = defineEmits(['submit', 'reset'])
15
+
16
+ const model = defineModel<Record<string, any>>({ default: () => ({}) })
17
+ const errors = reactive<Record<string, string>>({})
18
+
19
+ const handlePlaceholder = (item: FormSchema): string => {
20
+ return item.componentProps?.placeholder || `请输入${item.label}`
21
+ }
22
+
23
+ const handleRange = (item: FormSchema) => {
24
+ const options = item.componentProps?.options as FormOption[] | undefined
25
+ return options?.map(o => o.label) || []
26
+ }
27
+
28
+ const getSelectLabel = (item: FormSchema) => {
29
+ const options = item.componentProps?.options as FormOption[] | undefined
30
+ const opt = options?.find(o => o.value === model.value[item.field])
31
+ return opt?.label || ''
32
+ }
33
+
34
+ const onSelectChange = (e: any, item: FormSchema) => {
35
+ const index = e.detail.value as number
36
+ const options = item.componentProps?.options as FormOption[] | undefined
37
+ const value = options?.[index]?.value ?? ''
38
+ model.value[item.field] = value
39
+ validateField(item)
40
+ }
41
+
42
+ const onTimeChange = (e: any, item: FormSchema) => {
43
+ model.value[item.field] = e.detail.value
44
+ validateField(item)
45
+ }
46
+
47
+ const validateField = (item: FormSchema) => {
48
+ if (item.required && !model.value[item.field]) {
49
+ errors[item.field] = `请输入${item.label}`
50
+ } else {
51
+ delete errors[item.field]
52
+ }
53
+ }
54
+
55
+ const onRadioChange = (value: string | number, item: FormSchema) => {
56
+ model.value[item.field] = value
57
+ validateField(item)
58
+ }
59
+
60
+ const onCheckboxChange = (values: (string | number)[], item: FormSchema) => {
61
+ model.value[item.field] = values
62
+ validateField(item)
63
+ }
64
+
65
+ const onSwitchChange = (value: boolean, item: FormSchema) => {
66
+ model.value[item.field] = value
67
+ validateField(item)
68
+ }
69
+
70
+ const onRateChange = (value: number, item: FormSchema) => {
71
+ model.value[item.field] = value
72
+ validateField(item)
73
+ }
74
+
75
+ const onSliderChange = (value: number, item: FormSchema) => {
76
+ model.value[item.field] = value
77
+ validateField(item)
78
+ }
79
+
80
+ const onFormSubmit = async (e: UniFormSubmitEvent): Promise<{ valid: boolean, values: any }> => {
81
+ Object.keys(errors).forEach(k => delete errors[k])
82
+
83
+ let hasError = false
84
+ for (const item of props.schemas) {
85
+ validateField(item)
86
+ if (item.required && !model.value[item.field]) {
87
+ hasError = true
88
+ }
89
+ }
90
+
91
+ if (hasError) {
92
+ console.warn('表单验证失败')
93
+ return { valid: false, values: null }
94
+ }
95
+
96
+ try {
97
+ await emit('submit', model.value)
98
+ return { valid: true, values: model.value }
99
+ } catch (err) {
100
+ console.error('表单提交失败:', err)
101
+ return { valid: false, values: null }
102
+ }
103
+ }
104
+
105
+ const onFormReset = () => {
106
+ Object.keys(model.value).forEach(k => (model.value[k] = ''))
107
+ Object.keys(errors).forEach(k => delete errors[k])
108
+ emit('reset')
109
+ }
110
+
111
+ defineExpose({
112
+ submit: async () => {
113
+ return await onFormSubmit({} as UniFormSubmitEvent)
114
+ },
115
+ reset: () => {
116
+ onFormReset()
117
+ },
118
+ validate: () => {
119
+ Object.keys(errors).forEach(k => delete errors[k])
120
+ let hasError = false
121
+ for (const item of props.schemas) {
122
+ validateField(item)
123
+ if (item.required && !model.value[item.field]) {
124
+ hasError = true
125
+ }
126
+ }
127
+ return !hasError
128
+ },
129
+ getFormData: () => ({ ...model.value }),
130
+ getErrors: () => ({ ...errors })
131
+ })
132
+ </script>
133
+
134
+ <template>
135
+ <view class="t-form">
136
+ <form @submit="onFormSubmit" @reset="onFormReset">
137
+ <view v-for="(item, index) in schemas" class="form-item" :key="index">
138
+ <view class="form-label" :style="{ width: labelWidth }">
139
+ <text>{{ item.label }}</text>
140
+ <text v-if="item.required" class="required">*</text>
141
+ </view>
142
+
143
+ <view class="form-control">
144
+ <!-- 自定义插槽 -->
145
+ <slot v-if="$slots[item.field]" :name="item.field" :item="item" :value="model[item.field]" :error="errors[item.field]"></slot>
146
+
147
+ <!-- 输入框 -->
148
+ <input v-else-if="item.component === 'Input'"
149
+ v-model="model[item.field]"
150
+ :placeholder="handlePlaceholder(item)"
151
+ :name="item.field"
152
+ class="input"
153
+ v-bind="item.componentProps || {}"
154
+ @input="validateField(item)" />
155
+
156
+ <!-- 数字输入 -->
157
+ <input v-else-if="item.component === 'InputNumber'"
158
+ v-model="model[item.field]"
159
+ :placeholder="handlePlaceholder(item)"
160
+ type="number"
161
+ :name="item.field"
162
+ class="input"
163
+ v-bind="item.componentProps || {}"
164
+ @input="validateField(item)" />
165
+
166
+ <!-- 文本域 -->
167
+ <textarea v-else-if="item.component === 'Textarea'"
168
+ v-model="model[item.field]"
169
+ :placeholder="handlePlaceholder(item)"
170
+ :name="item.field"
171
+ class="textarea"
172
+ v-bind="item.componentProps || {}"
173
+ @input="validateField(item)"></textarea>
174
+
175
+ <!-- 下拉选择 -->
176
+ <picker v-else-if="item.component === 'Select'"
177
+ mode="selector"
178
+ :name="item.field"
179
+ :range="handleRange(item)"
180
+ v-bind="item.componentProps || {}"
181
+ @change="onSelectChange($event, item)">
182
+ <view class="picker">
183
+ {{ getSelectLabel(item) || '请选择' }}
184
+ </view>
185
+ </picker>
186
+
187
+ <!-- 日期选择 -->
188
+ <picker v-else-if="item.component === 'Date'"
189
+ mode="date"
190
+ :name="item.field"
191
+ :value="model[item.field]"
192
+ v-bind="item.componentProps || {}"
193
+ @change="onTimeChange($event, item)">
194
+ <view class="picker">
195
+ {{ model[item.field] || '请选择日期' }}
196
+ </view>
197
+ </picker>
198
+
199
+ <!-- 时间选择 -->
200
+ <picker v-else-if="item.component === 'Time'"
201
+ mode="time"
202
+ :name="item.field"
203
+ :value="model[item.field]"
204
+ v-bind="item.componentProps || {}"
205
+ @change="onTimeChange($event, item)">
206
+ <view class="picker">
207
+ {{ model[item.field] || '请选择时间' }}
208
+ </view>
209
+ </picker>
210
+
211
+ <!-- 单选 -->
212
+ <view v-else-if="item.component === 'Radio'" class="radio-group">
213
+ <TRadioButton
214
+ v-for="opt in (item.componentProps?.options as FormOption[])"
215
+ :key="opt.value"
216
+ v-model="model[item.field]"
217
+ :label="opt.label"
218
+ :value="opt.value"
219
+ @change="onRadioChange(opt.value, item)" />
220
+ </view>
221
+
222
+ <!-- 多选 -->
223
+ <view v-else-if="item.component === 'Checkbox'" class="checkbox-group">
224
+ <view
225
+ v-for="opt in (item.componentProps?.options as FormOption[])"
226
+ :key="opt.value"
227
+ class="checkbox-item">
228
+ <TCheckbox
229
+ :checked="(model[item.field] || []).includes(opt.value)"
230
+ v-bind="item.componentProps || {}"
231
+ @change="(checked) => {
232
+ const values = model[item.field] || []
233
+ if (checked) {
234
+ values.push(opt.value)
235
+ } else {
236
+ const index = values.indexOf(opt.value)
237
+ if (index > -1) values.splice(index, 1)
238
+ }
239
+ onCheckboxChange(values, item)
240
+ }" />
241
+ <text class="checkbox-label">{{ opt.label }}</text>
242
+ </view>
243
+ </view>
244
+
245
+ <!-- 开关 -->
246
+ <TSwitch v-else-if="item.component === 'Switch'"
247
+ :checked="model[item.field]"
248
+ v-bind="item.componentProps || {}"
249
+ @change="onSwitchChange($event, item)" />
250
+
251
+ <!-- 评分 -->
252
+ <TRate v-else-if="item.component === 'Rate'"
253
+ :value="model[item.field] || 0"
254
+ v-bind="item.componentProps || {}"
255
+ @change="onRateChange($event, item)" />
256
+
257
+ <!-- 滑块 -->
258
+ <TSlider v-else-if="item.component === 'Slider'"
259
+ :value="model[item.field] || 0"
260
+ v-bind="item.componentProps || {}"
261
+ @change="onSliderChange($event, item)" />
262
+ </view>
263
+
264
+ <!-- 错误提示 -->
265
+ <view v-if="errors[item.field]" class="error-message">
266
+ <text>{{ errors[item.field] }}</text>
267
+ </view>
268
+ </view>
269
+
270
+ <!-- 按钮区域 -->
271
+ <view v-if="!hideButtons" class="form-footer">
272
+ <button class="btn btn-submit" form-type="submit">提交</button>
273
+ <button class="btn btn-reset" form-type="reset">重置</button>
274
+ </view>
275
+ </form>
276
+ </view>
277
+ </template>
278
+
279
+ <style lang="scss" scoped>
280
+ .t-form {
281
+ padding: 32rpx;
282
+ }
283
+
284
+ .form-item {
285
+ margin-bottom: 32rpx;
286
+ }
287
+
288
+ .form-label {
289
+ display: flex;
290
+ flex-direction: row;
291
+ align-items: center;
292
+ margin-bottom: 16rpx;
293
+ font-size: 28rpx;
294
+ color: #333;
295
+ font-weight: 500;
296
+ }
297
+
298
+ .required {
299
+ color: #ff4d4f;
300
+ margin-left: 4rpx;
301
+ }
302
+
303
+ .form-control {
304
+ width: 100%;
305
+ }
306
+
307
+ .input,
308
+ .textarea,
309
+ .picker {
310
+ width: 100%;
311
+ padding: 20rpx 24rpx;
312
+ font-size: 28rpx;
313
+ color: #333;
314
+ background-color: #f5f5f5;
315
+ border-radius: 8rpx;
316
+ border: 1rpx solid #e0e0e0;
317
+ transition: all 0.3s;
318
+ }
319
+
320
+ .input:focus,
321
+ .textarea:focus {
322
+ background-color: #fff;
323
+ border-color: #007aff;
324
+ }
325
+
326
+ .textarea {
327
+ min-height: 120rpx;
328
+ }
329
+
330
+ .picker {
331
+ display: flex;
332
+ align-items: center;
333
+ }
334
+
335
+ .radio-group,
336
+ .checkbox-group {
337
+ display: flex;
338
+ flex-direction: column;
339
+ gap: 16rpx;
340
+ }
341
+
342
+ .checkbox-item {
343
+ display: flex;
344
+ flex-direction: row;
345
+ align-items: center;
346
+ gap: 16rpx;
347
+ }
348
+
349
+ .checkbox-label {
350
+ font-size: 28rpx;
351
+ color: #333;
352
+ }
353
+
354
+ .error-message {
355
+ margin-top: 8rpx;
356
+ font-size: 24rpx;
357
+ color: #ff4d4f;
358
+ }
359
+
360
+ .form-footer {
361
+ display: flex;
362
+ flex-direction: row;
363
+ gap: 24rpx;
364
+ margin-top: 48rpx;
365
+ }
366
+
367
+ .btn {
368
+ flex: 1;
369
+ height: 88rpx;
370
+ line-height: 88rpx;
371
+ text-align: center;
372
+ font-size: 32rpx;
373
+ border-radius: 8rpx;
374
+ transition: all 0.3s;
375
+ }
376
+
377
+ .btn-submit {
378
+ background-color: #007aff;
379
+ color: #fff;
380
+ }
381
+
382
+ .btn-reset {
383
+ background-color: #fff;
384
+ color: #007aff;
385
+ border: 1rpx solid #007aff;
386
+ }
387
+
388
+ .btn:active {
389
+ opacity: 0.7;
390
+ transform: scale(0.98);
391
+ }
392
+ </style>
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 表单选项类型
3
+ */
4
+ export type FormOption = {
5
+ label: string
6
+ value: string | number
7
+ }
8
+
9
+ /**
10
+ * 组件通用属性
11
+ */
12
+ export type ComponentProps = {
13
+ /** 占位符 */
14
+ placeholder?: string
15
+ /** 是否禁用 */
16
+ disabled?: boolean
17
+ /** 选项列表(用于 select、radio、checkbox) */
18
+ options?: FormOption[]
19
+ /** 输入类型(用于 input) */
20
+ type?: string
21
+ /** 最大长度(用于 input、textarea) */
22
+ maxlength?: number
23
+ /** 最小值(用于 number、slider) */
24
+ min?: number
25
+ /** 最大值(用于 number、slider、rate) */
26
+ max?: number
27
+ /** 步长(用于 slider) */
28
+ step?: number
29
+ /** 是否自动聚焦(用于 input、textarea) */
30
+ focus?: boolean
31
+ /** 确认按钮文字(用于 input) */
32
+ confirmType?: string
33
+ /** 是否自动高度(用于 textarea) */
34
+ autoHeight?: boolean
35
+ /** 是否显示确认栏(用于 textarea) */
36
+ showConfirmBar?: boolean
37
+ /** 开始日期(用于 date) */
38
+ start?: string
39
+ /** 结束日期(用于 date) */
40
+ end?: string
41
+ /** 激活颜色(用于 switch) */
42
+ color?: string
43
+ /** 是否允许半星(用于 rate) */
44
+ allowHalf?: boolean
45
+ /** 是否显示值(用于 slider) */
46
+ showValue?: boolean
47
+ /** 其他扩展属性 */
48
+ [key: string]: any
49
+ }
50
+
51
+ /**
52
+ * 表单字段配置
53
+ */
54
+ export type FormSchema = {
55
+ /** 字段名 */
56
+ field: string
57
+ /** 标签文本 */
58
+ label: string
59
+ /** 组件类型 */
60
+ component: 'Input' | 'Textarea' | 'Select' | 'Date' | 'Time' | 'Radio' | 'Checkbox' | 'Switch' | 'Rate' | 'Slider' | 'InputNumber'
61
+ /** 是否必填 */
62
+ required?: boolean
63
+ /** 组件属性 */
64
+ componentProps?: ComponentProps
65
+ }
66
+
67
+ /**
68
+ * 表单组件属性
69
+ */
70
+ export type TFormProps = {
71
+ /** 表单配置 */
72
+ schemas: FormSchema[]
73
+ /** 标签宽度 */
74
+ labelWidth?: string
75
+ /** 是否隐藏默认按钮 */
76
+ hideButtons?: boolean
77
+ }
78
+
79
+ // 导出所有类型
80
+ export { FormOption, ComponentProps, FormSchema, TFormProps }
@@ -0,0 +1,117 @@
1
+ # TRadioButton 单选按钮
2
+
3
+ 单选按钮组件,支持单个按钮和选项组两种使用方式。
4
+
5
+ ## 基础用法
6
+
7
+ ```vue
8
+ <template>
9
+ <TRadioButton v-model="value" value="option1">选项一</TRadioButton>
10
+ <TRadioButton v-model="value" value="option2">选项二</TRadioButton>
11
+ <TRadioButton v-model="value" value="option3">选项三</TRadioButton>
12
+ </template>
13
+
14
+ <script setup>
15
+ import { ref } from 'vue'
16
+ const value = ref('option1')
17
+ </script>
18
+ ```
19
+
20
+ ## 选项组模式
21
+
22
+ 通过 `options` 属性传入选项数组,自动渲染多个单选按钮。
23
+
24
+ ```vue
25
+ <template>
26
+ <TRadioButton
27
+ v-model="value"
28
+ :options="[
29
+ { label: '选项一', value: 'option1' },
30
+ { label: '选项二', value: 'option2' },
31
+ { label: '选项三', value: 'option3' }
32
+ ]"
33
+ />
34
+ </template>
35
+ ```
36
+
37
+ ## 禁用状态
38
+
39
+ ```vue
40
+ <TRadioButton v-model="value" value="option1" disabled>禁用选项</TRadioButton>
41
+ ```
42
+
43
+ ## 不同尺寸
44
+
45
+ ```vue
46
+ <TRadioButton v-model="value" value="small" size="small">小尺寸</TRadioButton>
47
+ <TRadioButton v-model="value" value="medium" size="medium">中等尺寸</TRadioButton>
48
+ <TRadioButton v-model="value" value="large" size="large">大尺寸</TRadioButton>
49
+ ```
50
+
51
+ ## 自定义颜色
52
+
53
+ ```vue
54
+ <TRadioButton
55
+ v-model="value"
56
+ value="option1"
57
+ activeColor="#f56c6c"
58
+ inactiveColor="#909399"
59
+ >
60
+ 自定义颜色
61
+ </TRadioButton>
62
+ ```
63
+
64
+ ## Props
65
+
66
+ | 参数 | 说明 | 类型 | 默认值 |
67
+ |------|------|------|--------|
68
+ | modelValue (v-model) | 绑定值 | `string \| number` | - |
69
+ | value | 单选按钮的值 | `string \| number` | - |
70
+ | label | 显示的标签文本 | `string` | - |
71
+ | options | 选项组模式的选项列表 | `FormOption[]` | `[]` |
72
+ | size | 按钮尺寸 | `'small' \| 'medium' \| 'large'` | `'medium'` |
73
+ | activeColor | 激活状态颜色 | `string` | `'#00bba7'` |
74
+ | inactiveColor | 非激活状态颜色 | `string` | `'#666666'` |
75
+ | disabled | 是否禁用 | `boolean` | `false` |
76
+ | checked | 是否选中(仅单个按钮模式) | `boolean` | `false` |
77
+ | name | 原生 name 属性 | `string` | - |
78
+
79
+ ## Events
80
+
81
+ | 事件名 | 说明 | 回调参数 |
82
+ |--------|------|----------|
83
+ | change | 选中值改变时触发 | `(value: string \| number)` |
84
+
85
+ ## FormOption 类型
86
+
87
+ ```typescript
88
+ type FormOption = {
89
+ label: string
90
+ value: string | number
91
+ }
92
+ ```
93
+
94
+ ## 在 TForm 中使用
95
+
96
+ TRadioButton 可以在 TForm 组件中使用:
97
+
98
+ ```vue
99
+ <TForm v-model="formData" :schemas="schemas" />
100
+
101
+ <script setup>
102
+ const schemas = [
103
+ {
104
+ field: 'gender',
105
+ label: '性别',
106
+ component: 'Radio',
107
+ required: true,
108
+ componentProps: {
109
+ options: [
110
+ { label: '男', value: 'male' },
111
+ { label: '女', value: 'female' }
112
+ ]
113
+ }
114
+ }
115
+ ]
116
+ </script>
117
+ ```