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,449 @@
1
+ <template>
2
+ <view class="mu-signature">
3
+ <!-- 纯粹画板容器 Pure Canvas Area -->
4
+ <view class="mu-signature__board" :style="{ height: formattedHeight, background: bgColor }">
5
+ <!-- Canvas 节点:直接绑定所有触摸与鼠标事件 -->
6
+ <canvas
7
+ :canvas-id="canvasId"
8
+ :id="canvasId"
9
+ class="mu-signature__canvas"
10
+ :disable-scroll="true"
11
+ @touchstart.stop.prevent="handleTouchStart"
12
+ @touchmove.stop.prevent="handleTouchMove"
13
+ @touchend.stop.prevent="handleTouchEnd"
14
+ @mousedown.stop.prevent="handleMouseDown"
15
+ @mousemove.stop.prevent="handleMouseMove"
16
+ @mouseup.stop.prevent="handleMouseUp"
17
+ @mouseleave.stop.prevent="handleMouseUp"
18
+ />
19
+
20
+ <!-- 水印手写文字提示 -->
21
+ <view v-if="isEmpty" class="mu-signature__placeholder">
22
+ <text class="mu-signature__placeholder-text">请在此手写签名</text>
23
+ </view>
24
+
25
+ <!-- 浮动操作按钮 (阻止冒泡,禁止触发画板落笔) -->
26
+ <view
27
+ v-if="showActions"
28
+ class="mu-signature__floating-tools"
29
+ @touchstart.stop
30
+ @touchmove.stop
31
+ @touchend.stop
32
+ @mousedown.stop
33
+ @mousemove.stop
34
+ @mouseup.stop
35
+ >
36
+ <view class="mu-signature__btn mu-signature__btn--text" @tap.stop="handleUndo">撤销</view>
37
+ <view class="mu-signature__btn mu-signature__btn--text" @tap.stop="handleClear">清空</view>
38
+ <view class="mu-signature__btn mu-signature__btn--primary" @tap.stop="handleConfirm">完成签名</view>
39
+ </view>
40
+ </view>
41
+ </view>
42
+ </template>
43
+
44
+ <script setup>
45
+ import { ref, computed, onMounted, getCurrentInstance } from 'vue'
46
+
47
+ const props = defineProps({
48
+ height: { type: [Number, String], default: 380 },
49
+ penColor: { type: String, default: '#0F172A' },
50
+ penWidth: { type: Number, default: 4 },
51
+ bgColor: { type: String, default: '#FFFFFF' },
52
+ showActions: { type: Boolean, default: true }
53
+ })
54
+
55
+ const emit = defineEmits(['confirm', 'clear', 'start', 'end', 'undo'])
56
+
57
+ const instance = getCurrentInstance()
58
+ const canvasId = ref('mu-sig-' + Math.random().toString(36).slice(2, 8))
59
+ const ctx = ref(null)
60
+ const isDrawing = ref(false)
61
+ const isMouseDown = ref(false)
62
+ const isEmpty = ref(true)
63
+
64
+ const boardRect = ref({ left: 0, top: 0, width: 0, height: 0 })
65
+ const historyLines = ref([])
66
+ const currentStrokePoints = ref([])
67
+ let lastPos = null
68
+
69
+ const formattedHeight = computed(() => {
70
+ if (typeof props.height === 'number') return `${props.height}rpx`
71
+ return props.height.includes('rpx') || props.height.includes('px') ? props.height : `${props.height}rpx`
72
+ })
73
+
74
+ function getCanvasElement() {
75
+ // #ifdef H5
76
+ let el = document.getElementById(canvasId.value)
77
+ if (!el) return null
78
+ if (el.tagName === 'CANVAS') return el
79
+ return el.querySelector('canvas') || el
80
+ // #endif
81
+ return null
82
+ }
83
+
84
+ function measureCanvasRect(callback) {
85
+ // #ifndef APP-NVUE
86
+ const query = uni.createSelectorQuery().in(instance)
87
+ query.select(`#${canvasId.value}`).boundingClientRect((rect) => {
88
+ if (rect) {
89
+ boardRect.value = rect
90
+ if (callback) callback()
91
+ }
92
+ }).exec()
93
+ // #endif
94
+ }
95
+
96
+ function initCanvas() {
97
+ // #ifndef H5
98
+ ctx.value = uni.createCanvasContext(canvasId.value, (instance && instance.proxy))
99
+ // #endif
100
+
101
+ measureCanvasRect(() => {
102
+ // #ifdef H5
103
+ const canvas = getCanvasElement()
104
+ if (canvas) {
105
+ const rect = canvas.getBoundingClientRect()
106
+ const w = Math.round(rect.width || canvas.clientWidth || 300)
107
+ const h = Math.round(rect.height || canvas.clientHeight || 180)
108
+ if (w > 0 && h > 0) {
109
+ canvas.width = w
110
+ canvas.height = h
111
+ }
112
+ }
113
+ // #endif
114
+ clearCanvas()
115
+ })
116
+ }
117
+
118
+ function clearCanvas() {
119
+ // #ifdef H5
120
+ const canvas = getCanvasElement()
121
+ if (canvas && typeof canvas.getContext === 'function') {
122
+ const c2d = canvas.getContext('2d')
123
+ c2d.fillStyle = props.bgColor
124
+ c2d.fillRect(0, 0, canvas.width, canvas.height)
125
+ return
126
+ }
127
+ // #endif
128
+
129
+ if (ctx.value && boardRect.value.width) {
130
+ ctx.value.beginPath()
131
+ ctx.value.setFillStyle(props.bgColor)
132
+ ctx.value.fillRect(0, 0, boardRect.value.width, boardRect.value.height)
133
+ ctx.value.draw()
134
+ }
135
+ }
136
+
137
+ function getCanvasPos(e) {
138
+ // 1. H5 桌面端 / 模拟器 MouseEvent: 直接优先使用原生的 e.offsetX 和 e.offsetY 绝对像素坐标
139
+ if (e && e.offsetX !== undefined && e.offsetY !== undefined) {
140
+ return {
141
+ x: e.offsetX,
142
+ y: e.offsetY
143
+ }
144
+ }
145
+
146
+ // 2. 触摸屏 TouchEvent / UniApp 事件
147
+ const touch = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]) || e
148
+
149
+ // #ifdef H5
150
+ const target = e.currentTarget || e.target
151
+ if (target && typeof target.getBoundingClientRect === 'function') {
152
+ const rect = target.getBoundingClientRect()
153
+ const clientX = touch.clientX !== undefined ? touch.clientX : 0
154
+ const clientY = touch.clientY !== undefined ? touch.clientY : 0
155
+ return {
156
+ x: clientX - rect.left,
157
+ y: clientY - rect.top
158
+ }
159
+ }
160
+ // #endif
161
+
162
+ // #ifndef H5
163
+ if (touch.x !== undefined && touch.y !== undefined) {
164
+ return { x: touch.x, y: touch.y }
165
+ }
166
+ // #endif
167
+
168
+ if (boardRect.value && boardRect.value.left !== undefined) {
169
+ const clientX = touch.clientX !== undefined ? touch.clientX : (touch.x || 0)
170
+ const clientY = touch.clientY !== undefined ? touch.clientY : (touch.y || 0)
171
+ return {
172
+ x: clientX - boardRect.value.left,
173
+ y: clientY - boardRect.value.top
174
+ }
175
+ }
176
+
177
+ return { x: 0, y: 0 }
178
+ }
179
+
180
+ function startStroke(e) {
181
+ isDrawing.value = true
182
+ const pos = getCanvasPos(e)
183
+ lastPos = pos
184
+ currentStrokePoints.value = [pos]
185
+ emit('start')
186
+ }
187
+
188
+ function moveStroke(e) {
189
+ if (!isDrawing.value || !lastPos) return
190
+ const pos = getCanvasPos(e)
191
+
192
+ const dist = Math.hypot(pos.x - lastPos.x, pos.y - lastPos.y)
193
+ if (dist < 0.5) return
194
+
195
+ isEmpty.value = false
196
+ drawLineSegment(lastPos, pos)
197
+
198
+ lastPos = pos
199
+ currentStrokePoints.value.push(pos)
200
+ }
201
+
202
+ function endStroke() {
203
+ if (!isDrawing.value) return
204
+ isDrawing.value = false
205
+ lastPos = null
206
+
207
+ if (currentStrokePoints.value.length > 1) {
208
+ historyLines.value.push([...currentStrokePoints.value])
209
+ }
210
+ currentStrokePoints.value = []
211
+ emit('end')
212
+ }
213
+
214
+ function drawLineSegment(p1, p2) {
215
+ // #ifdef H5
216
+ const canvasEl = getCanvasElement()
217
+ if (canvasEl && typeof canvasEl.getContext === 'function') {
218
+ const c2d = canvasEl.getContext('2d')
219
+ c2d.beginPath()
220
+ c2d.strokeStyle = props.penColor
221
+ c2d.lineWidth = props.penWidth
222
+ c2d.lineCap = 'round'
223
+ c2d.lineJoin = 'round'
224
+ c2d.moveTo(p1.x, p1.y)
225
+ c2d.lineTo(p2.x, p2.y)
226
+ c2d.stroke()
227
+ return
228
+ }
229
+ // #endif
230
+
231
+ // #ifndef H5
232
+ if (!ctx.value) return
233
+ ctx.value.beginPath()
234
+ ctx.value.setStrokeStyle(props.penColor)
235
+ ctx.value.setLineWidth(props.penWidth)
236
+ ctx.value.setLineCap('round')
237
+ ctx.value.setLineJoin('round')
238
+ ctx.value.moveTo(p1.x, p1.y)
239
+ ctx.value.lineTo(p2.x, p2.y)
240
+ ctx.value.stroke()
241
+ ctx.value.draw(true)
242
+ // #endif
243
+ }
244
+
245
+ // 触摸事件 handler
246
+ function handleTouchStart(e) {
247
+ startStroke(e)
248
+ }
249
+
250
+ function handleTouchMove(e) {
251
+ moveStroke(e)
252
+ }
253
+
254
+ function handleTouchEnd(e) {
255
+ endStroke()
256
+ }
257
+
258
+ // 鼠标事件 handler (PC / H5 模拟器支持)
259
+ function handleMouseDown(e) {
260
+ isMouseDown.value = true
261
+ startStroke(e)
262
+ }
263
+
264
+ function handleMouseMove(e) {
265
+ if (!isMouseDown.value) return
266
+ moveStroke(e)
267
+ }
268
+
269
+ function handleMouseUp(e) {
270
+ if (!isMouseDown.value) return
271
+ isMouseDown.value = false
272
+ endStroke()
273
+ }
274
+
275
+ function handleUndo() {
276
+ if (historyLines.value.length === 0) return
277
+ historyLines.value.pop()
278
+ if (historyLines.value.length === 0) {
279
+ isEmpty.value = true
280
+ }
281
+ redrawAll()
282
+ emit('undo')
283
+ }
284
+
285
+ function handleClear() {
286
+ historyLines.value = []
287
+ currentStrokePoints.value = []
288
+ lastPos = null
289
+ isEmpty.value = true
290
+ clearCanvas()
291
+ emit('clear')
292
+ }
293
+
294
+ function redrawAll() {
295
+ clearCanvas()
296
+ if (historyLines.value.length === 0) return
297
+
298
+ for (const stroke of historyLines.value) {
299
+ if (stroke.length < 2) continue
300
+ for (let i = 0; i < stroke.length - 1; i++) {
301
+ drawLineSegment(stroke[i], stroke[i + 1])
302
+ }
303
+ }
304
+ }
305
+
306
+ function handleConfirm() {
307
+ if (isEmpty.value) {
308
+ if (typeof uni !== 'undefined' && uni.$toast) uni.$toast('请先书写签名')
309
+ return
310
+ }
311
+
312
+ // #ifdef H5
313
+ const canvas = getCanvasElement()
314
+ if (canvas && typeof canvas.toDataURL === 'function') {
315
+ try {
316
+ const dataUrl = canvas.toDataURL('image/png')
317
+ emit('confirm', { tempFilePath: dataUrl })
318
+ return
319
+ } catch (e) {}
320
+ }
321
+ // #endif
322
+
323
+ uni.canvasToTempFilePath({
324
+ canvasId: canvasId.value,
325
+ fileType: 'png',
326
+ quality: 1,
327
+ success: (res) => {
328
+ emit('confirm', { tempFilePath: res.tempFilePath })
329
+ },
330
+ fail: () => {
331
+ if (typeof uni !== 'undefined' && uni.$toast) uni.$toast({ title: '生成签名图片失败', type: 'error' })
332
+ }
333
+ }, (instance && instance.proxy))
334
+ }
335
+
336
+ onMounted(() => {
337
+ setTimeout(() => {
338
+ initCanvas()
339
+ }, 150)
340
+ })
341
+
342
+ defineExpose({ clear: handleClear, undo: handleUndo, confirm: handleConfirm, isEmpty })
343
+ </script>
344
+
345
+ <style lang="scss" scoped>
346
+ @import "@/uni.scss";
347
+
348
+ .mu-signature {
349
+ width: 100%;
350
+ box-sizing: border-box;
351
+
352
+ &__board {
353
+ position: relative;
354
+ width: 100%;
355
+ border-radius: 20rpx;
356
+ border: 1rpx dashed rgba(148, 163, 184, 0.4);
357
+ overflow: hidden;
358
+ touch-action: none !important;
359
+ cursor: crosshair;
360
+ box-sizing: border-box;
361
+ }
362
+
363
+ &__canvas {
364
+ width: 100%;
365
+ height: 100%;
366
+ touch-action: none !important;
367
+ }
368
+
369
+ &__placeholder {
370
+ position: absolute;
371
+ top: 50%;
372
+ left: 50%;
373
+ transform: translate(-50%, -50%);
374
+ pointer-events: none;
375
+ opacity: 0.35;
376
+
377
+ &-text {
378
+ font-size: 28rpx;
379
+ color: #94A3B8;
380
+ font-weight: 600;
381
+ letter-spacing: 2rpx;
382
+ }
383
+ }
384
+
385
+ &__floating-tools {
386
+ position: absolute;
387
+ bottom: 16rpx;
388
+ right: 16rpx;
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 12rpx;
392
+ z-index: 10;
393
+ }
394
+
395
+ &__btn {
396
+ padding: 10rpx 24rpx;
397
+ border-radius: 999rpx;
398
+ font-size: 22rpx;
399
+ font-weight: 700;
400
+ cursor: pointer;
401
+ transition: all 0.2s ease;
402
+
403
+ &--text {
404
+ background: rgba(255, 255, 255, 0.9);
405
+ color: #475569;
406
+ border: 1rpx solid #E2E8F0;
407
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
408
+
409
+ &:active { background: #F1F5F9; }
410
+ }
411
+
412
+ &--primary {
413
+ background: #171717;
414
+ color: #ffffff;
415
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
416
+
417
+ &:active { opacity: 0.9; }
418
+ }
419
+ }
420
+ }
421
+
422
+ .mu-dark, page.mu-dark {
423
+ .mu-signature {
424
+ &__board {
425
+ border-color: rgba(255, 255, 255, 0.15);
426
+ }
427
+ &__placeholder-text { color: $mu-dark-text-secondary; }
428
+ &__btn--text {
429
+ background: rgba(30, 41, 59, 0.9);
430
+ color: $mu-dark-text-primary;
431
+ border-color: rgba(255, 255, 255, 0.1);
432
+ }
433
+ }
434
+ }
435
+
436
+ @media (prefers-color-scheme: dark) {
437
+ .mu-signature {
438
+ &__board {
439
+ border-color: rgba(255, 255, 255, 0.15);
440
+ }
441
+ &__placeholder-text { color: $mu-dark-text-secondary; }
442
+ &__btn--text {
443
+ background: rgba(30, 41, 59, 0.9);
444
+ color: $mu-dark-text-primary;
445
+ border-color: rgba(255, 255, 255, 0.1);
446
+ }
447
+ }
448
+ }
449
+ </style>
@@ -0,0 +1,225 @@
1
+ <template>
2
+ <view v-if="loading" class="mu-skeleton" :class="[
3
+ animate && 'mu-skeleton--animate',
4
+ `mu-skeleton--${type}`
5
+ ]">
6
+ <!-- 1. 卡片/商品形态 (type="card") -->
7
+ <view v-if="type === 'card'" class="mu-skeleton__card-wrap">
8
+ <view class="mu-skeleton__cover"></view>
9
+ <view class="mu-skeleton__card-body">
10
+ <view class="mu-skeleton__line" style="width: 80%; height: 32rpx; margin-bottom: 16rpx;"></view>
11
+ <view class="mu-skeleton__line" style="width: 50%; height: 24rpx; margin-bottom: 24rpx;"></view>
12
+ <view class="mu-skeleton__card-foot">
13
+ <view class="mu-skeleton__line" style="width: 30%; height: 36rpx;"></view>
14
+ <view class="mu-skeleton__line" style="width: 24%; height: 48rpx; border-radius: 999rpx;"></view>
15
+ </view>
16
+ </view>
17
+ </view>
18
+
19
+ <!-- 2. 标准文本/头像列表形态 (type="list" | "avatar" | "paragraph") -->
20
+ <template v-else>
21
+ <!-- 头像 -->
22
+ <view
23
+ v-if="avatar"
24
+ class="mu-skeleton__avatar"
25
+ :class="[
26
+ `mu-skeleton__avatar--${avatarSize}`,
27
+ avatarRound && 'mu-skeleton__avatar--round'
28
+ ]"
29
+ ></view>
30
+
31
+ <!-- 文本内容区块 -->
32
+ <view class="mu-skeleton__body">
33
+ <!-- 标题骨架 -->
34
+ <view v-if="title" class="mu-skeleton__title" :style="{ width: titleWidth }"></view>
35
+
36
+ <!-- 行段落骨架 -->
37
+ <view
38
+ v-for="(rowW, i) in rowsList"
39
+ :key="i"
40
+ class="mu-skeleton__row"
41
+ :style="{ width: rowW }"
42
+ ></view>
43
+ </view>
44
+ </template>
45
+ </view>
46
+
47
+ <!-- 加载完成后展示内容 -->
48
+ <slot v-else></slot>
49
+ </template>
50
+
51
+ <script setup>
52
+ import { computed } from 'vue'
53
+
54
+ const props = defineProps({
55
+ loading: { type: Boolean, default: true },
56
+ animate: { type: Boolean, default: true },
57
+ // 形态预设: paragraph(段落) | card(卡片) | avatar(带头像)
58
+ type: { type: String, default: 'paragraph' },
59
+ avatar: { type: Boolean, default: false },
60
+ avatarSize: { type: String, default: 'md' }, // sm | md | lg
61
+ avatarRound: { type: Boolean, default: true },
62
+ title: { type: Boolean, default: true },
63
+ titleWidth: { type: String, default: '40%' },
64
+ rows: { type: [Number, Array], default: 3 },
65
+ rowsWidth: { type: Array, default: () => [] }
66
+ })
67
+
68
+ const rowsList = computed(() => {
69
+ const count = typeof props.rows === 'number' ? props.rows : props.rows.length
70
+ return Array.from({ length: count }, (_, i) => {
71
+ if (props.rowsWidth[i]) return props.rowsWidth[i]
72
+ if (i === count - 1) return '60%'
73
+ return '100%'
74
+ })
75
+ })
76
+ </script>
77
+
78
+ <style lang="scss" scoped>
79
+ @import "@/uni.scss";
80
+
81
+ .mu-skeleton {
82
+ display: flex;
83
+ width: 100%;
84
+ box-sizing: border-box;
85
+
86
+ /* 骨架发光扫光动画 */
87
+ &--animate {
88
+ .mu-skeleton__avatar,
89
+ .mu-skeleton__title,
90
+ .mu-skeleton__row,
91
+ .mu-skeleton__cover,
92
+ .mu-skeleton__line {
93
+ background: linear-gradient(90deg, #F1F5F9 25%, #E2E8F0 50%, #F1F5F9 75%);
94
+ background-size: 400% 100%;
95
+ animation: mu-skeleton-sweep 1.6s ease-in-out infinite;
96
+ }
97
+ }
98
+
99
+ &__avatar {
100
+ flex-shrink: 0;
101
+ margin-right: 24rpx;
102
+ background: #F1F5F9;
103
+
104
+ &--sm { width: 64rpx; height: 64rpx; border-radius: 12rpx; }
105
+ &--md { width: 80rpx; height: 80rpx; border-radius: 16rpx; }
106
+ &--lg { width: 100rpx; height: 100rpx; border-radius: 20rpx; }
107
+ &--round { border-radius: 50% !important; }
108
+ }
109
+
110
+ &__body {
111
+ flex: 1;
112
+ min-width: 0;
113
+ }
114
+
115
+ &__title {
116
+ height: 32rpx;
117
+ background: #F1F5F9;
118
+ border-radius: 8rpx;
119
+ margin-bottom: 20rpx;
120
+ }
121
+
122
+ &__row {
123
+ height: 24rpx;
124
+ background: #F1F5F9;
125
+ border-radius: 6rpx;
126
+ margin-bottom: 16rpx;
127
+
128
+ &:last-child { margin-bottom: 0; }
129
+ }
130
+
131
+ /* ========== 卡片骨架形态 (card) ========== */
132
+ &__card-wrap {
133
+ width: 100%;
134
+ border-radius: 20rpx;
135
+ overflow: hidden;
136
+ background: #ffffff;
137
+ border: 1rpx solid rgba(226, 232, 240, 0.6);
138
+ }
139
+
140
+ &__cover {
141
+ width: 100%;
142
+ height: 240rpx;
143
+ background: #F1F5F9;
144
+ }
145
+
146
+ &__card-body {
147
+ padding: 24rpx;
148
+ }
149
+
150
+ &__line {
151
+ background: #F1F5F9;
152
+ border-radius: 8rpx;
153
+ }
154
+
155
+ &__card-foot {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: space-between;
159
+ margin-top: 16rpx;
160
+ }
161
+ }
162
+
163
+ @keyframes mu-skeleton-sweep {
164
+ 0% { background-position: 100% 0; }
165
+ 100% { background-position: 0 0; }
166
+ }
167
+
168
+ .mu-dark, page.mu-dark {
169
+
170
+ .mu-skeleton {
171
+ &--animate {
172
+ .mu-skeleton__avatar,
173
+ .mu-skeleton__title,
174
+ .mu-skeleton__row,
175
+ .mu-skeleton__cover,
176
+ .mu-skeleton__line {
177
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.04) 25%, rgba(255, 255, 255, 0.09) 50%, rgba(255, 255, 255, 0.04) 75%);
178
+ background-size: 400% 100%;
179
+ }
180
+ }
181
+
182
+ &__avatar,
183
+ &__title,
184
+ &__row,
185
+ &__cover,
186
+ &__line {
187
+ background: rgba(255, 255, 255, 0.04);
188
+ }
189
+
190
+ &__card-wrap {
191
+ background: $mu-dark-bg-base;
192
+ border-color: rgba(255, 255, 255, 0.08);
193
+ }
194
+ }
195
+ }
196
+
197
+ @media (prefers-color-scheme: dark) {
198
+
199
+ .mu-skeleton {
200
+ &--animate {
201
+ .mu-skeleton__avatar,
202
+ .mu-skeleton__title,
203
+ .mu-skeleton__row,
204
+ .mu-skeleton__cover,
205
+ .mu-skeleton__line {
206
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.04) 25%, rgba(255, 255, 255, 0.09) 50%, rgba(255, 255, 255, 0.04) 75%);
207
+ background-size: 400% 100%;
208
+ }
209
+ }
210
+
211
+ &__avatar,
212
+ &__title,
213
+ &__row,
214
+ &__cover,
215
+ &__line {
216
+ background: rgba(255, 255, 255, 0.04);
217
+ }
218
+
219
+ &__card-wrap {
220
+ background: $mu-dark-bg-base;
221
+ border-color: rgba(255, 255, 255, 0.08);
222
+ }
223
+ }
224
+ }
225
+ </style>