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.
Files changed (57) hide show
  1. package/LICENSE +1 -1
  2. package/attributes.json +1 -1
  3. package/components/common/abstracts/variable.scss +40 -4
  4. package/components/common/util.ts +44 -0
  5. package/components/composables/index.ts +1 -0
  6. package/components/composables/useVirtualScroll.ts +172 -0
  7. package/components/oxy-checkbox/index.scss +36 -6
  8. package/components/oxy-checkbox/oxy-checkbox.vue +5 -4
  9. package/components/oxy-checkbox/types.ts +2 -1
  10. package/components/oxy-col-picker/index.scss +18 -15
  11. package/components/oxy-col-picker/oxy-col-picker.vue +28 -3
  12. package/components/oxy-col-picker/types.ts +12 -0
  13. package/components/oxy-corner/index.scss +258 -0
  14. package/components/oxy-corner/oxy-corner.vue +67 -0
  15. package/components/oxy-corner/types.ts +50 -0
  16. package/components/oxy-drop-menu/index.scss +4 -0
  17. package/components/oxy-drop-menu/oxy-drop-menu.vue +5 -3
  18. package/components/oxy-drop-menu/types.ts +1 -1
  19. package/components/oxy-drop-menu-item/index.scss +4 -4
  20. package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +2 -0
  21. package/components/oxy-file-list/index.scss +83 -0
  22. package/components/oxy-file-list/oxy-file-list.vue +213 -0
  23. package/components/oxy-file-list/types.ts +54 -0
  24. package/components/oxy-list/index.scss +5 -0
  25. package/components/oxy-list/oxy-list.vue +206 -0
  26. package/components/oxy-list/types.ts +38 -0
  27. package/components/oxy-slider/index.scss +2 -2
  28. package/components/oxy-swiper/index.scss +1 -2
  29. package/components/oxy-textarea/oxy-textarea.vue +0 -4
  30. package/components/oxy-tree/components/tree-node-content.vue +72 -0
  31. package/components/oxy-tree/index.scss +83 -0
  32. package/components/oxy-tree/index.ts +51 -0
  33. package/components/oxy-tree/oxy-tree.vue +406 -0
  34. package/components/oxy-tree/types.ts +85 -0
  35. package/components/oxy-tree/utils.ts +51 -0
  36. package/components/oxy-upload/images/audio.png +0 -0
  37. package/components/oxy-upload/images/excle.png +0 -0
  38. package/components/oxy-upload/images/other.png +0 -0
  39. package/components/oxy-upload/images/pdf.png +0 -0
  40. package/components/oxy-upload/images/pic.png +0 -0
  41. package/components/oxy-upload/images/txt.png +0 -0
  42. package/components/oxy-upload/images/video.png +0 -0
  43. package/components/oxy-upload/images/word.png +0 -0
  44. package/components/oxy-upload/index.scss +50 -0
  45. package/components/oxy-upload/oxy-upload.vue +93 -7
  46. package/components/oxy-upload/types.ts +22 -1
  47. package/components/oxy-virtual-scroll/index.scss +35 -0
  48. package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +143 -0
  49. package/components/oxy-virtual-scroll/types.ts +155 -0
  50. package/components/oxy-virtual-scroll/virtual-scroll.ts +81 -0
  51. package/global.d.ts +3 -0
  52. package/locale/lang/ar-SA.ts +2 -1
  53. package/locale/lang/en-US.ts +10 -9
  54. package/locale/lang/zh-CN.ts +7 -6
  55. package/package.json +1 -1
  56. package/tags.json +1 -1
  57. 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 :class="['oxy-upload__preview', customPreviewClass]" v-for="(file, index) in uploadFiles" :key="index">
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 v-else @click="onEvokeClick" :class="['oxy-upload__evoke', disabled ? 'is-disabled' : '', customEvokeClass]">
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 class="oxy-upload__evoke-icon" name="fill-camera"></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']