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,156 @@
1
+ <template>
2
+ <!-- 线性进度条 -->
3
+ <view v-if="type === 'line'" class="mu-progress mu-progress--line">
4
+ <view v-if="showInfo" class="mu-progress__label">
5
+ <slot><text class="mu-progress__label-text">{{ percent }}%</text></slot>
6
+ </view>
7
+ <view class="mu-progress__track" :style="{ height: strokeWidth + 'rpx', background: bgColor }">
8
+ <view class="mu-progress__bar" :style="barStyle"></view>
9
+ </view>
10
+ </view>
11
+ <!-- 环形进度条 -->
12
+ <view v-else class="mu-progress mu-progress--circle" :style="circleWrapperStyle">
13
+ <view class="mu-progress__circle">
14
+ <!-- #ifdef H5 -->
15
+ <svg :width="circleSize" :height="circleSize" :viewBox="`0 0 ${circleSize} ${circleSize}`">
16
+ <circle :cx="half" :cy="half" :r="radius" fill="none" :stroke="bgColor" :stroke-width="strokeWidth" />
17
+ <circle :cx="half" :cy="half" :r="radius" fill="none" :stroke="activeColorComputed"
18
+ :stroke-width="strokeWidth" :stroke-linecap="'round'"
19
+ :stroke-dasharray="circumference" :stroke-dashoffset="dashOffset"
20
+ :style="{ transition: 'stroke-dashoffset 0.3s cubic-bezier(0.32, 0.72, 0, 1)' }"
21
+ transform="rotate(-90)" :transform-origin="`${half} ${half}`" />
22
+ </svg>
23
+ <!-- #endif -->
24
+ <!-- #ifndef H5 -->
25
+ <canvas :canvas-id="canvasId" :id="canvasId" :style="{ width: circleSize + 'px', height: circleSize + 'px' }" />
26
+ <!-- #endif -->
27
+ </view>
28
+ <view v-if="showInfo" class="mu-progress__circle-info">
29
+ <slot><text class="mu-progress__circle-text">{{ percent }}%</text></slot>
30
+ </view>
31
+ </view>
32
+ </template>
33
+
34
+ <script setup>
35
+ import { computed, watch, onMounted, ref, getCurrentInstance } from 'vue'
36
+ const props = defineProps({
37
+ percent: { type: Number, default: 0 },
38
+ type: { type: String, default: 'line' },
39
+ strokeWidth: { type: [Number, String], default: 12 },
40
+ activeColor: { type: String, default: '' },
41
+ bgColor: { type: String, default: '' },
42
+ showInfo: { type: Boolean, default: true },
43
+ size: { type: [Number, String], default: 120 },
44
+ status: { type: String, default: '' }
45
+ })
46
+ const colorMap = { success: '#00B42A', warning: '#FF7D00', error: '#F53F3F' }
47
+ const activeColorComputed = computed(() => props.activeColor || colorMap[props.status] || '#165DFF')
48
+ const barStyle = computed(() => ({
49
+ width: Math.min(Math.max(props.percent, 0), 100) + '%',
50
+ background: props.activeColor ? props.activeColor : '#171717',
51
+ height: '100%',
52
+ borderRadius: '999rpx',
53
+ boxShadow: '0 4rpx 14rpx rgba(22, 93, 255, 0.35)',
54
+ transition: 'width 0.35s cubic-bezier(0.32, 0.72, 0, 1)'
55
+ }))
56
+ const circleSize = computed(() => Number(props.size))
57
+ const half = computed(() => circleSize.value / 2)
58
+ const strokeW = computed(() => Number(props.strokeWidth))
59
+ const radius = computed(() => (circleSize.value - strokeW.value) / 2)
60
+ const circumference = computed(() => 2 * Math.PI * radius.value)
61
+ const dashOffset = computed(() => circumference.value * (1 - Math.min(Math.max(props.percent, 0), 100) / 100))
62
+ const circleWrapperStyle = computed(() => ({
63
+ width: circleSize.value + 'px', height: circleSize.value + 'px', position: 'relative', display: 'inline-flex',
64
+ alignItems: 'center', justifyContent: 'center'
65
+ }))
66
+ const canvasId = ref('mu-progress-' + Math.random().toString(36).slice(2, 8))
67
+ const ctx = ref(null)
68
+ function drawCircle() {
69
+ // #ifndef H5
70
+ const instance = getCurrentInstance()
71
+ if (!ctx.value) {
72
+ ctx.value = uni.createCanvasContext(canvasId.value, (instance && instance.proxy))
73
+ }
74
+ const c = ctx.value
75
+ const s = circleSize.value
76
+ const r = radius.value
77
+ const sw = strokeW.value
78
+ c.clearRect(0, 0, s, s)
79
+ c.beginPath()
80
+ c.arc(half.value, half.value, r, 0, 2 * Math.PI)
81
+ c.setStrokeStyle(props.bgColor || 'rgba(226, 232, 240, 0.6)')
82
+ c.setLineWidth(sw)
83
+ c.stroke()
84
+ const angle = (Math.min(Math.max(props.percent, 0), 100) / 100) * 2 * Math.PI - Math.PI / 2
85
+ if (props.percent > 0) {
86
+ c.beginPath()
87
+ c.arc(half.value, half.value, r, -Math.PI / 2, angle)
88
+ c.setStrokeStyle(activeColorComputed.value)
89
+ c.setLineWidth(sw)
90
+ c.setLineCap('round')
91
+ c.stroke()
92
+ }
93
+ c.draw()
94
+ // #endif
95
+ }
96
+ onMounted(() => { if (props.type === 'circle') drawCircle() })
97
+ watch(() => props.percent, () => { if (props.type === 'circle') drawCircle() })
98
+ </script>
99
+
100
+ <style lang="scss" scoped>
101
+ .mu-progress {
102
+ &--line {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: $mu-space-2;
106
+ width: 100%;
107
+ }
108
+ &__label { &-text { font-size: $mu-font-xs; color: $mu-text-secondary; font-weight: 700; } }
109
+ &__track {
110
+ width: 100%;
111
+ border-radius: $mu-radius-full;
112
+ overflow: hidden;
113
+ background: $mu-border-light;
114
+ }
115
+ &__bar { min-width: 0; }
116
+ &--circle {
117
+ position: relative;
118
+ display: inline-flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ }
122
+ &__circle {
123
+ position: absolute;
124
+ inset: 0;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ }
129
+ &__circle-info {
130
+ position: relative;
131
+ z-index: 1;
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ }
136
+ &__circle-text { font-size: $mu-font-sm; color: $mu-text-primary; font-weight: 800; }
137
+ }
138
+
139
+ .mu-dark, page.mu-dark {
140
+
141
+ .mu-progress {
142
+ &__label-text { color: $mu-dark-text-secondary; }
143
+ &__track { background: rgba(255, 255, 255, 0.08); }
144
+ &__circle-text { color: $mu-dark-text-primary; }
145
+ }
146
+ }
147
+
148
+ @media (prefers-color-scheme: dark) {
149
+
150
+ .mu-progress {
151
+ &__label-text { color: $mu-dark-text-secondary; }
152
+ &__track { background: rgba(255, 255, 255, 0.08); }
153
+ &__circle-text { color: $mu-dark-text-primary; }
154
+ }
155
+ }
156
+ </style>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <view class="mu-pull-refresh">
3
+ <scroll-view scroll-y class="mu-pull-refresh__scroll"
4
+ :style="{ height: scrollHeight }"
5
+ @scrolltolower="onScrollBottom"
6
+ :scroll-top="scrollTop"
7
+ :refresher-enabled="true"
8
+ :refresher-triggered="refreshing"
9
+ @refresherrefresh="onRefresh">
10
+ <slot></slot>
11
+ </scroll-view>
12
+ </view>
13
+ </template>
14
+
15
+ <script setup>
16
+ import { ref, watch } from 'vue'
17
+ const props = defineProps({
18
+ loading: { type: Boolean, default: false },
19
+ height: { type: String, default: '100vh' },
20
+ disabled: { type: Boolean, default: false },
21
+ successText: { type: String, default: '刷新成功' },
22
+ successDuration: { type: Number, default: 1000 }
23
+ })
24
+ const emit = defineEmits(['refresh', 'loadmore'])
25
+ const refreshing = ref(false)
26
+ const scrollTop = ref(0)
27
+ const scrollHeight = ref(props.height)
28
+ async function onRefresh() {
29
+ if (props.disabled) return
30
+ refreshing.value = true
31
+ emit('refresh')
32
+ }
33
+ function finishRefresh() {
34
+ refreshing.value = false
35
+ if (props.successText) {
36
+ uni.showToast({ title: props.successText, icon: 'none', duration: props.successDuration })
37
+ }
38
+ }
39
+ function onScrollBottom() { emit('loadmore') }
40
+ // 支持通过 loading prop 控制刷新状态
41
+ watch(() => props.loading, (v) => {
42
+ if (v) refreshing.value = true
43
+ else if (refreshing.value) finishRefresh()
44
+ })
45
+ defineExpose({ finishRefresh, startRefresh: () => { refreshing.value = true; emit('refresh') } })
46
+ </script>
47
+
48
+ <style lang="scss" scoped>
49
+ .mu-pull-refresh {
50
+ &__scroll {
51
+ width: 100%;
52
+ }
53
+ }
54
+ </style>
@@ -0,0 +1,340 @@
1
+ <template>
2
+ <view class="mu-qrcode" :style="containerStyle" @tap="handleQrClick">
3
+ <!-- 二维码视口容器 (全端极速纯净 Image + 离屏 Canvas,100% 秒扫识别 + 0 缝隙) -->
4
+ <view class="mu-qrcode__canvas-box" :style="{ width: `${sizeNumber}px`, height: `${sizeNumber}px` }">
5
+ <!-- 最终生成的高清 Base64 / 临时图片 (支持全端长按识别与秒扫) -->
6
+ <image
7
+ v-if="imgUrl"
8
+ class="mu-qrcode__img"
9
+ :src="imgUrl"
10
+ mode="aspectFit"
11
+ :style="{ width: `${sizeNumber}px`, height: `${sizeNumber}px` }"
12
+ :show-menu-by-longpress="true"
13
+ />
14
+
15
+ <!-- 离屏 Canvas (后台快速离屏绘制生成图片) -->
16
+ <canvas
17
+ :canvas-id="canvasId"
18
+ :id="canvasId"
19
+ class="mu-qrcode__offscreen-canvas"
20
+ :style="{ width: `${sizeNumber * 4}px`, height: `${sizeNumber * 4}px` }"
21
+ />
22
+
23
+ <!-- 中间 Logo 头像 (带白底防覆盖) -->
24
+ <view
25
+ v-if="logo && imgUrl"
26
+ class="mu-qrcode__logo-box"
27
+ :style="logoBoxStyle"
28
+ >
29
+ <image class="mu-qrcode__logo-img" :src="logo" mode="aspectFit" />
30
+ </view>
31
+ </view>
32
+
33
+ <!-- 底部说明文案 -->
34
+ <text v-if="text" class="mu-qrcode__text">{{ text }}</text>
35
+
36
+ <!-- 快捷操作按钮组 (下载/保存) -->
37
+ <view v-if="showDownload" class="mu-qrcode__actions" @tap.stop>
38
+ <view class="mu-qrcode__btn mu-touchable-flat" @tap="handleDownload">
39
+ <mu-icon name="download" :size="14" color="#171717" />
40
+ <text class="mu-qrcode__btn-text">保存二维码</text>
41
+ </view>
42
+ </view>
43
+
44
+ <!-- 全屏大图放大查看弹窗 -->
45
+ <mu-image-preview
46
+ v-model:show="previewShow"
47
+ :images="previewImages"
48
+ />
49
+ </view>
50
+ </template>
51
+
52
+ <script setup>
53
+ import { ref, computed, watch, onMounted, getCurrentInstance } from 'vue'
54
+ import QRCode from './qrcode.js'
55
+
56
+ const props = defineProps({
57
+ value: { type: String, default: '' }, // 待生成的二维码内容
58
+ size: { type: [Number, String], default: 180 }, // 尺寸 (px)
59
+ foreground: { type: String, default: '#000000' }, // 前景色/二维码暗块色
60
+ background: { type: String, default: '#FFFFFF' }, // 背景色
61
+ padding: { type: Number, default: 12 }, // 内边距 (px)
62
+ logo: { type: String, default: '' }, // 中间 Logo 图片 URL
63
+ logoSize: { type: Number, default: 40 }, // Logo 尺寸 (px)
64
+ text: { type: String, default: '' }, // 底部说明文本
65
+ preview: { type: Boolean, default: true }, // 点击是否放大查看
66
+ showDownload: { type: Boolean, default: false } // 是否显示下载按钮
67
+ })
68
+
69
+ const emit = defineEmits(['click', 'success', 'fail', 'download'])
70
+
71
+ const instance = getCurrentInstance()
72
+ const canvasId = ref('mu_qr_cv_' + Math.floor(Math.random() * 1000000))
73
+ const imgUrl = ref('')
74
+ const previewShow = ref(false)
75
+
76
+ const sizeNumber = computed(() => {
77
+ const n = Number(props.size)
78
+ return isNaN(n) ? 180 : n
79
+ })
80
+
81
+ const containerStyle = computed(() => ({
82
+ padding: '24rpx',
83
+ background: props.background,
84
+ borderRadius: '24rpx',
85
+ boxShadow: '0 8rpx 32rpx rgba(15, 23, 42, 0.06)'
86
+ }))
87
+
88
+ const logoBoxStyle = computed(() => ({
89
+ width: `${props.logoSize}px`,
90
+ height: `${props.logoSize}px`,
91
+ borderRadius: '16rpx',
92
+ background: '#FFFFFF',
93
+ padding: '4px',
94
+ boxShadow: '0 4rpx 16rpx rgba(0, 0, 0, 0.15)'
95
+ }))
96
+
97
+ const previewImages = computed(() => {
98
+ if (imgUrl.value) {
99
+ return [{ url: imgUrl.value, desc: props.text || props.value }]
100
+ }
101
+ return []
102
+ })
103
+
104
+ function renderQRCode() {
105
+ if (!props.value) return
106
+ try {
107
+ // 使用官方标准 qrcode-generator 引擎 (0: 自动识别版本大小, 'M': 15% 纠错率)
108
+ const qrcode = QRCode(0, 'M')
109
+ qrcode.addData(props.value)
110
+ qrcode.make()
111
+ const count = qrcode.getModuleCount()
112
+
113
+ // 定义 4 倍超清渲染像素倍率 (HD DPR)
114
+ const dpr = 4
115
+
116
+ // H5 模式直接使用 0 延迟 Base64 Canvas 导出图片,全平台 100% 没有任何缝隙
117
+ // #ifdef H5
118
+ const canvas = document.createElement('canvas')
119
+ const sz = sizeNumber.value * dpr
120
+ canvas.width = sz
121
+ canvas.height = sz
122
+ const ctx = canvas.getContext('2d')
123
+
124
+ const pad = props.padding * dpr
125
+ const drawSize = sz - pad * 2
126
+ const tile = drawSize / count
127
+
128
+ ctx.fillStyle = props.background
129
+ ctx.fillRect(0, 0, sz, sz)
130
+
131
+ ctx.fillStyle = props.foreground
132
+ for (let row = 0; row < count; row++) {
133
+ for (let col = 0; col < count; col++) {
134
+ if (qrcode.isDark(row, col)) {
135
+ const x = pad + col * tile
136
+ const y = pad + row * tile
137
+ // 消除抗锯齿缝隙
138
+ ctx.fillRect(x, y, tile + 0.5, tile + 0.5)
139
+ }
140
+ }
141
+ }
142
+
143
+ imgUrl.value = canvas.toDataURL('image/png')
144
+ emit('success')
145
+ // #endif
146
+
147
+ // 非 H5 模式使用 uni.createCanvasContext 导出图片
148
+ // #ifndef H5
149
+ const szNonH5 = sizeNumber.value * dpr
150
+ const padNonH5 = props.padding * dpr
151
+ const drawSizeNonH5 = szNonH5 - padNonH5 * 2
152
+ const tileNonH5 = drawSizeNonH5 / count
153
+
154
+ const uctx = uni.createCanvasContext(canvasId.value, (instance && instance.proxy))
155
+ uctx.setFillStyle(props.background)
156
+ uctx.fillRect(0, 0, szNonH5, szNonH5)
157
+
158
+ uctx.setFillStyle(props.foreground)
159
+ for (let row = 0; row < count; row++) {
160
+ for (let col = 0; col < count; col++) {
161
+ if (qrcode.isDark(row, col)) {
162
+ const x = Math.round(padNonH5 + col * tileNonH5)
163
+ const y = Math.round(padNonH5 + row * tileNonH5)
164
+ const w = Math.ceil((col + 1) * tileNonH5) - Math.floor(col * tileNonH5)
165
+ const h = Math.ceil((row + 1) * tileNonH5) - Math.floor(row * tileNonH5)
166
+ uctx.fillRect(x, y, w, h)
167
+ }
168
+ }
169
+ }
170
+
171
+ uctx.draw(false, () => {
172
+ setTimeout(() => {
173
+ uni.canvasToTempFilePath({
174
+ canvasId: canvasId.value,
175
+ destWidth: szNonH5,
176
+ destHeight: szNonH5,
177
+ success: (res) => {
178
+ imgUrl.value = res.tempFilePath
179
+ emit('success')
180
+ }
181
+ }, (instance && instance.proxy))
182
+ }, 150)
183
+ })
184
+ // #endif
185
+
186
+ } catch (e) {
187
+ console.error('[mu-qrcode] 生成出错:', e)
188
+ emit('fail', e)
189
+ }
190
+ }
191
+
192
+ function handleQrClick(e) {
193
+ emit('click', e)
194
+ if (props.preview) {
195
+ previewShow.value = true
196
+ }
197
+ }
198
+
199
+ function handleDownload() {
200
+ saveImage()
201
+ }
202
+
203
+ function saveImage() {
204
+ if (imgUrl.value) {
205
+ saveToAlbum(imgUrl.value)
206
+ return
207
+ }
208
+
209
+ uni.canvasToTempFilePath({
210
+ canvasId: canvasId.value,
211
+ success: (res) => {
212
+ saveToAlbum(res.tempFilePath)
213
+ },
214
+ fail: () => {
215
+ uni.showToast({ title: '导出图片失败', icon: 'none' })
216
+ }
217
+ }, (instance && instance.proxy))
218
+ }
219
+
220
+ function saveToAlbum(filePath) {
221
+ // #ifdef H5
222
+ try {
223
+ const a = document.createElement('a')
224
+ a.href = filePath
225
+ a.download = `qrcode_${Date.now()}.png`
226
+ document.body.appendChild(a)
227
+ a.click()
228
+ document.body.removeChild(a)
229
+ uni.showToast({ title: '已下载二维码图片', icon: 'success' })
230
+ } catch (e) {
231
+ uni.showToast({ title: '长按二维码保存到相册', icon: 'none' })
232
+ }
233
+ // #endif
234
+
235
+ // #ifndef H5
236
+ uni.saveImageToPhotosAlbum({
237
+ filePath,
238
+ success: () => {
239
+ uni.showToast({ title: '已保存到相册', icon: 'success' })
240
+ emit('download', { filePath })
241
+ },
242
+ fail: () => {
243
+ uni.showToast({ title: '保存失败或拒绝授权', icon: 'none' })
244
+ }
245
+ })
246
+ // #endif
247
+ }
248
+
249
+ onMounted(() => {
250
+ renderQRCode()
251
+ })
252
+
253
+ watch(() => [props.value, props.size, props.foreground, props.background], () => {
254
+ renderQRCode()
255
+ })
256
+
257
+ defineExpose({ renderQRCode, saveImage, openPreview: () => { previewShow.value = true } })
258
+ </script>
259
+
260
+ <style lang="scss" scoped>
261
+ @import "@/uni.scss";
262
+
263
+ .mu-qrcode {
264
+ display: inline-flex;
265
+ flex-direction: column;
266
+ align-items: center;
267
+ justify-content: center;
268
+ box-sizing: border-box;
269
+ cursor: pointer;
270
+
271
+ &__canvas-box {
272
+ position: relative;
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: center;
276
+ }
277
+
278
+ &__img {
279
+ display: block;
280
+ object-fit: contain;
281
+ }
282
+
283
+ &__offscreen-canvas {
284
+ position: absolute;
285
+ left: -9999px;
286
+ top: -9999px;
287
+ opacity: 0;
288
+ pointer-events: none;
289
+ }
290
+
291
+ &__logo-box {
292
+ position: absolute;
293
+ top: 50%;
294
+ left: 50%;
295
+ transform: translate(-50%, -50%);
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ z-index: 3;
300
+ box-sizing: border-box;
301
+ }
302
+
303
+ &__logo-img {
304
+ width: 100%;
305
+ height: 100%;
306
+ border-radius: 12rpx;
307
+ }
308
+
309
+ &__text {
310
+ font-size: 24rpx;
311
+ color: #64748B;
312
+ font-weight: 600;
313
+ margin-top: 16rpx;
314
+ letter-spacing: 1rpx;
315
+ }
316
+
317
+ &__actions {
318
+ margin-top: 16rpx;
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ }
323
+
324
+ &__btn {
325
+ display: flex;
326
+ align-items: center;
327
+ padding: 8rpx 20rpx;
328
+ background: #F1F5F9;
329
+ border-radius: 999rpx;
330
+ cursor: pointer;
331
+
332
+ &-text {
333
+ font-size: 22rpx;
334
+ font-weight: 700;
335
+ color: #171717;
336
+ margin-left: 6rpx;
337
+ }
338
+ }
339
+ }
340
+ </style>