@veltra/mobile 1.0.0 → 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.
Files changed (245) hide show
  1. package/package.json +32 -6
  2. package/src/components/_internal/bottom-sheet/bottom-sheet.vue +81 -0
  3. package/src/components/_internal/bottom-sheet/index.ts +1 -0
  4. package/src/components/_internal/bottom-sheet/style.scss +116 -0
  5. package/src/components/_internal/bottom-sheet/style.ts +1 -0
  6. package/src/components/_internal/overlay-container.ts +34 -0
  7. package/src/components/avatar/avatar.vue +44 -0
  8. package/src/components/avatar/index.ts +1 -0
  9. package/src/components/avatar/style.scss +53 -0
  10. package/src/components/avatar/style.ts +1 -0
  11. package/src/components/badge/badge.vue +59 -0
  12. package/src/components/badge/index.ts +1 -0
  13. package/src/components/badge/style.scss +62 -0
  14. package/src/components/badge/style.ts +1 -0
  15. package/src/components/button/button.vue +76 -0
  16. package/src/components/button/index.ts +1 -0
  17. package/src/components/button/style.scss +207 -0
  18. package/src/components/button/style.ts +1 -0
  19. package/src/components/card/card.vue +24 -0
  20. package/src/components/card/index.ts +1 -0
  21. package/src/components/card/style.scss +35 -0
  22. package/src/components/card/style.ts +1 -0
  23. package/src/components/cascade/cascade.vue +595 -0
  24. package/src/components/cascade/index.ts +1 -0
  25. package/src/components/cascade/node.ts +29 -0
  26. package/src/components/cascade/style.scss +424 -0
  27. package/src/components/cascade/style.ts +2 -0
  28. package/src/components/check-tag/check-tag.vue +35 -0
  29. package/src/components/check-tag/index.ts +1 -0
  30. package/src/components/check-tag/style.scss +46 -0
  31. package/src/components/check-tag/style.ts +1 -0
  32. package/src/components/checkbox/checkbox.vue +67 -0
  33. package/src/components/checkbox/index.ts +1 -0
  34. package/src/components/checkbox/style.scss +132 -0
  35. package/src/components/checkbox/style.ts +2 -0
  36. package/src/components/checkbox-group/checkbox-group.vue +94 -0
  37. package/src/components/checkbox-group/index.ts +1 -0
  38. package/src/components/checkbox-group/style.scss +57 -0
  39. package/src/components/checkbox-group/style.ts +2 -0
  40. package/src/components/collapse/collapse-item.vue +134 -0
  41. package/src/components/collapse/collapse.vue +117 -0
  42. package/src/components/collapse/di.ts +18 -0
  43. package/src/components/collapse/index.ts +3 -0
  44. package/src/components/collapse/style.scss +157 -0
  45. package/src/components/collapse/style.ts +1 -0
  46. package/src/components/date-picker/date-picker.vue +326 -0
  47. package/src/components/date-picker/index.ts +1 -0
  48. package/src/components/date-picker/style.scss +236 -0
  49. package/src/components/date-picker/style.ts +2 -0
  50. package/src/components/descriptions/descriptions-item.vue +12 -0
  51. package/src/components/descriptions/descriptions.vue +50 -0
  52. package/src/components/descriptions/index.ts +2 -0
  53. package/src/components/descriptions/style.scss +120 -0
  54. package/src/components/descriptions/style.ts +1 -0
  55. package/src/components/dialog/dialog.vue +143 -0
  56. package/src/components/dialog/index.ts +1 -0
  57. package/src/components/dialog/style.scss +163 -0
  58. package/src/components/dialog/style.ts +4 -0
  59. package/src/components/divider/divider.vue +29 -0
  60. package/src/components/divider/index.ts +1 -0
  61. package/src/components/divider/style.scss +91 -0
  62. package/src/components/divider/style.ts +1 -0
  63. package/src/components/drawer/drawer.vue +168 -0
  64. package/src/components/drawer/index.ts +1 -0
  65. package/src/components/drawer/style.scss +240 -0
  66. package/src/components/drawer/style.ts +2 -0
  67. package/src/components/empty/empty.vue +22 -0
  68. package/src/components/empty/index.ts +1 -0
  69. package/src/components/empty/style.scss +36 -0
  70. package/src/components/empty/style.ts +1 -0
  71. package/src/components/form/form.vue +119 -0
  72. package/src/components/form/index.ts +1 -0
  73. package/src/components/form/is-field-modified.ts +16 -0
  74. package/src/components/form/style.scss +58 -0
  75. package/src/components/form/style.ts +2 -0
  76. package/src/components/form/use-form-fields.ts +115 -0
  77. package/src/components/form/use-node-interceptor.ts +101 -0
  78. package/src/components/form-item/form-item.vue +171 -0
  79. package/src/components/form-item/helper.ts +9 -0
  80. package/src/components/form-item/index.ts +1 -0
  81. package/src/components/form-item/style.scss +121 -0
  82. package/src/components/form-item/style.ts +1 -0
  83. package/src/components/form-item/validate.ts +131 -0
  84. package/src/components/icon/icon.vue +23 -0
  85. package/src/components/icon/index.ts +1 -0
  86. package/src/components/icon/style.scss +37 -0
  87. package/src/components/icon/style.ts +1 -0
  88. package/src/components/input/index.ts +1 -0
  89. package/src/components/input/input.vue +187 -0
  90. package/src/components/input/style.scss +121 -0
  91. package/src/components/input/style.ts +1 -0
  92. package/src/components/list/index.ts +3 -0
  93. package/src/components/list/list-item.vue +20 -0
  94. package/src/components/list/list.vue +20 -0
  95. package/src/components/list/style.scss +50 -0
  96. package/src/components/list/style.ts +1 -0
  97. package/src/components/loading/directive.ts +34 -0
  98. package/src/components/loading/index.ts +3 -0
  99. package/src/components/loading/loading.vue +25 -0
  100. package/src/components/loading/style.scss +179 -0
  101. package/src/components/loading/style.ts +2 -0
  102. package/src/components/message/helper.ts +28 -0
  103. package/src/components/message/index.ts +3 -0
  104. package/src/components/message/message-box.vue +31 -0
  105. package/src/components/message/message.ts +122 -0
  106. package/src/components/message/message.vue +54 -0
  107. package/src/components/message/style.scss +130 -0
  108. package/src/components/message/style.ts +1 -0
  109. package/src/components/message-confirm/index.ts +3 -0
  110. package/src/components/message-confirm/message-confirm-box.vue +36 -0
  111. package/src/components/message-confirm/message-confirm.ts +139 -0
  112. package/src/components/message-confirm/message-confirm.vue +53 -0
  113. package/src/components/message-confirm/style.scss +138 -0
  114. package/src/components/message-confirm/style.ts +1 -0
  115. package/src/components/multi-select/index.ts +1 -0
  116. package/src/components/multi-select/multi-select.vue +410 -0
  117. package/src/components/multi-select/style.scss +376 -0
  118. package/src/components/multi-select/style.ts +2 -0
  119. package/src/components/number-input/index.ts +1 -0
  120. package/src/components/number-input/number-input.vue +407 -0
  121. package/src/components/number-input/style.scss +118 -0
  122. package/src/components/number-input/style.ts +2 -0
  123. package/src/components/password-input/index.ts +1 -0
  124. package/src/components/password-input/password-input.vue +134 -0
  125. package/src/components/password-input/style.scss +40 -0
  126. package/src/components/password-input/style.ts +2 -0
  127. package/src/components/progress/index.ts +1 -0
  128. package/src/components/progress/progress.vue +90 -0
  129. package/src/components/progress/style.scss +73 -0
  130. package/src/components/progress/style.ts +1 -0
  131. package/src/components/radio/index.ts +1 -0
  132. package/src/components/radio/radio.vue +57 -0
  133. package/src/components/radio/style.scss +113 -0
  134. package/src/components/radio/style.ts +1 -0
  135. package/src/components/radio-group/index.ts +1 -0
  136. package/src/components/radio-group/radio-group.vue +60 -0
  137. package/src/components/radio-group/style.scss +29 -0
  138. package/src/components/radio-group/style.ts +2 -0
  139. package/src/components/segment/index.ts +1 -0
  140. package/src/components/segment/segment.vue +121 -0
  141. package/src/components/segment/style.scss +118 -0
  142. package/src/components/segment/style.ts +1 -0
  143. package/src/components/select/index.ts +1 -0
  144. package/src/components/select/select.vue +278 -0
  145. package/src/components/select/style.scss +237 -0
  146. package/src/components/select/style.ts +2 -0
  147. package/src/components/select/use-options.ts +195 -0
  148. package/src/components/skeleton/index.ts +1 -0
  149. package/src/components/skeleton/skeleton.vue +37 -0
  150. package/src/components/skeleton/style.scss +95 -0
  151. package/src/components/skeleton/style.ts +1 -0
  152. package/src/components/space/index.ts +1 -0
  153. package/src/components/space/space.vue +45 -0
  154. package/src/components/space/style.scss +41 -0
  155. package/src/components/space/style.ts +1 -0
  156. package/src/components/steps/index.ts +1 -0
  157. package/src/components/steps/steps.vue +114 -0
  158. package/src/components/steps/style.scss +231 -0
  159. package/src/components/steps/style.ts +1 -0
  160. package/src/components/switch/index.ts +1 -0
  161. package/src/components/switch/style.scss +137 -0
  162. package/src/components/switch/style.ts +1 -0
  163. package/src/components/switch/switch.vue +63 -0
  164. package/src/components/tabs/index.ts +1 -0
  165. package/src/components/tabs/style.scss +285 -0
  166. package/src/components/tabs/style.ts +1 -0
  167. package/src/components/tabs/tabs.vue +127 -0
  168. package/src/components/tag/index.ts +1 -0
  169. package/src/components/tag/style.scss +114 -0
  170. package/src/components/tag/style.ts +1 -0
  171. package/src/components/tag/tag.vue +49 -0
  172. package/src/components/text/index.ts +1 -0
  173. package/src/components/text/style.scss +63 -0
  174. package/src/components/text/style.ts +1 -0
  175. package/src/components/text/text.vue +63 -0
  176. package/src/components/textarea/index.ts +1 -0
  177. package/src/components/textarea/style.scss +124 -0
  178. package/src/components/textarea/style.ts +1 -0
  179. package/src/components/textarea/textarea.vue +121 -0
  180. package/src/components/textarea/utils.ts +11 -0
  181. package/src/components/time-picker/index.ts +1 -0
  182. package/src/components/time-picker/style.scss +216 -0
  183. package/src/components/time-picker/style.ts +2 -0
  184. package/src/components/time-picker/time-picker.vue +263 -0
  185. package/src/components/timeline/index.ts +2 -0
  186. package/src/components/timeline/style.scss +107 -0
  187. package/src/components/timeline/style.ts +1 -0
  188. package/src/components/timeline/timeline-item.vue +43 -0
  189. package/src/components/timeline/timeline.vue +16 -0
  190. package/src/components/uploader/index.ts +1 -0
  191. package/src/components/uploader/style.scss +220 -0
  192. package/src/components/uploader/style.ts +1 -0
  193. package/src/components/uploader/uploader.vue +306 -0
  194. package/src/env.d.ts +7 -0
  195. package/src/index.ts +99 -1
  196. package/src/shared/bem.ts +12 -0
  197. package/src/shared/use-scroll-lock.ts +111 -0
  198. package/src/shared/use-touch-gesture.ts +150 -0
  199. package/src/styles/_tokens.scss +129 -0
  200. package/src/types/avatar.ts +31 -0
  201. package/src/types/badge.ts +28 -0
  202. package/src/types/button.ts +48 -0
  203. package/src/types/card.ts +16 -0
  204. package/src/types/cascade.ts +87 -0
  205. package/src/types/check-tag.ts +24 -0
  206. package/src/types/checkbox-group.ts +20 -0
  207. package/src/types/checkbox.ts +15 -0
  208. package/src/types/collapse.ts +79 -0
  209. package/src/types/date-picker.ts +39 -0
  210. package/src/types/descriptions.ts +22 -0
  211. package/src/types/dialog.ts +54 -0
  212. package/src/types/divider.ts +14 -0
  213. package/src/types/drawer.ts +38 -0
  214. package/src/types/empty.ts +19 -0
  215. package/src/types/form-item.ts +22 -0
  216. package/src/types/form.ts +54 -0
  217. package/src/types/icon.ts +10 -0
  218. package/src/types/input.ts +51 -0
  219. package/src/types/list.ts +13 -0
  220. package/src/types/loading.ts +20 -0
  221. package/src/types/message-confirm.ts +72 -0
  222. package/src/types/message.ts +71 -0
  223. package/src/types/multi-select.ts +38 -0
  224. package/src/types/number-input.ts +32 -0
  225. package/src/types/password-input.ts +19 -0
  226. package/src/types/progress.ts +22 -0
  227. package/src/types/radio-group.ts +33 -0
  228. package/src/types/radio.ts +18 -0
  229. package/src/types/segment.ts +42 -0
  230. package/src/types/select.ts +63 -0
  231. package/src/types/skeleton.ts +29 -0
  232. package/src/types/space.ts +22 -0
  233. package/src/types/steps.ts +67 -0
  234. package/src/types/switch.ts +17 -0
  235. package/src/types/tabs.ts +63 -0
  236. package/src/types/tag.ts +19 -0
  237. package/src/types/text.ts +28 -0
  238. package/src/types/textarea.ts +76 -0
  239. package/src/types/time-picker.ts +40 -0
  240. package/src/types/timeline.ts +15 -0
  241. package/src/types/uploader.ts +50 -0
  242. package/.turbo/turbo-build.log +0 -2
  243. package/.turbo/turbo-check-types.log +0 -1
  244. package/tsconfig.json +0 -12
  245. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,121 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m with (
2
+ $namespace: um
3
+ );
4
+ @use 'pkg:@veltra/styles/functions' as fn;
5
+ @use '../../styles/tokens' as t;
6
+
7
+ $root-name: form-item;
8
+
9
+ // 嵌入控件区后呈行式形态的控件类(与 form-item 联动的表单控件名单)
10
+ $form-controls: (
11
+ '.um-input',
12
+ '.um-number-input',
13
+ '.um-password-input',
14
+ '.um-select',
15
+ '.um-multi-select',
16
+ '.um-cascade',
17
+ '.um-date-picker',
18
+ '.um-time-picker',
19
+ '.um-textarea',
20
+ '.um-uploader'
21
+ );
22
+
23
+ @include m.b($root-name) {
24
+ // 列表行式:label 左、控件区占右侧剩余宽度,整行热区 ≥44px(行高 48px)
25
+ display: flex;
26
+ align-items: center;
27
+ box-sizing: border-box;
28
+ width: 100%;
29
+ min-height: t.use-var('form-row-height');
30
+ padding: 0 t.use-var('spacing-lg');
31
+ background-color: fn.use-var(bg-color);
32
+
33
+ // 相邻行细分隔线
34
+ & + & {
35
+ border-top: fn.use-var(border-muted);
36
+ }
37
+
38
+ @include m.e(label) {
39
+ flex-shrink: 0;
40
+ // 超长 label 封顶控件区一半宽度,避免挤压输入区
41
+ max-width: 60%;
42
+ color: fn.use-var(text-color, main);
43
+ font-size: t.use-var('font-size-main');
44
+ line-height: t.use-var('text-line-height');
45
+ padding-right: t.use-var('spacing-md');
46
+
47
+ @include m.flex($align: center);
48
+
49
+ @include m.is(required) {
50
+ &::before {
51
+ content: '*';
52
+ padding-right: 2px;
53
+ color: fn.use-var(color, danger);
54
+ }
55
+ }
56
+ }
57
+
58
+ @include m.e(wrapper) {
59
+ flex-grow: 1;
60
+ min-width: 0;
61
+ }
62
+
63
+ @include m.e(content) {
64
+ @include m.flex($wrap: wrap, $align: center);
65
+ width: 100%;
66
+ }
67
+
68
+ // 错误提示按移动端惯例展示在行内下方:出现时才占位,长文案允许换行
69
+ @include m.e(error-text) {
70
+ display: block;
71
+ margin-top: t.use-var('spacing-xs');
72
+ color: fn.use-var(color, danger);
73
+ font-size: t.use-var('font-size-secondary');
74
+ line-height: 1.4;
75
+ }
76
+
77
+ // label 在控件上方:长控件(文本域等)纵向堆叠
78
+ @include m.is(label-top) {
79
+ flex-direction: column;
80
+ align-items: stretch;
81
+ padding: t.use-var('spacing-sm') t.use-var('spacing-lg');
82
+
83
+ @include m.bem($root-name, label) {
84
+ max-width: none;
85
+ padding: 0 0 t.use-var('spacing-xs');
86
+ }
87
+ }
88
+ }
89
+
90
+ // 行式嵌入态:卡片行自身就是控件容器,嵌入控件去边框、透明底、占满控件区。
91
+ // 块级限定选择器(三层)确保覆盖控件自身尺寸档与聚焦态,不受样式引入顺序影响
92
+ .um-form-item .um-form-item__content {
93
+ #{$form-controls} {
94
+ height: auto;
95
+ min-height: t.use-var('touch-target');
96
+ border-color: transparent;
97
+ background-color: transparent;
98
+ box-shadow: none;
99
+ padding: 0;
100
+ }
101
+
102
+ // 文本域嵌入后与输入行左缘对齐:去原生左侧内边距,纵向保留行内呼吸空间
103
+ .um-textarea__native {
104
+ padding-left: 0;
105
+ }
106
+
107
+ .um-number-input__step button {
108
+ border-color: transparent;
109
+ background-color: transparent;
110
+ }
111
+ }
112
+
113
+ .#{$root-name}-tips-enter-from,
114
+ .#{$root-name}-tips-leave-to {
115
+ opacity: 0;
116
+ }
117
+
118
+ .#{$root-name}-tips-enter-active,
119
+ .#{$root-name}-tips-leave-active {
120
+ transition: opacity fn.use-var(transition, fast) fn.use-var(transition, ease);
121
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,131 @@
1
+ import { isEmpty, o } from '@cat-kit/core'
2
+ import type { PresetRule, Undef, ValidateRule } from '@veltra/utils'
3
+
4
+ const presetRules: Record<PresetRule, (value: string) => string | undefined> = {
5
+ email(v) {
6
+ const re = /^([\w_-]+)@([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}$/
7
+ if (!re.test(v)) {
8
+ return '邮箱格式不正确'
9
+ }
10
+ },
11
+ phone(v) {
12
+ const re = /^\d{11}$/
13
+ if (!re.test(v)) {
14
+ return '手机号格式不正确'
15
+ }
16
+ },
17
+ num(v) {
18
+ const re = /^\d+$/
19
+ if (!re.test(v)) {
20
+ return '数字格式不正确'
21
+ }
22
+ },
23
+ url(v) {
24
+ const re = /^(ftp|https?):\/\/([\w_-]+)\.([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}(.*)/
25
+ if (!re.test(v)) {
26
+ return '链接格式不正确'
27
+ }
28
+ },
29
+
30
+ idCard(v) {
31
+ const re = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/
32
+ if (!re.test(v)) {
33
+ return '身份证格式不正确'
34
+ }
35
+ }
36
+ }
37
+
38
+ /** 预设规则 */
39
+ const ruleTypes = {
40
+ required(value: any, required: ValidateRule['required']): Undef<string> {
41
+ if (required === false) return
42
+
43
+ const errMsg = typeof required === 'string' ? required : '该项不能为空'
44
+ if (isEmpty(value)) return errMsg
45
+
46
+ if (Array.isArray(value) && !value.length) return errMsg
47
+ if (typeof value === 'string' && !value) return errMsg
48
+ },
49
+ min(value: any, rule: ValidateRule['min']): Undef<string> {
50
+ if (isEmpty(value)) return
51
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
52
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项必须大于等于${_rule}`
53
+ if (typeof value !== 'number') return `${value}不是一个数字`
54
+ if (value < _rule) return errMsg
55
+ },
56
+ max(value: any, rule: ValidateRule['max']): Undef<string> {
57
+ if (isEmpty(value)) return
58
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
59
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项必须小于等于${_rule}`
60
+ if (typeof value !== 'number') return `${value}不是一个数字`
61
+ if (value > _rule) return errMsg
62
+ },
63
+
64
+ minLen(value: any, rule: ValidateRule['minLen']): Undef<string> {
65
+ if (isEmpty(value)) return
66
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
67
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须大于等于${_rule}`
68
+ if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
69
+ if (value.length < _rule) return errMsg
70
+ },
71
+ maxLen(value: any, rule: ValidateRule['maxLen']): Undef<string> {
72
+ if (isEmpty(value)) return
73
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
74
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须小于等于:${_rule}`
75
+ if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
76
+ if (value.length > _rule) return errMsg
77
+ },
78
+ match(value: any, rule: ValidateRule['match']): Undef<string> {
79
+ if (isEmpty(value) || value === '') return
80
+ if (typeof rule === 'string') {
81
+ if (!rule) return
82
+ rule = new RegExp(rule)
83
+ }
84
+ let _rule = Array.isArray(rule) ? rule[0] : rule!
85
+ let errMsg = Array.isArray(rule) ? rule[1] : `该项不匹配正则:${_rule}`
86
+ if (typeof value !== 'string') return `${value}不是一个字符串`
87
+ if (!_rule.test(value)) return errMsg
88
+ },
89
+ preset(value: any, rule: ValidateRule['preset']): Undef<string> {
90
+ if (isEmpty(value) || value === '' || !rule) return
91
+ if (typeof value !== 'string') return `${value}不是一个字符串`
92
+ const ruleValidator = presetRules[rule]
93
+ return ruleValidator(value)
94
+ }
95
+ }
96
+
97
+ export async function validateField(
98
+ formData: Record<string, any>,
99
+ field: string,
100
+ rules: ValidateRule
101
+ ): Promise<Undef<string>> {
102
+ const value = o(formData).get(field)
103
+
104
+ if (!rules) return
105
+
106
+ const { validator, required, ...normalRules } = rules
107
+
108
+ // 必填要先去校验
109
+ if (required) {
110
+ const err = ruleTypes.required(value, required)
111
+ if (err) return err
112
+ }
113
+
114
+ // 校验规则
115
+ for (const ruleKey in normalRules) {
116
+ const validate = ruleTypes[ruleKey as keyof typeof ruleTypes]
117
+ if (!validate) continue
118
+
119
+ const err = validate(value, normalRules[ruleKey])
120
+
121
+ if (err) return err
122
+ }
123
+
124
+ // 自定义校验最后校验
125
+ if (validator) {
126
+ const err = await validator(value, formData)
127
+ if (err) return err
128
+ }
129
+
130
+ return
131
+ }
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <i :class="cls.b" :style="style">
3
+ <slot></slot>
4
+ </i>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { withUnit } from '@veltra/utils'
9
+ import { type CSSProperties, computed } from 'vue'
10
+
11
+ import { bem } from '../../shared/bem'
12
+ import type { IconProps } from '../../types/icon'
13
+
14
+ defineOptions({ name: 'UIcon' })
15
+
16
+ const props = defineProps<IconProps>()
17
+
18
+ const cls = bem('icon')
19
+
20
+ const style = computed<CSSProperties>(() => {
21
+ return { fontSize: withUnit(props.size, 'px') }
22
+ })
23
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UIcon } from './icon.vue'
@@ -0,0 +1,37 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m with ($namespace: um);
2
+ @use '../../styles/tokens' as t;
3
+
4
+ @keyframes um-icon-rotating {
5
+ 0% {
6
+ transform: rotateZ(0deg);
7
+ }
8
+ 100% {
9
+ transform: rotateZ(360deg);
10
+ }
11
+ }
12
+
13
+ @include m.b(icon) {
14
+ height: 1em;
15
+ width: 1em;
16
+ line-height: 1em;
17
+ @include m.flex(inline-flex, center, center);
18
+ position: relative;
19
+ fill: currentColor;
20
+ // 默认尺寸对齐移动端 16px 主字号体系;显式 size 经内联 font-size 覆盖
21
+ font-size: t.use-var(font-size-main);
22
+ color: inherit;
23
+ vertical-align: middle;
24
+ user-select: none;
25
+ will-change: transform;
26
+
27
+ // 加载转圈
28
+ @include m.is(loading) {
29
+ transform-origin: center;
30
+ animation: um-icon-rotating 2s linear infinite;
31
+ }
32
+
33
+ svg {
34
+ height: 1em;
35
+ width: 1em;
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1 @@
1
+ export { default as UInput } from './input.vue'
@@ -0,0 +1,187 @@
1
+ <template>
2
+ <div v-if="!readonly" v-bind="rootAttrs" :class="inputClass">
3
+ <span v-if="$slots.prefix || prefix" :class="prefixClass" @click="handlePrefixClick">
4
+ {{ prefix }}
5
+ <slot name="prefix"></slot>
6
+ </span>
7
+
8
+ <input
9
+ :class="cls.e('native')"
10
+ :placeholder="props.placeholder"
11
+ type="text"
12
+ :value="model"
13
+ v-bind="nativeAttrs"
14
+ @input="handleInput"
15
+ @change="handleChange"
16
+ @focus="handleFocus"
17
+ @blur="handleBlur"
18
+ @compositionstart="handleCompositionStart"
19
+ @compositionend="handleCompositionEnd"
20
+ autocomplete="off"
21
+ ref="el"
22
+ :disabled="disabled"
23
+ :readonly="nativeReadonly"
24
+ />
25
+
26
+ <span :class="suffixClass" v-if="hasSuffix">
27
+ <button
28
+ v-if="showClear"
29
+ :class="cls.e('clear')"
30
+ type="button"
31
+ aria-label="清除"
32
+ @click.stop="clearModelValue"
33
+ key="clear"
34
+ >
35
+ <Close />
36
+ </button>
37
+
38
+ <span v-else :class="cls.e('suffix-content')" @click="handleSuffixClick" key="suffix">
39
+ {{ suffix }}
40
+ <slot name="suffix"></slot>
41
+ </span>
42
+ </span>
43
+ </div>
44
+
45
+ <template v-else>
46
+ {{ generateModel || FORM_EMPTY_CONTENT }}
47
+ </template>
48
+ </template>
49
+
50
+ <script lang="ts" setup>
51
+ import { o } from '@cat-kit/core'
52
+ import { useFocus, useFormFallbackProps } from '@veltra/compositions'
53
+ import { Close } from '@veltra/icons/normal'
54
+ import { FORM_EMPTY_CONTENT, injectFormContext } from '@veltra/utils'
55
+ import { computed, getCurrentInstance, nextTick, shallowRef, useAttrs } from 'vue'
56
+
57
+ import { bem } from '../../shared/bem'
58
+ import type { InputEmits, InputProps, _InputExposed } from '../../types/input'
59
+
60
+ defineOptions({ name: 'UInput', inheritAttrs: false })
61
+
62
+ const props = withDefaults(defineProps<InputProps>(), {
63
+ placeholder: '请输入',
64
+ clearable: true,
65
+ disabled: undefined,
66
+ readonly: undefined
67
+ })
68
+
69
+ const emit = defineEmits<InputEmits>()
70
+
71
+ const model = defineModel<string>()
72
+
73
+ const inst = getCurrentInstance()
74
+
75
+ const cls = bem('input')
76
+
77
+ const { formProps } = injectFormContext()
78
+
79
+ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])
80
+
81
+ const { focus, handleBlur, handleFocus } = useFocus((focused) => {
82
+ if (focused) {
83
+ emit('focus')
84
+ } else {
85
+ emit('blur')
86
+ }
87
+ })
88
+
89
+ /**
90
+ * 透传到原生 input 的原生属性(输入约束与移动端键盘提示,如 inputmode 弹出数字键盘、
91
+ * maxlength 限制长度),其余属性仍落在根元素上,不构成新的公开 prop
92
+ */
93
+ const NATIVE_INPUT_ATTRS = ['inputmode', 'enterkeyhint', 'maxlength'] as const
94
+
95
+ const attrs = useAttrs()
96
+
97
+ const nativeAttrs = computed(() => o(attrs as Record<string, any>).pick([...NATIVE_INPUT_ATTRS]))
98
+
99
+ const rootAttrs = computed(() => o(attrs as Record<string, any>).omit([...NATIVE_INPUT_ATTRS]))
100
+
101
+ const inputClass = computed(() => {
102
+ return [
103
+ cls.b,
104
+ cls.m(size.value),
105
+ bem.is('disabled', disabled.value),
106
+ bem.is('readonly', readonly.value),
107
+ bem.is('focus', focus.value)
108
+ ]
109
+ })
110
+
111
+ const prefixClass = [cls.e('prefix'), bem.is('clickable', !!inst?.vnode.props?.['onPrefix:click'])]
112
+
113
+ const suffixClass = [cls.e('suffix'), bem.is('clickable', !!inst?.vnode.props?.['onSuffix:click'])]
114
+
115
+ let isComposing = false
116
+
117
+ function handleCompositionStart() {
118
+ isComposing = true
119
+ }
120
+
121
+ function handleCompositionEnd(e: Event) {
122
+ isComposing = false
123
+ handleInput(e)
124
+ }
125
+
126
+ const handleInput = (e: Event) => {
127
+ if (isComposing) return
128
+
129
+ const inputVal = (e.target as HTMLInputElement).value
130
+ emit('native:input', e)
131
+
132
+ const valid = props.pattern?.test(inputVal) ?? true
133
+
134
+ if (!valid) return
135
+ model.value = inputVal
136
+ }
137
+
138
+ const handlePrefixClick = () => {
139
+ emit('prefix:click', model.value)
140
+ }
141
+
142
+ const handleSuffixClick = () => {
143
+ emit('suffix:click', model.value)
144
+ }
145
+
146
+ const clearModelValue = () => {
147
+ model.value = ''
148
+ emit('clear')
149
+ }
150
+
151
+ // 移动端无 hover,有值即展示清除按钮
152
+ const showClear = computed(() => {
153
+ return props.clearable && !disabled.value && !!model.value
154
+ })
155
+
156
+ const hasSuffixContent = computed(() => {
157
+ return !!inst?.slots.suffix || !!props.suffix
158
+ })
159
+
160
+ const hasSuffix = computed(() => {
161
+ return hasSuffixContent.value || showClear.value
162
+ })
163
+
164
+ const handleChange = (e: Event) => {
165
+ const target = e.target as HTMLInputElement
166
+
167
+ const valid = props.pattern?.test(target.value) ?? true
168
+
169
+ if (valid) {
170
+ emit('change', target.value)
171
+ } else {
172
+ nextTick(() => {
173
+ target.value = model.value ?? ''
174
+ })
175
+ }
176
+ }
177
+
178
+ const el = shallowRef<HTMLInputElement>()
179
+
180
+ const generateModel = computed(() => {
181
+ if (!model.value) return ''
182
+
183
+ return `${props.prefix ?? ''}${model.value}${props.suffix ?? ''}`
184
+ })
185
+
186
+ defineExpose<_InputExposed>({ el })
187
+ </script>
@@ -0,0 +1,121 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m with ($namespace: um);
2
+ @use 'pkg:@veltra/styles/functions' as fn;
3
+ @use '../../styles/tokens' as t;
4
+
5
+ $root-name: input;
6
+
7
+ @include m.b($root-name) {
8
+ box-sizing: border-box;
9
+ vertical-align: middle;
10
+ line-height: 1;
11
+ border: fn.use-var(border-muted);
12
+ box-shadow: fn.use-var(shadow, emboss);
13
+ background-color: fn.component-var(input, bg, fn.use-var(bg-color, middle));
14
+ transition:
15
+ border-color fn.use-var(transition, fast) fn.use-var(transition, ease),
16
+ box-shadow fn.use-var(transition, fast) fn.use-var(transition, ease);
17
+ overflow: hidden;
18
+ width: 100%;
19
+
20
+ @include m.flex($display: inline-flex);
21
+
22
+ @include m.size using ($size) {
23
+ // 密度全部取移动端 token:高度不小于 44px 触控热区,字号 16px(iOS 聚焦防缩放)
24
+ height: max(t.control-height($size), t.use-var('touch-target'));
25
+ border-radius: t.control-radius($size);
26
+ padding: 0 t.use-var('spacing-md');
27
+
28
+ @include m.bem($root-name, native) {
29
+ font-size: t.use-var('font-size-main');
30
+ line-height: t.use-var('text-line-height');
31
+ }
32
+ }
33
+
34
+ // 聚焦态:移动端高亮惯例(主色描边 + 焦点环)
35
+ @include m.is(focus) {
36
+ border-color: fn.use-var(color, primary);
37
+ box-shadow: fn.use-var(focus-ring);
38
+ }
39
+
40
+ @include m.e(native) {
41
+ // 清除默认样式
42
+ outline: none;
43
+ border: none;
44
+ background-color: transparent;
45
+ width: 100%;
46
+ flex-grow: 1;
47
+ height: 100%;
48
+ color: fn.use-var(text-color, main);
49
+ font-family: fn.use-var(font-family);
50
+ padding: 0;
51
+ -webkit-tap-highlight-color: transparent;
52
+
53
+ &::placeholder {
54
+ color: fn.use-var(text-color, placeholder);
55
+ }
56
+
57
+ &:disabled {
58
+ opacity: 1;
59
+ -webkit-text-fill-color: fn.use-var(text-color, disabled);
60
+ }
61
+ }
62
+
63
+ @include m.e(prefix, suffix) {
64
+ font-size: t.use-var('font-size-main');
65
+ color: fn.use-var(text-color, main);
66
+ flex-shrink: 0;
67
+ height: 100%;
68
+ text-align: center;
69
+ @include m.flex(inline-flex, $justify: center, $align: center);
70
+
71
+ // 前后缀可点击时保证 44px 宽热区
72
+ @include m.is(clickable) {
73
+ min-width: t.use-var('touch-target');
74
+ cursor: pointer;
75
+ }
76
+ }
77
+
78
+ @include m.e(clear) {
79
+ // 清除按钮:宽度 44px、高度撑满输入行内容盒(行高 ≥44px)
80
+ min-width: t.use-var('touch-target');
81
+ align-self: stretch;
82
+ border: none;
83
+ margin: 0;
84
+ padding: 0;
85
+ background-color: transparent;
86
+ display: inline-flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ color: fn.use-var(text-color, second);
90
+ font-size: t.use-var('font-size-main');
91
+ cursor: pointer;
92
+ -webkit-tap-highlight-color: transparent;
93
+
94
+ svg {
95
+ width: 1em;
96
+ height: 1em;
97
+ fill: currentColor;
98
+ }
99
+
100
+ &:active {
101
+ color: fn.use-var(color, primary);
102
+ }
103
+ }
104
+
105
+ @include m.e(suffix-content) {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ // 撑满前后缀区高度,slot 内的清除/切换按钮(number-input 等)才能拉伸满输入行
110
+ align-self: stretch;
111
+ }
112
+
113
+ @include m.is(disabled) {
114
+ background-color: fn.use-var(color, disabled);
115
+ box-shadow: none;
116
+
117
+ @include m.bem($root-name, native) {
118
+ cursor: not-allowed;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,3 @@
1
+ export { default as UList } from './list.vue'
2
+
3
+ export { default as UListItem } from './list-item.vue'
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <li :class="itemClass">
3
+ <slot />
4
+ </li>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { computed, useAttrs } from 'vue'
9
+
10
+ import { bem } from '../../shared/bem'
11
+
12
+ defineOptions({ name: 'UListItem' })
13
+
14
+ const cls = bem('list-item')
15
+
16
+ // 绑定了点击监听(如 @click)即视为可点行:加按压态反馈,点击由 attrs 透传到 li
17
+ const attrs = useAttrs()
18
+
19
+ const itemClass = computed(() => [cls.b, bem.is('clickable', 'onClick' in attrs)])
20
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <ul :class="[cls.b, cls.m(size)]">
3
+ <slot v-for="(item, index) of data" v-bind="{ item, index }" />
4
+ </ul>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { computed } from 'vue'
9
+
10
+ import { bem } from '../../shared/bem'
11
+ import type { ListProps } from '../../types/list'
12
+
13
+ defineOptions({ name: 'UList' })
14
+
15
+ const props = withDefaults(defineProps<ListProps>(), {})
16
+
17
+ const cls = bem('list')
18
+
19
+ const size = computed(() => props.size ?? 'default')
20
+ </script>