oxy-uni-ui 1.0.1 → 1.2.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.
- package/LICENSE +1 -1
- package/attributes.json +1 -1
- package/components/common/abstracts/variable.scss +40 -4
- package/components/common/util.ts +44 -0
- package/components/composables/index.ts +1 -0
- package/components/composables/useVirtualScroll.ts +172 -0
- package/components/oxy-checkbox/index.scss +36 -6
- package/components/oxy-checkbox/oxy-checkbox.vue +5 -4
- package/components/oxy-checkbox/types.ts +2 -1
- package/components/oxy-col-picker/index.scss +18 -15
- package/components/oxy-col-picker/oxy-col-picker.vue +28 -3
- package/components/oxy-col-picker/types.ts +12 -0
- package/components/oxy-corner/index.scss +258 -0
- package/components/oxy-corner/oxy-corner.vue +67 -0
- package/components/oxy-corner/types.ts +50 -0
- package/components/oxy-drop-menu/index.scss +4 -0
- package/components/oxy-drop-menu/oxy-drop-menu.vue +5 -3
- package/components/oxy-drop-menu/types.ts +1 -1
- package/components/oxy-drop-menu-item/index.scss +4 -4
- package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +2 -0
- package/components/oxy-file-list/index.scss +83 -0
- package/components/oxy-file-list/oxy-file-list.vue +213 -0
- package/components/oxy-file-list/types.ts +54 -0
- package/components/oxy-list/index.scss +5 -0
- package/components/oxy-list/oxy-list.vue +206 -0
- package/components/oxy-list/types.ts +38 -0
- package/components/oxy-slider/index.scss +2 -2
- package/components/oxy-swiper/index.scss +1 -2
- package/components/oxy-textarea/oxy-textarea.vue +0 -4
- package/components/oxy-tree/components/tree-node-content.vue +72 -0
- package/components/oxy-tree/index.scss +83 -0
- package/components/oxy-tree/index.ts +51 -0
- package/components/oxy-tree/oxy-tree.vue +406 -0
- package/components/oxy-tree/types.ts +85 -0
- package/components/oxy-tree/utils.ts +51 -0
- package/components/oxy-upload/images/audio.png +0 -0
- package/components/oxy-upload/images/excle.png +0 -0
- package/components/oxy-upload/images/other.png +0 -0
- package/components/oxy-upload/images/pdf.png +0 -0
- package/components/oxy-upload/images/pic.png +0 -0
- package/components/oxy-upload/images/txt.png +0 -0
- package/components/oxy-upload/images/video.png +0 -0
- package/components/oxy-upload/images/word.png +0 -0
- package/components/oxy-upload/index.scss +50 -0
- package/components/oxy-upload/oxy-upload.vue +93 -7
- package/components/oxy-upload/types.ts +22 -1
- package/components/oxy-virtual-scroll/index.scss +35 -0
- package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +143 -0
- package/components/oxy-virtual-scroll/types.ts +155 -0
- package/components/oxy-virtual-scroll/virtual-scroll.ts +81 -0
- package/global.d.ts +3 -0
- package/locale/lang/ar-SA.ts +2 -1
- package/locale/lang/en-US.ts +10 -9
- package/locale/lang/zh-CN.ts +7 -6
- package/package.json +1 -1
- package/tags.json +1 -1
- package/web-types.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
@import '../common/abstracts/variable';
|
|
2
|
+
@import '../common/abstracts/mixin';
|
|
3
|
+
@import '../oxy-virtual-scroll/index.scss';
|
|
4
|
+
|
|
5
|
+
.oxy-tree {
|
|
6
|
+
position: relative;
|
|
7
|
+
&__virtual-scroll {
|
|
8
|
+
position: relative;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&__view {
|
|
13
|
+
height: 100%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&__container {
|
|
17
|
+
position: relative;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&__items {
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: 0;
|
|
23
|
+
left: 0;
|
|
24
|
+
right: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.oxy-tree-node-content {
|
|
29
|
+
display: flex;
|
|
30
|
+
width: fit-content;
|
|
31
|
+
width: 100%;
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
align-items: center;
|
|
34
|
+
|
|
35
|
+
&.expanded {
|
|
36
|
+
:deep(.oxy-tree-node-icon) {
|
|
37
|
+
transform: rotate(0deg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&.is-leaf {
|
|
42
|
+
:deep(.oxy-tree-node-icon) {
|
|
43
|
+
opacity: 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&.is-current {
|
|
48
|
+
background-color: $-tree-node-current-bg;
|
|
49
|
+
color: $-tree-node-current-color;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&.is-disabled {
|
|
53
|
+
color: $-tree-node-disabled-color;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:deep(.oxy-checkbox) {
|
|
57
|
+
margin-right: 6px;
|
|
58
|
+
|
|
59
|
+
.oxy-checkbox__label {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.oxy-checkbox__shape {
|
|
64
|
+
transition: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.oxy-checkbox__indeterminate {
|
|
68
|
+
transition: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.oxy-checkbox__check {
|
|
72
|
+
transition: none;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:deep(.oxy-tree-node-icon) {
|
|
77
|
+
transform: rotate(-90deg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.oxy-tree-node {
|
|
81
|
+
flex: 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TextProps, RawTreeNode } from './types'
|
|
2
|
+
|
|
3
|
+
export const useTreeMethods = (props: TextProps) => {
|
|
4
|
+
function getDisabled(node: RawTreeNode): boolean {
|
|
5
|
+
return node.disabled ?? false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getChildren(node: RawTreeNode): RawTreeNode[] {
|
|
9
|
+
return node[props.childrenKey] ?? []
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getLabel(node: RawTreeNode): string {
|
|
13
|
+
return node[props.labelKey] ?? ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getKey(node: RawTreeNode | undefined): string {
|
|
17
|
+
if (!node) {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
return (node[props.valueKey] ?? '') as string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
getDisabled,
|
|
25
|
+
getChildren,
|
|
26
|
+
getLabel,
|
|
27
|
+
getKey
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 判断两个 Set 中的元素是否完全相同(不考虑顺序)
|
|
32
|
+
* @param {Set} setA - 第一个 Set
|
|
33
|
+
* @param {Set} setB - 第二个 Set
|
|
34
|
+
* @returns {boolean} 两个 Set 是否相同
|
|
35
|
+
*/
|
|
36
|
+
export function isSetsEqual(setA: Set<any>, setB: Set<any>) {
|
|
37
|
+
// 步骤1:如果大小不同,直接返回 false
|
|
38
|
+
if (setA.size !== setB.size) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 步骤2:遍历 setA 的所有元素,检查 setB 是否都包含
|
|
43
|
+
for (const item of setA) {
|
|
44
|
+
if (!setB.has(item)) {
|
|
45
|
+
return false // 有元素不匹配,返回 false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 所有元素匹配,返回 true
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="oxy-tree" :class="customClass" :style="customStyle">
|
|
3
|
+
<view v-if="data.length" class="oxy-tree__virtual-scroll" :style="{ height: height }">
|
|
4
|
+
<scroll-view class="oxy-tree__view" scroll-y :scroll-x="true" :scroll-top="scrollTop" @scroll="handleScroll">
|
|
5
|
+
<view class="oxy-tree__container" :style="{ height: totalHeight + 'px' }">
|
|
6
|
+
<view class="oxy-tree__items" :style="{ transform: `translateY(${virtualOffsetY}px)` }">
|
|
7
|
+
<view v-for="(item, index) in virtualData" :key="index">
|
|
8
|
+
<view class="oxy-tree-node-content" :style="getNodeStyle(item)" :class="getNodeClass(item)" @click="handleNodeClick(item)">
|
|
9
|
+
<!-- 兼容支付宝、微信小程序 -->
|
|
10
|
+
<view @tap.stop="handleClickExpand(item)">
|
|
11
|
+
<oxy-icon name="fill-arrow-down" custom-class="oxy-tree-node-icon" size="22px"></oxy-icon>
|
|
12
|
+
</view>
|
|
13
|
+
<oxy-checkbox
|
|
14
|
+
v-if="showCheckbox"
|
|
15
|
+
:modelValue="item.checked"
|
|
16
|
+
:disabled="item.disabled"
|
|
17
|
+
:indeterminate="item.immediate"
|
|
18
|
+
shape="square"
|
|
19
|
+
></oxy-checkbox>
|
|
20
|
+
<view class="oxy-tree-node">
|
|
21
|
+
<slot name="node" :node="item" :data="item.data">
|
|
22
|
+
{{ item.label }}
|
|
23
|
+
</slot>
|
|
24
|
+
</view>
|
|
25
|
+
</view>
|
|
26
|
+
</view>
|
|
27
|
+
</view>
|
|
28
|
+
</view>
|
|
29
|
+
</scroll-view>
|
|
30
|
+
</view>
|
|
31
|
+
<view v-else>
|
|
32
|
+
<oxy-status-tip image="content" :tip="emptyText" />
|
|
33
|
+
</view>
|
|
34
|
+
|
|
35
|
+
<!-- 回到顶部按钮 -->
|
|
36
|
+
<view v-if="showBackToTop && showBackTopBtn" class="oxy-virtual-scroll__back-top" @click="scrollToTop">
|
|
37
|
+
<oxy-icon name="backtop" color="#fff" size="20px"></oxy-icon>
|
|
38
|
+
</view>
|
|
39
|
+
</view>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
export default {
|
|
44
|
+
name: 'oxy-tree',
|
|
45
|
+
options: {
|
|
46
|
+
virtualHost: true,
|
|
47
|
+
addGlobalClass: true,
|
|
48
|
+
styleIsolation: 'shared'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<script lang="ts" setup>
|
|
54
|
+
import { computed, nextTick, provide, type Ref, ref, toRefs, watch } from 'vue'
|
|
55
|
+
import type { RawTreeNode, Tree, TreeInstance, TreeNode } from './types'
|
|
56
|
+
import { treeProps, type TreeExpose } from './types'
|
|
57
|
+
import { isSetsEqual, useTreeMethods } from './utils'
|
|
58
|
+
import { useVirtualScroll } from '../composables/useVirtualScroll'
|
|
59
|
+
|
|
60
|
+
// 获取组件的 props 和 emit 函数
|
|
61
|
+
const props = defineProps(treeProps)
|
|
62
|
+
const {
|
|
63
|
+
modelValue,
|
|
64
|
+
data,
|
|
65
|
+
defaultExpandedKeys,
|
|
66
|
+
expandAll,
|
|
67
|
+
showCheckbox,
|
|
68
|
+
checkStrictly,
|
|
69
|
+
selectionLeafOnly,
|
|
70
|
+
height,
|
|
71
|
+
itemHeight,
|
|
72
|
+
nodeKey,
|
|
73
|
+
backToTopThreshold
|
|
74
|
+
} = toRefs(props)
|
|
75
|
+
const emit = defineEmits<{
|
|
76
|
+
(e: 'node-click', node: TreeNode): void
|
|
77
|
+
(e: 'update:modelValue', value: string[] | string | undefined): void
|
|
78
|
+
(e: 'node-check', node: TreeNode): void
|
|
79
|
+
(e: 'node-expand', node: TreeNode): void
|
|
80
|
+
(e: 'node-collapse', node: TreeNode): void
|
|
81
|
+
}>()
|
|
82
|
+
|
|
83
|
+
const { getDisabled, getChildren, getLabel, getKey } = useTreeMethods(props)
|
|
84
|
+
const expandedKeySet = ref<Set<string>>(new Set())
|
|
85
|
+
const hiddenNodeKeySet = ref<Set<string>>(new Set())
|
|
86
|
+
const checkedKeys = ref<Set<string>>(new Set())
|
|
87
|
+
const immediateKeySet = ref<Set<string>>(new Set())
|
|
88
|
+
const currentNode = ref<TreeNode>()
|
|
89
|
+
|
|
90
|
+
const tree = ref<Tree>() as Ref<Tree>
|
|
91
|
+
const flattenTree = computed<TreeNode[]>(() => {
|
|
92
|
+
const expandedKeys = expandedKeySet.value
|
|
93
|
+
const hiddenKeys = hiddenNodeKeySet.value
|
|
94
|
+
const flattenNodes: TreeNode[] = []
|
|
95
|
+
const nodes = (tree.value && tree.value.treeNodes) || []
|
|
96
|
+
function traverse() {
|
|
97
|
+
const stack: TreeNode[] = []
|
|
98
|
+
for (let i = nodes.length - 1; i >= 0; --i) {
|
|
99
|
+
stack.push(nodes[i])
|
|
100
|
+
}
|
|
101
|
+
while (stack.length) {
|
|
102
|
+
const node = stack.pop()
|
|
103
|
+
if (!node) continue
|
|
104
|
+
if (!hiddenKeys.has(node.key)) {
|
|
105
|
+
flattenNodes.push(node)
|
|
106
|
+
}
|
|
107
|
+
if (expandedKeys.has(node.key)) {
|
|
108
|
+
const children = node.children
|
|
109
|
+
if (children) {
|
|
110
|
+
const length = children.length
|
|
111
|
+
for (let i = length - 1; i >= 0; --i) {
|
|
112
|
+
stack.push(children[i])
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
traverse()
|
|
119
|
+
return flattenNodes
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// 虚拟滚动逻辑
|
|
123
|
+
const {
|
|
124
|
+
scrollTop,
|
|
125
|
+
showBackTopBtn,
|
|
126
|
+
virtualData,
|
|
127
|
+
startIndex,
|
|
128
|
+
virtualOffsetY,
|
|
129
|
+
totalHeight,
|
|
130
|
+
displayData,
|
|
131
|
+
updateVisibleData,
|
|
132
|
+
scrollToTop: virtualScrollToTop,
|
|
133
|
+
scrollToBottom: virtualScrollToBottom,
|
|
134
|
+
scrollToPosition: virtualScrollToPosition,
|
|
135
|
+
scrollToElement: virtualScrollToElement,
|
|
136
|
+
scrollToElementById: virtualScrollToElementById,
|
|
137
|
+
onScroll: handleVirtualScroll
|
|
138
|
+
} = useVirtualScroll({
|
|
139
|
+
data: flattenTree,
|
|
140
|
+
virtual: ref(true),
|
|
141
|
+
height: height,
|
|
142
|
+
itemHeight: itemHeight,
|
|
143
|
+
idKey: ref('key'),
|
|
144
|
+
backToTopThreshold: backToTopThreshold
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const createTree = (data: RawTreeNode[]) => {
|
|
148
|
+
const treeNodeMap = new Map<string, TreeNode>()
|
|
149
|
+
const levelTreeNodeMap = new Map<number, TreeNode[]>()
|
|
150
|
+
|
|
151
|
+
let maxLevel = 1
|
|
152
|
+
|
|
153
|
+
function traverse(nodes: RawTreeNode[], level = 1, parent?: TreeNode): TreeNode[] {
|
|
154
|
+
const siblings: TreeNode[] = []
|
|
155
|
+
nodes.forEach((rawNode, index) => {
|
|
156
|
+
const value = getKey(rawNode)
|
|
157
|
+
const node: TreeNode = {
|
|
158
|
+
level,
|
|
159
|
+
key: value,
|
|
160
|
+
data: rawNode,
|
|
161
|
+
index,
|
|
162
|
+
isLast: index === nodes.length - 1,
|
|
163
|
+
expanded: expandedKeySet.value.has(value),
|
|
164
|
+
label: getLabel(rawNode),
|
|
165
|
+
parent: parent,
|
|
166
|
+
isLeaf: false
|
|
167
|
+
}
|
|
168
|
+
if (showCheckbox.value) {
|
|
169
|
+
node.checked = checkedKeys.value.has(value)
|
|
170
|
+
}
|
|
171
|
+
if (expandAll.value) {
|
|
172
|
+
node.expanded = true
|
|
173
|
+
expandedKeySet.value.add(value)
|
|
174
|
+
}
|
|
175
|
+
const children = getChildren(rawNode)
|
|
176
|
+
node.disabled = getDisabled(rawNode)
|
|
177
|
+
node.isLeaf = !children || children.length === 0
|
|
178
|
+
if (children && children.length) {
|
|
179
|
+
node.children = traverse(children, level + 1, node)
|
|
180
|
+
}
|
|
181
|
+
siblings.push(node)
|
|
182
|
+
treeNodeMap.set(value, node)
|
|
183
|
+
if (!levelTreeNodeMap.has(level)) {
|
|
184
|
+
levelTreeNodeMap.set(level, [])
|
|
185
|
+
}
|
|
186
|
+
levelTreeNodeMap.get(level)!.push(node)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (level > maxLevel) {
|
|
190
|
+
maxLevel = level
|
|
191
|
+
}
|
|
192
|
+
return siblings
|
|
193
|
+
}
|
|
194
|
+
const treeNodes = traverse(data)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
treeNodeMap,
|
|
198
|
+
levelTreeNodeMap,
|
|
199
|
+
maxLevel,
|
|
200
|
+
treeNodes
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const toggleExpand = (node: TreeNode, flag: boolean) => {
|
|
205
|
+
node.expanded = flag
|
|
206
|
+
const key = node.key
|
|
207
|
+
if (node.expanded) {
|
|
208
|
+
expandedKeySet.value.add(key)
|
|
209
|
+
emit('node-expand', node)
|
|
210
|
+
} else {
|
|
211
|
+
expandedKeySet.value.delete(key)
|
|
212
|
+
emit('node-collapse', node)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const toggleChecked = (node: TreeNode, flag: boolean, isClick: boolean = false) => {
|
|
216
|
+
if (!showCheckbox.value || !node || node.disabled) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toggle(currentNode: TreeNode) {
|
|
221
|
+
const key = currentNode.key
|
|
222
|
+
currentNode.checked = flag
|
|
223
|
+
currentNode.immediate = false
|
|
224
|
+
|
|
225
|
+
if (currentNode.checked && (!selectionLeafOnly.value || currentNode.isLeaf)) {
|
|
226
|
+
checkedKeys.value.add(key)
|
|
227
|
+
} else {
|
|
228
|
+
checkedKeys.value.delete(key)
|
|
229
|
+
}
|
|
230
|
+
if (!checkStrictly.value && currentNode.children) {
|
|
231
|
+
currentNode.children.forEach((child) => {
|
|
232
|
+
toggle(child)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
toggle(node)
|
|
237
|
+
!checkStrictly.value && updateParentNode(node)
|
|
238
|
+
updateValue()
|
|
239
|
+
isClick && emit('node-check', node)
|
|
240
|
+
}
|
|
241
|
+
const handleClick = (node: TreeNode) => {
|
|
242
|
+
emit('node-click', node)
|
|
243
|
+
|
|
244
|
+
if (node.disabled) return
|
|
245
|
+
currentNode.value = node
|
|
246
|
+
|
|
247
|
+
if (!showCheckbox.value) {
|
|
248
|
+
updateValue()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 滚动事件处理
|
|
253
|
+
const handleScroll = (event: any) => {
|
|
254
|
+
handleVirtualScroll(event.detail.scrollTop)
|
|
255
|
+
}
|
|
256
|
+
const updateParentNode = (node: TreeNode) => {
|
|
257
|
+
if (!node.parent) return
|
|
258
|
+
updateNode(node.parent)
|
|
259
|
+
updateParentNode(node.parent)
|
|
260
|
+
}
|
|
261
|
+
const updateNode = (node: TreeNode | undefined) => {
|
|
262
|
+
if (!node) return
|
|
263
|
+
if (node.children?.every((item) => item.checked)) {
|
|
264
|
+
node.checked = true
|
|
265
|
+
node.immediate = false
|
|
266
|
+
!node.disabled && (!selectionLeafOnly.value || node.isLeaf) && checkedKeys.value.add(node.key)
|
|
267
|
+
} else if (node.children?.every((item) => !item.checked && !item.immediate)) {
|
|
268
|
+
node.checked = false
|
|
269
|
+
node.immediate = false
|
|
270
|
+
checkedKeys.value.delete(node.key)
|
|
271
|
+
} else {
|
|
272
|
+
node.checked = false
|
|
273
|
+
node.immediate = true
|
|
274
|
+
checkedKeys.value.delete(node.key)
|
|
275
|
+
!node.disabled && immediateKeySet.value.add(node.key)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const expandNode = (node: TreeNode | undefined) => {
|
|
280
|
+
if (!node) return
|
|
281
|
+
!node.isLeaf && expandedKeySet.value.add(node.key)
|
|
282
|
+
node.expanded = true
|
|
283
|
+
if (node.parent) {
|
|
284
|
+
expandNode(node.parent)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const initValue = () => {
|
|
288
|
+
if (showCheckbox.value) {
|
|
289
|
+
if (isSetsEqual(checkedKeys.value, new Set(modelValue.value as string[]))) return
|
|
290
|
+
checkedKeys.value = new Set(modelValue.value as string[])
|
|
291
|
+
|
|
292
|
+
Array.isArray(modelValue.value) &&
|
|
293
|
+
modelValue.value.map((value) => {
|
|
294
|
+
const node = tree.value?.treeNodeMap.get(value as string)
|
|
295
|
+
if (node) {
|
|
296
|
+
toggleChecked(node, true)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
} else {
|
|
300
|
+
currentNode.value = tree.value?.treeNodeMap.get(modelValue.value as string)
|
|
301
|
+
expandNode(currentNode.value?.parent)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const initDefaultExpandedKeys = () => {
|
|
305
|
+
defaultExpandedKeys.value.map((key) => {
|
|
306
|
+
expandNode(tree.value?.treeNodeMap.get(key as string))
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
const updateValue = () => {
|
|
310
|
+
if (showCheckbox.value) {
|
|
311
|
+
emit('update:modelValue', Array.from(checkedKeys.value))
|
|
312
|
+
} else {
|
|
313
|
+
emit('update:modelValue', currentNode.value?.key)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const getNodeStyle = (item: TreeNode) => {
|
|
317
|
+
return {
|
|
318
|
+
height: itemHeight.value,
|
|
319
|
+
paddingLeft: `${(item.level - 1) * (props.indent || 16)}px`
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const getNodeClass = (item: TreeNode) => {
|
|
323
|
+
const currentValue = currentNode?.value
|
|
324
|
+
return {
|
|
325
|
+
expanded: item.expanded,
|
|
326
|
+
checked: item.checked,
|
|
327
|
+
'is-leaf': item.isLeaf,
|
|
328
|
+
immediate: item.immediate,
|
|
329
|
+
'is-current': currentValue === item,
|
|
330
|
+
'is-disabled': item.disabled
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const handleNodeClick = (item: TreeNode) => {
|
|
334
|
+
toggleChecked(item, !item.checked, true)
|
|
335
|
+
handleClick(item)
|
|
336
|
+
}
|
|
337
|
+
const handleClickExpand = (item: TreeNode) => {
|
|
338
|
+
toggleExpand(item, !item.expanded)
|
|
339
|
+
}
|
|
340
|
+
watch(
|
|
341
|
+
() => data.value,
|
|
342
|
+
() => {
|
|
343
|
+
tree.value = createTree(data.value)
|
|
344
|
+
nextTick(() => {
|
|
345
|
+
checkedKeys.value = new Set()
|
|
346
|
+
initValue()
|
|
347
|
+
initDefaultExpandedKeys()
|
|
348
|
+
})
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
immediate: true
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
watch(
|
|
355
|
+
() => modelValue.value,
|
|
356
|
+
() => {
|
|
357
|
+
initValue()
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
const getNodeById = (id: string | number) => {
|
|
361
|
+
return tree.value?.treeNodeMap.get(id as string)
|
|
362
|
+
}
|
|
363
|
+
const scrollToTop = () => {
|
|
364
|
+
virtualScrollToTop()
|
|
365
|
+
}
|
|
366
|
+
const scrollToBottom = () => {
|
|
367
|
+
const visibleCount = flattenTree.value.length
|
|
368
|
+
const containerHeight = parseFloat(height.value || '0')
|
|
369
|
+
const targetScrollTop = Math.max(visibleCount * parseFloat(itemHeight.value) - containerHeight, 0)
|
|
370
|
+
virtualScrollToPosition(targetScrollTop)
|
|
371
|
+
}
|
|
372
|
+
const scrollToPosition = (position: number | string) => {
|
|
373
|
+
virtualScrollToPosition(position)
|
|
374
|
+
}
|
|
375
|
+
const scrollToElement = (node: any) => {
|
|
376
|
+
expandNode(node)
|
|
377
|
+
nextTick(() => {
|
|
378
|
+
virtualScrollToElement(node)
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
const scrollToElementById = (id: string | number) => {
|
|
382
|
+
const targetNode = tree.value?.treeNodeMap.get(id as string)
|
|
383
|
+
if (targetNode) {
|
|
384
|
+
expandNode(targetNode)
|
|
385
|
+
nextTick(() => {
|
|
386
|
+
virtualScrollToElementById(id)
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
defineExpose<TreeExpose>({
|
|
391
|
+
toggleExpand,
|
|
392
|
+
toggleChecked,
|
|
393
|
+
getNodeById,
|
|
394
|
+
useTree: () => tree.value,
|
|
395
|
+
getTree: () => tree,
|
|
396
|
+
scrollToTop,
|
|
397
|
+
scrollToBottom,
|
|
398
|
+
scrollToPosition,
|
|
399
|
+
scrollToElement,
|
|
400
|
+
scrollToElementById
|
|
401
|
+
})
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<style lang="scss" scoped>
|
|
405
|
+
@import './index.scss';
|
|
406
|
+
</style>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ComponentPublicInstance, ExtractPropTypes, Ref } from 'vue'
|
|
2
|
+
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp } from '../common/props'
|
|
3
|
+
|
|
4
|
+
// 树结构类型定义
|
|
5
|
+
export type Tree = {
|
|
6
|
+
treeNodeMap: Map<string, TreeNode>
|
|
7
|
+
levelTreeNodeMap: Map<number, TreeNode[]>
|
|
8
|
+
maxLevel: number
|
|
9
|
+
treeNodes: TreeNode[]
|
|
10
|
+
}
|
|
11
|
+
// 原始树节点类型
|
|
12
|
+
export type RawTreeNode = {
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
[key: string]: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 处理后的树节点类型
|
|
18
|
+
export type TreeNode = {
|
|
19
|
+
level: number
|
|
20
|
+
key: string
|
|
21
|
+
data: RawTreeNode
|
|
22
|
+
index: number
|
|
23
|
+
isLast: boolean
|
|
24
|
+
expanded: boolean
|
|
25
|
+
label: string
|
|
26
|
+
parent?: TreeNode
|
|
27
|
+
children?: TreeNode[]
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
isLeaf: boolean
|
|
30
|
+
checked?: boolean
|
|
31
|
+
immediate?: boolean
|
|
32
|
+
isCurrent?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const treeProps = {
|
|
36
|
+
...baseProps,
|
|
37
|
+
data: makeArrayProp<RawTreeNode>(),
|
|
38
|
+
showCheckbox: makeBooleanProp(false),
|
|
39
|
+
childrenKey: makeStringProp('children'),
|
|
40
|
+
labelKey: makeStringProp('name'),
|
|
41
|
+
nodeKey: makeStringProp('id'),
|
|
42
|
+
defaultExpandedKeys: makeArrayProp<string>(),
|
|
43
|
+
expandAll: makeBooleanProp(false),
|
|
44
|
+
checkStrictly: makeBooleanProp(false),
|
|
45
|
+
modelValue: {
|
|
46
|
+
type: [Array<string>, String],
|
|
47
|
+
default: ''
|
|
48
|
+
},
|
|
49
|
+
emptyText: makeStringProp('暂无数据'),
|
|
50
|
+
height: makeStringProp('300px'),
|
|
51
|
+
/**
|
|
52
|
+
* 是否显示回到顶部按钮
|
|
53
|
+
* 类型:boolean
|
|
54
|
+
* 默认值:false
|
|
55
|
+
*/
|
|
56
|
+
showBackToTop: makeBooleanProp(false),
|
|
57
|
+
/**
|
|
58
|
+
* 滚动多远显示backToTop
|
|
59
|
+
* 类型:number
|
|
60
|
+
* 默认值:'300px'
|
|
61
|
+
*/
|
|
62
|
+
backToTopThreshold: makeStringProp('300px'),
|
|
63
|
+
/**
|
|
64
|
+
* 单个项目高度
|
|
65
|
+
* 类型:number
|
|
66
|
+
* 默认值:'44px'
|
|
67
|
+
*/
|
|
68
|
+
itemHeight: makeStringProp('44px'),
|
|
69
|
+
indent: makeNumberProp(16),
|
|
70
|
+
selectionLeafOnly: makeBooleanProp(false)
|
|
71
|
+
}
|
|
72
|
+
export type TreeExpose = {
|
|
73
|
+
toggleExpand: (node: TreeNode, flag: boolean) => void
|
|
74
|
+
toggleChecked: (node: TreeNode, flag: boolean, isClick: boolean) => void
|
|
75
|
+
getNodeById: (id: string | number) => TreeNode | undefined
|
|
76
|
+
useTree: () => Tree
|
|
77
|
+
getTree: () => Ref<Tree>
|
|
78
|
+
scrollToTop: () => void
|
|
79
|
+
scrollToBottom: () => void
|
|
80
|
+
scrollToPosition: (position: number | string) => void
|
|
81
|
+
scrollToElement: (item: any) => void
|
|
82
|
+
scrollToElementById: (id: string | number) => void
|
|
83
|
+
}
|
|
84
|
+
export type TreeProps = ExtractPropTypes<typeof treeProps>
|
|
85
|
+
export type TreeInstance = ComponentPublicInstance<TreeProps, TreeExpose>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RawTreeNode, TreeProps } from './types'
|
|
2
|
+
|
|
3
|
+
export const useTreeMethods = (props: TreeProps) => {
|
|
4
|
+
function getDisabled(node: RawTreeNode): boolean {
|
|
5
|
+
return node.disabled ?? false
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getChildren(node: RawTreeNode): RawTreeNode[] {
|
|
9
|
+
return node[props.childrenKey] ?? []
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getLabel(node: RawTreeNode): string {
|
|
13
|
+
return node[props.labelKey] ?? ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getKey(node: RawTreeNode | undefined): string {
|
|
17
|
+
if (!node) {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
return (node[props.nodeKey] ?? '') as string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
getDisabled,
|
|
25
|
+
getChildren,
|
|
26
|
+
getLabel,
|
|
27
|
+
getKey
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 判断两个 Set 中的元素是否完全相同(不考虑顺序)
|
|
32
|
+
* @param {Set} setA - 第一个 Set
|
|
33
|
+
* @param {Set} setB - 第二个 Set
|
|
34
|
+
* @returns {boolean} 两个 Set 是否相同
|
|
35
|
+
*/
|
|
36
|
+
export function isSetsEqual(setA: Set<any>, setB: Set<any>) {
|
|
37
|
+
// 步骤1:如果大小不同,直接返回 false
|
|
38
|
+
if (setA.size !== setB.size) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 步骤2:遍历 setA 的所有元素,检查 setB 是否都包含
|
|
43
|
+
for (const item of setA) {
|
|
44
|
+
if (!setB.has(item)) {
|
|
45
|
+
return false // 有元素不匹配,返回 false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 所有元素匹配,返回 true
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|