tang-ui-x 1.0.6 → 1.1.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.
@@ -0,0 +1,644 @@
1
+ # TForm 表单
2
+
3
+ 动态表单组件,支持多种表单控件类型,通过配置化的方式快速构建表单。
4
+
5
+ ## 特性
6
+
7
+ - 🎯 **配置化**:通过 JSON 配置快速生成表单
8
+ - 🎨 **多组件支持**:支持 11 种常用表单组件
9
+ - 🔧 **灵活扩展**:支持自定义插槽和组件属性
10
+ - ✅ **表单验证**:内置必填验证,支持自定义验证
11
+ - 📱 **响应式**:适配各种屏幕尺寸
12
+ - 🎭 **TypeScript**:完整的类型定义
13
+
14
+ ## 快速开始
15
+
16
+ 查看完整示例:[example.uvue](./example.uvue)
17
+
18
+ ## 基础用法
19
+
20
+ ```vue
21
+ <template>
22
+ <TForm v-model="formData" :schemas="schemas" @submit="handleSubmit" />
23
+ </template>
24
+
25
+ <script setup>
26
+ import { ref } from 'vue'
27
+
28
+ const formData = ref({})
29
+
30
+ const schemas = [
31
+ {
32
+ field: 'name',
33
+ label: '姓名',
34
+ component: 'Input',
35
+ required: true,
36
+ componentProps: {
37
+ placeholder: '请输入姓名',
38
+ maxlength: 20,
39
+ type: 'text'
40
+ }
41
+ },
42
+ {
43
+ field: 'age',
44
+ label: '年龄',
45
+ component: 'InputNumber',
46
+ required: true,
47
+ componentProps: {
48
+ placeholder: '请输入年龄',
49
+ min: 1,
50
+ max: 120
51
+ }
52
+ },
53
+ {
54
+ field: 'gender',
55
+ label: '性别',
56
+ component: 'Radio',
57
+ required: true,
58
+ componentProps: {
59
+ options: [
60
+ { label: '男', value: 'male' },
61
+ { label: '女', value: 'female' }
62
+ ]
63
+ }
64
+ }
65
+ ]
66
+
67
+ const handleSubmit = (values) => {
68
+ console.log('表单数据:', values)
69
+ }
70
+ </script>
71
+ ```
72
+
73
+ ## 支持的组件类型
74
+
75
+ ### 输入类
76
+
77
+ - `Input` - 文本输入框
78
+ - `InputNumber` - 数字输入框
79
+ - `Textarea` - 多行文本
80
+
81
+ ### 选择类
82
+
83
+ - `Select` - 下拉选择
84
+ - `Date` - 日期选择
85
+ - `Time` - 时间选择
86
+ - `Radio` - 单选框
87
+ - `Checkbox` - 多选框
88
+
89
+ ### 交互类
90
+
91
+ - `Switch` - 开关
92
+ - `Rate` - 评分
93
+ - `Slider` - 滑块
94
+
95
+ ## componentProps 属性
96
+
97
+ `componentProps` 包含所有组件相关的属性,会通过 `v-bind` 绑定到对应组件上。
98
+
99
+ ### 通用属性
100
+
101
+ | 属性 | 说明 | 类型 | 默认值 |
102
+ |------|------|------|--------|
103
+ | placeholder | 占位符 | String | - |
104
+ | disabled | 是否禁用 | Boolean | false |
105
+ | options | 选项列表 | FormOption[] | - |
106
+
107
+ ### Input / Number 属性
108
+
109
+ | 属性 | 说明 | 类型 | 默认值 |
110
+ |------|------|------|--------|
111
+ | type | 输入类型 | String | 'text' |
112
+ | maxlength | 最大长度 | Number | - |
113
+ | min | 最小值 | Number | - |
114
+ | max | 最大值 | Number | - |
115
+ | focus | 是否自动聚焦 | Boolean | false |
116
+ | confirmType | 确认按钮文字 | String | 'done' |
117
+
118
+ ### Textarea 属性
119
+
120
+ | 属性 | 说明 | 类型 | 默认值 |
121
+ |------|------|------|--------|
122
+ | maxlength | 最大长度 | Number | - |
123
+ | autoHeight | 是否自动高度 | Boolean | false |
124
+ | showConfirmBar | 是否显示确认栏 | Boolean | true |
125
+
126
+ ### Date / Time 属性
127
+
128
+ | 属性 | 说明 | 类型 | 默认值 |
129
+ |------|------|------|--------|
130
+ | start | 开始日期 | String | - |
131
+ | end | 结束日期 | String | - |
132
+
133
+ ### Switch 属性
134
+
135
+ | 属性 | 说明 | 类型 | 默认值 |
136
+ |------|------|------|--------|
137
+ | color | 激活颜色 | String | '#007aff' |
138
+
139
+ ### Rate 属性
140
+
141
+ | 属性 | 说明 | 类型 | 默认值 |
142
+ |------|------|------|--------|
143
+ | max | 最大值 | Number | 5 |
144
+ | allowHalf | 是否允许半星 | Boolean | false |
145
+
146
+ ### Slider 属性
147
+
148
+ | 属性 | 说明 | 类型 | 默认值 |
149
+ |------|------|------|--------|
150
+ | min | 最小值 | Number | 0 |
151
+ | max | 最大值 | Number | 100 |
152
+ | step | 步长 | Number | 1 |
153
+ | showValue | 是否显示值 | Boolean | false |
154
+
155
+ ### 常用属性示例
156
+
157
+ ```javascript
158
+ // Input 组件
159
+ {
160
+ field: 'username',
161
+ label: '用户名',
162
+ component: 'Input',
163
+ componentProps: {
164
+ placeholder: '请输入用户名',
165
+ type: 'text',
166
+ maxlength: 20,
167
+ disabled: false,
168
+ focus: true,
169
+ confirmType: 'done'
170
+ }
171
+ }
172
+
173
+ // InputNumber 组件
174
+ {
175
+ field: 'age',
176
+ label: '年龄',
177
+ component: 'InputNumber',
178
+ componentProps: {
179
+ min: 0,
180
+ max: 150,
181
+ disabled: false
182
+ }
183
+ }
184
+
185
+ // Textarea 组件
186
+ {
187
+ field: 'description',
188
+ label: '描述',
189
+ component: 'Textarea',
190
+ componentProps: {
191
+ placeholder: '请输入描述',
192
+ maxlength: 500,
193
+ autoHeight: true,
194
+ showConfirmBar: true,
195
+ disabled: false
196
+ }
197
+ }
198
+
199
+ // Select 组件
200
+ {
201
+ field: 'city',
202
+ label: '城市',
203
+ component: 'Select',
204
+ componentProps: {
205
+ options: [
206
+ { label: '北京', value: 'beijing' },
207
+ { label: '上海', value: 'shanghai' },
208
+ { label: '广州', value: 'guangzhou' }
209
+ ],
210
+ disabled: false
211
+ }
212
+ }
213
+
214
+ // Radio 组件
215
+ {
216
+ field: 'gender',
217
+ label: '性别',
218
+ component: 'Radio',
219
+ componentProps: {
220
+ options: [
221
+ { label: '男', value: 'male' },
222
+ { label: '女', value: 'female' }
223
+ ],
224
+ disabled: false
225
+ }
226
+ }
227
+
228
+ // Checkbox 组件
229
+ {
230
+ field: 'hobbies',
231
+ label: '爱好',
232
+ component: 'Checkbox',
233
+ componentProps: {
234
+ options: [
235
+ { label: '阅读', value: 'reading' },
236
+ { label: '运动', value: 'sports' },
237
+ { label: '音乐', value: 'music' }
238
+ ],
239
+ disabled: false
240
+ }
241
+ }
242
+
243
+ // picker 组件
244
+ {
245
+ field: 'date',
246
+ label: '日期',
247
+ component: 'date',
248
+ componentProps: {
249
+ start: '2020-01-01',
250
+ end: '2030-12-31',
251
+ disabled: false
252
+ }
253
+ }
254
+
255
+ // switch 组件
256
+ {
257
+ field: 'notify',
258
+ label: '通知',
259
+ component: 'switch',
260
+ componentProps: {
261
+ disabled: false,
262
+ color: '#007aff'
263
+ }
264
+ }
265
+
266
+ // rate 组件
267
+ {
268
+ field: 'rating',
269
+ label: '评分',
270
+ component: 'rate',
271
+ componentProps: {
272
+ max: 5,
273
+ disabled: false,
274
+ allowHalf: true
275
+ }
276
+ }
277
+
278
+ // slider 组件
279
+ {
280
+ field: 'volume',
281
+ label: '音量',
282
+ component: 'slider',
283
+ componentProps: {
284
+ min: 0,
285
+ max: 100,
286
+ step: 1,
287
+ disabled: false,
288
+ showValue: true
289
+ }
290
+ }
291
+ ```
292
+
293
+ ## Props
294
+
295
+ | 参数 | 说明 | 类型 | 默认值 |
296
+ |------|------|------|--------|
297
+ | v-model | 表单数据 | Object | {} |
298
+ | schemas | 表单配置 | FormSchema[] | [] |
299
+ | labelWidth | 标签宽度 | String | '160rpx' |
300
+ | hideButtons | 隐藏默认按钮 | Boolean | false |
301
+
302
+ ## FormSchema 配置
303
+
304
+ | 参数 | 说明 | 类型 | 必填 |
305
+ |------|------|------|------|
306
+ | field | 字段名 | String | 是 |
307
+ | label | 标签文本 | String | 是 |
308
+ | component | 组件类型 | String | 是 |
309
+ | required | 是否必填 | Boolean | 否 |
310
+ | componentProps | 组件属性(包含 placeholder、options 等) | Object | 否 |
311
+
312
+ ## Slots
313
+
314
+ 支持为每个字段自定义插槽,插槽名为字段的 `field` 值。
315
+
316
+ 插槽参数:
317
+ - `item` - 当前字段配置
318
+ - `value` - 当前字段值
319
+ - `error` - 当前字段错误信息
320
+
321
+ ```vue
322
+ <TForm v-model="formData" :schemas="schemas">
323
+ <!-- 自定义 username 字段 -->
324
+ <template #username="{ item, value, error }">
325
+ <view class="custom-input">
326
+ <input v-model="formData.username" placeholder="自定义用户名输入" />
327
+ <text v-if="error" class="error">{{ error }}</text>
328
+ </view>
329
+ </template>
330
+
331
+ <!-- 自定义 avatar 字段 -->
332
+ <template #avatar="{ item, value }">
333
+ <view class="avatar-upload">
334
+ <image :src="value || '/static/default-avatar.png'" />
335
+ <button @click="uploadAvatar">上传头像</button>
336
+ </view>
337
+ </template>
338
+ </TForm>
339
+ ```
340
+
341
+ ## Events
342
+
343
+ | 事件名 | 说明 | 回调参数 |
344
+ |--------|------|----------|
345
+ | submit | 提交表单 | (values: Object) |
346
+ | reset | 重置表单 | - |
347
+
348
+ ## Methods
349
+
350
+ | 方法名 | 说明 | 参数 | 返回值 |
351
+ |--------|------|------|--------|
352
+ | submit | 提交表单 | - | Promise<{valid, values}> |
353
+ | reset | 重置表单 | - | - |
354
+ | validate | 验证表单 | - | Boolean |
355
+ | getFormData | 获取表单数据 | - | Object |
356
+ | getErrors | 获取错误信息 | - | Object |
357
+
358
+ ## 完整示例
359
+
360
+ ```vue
361
+ <template>
362
+ <view>
363
+ <TForm
364
+ ref="formRef"
365
+ v-model="formData"
366
+ :schemas="schemas"
367
+ label-width="180rpx"
368
+ @submit="handleSubmit"
369
+ @reset="handleReset" />
370
+
371
+ <view class="custom-buttons">
372
+ <button @click="handleCustomSubmit">自定义提交</button>
373
+ <button @click="handleValidate">验证</button>
374
+ </view>
375
+ </view>
376
+ </template>
377
+
378
+ <script setup>
379
+ import { ref } from 'vue'
380
+
381
+ const formRef = ref(null)
382
+ const formData = ref({})
383
+
384
+ const schemas = [
385
+ {
386
+ field: 'username',
387
+ label: '用户名',
388
+ component: 'input',
389
+ required: true,
390
+ placeholder: '请输入用户名'
391
+ },
392
+ {
393
+ field: 'password',
394
+ label: '密码',
395
+ component: 'input',
396
+ type: 'password',
397
+ required: true
398
+ },
399
+ {
400
+ field: 'age',
401
+ label: '年龄',
402
+ component: 'number',
403
+ min: 1,
404
+ max: 120
405
+ },
406
+ {
407
+ field: 'bio',
408
+ label: '简介',
409
+ component: 'textarea',
410
+ placeholder: '请输入个人简介'
411
+ },
412
+ {
413
+ field: 'city',
414
+ label: '城市',
415
+ component: 'select',
416
+ required: true,
417
+ options: [
418
+ { label: '北京', value: 'beijing' },
419
+ { label: '上海', value: 'shanghai' },
420
+ { label: '广州', value: 'guangzhou' }
421
+ ]
422
+ },
423
+ {
424
+ field: 'birthday',
425
+ label: '生日',
426
+ component: 'date'
427
+ },
428
+ {
429
+ field: 'gender',
430
+ label: '性别',
431
+ component: 'radio',
432
+ options: [
433
+ { label: '男', value: 'male' },
434
+ { label: '女', value: 'female' }
435
+ ]
436
+ },
437
+ {
438
+ field: 'hobbies',
439
+ label: '爱好',
440
+ component: 'checkbox',
441
+ options: [
442
+ { label: '阅读', value: 'reading' },
443
+ { label: '运动', value: 'sports' },
444
+ { label: '音乐', value: 'music' }
445
+ ]
446
+ },
447
+ {
448
+ field: 'notify',
449
+ label: '接收通知',
450
+ component: 'switch'
451
+ },
452
+ {
453
+ field: 'rating',
454
+ label: '评分',
455
+ component: 'rate',
456
+ max: 5
457
+ },
458
+ {
459
+ field: 'volume',
460
+ label: '音量',
461
+ component: 'slider',
462
+ min: 0,
463
+ max: 100,
464
+ step: 1
465
+ }
466
+ ]
467
+
468
+ const handleSubmit = (values) => {
469
+ console.log('提交:', values)
470
+ uni.showToast({
471
+ title: '提交成功',
472
+ icon: 'success'
473
+ })
474
+ }
475
+
476
+ const handleReset = () => {
477
+ console.log('重置')
478
+ }
479
+
480
+ const handleCustomSubmit = async () => {
481
+ const result = await formRef.value.submit()
482
+ if (result.valid) {
483
+ console.log('验证通过:', result.values)
484
+ } else {
485
+ console.log('验证失败')
486
+ }
487
+ }
488
+
489
+ const handleValidate = () => {
490
+ const isValid = formRef.value.validate()
491
+ console.log('验证结果:', isValid)
492
+ if (!isValid) {
493
+ const errors = formRef.value.getErrors()
494
+ console.log('错误信息:', errors)
495
+ }
496
+ }
497
+ </script>
498
+ ```
499
+
500
+ ## 自定义插槽示例
501
+
502
+ ```vue
503
+ <template>
504
+ <TForm v-model="formData" :schemas="schemas" @submit="handleSubmit">
505
+ <!-- 自定义用户名输入 -->
506
+ <template #username="{ item, value, error }">
507
+ <view class="custom-field">
508
+ <input
509
+ v-model="formData.username"
510
+ placeholder="请输入用户名"
511
+ class="custom-input" />
512
+ <text class="hint">用户名长度 4-16 位</text>
513
+ <text v-if="error" class="error">{{ error }}</text>
514
+ </view>
515
+ </template>
516
+
517
+ <!-- 自定义头像上传 -->
518
+ <template #avatar="{ item, value }">
519
+ <view class="avatar-upload">
520
+ <image
521
+ :src="value || '/static/default-avatar.png'"
522
+ class="avatar-preview" />
523
+ <button @click="handleUploadAvatar" class="upload-btn">
524
+ 上传头像
525
+ </button>
526
+ </view>
527
+ </template>
528
+
529
+ <!-- 自定义地址选择 -->
530
+ <template #address="{ item, value }">
531
+ <view class="address-picker">
532
+ <text>{{ value || '请选择地址' }}</text>
533
+ <button @click="handleSelectAddress">选择地址</button>
534
+ </view>
535
+ </template>
536
+ </TForm>
537
+ </template>
538
+
539
+ <script setup>
540
+ import { ref } from 'vue'
541
+
542
+ const formData = ref({
543
+ username: '',
544
+ avatar: '',
545
+ address: ''
546
+ })
547
+
548
+ const schemas = [
549
+ {
550
+ field: 'username',
551
+ label: '用户名',
552
+ component: 'input',
553
+ required: true
554
+ },
555
+ {
556
+ field: 'avatar',
557
+ label: '头像',
558
+ component: 'input', // 使用插槽时 component 类型不重要
559
+ required: false
560
+ },
561
+ {
562
+ field: 'address',
563
+ label: '地址',
564
+ component: 'input',
565
+ required: true
566
+ }
567
+ ]
568
+
569
+ const handleSubmit = (values) => {
570
+ console.log('提交:', values)
571
+ }
572
+
573
+ const handleUploadAvatar = () => {
574
+ uni.chooseImage({
575
+ count: 1,
576
+ success: (res) => {
577
+ formData.value.avatar = res.tempFilePaths[0]
578
+ }
579
+ })
580
+ }
581
+
582
+ const handleSelectAddress = () => {
583
+ // 打开地址选择器
584
+ uni.chooseLocation({
585
+ success: (res) => {
586
+ formData.value.address = res.address
587
+ }
588
+ })
589
+ }
590
+ </script>
591
+
592
+ <style>
593
+ .custom-field {
594
+ display: flex;
595
+ flex-direction: column;
596
+ }
597
+
598
+ .custom-input {
599
+ padding: 20rpx;
600
+ border: 1rpx solid #e0e0e0;
601
+ border-radius: 8rpx;
602
+ }
603
+
604
+ .hint {
605
+ margin-top: 8rpx;
606
+ font-size: 24rpx;
607
+ color: #999;
608
+ }
609
+
610
+ .error {
611
+ margin-top: 8rpx;
612
+ font-size: 24rpx;
613
+ color: #ff4d4f;
614
+ }
615
+
616
+ .avatar-upload {
617
+ display: flex;
618
+ align-items: center;
619
+ gap: 24rpx;
620
+ }
621
+
622
+ .avatar-preview {
623
+ width: 120rpx;
624
+ height: 120rpx;
625
+ border-radius: 50%;
626
+ }
627
+
628
+ .upload-btn {
629
+ padding: 16rpx 32rpx;
630
+ background-color: #007aff;
631
+ color: #fff;
632
+ border-radius: 8rpx;
633
+ }
634
+
635
+ .address-picker {
636
+ display: flex;
637
+ justify-content: space-between;
638
+ align-items: center;
639
+ padding: 20rpx;
640
+ background-color: #f5f5f5;
641
+ border-radius: 8rpx;
642
+ }
643
+ </style>
644
+ ```