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,220 @@
1
+ <template>
2
+ <!-- MuZhiYu Flat Alert Component -->
3
+ <view v-if="visible" class="mu-alert" :class="alertClass" :style="alertStyle">
4
+ <!-- 纯粹高清矢量 SVG 图标 -->
5
+ <view v-if="showIcon" class="mu-alert__icon">
6
+ <slot name="icon">
7
+ <mu-icon :name="computedIconName" :size="20" :color="iconColor" />
8
+ </slot>
9
+ </view>
10
+
11
+ <!-- 文本主体 -->
12
+ <view class="mu-alert__body">
13
+ <text v-if="title" class="mu-alert__title">{{ title }}</text>
14
+ <text class="mu-alert__desc"><slot>{{ description }}</slot></text>
15
+ </view>
16
+
17
+ <!-- 矢量 SVG 关闭按钮 -->
18
+ <view v-if="closable" class="mu-alert__close mu-touchable-flat" @tap="handleClose">
19
+ <mu-icon name="close" :size="14" color="currentColor" />
20
+ </view>
21
+ </view>
22
+ </template>
23
+
24
+ <script setup>
25
+ import { ref, computed } from 'vue'
26
+
27
+ const props = defineProps({
28
+ type: { type: String, default: 'info' }, // info | success | warning | error
29
+ variant: { type: String, default: 'glass' }, // glass | filled | plain
30
+ icon: { type: String, default: '' }, // 自定义矢量图标名称
31
+ title: { type: String, default: '' },
32
+ description: { type: String, default: '' },
33
+ closable: { type: Boolean, default: false },
34
+ showIcon: { type: Boolean, default: true },
35
+ margin: { type: String, default: '' },
36
+ round: { type: Boolean, default: false }
37
+ })
38
+
39
+ const emit = defineEmits(['close'])
40
+
41
+ const visible = ref(true)
42
+
43
+ const computedIconName = computed(() => {
44
+ if (props.icon) return props.icon
45
+ const map = {
46
+ info: 'info',
47
+ success: 'success',
48
+ warning: 'warning',
49
+ error: 'error'
50
+ }
51
+ return map[props.type] || 'info'
52
+ })
53
+
54
+ const alertClass = computed(() => [
55
+ `mu-alert--${props.type}`,
56
+ `mu-alert--${props.variant}`,
57
+ props.round && 'mu-alert--round'
58
+ ])
59
+
60
+ const alertStyle = computed(() => ({
61
+ margin: props.margin || ''
62
+ }))
63
+
64
+ const iconColor = computed(() => {
65
+ if (props.variant === 'filled') return '#ffffff'
66
+ const map = {
67
+ info: '#0284C7',
68
+ success: '#059669',
69
+ warning: '#D97706',
70
+ error: '#E11D48'
71
+ }
72
+ return map[props.type] || '#171717'
73
+ })
74
+
75
+ function handleClose() {
76
+ visible.value = false
77
+ emit('close')
78
+ }
79
+ </script>
80
+
81
+ <style lang="scss" scoped>
82
+ @import "@/uni.scss";
83
+
84
+ .mu-alert {
85
+ position: relative;
86
+ display: flex;
87
+ align-items: flex-start;
88
+ padding: $mu-space-4 $mu-space-5;
89
+ border-radius: $mu-radius-xl;
90
+ border: 1rpx solid transparent;
91
+ overflow: hidden;
92
+ box-shadow: none;
93
+ transition: all $mu-duration $mu-ease;
94
+
95
+ &--round {
96
+ border-radius: $mu-radius-full;
97
+ }
98
+
99
+ &__icon {
100
+ margin-right: $mu-space-3;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ line-height: 1;
105
+ margin-top: 4rpx;
106
+ flex-shrink: 0;
107
+ }
108
+
109
+ &__body {
110
+ flex: 1;
111
+ }
112
+
113
+ &__title {
114
+ font-size: $mu-font-base;
115
+ font-weight: 800;
116
+ color: $mu-text-primary;
117
+ display: block;
118
+ margin-bottom: 4rpx;
119
+ letter-spacing: -0.3rpx;
120
+ line-height: 1.4;
121
+ }
122
+
123
+ &__desc {
124
+ font-size: $mu-font-sm;
125
+ color: $mu-text-secondary;
126
+ line-height: 1.5;
127
+ font-weight: 500;
128
+ }
129
+
130
+ &__close {
131
+ margin-left: $mu-space-3;
132
+ width: 36rpx;
133
+ height: 36rpx;
134
+ border-radius: 50%;
135
+ background: rgba(15, 23, 42, 0.05);
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ cursor: pointer;
140
+ margin-top: 4rpx;
141
+ color: $mu-text-secondary;
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ /* ===== 1. Glass 极简纯色扁平样式 ===== */
146
+ &--info#{&}--glass {
147
+ background: rgba(56, 189, 248, 0.08);
148
+ border-color: rgba(56, 189, 248, 0.22);
149
+ }
150
+
151
+ &--success#{&}--glass {
152
+ background: rgba(16, 185, 129, 0.08);
153
+ border-color: rgba(16, 185, 129, 0.22);
154
+ }
155
+
156
+ &--warning#{&}--glass {
157
+ background: rgba(245, 158, 11, 0.08);
158
+ border-color: rgba(245, 158, 11, 0.22);
159
+ }
160
+
161
+ &--error#{&}--glass {
162
+ background: rgba(244, 63, 94, 0.08);
163
+ border-color: rgba(244, 63, 94, 0.22);
164
+ }
165
+
166
+ /* ===== 2. Filled 饱满纯色填充样式 (无对角渐变色差线) ===== */
167
+ &--info#{&}--filled {
168
+ background: #0284C7;
169
+ color: #ffffff;
170
+ border-color: #0284C7;
171
+ .mu-alert__title, .mu-alert__desc { color: #ffffff; }
172
+ .mu-alert__close { background: rgba(255, 255, 255, 0.2); color: #ffffff; }
173
+ }
174
+ &--success#{&}--filled {
175
+ background: #059669;
176
+ color: #ffffff;
177
+ border-color: #059669;
178
+ .mu-alert__title, .mu-alert__desc { color: #ffffff; }
179
+ .mu-alert__close { background: rgba(255, 255, 255, 0.2); color: #ffffff; }
180
+ }
181
+ &--warning#{&}--filled {
182
+ background: #D97706;
183
+ color: #ffffff;
184
+ border-color: #D97706;
185
+ .mu-alert__title, .mu-alert__desc { color: #ffffff; }
186
+ .mu-alert__close { background: rgba(255, 255, 255, 0.2); color: #ffffff; }
187
+ }
188
+ &--error#{&}--filled {
189
+ background: #E11D48;
190
+ color: #ffffff;
191
+ border-color: #E11D48;
192
+ .mu-alert__title, .mu-alert__desc { color: #ffffff; }
193
+ .mu-alert__close { background: rgba(255, 255, 255, 0.2); color: #ffffff; }
194
+ }
195
+ }
196
+
197
+ .mu-dark, page.mu-dark {
198
+
199
+ .mu-alert {
200
+ &--glass {
201
+ background: rgba(255, 255, 255, 0.04) !important;
202
+ }
203
+ &__title { color: $mu-dark-text-primary; }
204
+ &__desc { color: $mu-dark-text-secondary; }
205
+ &__close { background: rgba(255, 255, 255, 0.1); color: $mu-dark-text-secondary; }
206
+ }
207
+ }
208
+
209
+ @media (prefers-color-scheme: dark) {
210
+
211
+ .mu-alert {
212
+ &--glass {
213
+ background: rgba(255, 255, 255, 0.04) !important;
214
+ }
215
+ &__title { color: $mu-dark-text-primary; }
216
+ &__desc { color: $mu-dark-text-secondary; }
217
+ &__close { background: rgba(255, 255, 255, 0.1); color: $mu-dark-text-secondary; }
218
+ }
219
+ }
220
+ </style>
@@ -0,0 +1,161 @@
1
+ <template>
2
+ <view v-show="show" class="mu-back-top" :style="positionStyle" @tap="handleClick">
3
+ <slot>
4
+ <view class="mu-back-top__inner" :style="{ background }">
5
+ <mu-icon :name="icon" :size="iconSize" :color="iconColor || '#171717'" />
6
+ <text v-if="text" class="mu-back-top__text" :style="{ color: textColor }">{{ text }}</text>
7
+ </view>
8
+ </slot>
9
+ </view>
10
+ </template>
11
+
12
+ <script setup>
13
+ /**
14
+ * mu-back-top 返回顶部
15
+ * @description 页面滑动时出现的悬浮返回顶部按钮
16
+ * @property {string} icon - 矢量图标名称 (默认 'arrow-up' 或 'chevron-up')
17
+ * @property {number|string} iconSize - 图标尺寸 (默认 22)
18
+ * @property {string} iconColor - 图标颜色
19
+ * @property {string} text - 辅助说明文本 (如 'TOP' 或 '顶部')
20
+ * @property {string} textColor - 文本色彩
21
+ * @property {string} background - 按钮背景色
22
+ * @property {number|string} bottom - 距离底部距离 rpx
23
+ * @property {number|string} right - 距离右侧距离 rpx
24
+ * @property {number} threshold - 触发显示的滚动高度 threshold (px)
25
+ * @property {number} duration - 滚动动画时长 ms
26
+ * @event {Function} click - 点击时触发
27
+ */
28
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
29
+
30
+ const props = defineProps({
31
+ text: { type: String, default: '' },
32
+ icon: { type: String, default: 'arrow-up' },
33
+ iconSize: { type: [Number, String], default: 22 },
34
+ iconColor: { type: String, default: '' },
35
+ textColor: { type: String, default: '' },
36
+ background: { type: String, default: '' },
37
+ bottom: { type: [Number, String], default: 120 },
38
+ right: { type: [Number, String], default: 40 },
39
+ threshold: { type: Number, default: 150 },
40
+ duration: { type: Number, default: 300 }
41
+ })
42
+
43
+ const emit = defineEmits(['click'])
44
+ const show = ref(false)
45
+
46
+ function formatPosUnit(val, defaultVal = '120rpx') {
47
+ if (typeof val === 'number') return `${val}rpx`
48
+ if (typeof val === 'string') {
49
+ if (val.endsWith('rpx') || val.endsWith('px')) return val
50
+ return `${val}rpx`
51
+ }
52
+ return defaultVal
53
+ }
54
+
55
+ const positionStyle = computed(() => ({
56
+ bottom: formatPosUnit(props.bottom, '120rpx'),
57
+ right: formatPosUnit(props.right, '40rpx')
58
+ }))
59
+
60
+ function onPageScroll(e) {
61
+ show.value = e.scrollTop >= props.threshold
62
+ }
63
+
64
+ function handleClick() {
65
+ uni.pageScrollTo({ scrollTop: 0, duration: props.duration })
66
+ emit('click')
67
+ }
68
+
69
+ // 页面滚动监听
70
+ let pageObserver = null
71
+ onMounted(() => {
72
+ // #ifdef H5
73
+ const handler = () => {
74
+ const st = document.documentElement.scrollTop || document.body.scrollTop
75
+ show.value = st >= props.threshold
76
+ }
77
+ window.addEventListener('scroll', handler, { passive: true })
78
+ pageObserver = { disconnect: () => window.removeEventListener('scroll', handler) }
79
+ // #endif
80
+ })
81
+
82
+ onUnmounted(() => {
83
+ if (pageObserver) pageObserver.disconnect()
84
+ })
85
+
86
+ defineExpose({ onPageScroll })
87
+ </script>
88
+
89
+ <style lang="scss" scoped>
90
+ @import "@/uni.scss";
91
+
92
+ .mu-back-top {
93
+ position: fixed;
94
+ z-index: $mu-z-fixed;
95
+ animation: mu-back-top-in 0.25s cubic-bezier(0.16, 1, 0.3, 1);
96
+ cursor: pointer;
97
+
98
+ &__inner {
99
+ width: 88rpx;
100
+ height: 88rpx;
101
+ border-radius: 50%;
102
+ background: rgba(255, 255, 255, 0.92);
103
+ backdrop-filter: blur(20px);
104
+ box-shadow: 0 10rpx 30rpx rgba(15, 23, 42, 0.12);
105
+ display: flex;
106
+ flex-direction: column;
107
+ align-items: center;
108
+ justify-content: center;
109
+ border: 1rpx solid rgba(226, 232, 240, 0.8);
110
+ transition: all 0.2s ease;
111
+
112
+ &:active {
113
+ transform: scale(0.92);
114
+ background: #F1F5F9;
115
+ }
116
+ }
117
+
118
+ &__text {
119
+ font-size: 18rpx;
120
+ color: #64748B;
121
+ font-weight: 700;
122
+ margin-top: 2rpx;
123
+ line-height: 1;
124
+ }
125
+ }
126
+
127
+ @keyframes mu-back-top-in {
128
+ from {
129
+ opacity: 0;
130
+ transform: translateY(20rpx) scale(0.8);
131
+ }
132
+ to {
133
+ opacity: 1;
134
+ transform: translateY(0) scale(1);
135
+ }
136
+ }
137
+
138
+ .mu-dark, page.mu-dark {
139
+
140
+ .mu-back-top {
141
+ &__inner {
142
+ background: rgba(24, 24, 36, 0.95) !important;
143
+ border-color: rgba(255, 255, 255, 0.12);
144
+ box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.6);
145
+ }
146
+ &__text { color: #94A3B8; }
147
+ }
148
+ }
149
+
150
+ @media (prefers-color-scheme: dark) {
151
+
152
+ .mu-back-top {
153
+ &__inner {
154
+ background: rgba(24, 24, 36, 0.95) !important;
155
+ border-color: rgba(255, 255, 255, 0.12);
156
+ box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.6);
157
+ }
158
+ &__text { color: #94A3B8; }
159
+ }
160
+ }
161
+ </style>
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <view class="mu-badge" :style="wrapStyle">
3
+ <slot></slot>
4
+ <view v-if="showBadge" class="mu-badge__dot" :class="dotClass" :style="dotStyle">
5
+ <text v-if="!actualIsDot" class="mu-badge__text">{{ displayCount }}</text>
6
+ </view>
7
+ </view>
8
+ </template>
9
+
10
+ <script setup>
11
+ import { computed } from 'vue'
12
+
13
+ const props = defineProps({
14
+ count: { type: [Number, String], default: 0 },
15
+ content: { type: [Number, String], default: '' },
16
+ max: { type: Number, default: 99 },
17
+ isDot: { type: Boolean, default: false },
18
+ dot: { type: Boolean, default: false },
19
+ hidden: { type: Boolean, default: false },
20
+ type: { type: String, default: 'error' },
21
+ background: { type: String, default: '' },
22
+ color: { type: String, default: '' },
23
+ offset: { type: Array, default: () => [0, 0] }
24
+ })
25
+
26
+ const actualCount = computed(() => {
27
+ if (props.content !== '' && props.content !== null && props.content !== undefined) return props.content
28
+ return props.count
29
+ })
30
+
31
+ const actualIsDot = computed(() => props.isDot || props.dot)
32
+
33
+ const showBadge = computed(() => !props.hidden && (actualIsDot.value || (actualCount.value !== '' && actualCount.value !== null && actualCount.value !== undefined && actualCount.value !== 0 && actualCount.value !== '0')))
34
+
35
+ const displayCount = computed(() => {
36
+ const c = actualCount.value
37
+ if (typeof c === 'number' && c > props.max) {
38
+ return `${props.max}+`
39
+ }
40
+ return c
41
+ })
42
+
43
+ const dotClass = computed(() => [
44
+ `mu-badge__dot--${props.type}`,
45
+ actualIsDot.value && 'mu-badge__dot--point'
46
+ ])
47
+
48
+ const wrapStyle = computed(() => ({
49
+ position: 'relative',
50
+ display: 'inline-flex'
51
+ }))
52
+
53
+ const dotStyle = computed(() => {
54
+ const style = {
55
+ background: props.background || '',
56
+ color: props.color || ''
57
+ }
58
+ if (props.offset && props.offset.length === 2) {
59
+ if (props.offset[0]) style.top = `${-props.offset[0]}rpx`
60
+ if (props.offset[1]) style.right = `${-props.offset[1]}rpx`
61
+ }
62
+ return style
63
+ })
64
+ </script>
65
+
66
+ <style lang="scss" scoped>
67
+ @import "@/uni.scss";
68
+
69
+ .mu-badge {
70
+ position: relative;
71
+ display: inline-flex;
72
+ vertical-align: middle;
73
+
74
+ &__dot {
75
+ position: absolute;
76
+ top: 0;
77
+ right: 0;
78
+ transform: translate(50%, -50%);
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ height: 28rpx;
83
+ min-width: 36rpx;
84
+ padding: 0 10rpx;
85
+ border-radius: 999rpx;
86
+ z-index: 10;
87
+ border: 2rpx solid #ffffff;
88
+ box-sizing: border-box;
89
+ white-space: nowrap;
90
+ text-align: center;
91
+
92
+ &--error {
93
+ background: #F43F5E;
94
+ box-shadow: 0 2rpx 8rpx rgba(244, 63, 94, 0.35);
95
+ }
96
+ &--warning {
97
+ background: #F59E0B;
98
+ box-shadow: 0 2rpx 8rpx rgba(245, 158, 11, 0.35);
99
+ }
100
+ &--success {
101
+ background: #10B981;
102
+ box-shadow: 0 2rpx 8rpx rgba(16, 185, 129, 0.35);
103
+ }
104
+ &--primary {
105
+ background: #171717;
106
+ box-shadow: 0 2rpx 8rpx rgba(99, 102, 241, 0.35);
107
+ }
108
+
109
+ &--point {
110
+ width: 14rpx;
111
+ min-width: 14rpx;
112
+ height: 14rpx;
113
+ padding: 0;
114
+ border-radius: 50%;
115
+ }
116
+ }
117
+
118
+ &__text {
119
+ font-size: 18rpx;
120
+ color: #ffffff;
121
+ font-weight: 800;
122
+ line-height: 24rpx;
123
+ height: 24rpx;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ text-align: center;
128
+ padding: 0;
129
+ margin: 0;
130
+ }
131
+ }
132
+
133
+ .mu-dark, page.mu-dark {
134
+
135
+ .mu-badge {
136
+ &__dot {
137
+ border-color: $mu-dark-bg-base;
138
+ }
139
+ }
140
+ }
141
+
142
+ @media (prefers-color-scheme: dark) {
143
+
144
+ .mu-badge {
145
+ &__dot {
146
+ border-color: $mu-dark-bg-base;
147
+ }
148
+ }
149
+ }
150
+ </style>
@@ -0,0 +1,100 @@
1
+ <template>
2
+ <view class="mu-barcode" :style="{ width: width + 'px', height: height + 'px' }">
3
+ <canvas :canvas-id="canvasId" :id="canvasId" class="mu-barcode__canvas"
4
+ :style="{ width: width + 'px', height: height + 'px' }" />
5
+ </view>
6
+ </template>
7
+
8
+ <script setup>
9
+ import { onMounted, watch, getCurrentInstance } from 'vue'
10
+ const props = defineProps({
11
+ value: { type: String, default: '' },
12
+ width: { type: Number, default: 300 },
13
+ height: { type: Number, default: 80 },
14
+ foreground: { type: String, default: '#000000' },
15
+ background: { type: String, default: '#ffffff' },
16
+ showText: { type: Boolean, default: true },
17
+ fontSize: { type: Number, default: 12 }
18
+ })
19
+ const canvasId = 'mu-bar-' + Math.random().toString(36).slice(2, 8)
20
+ const instance = getCurrentInstance()
21
+ // Code128B encoding table
22
+ const CODE128B = {
23
+ ' ': 0, '!': 1, '"': 2, '#': 3, '$': 4, '%': 5, '&': 6, "'": 7,
24
+ '(': 8, ')': 9, '*': 10, '+': 11, ',': 12, '-': 13, '.': 14, '/': 15,
25
+ '0': 16, '1': 17, '2': 18, '3': 19, '4': 20, '5': 21, '6': 22, '7': 23,
26
+ '8': 24, '9': 25, ':': 26, ';': 27, '<': 28, '=': 29, '>': 30, '?': 31,
27
+ '@': 32, 'A': 33, 'B': 34, 'C': 35, 'D': 36, 'E': 37, 'F': 38, 'G': 39,
28
+ 'H': 40, 'I': 41, 'J': 42, 'K': 43, 'L': 44, 'M': 45, 'N': 46, 'O': 47,
29
+ 'P': 48, 'Q': 49, 'R': 50, 'S': 51, 'T': 52, 'U': 53, 'V': 54, 'W': 55,
30
+ 'X': 56, 'Y': 57, 'Z': 58, '[': 59, '\\': 60, ']': 61, '^': 62, '_': 63,
31
+ '`': 64, 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71,
32
+ 'h': 72, 'i': 73, 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79,
33
+ 'p': 80, 'q': 81, 'r': 82, 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87,
34
+ 'x': 88, 'y': 89, 'z': 90, '{': 91, '|': 92, '}': 93, '~': 94
35
+ }
36
+ const PATTERNS = [
37
+ '11011001100', '11001101100', '11001100110', '10010011000', '10010001100',
38
+ '10001001100', '10011001000', '10011000100', '10001100100', '11001001000',
39
+ '11001000100', '11000100100', '10110011100', '10011011100', '10011001110',
40
+ '10111001100', '10011101100', '10011100110', '11001110010', '11001011100',
41
+ '11001001110', '11011100100', '11001110100', '11101101110', '11101001100',
42
+ '11100101100', '11100100110', '11101100100', '11100110100', '11100110010',
43
+ '11011011000', '11011000110', '11000110110', '10100011000', '10001011000',
44
+ '10001000110', '10110001000', '10001101000', '10001100010', '11010001000',
45
+ '11000101000', '11000100010', '10110111000', '10110001110', '10001101110',
46
+ '10111011000', '10111000110', '10001110110', '11101110110', '11010001110',
47
+ '11000101110', '11011101000', '11011100010', '11011101110', '11101011000',
48
+ '11101000110', '11100010110', '11101101000', '11101100010', '11100011010',
49
+ '11101111010', '11001000010', '11110001010', '10100110000', '10100001100',
50
+ '10010110000', '10010000110', '10000101100', '10000100110', '10110010000',
51
+ '10110000100', '10011010000', '10011000010', '10000110100', '10000110010',
52
+ '11000010010', '11001010000', '11110111010', '11000010100', '10001111010',
53
+ '10100111100', '10010111100', '10010011110', '10111100100', '10011110100',
54
+ '10011110010', '11110100100', '11110010100', '11110010010', '11011011110',
55
+ '11011110110', '11110110110', '10101111000', '10100011110', '10001011110',
56
+ '10111101000', '10111100010', '11110101000', '11110100010', '10111011110',
57
+ '10111101110', '11101011110', '11110101110', '11010000100', '11010010000',
58
+ '11010011100', '1100011101011'
59
+ ]
60
+ function generate() {
61
+ if (!props.value) return
62
+ const ctx = uni.createCanvasContext(canvasId, (instance && instance.proxy))
63
+ const text = props.value
64
+ const codes = [104] // Start Code B
65
+ let checksum = 104
66
+ for (let i = 0; i < text.length; i++) {
67
+ const code = (CODE128B[text[i]] !== undefined ? CODE128B[text[i]] : 0)
68
+ codes.push(code)
69
+ checksum += code * (i + 1)
70
+ }
71
+ codes.push(checksum % 103)
72
+ codes.push(106) // Stop
73
+ const binary = codes.map(c => PATTERNS[c]).join('')
74
+ const barWidth = props.width / binary.length
75
+ const barHeight = props.showText ? props.height - props.fontSize - 4 : props.height
76
+ ctx.setFillStyle(props.background)
77
+ ctx.fillRect(0, 0, props.width, props.height)
78
+ ctx.setFillStyle(props.foreground)
79
+ for (let i = 0; i < binary.length; i++) {
80
+ if (binary[i] === '1') {
81
+ ctx.fillRect(i * barWidth, 0, barWidth, barHeight)
82
+ }
83
+ }
84
+ if (props.showText) {
85
+ ctx.setFontSize(props.fontSize)
86
+ ctx.setFillStyle(props.foreground)
87
+ ctx.setTextAlign('center')
88
+ ctx.fillText(text, props.width / 2, props.height - 2)
89
+ }
90
+ ctx.draw()
91
+ }
92
+ onMounted(() => generate())
93
+ watch(() => props.value, () => generate())
94
+ </script>
95
+
96
+ <style lang="scss" scoped>
97
+ .mu-barcode {
98
+ &__canvas { display: block; }
99
+ }
100
+ </style>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <view class="mu-blank" :style="blankStyle">
3
+ <view class="mu-blank__content"><slot></slot></view>
4
+ </view>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { computed } from 'vue'
9
+ const props = defineProps({
10
+ width: { type: [Number, String], default: 32 }, // 两侧留白 rpx
11
+ background: { type: String, default: '' }
12
+ })
13
+ const formattedPadding = computed(() => {
14
+ if (typeof props.width === 'number') return `${props.width}rpx`
15
+ if (typeof props.width === 'string') {
16
+ if (props.width.endsWith('rpx') || props.width.endsWith('px')) return props.width
17
+ return `${props.width}rpx`
18
+ }
19
+ return '32rpx'
20
+ })
21
+ const blankStyle = computed(() => ({
22
+ paddingLeft: formattedPadding.value,
23
+ paddingRight: formattedPadding.value,
24
+ background: props.background || ''
25
+ }))
26
+ </script>
27
+
28
+ <style lang="scss" scoped>
29
+ .mu-blank { box-sizing: border-box; width: 100%; }
30
+ </style>