oxy-uni-ui 1.1.0 → 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/attributes.json +1 -1
- package/components/common/abstracts/variable.scss +8 -0
- package/components/composables/index.ts +1 -0
- package/components/composables/useVirtualScroll.ts +172 -0
- package/components/oxy-corner/index.scss +121 -1
- package/components/oxy-corner/oxy-corner.vue +3 -2
- package/components/oxy-corner/types.ts +9 -2
- package/components/oxy-list/index.scss +3 -2
- package/components/oxy-list/oxy-list.vue +121 -40
- package/components/oxy-list/types.ts +2 -14
- package/components/oxy-tree/index.scss +28 -6
- package/components/oxy-tree/oxy-tree.vue +147 -30
- package/components/oxy-tree/types.ts +43 -6
- package/components/oxy-tree/utils.ts +51 -0
- package/components/oxy-virtual-scroll/index.scss +1 -1
- package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +69 -110
- package/components/oxy-virtual-scroll/types.ts +95 -5
- package/locale/lang/en-US.ts +9 -9
- package/locale/lang/zh-CN.ts +5 -5
- package/package.json +1 -1
- package/tags.json +1 -1
- package/web-types.json +1 -1
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
@import '../common/abstracts/variable';
|
|
2
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
|
+
}
|
|
3
27
|
|
|
4
28
|
.oxy-tree-node-content {
|
|
5
|
-
line-height: 44px;
|
|
6
|
-
height: 44px;
|
|
7
29
|
display: flex;
|
|
8
30
|
width: fit-content;
|
|
9
31
|
width: 100%;
|
|
@@ -11,13 +33,13 @@
|
|
|
11
33
|
align-items: center;
|
|
12
34
|
|
|
13
35
|
&.expanded {
|
|
14
|
-
.oxy-tree-node-icon {
|
|
36
|
+
:deep(.oxy-tree-node-icon) {
|
|
15
37
|
transform: rotate(0deg);
|
|
16
38
|
}
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
&.is-leaf {
|
|
20
|
-
.oxy-tree-node-icon {
|
|
42
|
+
:deep(.oxy-tree-node-icon) {
|
|
21
43
|
opacity: 0;
|
|
22
44
|
}
|
|
23
45
|
}
|
|
@@ -51,11 +73,11 @@
|
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
|
|
54
|
-
.oxy-tree-node-icon {
|
|
76
|
+
:deep(.oxy-tree-node-icon) {
|
|
55
77
|
transform: rotate(-90deg);
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
.oxy-tree-node {
|
|
59
81
|
flex: 1;
|
|
60
82
|
}
|
|
61
|
-
}
|
|
83
|
+
}
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<view class="oxy-tree" :class="customClass" :style="customStyle">
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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>
|
|
12
31
|
<view v-else>
|
|
13
32
|
<oxy-status-tip image="content" :tip="emptyText" />
|
|
14
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>
|
|
15
39
|
</view>
|
|
16
40
|
</template>
|
|
17
41
|
|
|
@@ -27,23 +51,27 @@ export default {
|
|
|
27
51
|
</script>
|
|
28
52
|
|
|
29
53
|
<script lang="ts" setup>
|
|
30
|
-
import { computed, nextTick, provide, ref, toRefs, watch } from 'vue'
|
|
31
|
-
import type { RawTreeNode, TreeNode } from './types'
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
// 树结构类型定义
|
|
37
|
-
type Tree = {
|
|
38
|
-
treeNodeMap: Map<string, TreeNode>
|
|
39
|
-
levelTreeNodeMap: Map<number, TreeNode[]>
|
|
40
|
-
maxLevel: number
|
|
41
|
-
treeNodes: TreeNode[]
|
|
42
|
-
}
|
|
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'
|
|
43
59
|
|
|
44
60
|
// 获取组件的 props 和 emit 函数
|
|
45
|
-
const props = defineProps(
|
|
46
|
-
const {
|
|
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)
|
|
47
75
|
const emit = defineEmits<{
|
|
48
76
|
(e: 'node-click', node: TreeNode): void
|
|
49
77
|
(e: 'update:modelValue', value: string[] | string | undefined): void
|
|
@@ -58,11 +86,8 @@ const hiddenNodeKeySet = ref<Set<string>>(new Set())
|
|
|
58
86
|
const checkedKeys = ref<Set<string>>(new Set())
|
|
59
87
|
const immediateKeySet = ref<Set<string>>(new Set())
|
|
60
88
|
const currentNode = ref<TreeNode>()
|
|
61
|
-
provide('treeProps', props)
|
|
62
|
-
provide('currentNode', currentNode)
|
|
63
|
-
|
|
64
|
-
const tree = ref<Tree>()
|
|
65
89
|
|
|
90
|
+
const tree = ref<Tree>() as Ref<Tree>
|
|
66
91
|
const flattenTree = computed<TreeNode[]>(() => {
|
|
67
92
|
const expandedKeys = expandedKeySet.value
|
|
68
93
|
const hiddenKeys = hiddenNodeKeySet.value
|
|
@@ -93,6 +118,32 @@ const flattenTree = computed<TreeNode[]>(() => {
|
|
|
93
118
|
traverse()
|
|
94
119
|
return flattenNodes
|
|
95
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
|
+
|
|
96
147
|
const createTree = (data: RawTreeNode[]) => {
|
|
97
148
|
const treeNodeMap = new Map<string, TreeNode>()
|
|
98
149
|
const levelTreeNodeMap = new Map<number, TreeNode[]>()
|
|
@@ -197,6 +248,11 @@ const handleClick = (node: TreeNode) => {
|
|
|
197
248
|
updateValue()
|
|
198
249
|
}
|
|
199
250
|
}
|
|
251
|
+
|
|
252
|
+
// 滚动事件处理
|
|
253
|
+
const handleScroll = (event: any) => {
|
|
254
|
+
handleVirtualScroll(event.detail.scrollTop)
|
|
255
|
+
}
|
|
200
256
|
const updateParentNode = (node: TreeNode) => {
|
|
201
257
|
if (!node.parent) return
|
|
202
258
|
updateNode(node.parent)
|
|
@@ -257,6 +313,30 @@ const updateValue = () => {
|
|
|
257
313
|
emit('update:modelValue', currentNode.value?.key)
|
|
258
314
|
}
|
|
259
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
|
+
}
|
|
260
340
|
watch(
|
|
261
341
|
() => data.value,
|
|
262
342
|
() => {
|
|
@@ -277,10 +357,47 @@ watch(
|
|
|
277
357
|
initValue()
|
|
278
358
|
}
|
|
279
359
|
)
|
|
280
|
-
|
|
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>({
|
|
281
391
|
toggleExpand,
|
|
282
392
|
toggleChecked,
|
|
283
|
-
|
|
393
|
+
getNodeById,
|
|
394
|
+
useTree: () => tree.value,
|
|
395
|
+
getTree: () => tree,
|
|
396
|
+
scrollToTop,
|
|
397
|
+
scrollToBottom,
|
|
398
|
+
scrollToPosition,
|
|
399
|
+
scrollToElement,
|
|
400
|
+
scrollToElementById
|
|
284
401
|
})
|
|
285
402
|
</script>
|
|
286
403
|
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type { ExtractPropTypes } from 'vue'
|
|
1
|
+
import type { ComponentPublicInstance, ExtractPropTypes, Ref } from 'vue'
|
|
2
2
|
import { baseProps, makeArrayProp, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp } from '../common/props'
|
|
3
3
|
|
|
4
|
+
// 树结构类型定义
|
|
5
|
+
export type Tree = {
|
|
6
|
+
treeNodeMap: Map<string, TreeNode>
|
|
7
|
+
levelTreeNodeMap: Map<number, TreeNode[]>
|
|
8
|
+
maxLevel: number
|
|
9
|
+
treeNodes: TreeNode[]
|
|
10
|
+
}
|
|
4
11
|
// 原始树节点类型
|
|
5
12
|
export type RawTreeNode = {
|
|
6
13
|
disabled?: boolean
|
|
@@ -25,13 +32,13 @@ export type TreeNode = {
|
|
|
25
32
|
isCurrent?: boolean
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
export const
|
|
35
|
+
export const treeProps = {
|
|
29
36
|
...baseProps,
|
|
30
37
|
data: makeArrayProp<RawTreeNode>(),
|
|
31
38
|
showCheckbox: makeBooleanProp(false),
|
|
32
39
|
childrenKey: makeStringProp('children'),
|
|
33
40
|
labelKey: makeStringProp('name'),
|
|
34
|
-
|
|
41
|
+
nodeKey: makeStringProp('id'),
|
|
35
42
|
defaultExpandedKeys: makeArrayProp<string>(),
|
|
36
43
|
expandAll: makeBooleanProp(false),
|
|
37
44
|
checkStrictly: makeBooleanProp(false),
|
|
@@ -40,9 +47,39 @@ export const textProps = {
|
|
|
40
47
|
default: ''
|
|
41
48
|
},
|
|
42
49
|
emptyText: makeStringProp('暂无数据'),
|
|
43
|
-
|
|
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'),
|
|
44
69
|
indent: makeNumberProp(16),
|
|
45
70
|
selectionLeafOnly: makeBooleanProp(false)
|
|
46
71
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<view :class="['oxy-virtual-scroll', customClass]" :style="
|
|
2
|
+
<view :class="['oxy-virtual-scroll', customClass]" :style="customStyle">
|
|
3
3
|
<!-- 滚动内容区域 -->
|
|
4
4
|
<scroll-view
|
|
5
|
-
|
|
5
|
+
v-if="data.length"
|
|
6
|
+
:style="{ height: height }"
|
|
6
7
|
class="oxy-virtual-scroll__view"
|
|
7
|
-
:scroll-y="
|
|
8
|
+
:scroll-y="scrollY"
|
|
8
9
|
:scroll-x="scrollX"
|
|
10
|
+
:upper-threshold="upperThreshold"
|
|
11
|
+
:lower-threshold="lowerThreshold"
|
|
9
12
|
:scroll-top="scrollTop"
|
|
10
|
-
:scroll-
|
|
11
|
-
|
|
13
|
+
:scroll-left="scrollLeft"
|
|
14
|
+
:scroll-with-animation="scrollWithAnimation"
|
|
15
|
+
:enable-back-to-top="enableBackToTop"
|
|
16
|
+
:show-scrollbar="showScrollbar"
|
|
17
|
+
:refresher-enabled="refresherEnabled"
|
|
18
|
+
:refresher-threshold="refresherThreshold"
|
|
19
|
+
:refresher-default-style="refresherDefaultStyle"
|
|
20
|
+
:refresher-background="refresherBackground"
|
|
21
|
+
:refresher-triggered="triggered"
|
|
22
|
+
:enable-flex="enableFlex"
|
|
23
|
+
:scroll-anchoring="scrollAnchoring"
|
|
12
24
|
@scroll="onScroll"
|
|
13
25
|
@scrolltoupper="onScrollUpper"
|
|
14
26
|
@scrolltolower="onScrollLower"
|
|
15
27
|
>
|
|
16
28
|
<view class="oxy-virtual-scroll__container" :style="{ height: totalHeight + 'px' }">
|
|
17
29
|
<view class="oxy-virtual-scroll__items" :style="{ transform: `translateY(${virtualOffsetY}px)` }">
|
|
18
|
-
<
|
|
30
|
+
<view v-for="(item, index) in virtualData" :key="index">
|
|
31
|
+
<slot name="item" :item="item" :index="startIndex + index"></slot>
|
|
32
|
+
</view>
|
|
19
33
|
<slot name="bottom"></slot>
|
|
20
34
|
</view>
|
|
21
35
|
</view>
|
|
22
36
|
</scroll-view>
|
|
23
37
|
|
|
38
|
+
<view v-else>
|
|
39
|
+
<oxy-status-tip image="content" :tip="emptyText" />
|
|
40
|
+
</view>
|
|
41
|
+
|
|
24
42
|
<!-- 回到顶部按钮 -->
|
|
25
43
|
<view v-if="showBackToTop && showBackTopBtn" class="oxy-virtual-scroll__back-top" @click="scrollToTop">
|
|
26
44
|
<oxy-icon name="backtop" color="#fff" size="20px"></oxy-icon>
|
|
@@ -40,91 +58,47 @@ export default {
|
|
|
40
58
|
</script>
|
|
41
59
|
|
|
42
60
|
<script lang="ts" setup>
|
|
43
|
-
import {
|
|
61
|
+
import { toRefs } from 'vue'
|
|
44
62
|
import type { ScrollViewOnScrollEvent, ScrollViewOnScrolltolowerEvent, ScrollViewOnScrolltoupperEvent } from '@uni-helper/uni-app-types'
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
|
|
48
|
-
inheritAttrs: false
|
|
49
|
-
})
|
|
63
|
+
import { virtualScrollProps, type VirtualScrollExpose } from './types'
|
|
64
|
+
import { useVirtualScroll } from '../composables/useVirtualScroll'
|
|
65
|
+
|
|
50
66
|
const props = defineProps(virtualScrollProps)
|
|
51
67
|
const emit = defineEmits(['scroll', 'scroll-to-upper', 'scroll-to-lower'])
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
onMounted(() => {
|
|
84
|
-
initScrollEngine()
|
|
69
|
+
// 解构props用于组合式函数
|
|
70
|
+
const { data, virtual, height, itemHeight, idKey, showBackToTop, backToTopThreshold } = toRefs(props)
|
|
71
|
+
|
|
72
|
+
// 使用虚拟滚动组合式函数
|
|
73
|
+
const {
|
|
74
|
+
scrollTop,
|
|
75
|
+
showBackTopBtn,
|
|
76
|
+
virtualData,
|
|
77
|
+
startIndex,
|
|
78
|
+
virtualOffsetY,
|
|
79
|
+
totalHeight,
|
|
80
|
+
displayData,
|
|
81
|
+
initScrollData,
|
|
82
|
+
initScrollEngine,
|
|
83
|
+
updateVisibleData,
|
|
84
|
+
scrollToTop: virtualScrollToTop,
|
|
85
|
+
scrollToBottom: virtualScrollToBottom,
|
|
86
|
+
scrollToPosition: virtualScrollToPosition,
|
|
87
|
+
scrollToElement: virtualScrollToElement,
|
|
88
|
+
scrollToElementById: virtualScrollToElementById,
|
|
89
|
+
onScroll: handleVirtualScroll
|
|
90
|
+
} = useVirtualScroll({
|
|
91
|
+
data,
|
|
92
|
+
virtual,
|
|
93
|
+
height,
|
|
94
|
+
itemHeight,
|
|
95
|
+
idKey,
|
|
96
|
+
backToTopThreshold
|
|
85
97
|
})
|
|
86
98
|
|
|
87
|
-
// 初始化滚动数据
|
|
88
|
-
function initScrollData() {
|
|
89
|
-
if (!props.virtual) {
|
|
90
|
-
// 非虚拟滚动模式:直接使用全部数据
|
|
91
|
-
virtualData.value = displayData.value
|
|
92
|
-
virtualOffsetY.value = 0
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
virtualEngine.value = new VirtualScrollEngine({
|
|
96
|
-
containerHeight: parseFloat(props.height),
|
|
97
|
-
itemHeight: parseFloat(props.itemHeight),
|
|
98
|
-
data: displayData.value
|
|
99
|
-
})
|
|
100
|
-
updateVisibleData()
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 初始化滚动引擎
|
|
104
|
-
function initScrollEngine() {
|
|
105
|
-
if (!props.virtual) return
|
|
106
|
-
virtualEngine.value = new VirtualScrollEngine({
|
|
107
|
-
containerHeight: parseFloat(props.height),
|
|
108
|
-
itemHeight: parseFloat(props.itemHeight),
|
|
109
|
-
data: displayData.value
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 更新可见数据
|
|
114
|
-
function updateVisibleData() {
|
|
115
|
-
if (!props.virtual) return // 非虚拟模式不处理
|
|
116
|
-
if (virtualEngine.value && scrollView.value) {
|
|
117
|
-
const { visibleData, offsetY } = virtualEngine.value.updateVisibleData(scrollTop.value || 0)
|
|
118
|
-
virtualData.value = visibleData
|
|
119
|
-
virtualOffsetY.value = offsetY
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
99
|
// 滚动事件
|
|
124
100
|
function onScroll(event: ScrollViewOnScrollEvent) {
|
|
125
|
-
|
|
126
|
-
showBackTopBtn.value = scrollTop.value > parseFloat(props.backToTopThreshold)
|
|
127
|
-
updateVisibleData()
|
|
101
|
+
handleVirtualScroll(event.detail.scrollTop)
|
|
128
102
|
emit('scroll', event)
|
|
129
103
|
}
|
|
130
104
|
|
|
@@ -138,39 +112,24 @@ function onScrollLower(event: ScrollViewOnScrolltolowerEvent) {
|
|
|
138
112
|
emit('scroll-to-lower', event)
|
|
139
113
|
}
|
|
140
114
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
nextTick(() => {
|
|
145
|
-
scrollTop.value = 0
|
|
146
|
-
})
|
|
115
|
+
// 滚动方法
|
|
116
|
+
const scrollToTop = () => {
|
|
117
|
+
virtualScrollToTop()
|
|
147
118
|
}
|
|
148
|
-
|
|
149
|
-
|
|
119
|
+
const scrollToBottom = () => {
|
|
120
|
+
virtualScrollToBottom()
|
|
150
121
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
function scrollToPosition(position: number | string) {
|
|
154
|
-
scrollTop.value = typeof position === 'number' ? position : parseFloat(position)
|
|
122
|
+
const scrollToPosition = (position: number | string) => {
|
|
123
|
+
virtualScrollToPosition(position)
|
|
155
124
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
function scrollToElement(item: any) {
|
|
159
|
-
const index = props.data.findIndex((o) => item[props.idKey] && o[props.idKey] && o[props.idKey] === item[props.idKey])
|
|
160
|
-
if (index > 0) {
|
|
161
|
-
const scrollDistance = parseFloat(props.itemHeight) * index
|
|
162
|
-
scrollToPosition(scrollDistance)
|
|
163
|
-
}
|
|
125
|
+
const scrollToElement = (item: any) => {
|
|
126
|
+
virtualScrollToElement(item)
|
|
164
127
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (index > 0) {
|
|
168
|
-
const scrollDistance = parseFloat(props.itemHeight) * index
|
|
169
|
-
scrollToPosition(scrollDistance)
|
|
170
|
-
}
|
|
128
|
+
const scrollToElementById = (id: string | number) => {
|
|
129
|
+
virtualScrollToElementById(id)
|
|
171
130
|
}
|
|
172
131
|
|
|
173
|
-
defineExpose({
|
|
132
|
+
defineExpose<VirtualScrollExpose>({
|
|
174
133
|
scrollToTop,
|
|
175
134
|
scrollToBottom,
|
|
176
135
|
scrollToPosition,
|