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
|
@@ -642,6 +642,7 @@ $-tag-close-color: var(--oxy-tag-close-color, $-tag-info-color) !default; // 关
|
|
|
642
642
|
$-tag-close-active-color: var(--oxy-tag-close-active-color, rgba(0, 0, 0, 0.45)) !default; // 关闭按钮 active 颜色
|
|
643
643
|
|
|
644
644
|
/* corner */
|
|
645
|
+
$-corner-radius: var(--oxy-corner-radius, 12px) !default; // 圆角大小
|
|
645
646
|
$-corner-font-size: var(--oxy-corner-fs, $-fs-secondary) !default; // 字号
|
|
646
647
|
$-corner-font-weight: var(--oxy-corner-fs, 400) !default; // 字号
|
|
647
648
|
$-corner-color: var(--oxy-corner-color, $-color-white) !default; // 字体颜色
|
|
@@ -662,6 +663,13 @@ $-corner-horizontal-info-bg: var(--oxy-corner-info-bg, resultColor(90deg, $-colo
|
|
|
662
663
|
$-corner-horizontal-success-bg: var(--oxy-corner-success-bg, resultColor(90deg, $-color-success, 'light' 'dark', #ffffff #b2ebcf, 0% 100%)) !default; // success 背景颜色
|
|
663
664
|
$-corner-horizontal-warning-bg: var(--oxy-corner-warning-bg, resultColor(90deg, $-color-warning, 'light' 'dark', #ffffff #fcedd6, 0% 100%)) !default; // warning 背景颜色
|
|
664
665
|
$-corner-horizontal-danger-bg: var(--oxy-corner-danger-bg, resultColor(90deg, $-color-danger, 'light' 'dark', #ffffff #ffd5d8, 0% 100%)) !default; // danger 背景颜色
|
|
666
|
+
$-corner-embedded-font-size: var(--oxy-corner-fs, $-fs-content) !default; // 字号
|
|
667
|
+
$-corner-embedded-font-weight: var(--oxy-corner-fs, 400) !default; // 字号
|
|
668
|
+
$-corner-embedded-primary-bg: var(--oxy-corner-primary-bg, #cceaff) !default; // 主背景颜色
|
|
669
|
+
$-corner-embedded-info-bg: var(--oxy-corner-info-bg, #eef4fc) !default; // info 背景颜色
|
|
670
|
+
$-corner-embedded-success-bg: var(--oxy-corner-success-bg, #b2ebcf) !default; // success 背景颜色
|
|
671
|
+
$-corner-embedded-warning-bg: var(--oxy-corner-warning-bg, #fcedd6) !default; // warning 背景颜色
|
|
672
|
+
$-corner-embedded-danger-bg: var(--oxy-corner-danger-bg, #ffd5d8) !default; // danger 背景颜色
|
|
665
673
|
|
|
666
674
|
/* toast */
|
|
667
675
|
$-toast-color: var(--oxy-toast-color, $-color-white) !default; // 文字颜色
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 虚拟滚动组合式函数 - 用于复用虚拟滚动逻辑
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, computed, watch, nextTick, type Ref } from 'vue'
|
|
6
|
+
import { VirtualScrollEngine } from '../oxy-virtual-scroll/virtual-scroll'
|
|
7
|
+
|
|
8
|
+
export interface UseVirtualScrollOptions {
|
|
9
|
+
data: Ref<any[]>
|
|
10
|
+
virtual: Ref<boolean>
|
|
11
|
+
height: Ref<string>
|
|
12
|
+
itemHeight: Ref<string>
|
|
13
|
+
idKey: Ref<string>
|
|
14
|
+
backToTopThreshold: Ref<string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseVirtualScrollReturn {
|
|
18
|
+
// 响应式数据
|
|
19
|
+
scrollTop: Ref<number>
|
|
20
|
+
showBackTopBtn: Ref<boolean>
|
|
21
|
+
virtualData: Ref<any[]>
|
|
22
|
+
startIndex: Ref<number>
|
|
23
|
+
virtualOffsetY: Ref<number>
|
|
24
|
+
totalHeight: Ref<number>
|
|
25
|
+
displayData: Ref<any[]>
|
|
26
|
+
|
|
27
|
+
// 方法
|
|
28
|
+
initScrollData: () => void
|
|
29
|
+
initScrollEngine: () => void
|
|
30
|
+
updateVisibleData: () => void
|
|
31
|
+
scrollToTop: () => void
|
|
32
|
+
scrollToBottom: () => void
|
|
33
|
+
scrollToPosition: (position: number | string) => void
|
|
34
|
+
scrollToElement: (item: any) => void
|
|
35
|
+
scrollToElementById: (id: string | number) => void
|
|
36
|
+
onScroll: (scrollTopValue: number) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useVirtualScroll(options: UseVirtualScrollOptions): UseVirtualScrollReturn {
|
|
40
|
+
const { data, virtual, height, itemHeight, idKey, backToTopThreshold } = options
|
|
41
|
+
|
|
42
|
+
// 响应式数据
|
|
43
|
+
const scrollTop = ref<number>(0)
|
|
44
|
+
const showBackTopBtn = ref<boolean>(false)
|
|
45
|
+
const virtualData = ref<any[]>([])
|
|
46
|
+
const startIndex = ref<number>(0)
|
|
47
|
+
const virtualOffsetY = ref<number>(0)
|
|
48
|
+
const virtualEngine = ref<any>(null)
|
|
49
|
+
|
|
50
|
+
// 计算属性
|
|
51
|
+
const displayData = computed<any[]>(() => data.value)
|
|
52
|
+
|
|
53
|
+
const totalHeight = computed(() => {
|
|
54
|
+
return displayData.value.length * parseFloat(itemHeight.value)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// 监听数据变化
|
|
58
|
+
watch(
|
|
59
|
+
() => data.value,
|
|
60
|
+
() => {
|
|
61
|
+
nextTick(initScrollData)
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
immediate: true,
|
|
65
|
+
deep: true
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// 初始化滚动数据
|
|
70
|
+
function initScrollData() {
|
|
71
|
+
if (!virtual.value) {
|
|
72
|
+
// 非虚拟滚动模式:直接使用全部数据
|
|
73
|
+
virtualData.value = displayData.value
|
|
74
|
+
virtualOffsetY.value = 0
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
virtualEngine.value = new VirtualScrollEngine({
|
|
79
|
+
containerHeight: parseFloat(height.value),
|
|
80
|
+
itemHeight: parseFloat(itemHeight.value),
|
|
81
|
+
data: displayData.value
|
|
82
|
+
})
|
|
83
|
+
updateVisibleData()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 初始化滚动引擎
|
|
87
|
+
function initScrollEngine() {
|
|
88
|
+
if (!virtual.value) return
|
|
89
|
+
|
|
90
|
+
virtualEngine.value = new VirtualScrollEngine({
|
|
91
|
+
containerHeight: parseFloat(height.value),
|
|
92
|
+
itemHeight: parseFloat(itemHeight.value),
|
|
93
|
+
data: displayData.value
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 更新可见数据
|
|
98
|
+
function updateVisibleData() {
|
|
99
|
+
if (!virtual.value) return // 非虚拟模式不处理
|
|
100
|
+
|
|
101
|
+
if (virtualEngine.value) {
|
|
102
|
+
const { visibleData, offsetY } = virtualEngine.value.updateVisibleData(scrollTop.value || 0)
|
|
103
|
+
virtualData.value = visibleData
|
|
104
|
+
virtualOffsetY.value = offsetY
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 滚动事件处理
|
|
109
|
+
function onScroll(scrollTopValue: number) {
|
|
110
|
+
scrollTop.value = scrollTopValue
|
|
111
|
+
showBackTopBtn.value = scrollTopValue > parseFloat(backToTopThreshold.value)
|
|
112
|
+
updateVisibleData()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 滚动到顶部
|
|
116
|
+
function scrollToTop() {
|
|
117
|
+
scrollTop.value = 0
|
|
118
|
+
nextTick(() => {
|
|
119
|
+
scrollTop.value = 0
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 滚动到底部
|
|
124
|
+
function scrollToBottom() {
|
|
125
|
+
scrollToPosition(totalHeight.value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 滚动到指定位置
|
|
129
|
+
function scrollToPosition(position: number | string) {
|
|
130
|
+
scrollTop.value = typeof position === 'number' ? position : parseFloat(position)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 滚动到指定元素
|
|
134
|
+
function scrollToElement(item: any) {
|
|
135
|
+
const index = data.value.findIndex((o) => item[idKey.value] && o[idKey.value] && o[idKey.value] === item[idKey.value])
|
|
136
|
+
if (index > 0) {
|
|
137
|
+
const scrollDistance = parseFloat(itemHeight.value) * index
|
|
138
|
+
scrollToPosition(scrollDistance)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 根据ID滚动到指定元素
|
|
143
|
+
function scrollToElementById(id: string | number) {
|
|
144
|
+
const index = data.value.findIndex((o) => id && o[idKey.value] && o[idKey.value] === id)
|
|
145
|
+
if (index > 0) {
|
|
146
|
+
const scrollDistance = parseFloat(itemHeight.value) * index
|
|
147
|
+
scrollToPosition(scrollDistance)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
// 响应式数据
|
|
153
|
+
scrollTop,
|
|
154
|
+
showBackTopBtn,
|
|
155
|
+
virtualData,
|
|
156
|
+
startIndex,
|
|
157
|
+
virtualOffsetY,
|
|
158
|
+
totalHeight,
|
|
159
|
+
displayData,
|
|
160
|
+
|
|
161
|
+
// 方法
|
|
162
|
+
initScrollData,
|
|
163
|
+
initScrollEngine,
|
|
164
|
+
updateVisibleData,
|
|
165
|
+
scrollToTop,
|
|
166
|
+
scrollToBottom,
|
|
167
|
+
scrollToPosition,
|
|
168
|
+
scrollToElement,
|
|
169
|
+
scrollToElementById,
|
|
170
|
+
onScroll
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
width: 80px;
|
|
15
15
|
height: 80px;
|
|
16
16
|
overflow: hidden;
|
|
17
|
+
@include when(round) {
|
|
18
|
+
border-top-right-radius: $-corner-radius;
|
|
19
|
+
}
|
|
17
20
|
@include when(primary) {
|
|
18
21
|
.oxy-corner__text {
|
|
19
22
|
@include corner-type-style($-corner-color, $-corner-primary-bg);
|
|
@@ -23,6 +26,11 @@
|
|
|
23
26
|
@include corner-type-style($-corner-primary-color, $-corner-horizontal-primary-bg);
|
|
24
27
|
}
|
|
25
28
|
}
|
|
29
|
+
&.is-embedded {
|
|
30
|
+
.oxy-corner__text {
|
|
31
|
+
@include corner-type-style($-corner-primary-color, $-corner-embedded-primary-bg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
26
34
|
}
|
|
27
35
|
@include when(info) {
|
|
28
36
|
.oxy-corner__text {
|
|
@@ -33,6 +41,11 @@
|
|
|
33
41
|
@include corner-type-style($-corner-info-color, $-corner-horizontal-info-bg);
|
|
34
42
|
}
|
|
35
43
|
}
|
|
44
|
+
&.is-embedded {
|
|
45
|
+
.oxy-corner__text {
|
|
46
|
+
@include corner-type-style($-corner-info-color, $-corner-embedded-info-bg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
36
49
|
}
|
|
37
50
|
@include when(success) {
|
|
38
51
|
.oxy-corner__text {
|
|
@@ -43,6 +56,11 @@
|
|
|
43
56
|
@include corner-type-style($-corner-success-color, $-corner-horizontal-success-bg);
|
|
44
57
|
}
|
|
45
58
|
}
|
|
59
|
+
&.is-embedded {
|
|
60
|
+
.oxy-corner__text {
|
|
61
|
+
@include corner-type-style($-corner-success-color, $-corner-embedded-success-bg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
46
64
|
}
|
|
47
65
|
@include when(warning) {
|
|
48
66
|
.oxy-corner__text {
|
|
@@ -53,6 +71,11 @@
|
|
|
53
71
|
@include corner-type-style($-corner-warning-color, $-corner-horizontal-warning-bg);
|
|
54
72
|
}
|
|
55
73
|
}
|
|
74
|
+
&.is-embedded {
|
|
75
|
+
.oxy-corner__text {
|
|
76
|
+
@include corner-type-style($-corner-warning-color, $-corner-embedded-warning-bg);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
56
79
|
}
|
|
57
80
|
@include when(danger) {
|
|
58
81
|
.oxy-corner__text {
|
|
@@ -63,12 +86,21 @@
|
|
|
63
86
|
@include corner-type-style($-corner-danger-color, $-corner-horizontal-danger-bg);
|
|
64
87
|
}
|
|
65
88
|
}
|
|
89
|
+
&.is-embedded {
|
|
90
|
+
.oxy-corner__text {
|
|
91
|
+
@include corner-type-style($-corner-danger-color, $-corner-embedded-danger-bg);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
66
94
|
}
|
|
67
95
|
&:not(.is-horizontal) {
|
|
68
96
|
&.is-top-left{
|
|
69
97
|
top: 0;
|
|
70
98
|
left: 0;
|
|
71
99
|
right: auto;
|
|
100
|
+
&.is-round {
|
|
101
|
+
border-top-right-radius: 0;
|
|
102
|
+
border-top-left-radius: $-corner-radius;
|
|
103
|
+
}
|
|
72
104
|
.oxy-corner__text {
|
|
73
105
|
transform: rotate(-45deg);
|
|
74
106
|
top: 10px;
|
|
@@ -79,6 +111,10 @@
|
|
|
79
111
|
bottom: 0;
|
|
80
112
|
left: 0;
|
|
81
113
|
top: auto;
|
|
114
|
+
&.is-round {
|
|
115
|
+
border-top-right-radius: 0;
|
|
116
|
+
border-bottom-left-radius: $-corner-radius;
|
|
117
|
+
}
|
|
82
118
|
.oxy-corner__text {
|
|
83
119
|
transform: rotate(-135deg);
|
|
84
120
|
bottom: 10px;
|
|
@@ -94,6 +130,10 @@
|
|
|
94
130
|
bottom: 0;
|
|
95
131
|
right: 0;
|
|
96
132
|
top: auto;
|
|
133
|
+
&.is-round {
|
|
134
|
+
border-top-right-radius: 0;
|
|
135
|
+
border-bottom-right-radius: $-corner-radius;
|
|
136
|
+
}
|
|
97
137
|
.oxy-corner__text {
|
|
98
138
|
transform: rotate(135deg);
|
|
99
139
|
bottom: 10px;
|
|
@@ -105,6 +145,62 @@
|
|
|
105
145
|
}
|
|
106
146
|
}
|
|
107
147
|
}
|
|
148
|
+
&.is-embedded {
|
|
149
|
+
&.is-top-left{
|
|
150
|
+
&::after {
|
|
151
|
+
left: auto;
|
|
152
|
+
right: -6px;
|
|
153
|
+
bottom: 0;
|
|
154
|
+
transform: skewX(-30deg);
|
|
155
|
+
}
|
|
156
|
+
.oxy-corner__text {
|
|
157
|
+
transform: none;
|
|
158
|
+
top: 0;
|
|
159
|
+
left: 0;
|
|
160
|
+
border-bottom-left-radius: 0;
|
|
161
|
+
border-bottom-right-radius: 32px;
|
|
162
|
+
padding-left: 0;
|
|
163
|
+
padding-right: 12px;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
&.is-bottom-left{
|
|
167
|
+
&::after {
|
|
168
|
+
left: auto;
|
|
169
|
+
right: -6px;
|
|
170
|
+
bottom: 0;
|
|
171
|
+
transform: skewX(30deg);
|
|
172
|
+
}
|
|
173
|
+
.oxy-corner__text {
|
|
174
|
+
transform: none;
|
|
175
|
+
bottom: 0;
|
|
176
|
+
left: 0;
|
|
177
|
+
border-bottom-left-radius: 0;
|
|
178
|
+
border-top-right-radius: 32px;
|
|
179
|
+
padding-left: 0;
|
|
180
|
+
padding-right: 12px;
|
|
181
|
+
> span{
|
|
182
|
+
transform: none;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
&.is-bottom-right{
|
|
187
|
+
&::after {
|
|
188
|
+
left: -6px;
|
|
189
|
+
bottom: 0;
|
|
190
|
+
transform: skewX(-30deg);
|
|
191
|
+
}
|
|
192
|
+
.oxy-corner__text {
|
|
193
|
+
transform: none;
|
|
194
|
+
bottom: 0;
|
|
195
|
+
right: 0;
|
|
196
|
+
border-bottom-left-radius: 0;
|
|
197
|
+
border-top-left-radius: 32px;
|
|
198
|
+
> span{
|
|
199
|
+
transform: none;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
108
204
|
.oxy-corner__text {
|
|
109
205
|
width: 106px;
|
|
110
206
|
height: 32px;
|
|
@@ -134,5 +230,29 @@
|
|
|
134
230
|
text-align: right;
|
|
135
231
|
}
|
|
136
232
|
}
|
|
233
|
+
&.is-embedded {
|
|
234
|
+
width: 106px;
|
|
235
|
+
height: 32px;
|
|
236
|
+
&::after {
|
|
237
|
+
content: '';
|
|
238
|
+
position: absolute;
|
|
239
|
+
left: -6px;
|
|
240
|
+
bottom: 0;
|
|
241
|
+
width: 16px;
|
|
242
|
+
height: 32px;
|
|
243
|
+
background-color: #ffffff;
|
|
244
|
+
transform: skewX(30deg);
|
|
245
|
+
}
|
|
246
|
+
.oxy-corner__text {
|
|
247
|
+
right: 0;
|
|
248
|
+
top: 0;
|
|
249
|
+
transform: none;
|
|
250
|
+
padding-left: 12px;
|
|
251
|
+
box-sizing: border-box;
|
|
252
|
+
font-size: $-corner-embedded-font-size;
|
|
253
|
+
font-weight: $-corner-embedded-font-weight;
|
|
254
|
+
border-bottom-left-radius: 32px;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
137
257
|
}
|
|
138
|
-
}
|
|
258
|
+
}
|
|
@@ -30,7 +30,7 @@ const props = defineProps(cornerProps)
|
|
|
30
30
|
const cornerClass = ref<string>('')
|
|
31
31
|
|
|
32
32
|
watch(
|
|
33
|
-
[() => () => props.position, () => props.mode],
|
|
33
|
+
[() => () => props.position, () => props.mode, () => props.round],
|
|
34
34
|
() => {
|
|
35
35
|
computeCornerClass()
|
|
36
36
|
},
|
|
@@ -53,11 +53,12 @@ const textClass = computed(() => {
|
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
function computeCornerClass() {
|
|
56
|
-
const { type, position, mode } = props
|
|
56
|
+
const { type, position, mode, round } = props
|
|
57
57
|
let cornerClassList: string[] = []
|
|
58
58
|
type && cornerClassList.push(`is-${type}`)
|
|
59
59
|
position && cornerClassList.push(`is-${position}`)
|
|
60
60
|
mode && cornerClassList.push(`is-${mode}`)
|
|
61
|
+
round && cornerClassList.push('is-round')
|
|
61
62
|
cornerClass.value = cornerClassList.join(' ')
|
|
62
63
|
}
|
|
63
64
|
</script>
|
|
@@ -3,7 +3,7 @@ import { baseProps, makeBooleanProp, makeStringProp } from '../common/props'
|
|
|
3
3
|
|
|
4
4
|
export type CornerType = 'primary' | 'info' | 'success' | 'warning' | 'danger'
|
|
5
5
|
export type CornerPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
6
|
-
export type CornerMode = '' | 'horizontal'
|
|
6
|
+
export type CornerMode = '' | 'horizontal' | 'embedded'
|
|
7
7
|
|
|
8
8
|
export const cornerProps = {
|
|
9
9
|
...baseProps,
|
|
@@ -24,10 +24,17 @@ export const cornerProps = {
|
|
|
24
24
|
*/
|
|
25
25
|
position: makeStringProp<CornerPosition>('top-right'),
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 圆角类型
|
|
29
|
+
* 类型:boolean
|
|
30
|
+
* 默认值:false
|
|
31
|
+
*/
|
|
32
|
+
round: makeBooleanProp(false),
|
|
33
|
+
|
|
27
34
|
/**
|
|
28
35
|
* 角标模式
|
|
29
36
|
* 类型:string
|
|
30
|
-
* 可选值:'horizontal'
|
|
37
|
+
* 可选值:'horizontal' / 'embedded'
|
|
31
38
|
* 默认值:''
|
|
32
39
|
*/
|
|
33
40
|
mode: makeStringProp<CornerMode>(''),
|
|
@@ -1,38 +1,61 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<view :class="['oxy-list', customClass]" :style="customStyle">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
:
|
|
7
|
-
|
|
8
|
-
:
|
|
9
|
-
:
|
|
10
|
-
:
|
|
11
|
-
:
|
|
2
|
+
<view :class="['oxy-list', 'oxy-virtual-scroll', customClass]" :style="customStyle">
|
|
3
|
+
<!-- 滚动内容区域 -->
|
|
4
|
+
<scroll-view
|
|
5
|
+
v-if="data.length"
|
|
6
|
+
:style="{ height: height }"
|
|
7
|
+
class="oxy-virtual-scroll__view"
|
|
8
|
+
:scroll-y="true"
|
|
9
|
+
:scroll-x="scrollX"
|
|
10
|
+
:upper-threshold="upperThreshold"
|
|
11
|
+
:lower-threshold="lowerThreshold"
|
|
12
|
+
:scroll-top="scrollTop"
|
|
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"
|
|
12
21
|
:refresher-triggered="triggered"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@scroll
|
|
16
|
-
@
|
|
22
|
+
:enable-flex="enableFlex"
|
|
23
|
+
:scroll-anchoring="scrollAnchoring"
|
|
24
|
+
@scroll="handleScroll"
|
|
25
|
+
@scrolltoupper="onScrollToUpper"
|
|
26
|
+
@scrolltolower="onScrollToLower"
|
|
17
27
|
@refresherrefresh="onRefresh"
|
|
18
28
|
@refresherrestore="onRestore"
|
|
19
29
|
@refresherabort="onAbort"
|
|
20
30
|
>
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
<view class="oxy-virtual-scroll__container" :style="{ height: totalHeight + 'px' }">
|
|
32
|
+
<view class="oxy-virtual-scroll__items" :style="{ transform: `translateY(${virtualOffsetY}px)` }">
|
|
33
|
+
<view v-for="(item, index) in virtualData" :key="index">
|
|
34
|
+
<slot name="item" :item="item" :index="startIndex + index"></slot>
|
|
35
|
+
</view>
|
|
36
|
+
<slot name="bottom">
|
|
37
|
+
<oxy-loadmore
|
|
38
|
+
v-if="loadmoreState"
|
|
39
|
+
:state="loadmoreState"
|
|
40
|
+
:loading-text="loadingText"
|
|
41
|
+
:finished-text="finishedText"
|
|
42
|
+
:error-text="errorText"
|
|
43
|
+
:loading-props="loadingProps"
|
|
44
|
+
@reload="onReload"
|
|
45
|
+
/>
|
|
46
|
+
</slot>
|
|
47
|
+
</view>
|
|
48
|
+
</view>
|
|
49
|
+
</scroll-view>
|
|
50
|
+
|
|
51
|
+
<view v-else>
|
|
52
|
+
<oxy-status-tip image="content" :tip="emptyText" />
|
|
53
|
+
</view>
|
|
54
|
+
|
|
55
|
+
<!-- 回到顶部按钮 -->
|
|
56
|
+
<view v-if="showBackToTop && showBackTopBtn" class="oxy-virtual-scroll__back-top" @click="scrollToTop">
|
|
57
|
+
<oxy-icon name="backtop" color="#fff" size="20px"></oxy-icon>
|
|
58
|
+
</view>
|
|
36
59
|
</view>
|
|
37
60
|
</template>
|
|
38
61
|
|
|
@@ -48,7 +71,7 @@ export default {
|
|
|
48
71
|
</script>
|
|
49
72
|
|
|
50
73
|
<script lang="ts" setup>
|
|
51
|
-
import { computed,
|
|
74
|
+
import { computed, toRefs } from 'vue'
|
|
52
75
|
import { type ListExpose, listProps } from './types'
|
|
53
76
|
import type {
|
|
54
77
|
ScrollViewOnRefresherabortEvent,
|
|
@@ -56,18 +79,70 @@ import type {
|
|
|
56
79
|
ScrollViewOnRefresherrestoreEvent,
|
|
57
80
|
ScrollViewOnScrollEvent
|
|
58
81
|
} from '@uni-helper/uni-app-types/index'
|
|
59
|
-
import
|
|
82
|
+
import { useVirtualScroll } from '../composables/useVirtualScroll'
|
|
60
83
|
|
|
61
84
|
const props = defineProps(listProps)
|
|
62
85
|
|
|
63
86
|
const emit = defineEmits(['scroll-to-lower', 'reload', 'scroll-to-upper', 'scroll', 'refresh', 'restore', 'abort'])
|
|
64
|
-
const virtualScrollRef = ref<VirtualScrollInstance | null>(null)
|
|
65
87
|
|
|
66
88
|
const defaultscrollConfig = {
|
|
67
|
-
refresherEnabled:
|
|
89
|
+
refresherEnabled: false,
|
|
68
90
|
refresherThreshold: 60,
|
|
69
91
|
scrollWithAnimation: false
|
|
70
|
-
}
|
|
92
|
+
} as const
|
|
93
|
+
|
|
94
|
+
// 解构props用于组合式函数
|
|
95
|
+
const {
|
|
96
|
+
data,
|
|
97
|
+
virtual,
|
|
98
|
+
height,
|
|
99
|
+
itemHeight,
|
|
100
|
+
idKey,
|
|
101
|
+
showBackToTop,
|
|
102
|
+
backToTopThreshold,
|
|
103
|
+
scrollY,
|
|
104
|
+
scrollX,
|
|
105
|
+
upperThreshold,
|
|
106
|
+
lowerThreshold,
|
|
107
|
+
scrollLeft,
|
|
108
|
+
scrollWithAnimation,
|
|
109
|
+
enableBackToTop,
|
|
110
|
+
showScrollbar,
|
|
111
|
+
refresherEnabled,
|
|
112
|
+
refresherThreshold,
|
|
113
|
+
refresherDefaultStyle,
|
|
114
|
+
refresherBackground,
|
|
115
|
+
triggered,
|
|
116
|
+
enableFlex,
|
|
117
|
+
scrollAnchoring
|
|
118
|
+
} = toRefs(props)
|
|
119
|
+
|
|
120
|
+
// 使用虚拟滚动组合式函数
|
|
121
|
+
const {
|
|
122
|
+
scrollTop,
|
|
123
|
+
showBackTopBtn,
|
|
124
|
+
virtualData,
|
|
125
|
+
startIndex,
|
|
126
|
+
virtualOffsetY,
|
|
127
|
+
totalHeight,
|
|
128
|
+
displayData,
|
|
129
|
+
initScrollData,
|
|
130
|
+
initScrollEngine,
|
|
131
|
+
updateVisibleData,
|
|
132
|
+
scrollToTop: virtualScrollToTop,
|
|
133
|
+
scrollToBottom: virtualScrollToBottom,
|
|
134
|
+
scrollToPosition: virtualScrollToPosition,
|
|
135
|
+
scrollToElement: virtualScrollToElement,
|
|
136
|
+
scrollToElementById: virtualScrollToElementById,
|
|
137
|
+
onScroll: handleVirtualScroll
|
|
138
|
+
} = useVirtualScroll({
|
|
139
|
+
data,
|
|
140
|
+
virtual,
|
|
141
|
+
height,
|
|
142
|
+
itemHeight,
|
|
143
|
+
idKey,
|
|
144
|
+
backToTopThreshold
|
|
145
|
+
})
|
|
71
146
|
|
|
72
147
|
const onScrollToLower = async () => {
|
|
73
148
|
emit('scroll-to-lower')
|
|
@@ -78,7 +153,10 @@ const onScrollToUpper = async () => {
|
|
|
78
153
|
const onReload = async () => {
|
|
79
154
|
emit('reload')
|
|
80
155
|
}
|
|
81
|
-
|
|
156
|
+
|
|
157
|
+
// 滚动事件处理
|
|
158
|
+
const handleScroll = async (event: ScrollViewOnScrollEvent) => {
|
|
159
|
+
handleVirtualScroll(event.detail.scrollTop)
|
|
82
160
|
emit('scroll', event)
|
|
83
161
|
}
|
|
84
162
|
|
|
@@ -96,21 +174,24 @@ const onRestore = (e: ScrollViewOnRefresherrestoreEvent) => {
|
|
|
96
174
|
const onAbort = (e: ScrollViewOnRefresherabortEvent) => {
|
|
97
175
|
emit('abort', e)
|
|
98
176
|
}
|
|
177
|
+
|
|
178
|
+
// 滚动方法
|
|
99
179
|
const scrollToTop = () => {
|
|
100
|
-
|
|
180
|
+
virtualScrollToTop()
|
|
101
181
|
}
|
|
102
182
|
const scrollToBottom = () => {
|
|
103
|
-
|
|
183
|
+
virtualScrollToBottom()
|
|
104
184
|
}
|
|
105
185
|
const scrollToPosition = (position: number | string) => {
|
|
106
|
-
|
|
186
|
+
virtualScrollToPosition(position)
|
|
107
187
|
}
|
|
108
188
|
const scrollToElement = (item: any) => {
|
|
109
|
-
|
|
189
|
+
virtualScrollToElement(item)
|
|
110
190
|
}
|
|
111
191
|
const scrollToElementById = (id: string | number) => {
|
|
112
|
-
|
|
192
|
+
virtualScrollToElementById(id)
|
|
113
193
|
}
|
|
194
|
+
|
|
114
195
|
defineExpose<ListExpose>({
|
|
115
196
|
scrollToTop,
|
|
116
197
|
scrollToBottom,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
|
2
|
-
import { makeBooleanProp } from '../common/props'
|
|
2
|
+
import { makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
|
|
3
3
|
import { type LoadMoreState } from '../oxy-loadmore/types'
|
|
4
4
|
import { type LoadingProps } from '../oxy-loading/types'
|
|
5
5
|
import { virtualScrollProps } from '../oxy-virtual-scroll/types'
|
|
@@ -25,19 +25,7 @@ export const listProps = {
|
|
|
25
25
|
* 加载中loading组件的属性
|
|
26
26
|
* 参考loading组件
|
|
27
27
|
*/
|
|
28
|
-
loadingProps: Object as PropType<Partial<LoadingProps
|
|
29
|
-
triggered: makeBooleanProp(false),
|
|
30
|
-
scrollConfig: {
|
|
31
|
-
type: Object as PropType<ScrollConfig>,
|
|
32
|
-
default: () => ({})
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export type ScrollConfig = {
|
|
36
|
-
refresherEnabled: boolean
|
|
37
|
-
refresherThreshold: number
|
|
38
|
-
refresherBackground: string
|
|
39
|
-
animation: boolean
|
|
40
|
-
[key: string]: any
|
|
28
|
+
loadingProps: Object as PropType<Partial<LoadingProps>>
|
|
41
29
|
}
|
|
42
30
|
export type ListExpose = {
|
|
43
31
|
scrollToTop: () => void
|