muzhiyu-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 (96) hide show
  1. package/README.md +202 -0
  2. package/components/muzhiyu/mu-action-sheet/mu-action-sheet.vue +335 -0
  3. package/components/muzhiyu/mu-alert/mu-alert.vue +220 -0
  4. package/components/muzhiyu/mu-back-top/mu-back-top.vue +161 -0
  5. package/components/muzhiyu/mu-badge/mu-badge.vue +150 -0
  6. package/components/muzhiyu/mu-barcode/mu-barcode.vue +100 -0
  7. package/components/muzhiyu/mu-blank/mu-blank.vue +30 -0
  8. package/components/muzhiyu/mu-bottom-nav/mu-bottom-nav.vue +331 -0
  9. package/components/muzhiyu/mu-bubble/mu-bubble.vue +316 -0
  10. package/components/muzhiyu/mu-button/mu-button.vue +431 -0
  11. package/components/muzhiyu/mu-calendar/mu-calendar.vue +429 -0
  12. package/components/muzhiyu/mu-car-input/mu-car-input.vue +209 -0
  13. package/components/muzhiyu/mu-card/mu-card.vue +317 -0
  14. package/components/muzhiyu/mu-cascader/mu-cascader.vue +336 -0
  15. package/components/muzhiyu/mu-cell/mu-cell.vue +368 -0
  16. package/components/muzhiyu/mu-checkbox/mu-checkbox.vue +116 -0
  17. package/components/muzhiyu/mu-checkbox-group/mu-checkbox-group.vue +40 -0
  18. package/components/muzhiyu/mu-code-input/mu-code-input.vue +264 -0
  19. package/components/muzhiyu/mu-collapse/mu-collapse.vue +316 -0
  20. package/components/muzhiyu/mu-color-picker/mu-color-picker.vue +466 -0
  21. package/components/muzhiyu/mu-config/index.js +34 -0
  22. package/components/muzhiyu/mu-config/mu-config.vue +54 -0
  23. package/components/muzhiyu/mu-countdown/mu-countdown.vue +338 -0
  24. package/components/muzhiyu/mu-cropper/mu-cropper.vue +415 -0
  25. package/components/muzhiyu/mu-date-picker/mu-date-picker.vue +290 -0
  26. package/components/muzhiyu/mu-datetime/mu-datetime.vue +262 -0
  27. package/components/muzhiyu/mu-divider/mu-divider.vue +229 -0
  28. package/components/muzhiyu/mu-drawer/mu-drawer.vue +172 -0
  29. package/components/muzhiyu/mu-dropdown/mu-dropdown.vue +215 -0
  30. package/components/muzhiyu/mu-dropdown-top/mu-dropdown-top.vue +243 -0
  31. package/components/muzhiyu/mu-empty/mu-empty.vue +217 -0
  32. package/components/muzhiyu/mu-fab/mu-fab.vue +324 -0
  33. package/components/muzhiyu/mu-footer/mu-footer.vue +21 -0
  34. package/components/muzhiyu/mu-form/mu-form.vue +170 -0
  35. package/components/muzhiyu/mu-grid/mu-grid.vue +95 -0
  36. package/components/muzhiyu/mu-grid-item/mu-grid-item.vue +247 -0
  37. package/components/muzhiyu/mu-html/mu-html.vue +145 -0
  38. package/components/muzhiyu/mu-icon/mu-icon.vue +822 -0
  39. package/components/muzhiyu/mu-image-grid/mu-image-grid.vue +225 -0
  40. package/components/muzhiyu/mu-image-preview/mu-image-preview.vue +286 -0
  41. package/components/muzhiyu/mu-index-list/mu-index-list.vue +381 -0
  42. package/components/muzhiyu/mu-input/mu-input.vue +570 -0
  43. package/components/muzhiyu/mu-input-number/mu-input-number.vue +193 -0
  44. package/components/muzhiyu/mu-keyboard/mu-keyboard.vue +415 -0
  45. package/components/muzhiyu/mu-list/mu-list.vue +89 -0
  46. package/components/muzhiyu/mu-loading/mu-loading.vue +124 -0
  47. package/components/muzhiyu/mu-loading-page/mu-loading-page.vue +529 -0
  48. package/components/muzhiyu/mu-loadmore/mu-loadmore.vue +26 -0
  49. package/components/muzhiyu/mu-modal/mu-modal.vue +298 -0
  50. package/components/muzhiyu/mu-navbar/mu-navbar.vue +223 -0
  51. package/components/muzhiyu/mu-nomore/mu-nomore.vue +107 -0
  52. package/components/muzhiyu/mu-notice-bar/mu-notice-bar.vue +284 -0
  53. package/components/muzhiyu/mu-notify/mu-notify.vue +202 -0
  54. package/components/muzhiyu/mu-overlay/mu-overlay.vue +94 -0
  55. package/components/muzhiyu/mu-pagination/mu-pagination.vue +330 -0
  56. package/components/muzhiyu/mu-password-input/mu-password-input.vue +119 -0
  57. package/components/muzhiyu/mu-picker/mu-picker.vue +297 -0
  58. package/components/muzhiyu/mu-popup/mu-popup.vue +297 -0
  59. package/components/muzhiyu/mu-progress/mu-progress.vue +156 -0
  60. package/components/muzhiyu/mu-pull-refresh/mu-pull-refresh.vue +54 -0
  61. package/components/muzhiyu/mu-qrcode/mu-qrcode.vue +340 -0
  62. package/components/muzhiyu/mu-qrcode/qrcode.js +2237 -0
  63. package/components/muzhiyu/mu-radio/mu-radio.vue +125 -0
  64. package/components/muzhiyu/mu-radio-group/mu-radio-group.vue +37 -0
  65. package/components/muzhiyu/mu-rate/mu-rate.vue +103 -0
  66. package/components/muzhiyu/mu-search/mu-search.vue +320 -0
  67. package/components/muzhiyu/mu-section/mu-section.vue +206 -0
  68. package/components/muzhiyu/mu-select/mu-select.vue +440 -0
  69. package/components/muzhiyu/mu-share-sheet/mu-share-sheet.vue +269 -0
  70. package/components/muzhiyu/mu-sidebar/mu-sidebar.vue +304 -0
  71. package/components/muzhiyu/mu-signature/mu-signature.vue +449 -0
  72. package/components/muzhiyu/mu-skeleton/mu-skeleton.vue +225 -0
  73. package/components/muzhiyu/mu-slider/mu-slider.vue +228 -0
  74. package/components/muzhiyu/mu-space/mu-space.vue +84 -0
  75. package/components/muzhiyu/mu-steps/mu-steps.vue +311 -0
  76. package/components/muzhiyu/mu-sticky/mu-sticky.vue +57 -0
  77. package/components/muzhiyu/mu-swipe-cell/mu-swipe-cell.vue +281 -0
  78. package/components/muzhiyu/mu-swiper/mu-swiper.vue +107 -0
  79. package/components/muzhiyu/mu-switch/mu-switch.vue +99 -0
  80. package/components/muzhiyu/mu-tabbar/mu-tabbar.vue +382 -0
  81. package/components/muzhiyu/mu-table/mu-table.vue +420 -0
  82. package/components/muzhiyu/mu-tabs/mu-tabs.vue +339 -0
  83. package/components/muzhiyu/mu-tag/mu-tag.vue +65 -0
  84. package/components/muzhiyu/mu-text-ellipsis/mu-text-ellipsis.vue +54 -0
  85. package/components/muzhiyu/mu-timeline/mu-timeline.vue +166 -0
  86. package/components/muzhiyu/mu-tips/mu-tips.vue +36 -0
  87. package/components/muzhiyu/mu-toast/mu-toast.vue +151 -0
  88. package/components/muzhiyu/mu-transfer/mu-transfer.vue +114 -0
  89. package/components/muzhiyu/mu-tree/mu-tree.vue +263 -0
  90. package/components/muzhiyu/mu-upload/mu-upload.vue +415 -0
  91. package/components/muzhiyu/mu-waterfall/mu-waterfall.vue +321 -0
  92. package/components/muzhiyu/mu-watermark/mu-watermark.vue +60 -0
  93. package/index.js +83 -0
  94. package/package.json +38 -0
  95. package/utils/index.js +600 -0
  96. package/utils/request.js +265 -0
