@puresnake/snake-ui 0.0.0-stage → 0.0.1

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 (279) hide show
  1. package/LICENSE +21 -0
  2. package/changelog.md +1 -0
  3. package/components/snake-activity-list/snake-activity-list.vue +365 -0
  4. package/components/snake-activity-list/types.ts +29 -0
  5. package/components/snake-auxiliary-segmentation/snake-auxiliary-segmentation.vue +44 -0
  6. package/components/snake-auxiliary-segmentation/types.ts +5 -0
  7. package/components/snake-button/snake-button.vue +357 -0
  8. package/components/snake-button/types.ts +103 -0
  9. package/components/snake-button-sort/snake-button-sort.vue +72 -0
  10. package/components/snake-button-sort/types.ts +22 -0
  11. package/components/snake-calendar/snake-calendar.vue +998 -0
  12. package/components/snake-calendar/types.ts +189 -0
  13. package/components/snake-cell-group/snake-cell-group.vue +55 -0
  14. package/components/snake-cell-group/types.ts +19 -0
  15. package/components/snake-cell-item/snake-cell-item.vue +159 -0
  16. package/components/snake-cell-item/types.ts +66 -0
  17. package/components/snake-checkbox/snake-checkbox.vue +264 -0
  18. package/components/snake-checkbox/types.ts +70 -0
  19. package/components/snake-checkbox-group/snake-checkbox-group.vue +154 -0
  20. package/components/snake-checkbox-group/types.ts +52 -0
  21. package/components/snake-cms/blocks/v2/cms-v2-carousel.vue +87 -0
  22. package/components/snake-cms/blocks/v2/cms-v2-coupon.vue +25 -0
  23. package/components/snake-cms/blocks/v2/cms-v2-custom.vue +12 -0
  24. package/components/snake-cms/blocks/v2/cms-v2-divider.vue +33 -0
  25. package/components/snake-cms/blocks/v2/cms-v2-floatbutton.vue +28 -0
  26. package/components/snake-cms/blocks/v2/cms-v2-goods-list.vue +150 -0
  27. package/components/snake-cms/blocks/v2/cms-v2-hotzone.vue +41 -0
  28. package/components/snake-cms/blocks/v2/cms-v2-images.vue +41 -0
  29. package/components/snake-cms/blocks/v2/cms-v2-magiccube.vue +93 -0
  30. package/components/snake-cms/blocks/v2/cms-v2-notice.vue +80 -0
  31. package/components/snake-cms/blocks/v2/cms-v2-popup.vue +46 -0
  32. package/components/snake-cms/blocks/v2/cms-v2-richtext.vue +18 -0
  33. package/components/snake-cms/blocks/v2/cms-v2-search.vue +115 -0
  34. package/components/snake-cms/blocks/v2/cms-v2-video.vue +25 -0
  35. package/components/snake-cms/blocks/v2/shared.ts +84 -0
  36. package/components/snake-cms/snake-cms.vue +409 -0
  37. package/components/snake-cms/types.ts +50 -0
  38. package/components/snake-commodity-search/snake-commodity-search.vue +234 -0
  39. package/components/snake-commodity-search/types.ts +11 -0
  40. package/components/snake-config-provider/snake-config-provider.vue +61 -0
  41. package/components/snake-config-provider/types.ts +22 -0
  42. package/components/snake-count-down/snake-count-down.vue +231 -0
  43. package/components/snake-count-down/types.ts +103 -0
  44. package/components/snake-coupon/snake-coupon.vue +209 -0
  45. package/components/snake-coupon/types.ts +21 -0
  46. package/components/snake-custom-module/snake-custom-module.vue +25 -0
  47. package/components/snake-custom-module/types.ts +11 -0
  48. package/components/snake-drop-menu/snake-drop-menu.vue +123 -0
  49. package/components/snake-drop-menu/types.ts +95 -0
  50. package/components/snake-drop-menu-item/snake-drop-menu-item.vue +181 -0
  51. package/components/snake-drop-menu-item/types.ts +48 -0
  52. package/components/snake-empty/snake-empty.vue +100 -0
  53. package/components/snake-empty/types.ts +48 -0
  54. package/components/snake-filter-sorts/filter-page.vue +695 -0
  55. package/components/snake-filter-sorts/snake-filter-sorts.vue +185 -0
  56. package/components/snake-filter-sorts/types.ts +57 -0
  57. package/components/snake-floating-bubble/snake-floating-bubble.vue +157 -0
  58. package/components/snake-floating-bubble/types.ts +43 -0
  59. package/components/snake-follow/snake-follow.vue +18 -0
  60. package/components/snake-follow/types.ts +2 -0
  61. package/components/snake-form/snake-form.vue +124 -0
  62. package/components/snake-form/types.ts +96 -0
  63. package/components/snake-form-item/snake-form-item.vue +331 -0
  64. package/components/snake-form-item/types.ts +70 -0
  65. package/components/snake-goods-list/snake-goods-list.vue +792 -0
  66. package/components/snake-goods-list/types.ts +55 -0
  67. package/components/snake-graphic-navigation/snake-graphic-navigation.vue +198 -0
  68. package/components/snake-graphic-navigation/types.ts +43 -0
  69. package/components/snake-icon/icon-names.ts +222 -0
  70. package/components/snake-icon/snake-icon.vue +220 -0
  71. package/components/snake-icon/types.ts +45 -0
  72. package/components/snake-image/snake-image.vue +230 -0
  73. package/components/snake-image/types.ts +70 -0
  74. package/components/snake-images/snake-images.vue +80 -0
  75. package/components/snake-images/types.ts +5 -0
  76. package/components/snake-index-anchor/snake-index-anchor.vue +83 -0
  77. package/components/snake-index-anchor/types.ts +14 -0
  78. package/components/snake-index-list/anchor-registry.ts +80 -0
  79. package/components/snake-index-list/snake-index-list.vue +259 -0
  80. package/components/snake-index-list/types.ts +115 -0
  81. package/components/snake-index-list-panel/snake-index-list-panel.vue +277 -0
  82. package/components/snake-index-list-panel/types.ts +10 -0
  83. package/components/snake-input/snake-input.vue +225 -0
  84. package/components/snake-input/types.ts +99 -0
  85. package/components/snake-jump-applet/snake-jump-applet.vue +99 -0
  86. package/components/snake-jump-applet/types.ts +17 -0
  87. package/components/snake-line/snake-line.vue +42 -0
  88. package/components/snake-line/types.ts +30 -0
  89. package/components/snake-line-progress/snake-line-progress.vue +105 -0
  90. package/components/snake-line-progress/types.ts +26 -0
  91. package/components/snake-link/snake-link.vue +74 -0
  92. package/components/snake-link/types.ts +30 -0
  93. package/components/snake-list-switching/components/tree-select/index.vue +156 -0
  94. package/components/snake-list-switching/snake-list-switching.vue +1285 -0
  95. package/components/snake-list-switching/types.ts +11 -0
  96. package/components/snake-loading/snake-loading.vue +241 -0
  97. package/components/snake-loading/types.ts +34 -0
  98. package/components/snake-loadmore/snake-loadmore.vue +103 -0
  99. package/components/snake-loadmore/types.ts +56 -0
  100. package/components/snake-magic-cube/snake-magic-cube.vue +622 -0
  101. package/components/snake-magic-cube/types.ts +5 -0
  102. package/components/snake-modal/snake-modal.vue +434 -0
  103. package/components/snake-modal/types.ts +71 -0
  104. package/components/snake-navbar/snake-navbar.vue +194 -0
  105. package/components/snake-navbar/types.ts +54 -0
  106. package/components/snake-no-network/snake-no-network.vue +125 -0
  107. package/components/snake-no-network/types.ts +27 -0
  108. package/components/snake-notice/snake-notice.vue +44 -0
  109. package/components/snake-notice/types.ts +5 -0
  110. package/components/snake-notice-bar/snake-notice-bar.vue +336 -0
  111. package/components/snake-notice-bar/types.ts +82 -0
  112. package/components/snake-overlay/snake-overlay.vue +73 -0
  113. package/components/snake-overlay/types.ts +35 -0
  114. package/components/snake-page-popup/snake-page-popup.vue +146 -0
  115. package/components/snake-page-popup/types.ts +28 -0
  116. package/components/snake-parse/audio/audio.vue +286 -0
  117. package/components/snake-parse/audio/context.js +11 -0
  118. package/components/snake-parse/audio/index.js +34 -0
  119. package/components/snake-parse/node/node.vue +830 -0
  120. package/components/snake-parse/parser.js +1480 -0
  121. package/components/snake-parse/snake-parse.vue +548 -0
  122. package/components/snake-parse/static/app-plus/snake-parse/js/handler.js +109 -0
  123. package/components/snake-parse/static/app-plus/snake-parse/js/uni.webview.js +0 -0
  124. package/components/snake-parse/static/app-plus/snake-parse/js/uni.webview.min.js +1 -0
  125. package/components/snake-parse/static/app-plus/snake-parse/local.html +3 -0
  126. package/components/snake-parse/style/index.js +132 -0
  127. package/components/snake-parse/style/parser.js +175 -0
  128. package/components/snake-parse/types.ts +49 -0
  129. package/components/snake-picture-ads/snake-picture-ads.vue +292 -0
  130. package/components/snake-picture-ads/types.ts +5 -0
  131. package/components/snake-popup/snake-popup.vue +348 -0
  132. package/components/snake-popup/types.ts +88 -0
  133. package/components/snake-qr-code/qrcode.ts +1214 -0
  134. package/components/snake-qr-code/snake-qr-code.vue +408 -0
  135. package/components/snake-qr-code/types.ts +78 -0
  136. package/components/snake-radio/snake-radio.vue +271 -0
  137. package/components/snake-radio/types.ts +64 -0
  138. package/components/snake-radio-group/snake-radio-group.vue +98 -0
  139. package/components/snake-radio-group/types.ts +48 -0
  140. package/components/snake-read-more/snake-read-more.vue +140 -0
  141. package/components/snake-read-more/types.ts +53 -0
  142. package/components/snake-rich-text/snake-rich-text.vue +24 -0
  143. package/components/snake-rich-text/types.ts +5 -0
  144. package/components/snake-root-portal/snake-root-portal.vue +92 -0
  145. package/components/snake-root-portal/types.ts +9 -0
  146. package/components/snake-search/snake-search.vue +202 -0
  147. package/components/snake-search/types.ts +84 -0
  148. package/components/snake-select/snake-select.vue +371 -0
  149. package/components/snake-select/types.ts +75 -0
  150. package/components/snake-skeleton/snake-skeleton.vue +104 -0
  151. package/components/snake-skeleton/types.ts +32 -0
  152. package/components/snake-skeleton-item/snake-skeleton-item.vue +90 -0
  153. package/components/snake-skeleton-item/types.ts +30 -0
  154. package/components/snake-slider/snake-slider.vue +249 -0
  155. package/components/snake-slider/types.ts +59 -0
  156. package/components/snake-sticky/snake-sticky.vue +178 -0
  157. package/components/snake-sticky/types.ts +48 -0
  158. package/components/snake-subsection/snake-subsection.vue +188 -0
  159. package/components/snake-subsection/types.ts +54 -0
  160. package/components/snake-swiper/snake-swiper.vue +229 -0
  161. package/components/snake-swiper/types.ts +79 -0
  162. package/components/snake-switch/snake-switch.vue +100 -0
  163. package/components/snake-switch/types.ts +41 -0
  164. package/components/snake-tabbar/snake-tabbar.vue +306 -0
  165. package/components/snake-tabbar/types.ts +93 -0
  166. package/components/snake-tabs/snake-tabs.vue +305 -0
  167. package/components/snake-tabs/types.ts +79 -0
  168. package/components/snake-text/snake-text.vue +292 -0
  169. package/components/snake-text/types.ts +100 -0
  170. package/components/snake-textarea/snake-textarea.vue +171 -0
  171. package/components/snake-textarea/types.ts +79 -0
  172. package/components/snake-toast/snake-toast.vue +330 -0
  173. package/components/snake-toast/types.ts +55 -0
  174. package/components/snake-tooltip/position.ts +33 -0
  175. package/components/snake-tooltip/snake-tooltip.vue +339 -0
  176. package/components/snake-tooltip/types.ts +80 -0
  177. package/components/snake-transition/snake-transition.vue +315 -0
  178. package/components/snake-transition/types.ts +65 -0
  179. package/components/snake-upload/snake-upload.vue +1801 -0
  180. package/components/snake-upload/types.ts +256 -0
  181. package/components/snake-videos/snake-videos.vue +122 -0
  182. package/components/snake-videos/types.ts +7 -0
  183. package/iconfont.css +881 -0
  184. package/index.d.ts +7 -0
  185. package/index.scss +26 -0
  186. package/index.ts +94 -0
  187. package/libs/cms/blocks.ts +103 -0
  188. package/libs/cms/config.ts +25 -0
  189. package/libs/cms/index.ts +7 -0
  190. package/libs/cms/normalize.ts +135 -0
  191. package/libs/cms/resolve.ts +12 -0
  192. package/libs/cms/types.ts +164 -0
  193. package/libs/cms/use-commerce.ts +385 -0
  194. package/libs/cms/v2.ts +193 -0
  195. package/libs/config/index.ts +4 -0
  196. package/libs/config/modal.ts +93 -0
  197. package/libs/config/state.ts +33 -0
  198. package/libs/config/theme.ts +351 -0
  199. package/libs/config/toast.ts +71 -0
  200. package/libs/config/types.ts +135 -0
  201. package/libs/config/zIndex.ts +44 -0
  202. package/libs/css/color.scss +105 -0
  203. package/libs/css/common.scss +171 -0
  204. package/libs/css/style.h5.scss +7 -0
  205. package/libs/css/style.mp.scss +7 -0
  206. package/libs/css/style.theme.scss +70 -0
  207. package/libs/css/style.vue.scss +140 -0
  208. package/libs/form/async-validator.min.d.ts +75 -0
  209. package/libs/form/async-validator.min.js +3 -0
  210. package/libs/form/index.ts +3 -0
  211. package/libs/form/path.ts +40 -0
  212. package/libs/form/types.ts +51 -0
  213. package/libs/form/validate.ts +149 -0
  214. package/libs/foundation.ts +96 -0
  215. package/libs/function/$parent.ts +33 -0
  216. package/libs/function/clipboard.ts +13 -0
  217. package/libs/function/color.ts +55 -0
  218. package/libs/function/debounce.ts +34 -0
  219. package/libs/function/deepClone.ts +30 -0
  220. package/libs/function/deepMerge.ts +25 -0
  221. package/libs/function/emitter.ts +30 -0
  222. package/libs/function/format.ts +27 -0
  223. package/libs/function/getParent.ts +53 -0
  224. package/libs/function/getRect.ts +29 -0
  225. package/libs/function/guid.ts +11 -0
  226. package/libs/function/index.ts +31 -0
  227. package/libs/function/queryParams.ts +73 -0
  228. package/libs/function/random.ts +6 -0
  229. package/libs/function/randomArray.ts +14 -0
  230. package/libs/function/route.ts +155 -0
  231. package/libs/function/sleep.ts +7 -0
  232. package/libs/function/style.ts +58 -0
  233. package/libs/function/styleUtils.ts +83 -0
  234. package/libs/function/system.ts +14 -0
  235. package/libs/function/test.ts +202 -0
  236. package/libs/function/throttle.ts +36 -0
  237. package/libs/function/timeFormat.ts +27 -0
  238. package/libs/function/timeFrom.ts +27 -0
  239. package/libs/function/toast.ts +10 -0
  240. package/libs/function/trim.ts +11 -0
  241. package/libs/function/type2icon.ts +11 -0
  242. package/libs/function/unit.ts +45 -0
  243. package/libs/hooks/index.ts +11 -0
  244. package/libs/hooks/useChildren.ts +125 -0
  245. package/libs/hooks/useColor.ts +5 -0
  246. package/libs/hooks/useDebounce.ts +5 -0
  247. package/libs/hooks/useLocale.ts +13 -0
  248. package/libs/hooks/useModal.ts +131 -0
  249. package/libs/hooks/useParent.ts +62 -0
  250. package/libs/hooks/useRect.ts +44 -0
  251. package/libs/hooks/useRouter.ts +5 -0
  252. package/libs/hooks/useTheme.ts +31 -0
  253. package/libs/hooks/useThrottle.ts +5 -0
  254. package/libs/hooks/useToast.ts +116 -0
  255. package/libs/index.ts +18 -0
  256. package/libs/request/auto-http.ts +1 -0
  257. package/libs/request/index.ts +343 -0
  258. package/libs/runtime/accessors.ts +55 -0
  259. package/libs/runtime/context.ts +40 -0
  260. package/libs/runtime/index.ts +18 -0
  261. package/libs/runtime/types.ts +49 -0
  262. package/libs/util/calendar.ts +6 -0
  263. package/locale/index.ts +130 -0
  264. package/locale/lang/en-US.ts +67 -0
  265. package/locale/lang/zh-CN.ts +67 -0
  266. package/package.json +130 -5
  267. package/plugins/index.ts +1 -0
  268. package/plugins/root/README.md +86 -0
  269. package/plugins/root/index.ts +123 -0
  270. package/plugins/root/page.ts +77 -0
  271. package/plugins/root/root.ts +53 -0
  272. package/plugins/root/utils.ts +137 -0
  273. package/readme.md +79 -0
  274. package/theme.scss +118 -0
  275. package/types/components.d.ts +236 -0
  276. package/types/global.d.ts +21 -0
  277. package/types/index.d.ts +6 -0
  278. package/types/uni-app.d.ts +16 -0
  279. package/README.md +0 -3
