oxy-uni-ui 1.0.1 → 1.1.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 +32 -4
- package/components/common/util.ts +44 -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 +138 -0
- package/components/oxy-corner/oxy-corner.vue +66 -0
- package/components/oxy-corner/types.ts +43 -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 +4 -0
- package/components/oxy-list/oxy-list.vue +125 -0
- package/components/oxy-list/types.ts +50 -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 +61 -0
- package/components/oxy-tree/index.ts +51 -0
- package/components/oxy-tree/oxy-tree.vue +289 -0
- package/components/oxy-tree/types.ts +48 -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 +184 -0
- package/components/oxy-virtual-scroll/types.ts +65 -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 +2 -1
- package/locale/lang/zh-CN.ts +2 -1
- package/package.json +1 -1
- package/tags.json +1 -1
- package/web-types.json +1 -1
|
@@ -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
|
+
|
|
7
|
+
@include e(view) {
|
|
8
|
+
width: 100%;
|
|
9
|
+
height: 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,184 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view :class="['oxy-virtual-scroll', customClass]" :style="{ height: height }">
|
|
3
|
+
<!-- 滚动内容区域 -->
|
|
4
|
+
<scroll-view
|
|
5
|
+
ref="scrollView"
|
|
6
|
+
class="oxy-virtual-scroll__view"
|
|
7
|
+
:scroll-y="true"
|
|
8
|
+
:scroll-x="scrollX"
|
|
9
|
+
:scroll-top="scrollTop"
|
|
10
|
+
:scroll-into-view="itemId"
|
|
11
|
+
v-bind="$attrs"
|
|
12
|
+
@scroll="onScroll"
|
|
13
|
+
@scrolltoupper="onScrollUpper"
|
|
14
|
+
@scrolltolower="onScrollLower"
|
|
15
|
+
>
|
|
16
|
+
<view class="oxy-virtual-scroll__container" :style="{ height: totalHeight + 'px' }">
|
|
17
|
+
<view class="oxy-virtual-scroll__items" :style="{ transform: `translateY(${virtualOffsetY}px)` }">
|
|
18
|
+
<slot name="item" v-for="(item, index) in virtualData" :item="item" :index="startIndex + index"></slot>
|
|
19
|
+
<slot name="bottom"></slot>
|
|
20
|
+
</view>
|
|
21
|
+
</view>
|
|
22
|
+
</scroll-view>
|
|
23
|
+
|
|
24
|
+
<!-- 回到顶部按钮 -->
|
|
25
|
+
<view v-if="showBackToTop && showBackTopBtn" class="oxy-virtual-scroll__back-top" @click="scrollToTop">
|
|
26
|
+
<oxy-icon name="backtop" color="#fff" size="20px"></oxy-icon>
|
|
27
|
+
</view>
|
|
28
|
+
</view>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script lang="ts">
|
|
32
|
+
export default {
|
|
33
|
+
name: 'oxy-virtual-scroll',
|
|
34
|
+
options: {
|
|
35
|
+
addGlobalClass: true,
|
|
36
|
+
virtualHost: true,
|
|
37
|
+
styleIsolation: 'shared'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<script lang="ts" setup>
|
|
43
|
+
import { computed, onMounted, ref, watch, nextTick } from 'vue'
|
|
44
|
+
import type { ScrollViewOnScrollEvent, ScrollViewOnScrolltolowerEvent, ScrollViewOnScrolltoupperEvent } from '@uni-helper/uni-app-types'
|
|
45
|
+
import { VirtualScrollEngine } from './virtual-scroll'
|
|
46
|
+
import { virtualScrollProps } from './types'
|
|
47
|
+
defineOptions({
|
|
48
|
+
inheritAttrs: false
|
|
49
|
+
})
|
|
50
|
+
const props = defineProps(virtualScrollProps)
|
|
51
|
+
const emit = defineEmits(['scroll', 'scroll-to-upper', 'scroll-to-lower'])
|
|
52
|
+
|
|
53
|
+
const scrollView = ref(null)
|
|
54
|
+
const itemId = ref<string>('')
|
|
55
|
+
const scrollTop = ref<number>(0)
|
|
56
|
+
const showBackTopBtn = ref<boolean>(false)
|
|
57
|
+
const virtualData = ref<any[]>([])
|
|
58
|
+
const startIndex = ref<number>(0)
|
|
59
|
+
const virtualOffsetY = ref<number>(0)
|
|
60
|
+
const virtualEngine = ref<any>(null)
|
|
61
|
+
|
|
62
|
+
// 显示的数据(索引处理后的)
|
|
63
|
+
const displayData = computed<any[]>(() => {
|
|
64
|
+
return props.data
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// 虚拟列表总高度
|
|
68
|
+
const totalHeight = computed(() => {
|
|
69
|
+
return displayData.value.length * parseFloat(props.itemHeight) // 假设每项高度50px
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
watch(
|
|
73
|
+
() => props.data,
|
|
74
|
+
() => {
|
|
75
|
+
nextTick(initScrollData)
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
immediate: true,
|
|
79
|
+
deep: true
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
onMounted(() => {
|
|
84
|
+
initScrollEngine()
|
|
85
|
+
})
|
|
86
|
+
|
|
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
|
+
// 滚动事件
|
|
124
|
+
function onScroll(event: ScrollViewOnScrollEvent) {
|
|
125
|
+
scrollTop.value = event.detail.scrollTop
|
|
126
|
+
showBackTopBtn.value = scrollTop.value > parseFloat(props.backToTopThreshold)
|
|
127
|
+
updateVisibleData()
|
|
128
|
+
emit('scroll', event)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 滚动到顶部
|
|
132
|
+
function onScrollUpper(event: ScrollViewOnScrolltoupperEvent) {
|
|
133
|
+
emit('scroll-to-upper', event)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 滚动到底部
|
|
137
|
+
function onScrollLower(event: ScrollViewOnScrolltolowerEvent) {
|
|
138
|
+
emit('scroll-to-lower', event)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 回到顶部
|
|
142
|
+
function scrollToTop() {
|
|
143
|
+
scrollTop.value = 0
|
|
144
|
+
nextTick(() => {
|
|
145
|
+
scrollTop.value = 0
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
function scrollToBottom() {
|
|
149
|
+
scrollToPosition(totalHeight.value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 滚动到指定位置
|
|
153
|
+
function scrollToPosition(position: number | string) {
|
|
154
|
+
scrollTop.value = typeof position === 'number' ? position : parseFloat(position)
|
|
155
|
+
}
|
|
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
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function scrollToElementById(id: string | number) {
|
|
166
|
+
const index = props.data.findIndex((o) => id && o[props.idKey] && o[props.idKey] === id)
|
|
167
|
+
if (index > 0) {
|
|
168
|
+
const scrollDistance = parseFloat(props.itemHeight) * index
|
|
169
|
+
scrollToPosition(scrollDistance)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
defineExpose({
|
|
174
|
+
scrollToTop,
|
|
175
|
+
scrollToBottom,
|
|
176
|
+
scrollToPosition,
|
|
177
|
+
scrollToElement,
|
|
178
|
+
scrollToElementById
|
|
179
|
+
})
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<style lang="scss" scoped>
|
|
183
|
+
@import './index.scss';
|
|
184
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
|
2
|
+
import { baseProps, makeRequiredProp, makeBooleanProp, makeStringProp } 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
|
+
* 默认值:'50px'
|
|
41
|
+
*/
|
|
42
|
+
itemHeight: makeStringProp('50px'),
|
|
43
|
+
/**
|
|
44
|
+
* 是否开启水平滚动
|
|
45
|
+
* 类型:boolean
|
|
46
|
+
* 默认值:false
|
|
47
|
+
*/
|
|
48
|
+
scrollX: makeBooleanProp(false),
|
|
49
|
+
/**
|
|
50
|
+
* 是否开启虚拟滚动
|
|
51
|
+
* 类型:boolean
|
|
52
|
+
* 默认值:true
|
|
53
|
+
*/
|
|
54
|
+
virtual: makeBooleanProp(true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type CollapseExpose = {
|
|
58
|
+
scrollToTop: () => void
|
|
59
|
+
scrollToBottom: () => void
|
|
60
|
+
scrollToPosition: (position: number | string) => void
|
|
61
|
+
scrollToElement: (item: any) => void
|
|
62
|
+
scrollToElementById: (id: string | number) => void
|
|
63
|
+
}
|
|
64
|
+
export type VirtualScrollProps = ExtractPropTypes<typeof virtualScrollProps>
|
|
65
|
+
export type VirtualScrollInstance = ComponentPublicInstance<VirtualScrollProps, CollapseExpose>
|
|
@@ -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']
|
package/locale/lang/ar-SA.ts
CHANGED
package/locale/lang/en-US.ts
CHANGED
package/locale/lang/zh-CN.ts
CHANGED
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"oxy-uni-ui","name":"oxy-uni-ui","displayName":"oxy-uni-ui 基于vue3+Typescript的高颜值组件库","version":"1.0
|
|
1
|
+
{"id":"oxy-uni-ui","name":"oxy-uni-ui","displayName":"oxy-uni-ui 基于vue3+Typescript的高颜值组件库","version":"1.1.0","license":"MIT","description":"一个基于Vue3+TS开发的uni-app组件库,提供70+高质量组件,支持暗黑模式、国际化和自定义主题。","keywords":["oxy-uni-ui","国际化","组件库","vue3","暗黑模式"],"main":"index.ts","engines":{"HBuilderX":"^3.8.7"},"dcloudext":{"type":"component-vue","sale":{"regular":{"price":"0.00"},"sourcecode":{"price":"0.00"}},"contact":{"qq":""},"declaration":{"ads":"无","data":"插件不采集任何数据","permissions":"无"},"npmurl":"https://www.npmjs.com/package/oxy-uni-ui"},"vetur":{"tags":"tags.json","attributes":"attributes.json"},"web-types":"web-types.json","uni_modules":{"dependencies":[],"encrypt":[],"platforms":{"cloud":{"tcb":"y","aliyun":"y","alipay":"n"},"client":{"Vue":{"vue2":"n","vue3":"y"},"App":{"app-vue":"y","app-nvue":"n","app-uvue":"n"},"H5-mobile":{"Safari":"y","Android Browser":"y","微信浏览器(Android)":"y","QQ浏览器(Android)":"y"},"H5-pc":{"Chrome":"y","IE":"u","Edge":"y","Firefox":"y","Safari":"y"},"小程序":{"微信":"y","阿里":"y","百度":"u","字节跳动":"u","QQ":"y","钉钉":"y","快手":"u","飞书":"u","京东":"u"},"快应用":{"华为":"u","联盟":"u"}}}},"peerDependencies":{"vue":">=3.2.47"}}
|