@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,30 @@
1
+ import type { ComputedRef, InjectionKey } from 'vue';
2
+ import type { StyleValue } from '../../types/global';
3
+
4
+ /** SkeletonItem 的占位形态。 */
5
+ export type SkeletonItemVariant = 'text' | 'rect' | 'circle' | 'image';
6
+
7
+ /** SkeletonItem 组件的公开属性。 */
8
+ export interface SkeletonItemProps {
9
+ /** 占位形态。 */
10
+ variant?: SkeletonItemVariant;
11
+ /** 占位宽度。 */
12
+ width?: number | string;
13
+ /** 占位高度。 */
14
+ height?: number | string;
15
+ /** 圆角尺寸。 */
16
+ radius?: number | string;
17
+ /** 是否启用流光动画;未传时继承 Skeleton 容器。 */
18
+ animated?: boolean;
19
+ /** 根节点自定义样式。 */
20
+ customStyle?: StyleValue;
21
+ }
22
+
23
+ /** Skeleton 向占位单元提供的上下文。 */
24
+ export interface SkeletonContext {
25
+ /** 容器当前是否启用动画。 */
26
+ animated: ComputedRef<boolean>;
27
+ }
28
+
29
+ /** Skeleton 上下文注入键。 */
30
+ export const SNAKE_SKELETON_CONTEXT: InjectionKey<SkeletonContext> = Symbol('snake-skeleton-context');
@@ -0,0 +1,249 @@
1
+ <template>
2
+ <view class="snake-slider" :class="[customClass, { 'is-disabled': disabled }]" :style="rootStyle">
3
+ <text v-if="showEdgeValue" class="snake-slider__edge">{{ minValue }}</text>
4
+ <view class="snake-slider__main">
5
+ <view
6
+ class="snake-slider__track"
7
+ :style="{ height: addUnit(height, 'rpx'), backgroundColor: inactiveColor }"
8
+ role="slider"
9
+ :aria-valuemin="minValue"
10
+ :aria-valuemax="maxValue"
11
+ :aria-valuenow="current"
12
+ :aria-disabled="disabled"
13
+ :tabindex="disabled ? -1 : 0"
14
+ @click="tap"
15
+ @keydown.left.prevent="adjust(-1)"
16
+ @keydown.down.prevent="adjust(-1)"
17
+ @keydown.right.prevent="adjust(1)"
18
+ @keydown.up.prevent="adjust(1)"
19
+ @touchstart.stop.prevent="start"
20
+ @touchmove.stop.prevent="move"
21
+ @touchend.stop.prevent="end"
22
+ @touchcancel.stop.prevent="end"
23
+ >
24
+ <view class="snake-slider__active" :style="activeStyle" />
25
+ <view class="snake-slider__block" :style="blockStyleValue">
26
+ <slot v-if="useSlot || $slots.default" :value="current" />
27
+ </view>
28
+ <text
29
+ v-if="showValue"
30
+ class="snake-slider__value"
31
+ :class="`is-${valuePosition}`"
32
+ :style="{ left: `${percent}%` }"
33
+ >
34
+ {{ current }}
35
+ </text>
36
+ </view>
37
+ </view>
38
+ <text v-if="showEdgeValue" class="snake-slider__edge">{{ maxValue }}</text>
39
+ </view>
40
+ </template>
41
+ <script setup lang="ts">
42
+ /**
43
+ * @component Slider
44
+ * @description 通过轨道点击或触摸拖动选择数值,支持步长、数值气泡、边界值与自定义滑块。
45
+ * @see /components/slider
46
+ */
47
+
48
+ import { computed, getCurrentInstance, inject, onMounted, ref, watch } from 'vue';
49
+ import { addUnit } from '../../libs/function/unit';
50
+ import { formItemKey } from '../snake-form-item/types';
51
+ import type { SliderEmits, SliderExpose, SliderProps } from './types';
52
+ defineOptions({ name: 'Slider', options: { virtualHost: true, styleIsolation: 'shared' } });
53
+ const props = withDefaults(defineProps<SliderProps>(), {
54
+ modelValue: 0,
55
+ min: 0,
56
+ max: 100,
57
+ step: 1,
58
+ height: 6,
59
+ blockWidth: 30,
60
+ activeColor: 'var(--snake-color-primary,#06D290)',
61
+ inactiveColor: 'var(--snake-color-divider,#e4e7ed)',
62
+ blockColor: '#fff',
63
+ blockStyle: '',
64
+ disabled: false,
65
+ useSlot: false,
66
+ showValue: false,
67
+ valuePosition: 'top',
68
+ showEdgeValue: false,
69
+ customClass: '',
70
+ customStyle: ''
71
+ });
72
+ const emit = defineEmits<SliderEmits>();
73
+ const formItem = inject(formItemKey, null);
74
+ const instance = getCurrentInstance();
75
+ const trackLeft = ref(0);
76
+ const trackWidth = ref(0);
77
+ const dragging = ref(false);
78
+ const minValue = computed(() => Number(props.min) || 0);
79
+ const maxValue = computed(() => Math.max(minValue.value, Number(props.max) || 0));
80
+ const stepValue = computed(() => Math.max(Number(props.step) || 1, Number.EPSILON));
81
+ function decimals(value: number) {
82
+ const text = String(value);
83
+ return text.includes('.') ? text.split('.')[1].length : 0;
84
+ }
85
+ function normalize(raw: unknown) {
86
+ const number = Number(raw);
87
+ const safe = Number.isFinite(number) ? number : minValue.value;
88
+ const stepped = minValue.value + Math.round((safe - minValue.value) / stepValue.value) * stepValue.value;
89
+ return Number(
90
+ Math.max(minValue.value, Math.min(maxValue.value, stepped)).toFixed(
91
+ Math.max(decimals(stepValue.value), decimals(minValue.value))
92
+ )
93
+ );
94
+ }
95
+ const current = ref(normalize(props.modelValue));
96
+ watch(
97
+ () => props.modelValue,
98
+ value => {
99
+ if (!dragging.value) current.value = normalize(value);
100
+ }
101
+ );
102
+ const percent = computed(() =>
103
+ maxValue.value === minValue.value ? 0 : ((current.value - minValue.value) / (maxValue.value - minValue.value)) * 100
104
+ );
105
+ const rootStyle = computed(() => [
106
+ { padding: `${addUnit((Number.parseFloat(String(props.blockWidth)) || 30) / 2, 'rpx')} 0` },
107
+ props.customStyle
108
+ ]);
109
+ const activeStyle = computed(() => ({ width: `${percent.value}%`, backgroundColor: props.activeColor }));
110
+ const blockStyleValue = computed(() => [
111
+ {
112
+ left: `${percent.value}%`,
113
+ width: addUnit(props.blockWidth, 'rpx'),
114
+ height: addUnit(props.blockWidth, 'rpx'),
115
+ backgroundColor: props.blockColor
116
+ },
117
+ props.blockStyle
118
+ ]);
119
+ async function update() {
120
+ if (typeof uni === 'undefined') return;
121
+ await new Promise<void>(resolve => {
122
+ uni.createSelectorQuery()
123
+ .in(instance?.proxy as any)
124
+ .select('.snake-slider__track')
125
+ .boundingClientRect((rect: any) => {
126
+ if (rect) {
127
+ trackLeft.value = Number(rect.left) || 0;
128
+ trackWidth.value = Number(rect.width) || 0;
129
+ }
130
+ })
131
+ .exec(() => resolve());
132
+ });
133
+ }
134
+ function clientX(event: any) {
135
+ return Number(
136
+ event.touches?.[0]?.clientX ?? event.changedTouches?.[0]?.clientX ?? event.detail?.x ?? event.clientX
137
+ );
138
+ }
139
+ function valueFromEvent(event: any) {
140
+ const x = clientX(event);
141
+ if (!Number.isFinite(x) || trackWidth.value <= 0) return current.value;
142
+ return normalize(minValue.value + ((x - trackLeft.value) / trackWidth.value) * (maxValue.value - minValue.value));
143
+ }
144
+ function setFromEvent(event: any, moving = false) {
145
+ const next = valueFromEvent(event);
146
+ if (next === current.value) return;
147
+ current.value = next;
148
+ emit('update:modelValue', next);
149
+ if (moving) emit('moving', next);
150
+ }
151
+ async function start(event: any) {
152
+ if (props.disabled) return;
153
+ if (!trackWidth.value) await update();
154
+ dragging.value = true;
155
+ setFromEvent(event);
156
+ emit('start', current.value);
157
+ }
158
+ function move(event: any) {
159
+ if (!dragging.value || props.disabled) return;
160
+ setFromEvent(event, true);
161
+ }
162
+ function end(event: any) {
163
+ if (!dragging.value || props.disabled) return;
164
+ setFromEvent(event);
165
+ dragging.value = false;
166
+ emit('end', current.value);
167
+ emit('change', current.value);
168
+ void formItem?.notify('change');
169
+ }
170
+ async function tap(event: any) {
171
+ if (props.disabled || dragging.value) return;
172
+ if (!trackWidth.value) await update();
173
+ setFromEvent(event);
174
+ emit('change', current.value);
175
+ void formItem?.notify('change');
176
+ }
177
+ function adjust(direction: -1 | 1) {
178
+ if (props.disabled) return;
179
+ const next = normalize(current.value + stepValue.value * direction);
180
+ if (next === current.value) return;
181
+ current.value = next;
182
+ emit('update:modelValue', next);
183
+ emit('change', next);
184
+ void formItem?.notify('change');
185
+ }
186
+ onMounted(() => void update());
187
+ defineExpose<SliderExpose>({ update });
188
+ </script>
189
+ <style scoped>
190
+ .snake-slider {
191
+ display: flex;
192
+ width: 100%;
193
+ align-items: center;
194
+ gap: 18rpx;
195
+ }
196
+ .snake-slider.is-disabled {
197
+ opacity: 0.45;
198
+ }
199
+ .snake-slider__main {
200
+ flex: 1;
201
+ min-width: 0;
202
+ padding: 28rpx 0;
203
+ }
204
+ .snake-slider__track {
205
+ position: relative;
206
+ border-radius: 999px;
207
+ }
208
+ .snake-slider__track:focus-visible {
209
+ outline: 2px solid var(--snake-color-primary, #06d290);
210
+ outline-offset: 8rpx;
211
+ }
212
+ .snake-slider__active {
213
+ position: absolute;
214
+ top: 0;
215
+ bottom: 0;
216
+ left: 0;
217
+ border-radius: inherit;
218
+ }
219
+ .snake-slider__block {
220
+ position: absolute;
221
+ top: 50%;
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ border-radius: 50%;
226
+ box-shadow: 0 2rpx 12rpx rgba(15, 30, 24, 0.2);
227
+ transform: translate(-50%, -50%);
228
+ }
229
+ .snake-slider__value {
230
+ position: absolute;
231
+ padding: 4rpx 10rpx;
232
+ border-radius: 8rpx;
233
+ color: #fff;
234
+ font-size: 20rpx;
235
+ line-height: 1.2;
236
+ background: rgba(15, 30, 24, 0.76);
237
+ transform: translateX(-50%);
238
+ }
239
+ .snake-slider__value.is-top {
240
+ bottom: 22rpx;
241
+ }
242
+ .snake-slider__value.is-bottom {
243
+ top: 22rpx;
244
+ }
245
+ .snake-slider__edge {
246
+ color: var(--snake-color-text-muted, #909399);
247
+ font-size: 24rpx;
248
+ }
249
+ </style>
@@ -0,0 +1,59 @@
1
+ import type { UnitValue } from '../../libs/function/unit';
2
+ import type { StyleValue } from '../../types/global';
3
+ /** SliderValuePosition 类型定义。 */
4
+ export type SliderValuePosition = 'top' | 'bottom';
5
+ /** Slider 组件的公开属性。 */
6
+ export interface SliderProps {
7
+ /** 双向绑定的当前值。 */
8
+ modelValue?: number | string;
9
+ /** 允许设置的最小值。 */
10
+ min?: number | string;
11
+ /** 允许设置的最大值。 */
12
+ max?: number | string;
13
+ /** 每次变化的步长。 */
14
+ step?: number | string;
15
+ /** 组件高度。 */
16
+ height?: UnitValue;
17
+ /** block width 配置。 */
18
+ blockWidth?: UnitValue;
19
+ /** 激活状态颜色。 */
20
+ activeColor?: string;
21
+ /** 未激活状态颜色。 */
22
+ inactiveColor?: string;
23
+ /** block color 配置。 */
24
+ blockColor?: string;
25
+ /** block style 配置。 */
26
+ blockStyle?: StyleValue;
27
+ /** 是否禁用交互。 */
28
+ disabled?: boolean;
29
+ /** 是否使用默认插槽替代文本。 */
30
+ useSlot?: boolean;
31
+ /** show value 配置。 */
32
+ showValue?: boolean;
33
+ /** value position 配置。 */
34
+ valuePosition?: SliderValuePosition;
35
+ /** show edge value 配置。 */
36
+ showEdgeValue?: boolean;
37
+ /** 根节点自定义类名。 */
38
+ customClass?: string;
39
+ /** 根节点自定义样式。 */
40
+ customStyle?: StyleValue;
41
+ }
42
+ /** Slider 组件的事件契约。 */
43
+ export type SliderEmits = {
44
+ /** update:modelValue 事件携带的参数。 */
45
+ 'update:modelValue': [value: number];
46
+ /** change 事件携带的参数。 */
47
+ change: [value: number];
48
+ /** start 事件携带的参数。 */
49
+ start: [value: number];
50
+ /** moving 事件携带的参数。 */
51
+ moving: [value: number];
52
+ /** end 事件携带的参数。 */
53
+ end: [value: number];
54
+ };
55
+ /** Slider 组件实例公开的方法和状态。 */
56
+ export interface SliderExpose {
57
+ /** update 方法。 */
58
+ update(): Promise<void>;
59
+ }
@@ -0,0 +1,178 @@
1
+ <template>
2
+ <view class="snake-sticky">
3
+ <view class="snake-sticky__wrap" :class="[elClass, customClass]" :style="placeholderStyle">
4
+ <view class="snake-sticky__content" :style="contentStyle">
5
+ <slot />
6
+ </view>
7
+ </view>
8
+ </view>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ /**
13
+ * @component Sticky
14
+ * @description 通过 IntersectionObserver 相对视口观察占位层实现跨端吸顶,采用三层结构(外层容器 / 占位层 / 内容层),并在不支持 Observer 的环境降级为页面 scrollTop 定位。
15
+ * @see /components/sticky
16
+ */
17
+
18
+ import { onPageScroll } from '@dcloudio/uni-app';
19
+ import {
20
+ computed,
21
+ getCurrentInstance,
22
+ nextTick,
23
+ onBeforeUnmount,
24
+ onMounted,
25
+ ref,
26
+ watch,
27
+ type CSSProperties
28
+ } from 'vue';
29
+ import { getRect } from '../../libs/function/getRect';
30
+ import { guid } from '../../libs/function/guid';
31
+ import type { StickyEmits, StickyProps } from './types';
32
+
33
+ defineOptions({ name: 'Sticky', options: { virtualHost: true, styleIsolation: 'shared' } });
34
+
35
+ const props = withDefaults(defineProps<StickyProps>(), {
36
+ offsetTop: 0,
37
+ customNavHeight: 0,
38
+ zIndex: 10,
39
+ disabled: false,
40
+ bgColor: 'transparent',
41
+ scrollTop: 0,
42
+ index: 0,
43
+ customClass: '',
44
+ customStyle: ''
45
+ });
46
+ const emit = defineEmits<StickyEmits>();
47
+ const instance = getCurrentInstance();
48
+
49
+ const fixed = ref(false);
50
+ const height = ref(0);
51
+ const width = ref(0);
52
+ const left = ref(0);
53
+ // 页面绝对定位的原始位置,仅用于降级方案。
54
+ const originTop = ref(0);
55
+ const currentScrollTop = ref(props.scrollTop);
56
+
57
+ // 每个实例唯一类名,用于测量和观察占位层,避免 CMS 循环中多个实例互相串扰。
58
+ const elClass = guid('snake-sticky');
59
+ // 是否支持 IntersectionObserver(H5 / 微信小程序 / App 均支持)。
60
+ const supportsObserver = typeof uni !== 'undefined' && typeof uni.createIntersectionObserver === 'function';
61
+
62
+ let contentObserver: any = null;
63
+
64
+ function unitNumber(value: number | string | null | undefined) {
65
+ const number = Number.parseFloat(String(value ?? 0));
66
+ return Number.isFinite(number) ? number : 0;
67
+ }
68
+ function offsetTopPx(value: number | string | null | undefined) {
69
+ const number = unitNumber(value);
70
+ if (typeof value === 'string' && value.trim().endsWith('px')) return number;
71
+ return typeof uni !== 'undefined' && typeof uni.upx2px === 'function' ? uni.upx2px(number) : number;
72
+ }
73
+ // 正规化 disabled,避免宿主把 "false" 字符串传进来后被当成 true。
74
+ const isDisabled = computed(() => {
75
+ // CMS 数据可能传入字符串布尔值,公共类型保持 boolean,此处按 unknown 归一化。
76
+ const value = props.disabled as unknown;
77
+ return value === true || value === 1 || value === '1' || value === 'true';
78
+ });
79
+ const stickyTop = computed(() => offsetTopPx(props.offsetTop) + unitNumber(props.customNavHeight));
80
+ const placeholderStyle = computed(() => ({ height: fixed.value ? `${height.value}px` : 'auto' }));
81
+ const contentStyle = computed(() => [
82
+ (fixed.value
83
+ ? { position: 'fixed', top: `${stickyTop.value}px`, left: `${left.value}px`, width: `${width.value}px` }
84
+ : { position: 'static' }) as CSSProperties,
85
+ { zIndex: props.zIndex, backgroundColor: props.bgColor },
86
+ props.customStyle
87
+ ]);
88
+
89
+ async function setFixed(value: boolean) {
90
+ if (fixed.value === value) return;
91
+ // 吸顶瞬间补一次测量:内容(如 CMS 筛选数据)异步渲染时,挂载期重试可能仍测到 0。
92
+ // 此刻 fixed 尚未应用、占位层仍在文档流中,测得的就是真实高宽。
93
+ if (value) await measure();
94
+ fixed.value = value;
95
+ const detail = { isFixed: value, index: props.index };
96
+ if (value) emit('fixed', detail);
97
+ else emit('unfixed', detail);
98
+ emit('change', detail);
99
+ }
100
+
101
+ function disconnectObserver() {
102
+ if (contentObserver) {
103
+ contentObserver.disconnect();
104
+ contentObserver = null;
105
+ }
106
+ }
107
+
108
+ function observeContent() {
109
+ disconnectObserver();
110
+ contentObserver = uni.createIntersectionObserver(instance?.proxy as any, { thresholds: [0.95, 0.98, 1] });
111
+ contentObserver.relativeToViewport({ top: -stickyTop.value });
112
+ contentObserver.observe(`.${elClass}`, (res: any) => {
113
+ if (isDisabled.value) return;
114
+ setFixed(res.boundingClientRect.top < stickyTop.value);
115
+ });
116
+ }
117
+
118
+ // 降级方案:基于页面 scrollTop 手动计算是否吸顶。
119
+ function updatePosition(scrollTop = currentScrollTop.value) {
120
+ currentScrollTop.value = scrollTop;
121
+ setFixed(!isDisabled.value && scrollTop + stickyTop.value >= originTop.value);
122
+ }
123
+
124
+ async function measure() {
125
+ const rect = await getRect(`.${elClass}`, instance);
126
+ if (!rect || !rect.height) return false;
127
+ height.value = Number(rect.height) || height.value;
128
+ width.value = Number(rect.width) || width.value;
129
+ left.value = Number(rect.left) || 0;
130
+ originTop.value = Number(rect.top) + currentScrollTop.value;
131
+ return true;
132
+ }
133
+
134
+ async function update() {
135
+ await nextTick();
136
+ if (isDisabled.value) return;
137
+ if (typeof uni === 'undefined' || typeof uni.createSelectorQuery !== 'function') return;
138
+ // 兼容 MP 首屏异步内容/布局未完成时测量为 0 的情况:最多重试 5 次,避免首次拿到 0 就直接放弃。
139
+ for (let attempt = 0; attempt < 5; attempt++) {
140
+ if (await measure()) break;
141
+ await new Promise(resolve => setTimeout(resolve, 50));
142
+ }
143
+ nextTick(() => {
144
+ if (supportsObserver) observeContent();
145
+ else updatePosition();
146
+ });
147
+ }
148
+
149
+ watch(
150
+ () => props.offsetTop,
151
+ () => {
152
+ if (!isDisabled.value) void update();
153
+ }
154
+ );
155
+ watch(
156
+ () => isDisabled.value,
157
+ disabled => {
158
+ if (disabled) {
159
+ setFixed(false);
160
+ disconnectObserver();
161
+ } else {
162
+ void update();
163
+ }
164
+ }
165
+ );
166
+ watch(
167
+ () => props.scrollTop,
168
+ value => {
169
+ if (!supportsObserver) updatePosition(Number(value) || 0);
170
+ }
171
+ );
172
+ onPageScroll(event => {
173
+ if (!supportsObserver) updatePosition(Number(event.scrollTop) || 0);
174
+ });
175
+ onMounted(() => void update());
176
+ onBeforeUnmount(disconnectObserver);
177
+ defineExpose({ update });
178
+ </script>
@@ -0,0 +1,48 @@
1
+ import type { UnitValue } from '../../libs/function/unit';
2
+ import type { StyleValue } from '../../types/global';
3
+
4
+ /** Sticky 组件的公开属性。 */
5
+ export interface StickyProps {
6
+ /** 吸顶或定位的顶部偏移量,数值按 rpx 处理,带 px 后缀时按 px 处理。 */
7
+ offsetTop?: UnitValue;
8
+ /** 自定义导航栏高度,单位 px。 */
9
+ customNavHeight?: UnitValue;
10
+ /** 组件层级。 */
11
+ zIndex?: number;
12
+ /** 是否禁用交互。 */
13
+ disabled?: boolean;
14
+ /** 背景颜色。 */
15
+ bgColor?: string;
16
+ /** 页面或容器当前滚动距离。 */
17
+ scrollTop?: number;
18
+ /** 当前数据项索引。 */
19
+ index?: number | string;
20
+ /** 根节点自定义类名。 */
21
+ customClass?: string;
22
+ /** 根节点自定义样式。 */
23
+ customStyle?: StyleValue;
24
+ }
25
+
26
+ /** StickyChange 事件详情。 */
27
+ export interface StickyChangeDetail {
28
+ /** is fixed 配置。 */
29
+ isFixed: boolean;
30
+ /** 当前数据项索引。 */
31
+ index: number | string;
32
+ }
33
+
34
+ /** Sticky 组件的事件契约。 */
35
+ export type StickyEmits = {
36
+ /** fixed 事件携带的参数。 */
37
+ fixed: [detail: StickyChangeDetail];
38
+ /** unfixed 事件携带的参数。 */
39
+ unfixed: [detail: StickyChangeDetail];
40
+ /** change 事件携带的参数。 */
41
+ change: [detail: StickyChangeDetail];
42
+ };
43
+
44
+ /** Sticky 组件实例公开的方法和状态。 */
45
+ export interface StickyExpose {
46
+ /** update 方法。 */
47
+ update(): Promise<void>;
48
+ }