@@ -0,0 +1,83 @@
1
+ <template>
2
+ <view class="snake-index-anchor__wrapper" :style="wrapperStyle">
3
+ <view :id="anchorId" class="snake-index-anchor" :class="[customClass, { 'is-active': active }]" :style="style">
4
+ <slot v-if="useSlot" />
5
+ <text v-else>{{ text || index }}</text>
6
+ </view>
7
+ </view>
8
+ </template>
9
+ <script setup lang="ts">
10
+ /**
11
+ * @component IndexAnchor
12
+ * @description 定义 `IndexList` 中的分组位置,并根据父组件配置提供吸顶效果。
13
+ * @see /components/index-anchor
14
+ */
15
+
16
+ import { computed, getCurrentInstance, inject, onBeforeUnmount, onMounted, watch } from 'vue';
17
+ import { indexListKey } from '../snake-index-list/types';
18
+ import type { IndexAnchorProps } from './types';
19
+ defineOptions({ name: 'IndexAnchor', options: { virtualHost: true, styleIsolation: 'shared' } });
20
+ const props = withDefaults(defineProps<IndexAnchorProps>(), {
21
+ text: '',
22
+ useSlot: false,
23
+ customClass: '',
24
+ customStyle: ''
25
+ });
26
+ const instance = getCurrentInstance();
27
+ const list = inject(indexListKey);
28
+ const anchorId = computed(() => list?.anchorId(props.index) ?? `snake-index-anchor-${props.index}`);
29
+ const active = computed(() => list?.isActive(props.index) ?? false);
30
+ const wrapperStyle = computed(() => list?.wrapperStyle(props.index) ?? {});
31
+ const style = computed(() => [list?.anchorStyle(props.index) ?? {}, props.customStyle]);
32
+
33
+ // 微信端选择器查询不能跨自定义组件边界,锚点需在自身作用域内测量并回报父级。
34
+ onMounted(() => {
35
+ list?.registerAnchor(props.index);
36
+ measure();
37
+ });
38
+ onBeforeUnmount(() => list?.unregisterAnchor(props.index));
39
+ watch(
40
+ () => props.index,
41
+ (value, previous) => {
42
+ if (previous !== undefined && previous !== value) list?.unregisterAnchor(previous);
43
+ list?.registerAnchor(value);
44
+ measure();
45
+ }
46
+ );
47
+ watch(
48
+ () => list?.measureToken.value,
49
+ () => measure()
50
+ );
51
+
52
+ /** 测量自身节点位置并回报父级:查询限定在自身模板内,跨端可靠。 */
53
+ function measure() {
54
+ if (typeof uni === 'undefined' || !instance) return;
55
+ uni.createSelectorQuery()
56
+ .in(instance?.proxy as any)
57
+ .select('.snake-index-anchor__wrapper')
58
+ .boundingClientRect()
59
+ .exec((results: any[]) => {
60
+ const rect = results?.[0];
61
+ if (rect) {
62
+ list?.reportAnchor(props.index, { top: Number(rect.top) || 0, height: Number(rect.height) || 0 });
63
+ }
64
+ });
65
+ }
66
+ </script>
67
+ <style scoped>
68
+ .snake-index-anchor__wrapper {
69
+ position: relative;
70
+ }
71
+ .snake-index-anchor {
72
+ display: flex;
73
+ box-sizing: border-box;
74
+ width: 100%;
75
+ min-height: 64rpx;
76
+ align-items: center;
77
+ padding: 0 20rpx;
78
+ color: var(--snake-color-text, #303133);
79
+ font-size: 24rpx;
80
+ font-weight: 600;
81
+ background: var(--snake-color-background, #f5f6f8);
82
+ }
83
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { StyleValue } from '../../types/global';
2
+ /** IndexAnchor 组件的公开属性。 */
3
+ export interface IndexAnchorProps {
4
+ /** 当前数据项索引。 */
5
+ index: string | number;
6
+ /** 展示文本。 */
7
+ text?: string;
8
+ /** 是否使用默认插槽替代文本。 */
9
+ useSlot?: boolean;
10
+ /** 根节点自定义类名。 */
11
+ customClass?: string;
12
+ /** 根节点自定义样式。 */
13
+ customStyle?: StyleValue;
14
+ }
@@ -0,0 +1,80 @@
1
+ import { ref, type Ref } from 'vue';
2
+ import type { IndexAnchorRect, IndexValue } from './types';
3
+
4
+ /** 自测回报的兜底等待时长;超时后按已回报数据聚合,避免死等。 */
5
+ const COLLECT_TIMEOUT = 200;
6
+
7
+ /**
8
+ * 锚点登记与自测回报的共享实现。
9
+ *
10
+ * 微信小程序的选择器查询不能跨自定义组件边界:锚点节点位于
11
+ * `snake-index-anchor` 自身模板内、又经由插槽渲染,列表侧
12
+ * `createSelectorQuery().in(list)` 永远查不到。因此改为锚点在自身
13
+ * 作用域内测量并回报,列表 / 面板在 collect() 中聚合。
14
+ */
15
+ export function createAnchorRegistry() {
16
+ /** 自测轮次号:锚点监听到变化后重新测量。 */
17
+ const measureToken = ref(0);
18
+ /** 各锚点最近一次回报的几何信息(非响应式,聚合时机由 collect 控制)。 */
19
+ const rects = new Map<IndexValue, IndexAnchorRect>();
20
+ /** 已挂载锚点集合,collect 按其数量等待回报。 */
21
+ const registered = new Set<IndexValue>();
22
+ let waiter: { expected: number; received: Set<IndexValue>; done: boolean; resolve: () => void } | null = null;
23
+ let waiterTimer: ReturnType<typeof setTimeout> | undefined;
24
+
25
+ function registerAnchor(value: IndexValue) {
26
+ registered.add(value);
27
+ }
28
+
29
+ function unregisterAnchor(value: IndexValue) {
30
+ registered.delete(value);
31
+ rects.delete(value);
32
+ }
33
+
34
+ function reportAnchor(value: IndexValue, rect: IndexAnchorRect) {
35
+ rects.set(value, rect);
36
+ if (waiter && !waiter.done && registered.has(value) && !waiter.received.has(value)) {
37
+ waiter.received.add(value);
38
+ if (waiter.received.size >= waiter.expected) settle();
39
+ }
40
+ }
41
+
42
+ function settle() {
43
+ if (!waiter || waiter.done) return;
44
+ waiter.done = true;
45
+ if (waiterTimer) clearTimeout(waiterTimer);
46
+ const resolve = waiter.resolve;
47
+ waiter = null;
48
+ resolve();
49
+ }
50
+
51
+ /**
52
+ * 触发一轮锚点自测并等待全部已登记锚点回报。
53
+ * 无锚点或超时(200ms)时立即返回,调用方按现有数据聚合。
54
+ */
55
+ function collect(): Promise<void> {
56
+ if (waiter) settle();
57
+ const expected = registered.size;
58
+ if (!expected) return Promise.resolve();
59
+ measureToken.value += 1;
60
+ return new Promise(resolve => {
61
+ waiter = { expected, received: new Set(), done: false, resolve };
62
+ waiterTimer = setTimeout(settle, COLLECT_TIMEOUT);
63
+ });
64
+ }
65
+
66
+ return {
67
+ measureToken,
68
+ rects,
69
+ registerAnchor,
70
+ unregisterAnchor,
71
+ reportAnchor,
72
+ collect
73
+ };
74
+ }
75
+
76
+ /** createAnchorRegistry 返回的锚点登记器。 */
77
+ export interface AnchorRegistry extends ReturnType<typeof createAnchorRegistry> {
78
+ /** 自测轮次号(响应式,供锚点监听)。 */
79
+ measureToken: Ref<number>;
80
+ }
@@ -0,0 +1,259 @@
1
+ <template>
2
+ <view class="snake-index-list" :class="customClass" :style="customStyle">
3
+ <slot />
4
+ <view
5
+ v-if="indexList.length"
6
+ class="snake-index-list__rail"
7
+ :style="[{ zIndex: props.zIndex }, railStyle]"
8
+ aria-label="索引导航"
9
+ @touchstart.stop.prevent="touchSelect"
10
+ @touchmove.stop.prevent="touchSelect"
11
+ @touchend.stop.prevent="touching = false"
12
+ @touchcancel.stop.prevent="touching = false"
13
+ >
14
+ <view
15
+ v-for="(item, index) in indexList"
16
+ :key="`${item}-${index}`"
17
+ class="snake-index-list__key"
18
+ :class="{ 'is-active': activeIndex === index }"
19
+ :style="{ color: activeIndex === index ? activeColor : inactiveColor }"
20
+ @click="scrollTo(index)"
21
+ >
22
+ {{ item }}
23
+ </view>
24
+ </view>
25
+ <view
26
+ v-if="indexList.length"
27
+ class="snake-index-list__bubble"
28
+ :class="{ 'is-visible': touching }"
29
+ :style="{ zIndex: props.zIndex + 1 }"
30
+ >
31
+ {{ activeIndex >= 0 ? indexList[activeIndex] : '' }}
32
+ </view>
33
+ </view>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ /**
38
+ * @component IndexList
39
+ * @description 跟随页面滚动的索引列表,通过右侧索引快速定位分组内容,并与 IndexAnchor 配合实现吸顶。
40
+ * @see /components/index-list
41
+ */
42
+
43
+ import { computed, getCurrentInstance, nextTick, onMounted, provide, ref, toRef, watch, type CSSProperties } from 'vue';
44
+ import zIndex from '../../libs/config/zIndex';
45
+ import { createAnchorRegistry } from './anchor-registry';
46
+ import { indexFromTouch, offsetTopPx, resolveIndex, indexListKey } from './types';
47
+ import type { IndexListEmits, IndexListProps, IndexValue } from './types';
48
+
49
+ defineOptions({ name: 'IndexList', options: { virtualHost: true, styleIsolation: 'shared' } });
50
+
51
+ const props = withDefaults(defineProps<IndexListProps>(), {
52
+ indexList: () => [],
53
+ scrollTop: 0,
54
+ zIndex: zIndex.indexListSticky,
55
+ sticky: true,
56
+ offsetTop: 0,
57
+ activeColor: 'var(--snake-color-primary, #06D290)',
58
+ inactiveColor: 'var(--snake-color-text-muted, #909399)',
59
+ railStyle: '',
60
+ customClass: '',
61
+ customStyle: ''
62
+ });
63
+ const emit = defineEmits<IndexListEmits>();
64
+ const instance = getCurrentInstance();
65
+ const activeIndex = ref(-1);
66
+ const touching = ref(false);
67
+ const railTop = ref(0);
68
+ const railHeight = ref(0);
69
+ const listLeft = ref(0);
70
+ const listWidth = ref(0);
71
+ // 锚点位置由锚点组件自测回报(微信端父级查询跨不过自定义组件边界)。
72
+ const registry = createAnchorRegistry();
73
+ const anchorTops = ref<number[]>([]);
74
+ const anchorHeights = ref<number[]>([]);
75
+ const currentScrollTop = computed(() => Number(props.scrollTop) || 0);
76
+ const currentOffsetTop = computed(() => offsetTopPx(props.offsetTop));
77
+ const anchorId = (value: IndexValue) => `snake-index-anchor-${String(value).replace(/[^a-zA-Z0-9_-]/g, '-')}`;
78
+
79
+ function setActive(index: number, selected = false) {
80
+ if (!props.indexList.length || index < 0 || index >= props.indexList.length) return;
81
+ if (index !== activeIndex.value) {
82
+ activeIndex.value = index;
83
+ emit('change', { index, value: props.indexList[index] });
84
+ }
85
+ if (selected) emit('select', props.indexList[index]);
86
+ }
87
+
88
+ function updateActive() {
89
+ if (!anchorTops.value.length) {
90
+ activeIndex.value = -1;
91
+ return;
92
+ }
93
+ const target = currentScrollTop.value + currentOffsetTop.value;
94
+ let index = -1;
95
+ anchorTops.value.forEach((top, position) => {
96
+ if (top <= target + 1) index = position;
97
+ });
98
+ if (index >= 0) setActive(index);
99
+ else activeIndex.value = -1;
100
+ }
101
+
102
+ function scrollTo(indexOrValue: number | IndexValue) {
103
+ const index = resolveIndex(indexOrValue, props.indexList);
104
+ if (index < 0) return;
105
+ setActive(index, true);
106
+ const top = anchorTops.value[index];
107
+ if (Number.isFinite(top)) {
108
+ uni.pageScrollTo({ duration: 0, scrollTop: Math.max(0, top - currentOffsetTop.value) });
109
+ return;
110
+ }
111
+ // 兜底:锚点尚未回报时按锚点 id 定位(微信 2.23.1+ / H5 / App 支持 selector)。
112
+ uni.pageScrollTo({ duration: 0, selector: `#${anchorId(props.indexList[index])}` } as UniApp.PageScrollToOptions);
113
+ }
114
+
115
+ function touchSelect(event: any) {
116
+ touching.value = true;
117
+ const y = Number(event.touches?.[0]?.clientY ?? event.changedTouches?.[0]?.clientY);
118
+ const index = indexFromTouch(y, railTop.value, railHeight.value, props.indexList.length);
119
+ if (index >= 0) scrollTo(index);
120
+ }
121
+
122
+ async function update() {
123
+ if (typeof uni === 'undefined') return;
124
+ await nextTick();
125
+ await new Promise<void>(resolve => {
126
+ const query = uni.createSelectorQuery().in(instance?.proxy as any);
127
+ query.select('.snake-index-list').boundingClientRect();
128
+ query.select('.snake-index-list__rail').boundingClientRect();
129
+ query.exec((results: any[]) => {
130
+ const list = results?.[0];
131
+ const rail = results?.[1];
132
+ if (list) {
133
+ listLeft.value = Number(list.left) || 0;
134
+ listWidth.value = Number(list.width) || 0;
135
+ }
136
+ if (rail) {
137
+ railTop.value = Number(rail.top) || 0;
138
+ railHeight.value = Number(rail.height) || 0;
139
+ }
140
+ resolve();
141
+ });
142
+ });
143
+ await registry.collect();
144
+ const tops: number[] = [];
145
+ const heights: number[] = [];
146
+ props.indexList.forEach(value => {
147
+ const rect = registry.rects.get(value);
148
+ // 未回报的锚点以 Infinity 占位,避免被滚动激活逻辑命中。
149
+ tops.push(rect ? rect.top + currentScrollTop.value : Number.POSITIVE_INFINITY);
150
+ heights.push(rect?.height || 0);
151
+ });
152
+ anchorTops.value = tops;
153
+ anchorHeights.value = heights;
154
+ updateActive();
155
+ }
156
+
157
+ function indexOfAnchor(value: IndexValue) {
158
+ return props.indexList.indexOf(value);
159
+ }
160
+
161
+ function anchorStyle(value: IndexValue): CSSProperties {
162
+ const index = indexOfAnchor(value);
163
+ if (!props.sticky || index < 0 || index !== activeIndex.value) return {};
164
+ const height = anchorHeights.value[index] || 0;
165
+ const nextTop = anchorTops.value[index + 1];
166
+ const nextViewportTop = nextTop === undefined ? Number.POSITIVE_INFINITY : nextTop - currentScrollTop.value;
167
+ const translateY = Math.min(0, nextViewportTop - currentOffsetTop.value - height);
168
+ return {
169
+ position: 'fixed',
170
+ top: `${currentOffsetTop.value}px`,
171
+ left: `${listLeft.value}px`,
172
+ width: `${listWidth.value}px`,
173
+ zIndex: props.zIndex,
174
+ color: props.activeColor,
175
+ transform: `translate3d(0, ${translateY}px, 0)`
176
+ };
177
+ }
178
+
179
+ function wrapperStyle(value: IndexValue): CSSProperties {
180
+ const index = indexOfAnchor(value);
181
+ return props.sticky && index === activeIndex.value ? { height: `${anchorHeights.value[index] || 0}px` } : {};
182
+ }
183
+
184
+ provide(indexListKey, {
185
+ activeIndex,
186
+ sticky: toRef(props, 'sticky'),
187
+ offsetTop: toRef(props, 'offsetTop'),
188
+ zIndex: toRef(props, 'zIndex'),
189
+ anchorId,
190
+ anchorStyle,
191
+ wrapperStyle,
192
+ isActive: value => indexOfAnchor(value) === activeIndex.value,
193
+ measureToken: registry.measureToken,
194
+ registerAnchor: registry.registerAnchor,
195
+ unregisterAnchor: registry.unregisterAnchor,
196
+ reportAnchor: registry.reportAnchor
197
+ });
198
+
199
+ watch(() => props.scrollTop, updateActive);
200
+ watch(() => props.offsetTop, updateActive);
201
+ watch(
202
+ () => props.indexList,
203
+ () => void update(),
204
+ { deep: true }
205
+ );
206
+ onMounted(() => void update());
207
+ defineExpose({ scrollTo, update });
208
+ </script>
209
+
210
+ <style scoped>
211
+ .snake-index-list {
212
+ position: relative;
213
+ box-sizing: border-box;
214
+ }
215
+ .snake-index-list__rail {
216
+ position: fixed;
217
+ top: 50%;
218
+ right: 10rpx;
219
+ display: flex;
220
+ flex-direction: column;
221
+ padding: 8rpx 0;
222
+ border-radius: 999px;
223
+ background: rgba(255, 255, 255, 0.86);
224
+ transform: translateY(-50%);
225
+ user-select: none;
226
+ }
227
+ .snake-index-list__key {
228
+ display: flex;
229
+ min-width: 44rpx;
230
+ min-height: 34rpx;
231
+ align-items: center;
232
+ justify-content: center;
233
+ border-radius: 50%;
234
+ font-size: 20rpx;
235
+ line-height: 34rpx;
236
+ }
237
+ .snake-index-list__key.is-active {
238
+ font-weight: 700;
239
+ background: var(--snake-color-primary-light, rgba(6, 210, 144, 0.1));
240
+ }
241
+ .snake-index-list__bubble {
242
+ position: fixed;
243
+ top: 50%;
244
+ right: 90rpx;
245
+ display: none;
246
+ width: 96rpx;
247
+ height: 96rpx;
248
+ align-items: center;
249
+ justify-content: center;
250
+ border-radius: 16rpx;
251
+ color: #fff;
252
+ font-size: 42rpx;
253
+ background: rgba(0, 0, 0, 0.58);
254
+ transform: translateY(-50%);
255
+ }
256
+ .snake-index-list__bubble.is-visible {
257
+ display: flex;
258
+ }
259
+ </style>
@@ -0,0 +1,115 @@
1
+ import type { CSSProperties, InjectionKey, Ref } from 'vue';
2
+ import type { UnitValue } from '../../libs/function/unit';
3
+ import type { StyleValue } from '../../types/global';
4
+ /** IndexValue 类型定义。 */
5
+ export type IndexValue = string | number;
6
+
7
+ /** 锚点自测的几何信息(视口坐标系)。 */
8
+ export interface IndexAnchorRect {
9
+ /** 视口顶部到锚点顶部的距离,单位 px。 */
10
+ top: number;
11
+ /** 锚点高度,单位 px。 */
12
+ height: number;
13
+ }
14
+ /** IndexList 组件的公开属性。 */
15
+ export interface IndexListProps {
16
+ /** index list 配置。 */
17
+ indexList?: IndexValue[];
18
+ /** 页面当前滚动距离,由页面的 onPageScroll 事件传入。 */
19
+ scrollTop?: number | string;
20
+ /** 组件层级。 */
21
+ zIndex?: number;
22
+ /** sticky 配置。 */
23
+ sticky?: boolean;
24
+ /** 吸顶或定位的顶部偏移量。 */
25
+ offsetTop?: UnitValue;
26
+ /** 激活状态颜色。 */
27
+ activeColor?: string;
28
+ /** 未激活状态颜色。 */
29
+ inactiveColor?: string;
30
+ /** rail style 配置。 */
31
+ railStyle?: StyleValue;
32
+ /** 根节点自定义类名。 */
33
+ customClass?: string;
34
+ /** 根节点自定义样式。 */
35
+ customStyle?: StyleValue;
36
+ }
37
+ /** IndexListChange 事件详情。 */
38
+ export interface IndexListChangeDetail {
39
+ /** 当前数据项索引。 */
40
+ index: number;
41
+ /** 当前值。 */
42
+ value: IndexValue;
43
+ }
44
+ /** IndexList 组件的事件契约。 */
45
+ export type IndexListEmits = {
46
+ /** select 事件携带的参数。 */
47
+ select: [value: IndexValue];
48
+ /** change 事件携带的参数。 */
49
+ change: [detail: IndexListChangeDetail];
50
+ };
51
+ /** IndexList 组件实例公开的方法和状态。 */
52
+ export interface IndexListExpose {
53
+ /** scrollTo 方法。 */
54
+ scrollTo(index: number | IndexValue): void;
55
+ /** update 方法。 */
56
+ update(): Promise<void>;
57
+ }
58
+
59
+ /** IndexList 向 Anchor / Panel 注入的配对上下文。 */
60
+ export interface IndexListContext {
61
+ /** 当前激活项下标。 */
62
+ activeIndex: Ref<number>;
63
+ /** 锚点是否吸顶。 */
64
+ sticky: Ref<boolean>;
65
+ /** 吸顶或定位的顶部偏移量。 */
66
+ offsetTop: Ref<string | number | null | undefined>;
67
+ /** 组件层级。 */
68
+ zIndex: Ref<number>;
69
+ /** 生成锚点节点 id。 */
70
+ anchorId(index: IndexValue): string;
71
+ /** 生成锚点样式。 */
72
+ anchorStyle(index: IndexValue): CSSProperties;
73
+ /** 生成分组容器样式。 */
74
+ wrapperStyle(index: IndexValue): CSSProperties;
75
+ /** 判断指定项是否激活。 */
76
+ isActive(index: IndexValue): boolean;
77
+ /** 自测轮次号:变化时锚点需重新测量并回报。 */
78
+ measureToken: Ref<number>;
79
+ /** 登记已挂载的锚点。 */
80
+ registerAnchor(value: IndexValue): void;
81
+ /** 注销锚点。 */
82
+ unregisterAnchor(value: IndexValue): void;
83
+ /** 锚点回报自身几何信息。 */
84
+ reportAnchor(value: IndexValue, rect: IndexAnchorRect): void;
85
+ }
86
+
87
+ /** IndexList ↔ Anchor / Panel 配对注入键。 */
88
+ export const indexListKey: InjectionKey<IndexListContext> = Symbol('snake-index-list');
89
+
90
+ /** 索引或取值解析为下标:合法整数下标原样返回,否则按值查找。 */
91
+ export function resolveIndex(indexOrValue: number | IndexValue, indexList: IndexValue[]): number {
92
+ if (
93
+ typeof indexOrValue === 'number' &&
94
+ Number.isInteger(indexOrValue) &&
95
+ indexOrValue >= 0 &&
96
+ indexOrValue < indexList.length
97
+ ) {
98
+ return indexOrValue;
99
+ }
100
+ return indexList.indexOf(indexOrValue);
101
+ }
102
+
103
+ /** 由触摸纵坐标换算最近的数据项下标,越界自动收敛。 */
104
+ export function indexFromTouch(clientY: number, railTop: number, railHeight: number, length: number): number {
105
+ if (!Number.isFinite(clientY) || railHeight <= 0 || length <= 0) return -1;
106
+ return Math.max(0, Math.min(length - 1, Math.floor(((clientY - railTop) / railHeight) * length)));
107
+ }
108
+
109
+ /** offsetTop 值转 px 数值:rpx / 裸数字按 rpx 换算。 */
110
+ export function offsetTopPx(value: string | number | null | undefined): number {
111
+ const number = Number.parseFloat(String(value ?? 0));
112
+ if (!Number.isFinite(number)) return 0;
113
+ if (typeof value === 'string' && value.trim().endsWith('px')) return number;
114
+ return typeof uni !== 'undefined' && typeof uni.upx2px === 'function' ? uni.upx2px(number) : number;
115
+ }