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,151 @@
1
+ <template>
2
+ <view v-if="visible" class="mu-toast" :class="[`mu-toast--${currentPosition}`]">
3
+ <view class="mu-toast__inner mu-touchable-flat">
4
+ <view class="mu-toast__icon" v-if="currentType !== 'default'">
5
+ <mu-loading v-if="currentType === 'loading'" :size="18" color="#ffffff" />
6
+ <mu-icon v-else-if="currentType === 'success'" name="check" :size="18" color="#10B981" />
7
+ <mu-icon v-else-if="currentType === 'error'" name="close" :size="18" color="#F43F5E" />
8
+ <mu-icon v-else-if="currentType === 'warning'" name="warning" :size="18" color="#F59E0B" />
9
+ <mu-icon v-else :name="currentType" :size="18" color="#ffffff" />
10
+ </view>
11
+ <text class="mu-toast__text">{{ currentTitle }}</text>
12
+ </view>
13
+ </view>
14
+ </template>
15
+
16
+ <script setup>
17
+ import { ref, onMounted, onUnmounted } from 'vue'
18
+
19
+ const props = defineProps({
20
+ title: { type: String, default: '' },
21
+ type: { type: String, default: 'default' }, // default | success | error | warning | loading
22
+ duration: { type: Number, default: 2000 },
23
+ position: { type: String, default: 'center' } // top | center | bottom
24
+ })
25
+
26
+ const emit = defineEmits(['close'])
27
+
28
+ const visible = ref(false)
29
+ const currentTitle = ref('')
30
+ const currentType = ref('default')
31
+ const currentPosition = ref('center')
32
+ const currentDuration = ref(2000)
33
+ let timer = null
34
+
35
+ function show(arg1, arg2) {
36
+ if (typeof arg1 === 'string') {
37
+ currentTitle.value = arg1
38
+ currentType.value = arg2 || props.type || 'default'
39
+ currentPosition.value = props.position || 'center'
40
+ currentDuration.value = props.duration || 2000
41
+ } else if (arg1 && typeof arg1 === 'object') {
42
+ currentTitle.value = arg1.title || props.title || ''
43
+ currentType.value = arg1.type || props.type || 'default'
44
+ currentPosition.value = arg1.position || props.position || 'center'
45
+ currentDuration.value = (arg1.duration !== undefined ? arg1.duration : (props.duration !== undefined ? props.duration : 2000))
46
+ } else {
47
+ currentTitle.value = props.title || ''
48
+ currentType.value = props.type || 'default'
49
+ currentPosition.value = props.position || 'center'
50
+ currentDuration.value = (props.duration !== undefined ? props.duration : 2000)
51
+ }
52
+
53
+ visible.value = true
54
+ if (timer) clearTimeout(timer)
55
+ if (currentType.value !== 'loading' && currentDuration.value > 0) {
56
+ timer = setTimeout(() => { close() }, currentDuration.value)
57
+ }
58
+ }
59
+
60
+ function close() {
61
+ visible.value = false
62
+ emit('close')
63
+ }
64
+
65
+ // 注册全局 JS 事件总线监听器
66
+ onMounted(() => {
67
+ uni.$on('mu-toast-show', handleGlobalToastShow)
68
+ uni.$on('mu-toast-close', close)
69
+ uni.$on('mu-toast-hide', close)
70
+ })
71
+
72
+ onUnmounted(() => {
73
+ uni.$off('mu-toast-show', handleGlobalToastShow)
74
+ uni.$off('mu-toast-close', close)
75
+ uni.$off('mu-toast-hide', close)
76
+ })
77
+
78
+ function handleGlobalToastShow(options) {
79
+ show(options)
80
+ }
81
+
82
+ defineExpose({ show, open: show, close })
83
+ </script>
84
+
85
+ <style lang="scss" scoped>
86
+ @import "@/uni.scss";
87
+
88
+ .mu-toast {
89
+ position: fixed;
90
+ top: 0;
91
+ left: 0;
92
+ right: 0;
93
+ bottom: 0;
94
+ width: 100vw;
95
+ height: 100vh;
96
+ z-index: $mu-z-toast;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ pointer-events: none;
101
+ box-sizing: border-box;
102
+
103
+ &--top {
104
+ align-items: flex-start;
105
+ padding-top: calc(140rpx + env(safe-area-inset-top, 0px));
106
+ }
107
+ &--center {
108
+ align-items: center;
109
+ }
110
+ &--bottom {
111
+ align-items: flex-end;
112
+ padding-bottom: calc(160rpx + env(safe-area-inset-bottom, 0px));
113
+ }
114
+
115
+ &__inner {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ padding: 24rpx 40rpx;
119
+ background: rgba(15, 23, 42, 0.92);
120
+ border-radius: $mu-radius-full;
121
+ max-width: 540rpx;
122
+ pointer-events: auto;
123
+ border: none !important;
124
+ box-shadow: none !important;
125
+ animation: mu-toast-in 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
126
+ }
127
+
128
+ &__icon {
129
+ margin-right: 16rpx;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ line-height: 1;
134
+ flex-shrink: 0;
135
+ }
136
+
137
+ &__text {
138
+ color: #ffffff;
139
+ font-size: 28rpx;
140
+ font-weight: 700;
141
+ line-height: 1.4;
142
+ letter-spacing: -0.3rpx;
143
+ word-break: break-all;
144
+ }
145
+ }
146
+
147
+ @keyframes mu-toast-in {
148
+ 0% { opacity: 0; transform: scale(0.85) translateY(12rpx); }
149
+ 100% { opacity: 1; transform: scale(1) translateY(0); }
150
+ }
151
+ </style>
@@ -0,0 +1,114 @@
1
+ <template>
2
+ <view class="mu-transfer">
3
+ <view class="mu-transfer__panel">
4
+ <view class="mu-transfer__header">
5
+ <view v-if="checkable" class="mu-transfer__check" @tap="toggleAllLeft">
6
+ <view class="mu-transfer__checkbox" :class="{ 'mu-transfer__checkbox--checked': allLeftChecked }"></view>
7
+ </view>
8
+ <text class="mu-transfer__title">{{ titles[0] }}</text>
9
+ </view>
10
+ <scroll-view scroll-y class="mu-transfer__body">
11
+ <view v-for="item in leftList" :key="item[itemKey]" class="mu-transfer__item" @tap="toggleLeft(item)">
12
+ <view v-if="checkable" class="mu-transfer__checkbox" :class="{ 'mu-transfer__checkbox--checked': leftChecked.has(item[itemKey]) }"></view>
13
+ <text class="mu-transfer__item-text">{{ item[labelKey] }}</text>
14
+ </view>
15
+ </scroll-view>
16
+ </view>
17
+ <view class="mu-transfer__actions">
18
+ <view class="mu-transfer__action" :class="{ 'mu-transfer__action--disabled': leftChecked.size === 0 }" @tap="moveToRight">
19
+ <text class="mu-transfer__action-icon">›</text>
20
+ </view>
21
+ <view class="mu-transfer__action" :class="{ 'mu-transfer__action--disabled': rightChecked.size === 0 }" @tap="moveToLeft">
22
+ <text class="mu-transfer__action-icon">‹</text>
23
+ </view>
24
+ </view>
25
+ <view class="mu-transfer__panel">
26
+ <view class="mu-transfer__header">
27
+ <view v-if="checkable" class="mu-transfer__check" @tap="toggleAllRight">
28
+ <view class="mu-transfer__checkbox" :class="{ 'mu-transfer__checkbox--checked': allRightChecked }"></view>
29
+ </view>
30
+ <text class="mu-transfer__title">{{ titles[1] }}</text>
31
+ </view>
32
+ <scroll-view scroll-y class="mu-transfer__body">
33
+ <view v-for="item in rightList" :key="item[itemKey]" class="mu-transfer__item" @tap="toggleRight(item)">
34
+ <view v-if="checkable" class="mu-transfer__checkbox" :class="{ 'mu-transfer__checkbox--checked': rightChecked.has(item[itemKey]) }"></view>
35
+ <text class="mu-transfer__item-text">{{ item[labelKey] }}</text>
36
+ </view>
37
+ </scroll-view>
38
+ </view>
39
+ </view>
40
+ </template>
41
+
42
+ <script setup>
43
+ import { ref, computed, watch } from 'vue'
44
+ const props = defineProps({
45
+ data: { type: Array, default: () => [] },
46
+ dataSource: { type: Array, default: () => [] },
47
+ value: { type: Array, default: () => [] },
48
+ labelKey: { type: String, default: 'label' },
49
+ valueKey: { type: String, default: 'value' },
50
+ titles: { type: Array, default: () => ['源列表', '目标列表'] },
51
+ checkable: { type: Boolean, default: true }
52
+ })
53
+ const emit = defineEmits(['change', 'update:value'])
54
+ const rightValues = ref(new Set(props.value))
55
+ const leftChecked = ref(new Set())
56
+ const rightChecked = ref(new Set())
57
+ // 兼容 data / data-source 两种传法,并自动识别 key 字段
58
+ const sourceList = computed(() => props.data.length ? props.data : props.dataSource)
59
+ const itemKey = computed(() => {
60
+ const first = sourceList.value[0]
61
+ if (first && !(props.valueKey in first) && 'key' in first) return 'key'
62
+ return props.valueKey
63
+ })
64
+ const leftList = computed(() => sourceList.value.filter(d => !rightValues.value.has(d[itemKey.value])))
65
+ const rightList = computed(() => sourceList.value.filter(d => rightValues.value.has(d[itemKey.value])))
66
+ const allLeftChecked = computed(() => leftList.value.length > 0 && leftList.value.every(d => leftChecked.value.has(d[itemKey.value])))
67
+ const allRightChecked = computed(() => rightList.value.length > 0 && rightList.value.every(d => rightChecked.value.has(d[itemKey.value])))
68
+ function toggleLeft(item) { const k = item[itemKey.value]; leftChecked.value.has(k) ? leftChecked.value.delete(k) : leftChecked.value.add(k) }
69
+ function toggleRight(item) { const k = item[itemKey.value]; rightChecked.value.has(k) ? rightChecked.value.delete(k) : rightChecked.value.add(k) }
70
+ function toggleAllLeft() { if (allLeftChecked.value) leftChecked.value.clear(); else leftList.value.forEach(d => leftChecked.value.add(d[itemKey.value])) }
71
+ function toggleAllRight() { if (allRightChecked.value) rightChecked.value.clear(); else rightList.value.forEach(d => rightChecked.value.add(d[itemKey.value])) }
72
+ function moveToRight() { leftChecked.value.forEach(k => rightValues.value.add(k)); leftChecked.value.clear(); emitChange() }
73
+ function moveToLeft() { rightChecked.value.forEach(k => rightValues.value.delete(k)); rightChecked.value.clear(); emitChange() }
74
+ function emitChange() { emit('change', [...rightValues.value]); emit('update:value', [...rightValues.value]) }
75
+ watch(() => props.value, (v) => { rightValues.value = new Set(v) })
76
+ </script>
77
+
78
+ <style lang="scss" scoped>
79
+ .mu-transfer {
80
+ display: flex; align-items: stretch; gap: $mu-space-3;
81
+ &__panel {
82
+ flex: 1; border: 1rpx solid $mu-border-color; border-radius: $mu-radius-md;
83
+ overflow: hidden; display: flex; flex-direction: column;
84
+ }
85
+ &__header {
86
+ display: flex; align-items: center; padding: $mu-space-3;
87
+ border-bottom: 1rpx solid $mu-border-light; background: #fafafa;
88
+ }
89
+ &__check { margin-right: $mu-space-2; }
90
+ &__checkbox {
91
+ width: 32rpx; height: 32rpx; border: 2rpx solid $mu-border-color; border-radius: $mu-radius-sm;
92
+ display: flex; align-items: center; justify-content: center;
93
+ &--checked { background: $mu-primary; border-color: $mu-primary; &::after { content: '✓'; color: #fff; font-size: 20rpx; } }
94
+ }
95
+ &__title { font-size: $mu-font-sm; color: $mu-text-primary; font-weight: 500; }
96
+ &__body { flex: 1; max-height: 400rpx; }
97
+ &__item {
98
+ display: flex; align-items: center; padding: $mu-space-3; cursor: pointer;
99
+ border-bottom: 1rpx solid $mu-border-light;
100
+ &:active { background: $mu-bg-hover; }
101
+ &-text { font-size: $mu-font-sm; color: $mu-text-primary; margin-left: $mu-space-2; }
102
+ }
103
+ &__actions {
104
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: $mu-space-3;
105
+ }
106
+ &__action {
107
+ width: 64rpx; height: 64rpx; border-radius: $mu-radius-sm; background: $mu-primary;
108
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
109
+ &:active { opacity: 0.85; }
110
+ &--disabled { opacity: 0.4; pointer-events: none; }
111
+ &-icon { color: #fff; font-size: $mu-font-lg; font-weight: 600; }
112
+ }
113
+ }
114
+ </style>
@@ -0,0 +1,263 @@
1
+ <template>
2
+ <view class="mu-tree">
3
+ <view
4
+ v-for="(node, ni) in flatNodes"
5
+ :key="node.id"
6
+ class="mu-tree__node"
7
+ :style="{ paddingLeft: (node.level * indent) + 'rpx' }"
8
+ >
9
+ <view class="mu-tree__row" :class="{ 'mu-tree__row--selected': selectedId === node.id }" @tap="handleToggle(node)">
10
+ <!-- 展开/收起箭头 -->
11
+ <view class="mu-tree__arrow" :class="[node.expanded && 'mu-tree__arrow--expand', node.isLeaf && 'mu-tree__arrow--leaf']">
12
+ <mu-icon v-if="!node.isLeaf" name="chevron_right" :size="14" :color="node.expanded ? '#171717' : '#94A3B8'" />
13
+ </view>
14
+
15
+ <!-- 复选框 -->
16
+ <view v-if="checkable" class="mu-tree__check" @tap.stop="handleCheck(node)">
17
+ <view class="mu-tree__checkbox" :class="[node.checked && 'mu-tree__checkbox--checked', node.halfChecked && 'mu-tree__checkbox--half']">
18
+ <mu-icon v-if="node.checked" name="check" :size="12" color="#ffffff" />
19
+ <view v-else-if="node.halfChecked" class="mu-tree__checkbox-half-bar"></view>
20
+ </view>
21
+ </view>
22
+
23
+ <!-- 节点图标 -->
24
+ <view v-if="node.data.icon" class="mu-tree__icon">
25
+ <mu-icon :name="node.data.icon" :size="16" :color="selectedId === node.id ? '#171717' : (node.isLeaf ? '#94A3B8' : '#64748B')" />
26
+ </view>
27
+
28
+ <!-- 标签文字 -->
29
+ <view class="mu-tree__label">
30
+ <slot name="node" :node="node.data" :is-leaf="node.isLeaf" :expanded="node.expanded">
31
+ <text class="mu-tree__label-text" :class="{ 'mu-tree__label-text--selected': selectedId === node.id }">{{ node.label }}</text>
32
+ </slot>
33
+ </view>
34
+ </view>
35
+ </view>
36
+ </view>
37
+ </template>
38
+
39
+ <script setup>
40
+ import { ref, computed, watch } from 'vue'
41
+
42
+ const props = defineProps({
43
+ data: { type: Array, default: () => [] },
44
+ defaultExpandAll: { type: Boolean, default: false },
45
+ expandedKeys: { type: Array, default: () => [] },
46
+ checkedKeys: { type: Array, default: () => [] },
47
+ selectedKey: { type: [String, Number], default: '' },
48
+ checkable: { type: Boolean, default: false },
49
+ indent: { type: Number, default: 40 },
50
+ fieldNames: { type: Object, default: () => ({ label: 'label', children: 'children', key: 'id' }) }
51
+ })
52
+
53
+ const emit = defineEmits(['expand', 'check', 'select', 'update:checkedKeys', 'update:expandedKeys', 'update:selectedKey'])
54
+
55
+ const expandedSet = ref(new Set(props.expandedKeys))
56
+ const collapsedSet = ref(new Set())
57
+ const checkedSet = ref(new Set(props.checkedKeys))
58
+ const selectedId = ref(props.selectedKey)
59
+
60
+ function getKey(node) { return node[props.fieldNames.key] || node[props.fieldNames.label] }
61
+ function getLabel(node) { return node[props.fieldNames.label] || '' }
62
+ function getChildren(node) { return node[props.fieldNames.children] || [] }
63
+
64
+ // 将树形数据扁平化为可渲染数组
65
+ function buildFlat(data, level = 0, parent = null) {
66
+ if (!data || !Array.isArray(data)) return []
67
+ const result = []
68
+ for (const item of data) {
69
+ if (!item) continue
70
+ const id = getKey(item)
71
+ const children = getChildren(item)
72
+ const isLeaf = !children || children.length === 0
73
+ const expanded = collapsedSet.value.has(id) ? false : (props.defaultExpandAll || expandedSet.value.has(id))
74
+ const node = {
75
+ id, label: getLabel(item), level, isLeaf, expanded,
76
+ checked: checkedSet.value.has(id), halfChecked: false,
77
+ data: item, parent, _children: children
78
+ }
79
+ result.push(node)
80
+ if (expanded && !isLeaf) {
81
+ result.push(...buildFlat(children, level + 1, node))
82
+ }
83
+ }
84
+ return result
85
+ }
86
+
87
+ const flatNodes = computed(() => buildFlat(props.data))
88
+
89
+ function handleToggle(node) {
90
+ if (node.isLeaf) {
91
+ selectedId.value = node.id
92
+ emit('select', { key: node.id, data: node.data })
93
+ emit('update:selectedKey', node.id)
94
+ return
95
+ }
96
+ if (node.expanded) {
97
+ expandedSet.value.delete(node.id)
98
+ collapsedSet.value.add(node.id)
99
+ } else {
100
+ expandedSet.value.add(node.id)
101
+ collapsedSet.value.delete(node.id)
102
+ }
103
+ emit('expand', { key: node.id, expanded: !node.expanded })
104
+ emit('update:expandedKeys', [...expandedSet.value])
105
+ }
106
+
107
+ function handleCheck(node) {
108
+ const checked = !node.checked
109
+ toggleCheck(node, checked)
110
+ emit('check', { key: node.id, checked, checkedKeys: [...checkedSet.value] })
111
+ emit('update:checkedKeys', [...checkedSet.value])
112
+ }
113
+
114
+ function toggleCheck(node, checked) {
115
+ if (checked) checkedSet.value.add(node.id)
116
+ else checkedSet.value.delete(node.id)
117
+ // 向下级子节点传播勾选状态
118
+ const children = getChildren(node.data)
119
+ for (const child of children) {
120
+ toggleCheck({ id: getKey(child), data: child, isLeaf: getChildren(child).length === 0 }, checked)
121
+ }
122
+ }
123
+
124
+ watch(() => props.expandedKeys, (v) => { expandedSet.value = new Set(v) })
125
+ watch(() => props.checkedKeys, (v) => { checkedSet.value = new Set(v) })
126
+ watch(() => props.selectedKey, (v) => { selectedId.value = v })
127
+ </script>
128
+
129
+ <style lang="scss" scoped>
130
+ @import "@/uni.scss";
131
+
132
+ .mu-tree {
133
+ width: 100%;
134
+
135
+ &__node {
136
+ box-sizing: border-box;
137
+ }
138
+
139
+ &__row {
140
+ display: flex;
141
+ align-items: center;
142
+ padding: 14rpx 16rpx;
143
+ border-radius: 12rpx;
144
+ cursor: pointer;
145
+ transition: background 0.15s ease;
146
+
147
+ &:active {
148
+ background: rgba(99, 102, 241, 0.06);
149
+ }
150
+
151
+ &--selected {
152
+ background: rgba(99, 102, 241, 0.08);
153
+ }
154
+ }
155
+
156
+ &__arrow {
157
+ width: 36rpx;
158
+ height: 36rpx;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ flex-shrink: 0;
163
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
164
+ margin-right: 4rpx;
165
+
166
+ &--expand {
167
+ transform: rotate(90deg);
168
+ }
169
+
170
+ &--leaf {
171
+ visibility: hidden;
172
+ }
173
+ }
174
+
175
+ &__check {
176
+ margin-right: 12rpx;
177
+ flex-shrink: 0;
178
+ }
179
+
180
+ &__checkbox {
181
+ width: 32rpx;
182
+ height: 32rpx;
183
+ border: 2rpx solid #CBD5E1;
184
+ border-radius: 8rpx;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ transition: all 0.2s ease;
189
+
190
+ &--checked {
191
+ background: #171717;
192
+ border-color: #171717;
193
+ }
194
+
195
+ &--half {
196
+ background: #171717;
197
+ border-color: #171717;
198
+ }
199
+
200
+ &-half-bar {
201
+ width: 16rpx;
202
+ height: 4rpx;
203
+ background: #ffffff;
204
+ border-radius: 2rpx;
205
+ }
206
+ }
207
+
208
+ &__icon {
209
+ margin-right: 12rpx;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ flex-shrink: 0;
214
+ }
215
+
216
+ &__label {
217
+ flex: 1;
218
+ min-width: 0;
219
+
220
+ &-text {
221
+ font-size: 26rpx;
222
+ color: $mu-text-primary;
223
+ font-weight: 500;
224
+ line-height: 1.4;
225
+
226
+ &--selected {
227
+ color: #171717;
228
+ font-weight: 700;
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ .mu-dark, page.mu-dark {
235
+
236
+ .mu-tree {
237
+ &__row {
238
+ &:active { background: rgba(255, 255, 255, 0.05); }
239
+ &--selected { background: rgba(99, 102, 241, 0.15); }
240
+ }
241
+ &__checkbox { border-color: rgba(255, 255, 255, 0.2); }
242
+ &__label-text {
243
+ color: $mu-dark-text-primary;
244
+ &--selected { color: #FFFFFF; }
245
+ }
246
+ }
247
+ }
248
+
249
+ @media (prefers-color-scheme: dark) {
250
+
251
+ .mu-tree {
252
+ &__row {
253
+ &:active { background: rgba(255, 255, 255, 0.05); }
254
+ &--selected { background: rgba(99, 102, 241, 0.15); }
255
+ }
256
+ &__checkbox { border-color: rgba(255, 255, 255, 0.2); }
257
+ &__label-text {
258
+ color: $mu-dark-text-primary;
259
+ &--selected { color: #FFFFFF; }
260
+ }
261
+ }
262
+ }
263
+ </style>