@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,114 @@
1
+ @use 'sass:map';
2
+ @use 'pkg:@veltra/styles/mixins' as m with ($namespace: um);
3
+ @use 'pkg:@veltra/styles/functions' as fn;
4
+ @use 'pkg:@veltra/styles/vars';
5
+ @use '../../styles/tokens' as t;
6
+
7
+ $root-name: tag;
8
+
9
+ // 密度走 --um-*:小档辅助字号 / 默认档次字号 / 大档主字号;高度 = 字号 × 2(24 / 28 / 32)
10
+ $size-font: (
11
+ small: font-size-auxiliary,
12
+ default: font-size-secondary,
13
+ large: font-size-main
14
+ );
15
+
16
+ $size-padding: (
17
+ small: spacing-xs,
18
+ default: spacing-sm,
19
+ large: spacing-md
20
+ );
21
+
22
+ // 进入缩放动画(与 desktop tag 一致)
23
+ @keyframes um-tag-zoom-in {
24
+ 0% {
25
+ opacity: 0;
26
+ transform: scale(0.96);
27
+ }
28
+
29
+ 100% {
30
+ opacity: 1;
31
+ transform: scale(1);
32
+ }
33
+ }
34
+
35
+ @include m.b($root-name) {
36
+ cursor: default;
37
+ color: fn.use-var(text-color, main);
38
+ background-color: fn.use-var(color, default);
39
+ min-width: 0;
40
+ border: 1px solid fn.use-var(text-color, main);
41
+ @include m.flex(inline-flex, flex-start, center);
42
+
43
+ animation: um-tag-zoom-in 0.25s cubic-bezier(0.23, 1, 0.32, 1);
44
+
45
+ @include m.e(content) {
46
+ @include m.ellipsis;
47
+ flex-grow: 1;
48
+ }
49
+
50
+ // 关闭图标:视觉尺寸跟随字号,触控热区经 ::after 扩到 44×44(不撑破标签布局)
51
+ @include m.e(icon-close) {
52
+ position: relative;
53
+ cursor: pointer;
54
+ flex-shrink: 0;
55
+
56
+ &::after {
57
+ content: '';
58
+ position: absolute;
59
+ left: 50%;
60
+ top: 50%;
61
+ width: t.use-var(touch-target);
62
+ height: t.use-var(touch-target);
63
+ transform: translate(-50%, -50%);
64
+ }
65
+ }
66
+
67
+ @include m.size using ($size) {
68
+ $font: t.use-var(map.get($size-font, $size));
69
+
70
+ height: calc(#{$font} * 2);
71
+ font-size: $font;
72
+ padding: 0 t.use-var(map.get($size-padding, $size));
73
+ border-radius: t.control-radius($size);
74
+
75
+ @include m.bem($root-name, icon-close) {
76
+ margin-left: calc(#{t.use-var(spacing-xs)} / 2);
77
+
78
+ // 视觉尺寸跟随标签档位字号(覆盖 UIcon 的 16px 默认字号)
79
+ &.um-icon {
80
+ font-size: $font;
81
+ }
82
+ }
83
+
84
+ @include m.is(round) {
85
+ // 圆角半径 = 高度的一半 = 字号
86
+ border-radius: $font;
87
+ }
88
+ }
89
+
90
+ @include m.is(dark) {
91
+ border-color: fn.use-var(color, default);
92
+ }
93
+
94
+ // 颜色样式
95
+ @each $type, $_ in vars.$color-types {
96
+ @include m.m(color-#{$type}) {
97
+ color: fn.component-var(tag, #{$type}-color, fn.use-var(color, $type));
98
+ border-color: fn.component-var(tag, #{$type}-border, fn.use-var(color, $type));
99
+ background-color: fn.component-var(tag, #{$type}-bg, fn.use-var(color, $type, light-9));
100
+
101
+ @include m.is(dark) {
102
+ color: fn.component-var(tag, #{$type}-dark-color, fn.use-var(text-color, white));
103
+ background-color: fn.component-var(tag, #{$type}-dark-bg, fn.use-var(color, $type));
104
+ }
105
+
106
+ // 移动端无 hover,按压时给关闭图标触控反馈
107
+ @include m.bem($root-name, icon-close) {
108
+ &:active {
109
+ color: fn.use-var(color, $type);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <span :class="className">
3
+ <span :class="cls.e('content')">
4
+ <slot />
5
+ </span>
6
+
7
+ <UIcon v-if="closable" :class="cls.e('icon-close')" @click.stop="handleClose">
8
+ <Close />
9
+ </UIcon>
10
+ </span>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import { Close } from '@veltra/icons/normal'
15
+ import { injectFormContext } from '@veltra/utils'
16
+ import { computed } from 'vue'
17
+
18
+ import { bem } from '../../shared/bem'
19
+ import type { TagEmits, TagProps } from '../../types/tag'
20
+ import { UIcon } from '../icon'
21
+
22
+ defineOptions({ name: 'UTag' })
23
+
24
+ const cls = bem('tag')
25
+
26
+ const props = defineProps<TagProps>()
27
+
28
+ const emit = defineEmits<TagEmits>()
29
+
30
+ // 表单内继承表单尺寸,与 desktop tag 行为一致
31
+ const { formProps } = injectFormContext()
32
+
33
+ const size = computed(() => props.size ?? formProps?.size ?? 'default')
34
+
35
+ const handleClose = () => {
36
+ emit('close')
37
+ }
38
+
39
+ const className = computed(() => {
40
+ const { type } = props
41
+ return [
42
+ cls.b,
43
+ cls.m(size.value),
44
+ type && cls.m('color-' + type),
45
+ bem.is('round', props.round),
46
+ bem.is('dark', props.dark)
47
+ ]
48
+ })
49
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UText } from './text.vue'
@@ -0,0 +1,63 @@
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
+ // 角色字号走 --um-*:正文(content)主字号 16px,标题层级靠字重与颜色区分(移动端惯例),
6
+ // 辅助说明降为 12px
7
+ $role-size: (
8
+ main-title: (
9
+ font-size: calc(#{t.use-var(font-size-main)} * 1.25),
10
+ font-weight: 700,
11
+ color: fn.use-var(text-color, title)
12
+ ),
13
+ title: (
14
+ font-size: t.use-var(font-size-main),
15
+ font-weight: 600,
16
+ color: fn.use-var(text-color, main)
17
+ ),
18
+ sub-title: (
19
+ font-size: t.use-var(font-size-main),
20
+ font-weight: 500,
21
+ color: fn.use-var(text-color, placeholder)
22
+ ),
23
+ content: (
24
+ font-size: t.use-var(font-size-main),
25
+ font-weight: 400,
26
+ color: fn.use-var(text-color, second)
27
+ ),
28
+ additional: (
29
+ font-size: t.use-var(font-size-auxiliary),
30
+ font-weight: 400,
31
+ color: fn.use-var(text-color, assist)
32
+ )
33
+ );
34
+
35
+ @include m.b(text) {
36
+ margin: 0;
37
+ // 移动端阅读密度:行高比桌面(1.3)略放宽
38
+ line-height: 1.5;
39
+
40
+ @each $role, $styles in $role-size {
41
+ @include m.is(#{$role}) {
42
+ @each $prop, $val in $styles {
43
+ #{$prop}: $val;
44
+ }
45
+ }
46
+ }
47
+
48
+ @include m.is(italic) {
49
+ font-style: italic;
50
+ }
51
+
52
+ @include m.is(bold) {
53
+ font-weight: bold;
54
+ }
55
+
56
+ // 高亮:去掉浏览器默认黄底,改走主题 warning 色
57
+ mark {
58
+ padding: 0;
59
+ background-color: transparent;
60
+ color: fn.use-var(color, warning);
61
+ font-weight: 600;
62
+ }
63
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <p :class="classList" :style="style">
3
+ <template v-for="(chunk, i) of chunks" :key="i">
4
+ <mark v-if="chunk.highlight">{{ chunk.text }}</mark>
5
+ <template v-else>{{ chunk.text }}</template>
6
+ </template>
7
+ </p>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ import { getHighlightChunks, isTextNode, withUnit } from '@veltra/utils'
12
+ import { type CSSProperties, computed, useSlots } from 'vue'
13
+
14
+ import { bem } from '../../shared/bem'
15
+ import type { TextProps } from '../../types/text'
16
+
17
+ // Text 是 Vue 保留组件名(vue/no-reserved-component-names),与 desktop 一致带 U 前缀
18
+ defineOptions({ name: 'UText' })
19
+
20
+ const {
21
+ as = 'content',
22
+ fontSize,
23
+ deleted,
24
+ underline,
25
+ bold,
26
+ italic,
27
+ highlight
28
+ } = defineProps<TextProps>()
29
+
30
+ const cls = bem('text')
31
+
32
+ const classList = computed(() => {
33
+ return [cls.b, bem.is(as), bem.is('bold', bold), bem.is('italic', italic)]
34
+ })
35
+
36
+ const style = computed<CSSProperties>(() => {
37
+ const style: CSSProperties = { fontSize: withUnit(fontSize, 'px') }
38
+
39
+ if (deleted) {
40
+ style.textDecoration = 'line-through'
41
+ }
42
+ if (underline) {
43
+ style.textDecoration = 'underline'
44
+ }
45
+
46
+ return style
47
+ })
48
+
49
+ const slots = useSlots()
50
+
51
+ /** 只渲染文本节点;传 highlight 时按关键词切分为高亮块 */
52
+ const chunks = computed(() => {
53
+ const text = (slots.default?.() ?? [])
54
+ .filter(isTextNode)
55
+ .map((node) => String(node.children))
56
+ .join('')
57
+
58
+ if (!text) return []
59
+ if (!highlight) return [{ text }]
60
+
61
+ return getHighlightChunks(text, [highlight].flat())
62
+ })
63
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UTextarea } from './textarea.vue'
@@ -0,0 +1,124 @@
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: textarea;
6
+
7
+ @include m.b($root-name) {
8
+ position: relative;
9
+ box-sizing: border-box;
10
+ width: 100%;
11
+ border: fn.use-var(border-muted);
12
+ box-shadow: fn.use-var(shadow, emboss);
13
+ background-color: fn.component-var(textarea, 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
+ vertical-align: middle;
19
+
20
+ @include m.size using ($size) {
21
+ border-radius: t.control-radius($size);
22
+
23
+ @include m.bem($root-name, native) {
24
+ // 行高、内边距走移动端 token;右侧让位计数/清除 44px 热区
25
+ padding: t.spacing($size) t.use-var('touch-target') t.spacing($size) t.spacing($size);
26
+ // 触控热区:文本域高度不小于 44px;字号 16px(iOS 聚焦防视口缩放)
27
+ min-height: max(t.control-height($size), t.use-var('touch-target'));
28
+ font-size: t.use-var('font-size-main');
29
+ line-height: t.use-var('text-line-height');
30
+ }
31
+ }
32
+
33
+ @include m.e(native) {
34
+ display: block;
35
+ box-sizing: border-box;
36
+ color: fn.use-var(text-color, main);
37
+ outline: none;
38
+ border: none;
39
+ background-color: transparent;
40
+ font-family: fn.use-var(font-family);
41
+ width: 100%;
42
+ padding: 0;
43
+ // 触屏没有拖拽手柄,一律不缩放(resize prop 仅保留 API 兼容)
44
+ resize: none;
45
+ overflow: auto;
46
+ -webkit-tap-highlight-color: transparent;
47
+
48
+ &::placeholder {
49
+ color: fn.use-var(text-color, placeholder);
50
+ }
51
+
52
+ &:disabled {
53
+ opacity: 1;
54
+ -webkit-text-fill-color: fn.use-var(text-color, disabled);
55
+ }
56
+ }
57
+
58
+ // 聚焦态:移动端高亮惯例(主色描边 + 焦点环)
59
+ @include m.is(focus) {
60
+ border-color: fn.use-var(color, primary);
61
+ box-shadow: fn.use-var(focus-ring);
62
+ }
63
+
64
+ @include m.is(disabled) {
65
+ background-color: fn.use-var(color, disabled);
66
+ box-shadow: none;
67
+
68
+ @include m.bem($root-name, native) {
69
+ cursor: not-allowed;
70
+ }
71
+ }
72
+
73
+ @include m.m(count) {
74
+ position: absolute;
75
+ right: 0;
76
+ bottom: 0;
77
+ // 计数为展示元素:占位 44px 热区、不拦截触摸
78
+ min-width: t.use-var('touch-target');
79
+ min-height: t.use-var('touch-target');
80
+ display: inline-flex;
81
+ align-items: flex-end;
82
+ justify-content: flex-end;
83
+ padding: 0 t.use-var('spacing-sm') t.use-var('spacing-xs') 0;
84
+ font-size: t.use-var('font-size-secondary');
85
+ color: fn.use-var(text-color, placeholder);
86
+ pointer-events: none;
87
+ }
88
+
89
+ @include m.m(clear) {
90
+ // 清空按钮固定右上角,44×44 触控热区
91
+ position: absolute;
92
+ top: 0;
93
+ right: 0;
94
+ width: t.use-var('touch-target');
95
+ height: t.use-var('touch-target');
96
+ display: inline-flex;
97
+ align-items: center;
98
+ justify-content: center;
99
+ border: none;
100
+ margin: 0;
101
+ padding: 0;
102
+ background-color: transparent;
103
+ color: fn.use-var(text-color, second);
104
+ font-size: t.use-var('font-size-main');
105
+ cursor: pointer;
106
+ -webkit-tap-highlight-color: transparent;
107
+
108
+ svg {
109
+ width: 1em;
110
+ height: 1em;
111
+ fill: currentColor;
112
+ }
113
+
114
+ &:active {
115
+ color: fn.use-var(color, primary);
116
+ }
117
+ }
118
+
119
+ @include m.m(readonly) {
120
+ display: inline-block;
121
+ white-space: pre-wrap;
122
+ color: fn.use-var(text-color, main);
123
+ }
124
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div v-if="!readonly" :class="textareaClass">
3
+ <textarea
4
+ :class="cls.e('native')"
5
+ :placeholder="placeholder"
6
+ v-model="model"
7
+ :maxlength="maxlength"
8
+ :rows="rows"
9
+ :cols="cols"
10
+ @input="handleInput"
11
+ @focus="handleFocus"
12
+ @blur="handleBlur"
13
+ @change="handleChange"
14
+ :disabled="disabled"
15
+ :readonly="nativeReadonly"
16
+ ref="textareaRef"
17
+ />
18
+ <span v-if="props.maxlength && props.showCount" :class="cls.m('count')">
19
+ {{ model?.length ?? 0 }}/{{ props.maxlength }}
20
+ </span>
21
+ <button
22
+ v-if="props.clearable && model && !disabled"
23
+ :class="cls.m('clear')"
24
+ type="button"
25
+ aria-label="清空"
26
+ @click.stop="handleClear"
27
+ >
28
+ <Close />
29
+ </button>
30
+ </div>
31
+
32
+ <span v-else :class="cls.m('readonly')">
33
+ {{ model || FORM_EMPTY_CONTENT }}
34
+ </span>
35
+ </template>
36
+
37
+ <script lang="ts" setup>
38
+ import { useFocus, useFormFallbackProps } from '@veltra/compositions'
39
+ import { Close } from '@veltra/icons/normal'
40
+ import { FORM_EMPTY_CONTENT, injectFormContext } from '@veltra/utils'
41
+ import type { ComponentSize } from '@veltra/utils'
42
+ import { computed, ref, watch } from 'vue'
43
+
44
+ import { bem } from '../../shared/bem'
45
+ import type { TextareaEmits, TextareaProps } from '../../types/textarea'
46
+ import { calcTextareaHeight } from './utils'
47
+
48
+ defineOptions({ name: 'UTextarea' })
49
+
50
+ const props = withDefaults(defineProps<TextareaProps>(), {
51
+ placeholder: '请输入',
52
+ resize: true,
53
+ clearable: true,
54
+ disabled: undefined,
55
+ readonly: undefined,
56
+ autosize: false
57
+ })
58
+
59
+ const cls = bem('textarea')
60
+
61
+ const { formProps } = injectFormContext()
62
+
63
+ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props], {
64
+ size: 'default' as ComponentSize,
65
+ disabled: false,
66
+ readonly: false
67
+ })
68
+
69
+ const emit = defineEmits<TextareaEmits>()
70
+
71
+ const textareaRef = ref<HTMLTextAreaElement | null>(null)
72
+
73
+ const model = defineModel<string>()
74
+
75
+ const { focus, handleBlur, handleFocus } = useFocus((focused) => {
76
+ focused ? emit('focus') : emit('blur')
77
+ })
78
+
79
+ const textareaClass = computed(() => {
80
+ return [
81
+ cls.b,
82
+ cls.m(size.value),
83
+ bem.is('resize-none', !props.resize),
84
+ bem.is('disabled', disabled.value),
85
+ bem.is('readonly', readonly.value),
86
+ bem.is('focus', focus.value),
87
+ cls.m('more')
88
+ ]
89
+ })
90
+
91
+ const handleInput = (e: Event) => {
92
+ const value = (e.target as HTMLTextAreaElement).value
93
+ if (value.length > props.maxlength!) {
94
+ // 如果输入的字符数超过了最大长度,截取字符串到最大长度
95
+ const truncatedValue = value.slice(0, props.maxlength)
96
+ emit('update:modelValue', truncatedValue)
97
+ } else {
98
+ // 如果没有超过最大长度,则正常更新模型值
99
+ emit('update:modelValue', value)
100
+ }
101
+ }
102
+
103
+ const handleClear = () => {
104
+ model.value = ''
105
+ emit('clear')
106
+ }
107
+
108
+ const handleChange = (e: Event) => {
109
+ emit('change', (e.target as HTMLTextAreaElement).value)
110
+ }
111
+
112
+ watch(
113
+ [model, textareaRef],
114
+ ([model, textareaRef]) => {
115
+ if (!textareaRef) return
116
+ if (!props.autosize) return
117
+ calcTextareaHeight(textareaRef)
118
+ },
119
+ { immediate: true }
120
+ )
121
+ </script>
@@ -0,0 +1,11 @@
1
+ import { setStyles } from '@veltra/utils'
2
+ import { nextTick } from 'vue'
3
+
4
+ export function calcTextareaHeight(el: HTMLTextAreaElement): void {
5
+ setStyles(el, { height: 'auto' })
6
+ nextTick(() => {
7
+ const { scrollHeight } = el
8
+
9
+ setStyles(el, { overflow: 'hidden', height: scrollHeight + 'px' })
10
+ })
11
+ }
@@ -0,0 +1 @@
1
+ export { default as UTimePicker } from './time-picker.vue'