@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
package/package.json CHANGED
@@ -1,16 +1,42 @@
1
1
  {
2
2
  "name": "@veltra/mobile",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
+ "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
+ "bugs": {
6
+ "url": "https://github.com/cabinet-fe/ultra-ui/issues"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/cabinet-fe/ultra-ui",
11
+ "directory": "packages/mobile"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
4
17
  "type": "module",
18
+ "sideEffects": [
19
+ "src/**/style.ts",
20
+ "src/**/*.scss",
21
+ "dist/**/style.js",
22
+ "dist/**/*.css"
23
+ ],
5
24
  "exports": {
6
25
  ".": {
7
- "import": "./src/index.ts"
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ },
29
+ "./*": {
30
+ "types": "./dist/*",
31
+ "import": "./dist/*"
8
32
  }
9
33
  },
10
- "scripts": {
11
- "check-types": "tsc -p tsconfig.json --noEmit"
12
- },
13
34
  "peerDependencies": {
14
- "vue": "^3.5.0"
35
+ "@cat-kit/core": ">=1.2.1",
36
+ "@veltra/compositions": "^1.8.5",
37
+ "@veltra/icons": "^1.5.0",
38
+ "@veltra/styles": "^1.8.5",
39
+ "@veltra/utils": "^1.8.5",
40
+ "vue": ">=3.5.44"
15
41
  }
16
42
  }
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <Teleport :to="getOverlayContainer()">
3
+ <Transition name="um-sheet" @after-leave="scrollLock.unlock">
4
+ <div v-if="visible" :class="overlayCls" :style="{ zIndex: zIndex() }" @click.self="close">
5
+ <section
6
+ :class="[panelCls, contentClass]"
7
+ :style="contentStyle"
8
+ role="dialog"
9
+ aria-modal="true"
10
+ >
11
+ <header v-if="title || $slots.header" :class="cls.e('header')">
12
+ <slot name="header">
13
+ <span :class="cls.e('title')">{{ title }}</span>
14
+ <button :class="cls.e('close')" type="button" aria-label="关闭" @click="close">
15
+ <Close />
16
+ </button>
17
+ </slot>
18
+ </header>
19
+
20
+ <div :class="cls.e('body')">
21
+ <slot />
22
+ </div>
23
+ </section>
24
+ </div>
25
+ </Transition>
26
+ </Teleport>
27
+ </template>
28
+
29
+ <script lang="ts" setup>
30
+ import { Close } from '@veltra/icons/normal'
31
+ import { zIndex } from '@veltra/utils'
32
+ import { watch } from 'vue'
33
+ import type { CSSProperties } from 'vue'
34
+
35
+ import { bem } from '../../../shared/bem'
36
+ import { getOverlayContainer, useOverlayScrollLock } from '../overlay-container'
37
+
38
+ /**
39
+ * 移动端底部弹层基座(内部组件,不进公开导出)
40
+ *
41
+ * 遮罩 + 底部面板进出场,供 select / multi-select / 后续弹层类组件复用。
42
+ * 面板打开期间锁定背景滚动(body / 设备外壳视口),内容区 `body` 自身可滚,
43
+ * 完全关闭后恢复原滚动位置。
44
+ */
45
+ defineOptions({ name: 'UBottomSheet' })
46
+
47
+ const props = defineProps<{
48
+ /** 是否可见(v-model:visible) */
49
+ visible?: boolean
50
+ /** 头部标题;提供 header 插槽时忽略 */
51
+ title?: string
52
+ /** 面板内容容器样式 */
53
+ contentStyle?: CSSProperties | string
54
+ /** 面板内容容器类名 */
55
+ contentClass?: unknown
56
+ }>()
57
+
58
+ const emit = defineEmits<{ (e: 'update:visible', visible: boolean): void }>()
59
+
60
+ const cls = bem('sheet')
61
+
62
+ const overlayCls = cls.b
63
+ const panelCls = cls.e('panel')
64
+
65
+ function close() {
66
+ emit('update:visible', false)
67
+ }
68
+
69
+ const scrollLock = useOverlayScrollLock()
70
+
71
+ watch(
72
+ () => props.visible,
73
+ (visible) => {
74
+ if (visible) {
75
+ scrollLock.lock()
76
+ }
77
+ // 解锁挂在退场过渡 after-leave:动画期间背景保持锁定
78
+ },
79
+ { immediate: true }
80
+ )
81
+ </script>
@@ -0,0 +1 @@
1
+ export { default as BottomSheet } from './bottom-sheet.vue'
@@ -0,0 +1,116 @@
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: sheet;
8
+
9
+ @include m.b($root-name) {
10
+ position: fixed;
11
+ inset: 0;
12
+ display: flex;
13
+ align-items: flex-end;
14
+ justify-content: center;
15
+ // 遮罩点击热区由根元素自身承担,面板区域不透传
16
+ background-color: fn.color-a(shadow, 52, color);
17
+
18
+ @include m.e(panel) {
19
+ width: 100%;
20
+ max-width: 100%;
21
+ box-sizing: border-box;
22
+ display: flex;
23
+ flex-direction: column;
24
+ max-height: 75%;
25
+ flex-shrink: 0;
26
+ border-radius: t.use-var('panel-radius') t.use-var('panel-radius') 0 0;
27
+ background-color: fn.use-var(bg-color, top);
28
+ box-shadow: fn.use-var(shadow, lg);
29
+ // 底部安全区(全面屏手势条),内容不与系统区域重叠
30
+ padding-bottom: constant(safe-area-inset-bottom);
31
+ padding-bottom: env(safe-area-inset-bottom);
32
+ -webkit-tap-highlight-color: transparent;
33
+ }
34
+
35
+ @include m.e(header) {
36
+ // 关闭钮绝对定位后,标题占满整行宽度,文字才能真正水平居中
37
+ position: relative;
38
+ flex-shrink: 0;
39
+ display: flex;
40
+ align-items: center;
41
+ gap: fn.use-var(gap, default);
42
+ min-height: t.use-var('form-row-height');
43
+ padding: fn.use-var(gap, small) fn.use-var(gap, default) 0;
44
+ }
45
+
46
+ @include m.e(title) {
47
+ flex: 1;
48
+ min-width: 0;
49
+ font-size: t.use-var('font-size-title');
50
+ font-weight: 600;
51
+ color: fn.use-var(text-color, title);
52
+ text-align: center;
53
+ @include m.ellipsis;
54
+ }
55
+
56
+ @include m.e(close) {
57
+ // 头部操作热区 ≥44×44;不参与文档流,避免挤压标题的居中基准
58
+ position: absolute;
59
+ right: fn.use-var(gap, small);
60
+ top: 50%;
61
+ transform: translateY(-50%);
62
+ min-width: t.use-var('touch-target');
63
+ min-height: t.use-var('touch-target');
64
+ padding: 0;
65
+ display: inline-flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ border: none;
69
+ background: none;
70
+ // 关闭图标走标题档字号,与面板标题同级
71
+ font-size: t.use-var('font-size-title');
72
+ color: fn.use-var(text-color, main);
73
+ cursor: pointer;
74
+
75
+ svg {
76
+ width: 1em;
77
+ height: 1em;
78
+ }
79
+
80
+ &:active {
81
+ color: fn.use-var(text-color, title);
82
+ }
83
+ }
84
+
85
+ @include m.e(body) {
86
+ flex: 1;
87
+ min-height: 0;
88
+ overflow-y: auto;
89
+ -webkit-overflow-scrolling: touch;
90
+ overscroll-behavior: contain;
91
+ padding: fn.use-var(gap, small) fn.use-var(gap, default) fn.use-var(gap, large);
92
+ }
93
+ }
94
+
95
+ // 进出场:遮罩淡入淡出,面板自底部滑入滑出
96
+ .um-sheet-enter-active,
97
+ .um-sheet-leave-active {
98
+ transition: opacity fn.use-var(transition, normal) fn.use-var(transition, ease);
99
+
100
+ @include m.bem($root-name, panel) {
101
+ transition:
102
+ transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
103
+ border-radius 0.3s cubic-bezier(0.22, 1, 0.36, 1);
104
+ }
105
+ }
106
+
107
+ .um-sheet-enter-from,
108
+ .um-sheet-leave-to {
109
+ opacity: 0;
110
+
111
+ @include m.bem($root-name, panel) {
112
+ transform: translateY(100%);
113
+ border-top-left-radius: 0;
114
+ border-top-right-radius: 0;
115
+ }
116
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,34 @@
1
+ import { createScrollLock } from '../../shared/use-scroll-lock'
2
+ import type { ScrollLock } from '../../shared/use-scroll-lock'
3
+
4
+ /**
5
+ * 弹层宿主容器(内部机制,不进公开导出)
6
+ *
7
+ * 设备外壳等嵌入场景把承载元素注册进来,底部弹层 / 对话框 / 抽屉 / 函数式消息
8
+ * 改挂到该元素内,随容器一起定位与缩放;未注册时行为不变(挂 body)。
9
+ *
10
+ * `scrollLockEl` 是背景滚动锁定的目标:挂载层自身通常不滚动(弹层不随内容
11
+ * 滚动),真正滚动的是与其并列的内容视口,注册时需一并传入;缺省回落到
12
+ * 挂载元素(未注册时即 body)。
13
+ */
14
+ let container: HTMLElement | undefined
15
+ let scrollLockEl: HTMLElement | undefined
16
+
17
+ export function setOverlayContainer(el: HTMLElement | undefined, scrollEl?: HTMLElement): void {
18
+ container = el
19
+ scrollLockEl = scrollEl
20
+ }
21
+
22
+ export function getOverlayContainer(): HTMLElement {
23
+ return container ?? document.body
24
+ }
25
+
26
+ /**
27
+ * 弹层背景滚动锁(Dialog / Drawer / BottomSheet 共用)
28
+ *
29
+ * 打开期间锁定(未注册宿主时锁 body,注册设备外壳时锁其内容视口),
30
+ * 完全关闭后由组件调用 `unlock()` 还原滚动位置;嵌套弹层计数叠加。
31
+ */
32
+ export function useOverlayScrollLock(): ScrollLock {
33
+ return createScrollLock(() => scrollLockEl ?? getOverlayContainer())
34
+ }
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <span :class="classList">
3
+ <img v-if="src && !errored" :class="cls.e('image')" :src :alt @error="handleError" />
4
+ <span v-else :class="cls.e('fallback')">
5
+ <slot />
6
+ </span>
7
+ </span>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ import { computed, shallowRef, watch } from 'vue'
12
+
13
+ import { bem } from '../../shared/bem'
14
+ import type { AvatarEmits, AvatarProps } from '../../types/avatar'
15
+
16
+ defineOptions({ name: 'UAvatar' })
17
+
18
+ const { src, alt, shape, size } = defineProps<AvatarProps>()
19
+
20
+ const emit = defineEmits<AvatarEmits>()
21
+
22
+ const cls = bem('avatar')
23
+
24
+ const errored = shallowRef(false)
25
+
26
+ // 切换 src 时重置错误态,让新地址的图片重新尝试加载
27
+ watch(
28
+ () => src,
29
+ () => {
30
+ errored.value = false
31
+ }
32
+ )
33
+
34
+ const classList = computed(() => [
35
+ cls.b,
36
+ cls.m(size ?? 'default'),
37
+ bem.is('round', shape === 'round')
38
+ ])
39
+
40
+ function handleError(ev: Event) {
41
+ errored.value = true
42
+ emit('error', ev)
43
+ }
44
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UAvatar } from './avatar.vue'
@@ -0,0 +1,53 @@
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 '../../styles/tokens' as t;
5
+
6
+ $root-name: avatar;
7
+
8
+ // 尺寸档位走移动端密度 token(32 / 40 / 48),不再沿用桌面 24/32/40 表单控件高度
9
+ $size-tier: (
10
+ small: avatar-size-small,
11
+ default: avatar-size-default,
12
+ large: avatar-size-large
13
+ );
14
+
15
+ @include m.b($root-name) {
16
+ display: inline-flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ flex-shrink: 0;
20
+ box-sizing: border-box;
21
+ overflow: hidden;
22
+ border-radius: 50%;
23
+ color: fn.use-var(text-color, white);
24
+ background-color: fn.use-var(color, primary);
25
+ user-select: none;
26
+
27
+ @include m.size using ($size) {
28
+ $side: t.use-var(map.get($size-tier, $size));
29
+
30
+ width: $side;
31
+ height: $side;
32
+ // 回退文字对齐主字号体系,各档同字号,层级交给尺寸本身
33
+ font-size: t.use-var(font-size-main);
34
+ }
35
+
36
+ @include m.is(round) {
37
+ border-radius: t.control-radius(large);
38
+ }
39
+
40
+ @include m.e(image) {
41
+ width: 100%;
42
+ height: 100%;
43
+ object-fit: cover;
44
+ }
45
+
46
+ @include m.e(fallback) {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ line-height: 1;
51
+ white-space: nowrap;
52
+ }
53
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <div :class="cls.b">
3
+ <slot />
4
+ <sup v-if="!hidden" ref="supRef" :class="classList">{{ badgeValue }}</sup>
5
+ </div>
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import { setStyles, zIndex } from '@veltra/utils'
10
+ import { computed, nextTick, shallowRef, watch } from 'vue'
11
+
12
+ import { bem } from '../../shared/bem'
13
+ import type { BadgeProps } from '../../types/badge'
14
+
15
+ defineOptions({ name: 'UBadge' })
16
+
17
+ const cls = bem('badge')
18
+
19
+ const props = withDefaults(defineProps<BadgeProps>(), { max: 99 })
20
+
21
+ const badgeValue = computed(() => {
22
+ if (props.dot) return ''
23
+ if (typeof props.value === 'number' && typeof props.max === 'number') {
24
+ return props.value > props.max ? `${props.max}+` : props.value
25
+ }
26
+ return props.value
27
+ })
28
+
29
+ const classList = computed(() => {
30
+ return [
31
+ cls.e('sup'),
32
+ props.type && cls.m('color-' + props.type),
33
+ cls.m(props.size ?? 'default'),
34
+ bem.is('dot', props.dot)
35
+ ]
36
+ })
37
+
38
+ const supRef = shallowRef<HTMLElement>()
39
+
40
+ // 角标向内容末端偏移半个自身,实现悬浮角标形态;color/zIndex 需运行时写入
41
+ const setPosition = () => {
42
+ if (supRef.value) {
43
+ const { width, height } = supRef.value.getBoundingClientRect()
44
+ setStyles(supRef.value, {
45
+ transform: `translate(-${width / 2}px, -${height / 2}px)`,
46
+ backgroundColor: props.color,
47
+ zIndex: zIndex()
48
+ })
49
+ }
50
+ }
51
+
52
+ // sup 因 hidden 切换而销毁重建、或 size/dot 改变自身尺寸时,重新写入偏移
53
+ watch([supRef, () => props.size, () => props.dot], ([el]) => {
54
+ if (!el) return
55
+ nextTick(() => {
56
+ setPosition()
57
+ })
58
+ })
59
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UBadge } from './badge.vue'
@@ -0,0 +1,62 @@
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: badge;
8
+
9
+ // 密度走 --um-*:小档辅助字号 / 默认档次字号 / 大档主字号
10
+ $size-font: (
11
+ small: font-size-auxiliary,
12
+ default: font-size-secondary,
13
+ large: font-size-main
14
+ );
15
+
16
+ // 修饰符类(u-badge--color-* / u-badge--<size>)由组件挂在 sup 元素上,
17
+ // 因此以下规则直接作用于 sup 本身,与 desktop badge 选择器结构一致
18
+ @include m.b($root-name) {
19
+ width: max-content;
20
+ position: relative;
21
+
22
+ @include m.e(sup) {
23
+ position: absolute;
24
+ cursor: default;
25
+ display: inline-flex;
26
+ justify-content: center;
27
+ align-items: center;
28
+ // sup 不在全局 border-box 名单内,需自带,min-width 才能含左右 padding 计入宽度
29
+ box-sizing: border-box;
30
+ background-color: fn.use-var(color, danger);
31
+ // 胶囊圆角:远大于自身高度,保证两端全圆
32
+ border-radius: 999px;
33
+ color: fn.use-var(text-color, white);
34
+
35
+ @include m.is(dot) {
36
+ height: t.use-var(spacing-sm);
37
+ width: t.use-var(spacing-sm);
38
+ // 尺寸档的 min-width 会让圆点撑成椭圆,圆点固定宽高需归零
39
+ min-width: 0;
40
+ padding: 0;
41
+ border-radius: 50%;
42
+ }
43
+ }
44
+
45
+ @each $type, $_ in vars.$color-types {
46
+ @include m.m(color-#{$type}) {
47
+ background-color: fn.use-var(color, $type, light-7);
48
+ color: fn.use-var(color, $type);
49
+ }
50
+ }
51
+
52
+ @include m.size using ($size) {
53
+ $font: t.use-var(map.get($size-font, $size));
54
+
55
+ font-size: $font;
56
+ // 高度随字号伸缩:字号 + 上下合计 8px 呼吸空间
57
+ height: calc(#{$font} + #{t.use-var(spacing-sm)});
58
+ // 个位数角标呈正圆:最小宽度等于高度;多位数向两侧撑开为胶囊
59
+ min-width: calc(#{$font} + #{t.use-var(spacing-sm)});
60
+ padding: 0 t.use-var(spacing-xs);
61
+ }
62
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <button :class="classList" type="button" ref="buttonRef" @click="handleClick">
3
+ <!-- 加载图标 -->
4
+ <span v-if="loading" :class="[iconCls, bem.is('loading')]" :style="iconStyle">
5
+ <component :is="loadingIcon" />
6
+ </span>
7
+
8
+ <!-- 左侧图标 -->
9
+ <span v-else-if="!!icon && iconPosition === 'left'" :class="iconCls" :style="iconStyle">
10
+ <component :is="icon" />
11
+ </span>
12
+
13
+ <slot />
14
+
15
+ <!-- 右侧图标 -->
16
+ <span v-if="!!icon && iconPosition === 'right'" :class="iconCls" :style="iconStyle">
17
+ <component :is="icon" />
18
+ </span>
19
+ </button>
20
+ </template>
21
+
22
+ <script lang="ts" setup>
23
+ import { Loading } from '@veltra/icons'
24
+ import { withUnit } from '@veltra/utils'
25
+ import { computed, shallowRef } from 'vue'
26
+
27
+ import { bem } from '../../shared/bem'
28
+ import type { ButtonEmits, ButtonProps, _ButtonExposed } from '../../types/button'
29
+
30
+ defineOptions({ name: 'UButton' })
31
+
32
+ const props = withDefaults(defineProps<ButtonProps>(), {
33
+ iconPosition: 'left',
34
+ loadingIcon: () => Loading,
35
+ disabled: false,
36
+ propagate: true
37
+ })
38
+
39
+ const emit = defineEmits<ButtonEmits>()
40
+
41
+ const cls = bem('button')
42
+
43
+ const classList = computed(() => {
44
+ return [
45
+ cls.b,
46
+ cls.m(props.size ?? 'default'),
47
+ props.type && cls.m('color-' + props.type),
48
+ bem.is('circle', props.circle),
49
+ bem.is('disabled', props.disabled),
50
+ bem.is('loading', props.loading),
51
+ bem.is('plain', props.plain),
52
+ bem.is('text', props.text)
53
+ ]
54
+ })
55
+
56
+ const iconCls = cls.e('icon')
57
+
58
+ const iconStyle = computed(() => ({ fontSize: withUnit(props.iconSize, 'px') }))
59
+
60
+ const handleClick = (e: MouseEvent) => {
61
+ if (props.disabled || props.loading) {
62
+ e.stopPropagation()
63
+ return
64
+ }
65
+
66
+ !props.propagate && e.stopPropagation()
67
+
68
+ emit('click', e)
69
+ }
70
+
71
+ const buttonRef = shallowRef<HTMLButtonElement>()
72
+
73
+ const exposed: _ButtonExposed = { el: buttonRef }
74
+
75
+ defineExpose(exposed)
76
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UButton } from './button.vue'