@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,262 @@
1
+ <template>
2
+ <view
3
+ class="u-number-box"
4
+ :class="{
5
+ 'u-number-box--disabled': disabled
6
+ }"
7
+ >
8
+ <view
9
+ class="u-number-box__btn u-number-box__btn--minus"
10
+ :class="{
11
+ 'u-number-box__btn--disabled': minusDisabled
12
+ }"
13
+ :style="btnStyle"
14
+ @click="handleMinus"
15
+ @touchstart="startLongPress('minus')"
16
+ @touchend="stopLongPress"
17
+ @touchcancel="stopLongPress"
18
+ >
19
+ <text class="u-number-box__btn-icon">−</text>
20
+ </view>
21
+
22
+ <input
23
+ class="u-number-box__input"
24
+ :class="{
25
+ 'u-number-box__input--disabled': disabledInput
26
+ }"
27
+ :style="inputStyle"
28
+ :type="inputType"
29
+ :value="displayValue"
30
+ :disabled="disabled || disabledInput"
31
+ @input="handleInput"
32
+ @blur="handleBlur"
33
+ @focus="handleFocus"
34
+ />
35
+
36
+ <view
37
+ class="u-number-box__btn u-number-box__btn--plus"
38
+ :class="{
39
+ 'u-number-box__btn--disabled': plusDisabled
40
+ }"
41
+ :style="btnStyle"
42
+ @click="handlePlus"
43
+ @touchstart="startLongPress('plus')"
44
+ @touchend="stopLongPress"
45
+ @touchcancel="stopLongPress"
46
+ >
47
+ <text class="u-number-box__btn-icon">+</text>
48
+ </view>
49
+ </view>
50
+ </template>
51
+
52
+ <script setup lang="ts">
53
+ import { ref, computed, watch, onUnmounted } from 'vue'
54
+
55
+ interface Props {
56
+ modelValue?: number
57
+ min?: number
58
+ max?: number
59
+ step?: number
60
+ integer?: boolean
61
+ decimalPlaces?: number
62
+ disabled?: boolean
63
+ disabledInput?: boolean
64
+ disabledLongPress?: boolean
65
+ buttonSize?: number | string
66
+ inputWidth?: number | string
67
+ theme?: 'default' | 'primary'
68
+ }
69
+
70
+ const props = withDefaults(defineProps<Props>(), {
71
+ modelValue: 0,
72
+ min: 0,
73
+ max: Infinity,
74
+ step: 1,
75
+ integer: false,
76
+ decimalPlaces: 0,
77
+ disabled: false,
78
+ disabledInput: false,
79
+ disabledLongPress: false,
80
+ buttonSize: 28,
81
+ inputWidth: 36,
82
+ theme: 'default'
83
+ })
84
+
85
+ const emit = defineEmits<{
86
+ 'update:modelValue': [value: number]
87
+ change: [value: number]
88
+ overlimit: [type: 'minus' | 'plus']
89
+ focus: [event: FocusEvent]
90
+ blur: [event: FocusEvent]
91
+ }>()
92
+
93
+ const currentValue = ref(props.modelValue)
94
+ let longPressTimer: ReturnType<typeof setInterval> | null = null
95
+
96
+ const minusDisabled = computed(() => props.disabled || currentValue.value <= props.min)
97
+ const plusDisabled = computed(() => props.disabled || currentValue.value >= props.max)
98
+
99
+ const inputType = computed(() => props.integer ? 'number' : 'digit')
100
+
101
+ const displayValue = computed(() => {
102
+ if (props.decimalPlaces > 0) {
103
+ return currentValue.value.toFixed(props.decimalPlaces)
104
+ }
105
+ return currentValue.value
106
+ })
107
+
108
+ const btnSize = computed(() => {
109
+ const size = props.buttonSize
110
+ return typeof size === 'number' ? `${size}px` : size
111
+ })
112
+
113
+ const iptWidth = computed(() => {
114
+ const width = props.inputWidth
115
+ return typeof width === 'number' ? `${width}px` : width
116
+ })
117
+
118
+ const btnStyle = computed(() => ({
119
+ width: btnSize.value,
120
+ height: btnSize.value,
121
+ fontSize: `calc(${btnSize.value} * 0.5)`,
122
+ backgroundColor: props.theme === 'primary' ? '#1989fa' : '#f2f3f5',
123
+ color: props.theme === 'primary' ? '#fff' : '#323233'
124
+ }))
125
+
126
+ const inputStyle = computed(() => ({
127
+ width: iptWidth.value,
128
+ height: btnSize.value
129
+ }))
130
+
131
+ watch(() => props.modelValue, (val) => {
132
+ currentValue.value = formatValue(val)
133
+ })
134
+
135
+ const formatValue = (val: number): number => {
136
+ let value = Number(val)
137
+ if (isNaN(value)) value = props.min
138
+ if (props.integer) value = Math.round(value)
139
+ value = Math.max(props.min, Math.min(props.max, value))
140
+ return value
141
+ }
142
+
143
+ const updateValue = (val: number) => {
144
+ const formatted = formatValue(val)
145
+ currentValue.value = formatted
146
+ emit('update:modelValue', formatted)
147
+ emit('change', formatted)
148
+ }
149
+
150
+ const handleMinus = () => {
151
+ if (minusDisabled.value) {
152
+ emit('overlimit', 'minus')
153
+ return
154
+ }
155
+ updateValue(currentValue.value - props.step)
156
+ }
157
+
158
+ const handlePlus = () => {
159
+ if (plusDisabled.value) {
160
+ emit('overlimit', 'plus')
161
+ return
162
+ }
163
+ updateValue(currentValue.value + props.step)
164
+ }
165
+
166
+ const handleInput = (event: any) => {
167
+ const val = event.detail.value
168
+ if (val === '' || val === '-') return
169
+ let value = Number(val)
170
+ if (!isNaN(value)) {
171
+ currentValue.value = value
172
+ }
173
+ }
174
+
175
+ const handleBlur = (event: FocusEvent) => {
176
+ updateValue(currentValue.value)
177
+ emit('blur', event)
178
+ }
179
+
180
+ const handleFocus = (event: FocusEvent) => {
181
+ emit('focus', event)
182
+ }
183
+
184
+ const startLongPress = (type: 'minus' | 'plus') => {
185
+ if (props.disabledLongPress || props.disabled) return
186
+
187
+ const action = type === 'minus' ? handleMinus : handlePlus
188
+ longPressTimer = setInterval(() => {
189
+ action()
190
+ }, 150)
191
+ }
192
+
193
+ const stopLongPress = () => {
194
+ if (longPressTimer) {
195
+ clearInterval(longPressTimer)
196
+ longPressTimer = null
197
+ }
198
+ }
199
+
200
+ onUnmounted(() => {
201
+ stopLongPress()
202
+ })
203
+ </script>
204
+
205
+ <script lang="ts">
206
+ export default {
207
+ options: {
208
+ virtualHost: true,
209
+ styleIsolation: 'shared'
210
+ }
211
+ }
212
+ </script>
213
+
214
+ <style lang="scss" scoped>
215
+ .u-number-box {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ gap: 2px;
219
+
220
+ &--disabled {
221
+ opacity: 0.5;
222
+ }
223
+
224
+ &__btn {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ -webkit-user-select: none;
229
+ -moz-user-select: none;
230
+ -ms-user-select: none;
231
+ user-select: none;
232
+ transition: all $--transition-duration;
233
+ border-radius: $--border-radius-sm;
234
+
235
+ &:active {
236
+ opacity: 0.7;
237
+ }
238
+
239
+ &--disabled {
240
+ opacity: 0.4;
241
+ }
242
+ }
243
+
244
+ &__btn-icon {
245
+ line-height: 1;
246
+ }
247
+
248
+ &__input {
249
+ text-align: center;
250
+ font-size: $--font-size-md;
251
+ color: $--text-color;
252
+ background: $--bg-color-2;
253
+ border: 1px solid $--border-color;
254
+ border-radius: $--border-radius-sm;
255
+ outline: none;
256
+
257
+ &--disabled {
258
+ background: $--bg-color;
259
+ }
260
+ }
261
+ }
262
+ </style>
@@ -0,0 +1,197 @@
1
+ <template>
2
+ <view class="u-parse">
3
+ <rich-text
4
+ :nodes="parsedNodes"
5
+ :selectable="selectable"
6
+ :image-menu-prevent="imageMenuPrevent"
7
+ @tap="handleTap"
8
+ />
9
+ </view>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from 'vue'
14
+
15
+ interface Props {
16
+ content: string
17
+ selectable?: boolean
18
+ imageMenuPrevent?: boolean
19
+ imageMode?: 'aspectFit' | 'aspectFill' | 'widthFix' | 'scaleToFill'
20
+ tagStyle?: Record<string, string>
21
+ lazyLoad?: boolean
22
+ }
23
+
24
+ const props = withDefaults(defineProps<Props>(), {
25
+ content: '',
26
+ selectable: false,
27
+ imageMenuPrevent: false,
28
+ imageMode: 'widthFix',
29
+ lazyLoad: false
30
+ })
31
+
32
+ const emit = defineEmits<{
33
+ load: []
34
+ error: [event: Event]
35
+ imgTap: [src: string]
36
+ linkTap: [href: string]
37
+ }>()
38
+
39
+ // 默认标签样式
40
+ const defaultTagStyle: Record<string, string> = {
41
+ p: 'margin: 0 0 16px 0; line-height: 1.6;',
42
+ h1: 'font-size: 28px; font-weight: bold; margin: 0 0 16px 0;',
43
+ h2: 'font-size: 24px; font-weight: bold; margin: 0 0 14px 0;',
44
+ h3: 'font-size: 20px; font-weight: bold; margin: 0 0 12px 0;',
45
+ h4: 'font-size: 18px; font-weight: bold; margin: 0 0 10px 0;',
46
+ h5: 'font-size: 16px; font-weight: bold; margin: 0 0 8px 0;',
47
+ h6: 'font-size: 14px; font-weight: bold; margin: 0 0 8px 0;',
48
+ img: 'max-width: 100%; height: auto; display: block;',
49
+ ul: 'margin: 0 0 16px 0; padding-left: 20px;',
50
+ ol: 'margin: 0 0 16px 0; padding-left: 20px;',
51
+ li: 'margin: 4px 0;',
52
+ blockquote: 'margin: 0 0 16px 0; padding: 8px 16px; border-left: 4px solid #ebedf0; background: #f7f8fa;',
53
+ pre: 'margin: 0 0 16px 0; padding: 12px; background: #f7f8fa; border-radius: 4px; overflow-x: auto;',
54
+ code: 'padding: 2px 6px; background: #f7f8fa; border-radius: 2px; font-family: monospace;',
55
+ a: 'color: #1989fa; text-decoration: none;',
56
+ table: 'width: 100%; border-collapse: collapse; margin: 0 0 16px 0;',
57
+ th: 'padding: 8px; border: 1px solid #ebedf0; background: #f7f8fa; font-weight: bold;',
58
+ td: 'padding: 8px; border: 1px solid #ebedf0;',
59
+ hr: 'border: none; border-top: 1px solid #ebedf0; margin: 16px 0;',
60
+ br: 'content: ""; display: block; margin-bottom: 8px;'
61
+ }
62
+
63
+ // 合并样式
64
+ const mergedTagStyle = computed(() => {
65
+ return { ...defaultTagStyle, ...props.tagStyle }
66
+ })
67
+
68
+ // 解析内容
69
+ const parsedNodes = computed(() => {
70
+ if (!props.content) return ''
71
+
72
+ let html = props.content
73
+
74
+ // 处理图片
75
+ html = html.replace(/<img([^>]*)>/gi, (match, attrs) => {
76
+ const srcMatch = attrs.match(/src=["']([^"']+)["']/i)
77
+ const altMatch = attrs.match(/alt=["']([^"']*)["']/i)
78
+ const widthMatch = attrs.match(/width=["']?(\d+)["']?/i)
79
+ const heightMatch = attrs.match(/height=["']?(\d+)["']?/i)
80
+
81
+ const src = srcMatch ? srcMatch[1] : ''
82
+ const alt = altMatch ? altMatch[1] : ''
83
+ const width = widthMatch ? widthMatch[1] : 'auto'
84
+ const height = heightMatch ? heightMatch[1] : 'auto'
85
+
86
+ const style = mergedTagStyle.value.img || ''
87
+ const widthStyle = width !== 'auto' ? `width: ${width}px;` : 'width: 100%;'
88
+ const heightStyle = height !== 'auto' ? `height: ${height}px;` : ''
89
+
90
+ // 懒加载处理
91
+ const lazyAttr = props.lazyLoad ? `lazy-load="true"` : ''
92
+ return `<img src="${src}" alt="${alt}" style="${style} ${widthStyle} ${heightStyle}" data-src="${src}" ${lazyAttr} />`
93
+ })
94
+
95
+ // 处理链接
96
+ html = html.replace(/<a([^>]*)>/gi, (match, attrs) => {
97
+ const hrefMatch = attrs.match(/href=["']([^"']+)["']/i)
98
+ const href = hrefMatch ? hrefMatch[1] : '#'
99
+ const style = mergedTagStyle.value.a || ''
100
+ return `<a href="${href}" style="${style}" data-href="${href}">`
101
+ })
102
+
103
+ // 添加默认样式到标签
104
+ const tags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'table', 'th', 'td', 'hr']
105
+ tags.forEach(tag => {
106
+ const style = mergedTagStyle.value[tag]
107
+ if (style) {
108
+ const regex = new RegExp(`<${tag}([^>]*)>`, 'gi')
109
+ html = html.replace(regex, (match, attrs) => {
110
+ const hasStyle = /style=/i.test(attrs)
111
+ if (hasStyle) {
112
+ return match.replace(/style=["']([^"']*)["']/i, `style="$1; ${style}"`)
113
+ }
114
+ return `<${tag} style="${style}"${attrs}>`
115
+ })
116
+ }
117
+ })
118
+
119
+ return html
120
+ })
121
+
122
+ // 处理点击事件
123
+ const handleTap = (event: any) => {
124
+ const target = event.target
125
+
126
+ // 处理图片点击
127
+ if (target.tagName === 'IMG') {
128
+ const src = target.getAttribute('data-src') || target.getAttribute('src')
129
+ if (src) {
130
+ emit('imgTap', src)
131
+ }
132
+ }
133
+
134
+ // 处理链接点击
135
+ if (target.tagName === 'A') {
136
+ event.stopPropagation?.()
137
+ const href = target.getAttribute('data-href') || target.getAttribute('href')
138
+ if (href && href !== '#') {
139
+ emit('linkTap', href)
140
+ }
141
+ }
142
+ }
143
+ </script>
144
+
145
+ <script lang="ts">
146
+ export default {
147
+ options: {
148
+ virtualHost: true,
149
+ styleIsolation: 'shared'
150
+ }
151
+ }
152
+ </script>
153
+
154
+ <style lang="scss" scoped>
155
+ .u-parse {
156
+ font-size: $--font-size-md;
157
+ line-height: 1.6;
158
+ color: $--text-color;
159
+ word-break: break-word;
160
+
161
+ :deep(img) {
162
+ max-width: 100%;
163
+ height: auto;
164
+ vertical-align: middle;
165
+ }
166
+
167
+ :deep(a) {
168
+ color: $--color-primary;
169
+ text-decoration: none;
170
+
171
+ &:active {
172
+ opacity: 0.7;
173
+ }
174
+ }
175
+
176
+ :deep(pre) {
177
+ white-space: pre-wrap;
178
+ word-wrap: break-word;
179
+ }
180
+
181
+ :deep(code) {
182
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
183
+ }
184
+
185
+ :deep(table) {
186
+ display: block;
187
+ overflow-x: auto;
188
+ }
189
+
190
+ :deep(blockquote) {
191
+ border-left: 4px solid $--border-color;
192
+ background-color: $--bg-color;
193
+ padding: $--spacing-md $--spacing-lg;
194
+ margin: $--spacing-md 0;
195
+ }
196
+ }
197
+ </style>
@@ -0,0 +1,219 @@
1
+ <template>
2
+ <view v-if="visible" class="u-picker" @click="handleOverlayClick">
3
+ <view class="u-picker__popup" @click.stop>
4
+ <view class="u-picker__toolbar">
5
+ <text class="u-picker__action" @click="handleCancel">取消</text>
6
+ <text class="u-picker__title">{{ title }}</text>
7
+ <text class="u-picker__action u-picker__action--confirm" @click="handleConfirm">确定</text>
8
+ </view>
9
+
10
+ <view v-if="loading" class="u-picker__loading">
11
+ <text>加载中...</text>
12
+ </view>
13
+
14
+ <picker-view
15
+ v-else
16
+ class="u-picker__view"
17
+ :value="innerIndexes"
18
+ indicator-class="u-picker__indicator"
19
+ @change="handlePickerChange"
20
+ >
21
+ <picker-view-column v-for="(column, columnIndex) in normalizedColumns" :key="columnIndex">
22
+ <view
23
+ v-for="(option, optionIndex) in column"
24
+ :key="option.value ?? option.label ?? optionIndex"
25
+ class="u-picker__option"
26
+ >
27
+ <text>{{ option.label }}</text>
28
+ </view>
29
+ </picker-view-column>
30
+ </picker-view>
31
+ </view>
32
+ </view>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed, ref, watch } from 'vue'
37
+
38
+ interface PickerOption {
39
+ label: string
40
+ value: string | number
41
+ }
42
+
43
+ type PickerColumn = Array<string | number | PickerOption>
44
+ type PickerValue = Array<string | number>
45
+
46
+ interface Props {
47
+ visible?: boolean
48
+ columns?: PickerColumn[]
49
+ modelValue?: PickerValue
50
+ title?: string
51
+ loading?: boolean
52
+ closeOnClickOverlay?: boolean
53
+ }
54
+
55
+ const props = withDefaults(defineProps<Props>(), {
56
+ visible: false,
57
+ columns: () => [],
58
+ modelValue: () => [],
59
+ title: '选择器',
60
+ loading: false,
61
+ closeOnClickOverlay: false
62
+ })
63
+
64
+ const emit = defineEmits<{
65
+ 'update:visible': [value: boolean]
66
+ 'update:modelValue': [value: PickerValue]
67
+ change: [value: PickerValue, columnIndex: number]
68
+ confirm: [value: PickerValue]
69
+ cancel: []
70
+ }>()
71
+
72
+ const normalizedColumns = computed(() => props.columns.map((column) => column.map(normalizeOption)))
73
+ const innerIndexes = ref<number[]>([])
74
+
75
+ watch(
76
+ () => [props.columns, props.modelValue],
77
+ () => {
78
+ innerIndexes.value = getIndexesByValue(props.modelValue)
79
+ },
80
+ { immediate: true, deep: true }
81
+ )
82
+
83
+ const currentValues = computed(() => getValuesByIndexes(innerIndexes.value))
84
+
85
+ const handlePickerChange = (event: any) => {
86
+ const values = event.detail.value as number[]
87
+ const previousIndexes = [...innerIndexes.value]
88
+ innerIndexes.value = values
89
+ const changedColumn = getChangedColumn(previousIndexes, values)
90
+ const nextValues = getValuesByIndexes(values)
91
+ emit('update:modelValue', nextValues)
92
+ emit('change', nextValues, changedColumn)
93
+ }
94
+
95
+ const handleCancel = () => {
96
+ emit('update:visible', false)
97
+ emit('cancel')
98
+ }
99
+
100
+ const handleConfirm = () => {
101
+ emit('update:visible', false)
102
+ emit('confirm', currentValues.value)
103
+ }
104
+
105
+ const handleOverlayClick = () => {
106
+ if (props.closeOnClickOverlay) {
107
+ emit('update:visible', false)
108
+ emit('cancel')
109
+ }
110
+ }
111
+
112
+ function normalizeOption(option: string | number | PickerOption): PickerOption {
113
+ if (typeof option === 'object' && option !== null) {
114
+ return option
115
+ }
116
+
117
+ return {
118
+ label: String(option),
119
+ value: option
120
+ }
121
+ }
122
+
123
+ function getIndexesByValue(values: PickerValue) {
124
+ return normalizedColumns.value.map((column, index) => {
125
+ const target = values[index]
126
+ const foundIndex = column.findIndex((option) => option.value === target)
127
+ return foundIndex >= 0 ? foundIndex : 0
128
+ })
129
+ }
130
+
131
+ function getValuesByIndexes(indexes: number[]) {
132
+ return normalizedColumns.value.map((column, index) => column[indexes[index] || 0]?.value).filter((value) => value !== undefined)
133
+ }
134
+
135
+ function getChangedColumn(previousIndexes: number[], nextIndexes: number[]) {
136
+ return nextIndexes.findIndex((item, index) => item !== previousIndexes[index])
137
+ }
138
+ </script>
139
+
140
+ <script lang="ts">
141
+ export default {
142
+ options: {
143
+ virtualHost: true,
144
+ styleIsolation: 'shared'
145
+ }
146
+ }
147
+ </script>
148
+
149
+ <style lang="scss" scoped>
150
+ .u-picker {
151
+ position: fixed;
152
+ inset: 0;
153
+ display: flex;
154
+ align-items: flex-end;
155
+ background: rgba(0, 0, 0, 0.45);
156
+ z-index: 1000;
157
+
158
+ &__popup {
159
+ width: 100%;
160
+ background: $--bg-color-2;
161
+ border-radius: $--border-radius-lg $--border-radius-lg 0 0;
162
+ overflow: hidden;
163
+ }
164
+
165
+ &__toolbar {
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: space-between;
169
+ height: 48px;
170
+ padding: 0 $--spacing-lg;
171
+ border-bottom: 1px solid $--border-color;
172
+ }
173
+
174
+ &__title {
175
+ font-size: $--font-size-md;
176
+ color: $--text-color;
177
+ font-weight: 600;
178
+ }
179
+
180
+ &__action {
181
+ font-size: $--font-size-md;
182
+ color: $--text-color-2;
183
+
184
+ &--confirm {
185
+ color: $--color-primary;
186
+ }
187
+ }
188
+
189
+ &__loading {
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ height: 240px;
194
+ font-size: $--font-size-md;
195
+ color: $--text-color-2;
196
+ }
197
+
198
+ &__view {
199
+ width: 100%;
200
+ height: 240px;
201
+ }
202
+
203
+ &__indicator {
204
+ height: 48px;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ }
209
+
210
+ &__option {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ height: 48px;
215
+ font-size: $--font-size-lg;
216
+ color: $--text-color;
217
+ }
218
+ }
219
+ </style>