@@ -0,0 +1,415 @@
1
+ <template>
2
+ <view v-if="visible" class="mu-cropper">
3
+ <!-- 核心画布与操作视口 -->
4
+ <view
5
+ class="mu-cropper__canvas-area"
6
+ @touchstart="onTouchStart"
7
+ @touchmove="onTouchMove"
8
+ @touchend="onTouchEnd"
9
+ >
10
+ <!-- GPU 硬件加速变换中的图片 -->
11
+ <image
12
+ class="mu-cropper__img"
13
+ :src="src"
14
+ mode="aspectFit"
15
+ :style="imgTransformStyle"
16
+ />
17
+
18
+ <!-- 遮罩全屏(挖空裁剪框) -->
19
+ <view class="mu-cropper__mask"></view>
20
+
21
+ <!-- 裁剪框与 L 型高亮四角手柄 -->
22
+ <view
23
+ class="mu-cropper__frame"
24
+ :class="[shape === 'circle' && 'mu-cropper__frame--circle']"
25
+ :style="frameStyle"
26
+ >
27
+ <!-- 九宫格网格线 -->
28
+ <view class="mu-cropper__grid-h1"></view>
29
+ <view class="mu-cropper__grid-h2"></view>
30
+ <view class="mu-cropper__grid-v1"></view>
31
+ <view class="mu-cropper__grid-v2"></view>
32
+
33
+ <!-- 4 个 L 型四角装饰 -->
34
+ <view class="mu-cropper__corner mu-cropper__corner--tl"></view>
35
+ <view class="mu-cropper__corner mu-cropper__corner--tr"></view>
36
+ <view class="mu-cropper__corner mu-cropper__corner--bl"></view>
37
+ <view class="mu-cropper__corner mu-cropper__corner--br"></view>
38
+ </view>
39
+ </view>
40
+
41
+ <!-- 底部极奢工具栏 -->
42
+ <view class="mu-cropper__toolbar">
43
+ <view class="mu-cropper__tool-btn" @tap="handleCancel">
44
+ <text class="mu-cropper__tool-text">取消</text>
45
+ </view>
46
+
47
+ <view class="mu-cropper__tool-btn" @tap="handleRotate">
48
+ <mu-icon name="redo" :size="20" color="#FFFFFF" />
49
+ <text class="mu-cropper__tool-text" style="margin-left: 8rpx;">旋转</text>
50
+ </view>
51
+
52
+ <view class="mu-cropper__tool-btn" @tap="handleReset">
53
+ <mu-icon name="undo" :size="20" color="#FFFFFF" />
54
+ <text class="mu-cropper__tool-text" style="margin-left: 8rpx;">还原</text>
55
+ </view>
56
+
57
+ <view class="mu-cropper__confirm-btn mu-touchable-flat" @tap="handleConfirm">
58
+ <text class="mu-cropper__confirm-text">完成</text>
59
+ </view>
60
+ </view>
61
+
62
+ <!-- 离屏 Canvas (非 H5 端生成临时图片) -->
63
+ <!-- #ifndef H5 -->
64
+ <canvas
65
+ canvas-id="mu-cropper-export-canvas"
66
+ id="mu-cropper-export-canvas"
67
+ :style="{ position: 'fixed', left: '-9999px', top: '-9999px', width: props.width + 'px', height: props.height + 'px' }"
68
+ />
69
+ <!-- #endif -->
70
+ </view>
71
+ </template>
72
+
73
+ <script setup>
74
+ import { ref, computed, watch, getCurrentInstance } from 'vue'
75
+
76
+ const props = defineProps({
77
+ src: { type: String, default: '' },
78
+ show: { type: Boolean, default: false },
79
+ width: { type: Number, default: 300 },
80
+ height: { type: Number, default: 300 },
81
+ shape: { type: String, default: 'square' },
82
+ quality: { type: Number, default: 0.9 }
83
+ })
84
+
85
+ const emit = defineEmits(['confirm', 'cancel', 'update:show'])
86
+
87
+ const visible = ref(false)
88
+
89
+ // 图片变换数据
90
+ const imgX = ref(0)
91
+ const imgY = ref(0)
92
+ const imgScale = ref(1)
93
+ const rotateAngle = ref(0)
94
+
95
+ // 触控流控锁
96
+ const startTouchX = ref(0)
97
+ const startTouchY = ref(0)
98
+ const startImgX = ref(0)
99
+ const startImgY = ref(0)
100
+ const startDist = ref(0)
101
+ const startScale = ref(1)
102
+ let ticking = false
103
+
104
+ const frameStyle = computed(() => ({
105
+ width: `${props.width}px`,
106
+ height: `${props.height}px`
107
+ }))
108
+
109
+ const imgTransformStyle = computed(() => ({
110
+ transform: `translate3d(${imgX.value}px, ${imgY.value}px, 0) scale(${imgScale.value}) rotate(${rotateAngle.value}deg)`,
111
+ transition: 'none'
112
+ }))
113
+
114
+ function onTouchStart(e) {
115
+ if (!e || !e.touches || !e.touches.length) return
116
+ if (e.touches.length === 1 && e.touches[0]) {
117
+ const touch = e.touches[0]
118
+ startTouchX.value = touch.clientX
119
+ startTouchY.value = touch.clientY
120
+ startImgX.value = imgX.value
121
+ startImgY.value = imgY.value
122
+ } else if (e.touches.length === 2 && e.touches[0] && e.touches[1]) {
123
+ const t1 = e.touches[0]
124
+ const t2 = e.touches[1]
125
+ startDist.value = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
126
+ startScale.value = imgScale.value
127
+ }
128
+ }
129
+
130
+ function onTouchMove(e) {
131
+ if (e && e.preventDefault) e.preventDefault()
132
+ if (ticking || !e || !e.touches || !e.touches.length) return
133
+ ticking = true
134
+
135
+ // rAF 满帧调优,避免打爆主线程响应式计算
136
+ const update = () => {
137
+ if (e.touches.length === 1 && e.touches[0]) {
138
+ const touch = e.touches[0]
139
+ const deltaX = touch.clientX - startTouchX.value
140
+ const deltaY = touch.clientY - startTouchY.value
141
+ imgX.value = startImgX.value + deltaX
142
+ imgY.value = startImgY.value + deltaY
143
+ } else if (e.touches.length === 2 && e.touches[0] && e.touches[1]) {
144
+ const t1 = e.touches[0]
145
+ const t2 = e.touches[1]
146
+ const currentDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY)
147
+ if (startDist.value > 0) {
148
+ const scale = startScale.value * (currentDist / startDist.value)
149
+ imgScale.value = Math.max(0.3, Math.min(4, scale))
150
+ }
151
+ }
152
+ ticking = false
153
+ }
154
+
155
+ // #ifdef H5
156
+ if (window.requestAnimationFrame) {
157
+ window.requestAnimationFrame(update)
158
+ } else {
159
+ update()
160
+ }
161
+ // #endif
162
+
163
+ // #ifndef H5
164
+ update()
165
+ // #endif
166
+ }
167
+
168
+ function onTouchEnd() {
169
+ ticking = false
170
+ }
171
+
172
+ function handleRotate() {
173
+ rotateAngle.value = (rotateAngle.value + 90) % 360
174
+ }
175
+
176
+ function handleReset() {
177
+ imgX.value = 0
178
+ imgY.value = 0
179
+ imgScale.value = 1
180
+ rotateAngle.value = 0
181
+ }
182
+
183
+ function handleCancel() {
184
+ visible.value = false
185
+ emit('cancel')
186
+ emit('update:show', false)
187
+ }
188
+
189
+ function handleConfirm() {
190
+ // #ifdef H5
191
+ const canvas = document.createElement('canvas')
192
+ canvas.width = props.width
193
+ canvas.height = props.height
194
+ const ctx = canvas.getContext('2d')
195
+ const img = new Image()
196
+ img.crossOrigin = 'anonymous'
197
+
198
+ img.onload = () => {
199
+ ctx.clearRect(0, 0, props.width, props.height)
200
+ ctx.save()
201
+ ctx.translate(props.width / 2, props.height / 2)
202
+ ctx.rotate((rotateAngle.value * Math.PI) / 180)
203
+ ctx.scale(imgScale.value, imgScale.value)
204
+ ctx.drawImage(img, -props.width / 2 + imgX.value, -props.height / 2 + imgY.value, props.width, props.height)
205
+ ctx.restore()
206
+
207
+ canvas.toBlob((blob) => {
208
+ const url = URL.createObjectURL(blob)
209
+ emit('confirm', { url, blob })
210
+ handleCancel()
211
+ }, 'image/png', props.quality)
212
+ }
213
+ img.src = props.src
214
+ // #endif
215
+
216
+ // #ifndef H5
217
+ const instance = getCurrentInstance()
218
+
219
+ const doDraw = (localSrc) => {
220
+ const ctx = uni.createCanvasContext('mu-cropper-export-canvas', (instance && instance.proxy))
221
+ ctx.save()
222
+ ctx.translate(props.width / 2, props.height / 2)
223
+ ctx.rotate((rotateAngle.value * Math.PI) / 180)
224
+ ctx.scale(imgScale.value, imgScale.value)
225
+ ctx.drawImage(localSrc, -props.width / 2 + imgX.value, -props.height / 2 + imgY.value, props.width, props.height)
226
+ ctx.restore()
227
+
228
+ ctx.draw(false, () => {
229
+ setTimeout(() => {
230
+ uni.canvasToTempFilePath({
231
+ canvasId: 'mu-cropper-export-canvas',
232
+ quality: props.quality,
233
+ success: (res) => {
234
+ let tempFilePath = res.tempFilePath
235
+ if (typeof tempFilePath === 'string' && tempFilePath.startsWith('http://tmp/')) {
236
+ tempFilePath = tempFilePath.replace('http://tmp/', 'wxfile://tmp/')
237
+ }
238
+ emit('confirm', { url: tempFilePath })
239
+ handleCancel()
240
+ },
241
+ fail: () => {
242
+ uni.showToast({ title: '图片裁剪失败', icon: 'none' })
243
+ }
244
+ }, (instance && instance.proxy))
245
+ }, 100)
246
+ })
247
+ }
248
+
249
+ if (props.src.startsWith('http')) {
250
+ uni.showLoading({ title: '正在处理图片...' })
251
+ uni.downloadFile({
252
+ url: props.src,
253
+ success: (res) => {
254
+ uni.hideLoading()
255
+ if (res.statusCode === 200 && res.tempFilePath) {
256
+ doDraw(res.tempFilePath)
257
+ } else {
258
+ doDraw(props.src)
259
+ }
260
+ },
261
+ fail: () => {
262
+ uni.hideLoading()
263
+ doDraw(props.src)
264
+ }
265
+ })
266
+ } else {
267
+ doDraw(props.src)
268
+ }
269
+ // #endif
270
+ }
271
+
272
+ watch(() => props.show, (val) => {
273
+ if (val) {
274
+ visible.value = true
275
+ handleReset()
276
+ } else {
277
+ visible.value = false
278
+ }
279
+ })
280
+ </script>
281
+
282
+ <style lang="scss" scoped>
283
+ @import "@/uni.scss";
284
+
285
+ .mu-cropper {
286
+ position: fixed;
287
+ inset: 0;
288
+ z-index: $mu-z-popup;
289
+ background: #000000;
290
+ display: flex;
291
+ flex-direction: column;
292
+ overflow: hidden;
293
+
294
+ &__canvas-area {
295
+ flex: 1;
296
+ position: relative;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ overflow: hidden;
301
+ touch-action: none;
302
+ }
303
+
304
+ &__img {
305
+ position: absolute;
306
+ max-width: none;
307
+ max-height: none;
308
+ will-change: transform;
309
+ transform-style: preserve-3d;
310
+ backface-visibility: hidden;
311
+ }
312
+
313
+ &__mask {
314
+ position: absolute;
315
+ inset: 0;
316
+ background: rgba(0, 0, 0, 0.65);
317
+ pointer-events: none;
318
+ z-index: 2;
319
+ }
320
+
321
+ &__frame {
322
+ position: relative;
323
+ z-index: 3;
324
+ border: 1rpx solid rgba(255, 255, 255, 0.8);
325
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.65);
326
+ pointer-events: none;
327
+ box-sizing: border-box;
328
+
329
+ &--circle {
330
+ border-radius: 50%;
331
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
332
+ overflow: hidden;
333
+ }
334
+ }
335
+
336
+ /* 网格线 */
337
+ &__grid-h1, &__grid-h2 {
338
+ position: absolute;
339
+ left: 0;
340
+ right: 0;
341
+ height: 1rpx;
342
+ background: rgba(255, 255, 255, 0.25);
343
+ }
344
+ &__grid-h1 { top: 33.33%; }
345
+ &__grid-h2 { top: 66.66%; }
346
+
347
+ &__grid-v1, &__grid-v2 {
348
+ position: absolute;
349
+ top: 0;
350
+ bottom: 0;
351
+ width: 1rpx;
352
+ background: rgba(255, 255, 255, 0.25);
353
+ }
354
+ &__grid-v1 { left: 33.33%; }
355
+ &__grid-v2 { left: 66.66%; }
356
+
357
+ /* 4 角 L 型角标 */
358
+ &__corner {
359
+ position: absolute;
360
+ width: 28rpx;
361
+ height: 28rpx;
362
+ border-color: #ffffff;
363
+ border-style: solid;
364
+ border-width: 0;
365
+ z-index: 4;
366
+
367
+ &--tl { top: -2rpx; left: -2rpx; border-top-width: 6rpx; border-left-width: 6rpx; }
368
+ &--tr { top: -2rpx; right: -2rpx; border-top-width: 6rpx; border-right-width: 6rpx; }
369
+ &--bl { bottom: -2rpx; left: -2rpx; border-bottom-width: 6rpx; border-left-width: 6rpx; }
370
+ &--br { bottom: -2rpx; right: -2rpx; border-bottom-width: 6rpx; border-right-width: 6rpx; }
371
+ }
372
+
373
+ &__toolbar {
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: space-between;
377
+ padding: 28rpx 36rpx;
378
+ padding-bottom: calc(28rpx + env(safe-area-inset-bottom));
379
+ background: #0F172A;
380
+ z-index: 10;
381
+ }
382
+
383
+ &__tool-btn {
384
+ display: flex;
385
+ align-items: center;
386
+ cursor: pointer;
387
+ padding: 12rpx 20rpx;
388
+ border-radius: 12rpx;
389
+
390
+ &:active {
391
+ background: rgba(255, 255, 255, 0.1);
392
+ }
393
+ }
394
+
395
+ &__tool-text {
396
+ font-size: 26rpx;
397
+ color: #ffffff;
398
+ font-weight: 600;
399
+ }
400
+
401
+ &__confirm-btn {
402
+ padding: 14rpx 36rpx;
403
+ background: #171717;
404
+ border-radius: 999rpx;
405
+ box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
406
+ cursor: pointer;
407
+ }
408
+
409
+ &__confirm-text {
410
+ font-size: 26rpx;
411
+ font-weight: 800;
412
+ color: #ffffff;
413
+ }
414
+ }
415
+ </style>
@@ -0,0 +1,290 @@
1
+ <template>
2
+ <view class="mu-date-picker" :class="[
3
+ `mu-date-picker--${shape}`,
4
+ `mu-date-picker--label-${labelPosition}`,
5
+ disabled && 'mu-date-picker--disabled',
6
+ error && 'mu-date-picker--error',
7
+ filled && 'mu-date-picker--filled'
8
+ ]">
9
+ <!-- 标题 Label (包含必填红星与位置布局) -->
10
+ <view v-if="label || $slots.label" class="mu-date-picker__label-box" :style="labelBoxStyle">
11
+ <text v-if="required" class="mu-date-picker__required">*</text>
12
+ <slot name="label">
13
+ <text class="mu-date-picker__label-text">{{ label }}</text>
14
+ </slot>
15
+ </view>
16
+
17
+ <!-- 日期/时间框主容器 Main Box -->
18
+ <view class="mu-date-picker__main">
19
+ <view class="mu-date-picker__wrap" @tap="togglePicker">
20
+ <!-- 前缀图标与前缀插槽 -->
21
+ <view v-if="computedPrefixIcon || $slots.prefix" class="mu-date-picker__prefix">
22
+ <slot name="prefix">
23
+ <mu-icon :name="computedPrefixIcon" :size="16" color="#94A3B8" />
24
+ </slot>
25
+ </view>
26
+
27
+ <!-- 选中文本或 Placeholder -->
28
+ <text class="mu-date-picker__text" :class="{ 'mu-date-picker__text--placeholder': !modelValue }">
29
+ {{ modelValue || computedPlaceholder }}
30
+ </text>
31
+
32
+ <!-- 清除按钮 -->
33
+ <view v-if="clearable && modelValue" class="mu-date-picker__clear" @tap.stop="onClear">
34
+ <mu-icon name="close" :size="12" color="#94A3B8" />
35
+ </view>
36
+
37
+ <!-- 后缀图标与后缀插槽 -->
38
+ <view v-if="suffixIcon || $slots.suffix" class="mu-date-picker__suffix">
39
+ <slot name="suffix">
40
+ <mu-icon :name="suffixIcon" :size="16" color="#94A3B8" />
41
+ </slot>
42
+ </view>
43
+ </view>
44
+
45
+ <!-- 错误提示文本 -->
46
+ <text v-if="error" class="mu-date-picker__error">{{ errorMessage }}</text>
47
+ </view>
48
+
49
+ <!-- 高颜值主题色日期/时间选择弹窗 mu-datetime -->
50
+ <mu-datetime
51
+ v-model:show="showPicker"
52
+ :value="modelValue"
53
+ :title="computedTitle"
54
+ :mode="mode"
55
+ @confirm="onConfirmDate"
56
+ />
57
+ </view>
58
+ </template>
59
+
60
+ <script setup>
61
+ import { ref, computed } from 'vue'
62
+
63
+ const props = defineProps({
64
+ modelValue: { type: String, default: '' },
65
+ shape: { type: String, default: 'radius' }, // square | radius | round
66
+ filled: { type: Boolean, default: false }, // 背景填充模式
67
+ label: { type: String, default: '' },
68
+ title: { type: String, default: '' },
69
+ labelPosition: { type: String, default: 'top' }, // top | left
70
+ labelWidth: { type: String, default: '' },
71
+ required: { type: Boolean, default: false },
72
+ placeholder: { type: String, default: '' },
73
+ disabled: { type: Boolean, default: false },
74
+ clearable: { type: Boolean, default: false },
75
+ mode: { type: String, default: 'date' }, // date | time | datetime | year-month
76
+ error: { type: Boolean, default: false },
77
+ errorMessage: { type: String, default: '' },
78
+ prefixIcon: { type: String, default: null }, // 若为 null,则根据 mode 智能匹配 (date -> calendar, time -> time)
79
+ suffixIcon: { type: String, default: 'chevron_down' } // 右侧默认微型下拉箭头
80
+ })
81
+
82
+ const emit = defineEmits(['update:modelValue', 'change', 'clear'])
83
+
84
+ const showPicker = ref(false)
85
+
86
+ const computedPrefixIcon = computed(() => {
87
+ if (props.prefixIcon !== null) return props.prefixIcon
88
+ return props.mode === 'time' ? 'time' : 'calendar'
89
+ })
90
+
91
+ const computedPlaceholder = computed(() => {
92
+ if (props.placeholder) return props.placeholder
93
+ if (props.mode === 'time') return '请选择时间'
94
+ if (props.mode === 'datetime') return '请选择日期时间'
95
+ return '请选择日期'
96
+ })
97
+
98
+ const computedTitle = computed(() => {
99
+ if (props.title) return props.title
100
+ if (props.label) return props.label
101
+ if (props.mode === 'time') return '选择时间'
102
+ if (props.mode === 'datetime') return '选择日期时间'
103
+ return '选择日期'
104
+ })
105
+
106
+ const labelBoxStyle = computed(() => {
107
+ if (props.labelPosition === 'left' && props.labelWidth) {
108
+ return { width: props.labelWidth }
109
+ }
110
+ return {}
111
+ })
112
+
113
+ function togglePicker() {
114
+ if (props.disabled) return
115
+ showPicker.value = true
116
+ }
117
+
118
+ function onConfirmDate(val) {
119
+ emit('update:modelValue', val)
120
+ emit('change', val)
121
+ }
122
+
123
+ function onClear() {
124
+ emit('update:modelValue', '')
125
+ emit('clear')
126
+ }
127
+ </script>
128
+
129
+ <style lang="scss" scoped>
130
+ @import "@/uni.scss";
131
+
132
+ .mu-date-picker {
133
+ margin-bottom: 24rpx;
134
+
135
+ &--disabled { opacity: $mu-opacity-disabled; pointer-events: none; }
136
+ &--error {
137
+ .mu-date-picker__wrap {
138
+ border-color: $mu-error;
139
+ box-shadow: 0 0 14rpx rgba(244, 63, 94, 0.2);
140
+ }
141
+ }
142
+
143
+ /* Label 布局配置 */
144
+ &--label-top {
145
+ display: flex;
146
+ flex-direction: column;
147
+ }
148
+
149
+ &--label-left {
150
+ display: flex;
151
+ align-items: center;
152
+ .mu-date-picker__label-box {
153
+ margin-bottom: 0;
154
+ margin-right: 20rpx;
155
+ flex-shrink: 0;
156
+ width: 140rpx;
157
+ }
158
+ .mu-date-picker__main {
159
+ flex: 1;
160
+ min-width: 0;
161
+ }
162
+ }
163
+
164
+ &__label-box {
165
+ display: flex;
166
+ align-items: center;
167
+ margin-bottom: 12rpx;
168
+ }
169
+
170
+ &__required {
171
+ color: #F43F5E;
172
+ font-size: 26rpx;
173
+ font-weight: 800;
174
+ margin-right: 6rpx;
175
+ line-height: 1;
176
+ }
177
+
178
+ &__label-text {
179
+ font-size: 26rpx;
180
+ color: $mu-text-primary;
181
+ font-weight: 700;
182
+ line-height: 1.4;
183
+ }
184
+
185
+ &__main {
186
+ width: 100%;
187
+ }
188
+
189
+ &__wrap {
190
+ display: flex;
191
+ align-items: center;
192
+ border: 1rpx solid $mu-border;
193
+ padding: 0 20rpx;
194
+ height: 68rpx;
195
+ background: #ffffff;
196
+ transition: all $mu-duration $mu-ease;
197
+ cursor: pointer;
198
+ box-sizing: border-box;
199
+ }
200
+
201
+ /* 填充背景模式 */
202
+ &--filled &__wrap {
203
+ background: rgba(241, 245, 249, 0.6);
204
+ border-color: transparent;
205
+ }
206
+
207
+ /* 形态定义 (方形 / 柔和圆角 / 全圆胶囊) */
208
+ &--square &__wrap {
209
+ border-radius: 6rpx;
210
+ }
211
+ &--radius &__wrap {
212
+ border-radius: 12rpx;
213
+ }
214
+ &--round &__wrap {
215
+ border-radius: 999rpx;
216
+ padding: 0 28rpx;
217
+ }
218
+
219
+ &__prefix {
220
+ margin-right: 16rpx;
221
+ display: flex;
222
+ align-items: center;
223
+ flex-shrink: 0;
224
+ }
225
+
226
+ &__text {
227
+ flex: 1;
228
+ font-size: 26rpx;
229
+ color: $mu-text-primary;
230
+ font-weight: 500;
231
+
232
+ &--placeholder {
233
+ color: #94A3B8;
234
+ font-weight: 400;
235
+ }
236
+ }
237
+
238
+ &__clear {
239
+ width: 32rpx;
240
+ height: 32rpx;
241
+ border-radius: 50%;
242
+ background: rgba(148, 163, 184, 0.15);
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ cursor: pointer;
247
+ margin-left: 12rpx;
248
+ flex-shrink: 0;
249
+ }
250
+
251
+ &__suffix {
252
+ margin-left: 12rpx;
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ flex-shrink: 0;
257
+ }
258
+
259
+ &__error {
260
+ font-size: 22rpx;
261
+ color: $mu-error;
262
+ margin-top: 8rpx;
263
+ display: block;
264
+ }
265
+ }
266
+
267
+ .mu-dark, page.mu-dark {
268
+
269
+ .mu-date-picker {
270
+ &__label-text { color: $mu-dark-text-primary; }
271
+ &__wrap {
272
+ background: rgba(255, 255, 255, 0.04);
273
+ border-color: $mu-dark-border;
274
+ }
275
+ &__text { color: $mu-dark-text-primary; }
276
+ }
277
+ }
278
+
279
+ @media (prefers-color-scheme: dark) {
280
+
281
+ .mu-date-picker {
282
+ &__label-text { color: $mu-dark-text-primary; }
283
+ &__wrap {
284
+ background: rgba(255, 255, 255, 0.04);
285
+ border-color: $mu-dark-border;
286
+ }
287
+ &__text { color: $mu-dark-text-primary; }
288
+ }
289
+ }
290
+ </style>