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
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
@include e(evoke-file) {
|
|
50
|
+
border-radius: 8px;
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
@include e(evoke-num) {
|
|
50
54
|
font-size: 14px;
|
|
51
55
|
line-height: 14px;
|
|
@@ -172,4 +176,50 @@
|
|
|
172
176
|
width: 100%;
|
|
173
177
|
height: 100%;
|
|
174
178
|
}
|
|
179
|
+
|
|
180
|
+
@include e(preview-file) {
|
|
181
|
+
margin-bottom: 32px;
|
|
182
|
+
.oxy-upload__status-content:not(.oxy-upload__mask) {
|
|
183
|
+
border: 1px solid $-color-border;
|
|
184
|
+
box-sizing: border-box;
|
|
185
|
+
border-radius: 8px;
|
|
186
|
+
.oxy-upload__picture {
|
|
187
|
+
border-radius: 8px;
|
|
188
|
+
}
|
|
189
|
+
.oxy-upload__picture-icon {
|
|
190
|
+
width: 32px;
|
|
191
|
+
height: 32px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
.oxy-upload__mask {
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
}
|
|
197
|
+
:deep(.oxy-tooltip) {
|
|
198
|
+
width: 100%;
|
|
199
|
+
.oxy-tooltip__inner {
|
|
200
|
+
width: fit-content;
|
|
201
|
+
white-space: pre-wrap;
|
|
202
|
+
word-break: break-all;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
.oxy-upload____status-content-name {
|
|
206
|
+
display: block;
|
|
207
|
+
width: 100%;
|
|
208
|
+
font-size: $-upload-file-fs;
|
|
209
|
+
color: $-upload-file-color;
|
|
210
|
+
box-sizing: border-box;
|
|
211
|
+
padding: 0 4px;
|
|
212
|
+
text-align: center;
|
|
213
|
+
margin-top: 8px;
|
|
214
|
+
white-space: nowrap;
|
|
215
|
+
overflow: hidden;
|
|
216
|
+
text-overflow: ellipsis;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&.oxy-upload__position-right {
|
|
221
|
+
.oxy-upload__evoke {
|
|
222
|
+
margin-right: 12px;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
175
225
|
}
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<view :class="['oxy-upload', customClass]" :style="customStyle">
|
|
2
|
+
<view :class="['oxy-upload', { 'oxy-upload__position-right': props.listPosition === 'right' }, customClass]" :style="customStyle">
|
|
3
|
+
<block v-if="showUpload && props.listPosition === 'right'">
|
|
4
|
+
<view :class="['oxy-upload__evoke-slot', customEvokeClass]" v-if="$slots.default" @click="onEvokeClick">
|
|
5
|
+
<slot></slot>
|
|
6
|
+
</view>
|
|
7
|
+
<!-- 唤起项 -->
|
|
8
|
+
<view v-else @click="onEvokeClick" :class="['oxy-upload__evoke', disabled ? 'is-disabled' : '', customEvokeClass]">
|
|
9
|
+
<!-- 唤起项图标 -->
|
|
10
|
+
<oxy-icon
|
|
11
|
+
class="oxy-upload__evoke-icon"
|
|
12
|
+
:name="props.evokeIcon ? props.evokeIcon : props.listType === 'card' ? 'add' : 'fill-camera'"
|
|
13
|
+
></oxy-icon>
|
|
14
|
+
<!-- 有限制个数时确认是否展示限制个数 -->
|
|
15
|
+
<view v-if="limit && showLimitNum" class="oxy-upload__evoke-num">({{ uploadFiles.length }}/{{ limit }})</view>
|
|
16
|
+
</view>
|
|
17
|
+
</block>
|
|
18
|
+
|
|
3
19
|
<!-- 预览列表 -->
|
|
4
|
-
<view
|
|
20
|
+
<view
|
|
21
|
+
:class="['oxy-upload__preview', { 'oxy-upload__preview-file': props.listType === 'card' }, customPreviewClass]"
|
|
22
|
+
v-for="(file, index) in uploadFiles"
|
|
23
|
+
:key="index"
|
|
24
|
+
>
|
|
5
25
|
<!-- 成功时展示图片 -->
|
|
6
|
-
<view class="oxy-upload__status-content">
|
|
26
|
+
<view v-if="props.listType === 'card'" class="oxy-upload__status-content">
|
|
27
|
+
<image v-if="isImage(file)" :src="file.url" :mode="imageMode" class="oxy-upload__picture" @click="onPreviewImage(file)" />
|
|
28
|
+
<image v-else :src="getUploadImage(file)" :mode="imageMode" class="oxy-upload__picture-icon" @click="onPreview(file)" />
|
|
29
|
+
</view>
|
|
30
|
+
<view v-else class="oxy-upload__status-content">
|
|
7
31
|
<image v-if="isImage(file)" :src="file.url" :mode="imageMode" class="oxy-upload__picture" @click="onPreviewImage(file)" />
|
|
8
32
|
<template v-else-if="isVideo(file)">
|
|
9
33
|
<view class="oxy-upload__video" v-if="file.thumb" @click="onPreviewVideo(file)">
|
|
@@ -66,16 +90,26 @@
|
|
|
66
90
|
></oxy-icon>
|
|
67
91
|
<!-- 自定义预览样式 -->
|
|
68
92
|
<slot name="preview-cover" v-if="$slots['preview-cover']" :file="file" :index="index"></slot>
|
|
93
|
+
<oxy-tooltip v-if="props.listType === 'card'" placement="top" :content="file.name">
|
|
94
|
+
<text class="oxy-upload____status-content-name">{{ file.name }}</text>
|
|
95
|
+
</oxy-tooltip>
|
|
69
96
|
</view>
|
|
70
97
|
|
|
71
|
-
<block v-if="showUpload">
|
|
98
|
+
<block v-if="showUpload && props.listPosition === 'left'">
|
|
72
99
|
<view :class="['oxy-upload__evoke-slot', customEvokeClass]" v-if="$slots.default" @click="onEvokeClick">
|
|
73
100
|
<slot></slot>
|
|
74
101
|
</view>
|
|
75
102
|
<!-- 唤起项 -->
|
|
76
|
-
<view
|
|
103
|
+
<view
|
|
104
|
+
v-else
|
|
105
|
+
@click="onEvokeClick"
|
|
106
|
+
:class="['oxy-upload__evoke', { 'oxy-upload__evoke-file': props.listType === 'card' }, disabled ? 'is-disabled' : '', customEvokeClass]"
|
|
107
|
+
>
|
|
77
108
|
<!-- 唤起项图标 -->
|
|
78
|
-
<oxy-icon
|
|
109
|
+
<oxy-icon
|
|
110
|
+
class="oxy-upload__evoke-icon"
|
|
111
|
+
:name="props.evokeIcon ? props.evokeIcon : props.listType === 'card' ? 'add' : 'fill-camera'"
|
|
112
|
+
></oxy-icon>
|
|
79
113
|
<!-- 有限制个数时确认是否展示限制个数 -->
|
|
80
114
|
<view v-if="limit && showLimitNum" class="oxy-upload__evoke-num">({{ uploadFiles.length }}/{{ limit }})</view>
|
|
81
115
|
</view>
|
|
@@ -96,12 +130,19 @@ export default {
|
|
|
96
130
|
</script>
|
|
97
131
|
|
|
98
132
|
<script lang="ts" setup>
|
|
133
|
+
import ImgPdf from './images/pdf.png'
|
|
134
|
+
import ImgWord from './images/word.png'
|
|
135
|
+
import ImgAudio from './images/audio.png'
|
|
136
|
+
import ImgVideo from './images/video.png'
|
|
137
|
+
import ImgPic from './images/pic.png'
|
|
138
|
+
import ImgOther from './images/other.png'
|
|
99
139
|
import OxyIcon from '../oxy-icon/oxy-icon.vue'
|
|
100
140
|
import OxyVideoPreview from '../oxy-video-preview/oxy-video-preview.vue'
|
|
101
141
|
import OxyLoading from '../oxy-loading/oxy-loading.vue'
|
|
142
|
+
import OxyTooltip from '../oxy-tooltip/oxy-tooltip.vue'
|
|
102
143
|
|
|
103
144
|
import { computed, ref, watch } from 'vue'
|
|
104
|
-
import { context, isEqual, isImageUrl, isVideoUrl, isFunction, isDef, deepClone } from '../common/util'
|
|
145
|
+
import { context, isEqual, isImageUrl, isVideoUrl, isAudioUrl, isPdfUrl, isDocUrl, isFunction, isDef, deepClone } from '../common/util'
|
|
105
146
|
import { useTranslate } from '../composables/useTranslate'
|
|
106
147
|
import { useUpload } from '../composables/useUpload'
|
|
107
148
|
import {
|
|
@@ -119,6 +160,15 @@ import {
|
|
|
119
160
|
} from './types'
|
|
120
161
|
import type { VideoPreviewInstance } from '../oxy-video-preview/types'
|
|
121
162
|
|
|
163
|
+
const imgs: AnyObject = {
|
|
164
|
+
pdf: ImgPdf,
|
|
165
|
+
word: ImgWord,
|
|
166
|
+
audio: ImgAudio,
|
|
167
|
+
video: ImgVideo,
|
|
168
|
+
pic: ImgPic,
|
|
169
|
+
other: ImgOther
|
|
170
|
+
}
|
|
171
|
+
|
|
122
172
|
const props = defineProps(uploadProps)
|
|
123
173
|
|
|
124
174
|
const emit = defineEmits<{
|
|
@@ -660,6 +710,14 @@ function onPreviewFile(file: UploadFileItem) {
|
|
|
660
710
|
}
|
|
661
711
|
}
|
|
662
712
|
|
|
713
|
+
function onPreview(file: UploadFileItem) {
|
|
714
|
+
if (isVideo(file)) {
|
|
715
|
+
onPreviewVideo(file)
|
|
716
|
+
} else {
|
|
717
|
+
onPreviewFile(file)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
663
721
|
function isVideo(file: UploadFileItem) {
|
|
664
722
|
return (file.name && isVideoUrl(file.name)) || isVideoUrl(file.url)
|
|
665
723
|
}
|
|
@@ -667,6 +725,34 @@ function isVideo(file: UploadFileItem) {
|
|
|
667
725
|
function isImage(file: UploadFileItem) {
|
|
668
726
|
return (file.name && isImageUrl(file.name)) || isImageUrl(file.url)
|
|
669
727
|
}
|
|
728
|
+
|
|
729
|
+
function isAudio(file: UploadFileItem) {
|
|
730
|
+
return (file.name && isAudioUrl(file.name)) || isAudioUrl(file.url)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function isPdf(file: UploadFileItem) {
|
|
734
|
+
return (file.name && isPdfUrl(file.name)) || isPdfUrl(file.url)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function isDoc(file: UploadFileItem) {
|
|
738
|
+
return (file.name && isDocUrl(file.name)) || isDocUrl(file.url)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function getUploadImage(file: UploadFileItem) {
|
|
742
|
+
if (isPdf(file)) {
|
|
743
|
+
return imgs.pdf
|
|
744
|
+
} else if (isDoc(file)) {
|
|
745
|
+
return imgs.word
|
|
746
|
+
} else if (isAudio(file)) {
|
|
747
|
+
return imgs.audio
|
|
748
|
+
} else if (isVideo(file)) {
|
|
749
|
+
return imgs.video
|
|
750
|
+
} else if (isImage(file)) {
|
|
751
|
+
return imgs.pic
|
|
752
|
+
} else {
|
|
753
|
+
return imgs.other
|
|
754
|
+
}
|
|
755
|
+
}
|
|
670
756
|
</script>
|
|
671
757
|
<style lang="scss" scoped>
|
|
672
758
|
@import './index.scss';
|
|
@@ -68,6 +68,8 @@ export type UploadSizeType = 'original' | 'compressed'
|
|
|
68
68
|
export type UploadFileType = 'image' | 'video' | 'media' | 'all' | 'file'
|
|
69
69
|
export type UploadCameraType = 'front' | 'back'
|
|
70
70
|
export type UploadStatusType = 'pending' | 'loading' | 'success' | 'fail'
|
|
71
|
+
export type UploadListType = 'default' | 'card'
|
|
72
|
+
export type UploadListPosition = 'left' | 'right'
|
|
71
73
|
|
|
72
74
|
export type UploadBeforePreviewOption = {
|
|
73
75
|
file: UploadFileItem
|
|
@@ -342,7 +344,26 @@ export const uploadProps = {
|
|
|
342
344
|
* H5支持全部类型过滤。
|
|
343
345
|
* 微信小程序支持all和file时过滤,其余平台不支持。
|
|
344
346
|
*/
|
|
345
|
-
extension: Array as PropType<string[]
|
|
347
|
+
extension: Array as PropType<string[]>,
|
|
348
|
+
/**
|
|
349
|
+
* 唤起图标
|
|
350
|
+
* 类型:string
|
|
351
|
+
* 可选值:'fill-camera' / 'add'
|
|
352
|
+
* 默认值:fill-camera
|
|
353
|
+
*/
|
|
354
|
+
evokeIcon: makeStringProp(''),
|
|
355
|
+
/**
|
|
356
|
+
* 文件列表的类型
|
|
357
|
+
* 类型:string
|
|
358
|
+
*/
|
|
359
|
+
listType: makeStringProp<UploadListType>('default'),
|
|
360
|
+
/**
|
|
361
|
+
* 文件列表的位置
|
|
362
|
+
* 类型:string
|
|
363
|
+
* 可选值:'left' / 'right'
|
|
364
|
+
* 默认值:left
|
|
365
|
+
*/
|
|
366
|
+
listPosition: makeStringProp<UploadListPosition>('left')
|
|
346
367
|
}
|
|
347
368
|
|
|
348
369
|
export type UploadProps = ExtractPropTypes<typeof uploadProps>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
@import './../common/abstracts/_mixin.scss';
|
|
2
|
+
@import './../common/abstracts/variable.scss';
|
|
3
|
+
@include b(virtual-scroll) {
|
|
4
|
+
position: relative;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
|
|
8
|
+
@include e(view) {
|
|
9
|
+
width: 100%;
|
|
10
|
+
}
|
|
11
|
+
@include e(container) {
|
|
12
|
+
position: relative;
|
|
13
|
+
.oxy-virtual-scroll__items {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@include e(back-top) {
|
|
22
|
+
position: absolute;
|
|
23
|
+
right: 20px;
|
|
24
|
+
bottom: 20px;
|
|
25
|
+
width: 40px;
|
|
26
|
+
height: 40px;
|
|
27
|
+
background: rgba(0, 0, 0, 0.6);
|
|
28
|
+
border-radius: 50%;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
z-index: 100;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view :class="['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="scrollY"
|
|
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"
|
|
21
|
+
:refresher-triggered="triggered"
|
|
22
|
+
:enable-flex="enableFlex"
|
|
23
|
+
:scroll-anchoring="scrollAnchoring"
|
|
24
|
+
@scroll="onScroll"
|
|
25
|
+
@scrolltoupper="onScrollUpper"
|
|
26
|
+
@scrolltolower="onScrollLower"
|
|
27
|
+
>
|
|
28
|
+
<view class="oxy-virtual-scroll__container" :style="{ height: totalHeight + 'px' }">
|
|
29
|
+
<view class="oxy-virtual-scroll__items" :style="{ transform: `translateY(${virtualOffsetY}px)` }">
|
|
30
|
+
<view v-for="(item, index) in virtualData" :key="index">
|
|
31
|
+
<slot name="item" :item="item" :index="startIndex + index"></slot>
|
|
32
|
+
</view>
|
|
33
|
+
<slot name="bottom"></slot>
|
|
34
|
+
</view>
|
|
35
|
+
</view>
|
|
36
|
+
</scroll-view>
|
|
37
|
+
|
|
38
|
+
<view v-else>
|
|
39
|
+
<oxy-status-tip image="content" :tip="emptyText" />
|
|
40
|
+
</view>
|
|
41
|
+
|
|
42
|
+
<!-- 回到顶部按钮 -->
|
|
43
|
+
<view v-if="showBackToTop && showBackTopBtn" class="oxy-virtual-scroll__back-top" @click="scrollToTop">
|
|
44
|
+
<oxy-icon name="backtop" color="#fff" size="20px"></oxy-icon>
|
|
45
|
+
</view>
|
|
46
|
+
</view>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script lang="ts">
|
|
50
|
+
export default {
|
|
51
|
+
name: 'oxy-virtual-scroll',
|
|
52
|
+
options: {
|
|
53
|
+
addGlobalClass: true,
|
|
54
|
+
virtualHost: true,
|
|
55
|
+
styleIsolation: 'shared'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<script lang="ts" setup>
|
|
61
|
+
import { toRefs } from 'vue'
|
|
62
|
+
import type { ScrollViewOnScrollEvent, ScrollViewOnScrolltolowerEvent, ScrollViewOnScrolltoupperEvent } from '@uni-helper/uni-app-types'
|
|
63
|
+
import { virtualScrollProps, type VirtualScrollExpose } from './types'
|
|
64
|
+
import { useVirtualScroll } from '../composables/useVirtualScroll'
|
|
65
|
+
|
|
66
|
+
const props = defineProps(virtualScrollProps)
|
|
67
|
+
const emit = defineEmits(['scroll', 'scroll-to-upper', 'scroll-to-lower'])
|
|
68
|
+
|
|
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
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// 滚动事件
|
|
100
|
+
function onScroll(event: ScrollViewOnScrollEvent) {
|
|
101
|
+
handleVirtualScroll(event.detail.scrollTop)
|
|
102
|
+
emit('scroll', event)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 滚动到顶部
|
|
106
|
+
function onScrollUpper(event: ScrollViewOnScrolltoupperEvent) {
|
|
107
|
+
emit('scroll-to-upper', event)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 滚动到底部
|
|
111
|
+
function onScrollLower(event: ScrollViewOnScrolltolowerEvent) {
|
|
112
|
+
emit('scroll-to-lower', event)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 滚动方法
|
|
116
|
+
const scrollToTop = () => {
|
|
117
|
+
virtualScrollToTop()
|
|
118
|
+
}
|
|
119
|
+
const scrollToBottom = () => {
|
|
120
|
+
virtualScrollToBottom()
|
|
121
|
+
}
|
|
122
|
+
const scrollToPosition = (position: number | string) => {
|
|
123
|
+
virtualScrollToPosition(position)
|
|
124
|
+
}
|
|
125
|
+
const scrollToElement = (item: any) => {
|
|
126
|
+
virtualScrollToElement(item)
|
|
127
|
+
}
|
|
128
|
+
const scrollToElementById = (id: string | number) => {
|
|
129
|
+
virtualScrollToElementById(id)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
defineExpose<VirtualScrollExpose>({
|
|
133
|
+
scrollToTop,
|
|
134
|
+
scrollToBottom,
|
|
135
|
+
scrollToPosition,
|
|
136
|
+
scrollToElement,
|
|
137
|
+
scrollToElementById
|
|
138
|
+
})
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<style lang="scss" scoped>
|
|
142
|
+
@import './index.scss';
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
|
2
|
+
import { baseProps, makeRequiredProp, makeBooleanProp, makeStringProp, makeNumericProp, makeNumberProp } from '../common/props'
|
|
3
|
+
|
|
4
|
+
export const virtualScrollProps = {
|
|
5
|
+
...baseProps,
|
|
6
|
+
/**
|
|
7
|
+
* 虚拟列表数据源
|
|
8
|
+
* 类型:array
|
|
9
|
+
* 默认值:[]
|
|
10
|
+
*/
|
|
11
|
+
data: makeRequiredProp(Array as PropType<any[]>),
|
|
12
|
+
/**
|
|
13
|
+
* id的取值字段
|
|
14
|
+
* 类型:string
|
|
15
|
+
* 默认值:'id'
|
|
16
|
+
*/
|
|
17
|
+
idKey: makeStringProp('id'),
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 容器高度
|
|
21
|
+
* 类型:string
|
|
22
|
+
* 默认值:'100%'
|
|
23
|
+
*/
|
|
24
|
+
height: makeStringProp('100%'),
|
|
25
|
+
/**
|
|
26
|
+
* 是否显示回到顶部按钮
|
|
27
|
+
* 类型:boolean
|
|
28
|
+
* 默认值:false
|
|
29
|
+
*/
|
|
30
|
+
showBackToTop: makeBooleanProp(false),
|
|
31
|
+
/**
|
|
32
|
+
* 滚动多远显示backToTop
|
|
33
|
+
* 类型:number
|
|
34
|
+
* 默认值:'300px'
|
|
35
|
+
*/
|
|
36
|
+
backToTopThreshold: makeStringProp('300px'),
|
|
37
|
+
/**
|
|
38
|
+
* 单个项目高度
|
|
39
|
+
* 类型:number
|
|
40
|
+
* 默认值:'44px'
|
|
41
|
+
*/
|
|
42
|
+
itemHeight: makeStringProp('44px'),
|
|
43
|
+
/**
|
|
44
|
+
* 是否开启水平滚动
|
|
45
|
+
* 类型:boolean
|
|
46
|
+
* 默认值:false
|
|
47
|
+
*/
|
|
48
|
+
scrollX: makeBooleanProp(false),
|
|
49
|
+
/**
|
|
50
|
+
* 是否开启纵向滚动
|
|
51
|
+
* 类型:boolean
|
|
52
|
+
* 默认值:true
|
|
53
|
+
*/
|
|
54
|
+
scrollY: makeBooleanProp(true),
|
|
55
|
+
/**
|
|
56
|
+
* 距顶部/左边多远时(单位px),触发 scrolltoupper 事件
|
|
57
|
+
* 类型:number | string
|
|
58
|
+
* 默认值:50
|
|
59
|
+
*/
|
|
60
|
+
upperThreshold: makeNumericProp(50),
|
|
61
|
+
/**
|
|
62
|
+
* 距底部/右边多远时(单位px),触发 scrolltolower 事件
|
|
63
|
+
* 类型:number | string
|
|
64
|
+
* 默认值:50
|
|
65
|
+
*/
|
|
66
|
+
lowerThreshold: makeNumericProp(50),
|
|
67
|
+
/**
|
|
68
|
+
* 设置横向滚动条位置
|
|
69
|
+
* 类型:number | string
|
|
70
|
+
* 默认值:0
|
|
71
|
+
*/
|
|
72
|
+
scrollLeft: makeNumericProp(0),
|
|
73
|
+
/**
|
|
74
|
+
* 是否在设置滚动条位置时使用滚动动画
|
|
75
|
+
* 类型:boolean
|
|
76
|
+
* 默认值:false
|
|
77
|
+
*/
|
|
78
|
+
scrollWithAnimation: makeBooleanProp(false),
|
|
79
|
+
/**
|
|
80
|
+
* iOS/Android 返回顶部功能,竖向有效
|
|
81
|
+
* 类型:boolean
|
|
82
|
+
* 默认值:false
|
|
83
|
+
*/
|
|
84
|
+
enableBackToTop: makeBooleanProp(false),
|
|
85
|
+
/**
|
|
86
|
+
* 控制是否出现滚动条(App-nvue 有效)
|
|
87
|
+
* 类型:boolean
|
|
88
|
+
* 默认值:true
|
|
89
|
+
*/
|
|
90
|
+
showScrollbar: makeBooleanProp(true),
|
|
91
|
+
/**
|
|
92
|
+
* 开启自定义下拉刷新
|
|
93
|
+
* 类型:boolean
|
|
94
|
+
* 默认值:false
|
|
95
|
+
*/
|
|
96
|
+
refresherEnabled: makeBooleanProp(false),
|
|
97
|
+
/**
|
|
98
|
+
* 设置自定义下拉刷新阈值
|
|
99
|
+
* 类型:number
|
|
100
|
+
* 默认值:45
|
|
101
|
+
*/
|
|
102
|
+
refresherThreshold: makeNumberProp(45),
|
|
103
|
+
/**
|
|
104
|
+
* 设置自定义下拉刷新默认样式
|
|
105
|
+
* 类型:'black' | 'white' | 'none'
|
|
106
|
+
* 默认值:'black'
|
|
107
|
+
*/
|
|
108
|
+
refresherDefaultStyle: makeStringProp<'black' | 'white' | 'none'>('black'),
|
|
109
|
+
/**
|
|
110
|
+
* 设置自定义下拉刷新区域背景颜色
|
|
111
|
+
* 类型:string
|
|
112
|
+
* 默认值:'#FFF'
|
|
113
|
+
*/
|
|
114
|
+
refresherBackground: makeStringProp('#FFF'),
|
|
115
|
+
/**
|
|
116
|
+
* 设置当前下拉刷新状态
|
|
117
|
+
* 类型:boolean
|
|
118
|
+
* 默认值:false
|
|
119
|
+
*/
|
|
120
|
+
triggered: makeBooleanProp(false),
|
|
121
|
+
/**
|
|
122
|
+
* 启用 flexbox 布局
|
|
123
|
+
* 类型:boolean
|
|
124
|
+
* 默认值:false
|
|
125
|
+
*/
|
|
126
|
+
enableFlex: makeBooleanProp(false),
|
|
127
|
+
/**
|
|
128
|
+
* 开启 scroll anchoring 特性
|
|
129
|
+
* 类型:boolean
|
|
130
|
+
* 默认值:false
|
|
131
|
+
*/
|
|
132
|
+
scrollAnchoring: makeBooleanProp(false),
|
|
133
|
+
/**
|
|
134
|
+
* 暂无数据时的提示问题
|
|
135
|
+
* 类型:string
|
|
136
|
+
* 默认值:'暂无数据'
|
|
137
|
+
*/
|
|
138
|
+
emptyText: makeStringProp('暂无数据'),
|
|
139
|
+
/**
|
|
140
|
+
* 是否开启虚拟滚动
|
|
141
|
+
* 类型:boolean
|
|
142
|
+
* 默认值:true
|
|
143
|
+
*/
|
|
144
|
+
virtual: makeBooleanProp(true)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type VirtualScrollExpose = {
|
|
148
|
+
scrollToTop: () => void
|
|
149
|
+
scrollToBottom: () => void
|
|
150
|
+
scrollToPosition: (position: number | string) => void
|
|
151
|
+
scrollToElement: (item: any) => void
|
|
152
|
+
scrollToElementById: (id: string | number) => void
|
|
153
|
+
}
|
|
154
|
+
export type VirtualScrollProps = ExtractPropTypes<typeof virtualScrollProps>
|
|
155
|
+
export type VirtualScrollInstance = ComponentPublicInstance<VirtualScrollProps, VirtualScrollExpose>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 虚拟滚动引擎 - 优化长列表性能
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface VirtualScrollItem {
|
|
6
|
+
key?: string
|
|
7
|
+
items?: VirtualScrollItem[]
|
|
8
|
+
[key: string]: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VirtualScrollOptions {
|
|
12
|
+
containerHeight?: number
|
|
13
|
+
itemHeight?: number
|
|
14
|
+
bufferSize?: number
|
|
15
|
+
data?: VirtualScrollItem[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface VisibleDataResult {
|
|
19
|
+
visibleData: VirtualScrollItem[]
|
|
20
|
+
startIndex: number
|
|
21
|
+
endIndex: number
|
|
22
|
+
offsetY: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class VirtualScrollEngine {
|
|
26
|
+
containerHeight: number
|
|
27
|
+
itemHeight: number
|
|
28
|
+
bufferSize: number
|
|
29
|
+
data: VirtualScrollItem[]
|
|
30
|
+
visibleData: VirtualScrollItem[]
|
|
31
|
+
startIndex: number
|
|
32
|
+
endIndex: number
|
|
33
|
+
scrollTop: number
|
|
34
|
+
cache: Map<string, VisibleDataResult>
|
|
35
|
+
|
|
36
|
+
constructor(options: VirtualScrollOptions = {}) {
|
|
37
|
+
this.containerHeight = options.containerHeight || 400
|
|
38
|
+
this.itemHeight = options.itemHeight || 50
|
|
39
|
+
this.bufferSize = options.bufferSize || 5
|
|
40
|
+
this.data = options.data || []
|
|
41
|
+
|
|
42
|
+
this.visibleData = []
|
|
43
|
+
this.startIndex = 0
|
|
44
|
+
this.endIndex = 0
|
|
45
|
+
this.scrollTop = 0
|
|
46
|
+
|
|
47
|
+
this.cache = new Map()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 更新可见区域数据
|
|
51
|
+
updateVisibleData(scrollTop: number): VisibleDataResult {
|
|
52
|
+
this.scrollTop = scrollTop
|
|
53
|
+
|
|
54
|
+
const cacheKey = `${scrollTop}_${this.data.length}`
|
|
55
|
+
if (this.cache.has(cacheKey)) {
|
|
56
|
+
return this.cache.get(cacheKey)!
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)
|
|
60
|
+
this.startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.bufferSize)
|
|
61
|
+
this.endIndex = Math.min(this.data.length - 1, this.startIndex + visibleCount + this.bufferSize * 2)
|
|
62
|
+
|
|
63
|
+
this.visibleData = this.data.slice(this.startIndex, this.endIndex + 1)
|
|
64
|
+
|
|
65
|
+
const result: VisibleDataResult = {
|
|
66
|
+
visibleData: this.visibleData,
|
|
67
|
+
startIndex: this.startIndex,
|
|
68
|
+
endIndex: this.endIndex,
|
|
69
|
+
offsetY: this.startIndex * this.itemHeight
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 缓存优化
|
|
73
|
+
if (this.cache.size > 50) {
|
|
74
|
+
const firstKey = this.cache.keys().next().value as string
|
|
75
|
+
this.cache.delete(firstKey)
|
|
76
|
+
}
|
|
77
|
+
this.cache.set(cacheKey, result)
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
}
|
package/global.d.ts
CHANGED
|
@@ -59,10 +59,12 @@ declare module 'vue' {
|
|
|
59
59
|
OxyTab: typeof import('./components/oxy-tab/oxy-tab.vue')['default']
|
|
60
60
|
OxyTabs: typeof import('./components/oxy-tabs/oxy-tabs.vue')['default']
|
|
61
61
|
OxyTag: typeof import('./components/oxy-tag/oxy-tag.vue')['default']
|
|
62
|
+
OxyCorner: typeof import('./components/oxy-corner/oxy-corner.vue')['default']
|
|
62
63
|
OxyToast: typeof import('./components/oxy-toast/oxy-toast.vue')['default']
|
|
63
64
|
OxyTooltip: typeof import('./components/oxy-tooltip/oxy-tooltip.vue')['default']
|
|
64
65
|
OxyTransition: typeof import('./components/oxy-transition/oxy-transition.vue')['default']
|
|
65
66
|
OxyUpload: typeof import('./components/oxy-upload/oxy-upload.vue')['default']
|
|
67
|
+
OxyFileList: typeof import('./components/oxy-file-list/oxy-file-list.vue')['default']
|
|
66
68
|
OxyNotify: typeof import('./components/oxy-notify/oxy-notify.vue')['default']
|
|
67
69
|
OxyWatermark: typeof import('./components/oxy-watermark/oxy-watermark.vue')['default']
|
|
68
70
|
OxyCircle: typeof import('./components/oxy-circle/oxy-circle.vue')['default']
|
|
@@ -75,6 +77,7 @@ declare module 'vue' {
|
|
|
75
77
|
OxyNavbarCapsule: typeof import('./components/oxy-navbar-capsule/oxy-navbar-capsule.vue')['default']
|
|
76
78
|
OxyTable: typeof import('./components/oxy-table/oxy-table.vue')['default']
|
|
77
79
|
OxyTableCol: typeof import('./components/oxy-table-col/oxy-table-col.vue')['default']
|
|
80
|
+
OxyVirtualScroll: typeof import('./components/oxy-virtual-scroll/oxy-virtual-scroll.vue')['default']
|
|
78
81
|
OxySidebar: typeof import('./components/oxy-sidebar/oxy-sidebar.vue')['default']
|
|
79
82
|
OxySidebarItem: typeof import('./components/oxy-sidebar-item/oxy-sidebar-item.vue')['default']
|
|
80
83
|
OxyFab: typeof import('./components/oxy-fab/oxy-fab.vue')['default']
|