@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,407 @@
1
+ <template>
2
+ <div v-if="!readonly" :class="className">
3
+ <u-input
4
+ ref="inputRef"
5
+ :class="cls.e('field')"
6
+ :model-value="displayed"
7
+ v-bind="inputProps"
8
+ inputmode="decimal"
9
+ @update:model-value="handleUpdateModelValue"
10
+ @change="handleChange"
11
+ @keydown.stop="handleKeydown"
12
+ @focus="handleFocus"
13
+ @blur="handleBlur"
14
+ :size="size"
15
+ :readonly="readonly"
16
+ :disabled="disabled"
17
+ :clearable="false"
18
+ >
19
+ <template #suffix v-if="hasSuffix">
20
+ <span :class="cls.e('suffix')">
21
+ <button
22
+ v-if="props.clearable"
23
+ :class="[cls.e('clear'), bem.is('hidden', !showClear)]"
24
+ type="button"
25
+ aria-label="清除"
26
+ @click.stop="handleClear"
27
+ >
28
+ <Close />
29
+ </button>
30
+ <span v-if="hasCustomSuffix" :class="cls.e('suffix-content')">
31
+ {{ suffix }}
32
+ <slot name="suffix" />
33
+ </span>
34
+ </span>
35
+ </template>
36
+
37
+ <template #prefix v-if="slots.prefix">
38
+ <slot name="prefix" />
39
+ </template>
40
+ </u-input>
41
+
42
+ <!-- 步进按钮外置在输入框右侧,保证 44×44 触控热区(移动端形态) -->
43
+ <div v-if="showStep" :class="cls.e('step')">
44
+ <button
45
+ type="button"
46
+ aria-label="减少"
47
+ :class="bem.is('disabled', disabled || !reducible)"
48
+ :disabled="disabled || !reducible"
49
+ @click="decrease"
50
+ >
51
+ <Minus />
52
+ </button>
53
+ <button
54
+ type="button"
55
+ aria-label="增加"
56
+ :class="bem.is('disabled', disabled || !increasable)"
57
+ :disabled="disabled || !increasable"
58
+ @click="increase"
59
+ >
60
+ <Plus />
61
+ </button>
62
+ </div>
63
+ </div>
64
+
65
+ <template v-else>
66
+ {{ generateDisplayed || FORM_EMPTY_CONTENT }}
67
+ </template>
68
+ </template>
69
+
70
+ <script lang="ts" setup>
71
+ import { $n, n, o, isUndef } from '@cat-kit/core'
72
+ import { useFormFallbackProps } from '@veltra/compositions'
73
+ import { Close, Minus, Plus } from '@veltra/icons/normal'
74
+ import { FORM_EMPTY_CONTENT, Tween, injectFormContext } from '@veltra/utils'
75
+ import { computed, shallowRef, useAttrs, watch } from 'vue'
76
+
77
+ import { bem } from '../../shared/bem'
78
+ import type { InputExposed } from '../../types/input'
79
+ import type { NumberInputEmits, NumberInputProps } from '../../types/number-input'
80
+ import { UInput } from '../input'
81
+
82
+ defineOptions({ name: 'UNumberInput', inheritAttrs: false })
83
+
84
+ const props = withDefaults(defineProps<NumberInputProps>(), {
85
+ placeholder: '请输入',
86
+ clearable: true,
87
+ disabled: undefined,
88
+ readonly: undefined
89
+ })
90
+ const emit = defineEmits<NumberInputEmits>()
91
+
92
+ const slots = defineSlots<{ prefix?: () => any; suffix?: () => any }>()
93
+
94
+ const { formProps } = injectFormContext()
95
+
96
+ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
97
+ size: 'default',
98
+ disabled: false,
99
+ readonly: false
100
+ })
101
+
102
+ const attrs = useAttrs()
103
+
104
+ const inputProps = computed(() => ({
105
+ ...attrs,
106
+ ...o(props as Record<string, any>).pick(['disabled', 'placeholder', 'size', 'prefix'])
107
+ }))
108
+
109
+ const inputRef = shallowRef<InputExposed>()
110
+
111
+ const inputDom = computed(() => inputRef.value?.el)
112
+
113
+ const cls = bem('number-input')
114
+
115
+ const className = computed(() => {
116
+ return [cls.b, cls.m(size.value)]
117
+ })
118
+
119
+ /** 实际值 */
120
+ const model = defineModel<NumberInputProps['modelValue']>()
121
+
122
+ // 展示值
123
+ const displayed = shallowRef('')
124
+
125
+ const focused = shallowRef(false)
126
+
127
+ const generateDisplayed = computed(() => {
128
+ if (!displayed.value) return ''
129
+
130
+ return `${props.prefix ?? ''}${displayed.value}${props.suffix ?? ''}`
131
+ })
132
+
133
+ const showStep = computed(() => props.step !== undefined && props.step !== false)
134
+
135
+ const hasCustomSuffix = computed(() => !!props.suffix || !!slots.suffix)
136
+
137
+ const hasSuffix = computed(() => props.clearable || hasCustomSuffix.value)
138
+
139
+ // 移动端无 hover,有输入即展示清除按钮
140
+ const showClear = computed(() => {
141
+ return props.clearable && !disabled.value && displayed.value !== ''
142
+ })
143
+
144
+ /** 步长 */
145
+ const stepVal = computed<number>(() => {
146
+ const { step } = props
147
+ if (step === undefined) return 1
148
+ return typeof step === 'boolean' ? 1 : step
149
+ })
150
+
151
+ /** 是否可增 */
152
+ const increasable = computed(() => {
153
+ const { max, multiple } = props
154
+ if (isUndef(max) || isUndef(model.value)) return true
155
+ // 如果存在倍数,先将 model.value 除以倍数得到原始值再比较
156
+ const rawValue = multiple ? $n.div(model.value, multiple) : model.value
157
+ return rawValue < max
158
+ })
159
+
160
+ /** 是否可减 */
161
+ const reducible = computed(() => {
162
+ const { min, multiple } = props
163
+ if (isUndef(min) || isUndef(model.value)) return true
164
+ // 如果存在倍数,先将 model.value 除以倍数得到原始值再比较
165
+ const rawValue = multiple ? $n.div(model.value, multiple) : model.value
166
+ return rawValue > min
167
+ })
168
+
169
+ // 通过值和步长值计算默认的最大精度
170
+ const defaultMaxPrecision = computed(() => {
171
+ const { multiple } = props
172
+ // 如果存在倍数,基于原始值计算精度
173
+ const rawValue =
174
+ multiple && model.value !== undefined ? $n.div(model.value, multiple) : model.value
175
+ return Math.max(
176
+ String(rawValue).split('.')[1]?.length ?? 0,
177
+ String(stepVal.value).split('.')[1]?.length ?? 0
178
+ )
179
+ })
180
+
181
+ /**
182
+ * 获取展示值
183
+ * @param num 实际值
184
+ */
185
+ function getDisplayed(num?: number): string {
186
+ if (!num && num !== 0) return ''
187
+
188
+ const {
189
+ currency,
190
+ precision,
191
+ minPrecision,
192
+ // 如果没有指定最大精度那么设置默认为值和步长值中的较大值
193
+ maxPrecision = defaultMaxPrecision.value,
194
+ multiple
195
+ } = props
196
+
197
+ // 如果存在倍数,先将实际值除以倍数得到原始值
198
+ const displayValue = multiple ? $n.div(num, multiple) : num
199
+
200
+ return currency
201
+ ? n(displayValue).currency('CNY', { precision, minPrecision, maxPrecision })
202
+ : n(displayValue).fixed(precision ?? { minPrecision, maxPrecision })
203
+ }
204
+
205
+ watch(
206
+ [model, focused, () => props.currency],
207
+ ([modelValue, inputFocused]) => {
208
+ if (inputFocused) return
209
+ displayed.value = getDisplayed(modelValue)
210
+ },
211
+ { immediate: true }
212
+ )
213
+
214
+ /**
215
+ * 解析展示值
216
+ * @param str 展示值
217
+ */
218
+ function parseDisplayed(str: string): number | undefined {
219
+ if (!str) return undefined
220
+
221
+ // 将货币格式去掉再转化为数字
222
+ const number = +str.replace(/,/g, '')
223
+ const { multiple } = props
224
+
225
+ // 如果解析失败,使用当前值作为回退
226
+ // 如果存在倍数,需要先将 model.value 除以倍数得到原始值
227
+ let result: number | undefined
228
+ if (isNaN(number)) {
229
+ if (model.value === undefined) return undefined
230
+ result = multiple ? $n.div(model.value, multiple) : model.value
231
+ } else {
232
+ result = number
233
+ }
234
+
235
+ if (result === undefined) return undefined
236
+
237
+ const { precision, maxPrecision, minPrecision } = props
238
+
239
+ const fixedResult = +n(result).fixed(precision ?? { minPrecision, maxPrecision })
240
+
241
+ // 如果存在倍数,将原始值乘以倍数返回实际值
242
+ return multiple ? $n.mul(fixedResult, multiple) : fixedResult
243
+ }
244
+
245
+ /**
246
+ * 获取有效值
247
+ * @param val 值
248
+ */
249
+ function getValidValue<T extends undefined | number>(val: T): T {
250
+ if (val === undefined) return val
251
+ const { min, max, multiple } = props
252
+
253
+ // 如果存在倍数,先将值除以倍数得到原始值进行验证,验证后再乘以倍数
254
+ if (multiple) {
255
+ const rawVal = $n.div(val, multiple)
256
+ let validRawVal = rawVal
257
+ if (min !== undefined && rawVal < min) validRawVal = min
258
+ if (max !== undefined && rawVal > max) validRawVal = max
259
+ return $n.mul(validRawVal, multiple) as T
260
+ }
261
+
262
+ if (min !== undefined && val < min) return min as T
263
+ if (max !== undefined && val > max) return max as T
264
+ return val
265
+ }
266
+
267
+ function handleUpdateModelValue(input: string): void {
268
+ const newVal = parseDisplayed(input)
269
+ model.value = getValidValue(newVal)
270
+ displayed.value = input
271
+ }
272
+
273
+ function handleClear(): void {
274
+ if (!showClear.value) return
275
+ model.value = undefined
276
+ displayed.value = ''
277
+ emit('clear')
278
+ }
279
+
280
+ /**
281
+ * 处理输入值变化的函数, 该函数仅在输入框失去焦点时触发,
282
+ * 主要用于在输入非数字的情况下的修正处理
283
+ */
284
+ function handleChange(): void {
285
+ emit('change', model.value)
286
+ }
287
+
288
+ const tween = new Tween(
289
+ { n: model.value ?? 0 },
290
+ {
291
+ onUpdate(state) {
292
+ const _rawInput = inputDom.value
293
+ if (!_rawInput) return
294
+ const { multiple } = props
295
+ // 如果存在倍数,tween.state.n 存储的是原始值,需要乘以倍数后传给 getDisplayed
296
+ const actualValue = multiple ? $n.mul(state.n, multiple) : state.n
297
+ _rawInput.value = getDisplayed(actualValue)
298
+ },
299
+ // 动画进行的过程值有可能被改变, 因此在onComplete中确保还原的是原本的值
300
+ onComplete() {
301
+ const _rawInput = inputDom.value
302
+ if (!_rawInput) return
303
+ _rawInput.value = getDisplayed(model.value)
304
+ }
305
+ }
306
+ )
307
+
308
+ /**
309
+ * 步进后更新输入框的展示值
310
+ * 步长为 1 时直接展示目标值, 不播放数字滚动动画
311
+ * @param rawFrom 步进前的原始值(存在倍数时为除以倍数后的值)
312
+ * @param rawTarget 步进后的原始值(存在倍数时为除以倍数后的值)
313
+ */
314
+ function updateStepDisplay(rawFrom: number, rawTarget: number): void {
315
+ tween.state.n = rawFrom
316
+
317
+ if (stepVal.value === 1) {
318
+ tween.state.n = rawTarget
319
+ const _rawInput = inputDom.value
320
+ if (!_rawInput) return
321
+ const { multiple } = props
322
+ _rawInput.value = getDisplayed(multiple ? $n.mul(rawTarget, multiple) : rawTarget)
323
+ return
324
+ }
325
+
326
+ // tween 动画在原始值上进行
327
+ tween.to({ n: rawTarget })
328
+ }
329
+
330
+ /** 增 */
331
+ function increase(): void {
332
+ if (disabled.value) return
333
+ const { multiple } = props
334
+ const val = model.value ?? 0
335
+
336
+ if (multiple) {
337
+ // 如果存在倍数,先将实际值除以倍数得到原始值
338
+ const rawVal = $n.div(val, multiple)
339
+ // 在原始值基础上加步长
340
+ const newRawVal = $n.plus(rawVal, stepVal.value)
341
+ // 乘以倍数得到新的实际值
342
+ const target = getValidValue($n.mul(newRawVal, multiple))
343
+ model.value = target
344
+ updateStepDisplay($n.div(val, multiple), $n.div(target, multiple))
345
+ } else {
346
+ const target = getValidValue($n.plus(val, stepVal.value))
347
+ model.value = target
348
+ updateStepDisplay(val, target)
349
+ }
350
+ }
351
+
352
+ /** 减 */
353
+ function decrease(): void {
354
+ if (disabled.value) return
355
+ const { multiple } = props
356
+ const val = model.value ?? 0
357
+
358
+ if (multiple) {
359
+ // 如果存在倍数,先将实际值除以倍数得到原始值
360
+ const rawVal = $n.div(val, multiple)
361
+ // 在原始值基础上减步长
362
+ const newRawVal = $n.minus(rawVal, stepVal.value)
363
+ // 乘以倍数得到新的实际值
364
+ const target = getValidValue($n.mul(newRawVal, multiple))
365
+ model.value = target
366
+ updateStepDisplay($n.div(val, multiple), $n.div(target, multiple))
367
+ } else {
368
+ const target = getValidValue($n.minus(val, stepVal.value))
369
+ model.value = target
370
+ updateStepDisplay(val, target)
371
+ }
372
+ }
373
+
374
+ function handleKeydown(e: KeyboardEvent): void {
375
+ if (!props.step) return
376
+ if (e.key === 'ArrowUp') {
377
+ e.preventDefault()
378
+ return increase()
379
+ }
380
+ if (e.key === 'ArrowDown') {
381
+ e.preventDefault()
382
+ return decrease()
383
+ }
384
+ }
385
+
386
+ function handleFocus(): void {
387
+ focused.value = true
388
+
389
+ // 如果初始值和当前设置的精度不匹配则进行精度修正
390
+ if (model.value === undefined) return
391
+ const { precision, maxPrecision, minPrecision, multiple } = props
392
+
393
+ if (multiple) {
394
+ // 如果存在倍数,先除以倍数得到原始值进行精度修正,再乘以倍数
395
+ const rawVal = $n.div(model.value, multiple)
396
+ const fixedRawVal = +n(rawVal).fixed(precision ?? { maxPrecision, minPrecision })
397
+ model.value = $n.mul(fixedRawVal, multiple)
398
+ } else {
399
+ model.value = +n(model.value).fixed(precision ?? { maxPrecision, minPrecision })
400
+ }
401
+ }
402
+
403
+ function handleBlur(): void {
404
+ focused.value = false
405
+ model.value = props.modelValue
406
+ }
407
+ </script>
@@ -0,0 +1,118 @@
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: number-input;
6
+
7
+ @include m.b($root-name) {
8
+ display: flex;
9
+ align-items: center;
10
+ width: 100%;
11
+ gap: t.use-var('spacing-xs');
12
+
13
+ @include m.e(field) {
14
+ flex: 1;
15
+ min-width: 0;
16
+ }
17
+
18
+ @include m.e(suffix) {
19
+ display: inline-flex;
20
+ // 拉伸让清除按钮撑满输入行高度(行高 ≥44px)
21
+ align-items: stretch;
22
+ height: 100%;
23
+ }
24
+
25
+ @include m.e(suffix-content) {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ }
30
+
31
+ @include m.e(clear) {
32
+ // 清除按钮:宽度 44px、高度撑满输入行内容盒(行高 ≥44px)
33
+ min-width: t.use-var('touch-target');
34
+ align-self: stretch;
35
+ display: inline-flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ border: none;
39
+ margin: 0;
40
+ padding: 0;
41
+ background-color: transparent;
42
+ color: fn.use-var(text-color, second);
43
+ font-size: t.use-var('font-size-main');
44
+ cursor: pointer;
45
+ -webkit-tap-highlight-color: transparent;
46
+
47
+ svg {
48
+ width: 1em;
49
+ height: 1em;
50
+ fill: currentColor;
51
+ }
52
+
53
+ &:active {
54
+ color: fn.use-var(color, primary);
55
+ }
56
+
57
+ @include m.is(hidden) {
58
+ visibility: hidden;
59
+ pointer-events: none;
60
+ }
61
+ }
62
+
63
+ // 步进按钮组:外置于输入框右侧,各 44×44 触控热区
64
+ @include m.e(step) {
65
+ display: flex;
66
+ flex-shrink: 0;
67
+
68
+ button {
69
+ width: t.use-var('touch-target');
70
+ height: t.use-var('touch-target');
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ border: fn.use-var(border-muted);
75
+ background-color: fn.component-var(number-input, step-bg, fn.use-var(bg-color, middle));
76
+ color: fn.use-var(text-color, main);
77
+ font-size: t.use-var('font-size-main');
78
+ cursor: pointer;
79
+ -webkit-tap-highlight-color: transparent;
80
+ transition:
81
+ background-color fn.use-var(transition, fast) fn.use-var(transition, ease),
82
+ color fn.use-var(transition, fast) fn.use-var(transition, ease);
83
+
84
+ svg {
85
+ width: 1em;
86
+ height: 1em;
87
+ fill: currentColor;
88
+ }
89
+
90
+ &:active {
91
+ background-color: fn.use-var(color, primary, light-9);
92
+ color: fn.use-var(color, primary);
93
+ }
94
+
95
+ @include m.is(disabled) {
96
+ cursor: not-allowed;
97
+ color: fn.use-var(text-color, disabled);
98
+
99
+ &:active {
100
+ background-color: fn.component-var(number-input, step-bg, fn.use-var(bg-color, middle));
101
+ }
102
+ }
103
+
104
+ &:first-child {
105
+ border-radius: t.use-var('control-radius-default') 0 0 t.use-var('control-radius-default');
106
+ }
107
+
108
+ &:last-child {
109
+ border-left: none;
110
+ border-radius: 0 t.use-var('control-radius-default') t.use-var('control-radius-default') 0;
111
+ }
112
+
113
+ &:only-child {
114
+ border-radius: t.use-var('control-radius-default');
115
+ }
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,2 @@
1
+ import '../input/style'
2
+ import './style.scss'
@@ -0,0 +1 @@
1
+ export { default as UPasswordInput } from './password-input.vue'
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <u-input
3
+ :class="cls.b"
4
+ :model-value="passwordText"
5
+ v-bind="inputProps"
6
+ :size="size"
7
+ :readonly="readonly"
8
+ :disabled="disabled"
9
+ :clearable="false"
10
+ @native:input="handleUpdatePwd"
11
+ @update:model-value="!$event && handleClear()"
12
+ >
13
+ <template #suffix>
14
+ <span :class="cls.e('suffix')">
15
+ <button
16
+ v-if="showClear"
17
+ :class="cls.e('clear')"
18
+ type="button"
19
+ aria-label="清除"
20
+ @click.stop="handleClear"
21
+ >
22
+ <Close />
23
+ </button>
24
+ <button
25
+ :class="cls.e('visibility-toggle')"
26
+ type="button"
27
+ :aria-label="pwdVisible ? '隐藏密码' : '显示密码'"
28
+ @click.stop="toggleVisible"
29
+ >
30
+ <Hide v-if="pwdVisible" />
31
+ <View v-else />
32
+ </button>
33
+ </span>
34
+ </template>
35
+
36
+ <template #prefix v-if="slots.prefix">
37
+ <slot name="prefix" />
38
+ </template>
39
+ </u-input>
40
+ </template>
41
+
42
+ <script lang="ts" setup>
43
+ import { o } from '@cat-kit/core'
44
+ import { useFormFallbackProps } from '@veltra/compositions'
45
+ import { Close, Hide, View } from '@veltra/icons/normal'
46
+ import { injectFormContext } from '@veltra/utils'
47
+ import { computed, nextTick, shallowRef } from 'vue'
48
+
49
+ import { bem } from '../../shared/bem'
50
+ import type { PasswordInputProps } from '../../types/password-input'
51
+ import { UInput } from '../input'
52
+
53
+ defineOptions({ name: 'UPasswordInput' })
54
+
55
+ const props = withDefaults(defineProps<PasswordInputProps>(), {
56
+ clearable: false,
57
+ disabled: undefined,
58
+ readonly: undefined
59
+ })
60
+
61
+ const slots = defineSlots<{ prefix?: () => any }>()
62
+
63
+ const { formProps } = injectFormContext()
64
+
65
+ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
66
+ size: 'default',
67
+ disabled: false,
68
+ readonly: false
69
+ })
70
+
71
+ const inputProps = computed(() => {
72
+ return o(props as Record<string, any>).pick(['disabled', 'placeholder', 'size'])
73
+ })
74
+
75
+ const cls = bem('password-input')
76
+
77
+ const model = defineModel<string>()
78
+
79
+ // 移动端无 hover,有值即展示清除按钮
80
+ const showClear = computed(() => {
81
+ return props.clearable && !disabled.value && !!model.value
82
+ })
83
+
84
+ const passwordChar = '●'
85
+
86
+ const handleUpdatePwd = (e: Event): void => {
87
+ const target = e.target as HTMLInputElement
88
+ const val = target.value
89
+ if (!val) {
90
+ model.value = val
91
+ return
92
+ }
93
+
94
+ // 字符从无到有
95
+ if (!model.value) {
96
+ model.value = val
97
+ return
98
+ }
99
+
100
+ const pointIndex = target.selectionStart!
101
+ let inputChar = pointIndex === 0 ? '' : val[pointIndex - 1]!
102
+ if (inputChar === passwordChar) inputChar = ''
103
+
104
+ // 光标右侧字符数量(当前字符长度减去光标位置)
105
+ const rightLen = val.length - pointIndex
106
+ // 光标左侧字符数量 (当光标前的字符是刚刚输入的,则为这个字符前的所有的字符,否则为光标前的所有字符)
107
+ const leftLen = inputChar === '' ? pointIndex : pointIndex - 1
108
+
109
+ const leftVal = model.value.slice(0, leftLen)
110
+ const rightVal = rightLen === 0 ? '' : model.value.slice(-rightLen)
111
+
112
+ model.value = leftVal + inputChar + rightVal
113
+
114
+ nextTick(() => {
115
+ target.selectionStart = pointIndex
116
+ target.selectionEnd = pointIndex
117
+ })
118
+ }
119
+
120
+ const handleClear = (): void => {
121
+ model.value = ''
122
+ }
123
+
124
+ const pwdVisible = shallowRef(false)
125
+
126
+ const passwordText = computed<string | undefined>(() => {
127
+ if (pwdVisible.value) return model.value
128
+ return model.value ? passwordChar.repeat(model.value.length) : ''
129
+ })
130
+
131
+ const toggleVisible = () => {
132
+ pwdVisible.value = !pwdVisible.value
133
+ }
134
+ </script>
@@ -0,0 +1,40 @@
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: password-input;
6
+
7
+ @include m.b($root-name) {
8
+ @include m.e(suffix) {
9
+ display: inline-flex;
10
+ align-items: stretch;
11
+ height: 100%;
12
+ }
13
+
14
+ @include m.e(clear, visibility-toggle) {
15
+ // 清除与明文切换按钮:宽度 44px、高度撑满输入行内容盒(行高 ≥44px)
16
+ min-width: t.use-var('touch-target');
17
+ align-self: stretch;
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ border: none;
22
+ margin: 0;
23
+ padding: 0;
24
+ background-color: transparent;
25
+ color: fn.use-var(text-color, second);
26
+ font-size: t.use-var('font-size-main');
27
+ cursor: pointer;
28
+ -webkit-tap-highlight-color: transparent;
29
+
30
+ svg {
31
+ width: 1em;
32
+ height: 1em;
33
+ fill: currentColor;
34
+ }
35
+
36
+ &:active {
37
+ color: fn.use-var(color, primary);
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ import '../input/style'
2
+ import './style.scss'