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,379 @@
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
+ :label="opt.label"
217
+ :value="opt.value"
218
+ :checked="model[item.field] === opt.value"
219
+ v-bind="item.componentProps || {}"
220
+ @change="onRadioChange(opt.value, item)" />
221
+ </view>
222
+
223
+ <!-- 多选 -->
224
+ <view v-else-if="item.component === 'Checkbox'" class="checkbox-group">
225
+ <TCheckbox
226
+ v-for="opt in (item.componentProps?.options as FormOption[])"
227
+ :key="opt.value"
228
+ :label="opt.label"
229
+ :value="opt.value"
230
+ :checked="(model[item.field] || []).includes(opt.value)"
231
+ v-bind="item.componentProps || {}"
232
+ @change="(checked) => {
233
+ const values = model[item.field] || []
234
+ if (checked) {
235
+ values.push(opt.value)
236
+ } else {
237
+ const index = values.indexOf(opt.value)
238
+ if (index > -1) values.splice(index, 1)
239
+ }
240
+ onCheckboxChange(values, item)
241
+ }" />
242
+ </view>
243
+
244
+ <!-- 开关 -->
245
+ <TSwitch v-else-if="item.component === 'Switch'"
246
+ :checked="model[item.field]"
247
+ v-bind="item.componentProps || {}"
248
+ @change="onSwitchChange($event, item)" />
249
+
250
+ <!-- 评分 -->
251
+ <TRate v-else-if="item.component === 'Rate'"
252
+ :value="model[item.field] || 0"
253
+ v-bind="item.componentProps || {}"
254
+ @change="onRateChange($event, item)" />
255
+
256
+ <!-- 滑块 -->
257
+ <TSlider v-else-if="item.component === 'Slider'"
258
+ :value="model[item.field] || 0"
259
+ v-bind="item.componentProps || {}"
260
+ @change="onSliderChange($event, item)" />
261
+ </view>
262
+
263
+ <!-- 错误提示 -->
264
+ <view v-if="errors[item.field]" class="error-message">
265
+ <text>{{ errors[item.field] }}</text>
266
+ </view>
267
+ </view>
268
+
269
+ <!-- 按钮区域 -->
270
+ <view v-if="!hideButtons" class="form-footer">
271
+ <button class="btn btn-submit" form-type="submit">提交</button>
272
+ <button class="btn btn-reset" form-type="reset">重置</button>
273
+ </view>
274
+ </form>
275
+ </view>
276
+ </template>
277
+
278
+ <style lang="scss" scoped>
279
+ .t-form {
280
+ padding: 32rpx;
281
+ }
282
+
283
+ .form-item {
284
+ margin-bottom: 32rpx;
285
+ }
286
+
287
+ .form-label {
288
+ display: flex;
289
+ flex-direction: row;
290
+ align-items: center;
291
+ margin-bottom: 16rpx;
292
+ font-size: 28rpx;
293
+ color: #333;
294
+ font-weight: 500;
295
+ }
296
+
297
+ .required {
298
+ color: #ff4d4f;
299
+ margin-left: 4rpx;
300
+ }
301
+
302
+ .form-control {
303
+ width: 100%;
304
+ }
305
+
306
+ .input,
307
+ .textarea,
308
+ .picker {
309
+ width: 100%;
310
+ padding: 20rpx 24rpx;
311
+ font-size: 28rpx;
312
+ color: #333;
313
+ background-color: #f5f5f5;
314
+ border-radius: 8rpx;
315
+ border: 1rpx solid #e0e0e0;
316
+ transition: all 0.3s;
317
+ }
318
+
319
+ .input:focus,
320
+ .textarea:focus {
321
+ background-color: #fff;
322
+ border-color: #007aff;
323
+ }
324
+
325
+ .textarea {
326
+ min-height: 120rpx;
327
+ }
328
+
329
+ .picker {
330
+ display: flex;
331
+ align-items: center;
332
+ }
333
+
334
+ .radio-group,
335
+ .checkbox-group {
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 16rpx;
339
+ }
340
+
341
+ .error-message {
342
+ margin-top: 8rpx;
343
+ font-size: 24rpx;
344
+ color: #ff4d4f;
345
+ }
346
+
347
+ .form-footer {
348
+ display: flex;
349
+ flex-direction: row;
350
+ gap: 24rpx;
351
+ margin-top: 48rpx;
352
+ }
353
+
354
+ .btn {
355
+ flex: 1;
356
+ height: 88rpx;
357
+ line-height: 88rpx;
358
+ text-align: center;
359
+ font-size: 32rpx;
360
+ border-radius: 8rpx;
361
+ transition: all 0.3s;
362
+ }
363
+
364
+ .btn-submit {
365
+ background-color: #007aff;
366
+ color: #fff;
367
+ }
368
+
369
+ .btn-reset {
370
+ background-color: #fff;
371
+ color: #007aff;
372
+ border: 1rpx solid #007aff;
373
+ }
374
+
375
+ .btn:active {
376
+ opacity: 0.7;
377
+ transform: scale(0.98);
378
+ }
379
+ </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 }
package/index.uts CHANGED
@@ -1,107 +1,108 @@
1
- /**
2
- * Tang UI - UniApp X UI 组件库
3
- * @description 基于 uni-app x 的移动端 UI 组件库
4
- * @version 1.0.2
5
- * @author sugar258596
6
- */
7
-
8
- /**
9
- * 注意:uni-app x 项目推荐使用 easycom 自动导入组件
10
- *
11
- * 在 pages.json 中配置:
12
- * {
13
- * "easycom": {
14
- * "autoscan": true,
15
- * "custom": {
16
- * "^T(.*)": "tang-ui-x/components/T$1/index.uvue"
17
- * }
18
- * }
19
- * }
20
- *
21
- * 然后在页面中直接使用:
22
- * <TButton type="primary">按钮</TButton>
23
- */
24
-
25
- // 导出工具函数
26
- export * from './utils/index.uts'
27
-
28
- // 导出 composables
29
- export { useTheme } from './composables/useTheme.uts'
30
- export { useToast } from './composables/useToast.uts'
31
- export { useModal } from './composables/useModal.uts'
32
-
33
- // 版本信息
34
- export const version: string = '1.0.5'
35
-
36
- // 组件列表(用于文档和类型提示)
37
- export const components: string[] = [
38
- // 基础组件
39
- 'TButton',
40
- 'TIcon',
41
- 'TText',
42
- 'TImage',
43
- 'TDivider',
44
-
45
- // 布局组件
46
- 'TCard',
47
- 'TList',
48
- 'TListItem',
49
- 'TCell',
50
- 'TGrid',
51
- 'TGridItem',
52
- 'TRow',
53
- 'TCol',
54
-
55
- // 表单组件
56
- 'TInput',
57
- 'TNumberInput',
58
- 'TTextarea',
59
- 'TSearchBar',
60
- 'TSwitch',
61
- 'TCheckbox',
62
- 'TCheckboxGroup',
63
- 'TRadioButton',
64
- 'TRadioGroup',
65
- 'TSelect',
66
- 'TSlider',
67
- 'TRate',
68
- 'TPicker',
69
-
70
- // 数据展示
71
- 'Tags',
72
- 'TBadge',
73
- 'TAvatar',
74
- 'TProgress',
75
- 'TNoticeBar',
76
- 'TCollapse',
77
- 'TCollapseItem',
78
- 'TEmpty',
79
- 'TErrorState',
80
- 'TSwiper',
81
-
82
- // 反馈组件
83
- 'TLoading',
84
- 'TToast',
85
- 'TDialog',
86
- 'TPopup',
87
- 'TActionSheet',
88
-
89
- // 导航组件
90
- 'Tabs',
91
- 'TNavBar'
92
- ]
93
-
94
- /**
95
- * Tang UI 插件
96
- */
97
- const TangUI = {
98
- install(app: any) {
99
- // uni-app x 使用 easycom 自动导入组件,这里不需要注册组件
100
- console.log('Tang UI X installed, version:', version)
101
- },
102
- version,
103
- components
104
- }
105
-
106
- // 默认导出
107
- export default TangUI
1
+ /**
2
+ * Tang UI - UniApp X UI 组件库
3
+ * @description 基于 uni-app x 的移动端 UI 组件库
4
+ * @version 1.0.2
5
+ * @author sugar258596
6
+ */
7
+
8
+ /**
9
+ * 注意:uni-app x 项目推荐使用 easycom 自动导入组件
10
+ *
11
+ * 在 pages.json 中配置:
12
+ * {
13
+ * "easycom": {
14
+ * "autoscan": true,
15
+ * "custom": {
16
+ * "^T(.*)": "tang-ui-x/components/T$1/index.uvue"
17
+ * }
18
+ * }
19
+ * }
20
+ *
21
+ * 然后在页面中直接使用:
22
+ * <TButton type="primary">按钮</TButton>
23
+ */
24
+
25
+ // 导出工具函数
26
+ export * from './utils/index.uts'
27
+
28
+ // 导出 composables
29
+ export { useTheme } from './composables/useTheme.uts'
30
+ export { useToast } from './composables/useToast.uts'
31
+ export { useModal } from './composables/useModal.uts'
32
+
33
+ // 版本信息
34
+ export const version: string = '1.0.5'
35
+
36
+ // 组件列表(用于文档和类型提示)
37
+ export const components: string[] = [
38
+ // 基础组件
39
+ 'TButton',
40
+ 'TIcon',
41
+ 'TText',
42
+ 'TImage',
43
+ 'TDivider',
44
+
45
+ // 布局组件
46
+ 'TCard',
47
+ 'TList',
48
+ 'TListItem',
49
+ 'TCell',
50
+ 'TGrid',
51
+ 'TGridItem',
52
+ 'TRow',
53
+ 'TCol',
54
+
55
+ // 表单组件
56
+ 'TInput',
57
+ 'TNumberInput',
58
+ 'TTextarea',
59
+ 'TSearchBar',
60
+ 'TSwitch',
61
+ 'TCheckbox',
62
+ 'TCheckboxGroup',
63
+ 'TRadioButton',
64
+ 'TRadioGroup',
65
+ 'TSelect',
66
+ 'TSlider',
67
+ 'TRate',
68
+ 'TPicker',
69
+ 'TForm',
70
+
71
+ // 数据展示
72
+ 'Tags',
73
+ 'TBadge',
74
+ 'TAvatar',
75
+ 'TProgress',
76
+ 'TNoticeBar',
77
+ 'TCollapse',
78
+ 'TCollapseItem',
79
+ 'TEmpty',
80
+ 'TErrorState',
81
+ 'TSwiper',
82
+
83
+ // 反馈组件
84
+ 'TLoading',
85
+ 'TToast',
86
+ 'TDialog',
87
+ 'TPopup',
88
+ 'TActionSheet',
89
+
90
+ // 导航组件
91
+ 'Tabs',
92
+ 'TNavBar'
93
+ ]
94
+
95
+ /**
96
+ * Tang UI 插件
97
+ */
98
+ const TangUI = {
99
+ install(app: any) {
100
+ // uni-app x 使用 easycom 自动导入组件,这里不需要注册组件
101
+ console.log('Tang UI X installed, version:', version)
102
+ },
103
+ version,
104
+ components
105
+ }
106
+
107
+ // 默认导出
108
+ export default TangUI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tang-ui-x",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "UniApp X UI 组件库 - 基于 uni-app x 的移动端 UI 组件库",
5
5
  "main": "index.uts",
6
6
  "module": "index.uts",