@atooyu/uxto-ui 1.0.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 (141) hide show
  1. package/README.md +259 -0
  2. package/dist/index.js +5055 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/index.mjs +5055 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/dist/style.css +2528 -0
  7. package/package.json +93 -0
  8. package/src/components/index.ts +51 -0
  9. package/src/components/u-avatar/u-avatar.vue +205 -0
  10. package/src/components/u-badge/u-badge.vue +145 -0
  11. package/src/components/u-button/u-button.vue +239 -0
  12. package/src/components/u-cell/u-cell.vue +179 -0
  13. package/src/components/u-cell-group/u-cell-group.vue +46 -0
  14. package/src/components/u-checkbox/u-checkbox.vue +174 -0
  15. package/src/components/u-checkbox-group/u-checkbox-group.vue +72 -0
  16. package/src/components/u-code-input/u-code-input.vue +248 -0
  17. package/src/components/u-count-down/u-count-down.vue +182 -0
  18. package/src/components/u-datetime-picker/u-datetime-picker.vue +377 -0
  19. package/src/components/u-divider/u-divider.vue +71 -0
  20. package/src/components/u-empty/u-empty.vue +98 -0
  21. package/src/components/u-grid/u-grid.vue +63 -0
  22. package/src/components/u-grid-item/u-grid-item.vue +170 -0
  23. package/src/components/u-icon/icons/account.svg +3 -0
  24. package/src/components/u-icon/icons/arrow-down.svg +3 -0
  25. package/src/components/u-icon/icons/arrow-left.svg +3 -0
  26. package/src/components/u-icon/icons/arrow-right.svg +3 -0
  27. package/src/components/u-icon/icons/arrow-up.svg +3 -0
  28. package/src/components/u-icon/icons/bell.svg +3 -0
  29. package/src/components/u-icon/icons/bookmark-o.svg +3 -0
  30. package/src/components/u-icon/icons/bookmark.svg +3 -0
  31. package/src/components/u-icon/icons/chat.svg +3 -0
  32. package/src/components/u-icon/icons/check-circle.svg +3 -0
  33. package/src/components/u-icon/icons/check.svg +3 -0
  34. package/src/components/u-icon/icons/chevron-left.svg +3 -0
  35. package/src/components/u-icon/icons/chevron-right.svg +3 -0
  36. package/src/components/u-icon/icons/clear-o.svg +3 -0
  37. package/src/components/u-icon/icons/clear.svg +3 -0
  38. package/src/components/u-icon/icons/clipboard.svg +3 -0
  39. package/src/components/u-icon/icons/clock.svg +3 -0
  40. package/src/components/u-icon/icons/close.svg +3 -0
  41. package/src/components/u-icon/icons/code.svg +3 -0
  42. package/src/components/u-icon/icons/copy.svg +3 -0
  43. package/src/components/u-icon/icons/delete.svg +3 -0
  44. package/src/components/u-icon/icons/download.svg +3 -0
  45. package/src/components/u-icon/icons/edit.svg +3 -0
  46. package/src/components/u-icon/icons/email.svg +3 -0
  47. package/src/components/u-icon/icons/error-o.svg +3 -0
  48. package/src/components/u-icon/icons/error.svg +3 -0
  49. package/src/components/u-icon/icons/exit-fullscreen.svg +3 -0
  50. package/src/components/u-icon/icons/expand-less.svg +3 -0
  51. package/src/components/u-icon/icons/expand-more.svg +3 -0
  52. package/src/components/u-icon/icons/eye-off.svg +3 -0
  53. package/src/components/u-icon/icons/eye.svg +3 -0
  54. package/src/components/u-icon/icons/flag-o.svg +3 -0
  55. package/src/components/u-icon/icons/flag.svg +3 -0
  56. package/src/components/u-icon/icons/fullscreen.svg +3 -0
  57. package/src/components/u-icon/icons/grid.svg +3 -0
  58. package/src/components/u-icon/icons/group.svg +3 -0
  59. package/src/components/u-icon/icons/heart-o.svg +3 -0
  60. package/src/components/u-icon/icons/heart.svg +3 -0
  61. package/src/components/u-icon/icons/info-o.svg +3 -0
  62. package/src/components/u-icon/icons/info.svg +3 -0
  63. package/src/components/u-icon/icons/keyboard-arrow-down.svg +3 -0
  64. package/src/components/u-icon/icons/keyboard-arrow-left.svg +3 -0
  65. package/src/components/u-icon/icons/keyboard-arrow-right.svg +3 -0
  66. package/src/components/u-icon/icons/keyboard-arrow-up.svg +3 -0
  67. package/src/components/u-icon/icons/like-o.svg +3 -0
  68. package/src/components/u-icon/icons/like.svg +3 -0
  69. package/src/components/u-icon/icons/link.svg +3 -0
  70. package/src/components/u-icon/icons/list.svg +3 -0
  71. package/src/components/u-icon/icons/loading.svg +3 -0
  72. package/src/components/u-icon/icons/lock.svg +3 -0
  73. package/src/components/u-icon/icons/menu-o.svg +3 -0
  74. package/src/components/u-icon/icons/menu.svg +3 -0
  75. package/src/components/u-icon/icons/message.svg +3 -0
  76. package/src/components/u-icon/icons/minus.svg +3 -0
  77. package/src/components/u-icon/icons/notification.svg +3 -0
  78. package/src/components/u-icon/icons/phone.svg +3 -0
  79. package/src/components/u-icon/icons/plus.svg +3 -0
  80. package/src/components/u-icon/icons/question.svg +3 -0
  81. package/src/components/u-icon/icons/redo.svg +3 -0
  82. package/src/components/u-icon/icons/refresh-o.svg +3 -0
  83. package/src/components/u-icon/icons/refresh.svg +3 -0
  84. package/src/components/u-icon/icons/reload.svg +3 -0
  85. package/src/components/u-icon/icons/search-o.svg +3 -0
  86. package/src/components/u-icon/icons/search.svg +3 -0
  87. package/src/components/u-icon/icons/setting.svg +3 -0
  88. package/src/components/u-icon/icons/share.svg +3 -0
  89. package/src/components/u-icon/icons/smile-o.svg +3 -0
  90. package/src/components/u-icon/icons/smile.svg +3 -0
  91. package/src/components/u-icon/icons/star-o.svg +3 -0
  92. package/src/components/u-icon/icons/star.svg +3 -0
  93. package/src/components/u-icon/icons/success-o.svg +3 -0
  94. package/src/components/u-icon/icons/success.svg +3 -0
  95. package/src/components/u-icon/icons/sync.svg +3 -0
  96. package/src/components/u-icon/icons/tick.svg +3 -0
  97. package/src/components/u-icon/icons/undo.svg +3 -0
  98. package/src/components/u-icon/icons/unlock.svg +3 -0
  99. package/src/components/u-icon/icons/upload.svg +3 -0
  100. package/src/components/u-icon/icons/user.svg +3 -0
  101. package/src/components/u-icon/icons/warning-o.svg +3 -0
  102. package/src/components/u-icon/icons/warning.svg +3 -0
  103. package/src/components/u-icon/icons/zoom-in.svg +3 -0
  104. package/src/components/u-icon/icons/zoom-out.svg +3 -0
  105. package/src/components/u-icon/index.ts +219 -0
  106. package/src/components/u-icon/u-icon.vue +117 -0
  107. package/src/components/u-image/u-image.vue +106 -0
  108. package/src/components/u-input/u-input.vue +208 -0
  109. package/src/components/u-keyboard/u-keyboard.vue +213 -0
  110. package/src/components/u-layout/u-layout.vue +58 -0
  111. package/src/components/u-line-progress/u-line-progress.vue +156 -0
  112. package/src/components/u-link/u-link.vue +113 -0
  113. package/src/components/u-list/u-list.vue +148 -0
  114. package/src/components/u-list-item/u-list-item.vue +180 -0
  115. package/src/components/u-loading/u-loading.vue +80 -0
  116. package/src/components/u-loading-page/u-loading-page.vue +94 -0
  117. package/src/components/u-modal/u-modal.vue +159 -0
  118. package/src/components/u-notice-bar/u-notice-bar.vue +113 -0
  119. package/src/components/u-number-box/u-number-box.vue +262 -0
  120. package/src/components/u-parse/u-parse.vue +197 -0
  121. package/src/components/u-picker/u-picker.vue +219 -0
  122. package/src/components/u-popup/u-popup.vue +257 -0
  123. package/src/components/u-radio/u-radio.vue +159 -0
  124. package/src/components/u-radio-group/u-radio-group.vue +61 -0
  125. package/src/components/u-rate/u-rate.vue +187 -0
  126. package/src/components/u-read-more/u-read-more.vue +117 -0
  127. package/src/components/u-search/u-search.vue +238 -0
  128. package/src/components/u-skeleton/u-skeleton.vue +192 -0
  129. package/src/components/u-slider/u-slider.vue +453 -0
  130. package/src/components/u-swiper/u-swiper.vue +301 -0
  131. package/src/components/u-swiper-item/u-swiper-item.vue +82 -0
  132. package/src/components/u-switch/u-switch.vue +105 -0
  133. package/src/components/u-tabbar/u-tabbar.vue +221 -0
  134. package/src/components/u-tag/u-tag.vue +144 -0
  135. package/src/components/u-textarea/u-textarea.vue +189 -0
  136. package/src/components/u-toast/u-toast.vue +186 -0
  137. package/src/components/u-tooltip/u-tooltip.vue +364 -0
  138. package/src/components/u-transition/u-transition.vue +216 -0
  139. package/src/components/u-upload/u-upload.vue +403 -0
  140. package/src/styles/index.scss +59 -0
  141. package/src/styles/variables.scss +68 -0
