vite-uni-dev-tool 1.0.0 → 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.
Files changed (168) hide show
  1. package/README.md +46 -0
  2. package/dist/const.cjs +1 -1
  3. package/dist/const.d.ts +12 -0
  4. package/dist/const.d.ts.map +1 -1
  5. package/dist/const.js +1 -1
  6. package/dist/core-shared.d.ts +1 -1
  7. package/dist/core-shared.d.ts.map +1 -1
  8. package/dist/core-shared.js +1 -1
  9. package/dist/core.d.ts +10 -3
  10. package/dist/core.d.ts.map +1 -1
  11. package/dist/core.js +2 -2
  12. package/dist/i18n/locales/en.cjs +1 -1
  13. package/dist/i18n/locales/en.d.ts +81 -0
  14. package/dist/i18n/locales/en.d.ts.map +1 -1
  15. package/dist/i18n/locales/en.js +1 -1
  16. package/dist/i18n/locales/zh-Hans.cjs +1 -1
  17. package/dist/i18n/locales/zh-Hans.d.ts +82 -1
  18. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  19. package/dist/i18n/locales/zh-Hans.js +1 -1
  20. package/dist/modules/devConsole/index.cjs +1 -1
  21. package/dist/modules/devConsole/index.js +3 -3
  22. package/dist/modules/devEvent/index.cjs +3 -3
  23. package/dist/modules/devEvent/index.d.ts +1 -0
  24. package/dist/modules/devEvent/index.d.ts.map +1 -1
  25. package/dist/modules/devEvent/index.js +3 -3
  26. package/dist/modules/devIntercept/index.cjs +14 -13
  27. package/dist/modules/devIntercept/index.d.ts +19 -0
  28. package/dist/modules/devIntercept/index.d.ts.map +1 -1
  29. package/dist/modules/devIntercept/index.js +14 -13
  30. package/dist/modules/devStore/index.cjs +1 -1
  31. package/dist/modules/devStore/index.d.ts +21 -0
  32. package/dist/modules/devStore/index.d.ts.map +1 -1
  33. package/dist/modules/devStore/index.js +1 -1
  34. package/dist/plugins/uniDevTool/transform/transformMain.cjs +1 -1
  35. package/dist/plugins/uniDevTool/transform/transformMain.js +1 -1
  36. package/dist/type.d.ts +47 -2
  37. package/dist/type.d.ts.map +1 -1
  38. package/dist/utils/language.cjs +1 -1
  39. package/dist/utils/language.d.ts.map +1 -1
  40. package/dist/utils/language.js +1 -1
  41. package/dist/utils/object.cjs +1 -1
  42. package/dist/utils/object.d.ts.map +1 -1
  43. package/dist/utils/object.js +1 -1
  44. package/dist/v3/DevTool/components/BluetoothList/BluetoothItem.vue +199 -0
  45. package/dist/v3/DevTool/components/BluetoothList/BluetoothTool.vue +730 -0
  46. package/dist/v3/DevTool/components/BluetoothList/index.vue +167 -0
  47. package/dist/v3/{CaptureScreen → DevTool/components/CaptureScreen}/index.vue +109 -109
  48. package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/ConsoleItem.vue +225 -224
  49. package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/RunJSInput.vue +247 -249
  50. package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/index.vue +171 -160
  51. package/dist/v3/{ConsoleList → DevTool/components/ConsoleList}/staticTips.ts +1145 -1145
  52. package/dist/v3/{DevToolButton → DevTool/components/DevToolButton}/index.vue +7 -4
  53. package/dist/v3/{DevToolTitle → DevTool/components/DevToolTitle}/index.vue +24 -24
  54. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/DevToolOverlay.vue +197 -182
  55. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/const.ts +28 -5
  56. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/dataUtils.ts +48 -48
  57. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/useDevToolData.ts +387 -338
  58. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/useDevToolHandlers.ts +629 -549
  59. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/hooks/useDevToolOverlay.ts +197 -184
  60. package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/index.vue +67 -16
  61. package/dist/v3/{ElEvent → DevTool/components/ElEvent}/ElEventItem.vue +105 -105
  62. package/dist/v3/{ElEvent → DevTool/components/ElEvent}/index.vue +106 -109
  63. package/dist/v3/{Instance → DevTool/components/Instance}/components/InstanceTreeNode.vue +265 -265
  64. package/dist/v3/{Instance → DevTool/components/Instance}/flatten.ts +226 -226
  65. package/dist/v3/{Instance → DevTool/components/Instance}/index.vue +94 -94
  66. package/dist/v3/{Instance → DevTool/components/Instance}/registry.ts +49 -49
  67. package/dist/v3/{Instance → DevTool/components/Instance}/transformTree.ts +375 -375
  68. package/dist/v3/{Instance → DevTool/components/Instance}/transformTreeCtx.ts +268 -268
  69. package/dist/v3/{Instance → DevTool/components/Instance}/typing.d.ts +43 -43
  70. package/dist/v3/{InstanceDetail → DevTool/components/InstanceDetail}/index.vue +485 -485
  71. package/dist/v3/{JsonDetail → DevTool/components/JsonDetail}/index.vue +70 -70
  72. package/dist/v3/{NFCList → DevTool/components/NFCList}/NFCItem.vue +112 -113
  73. package/dist/v3/{NFCList → DevTool/components/NFCList}/NFCTool.vue +454 -478
  74. package/dist/v3/{NFCList → DevTool/components/NFCList}/const.ts +56 -56
  75. package/dist/v3/{NFCList → DevTool/components/NFCList}/index.vue +94 -98
  76. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/InterceptConfig.vue +624 -608
  77. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/InterceptItem.vue +140 -140
  78. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkDetail.vue +287 -296
  79. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkIntercept.vue +88 -93
  80. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkItem.vue +163 -167
  81. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/NetworkSend.vue +589 -556
  82. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/const.ts +4 -4
  83. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/hooks/useNetworkForm.ts +86 -86
  84. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/index.vue +160 -160
  85. package/dist/v3/{NetworkList → DevTool/components/NetworkList}/utils.ts +101 -101
  86. package/dist/v3/{Performance → DevTool/components/Performance}/index.vue +498 -495
  87. package/dist/v3/{Performance → DevTool/components/Performance}/modules/PerformanceMetrics.vue +153 -153
  88. package/dist/v3/{Performance → DevTool/components/Performance}/modules/PerformanceWidget.vue +12 -9
  89. package/dist/v3/{Performance → DevTool/components/Performance}/modules/usePerformanceChart.ts +460 -460
  90. package/dist/v3/{Performance → DevTool/components/Performance}/modules/usePerformanceData.ts +258 -258
  91. package/dist/v3/{PiniaList → DevTool/components/PiniaList}/index.vue +93 -94
  92. package/dist/v3/{RouteList → DevTool/components/RouteList}/index.vue +21 -24
  93. package/dist/v3/{RunJS → DevTool/components/RunJS}/index.vue +148 -148
  94. package/dist/v3/{ScanCodeList → DevTool/components/ScanCodeList}/ScanCodeItem.vue +97 -98
  95. package/dist/v3/{ScanCodeList → DevTool/components/ScanCodeList}/index.vue +100 -104
  96. package/dist/v3/{SettingButton → DevTool/components/SettingButton}/index.vue +45 -45
  97. package/dist/v3/{SettingList → DevTool/components/SettingList}/index.vue +218 -150
  98. package/dist/v3/DevTool/components/SettingList/modules/SettingBarrage.vue +304 -0
  99. package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingDevTool.vue +212 -208
  100. package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingInfo.vue +157 -119
  101. package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingLanguage.vue +74 -74
  102. package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingLog.vue +230 -230
  103. package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingNetwork.vue +3 -3
  104. package/dist/v3/{SettingList → DevTool/components/SettingList}/modules/SettingTheme.vue +37 -7
  105. package/dist/v3/{SettingList → DevTool/components/SettingList}/typing.d.ts +2 -2
  106. package/dist/v3/{SourceCode → DevTool/components/SourceCode}/Line.vue +127 -116
  107. package/dist/v3/{SourceCode → DevTool/components/SourceCode}/index.vue +8 -8
  108. package/dist/v3/{SourceCode → DevTool/components/SourceCode}/parseCode.ts +609 -701
  109. package/dist/v3/{StorageList → DevTool/components/StorageList}/index.vue +174 -174
  110. package/dist/v3/{TransferList → DevTool/components/TransferList}/TransferDetail.vue +268 -268
  111. package/dist/v3/{TransferList → DevTool/components/TransferList}/TransferItem.vue +4 -4
  112. package/dist/v3/{TransferList → DevTool/components/TransferList}/index.vue +8 -8
  113. package/dist/v3/{UniEvent → DevTool/components/UniEvent}/UniEventItem.vue +6 -7
  114. package/dist/v3/{UniEvent → DevTool/components/UniEvent}/index.vue +6 -6
  115. package/dist/v3/{VuexList → DevTool/components/VuexList}/index.vue +84 -84
  116. package/dist/v3/{WebSocket → DevTool/components/WebSocket}/WebSocketDetail.vue +8 -8
  117. package/dist/v3/{WebSocket → DevTool/components/WebSocket}/WebSocketItem.vue +4 -4
  118. package/dist/v3/{WebSocket → DevTool/components/WebSocket}/index.vue +8 -8
  119. package/dist/v3/DevTool/index.vue +179 -5
  120. package/dist/v3/{AppTransition → components/AppTransition}/index.vue +176 -170
  121. package/dist/v3/{AutoSizer → components/AutoSizer}/index.vue +192 -192
  122. package/dist/v3/{AutoSizer → components/AutoSizer}/index1.vue +184 -184
  123. package/dist/v3/{AutoSizer → components/AutoSizer}/utils.ts +49 -49
  124. package/dist/v3/components/Barrage/BarrageItem.vue +137 -0
  125. package/dist/v3/components/Barrage/index.vue +202 -0
  126. package/dist/v3/{CircularButton → components/CircularButton}/index.vue +84 -84
  127. package/dist/v3/{CustomSwiper → components/CustomSwiper}/CustomSwiperItem.vue +49 -49
  128. package/dist/v3/{CustomSwiper → components/CustomSwiper}/index.vue +104 -104
  129. package/dist/v3/{DraggableContainer → components/DraggableContainer}/index.vue +1 -1
  130. package/dist/v3/{Empty → components/Empty}/index.vue +29 -29
  131. package/dist/v3/{FilterInput → components/FilterInput}/index.vue +1 -1
  132. package/dist/v3/{FilterSelect → components/FilterSelect}/index.vue +179 -179
  133. package/dist/v3/{JsonPretty → components/JsonPretty}/components/Brackets/index.vue +27 -27
  134. package/dist/v3/{JsonPretty → components/JsonPretty}/components/Carets/index.vue +59 -59
  135. package/dist/v3/{JsonPretty → components/JsonPretty}/components/CheckController/index.vue +136 -136
  136. package/dist/v3/{JsonPretty → components/JsonPretty}/components/TreeNode/index.vue +387 -381
  137. package/dist/v3/{JsonPretty → components/JsonPretty}/hooks/useClipboard.ts +21 -21
  138. package/dist/v3/{JsonPretty → components/JsonPretty}/hooks/useError.ts +21 -21
  139. package/dist/v3/{JsonPretty → components/JsonPretty}/index.vue +16 -13
  140. package/dist/v3/{JsonPretty → components/JsonPretty}/type.ts +127 -126
  141. package/dist/v3/{JsonPretty → components/JsonPretty}/utils/index.ts +169 -169
  142. package/dist/v3/{MovableContainer → components/MovableContainer}/index.vue +1 -1
  143. package/dist/v3/{Pick → components/Pick}/index.vue +322 -322
  144. package/dist/v3/{Tabs → components/Tabs}/index.vue +30 -4
  145. package/dist/v3/{Tag → components/Tag}/index.vue +113 -113
  146. package/dist/v3/{VirtualList → components/VirtualList}/AutoSize.vue +40 -40
  147. package/dist/v3/{VirtualList → components/VirtualList}/index.vue +416 -412
  148. package/dist/v3/hooks/useBluetooth/index.ts +561 -0
  149. package/dist/v3/hooks/useContainerStyle.ts +153 -153
  150. package/dist/v3/hooks/useNFC/index.ts +107 -107
  151. package/dist/v3/hooks/useNFC/typing.d.ts +396 -396
  152. package/dist/v3/hooks/useNFC/useNFCAndroid.ts +966 -966
  153. package/dist/v3/hooks/useNFC/useNFCMpWeiXin.ts +812 -812
  154. package/dist/v3/hooks/useNFC/utils.ts +754 -754
  155. package/dist/v3/hooks/useRequest/index.ts +586 -573
  156. package/dist/v3/hooks/useRequest/utils.ts +267 -267
  157. package/dist/v3/hooks/useScanCode/index.ts +206 -206
  158. package/dist/v3/hooks/useWebsocket/README.md +79 -0
  159. package/dist/v3/hooks/useWebsocket/index.ts +253 -0
  160. package/dist/v3/styles/theme.css +17 -10
  161. package/dist/v3/styles/theme.ts +12 -12
  162. package/package.json +59 -64
  163. package/dist/plugins/uniParseStock/index.d.ts +0 -10
  164. package/dist/plugins/uniParseStock/index.d.ts.map +0 -1
  165. /package/dist/v3/{DevToolWindow → DevTool/components/DevToolWindow}/index.css +0 -0
  166. /package/dist/v3/{SettingList → DevTool/components/SettingList}/index.css +0 -0
  167. /package/dist/v3/{Empty → components/Empty}/empty.png +0 -0
  168. /package/dist/v3/{VirtualList → components/VirtualList}/readme.md +0 -0
