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,324 @@
1
+ <template>
2
+ <view class="mu-fab-container">
3
+ <!-- 暗化遮罩层 -->
4
+ <view
5
+ v-if="mask && isOpen"
6
+ class="mu-fab-mask"
7
+ :style="{ zIndex: zIndex - 1 }"
8
+ @tap="close"
9
+ ></view>
10
+
11
+ <!-- 悬浮主容器 -->
12
+ <view
13
+ class="mu-fab"
14
+ :class="[
15
+ `mu-fab--${placement}`
16
+ ]"
17
+ :style="fabContainerStyle"
18
+ >
19
+ <!-- 展开的菜单子项列表 -->
20
+ <view
21
+ v-if="isOpen && items.length"
22
+ class="mu-fab__actions"
23
+ :class="[`mu-fab__actions--${placement}`]"
24
+ >
25
+ <view
26
+ v-for="(item, index) in items"
27
+ :key="index"
28
+ class="mu-fab__action-item mu-touchable-flat"
29
+ @tap.stop="handleItemClick(index, item)"
30
+ >
31
+ <text v-if="item.text" class="mu-fab__action-text">{{ item.text }}</text>
32
+ <view
33
+ class="mu-fab__action-btn"
34
+ :style="{ background: item.bgColor || item.bg || '#FFFFFF' }"
35
+ >
36
+ <mu-icon
37
+ v-if="item.icon"
38
+ :name="item.icon"
39
+ :size="20"
40
+ :color="item.iconColor || item.color || '#171717'"
41
+ />
42
+ <text v-else class="mu-fab__action-label">{{ item.label || '' }}</text>
43
+ </view>
44
+ </view>
45
+ </view>
46
+
47
+ <!-- 悬浮主按钮 -->
48
+ <view
49
+ class="mu-fab__main-btn"
50
+ :class="[isOpen && 'mu-fab__main-btn--open']"
51
+ :style="mainBtnStyle"
52
+ @tap.stop="handleClick"
53
+ >
54
+ <slot>
55
+ <view
56
+ class="mu-fab__icon-box"
57
+ :class="[isOpen && 'mu-fab__icon-box--rotate']"
58
+ >
59
+ <mu-icon
60
+ :name="isOpen ? (activeIcon || 'plus') : icon"
61
+ :size="iconSize"
62
+ color="#FFFFFF"
63
+ />
64
+ </view>
65
+ </slot>
66
+ </view>
67
+ </view>
68
+ </view>
69
+ </template>
70
+
71
+ <script setup>
72
+ import { ref, computed } from 'vue'
73
+
74
+ const props = defineProps({
75
+ items: { type: Array, default: () => [] },
76
+ placement: { type: String, default: 'bottom-right' }, // bottom-right | bottom-left | top-right | top-left
77
+ offset: { type: Array, default: () => [60, 40] }, // [bottom/top, right/left] rpx
78
+ icon: { type: String, default: 'plus' }, // 主图标
79
+ activeIcon: { type: String, default: '' },
80
+ iconSize: { type: [Number, String], default: 24 },
81
+ bgColor: { type: String, default: '' },
82
+ size: { type: [Number, String], default: 104 }, // 主按钮尺寸 (rpx)
83
+ autoClose: { type: Boolean, default: true },
84
+ mask: { type: Boolean, default: true }, // 是否开启展开遮罩
85
+ zIndex: { type: Number, default: 999 }
86
+ })
87
+
88
+ const emit = defineEmits(['click', 'item-click', 'open', 'close', 'change'])
89
+
90
+ const isOpen = ref(false)
91
+
92
+ const fabContainerStyle = computed(() => {
93
+ const [offsetV, offsetH] = props.offset || [60, 40]
94
+ const style = {
95
+ position: 'fixed',
96
+ zIndex: props.zIndex
97
+ }
98
+
99
+ if (props.placement.includes('bottom')) {
100
+ style.bottom = `calc(${offsetV}rpx + env(safe-area-inset-bottom))`
101
+ } else {
102
+ style.top = `calc(${offsetV}rpx + env(safe-area-inset-top))`
103
+ }
104
+
105
+ if (props.placement.includes('right')) {
106
+ style.right = `${offsetH}rpx`
107
+ } else {
108
+ style.left = `${offsetH}rpx`
109
+ }
110
+
111
+ return style
112
+ })
113
+
114
+ const mainBtnStyle = computed(() => {
115
+ const sz = typeof props.size === 'number' ? `${props.size}rpx` : props.size
116
+ const style = {
117
+ width: sz,
118
+ height: sz
119
+ }
120
+ if (props.bgColor) {
121
+ style.background = props.bgColor
122
+ }
123
+ return style
124
+ })
125
+
126
+ function handleClick() {
127
+ if (props.items.length) {
128
+ if (isOpen.value) close()
129
+ else open()
130
+ } else {
131
+ emit('click')
132
+ }
133
+ }
134
+
135
+ function open() {
136
+ isOpen.value = true
137
+ emit('open')
138
+ emit('change', true)
139
+ }
140
+
141
+ function close() {
142
+ isOpen.value = false
143
+ emit('close')
144
+ emit('change', false)
145
+ }
146
+
147
+ function handleItemClick(index, item) {
148
+ emit('item-click', { index, item })
149
+ if (props.autoClose) {
150
+ close()
151
+ }
152
+ }
153
+
154
+ defineExpose({ open, close, isOpen })
155
+ </script>
156
+
157
+ <style lang="scss" scoped>
158
+ @import "@/uni.scss";
159
+
160
+ .mu-fab-mask {
161
+ position: fixed;
162
+ inset: 0;
163
+ background: rgba(15, 23, 42, 0.45);
164
+ backdrop-filter: blur(6px);
165
+ animation: mu-fab-fade 0.2s ease;
166
+ }
167
+
168
+ .mu-fab {
169
+ display: flex;
170
+ flex-direction: column;
171
+ align-items: center;
172
+ box-sizing: border-box;
173
+
174
+ &--bottom-right {
175
+ align-items: flex-end;
176
+ }
177
+
178
+ &--bottom-left {
179
+ align-items: flex-start;
180
+ }
181
+
182
+ &--top-right {
183
+ flex-direction: column-reverse;
184
+ align-items: flex-end;
185
+ }
186
+
187
+ &--top-left {
188
+ flex-direction: column-reverse;
189
+ align-items: flex-start;
190
+ }
191
+
192
+ &__main-btn {
193
+ border-radius: 50%;
194
+ background: #171717;
195
+ box-shadow: 0 12rpx 36rpx rgba(99, 102, 241, 0.4);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ cursor: pointer;
200
+ transition: transform 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
201
+ box-sizing: border-box;
202
+
203
+ &:active {
204
+ transform: scale(0.92);
205
+ }
206
+
207
+ &--open {
208
+ background: linear-gradient(135deg, #1E293B, #0F172A);
209
+ box-shadow: 0 12rpx 36rpx rgba(15, 23, 42, 0.5);
210
+ }
211
+ }
212
+
213
+ &__icon-box {
214
+ display: flex;
215
+ align-items: center;
216
+ justify-content: center;
217
+ transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
218
+
219
+ &--rotate {
220
+ transform: rotate(135deg);
221
+ }
222
+ }
223
+
224
+ &__actions {
225
+ display: flex;
226
+ flex-direction: column;
227
+ gap: 24rpx;
228
+ margin-bottom: 24rpx;
229
+ animation: mu-fab-slide 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
230
+
231
+ &--top-right, &--top-left {
232
+ margin-bottom: 0;
233
+ margin-top: 24rpx;
234
+ }
235
+ }
236
+
237
+ &__action-item {
238
+ display: flex;
239
+ align-items: center;
240
+ cursor: pointer;
241
+ }
242
+
243
+ &__action-text {
244
+ font-size: 24rpx;
245
+ font-weight: 700;
246
+ color: #0F172A;
247
+ margin-right: 16rpx;
248
+ background: rgba(255, 255, 255, 0.95);
249
+ backdrop-filter: blur(12px);
250
+ padding: 8rpx 20rpx;
251
+ border-radius: 999rpx;
252
+ box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.1);
253
+ border: 1rpx solid rgba(226, 232, 240, 0.9);
254
+ white-space: nowrap;
255
+ }
256
+
257
+ &__action-btn {
258
+ width: 84rpx;
259
+ height: 84rpx;
260
+ border-radius: 50%;
261
+ background: #ffffff;
262
+ display: flex;
263
+ align-items: center;
264
+ justify-content: center;
265
+ box-shadow: 0 6rpx 20rpx rgba(15, 23, 42, 0.12);
266
+ border: 1rpx solid rgba(226, 232, 240, 0.9);
267
+ box-sizing: border-box;
268
+ }
269
+
270
+ &__action-label {
271
+ font-size: 24rpx;
272
+ color: #0F172A;
273
+ font-weight: 800;
274
+ }
275
+ }
276
+
277
+ @keyframes mu-fab-fade {
278
+ from { opacity: 0; }
279
+ to { opacity: 1; }
280
+ }
281
+
282
+ @keyframes mu-fab-slide {
283
+ from {
284
+ opacity: 0;
285
+ transform: translateY(30rpx) scale(0.8);
286
+ }
287
+ to {
288
+ opacity: 1;
289
+ transform: translateY(0) scale(1);
290
+ }
291
+ }
292
+
293
+ .mu-dark, page.mu-dark {
294
+
295
+ .mu-fab {
296
+ &__action-text {
297
+ background: $mu-dark-bg-base;
298
+ color: $mu-dark-text-primary;
299
+ border-color: rgba(255, 255, 255, 0.1);
300
+ }
301
+ &__action-btn {
302
+ background: $mu-dark-bg-base;
303
+ border-color: rgba(255, 255, 255, 0.1);
304
+ }
305
+ &__action-label { color: $mu-dark-text-primary; }
306
+ }
307
+ }
308
+
309
+ @media (prefers-color-scheme: dark) {
310
+
311
+ .mu-fab {
312
+ &__action-text {
313
+ background: $mu-dark-bg-base;
314
+ color: $mu-dark-text-primary;
315
+ border-color: rgba(255, 255, 255, 0.1);
316
+ }
317
+ &__action-btn {
318
+ background: $mu-dark-bg-base;
319
+ border-color: rgba(255, 255, 255, 0.1);
320
+ }
321
+ &__action-label { color: $mu-dark-text-primary; }
322
+ }
323
+ }
324
+ </style>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <view class="mu-footer" :style="{ padding }">
3
+ <slot>
4
+ <text class="mu-footer__text">{{ text }}</text>
5
+ </slot>
6
+ </view>
7
+ </template>
8
+
9
+ <script setup>
10
+ const props = defineProps({
11
+ text: { type: String, default: '' },
12
+ padding: { type: String, default: '40rpx 0' }
13
+ })
14
+ </script>
15
+
16
+ <style lang="scss" scoped>
17
+ .mu-footer {
18
+ display: flex; align-items: center; justify-content: center; flex-direction: column;
19
+ &__text { font-size: $mu-font-xs; color: $mu-text-tertiary; text-align: center; line-height: $mu-line-height; }
20
+ }
21
+ </style>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <view class="mu-form" :class="[`mu-form--label-${labelPosition}`, inline && 'mu-form--inline']">
3
+ <slot></slot>
4
+ </view>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { provide, computed, reactive, watch } from 'vue'
9
+
10
+ const props = defineProps({
11
+ model: { type: Object, default: () => ({}) },
12
+ rules: { type: Object, default: () => ({}) },
13
+ labelWidth: { type: String, default: '160rpx' },
14
+ labelPosition: { type: String, default: 'left' }, // left | top
15
+ inline: { type: Boolean, default: false },
16
+ disabled: { type: Boolean, default: false },
17
+ showMessage: { type: Boolean, default: true }
18
+ })
19
+
20
+ const emit = defineEmits(['validate'])
21
+
22
+ // 响应式向下注入表单配置
23
+ const formState = reactive({
24
+ model: computed(() => props.model),
25
+ rules: computed(() => props.rules),
26
+ labelWidth: computed(() => props.labelWidth),
27
+ labelPosition: computed(() => props.labelPosition),
28
+ disabled: computed(() => props.disabled)
29
+ })
30
+
31
+ provide('muForm', formState)
32
+ provide('mu-form', formState) // 兼容性 key
33
+
34
+ /**
35
+ * 内置常用校验器
36
+ */
37
+ const BUILTIN_PATTERNS = {
38
+ phone: /^1[3-9]\d{9}$/,
39
+ email: /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
40
+ idcard: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/,
41
+ url: /^https?:\/\/.+/
42
+ }
43
+
44
+ /**
45
+ * 校验单个字段
46
+ */
47
+ function validateField(field) {
48
+ const value = props.model[field]
49
+ const ruleArr = props.rules[field]
50
+ if (!ruleArr) return { isValid: true, error: null }
51
+
52
+ const rules = Array.isArray(ruleArr) ? ruleArr : [ruleArr]
53
+
54
+ for (const rule of rules) {
55
+ // 1. required 必填校验
56
+ if (rule.required && (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0))) {
57
+ return { isValid: false, error: { field, message: rule.message || `${field}不能为空` } }
58
+ }
59
+
60
+ // 如果未填且非必填,跳过后续格式校验
61
+ if (!rule.required && (value === undefined || value === null || value === '')) {
62
+ continue
63
+ }
64
+
65
+ // 2. type 内置类型正则 (phone | email | idcard | url)
66
+ if (rule.type && BUILTIN_PATTERNS[rule.type]) {
67
+ if (!BUILTIN_PATTERNS[rule.type].test(String(value))) {
68
+ return { isValid: false, error: { field, message: rule.message || `${field}格式不正确` } }
69
+ }
70
+ }
71
+
72
+ // 3. pattern 自定义正则表达式
73
+ if (rule.pattern && rule.pattern instanceof RegExp) {
74
+ if (!rule.pattern.test(String(value))) {
75
+ return { isValid: false, error: { field, message: rule.message || `${field}格式不符合要求` } }
76
+ }
77
+ }
78
+
79
+ // 4. min / max 长度或数值校验
80
+ if (rule.min !== undefined && String(value).length < rule.min) {
81
+ return { isValid: false, error: { field, message: rule.message || `${field}长度不能少于${rule.min}位` } }
82
+ }
83
+ if (rule.max !== undefined && String(value).length > rule.max) {
84
+ return { isValid: false, error: { field, message: rule.message || `${field}长度不能超过${rule.max}位` } }
85
+ }
86
+
87
+ // 5. validator 自定义函数校验
88
+ if (rule.validator && typeof rule.validator === 'function') {
89
+ const result = rule.validator(value, props.model)
90
+ if (result !== true && typeof result === 'string') {
91
+ return { isValid: false, error: { field, message: result } }
92
+ } else if (result === false) {
93
+ return { isValid: false, error: { field, message: rule.message || `${field}校验不通过` } }
94
+ }
95
+ }
96
+ }
97
+
98
+ return { isValid: true, error: null }
99
+ }
100
+
101
+ /**
102
+ * 校验全量表单 (支持 Promise / Callback 两种风格)
103
+ */
104
+ function validate(callback) {
105
+ return new Promise((resolve) => {
106
+ const errors = []
107
+
108
+ for (const field of Object.keys(props.rules)) {
109
+ const res = validateField(field)
110
+ if (!res.isValid && res.error) {
111
+ errors.push(res.error)
112
+ }
113
+ }
114
+
115
+ const isValid = errors.length === 0
116
+
117
+ // 弹出错误轻提示 Toast
118
+ if (props.showMessage && errors.length > 0) {
119
+ uni.showToast({ title: errors[0].message, icon: 'none' })
120
+ }
121
+
122
+ emit('validate', isValid, errors)
123
+
124
+ if (typeof callback === 'function') {
125
+ callback(isValid, errors)
126
+ }
127
+
128
+ resolve(isValid)
129
+ })
130
+ }
131
+
132
+ /**
133
+ * 重置表单模型
134
+ */
135
+ function resetFields() {
136
+ for (const key of Object.keys(props.model)) {
137
+ const original = props.model[key]
138
+ if (Array.isArray(original)) {
139
+ props.model[key] = []
140
+ } else if (typeof original === 'number') {
141
+ props.model[key] = 0
142
+ } else if (typeof original === 'boolean') {
143
+ props.model[key] = false
144
+ } else {
145
+ props.model[key] = ''
146
+ }
147
+ }
148
+ }
149
+
150
+ defineExpose({
151
+ validate,
152
+ validateField,
153
+ resetFields
154
+ })
155
+ </script>
156
+
157
+ <style lang="scss" scoped>
158
+ @import "@/uni.scss";
159
+
160
+ .mu-form {
161
+ width: 100%;
162
+
163
+ &--inline {
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ gap: 20rpx;
167
+ align-items: center;
168
+ }
169
+ }
170
+ </style>
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <view
3
+ class="mu-grid"
4
+ :class="[
5
+ border && mode !== 'card' && 'mu-grid--border'
6
+ ]"
7
+ :style="gridStyle"
8
+ >
9
+ <slot></slot>
10
+ </view>
11
+ </template>
12
+
13
+ <script setup>
14
+ import { computed, provide } from 'vue'
15
+
16
+ const props = defineProps({
17
+ columns: { type: [Number, String], default: 4 }, // 列数 (默认 4)
18
+ border: { type: Boolean, default: true }, // 是否显示分割线 (默认开启)
19
+ square: { type: Boolean, default: false }, // 是否正方形单格
20
+ gutter: { type: [Number, String], default: 0 }, // 格子间距 (rpx)
21
+ clickable: { type: Boolean, default: true }, // 是否开启点击反馈
22
+ background: { type: String, default: '' },
23
+ mode: { type: String, default: 'normal' } // normal (普通) | card (Bento 悬浮卡片)
24
+ })
25
+
26
+ provide('muGrid', {
27
+ columns: computed(() => Number(props.columns) || 4),
28
+ border: computed(() => props.border),
29
+ square: computed(() => props.square),
30
+ gutter: computed(() => Number(props.gutter) || 0),
31
+ clickable: computed(() => props.clickable),
32
+ mode: computed(() => props.mode)
33
+ })
34
+
35
+ const gridStyle = computed(() => {
36
+ const style = {
37
+ display: 'flex',
38
+ flexWrap: 'wrap',
39
+ boxSizing: 'border-box'
40
+ }
41
+ const g = Number(props.gutter) || 0
42
+ if (g > 0) {
43
+ const halfG = g / 2
44
+ style.marginLeft = `-${halfG}rpx`
45
+ style.marginRight = `-${halfG}rpx`
46
+ style.width = 'auto'
47
+ } else {
48
+ style.width = '100%'
49
+ if (props.border && props.mode !== 'card') {
50
+ style.background = props.background || '#ffffff'
51
+ }
52
+ }
53
+
54
+ if (props.background && !style.background) {
55
+ style.background = props.background
56
+ }
57
+ return style
58
+ })
59
+ </script>
60
+
61
+ <style lang="scss" scoped>
62
+ @import "@/uni.scss";
63
+
64
+ .mu-grid {
65
+ position: relative;
66
+ box-sizing: border-box;
67
+
68
+ &--border {
69
+ border-top: 1rpx solid rgba(226, 232, 240, 0.8);
70
+ border-left: 1rpx solid rgba(226, 232, 240, 0.8);
71
+ }
72
+ }
73
+
74
+ .mu-dark, page.mu-dark {
75
+
76
+ .mu-grid {
77
+ &--border {
78
+ border-top-color: rgba(255, 255, 255, 0.12);
79
+ border-left-color: rgba(255, 255, 255, 0.12);
80
+ background: $mu-dark-bg-base !important;
81
+ }
82
+ }
83
+ }
84
+
85
+ @media (prefers-color-scheme: dark) {
86
+
87
+ .mu-grid {
88
+ &--border {
89
+ border-top-color: rgba(255, 255, 255, 0.12);
90
+ border-left-color: rgba(255, 255, 255, 0.12);
91
+ background: $mu-dark-bg-base !important;
92
+ }
93
+ }
94
+ }
95
+ </style>