@@ -0,0 +1,257 @@
1
+ <template>
2
+ <view class="u-popup" :class="popupClass" @touchmove.stop.prevent>
3
+ <view
4
+ v-if="visible"
5
+ class="u-popup__overlay"
6
+ :class="{ 'u-popup__overlay--visible': overlayVisible }"
7
+ @click="handleOverlayClick"
8
+ />
9
+ <view v-if="visible" class="u-popup__content" :class="contentClass" :style="contentStyle">
10
+ <view v-if="closeable" class="u-popup__close" @click="handleClose">
11
+ <text class="u-popup__close-icon">✕</text>
12
+ </view>
13
+ <slot />
14
+ </view>
15
+ </view>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { ref, computed, watch } from 'vue'
20
+
21
+ type PopupPosition = 'top' | 'bottom' | 'left' | 'right' | 'center'
22
+
23
+ interface Props {
24
+ visible?: boolean
25
+ position?: PopupPosition
26
+ overlay?: boolean
27
+ overlayClass?: string
28
+ closeable?: boolean
29
+ closeOnClickOverlay?: boolean
30
+ safeArea?: boolean
31
+ round?: boolean
32
+ duration?: number
33
+ zIndex?: number
34
+ }
35
+
36
+ const props = withDefaults(defineProps<Props>(), {
37
+ visible: false,
38
+ position: 'bottom',
39
+ overlay: true,
40
+ overlayClass: '',
41
+ closeable: false,
42
+ closeOnClickOverlay: true,
43
+ safeArea: true,
44
+ round: false,
45
+ duration: 300,
46
+ zIndex: 1000
47
+ })
48
+
49
+ const emit = defineEmits<{
50
+ 'update:visible': [value: boolean]
51
+ open: []
52
+ close: []
53
+ }>()
54
+
55
+ const overlayVisible = ref(false)
56
+ const contentVisible = ref(false)
57
+
58
+ let timer: ReturnType<typeof setTimeout> | null = null
59
+
60
+ const popupClass = computed(() => [
61
+ {
62
+ 'u-popup--safe-area': props.safeArea
63
+ }
64
+ ])
65
+
66
+ const contentClass = computed(() => [
67
+ `u-popup__content--${props.position}`,
68
+ {
69
+ 'u-popup__content--round': props.round,
70
+ 'u-popup__content--visible': contentVisible.value
71
+ }
72
+ ])
73
+
74
+ const contentStyle = computed(() => {
75
+ const style: Record<string, string> = {}
76
+ const positions: Record<string, string> = {
77
+ top: 'top: 0; left: 0; right: 0;',
78
+ bottom: 'bottom: 0; left: 0; right: 0;',
79
+ left: 'left: 0; top: 0; bottom: 0;',
80
+ right: 'right: 0; top: 0; bottom: 0;',
81
+ center: 'top: 50%; left: 50%;'
82
+ }
83
+
84
+ style.zIndex = String(props.zIndex + 1)
85
+
86
+ if (props.position === 'center') {
87
+ style.transitionDuration = `${props.duration}ms`
88
+ }
89
+
90
+ return style
91
+ })
92
+
93
+ watch(() => props.visible, (val) => {
94
+ if (val) {
95
+ overlayVisible.value = true
96
+ timer = setTimeout(() => {
97
+ contentVisible.value = true
98
+ emit('open')
99
+ }, 50)
100
+ } else {
101
+ contentVisible.value = false
102
+ timer = setTimeout(() => {
103
+ overlayVisible.value = false
104
+ emit('close')
105
+ }, props.duration)
106
+ }
107
+ })
108
+
109
+ const handleOverlayClick = () => {
110
+ if (props.closeOnClickOverlay) {
111
+ handleClose()
112
+ }
113
+ }
114
+
115
+ const handleClose = () => {
116
+ emit('update:visible', false)
117
+ }
118
+ </script>
119
+
120
+ <script lang="ts">
121
+ export default {
122
+ options: {
123
+ virtualHost: true,
124
+ styleIsolation: 'shared'
125
+ }
126
+ }
127
+ </script>
128
+
129
+ <style lang="scss" scoped>
130
+ .u-popup {
131
+ position: fixed;
132
+ top: 0;
133
+ left: 0;
134
+ right: 0;
135
+ bottom: 0;
136
+ z-index: 1000;
137
+ pointer-events: none;
138
+
139
+ &--safe-area {
140
+ padding-bottom: constant(safe-area-inset-bottom);
141
+ padding-bottom: env(safe-area-inset-bottom);
142
+ /* 鴻蒙兼容 */
143
+ padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
144
+ }
145
+
146
+ &__overlay {
147
+ position: absolute;
148
+ top: 0;
149
+ left: 0;
150
+ right: 0;
151
+ bottom: 0;
152
+ background-color: rgba(0, 0, 0, 0.7);
153
+ opacity: 0;
154
+ transition: opacity 0.3s;
155
+ pointer-events: auto;
156
+
157
+ &--visible {
158
+ opacity: 1;
159
+ }
160
+ }
161
+
162
+ &__content {
163
+ position: absolute;
164
+ background-color: $--bg-color-2;
165
+ transition: transform 0.3s ease-out;
166
+ pointer-events: auto;
167
+ overflow: hidden;
168
+
169
+ &--top {
170
+ transform: translateY(-100%);
171
+
172
+ &--visible {
173
+ transform: translateY(0);
174
+ }
175
+
176
+ &--round {
177
+ border-radius: 0 0 $--radius-lg $--radius-lg;
178
+ }
179
+ }
180
+
181
+ &--bottom {
182
+ transform: translateY(100%);
183
+
184
+ &--visible {
185
+ transform: translateY(0);
186
+ }
187
+
188
+ &--round {
189
+ border-radius: $--radius-lg $--radius-lg 0 0;
190
+ }
191
+ }
192
+
193
+ &--left {
194
+ width: 80%;
195
+ height: 100%;
196
+ transform: translateX(-100%);
197
+
198
+ &--visible {
199
+ transform: translateX(0);
200
+ }
201
+
202
+ &--round {
203
+ border-radius: 0 $--radius-lg $--radius-lg 0;
204
+ }
205
+ }
206
+
207
+ &--right {
208
+ width: 80%;
209
+ height: 100%;
210
+ transform: translateX(100%);
211
+
212
+ &--visible {
213
+ transform: translateX(0);
214
+ }
215
+
216
+ &--round {
217
+ border-radius: $--radius-lg 0 0 $--radius-lg;
218
+ }
219
+ }
220
+
221
+ &--center {
222
+ top: 50%;
223
+ left: 50%;
224
+ transform: translate(-50%, -50%) scale(0.9);
225
+ opacity: 0;
226
+ min-width: 280px;
227
+ border-radius: $--radius-lg;
228
+
229
+ &--visible {
230
+ transform: translate(-50%, -50%) scale(1);
231
+ opacity: 1;
232
+ }
233
+
234
+ &--round {
235
+ border-radius: $--radius-lg;
236
+ }
237
+ }
238
+ }
239
+
240
+ &__close {
241
+ position: absolute;
242
+ top: $--spacing-base;
243
+ right: $--spacing-base;
244
+ width: 24px;
245
+ height: 24px;
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ z-index: 1;
250
+
251
+ &-icon {
252
+ font-size: 18px;
253
+ color: $--text-color-3;
254
+ }
255
+ }
256
+ }
257
+ </style>
@@ -0,0 +1,159 @@
1
+ <template>
2
+ <view class="u-radio" :class="radioClass" @click="handleClick">
3
+ <view class="u-radio__icon" :class="iconClass" :style="iconStyle">
4
+ <view v-if="checked" class="u-radio__dot" />
5
+ </view>
6
+ <view v-if="label || $slots.default" class="u-radio__label">
7
+ <slot>{{ label }}</slot>
8
+ </view>
9
+ </view>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed, inject } from 'vue'
14
+ import type { ComputedRef, Ref } from 'vue'
15
+
16
+ interface RadioGroupContext {
17
+ modelValue: Ref<any>
18
+ disabled: ComputedRef<boolean>
19
+ select: (value: any) => void
20
+ }
21
+
22
+ interface Props {
23
+ value?: any
24
+ label?: string
25
+ disabled?: boolean
26
+ checkedColor?: string
27
+ iconSize?: number
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ value: '',
32
+ label: '',
33
+ disabled: false,
34
+ checkedColor: '',
35
+ iconSize: 20
36
+ })
37
+
38
+ const emit = defineEmits<{
39
+ 'update:modelValue': [value: any]
40
+ change: [value: any]
41
+ }>()
42
+
43
+ const group = inject<RadioGroupContext | null>('u-radio-group', null)
44
+
45
+ const checked = computed(() => {
46
+ if (group) {
47
+ return group.modelValue.value === props.value
48
+ }
49
+ return false
50
+ })
51
+
52
+ const isDisabled = computed(() => {
53
+ if (group) {
54
+ return group.disabled.value || props.disabled
55
+ }
56
+ return props.disabled
57
+ })
58
+
59
+ const radioClass = computed(() => [
60
+ {
61
+ 'u-radio--disabled': isDisabled.value,
62
+ 'u-radio--checked': checked.value
63
+ }
64
+ ])
65
+
66
+ const iconClass = computed(() => [
67
+ {
68
+ 'u-radio__icon--checked': checked.value,
69
+ 'u-radio__icon--disabled': isDisabled.value
70
+ }
71
+ ])
72
+
73
+ const iconStyle = computed(() => {
74
+ const style: Record<string, string> = {}
75
+ style.width = `${props.iconSize}px`
76
+ style.height = `${props.iconSize}px`
77
+
78
+ if (checked.value && props.checkedColor) {
79
+ style.borderColor = props.checkedColor
80
+ }
81
+
82
+ return style
83
+ })
84
+
85
+ const handleClick = () => {
86
+ if (isDisabled.value) return
87
+
88
+ if (group) {
89
+ group.select(props.value)
90
+ } else {
91
+ emit('update:modelValue', props.value)
92
+ emit('change', props.value)
93
+ }
94
+ }
95
+ </script>
96
+
97
+ <script lang="ts">
98
+ export default {
99
+ options: {
100
+ virtualHost: true,
101
+ styleIsolation: 'shared'
102
+ }
103
+ }
104
+ </script>
105
+
106
+ <style lang="scss" scoped>
107
+ .u-radio {
108
+ display: inline-flex;
109
+ align-items: center;
110
+ -webkit-user-select: none;
111
+ -moz-user-select: none;
112
+ -ms-user-select: none;
113
+ user-select: none;
114
+
115
+ &--disabled {
116
+ opacity: 0.5;
117
+ }
118
+
119
+ &__icon {
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+ border: 1px solid $--border-color;
124
+ background-color: $--bg-color-2;
125
+ border-radius: 50%;
126
+ transition: all 0.2s;
127
+ flex-shrink: 0;
128
+
129
+ &--checked {
130
+ border-color: $--color-primary;
131
+ border-width: 6px;
132
+ background-color: $--bg-color-2;
133
+ }
134
+
135
+ &--disabled {
136
+ background-color: $--bg-color;
137
+ border-color: $--border-color;
138
+ }
139
+ }
140
+
141
+ &__dot {
142
+ width: 8px;
143
+ height: 8px;
144
+ border-radius: 50%;
145
+ background-color: $--color-primary;
146
+ }
147
+
148
+ &__label {
149
+ margin-left: $--spacing-sm;
150
+ font-size: $--font-size-base;
151
+ color: $--text-color;
152
+ line-height: 1.5;
153
+ }
154
+
155
+ &--disabled &__label {
156
+ color: $--text-color-3;
157
+ }
158
+ }
159
+ </style>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <view class="u-radio-group">
3
+ <slot />
4
+ </view>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { computed, provide, ref, watch } from 'vue'
9
+
10
+ interface Props {
11
+ modelValue?: any
12
+ disabled?: boolean
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ modelValue: '',
17
+ disabled: false
18
+ })
19
+
20
+ const emit = defineEmits<{
21
+ 'update:modelValue': [value: any]
22
+ change: [value: any]
23
+ }>()
24
+
25
+ const innerValue = ref(props.modelValue)
26
+
27
+ watch(() => props.modelValue, (val) => {
28
+ innerValue.value = val
29
+ })
30
+
31
+ const select = (value: any) => {
32
+ if (innerValue.value === value) return
33
+
34
+ innerValue.value = value
35
+ emit('update:modelValue', value)
36
+ emit('change', value)
37
+ }
38
+
39
+ provide('u-radio-group', {
40
+ modelValue: innerValue,
41
+ disabled: computed(() => props.disabled),
42
+ select
43
+ })
44
+ </script>
45
+
46
+ <script lang="ts">
47
+ export default {
48
+ options: {
49
+ virtualHost: true,
50
+ styleIsolation: 'shared'
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <style lang="scss" scoped>
56
+ .u-radio-group {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ gap: $--spacing-base;
60
+ }
61
+ </style>
@@ -0,0 +1,187 @@
1
+ <template>
2
+ <view
3
+ class="u-rate"
4
+ :class="{
5
+ 'u-rate--disabled': disabled,
6
+ 'u-rate--readonly': readonly
7
+ }"
8
+ >
9
+ <view
10
+ v-for="(_, index) in normalizedCount"
11
+ :key="index"
12
+ class="u-rate__item"
13
+ :style="itemStyle"
14
+ @click="handleClick(index, $event)"
15
+ >
16
+ <text class="u-rate__icon u-rate__icon--void" :style="voidStyle">★</text>
17
+ <view
18
+ class="u-rate__icon-wrapper"
19
+ :style="getActiveWrapperStyle(index)"
20
+ >
21
+ <text class="u-rate__icon u-rate__icon--active" :style="activeStyle">★</text>
22
+ </view>
23
+ </view>
24
+ </view>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { computed } from 'vue'
29
+
30
+ type RateValue = number
31
+
32
+ interface Props {
33
+ modelValue?: RateValue
34
+ count?: number
35
+ size?: number | string
36
+ color?: string
37
+ voidColor?: string
38
+ readonly?: boolean
39
+ disabled?: boolean
40
+ allowHalf?: boolean
41
+ }
42
+
43
+ const props = withDefaults(defineProps<Props>(), {
44
+ modelValue: 0,
45
+ count: 5,
46
+ size: 20,
47
+ color: '#ee0a24',
48
+ voidColor: '#c8c9cc',
49
+ readonly: false,
50
+ disabled: false,
51
+ allowHalf: false
52
+ })
53
+
54
+ const emit = defineEmits<{
55
+ 'update:modelValue': [value: number]
56
+ change: [value: number]
57
+ }>()
58
+
59
+ const normalizedCount = computed(() => Math.max(1, Math.floor(props.count)))
60
+ const normalizedValue = computed(() => {
61
+ const value = Number(props.modelValue) || 0
62
+ return Math.max(0, Math.min(normalizedCount.value, value))
63
+ })
64
+
65
+ const itemStyle = computed(() => ({
66
+ width: addUnit(props.size),
67
+ height: addUnit(props.size)
68
+ }))
69
+
70
+ const activeStyle = computed(() => ({
71
+ color: props.color,
72
+ fontSize: addUnit(props.size)
73
+ }))
74
+
75
+ const voidStyle = computed(() => ({
76
+ color: props.voidColor,
77
+ fontSize: addUnit(props.size)
78
+ }))
79
+
80
+ const getActiveWrapperStyle = (index: number) => {
81
+ const diff = normalizedValue.value - index
82
+ const percent = diff >= 1 ? 100 : diff > 0 ? 50 : 0
83
+
84
+ return {
85
+ width: `${percent}%`
86
+ }
87
+ }
88
+
89
+ const handleClick = (index: number, event: any) => {
90
+ if (props.disabled || props.readonly) {
91
+ return
92
+ }
93
+
94
+ let value = index + 1
95
+
96
+ if (props.allowHalf) {
97
+ // 获取触摸位置相对于元素的位置
98
+ const touch = event.touches?.[0] || event
99
+ const clientX = touch.clientX || 0
100
+
101
+ // 使用 uni.createSelectorQuery 获取元素位置
102
+ uni.createSelectorQuery()
103
+ .select('.u-rate__item')
104
+ .boundingClientRect((rect: any) => {
105
+ if (rect) {
106
+ const itemWidth = rect.width
107
+ // 计算当前点击的item位置
108
+ const itemLeft = rect.left + index * (itemWidth + 4) // 4是gap
109
+ if (clientX - itemLeft <= itemWidth / 2) {
110
+ value = index + 0.5
111
+ }
112
+ emit('update:modelValue', value)
113
+ emit('change', value)
114
+ } else {
115
+ // fallback: 直接使用整星
116
+ emit('update:modelValue', value)
117
+ emit('change', value)
118
+ }
119
+ })
120
+ .exec()
121
+ } else {
122
+ emit('update:modelValue', value)
123
+ emit('change', value)
124
+ }
125
+ }
126
+
127
+ function addUnit(value: string | number) {
128
+ return typeof value === 'number' ? `${value}px` : value
129
+ }
130
+ </script>
131
+
132
+ <script lang="ts">
133
+ export default {
134
+ options: {
135
+ virtualHost: true,
136
+ styleIsolation: 'shared'
137
+ }
138
+ }
139
+ </script>
140
+
141
+ <style lang="scss" scoped>
142
+ .u-rate {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: $--spacing-xs;
146
+
147
+ &--disabled,
148
+ &--readonly {
149
+ .u-rate__item {
150
+ }
151
+ }
152
+
153
+ &--disabled {
154
+ opacity: 0.5;
155
+ }
156
+
157
+ &__item {
158
+ position: relative;
159
+ display: inline-flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ }
163
+
164
+ &__icon {
165
+ line-height: 1;
166
+
167
+ &--void {
168
+ position: relative;
169
+ z-index: 1;
170
+ }
171
+
172
+ &--active {
173
+ display: block;
174
+ }
175
+ }
176
+
177
+ &__icon-wrapper {
178
+ position: absolute;
179
+ top: 50%;
180
+ left: 0;
181
+ z-index: 2;
182
+ overflow: hidden;
183
+ transform: translateY(-50%);
184
+ white-space: nowrap;
185
+ }
186
+ }
187
+ </style>