@@ -1,412 +1,416 @@
1
- <template>
2
- <view :class="['virtual-list-wrapper', className]" :style="wrapperStyle">
3
- <scroll-view
4
- scroll-y
5
- :scroll-x="scrollX && !isEdgeSwipe"
6
- class="virtual-list-scroll"
7
- :scroll-top="scrollTopVal"
8
- :scroll-with-animation="scrollWithAnimation"
9
- :scroll-into-view="scrollIntoView"
10
- @scroll="handleScroll"
11
- @touchstart="onTouchStart"
12
- @touchmove="onTouchMove"
13
- @touchend="onTouchEnd"
14
- @mousedown="onMouseDown">
15
- <!-- 滚动容器:统一负责撑开横向和纵向的滚动区域 -->
16
- <view class="virtual-list-container" :style="containerStyle">
17
- <!-- 实际渲染内容的列表块 -->
18
- <view class="virtual-list-content" :style="contentStyle">
19
- <view class="virtual-list-measure-inner">
20
- <slot
21
- :list="visibleData"
22
- :startIndex="startIndex"
23
- :start="startIndex"
24
- :current="Math.floor(startIndex / props.pageSize) + 1"
25
- :itemsHeight="itemsHeightMap"
26
- :onSizeChange="onSizeChange"></slot>
27
- </view>
28
- </view>
29
- </view>
30
- </scroll-view>
31
-
32
- <!-- 返回顶部按钮:移出 scroll-view,绝对定位在 wrapper 内部 -->
33
- <view
34
- :style="{ display: showBackTop && scrollTop > 200 ? 'flex' : 'none' }"
35
- class="virtual-back-top"
36
- @click="scrollToTop">
37
-
38
- </view>
39
- </view>
40
- </template>
41
-
42
- <script lang="ts" setup>
43
- import {
44
- ref,
45
- reactive,
46
- computed,
47
- watch,
48
- nextTick,
49
- getCurrentInstance,
50
- onMounted,
51
- } from 'vue';
52
-
53
- interface Props {
54
- /** 渲染数据源 */
55
- data: any[];
56
- /** 容器高度 */
57
- height?: number | string;
58
- /** 每页渲染数量 (虚拟化块大小) */
59
- pageSize?: number;
60
- /** 预估的项高度 (用于初次计算) */
61
- itemHeight?: number;
62
- /** 额外的缓冲区数量 (防止滚动过快白屏) */
63
- buffer?: number;
64
- /** 自定义类名 */
65
- className?: string;
66
- /** 是否显示返回顶部 */
67
- showBackTop?: boolean;
68
- /** 数据更新时是否自动滚动到顶部 */
69
- dataChangeScrollToTop?: boolean;
70
- /** 预加载高度 (兼容 Pro) */
71
- preLodeHeight?: number;
72
- /** 默认当前页 (从 1 开始) */
73
- defaultCurrent?: number;
74
- /** 横向滚动 */
75
- scrollX?: boolean;
76
- /** 滚动动画 */
77
- scrollWithAnimation?: boolean;
78
- /** 滚动到指定元素 ID */
79
- scrollIntoView?: string;
80
- }
81
-
82
- const props = withDefaults(defineProps<Props>(), {
83
- height: '100%',
84
- pageSize: 20,
85
- itemHeight: 50,
86
- buffer: 5,
87
- showBackTop: true,
88
- dataChangeScrollToTop: true,
89
- preLodeHeight: 100,
90
- defaultCurrent: 1,
91
- scrollX: false,
92
- scrollWithAnimation: false,
93
- scrollIntoView: '',
94
- });
95
-
96
- const emit = defineEmits(['scroll']);
97
-
98
- /** 定义插槽类型 */
99
- defineSlots<{
100
- default(props: {
101
- /** 当前可见数据列表 */
102
- list: any[];
103
- /** 起始索引 (绝对索引) */
104
- startIndex: number;
105
- /** 起始索引 (别名,同 startIndex) */
106
- start: number;
107
- /** 当前页码 (基于 startIndex 和 pageSize 计算) */
108
- current: number;
109
- /** 各项高度映射记录 */
110
- itemsHeight: Record<number, number>;
111
- /** 子项尺寸变化回调 */
112
- onSizeChange: (index: number, height: number) => void;
113
- }): any;
114
- }>();
115
-
116
- // --- 状态变量 ---
117
- const scrollTop = ref(0);
118
- const scrollTopVal = ref(0); // 用于控制转跳的响应式值
119
- const startIndex = ref(0);
120
- const itemsHeightMap = reactive<Record<number, number>>({}); // 存储每一项的实际高度
121
-
122
- const isEdgeSwipe = ref(false);
123
- const isCapturing = ref(false); // 是否由当前列表锁定捕获手势
124
- let containerRect = { left: 0, right: 0, width: 0 };
125
-
126
- const instance = getCurrentInstance();
127
- const maxWidth = ref(0);
128
-
129
- onMounted(() => {
130
- // 获取容器位置和宽度,用于边缘检测
131
- uni
132
- .createSelectorQuery()
133
- .in(instance)
134
- .select('.virtual-list-wrapper')
135
- .boundingClientRect((data: any) => {
136
- if (data) {
137
- containerRect = data;
138
- }
139
- })
140
- .exec();
141
- });
142
-
143
- function onTouchStart(e: TouchEvent | MouseEvent) {
144
- if (!props.scrollX) return;
145
- const touch = (e as TouchEvent).touches
146
- ? (e as TouchEvent).touches[0]
147
- : (e as MouseEvent);
148
- const x = touch.clientX;
149
-
150
- // 如果从边缘 40px 内开始滑动,则视为侧滑切换 Tab,允许事件冒泡到父级
151
- const threshold = 40;
152
- if (
153
- x < containerRect.left + threshold ||
154
- x > containerRect.right - threshold
155
- ) {
156
- isEdgeSwipe.value = true;
157
- isCapturing.value = false;
158
- } else {
159
- isEdgeSwipe.value = false;
160
- isCapturing.value = true;
161
- // 重要:锁定在此,阻止事件冒泡到 CustomSwiper
162
- e.stopPropagation();
163
- }
164
- }
165
-
166
- function onTouchMove(e: TouchEvent) {
167
- if (isCapturing.value) {
168
- // 已经锁定时,继续阻止冒泡,确保父级不会触发滑屏逻辑
169
- e.stopPropagation();
170
- }
171
- }
172
-
173
- function onTouchEnd(e: TouchEvent | MouseEvent) {
174
- if (isCapturing.value) {
175
- e.stopPropagation();
176
- }
177
- isCapturing.value = false;
178
- isEdgeSwipe.value = false;
179
- }
180
-
181
- function onMouseDown(e: MouseEvent) {
182
- onTouchStart(e);
183
- }
184
-
185
- // 根据 defaultCurrent 初始化位置
186
- if (props.defaultCurrent > 1) {
187
- startIndex.value = Math.max(0, (props.defaultCurrent - 1) * props.pageSize);
188
- }
189
- const offset = ref(0);
190
-
191
- // 初始化 offset
192
- if (startIndex.value > 0) {
193
- let initialOffset = 0;
194
- for (let i = 0; i < startIndex.value; i++) {
195
- initialOffset += itemsHeightMap[i] || props.itemHeight;
196
- }
197
- offset.value = initialOffset;
198
- }
199
-
200
- // --- 计算属性 ---
201
- // 外层容器样式
202
- const wrapperStyle = computed(
203
- () =>
204
- ({
205
- height:
206
- typeof props.height === 'number' ? `${props.height}px` : props.height,
207
- position: 'relative' as const,
208
- overflow: 'hidden' as const,
209
- }) as any,
210
- );
211
-
212
- // 计算总高度:已测量的高度 + 未测量的默认高度
213
- const totalHeight = computed(() => {
214
- let total = 0;
215
- for (let i = 0; i < props.data.length; i++) {
216
- total += itemsHeightMap[i] || props.itemHeight;
217
- }
218
- return total;
219
- });
220
-
221
- // 可见数据切片
222
- const visibleData = computed(() => {
223
- // 我们多渲染一点,避免滚动白屏
224
- const end = Math.min(
225
- startIndex.value + props.pageSize + props.buffer,
226
- props.data.length,
227
- );
228
- return props.data.slice(startIndex.value, end);
229
- });
230
-
231
- // 滚动容器样式 (撑开滚动区域的核心)
232
- const containerStyle = computed(() => {
233
- const style: any = {
234
- height: `${totalHeight.value}px`,
235
- position: 'relative',
236
- width: '100%',
237
- };
238
- if (props.scrollX) {
239
- if (maxWidth.value) {
240
- style.minWidth = `${maxWidth.value}px`;
241
- }
242
- }
243
- return style;
244
- });
245
-
246
- // 内容块样式
247
- const contentStyle = computed(() => {
248
- return {
249
- transform: `translateY(${offset.value}px)`,
250
- position: 'absolute' as const,
251
- left: 0,
252
- top: 0,
253
- width: '100%',
254
- // minWidth: 'max-content',
255
- };
256
- });
257
-
258
- // --- 方法 ---
259
-
260
- /**
261
- * 核心滚动处理函数
262
- */
263
- const handleScroll = (e: any) => {
264
- // Uni-app 环境下 e.detail.scrollTop 可能存在,标准 H5 下是 e.target.scrollTop
265
- const st = e.detail?.scrollTop ?? e.target?.scrollTop ?? 0;
266
- scrollTop.value = st;
267
-
268
- // 根据当前滚动位置计算 startIndex
269
- let accumulatedHeight = 0;
270
- let index = -1;
271
- const dataLen = props.data.length;
272
-
273
- for (let i = 0; i < dataLen; i++) {
274
- const h = itemsHeightMap[i] || props.itemHeight;
275
- if (accumulatedHeight + h > st) {
276
- index = i;
277
- break;
278
- }
279
- accumulatedHeight += h;
280
- }
281
-
282
- // 修复核心:如果 st 很大,超过了所有项的高度和,或者刚好在最后,确保 index 不会回滚到 0
283
- if (index === -1) {
284
- index = dataLen > 0 ? dataLen - 1 : 0;
285
- }
286
-
287
- // 加上缓冲区处理
288
- const targetIndex = Math.max(0, index - props.buffer);
289
-
290
- // 更新状态
291
- if (targetIndex !== startIndex.value) {
292
- startIndex.value = targetIndex;
293
-
294
- // 计算内容块的偏移量:即渲染起始项之前的全部高度
295
- let currentOffset = 0;
296
- for (let i = 0; i < targetIndex; i++) {
297
- currentOffset += itemsHeightMap[i] || props.itemHeight;
298
- }
299
- offset.value = currentOffset;
300
- }
301
-
302
- emit('scroll', e);
303
- };
304
-
305
- /**
306
- * 子项尺寸变化时更新高度映射
307
- */
308
- const onSizeChange = (index: number, height: number) => {
309
- if (height > 0 && itemsHeightMap[index] !== height) {
310
- itemsHeightMap[index] = height;
311
- }
312
- };
313
-
314
- /**
315
- * 滚动到指定位置
316
- */
317
- const scrollToTop = () => {
318
- scrollTopVal.value = scrollTop.value; // 先同步一次
319
- nextTick(() => {
320
- scrollTopVal.value = 0;
321
- scrollTop.value = 0;
322
- });
323
- };
324
-
325
- // 暴露 API 给外部使用
326
- defineExpose({
327
- scrollToTop,
328
- itemsHeightMap,
329
- });
330
-
331
- /**
332
- * 更新记录的最大宽度
333
- */
334
- const updateMaxWidth = () => {
335
- if (!props.scrollX) return;
336
- uni
337
- .createSelectorQuery()
338
- .in(instance)
339
- .select('.virtual-list-measure-inner')
340
- .boundingClientRect((res: any) => {
341
- // 只有当检测到更大的宽度时才更新,实现“只增不减”
342
- if (res && res.width > maxWidth.value) {
343
- maxWidth.value = res.width;
344
- }
345
- })
346
- .exec();
347
- };
348
-
349
- // 监听可见数据变化,更新最大宽度
350
- watch(visibleData, () => {
351
- nextTick(() => {
352
- updateMaxWidth();
353
- });
354
- });
355
-
356
- // 数据变化处理
357
- watch(
358
- () => props.data,
359
- () => {
360
- if (props.dataChangeScrollToTop) {
361
- maxWidth.value = 0; // 只有在需要回到顶部(彻底换数据)时才重置宽度
362
- scrollToTop();
363
- // 数据重置时,立即更新内部索引,防止旧位置引起的白屏
364
- startIndex.value = 0;
365
- offset.value = 0;
366
- scrollTop.value = 0;
367
- }
368
- },
369
- { deep: false },
370
- );
371
- </script>
372
-
373
- <style scoped>
374
- .virtual-list-wrapper {
375
- box-sizing: border-box;
376
- }
377
-
378
- .virtual-list-scroll {
379
- width: 100%;
380
- height: 100%;
381
- }
382
-
383
- .virtual-list-measure-inner {
384
- /* width: max-content; */
385
- min-width: 100%;
386
- }
387
-
388
- .virtual-back-top {
389
- position: absolute;
390
- bottom: 24px;
391
- right: 24px;
392
- width: 44px;
393
- height: 44px;
394
- background-color: var(--dev-tool-main-color, #9254de);
395
- color: #fff;
396
- border-radius: 50%;
397
- display: flex;
398
- align-items: center;
399
- justify-content: center;
400
- box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
401
- font-size: 20px;
402
- z-index: 1000;
403
- cursor: pointer;
404
- transition:
405
- opacity 0.3s,
406
- transform 0.2s;
407
- }
408
-
409
- .virtual-back-top:active {
410
- transform: scale(0.9);
411
- }
412
- </style>
1
+ <template>
2
+ <view :class="['virtual-list-wrapper', className]" :style="wrapperStyle">
3
+ <scroll-view
4
+ scroll-y
5
+ :scroll-x="scrollX && !isEdgeSwipe"
6
+ class="virtual-list-scroll"
7
+ :scroll-top="scrollTopVal"
8
+ :scroll-with-animation="scrollWithAnimation"
9
+ :scroll-into-view="scrollIntoView"
10
+ @scroll="handleScroll"
11
+ @touchstart="onTouchStart"
12
+ @touchmove="onTouchMove"
13
+ @touchend="onTouchEnd"
14
+ @mousedown="onMouseDown">
15
+ <!-- 滚动容器:统一负责撑开横向和纵向的滚动区域 -->
16
+ <view class="virtual-list-container" :style="containerStyle">
17
+ <!-- 实际渲染内容的列表块 -->
18
+ <view class="virtual-list-content" :style="contentStyle">
19
+ <view class="virtual-list-measure-inner">
20
+ <slot
21
+ :list="visibleData"
22
+ :startIndex="startIndex"
23
+ :start="startIndex"
24
+ :current="Math.floor(startIndex / props.pageSize) + 1"
25
+ :itemsHeight="itemsHeightMap"
26
+ :onSizeChange="onSizeChange"></slot>
27
+ </view>
28
+ </view>
29
+ </view>
30
+ </scroll-view>
31
+
32
+ <!-- 返回顶部按钮:移出 scroll-view,绝对定位在 wrapper 内部 -->
33
+ <view
34
+ :style="{ display: showBackTop && scrollTop > 200 ? 'flex' : 'none' }"
35
+ class="virtual-back-top"
36
+ @click="scrollToTop">
37
+
38
+ </view>
39
+ </view>
40
+ </template>
41
+
42
+ <script lang="ts" setup>
43
+ import {
44
+ ref,
45
+ reactive,
46
+ computed,
47
+ watch,
48
+ nextTick,
49
+ getCurrentInstance,
50
+ onMounted,
51
+ } from 'vue';
52
+
53
+ interface Props {
54
+ /** 渲染数据源 */
55
+ data: any[];
56
+ /** 容器高度 */
57
+ height?: number | string;
58
+ /** 每页渲染数量 (虚拟化块大小) */
59
+ pageSize?: number;
60
+ /** 预估的项高度 (用于初次计算) */
61
+ itemHeight?: number;
62
+ /** 额外的缓冲区数量 (防止滚动过快白屏) */
63
+ buffer?: number;
64
+ /** 自定义类名 */
65
+ className?: string;
66
+ /** 是否显示返回顶部 */
67
+ showBackTop?: boolean;
68
+ /** 数据更新时是否自动滚动到顶部 */
69
+ dataChangeScrollToTop?: boolean;
70
+ /** 预加载高度 (兼容 Pro) */
71
+ preLodeHeight?: number;
72
+ /** 默认当前页 (从 1 开始) */
73
+ defaultCurrent?: number;
74
+ /** 横向滚动 */
75
+ scrollX?: boolean;
76
+ /** 滚动动画 */
77
+ scrollWithAnimation?: boolean;
78
+ /** 滚动到指定元素 ID */
79
+ scrollIntoView?: string;
80
+ }
81
+
82
+ const props = withDefaults(defineProps<Props>(), {
83
+ height: '100%',
84
+ pageSize: 20,
85
+ itemHeight: 50,
86
+ buffer: 5,
87
+ showBackTop: true,
88
+ dataChangeScrollToTop: true,
89
+ preLodeHeight: 100,
90
+ defaultCurrent: 1,
91
+ scrollX: false,
92
+ scrollWithAnimation: false,
93
+ scrollIntoView: '',
94
+ });
95
+
96
+ const emit = defineEmits(['scroll']);
97
+
98
+ /** 定义插槽类型 */
99
+ defineSlots<{
100
+ default(props: {
101
+ /** 当前可见数据列表 */
102
+ list: any[];
103
+ /** 起始索引 (绝对索引) */
104
+ startIndex: number;
105
+ /** 起始索引 (别名,同 startIndex) */
106
+ start: number;
107
+ /** 当前页码 (基于 startIndex 和 pageSize 计算) */
108
+ current: number;
109
+ /** 各项高度映射记录 */
110
+ itemsHeight: Record<number, number>;
111
+ /** 子项尺寸变化回调 */
112
+ onSizeChange: (index: number, height: number) => void;
113
+ }): any;
114
+ }>();
115
+
116
+ // --- 状态变量 ---
117
+ const scrollTop = ref(0);
118
+ const scrollTopVal = ref(0); // 用于控制转跳的响应式值
119
+ const startIndex = ref(0);
120
+ const itemsHeightMap = reactive<Record<number, number>>({}); // 存储每一项的实际高度
121
+
122
+ const isEdgeSwipe = ref(false);
123
+ const isCapturing = ref(false); // 是否由当前列表锁定捕获手势
124
+ let containerRect = { left: 0, right: 0, width: 0 };
125
+
126
+ const instance = getCurrentInstance();
127
+ const maxWidth = ref(0);
128
+
129
+ onMounted(() => {
130
+ // 获取容器位置和宽度,用于边缘检测
131
+ uni
132
+ .createSelectorQuery()
133
+ .in(instance)
134
+ .select('.virtual-list-wrapper')
135
+ .boundingClientRect((data: any) => {
136
+ if (data) {
137
+ containerRect = data;
138
+ }
139
+ })
140
+ .exec();
141
+ });
142
+
143
+ function onTouchStart(e: TouchEvent | MouseEvent) {
144
+ if (!props.scrollX) return;
145
+ const touch = (e as TouchEvent).touches
146
+ ? (e as TouchEvent).touches[0]
147
+ : (e as MouseEvent);
148
+ const x = touch.clientX;
149
+
150
+ // 如果从边缘 40px 内开始滑动,则视为侧滑切换 Tab,允许事件冒泡到父级
151
+ const threshold = 40;
152
+ if (
153
+ x < containerRect.left + threshold ||
154
+ x > containerRect.right - threshold
155
+ ) {
156
+ isEdgeSwipe.value = true;
157
+ isCapturing.value = false;
158
+ } else {
159
+ isEdgeSwipe.value = false;
160
+ isCapturing.value = true;
161
+ // 重要:锁定在此,阻止事件冒泡到 CustomSwiper
162
+ e.stopPropagation();
163
+ }
164
+ }
165
+
166
+ function onTouchMove(e: TouchEvent) {
167
+ if (isCapturing.value) {
168
+ // 已经锁定时,继续阻止冒泡,确保父级不会触发滑屏逻辑
169
+ e.stopPropagation();
170
+ }
171
+ }
172
+
173
+ function onTouchEnd(e: TouchEvent | MouseEvent) {
174
+ if (isCapturing.value) {
175
+ e.stopPropagation();
176
+ }
177
+ isCapturing.value = false;
178
+ isEdgeSwipe.value = false;
179
+ }
180
+
181
+ function onMouseDown(e: MouseEvent) {
182
+ onTouchStart(e);
183
+ }
184
+
185
+ // 根据 defaultCurrent 初始化位置
186
+ if (props.defaultCurrent > 1) {
187
+ startIndex.value = Math.max(0, (props.defaultCurrent - 1) * props.pageSize);
188
+ }
189
+ const offset = ref(0);
190
+
191
+ // 初始化 offset
192
+ if (startIndex.value > 0) {
193
+ let initialOffset = 0;
194
+ for (let i = 0; i < startIndex.value; i++) {
195
+ initialOffset += itemsHeightMap[i] || props.itemHeight;
196
+ }
197
+ offset.value = initialOffset;
198
+ }
199
+
200
+ // --- 计算属性 ---
201
+ // 外层容器样式
202
+ const wrapperStyle = computed(
203
+ () =>
204
+ ({
205
+ height:
206
+ typeof props.height === 'number' ? `${props.height}px` : props.height,
207
+ position: 'relative' as const,
208
+ overflow: 'hidden' as const,
209
+ }) as any,
210
+ );
211
+
212
+ // 计算总高度:已测量的高度 + 未测量的默认高度
213
+ const totalHeight = computed(() => {
214
+ let total = 0;
215
+ for (let i = 0; i < props.data.length; i++) {
216
+ total += itemsHeightMap[i] || props.itemHeight;
217
+ }
218
+ return total;
219
+ });
220
+
221
+ // 可见数据切片
222
+ const visibleData = computed(() => {
223
+ // 我们多渲染一点,避免滚动白屏
224
+ const end = Math.min(
225
+ startIndex.value + props.pageSize + props.buffer,
226
+ props.data.length,
227
+ );
228
+ return props.data.slice(startIndex.value, end);
229
+ });
230
+
231
+ // 滚动容器样式 (撑开滚动区域的核心)
232
+ const containerStyle = computed(() => {
233
+ const style: any = {
234
+ height: `${totalHeight.value}px`,
235
+ position: 'relative',
236
+ width: '100%',
237
+ };
238
+ if (props.scrollX) {
239
+ if (maxWidth.value) {
240
+ style.minWidth = `${maxWidth.value}px`;
241
+ }
242
+ }
243
+ return style;
244
+ });
245
+
246
+ // 内容块样式
247
+ const contentStyle = computed(() => {
248
+ return {
249
+ transform: `translateY(${offset.value}px)`,
250
+ position: 'absolute' as const,
251
+ left: 0,
252
+ top: 0,
253
+ width: '100%',
254
+ // minWidth: 'max-content',
255
+ };
256
+ });
257
+
258
+ // --- 方法 ---
259
+
260
+ /**
261
+ * 核心滚动处理函数
262
+ */
263
+ const handleScroll = (e: any) => {
264
+ // Uni-app 环境下 e.detail.scrollTop 可能存在,标准 H5 下是 e.target.scrollTop
265
+ const st = e.detail?.scrollTop ?? e.target?.scrollTop ?? 0;
266
+ scrollTop.value = st;
267
+
268
+ // 根据当前滚动位置计算 startIndex
269
+ let accumulatedHeight = 0;
270
+ let index = -1;
271
+ const dataLen = props.data.length;
272
+
273
+ for (let i = 0; i < dataLen; i++) {
274
+ const h = itemsHeightMap[i] || props.itemHeight;
275
+ if (accumulatedHeight + h > st) {
276
+ index = i;
277
+ break;
278
+ }
279
+ accumulatedHeight += h;
280
+ }
281
+
282
+ // 修复核心:如果 st 很大,超过了所有项的高度和,或者刚好在最后,确保 index 不会回滚到 0
283
+ if (index === -1) {
284
+ index = dataLen > 0 ? dataLen - 1 : 0;
285
+ }
286
+
287
+ // 加上缓冲区处理
288
+ const targetIndex = Math.max(0, index - props.buffer);
289
+
290
+ // 更新状态
291
+ if (targetIndex !== startIndex.value) {
292
+ startIndex.value = targetIndex;
293
+
294
+ // 计算内容块的偏移量:即渲染起始项之前的全部高度
295
+ let currentOffset = 0;
296
+ for (let i = 0; i < targetIndex; i++) {
297
+ currentOffset += itemsHeightMap[i] || props.itemHeight;
298
+ }
299
+ offset.value = currentOffset;
300
+ }
301
+
302
+ emit('scroll', e);
303
+ };
304
+
305
+ /**
306
+ * 子项尺寸变化时更新高度映射
307
+ */
308
+ const onSizeChange = (index: number, height: number) => {
309
+ if (height > 0 && itemsHeightMap[index] !== height) {
310
+ itemsHeightMap[index] = height;
311
+ }
312
+ };
313
+
314
+ /**
315
+ * 滚动到指定位置
316
+ */
317
+ const scrollToTop = () => {
318
+ scrollTopVal.value = scrollTop.value; // 先同步一次
319
+ nextTick(() => {
320
+ scrollTopVal.value = 0;
321
+ scrollTop.value = 0;
322
+ });
323
+ };
324
+
325
+ // 暴露 API 给外部使用
326
+ defineExpose({
327
+ scrollToTop,
328
+ itemsHeightMap,
329
+ });
330
+
331
+ /**
332
+ * 更新记录的最大宽度
333
+ */
334
+ const updateMaxWidth = () => {
335
+ if (!props.scrollX) return;
336
+ uni
337
+ .createSelectorQuery()
338
+ .in(instance)
339
+ .select('.virtual-list-measure-inner')
340
+ .boundingClientRect((res: any) => {
341
+ // 只有当检测到更大的宽度时才更新,实现“只增不减”
342
+ if (res && res.width > maxWidth.value) {
343
+ maxWidth.value = res.width;
344
+ }
345
+ })
346
+ .exec();
347
+ };
348
+
349
+ // 监听可见数据变化,更新最大宽度
350
+ watch(visibleData, () => {
351
+ nextTick(() => {
352
+ updateMaxWidth();
353
+ });
354
+ });
355
+
356
+ // 数据变化处理
357
+ watch(
358
+ () => props.data,
359
+ () => {
360
+ if (props.dataChangeScrollToTop) {
361
+ maxWidth.value = 0; // 只有在需要回到顶部(彻底换数据)时才重置宽度
362
+ scrollToTop();
363
+ // 数据重置时,立即更新内部索引,防止旧位置引起的白屏
364
+ startIndex.value = 0;
365
+ offset.value = 0;
366
+ scrollTop.value = 0;
367
+ }
368
+ },
369
+ { deep: false },
370
+ );
371
+ </script>
372
+
373
+ <style scoped>
374
+ .virtual-list-wrapper {
375
+ box-sizing: border-box;
376
+ }
377
+
378
+ .virtual-list-scroll {
379
+ width: 100%;
380
+ height: 100%;
381
+ }
382
+
383
+ :deep(.virtual-list-scroll .uni-scroll-view .uni-scroll-view) {
384
+ overflow: auto !important;
385
+ }
386
+
387
+ .virtual-list-measure-inner {
388
+ /* width: max-content; */
389
+ min-width: 100%;
390
+ }
391
+
392
+ .virtual-back-top {
393
+ position: absolute;
394
+ bottom: 24px;
395
+ right: 24px;
396
+ width: 44px;
397
+ height: 44px;
398
+ background-color: var(--dev-tool-main-color, #9254de);
399
+ color: #fff;
400
+ border-radius: 50%;
401
+ display: flex;
402
+ align-items: center;
403
+ justify-content: center;
404
+ box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
405
+ font-size: 20px;
406
+ z-index: 1000;
407
+ cursor: pointer;
408
+ transition:
409
+ opacity 0.3s,
410
+ transform 0.2s;
411
+ }
412
+
413
+ .virtual-back-top:active {
414
+ transform: scale(0.9);
415
+ }
416
+ </style>