This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
.nut-sticky {
/* #ifdef APP-VUE || MP-WEIXIN */
// 此处默认写sticky属性是为了给微信和APP通过uni.createSelectorQuery查询是否支持css sticky使用
position: sticky;
/* #endif */
}

View File

@@ -0,0 +1 @@
export * from './sticky'

View File

@@ -0,0 +1,32 @@
import type { ExtractPropTypes } from 'vue'
import { commonProps, isH5, makeNumericProp, makeStringProp } from '../_utils'
export const stickyProps = {
...commonProps,
/**
* @description 吸顶距离
*/
offsetTop: makeNumericProp(0),
/**
* @description 吸附时的层级
*/
zIndex: {
type: [Number, String],
default: 2000,
},
/**
* @description 导航栏高度,自定义导航栏时,需要传入此值
* - H5端的导航栏属于“自定义”导航栏的范畴因为它是非原生的与普通元素一致
*/
customNavHeight: makeNumericProp(isH5 ? 44 : 0),
/**
* @description 是否开启吸顶功能
*/
disabled: Boolean,
/**
* @description 吸顶区域的背景颜色
*/
bgColor: makeStringProp('transparent'),
}
export type StickyProps = ExtractPropTypes<typeof stickyProps>

View File

@@ -0,0 +1,235 @@
<script lang="ts" setup>
import type { ComponentInternalInstance, CSSProperties } from 'vue'
import { computed, defineComponent, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import { PREFIX } from '../_constants'
import { useRect } from '../_hooks'
import { getMainClass, getMainStyle, getPx, getRandomId, pxCheck } from '../_utils'
import { stickyProps } from './sticky'
const props = defineProps(stickyProps)
const instance = getCurrentInstance() as ComponentInternalInstance
// eslint-disable-next-line ts/no-use-before-define
const elementId = `${componentName}-${getRandomId()}`
const cssSticky = ref(false)
const left = ref(0)
const width = ref('auto')
const height = ref('auto')
const fixed = ref(false)
const classes = computed(() => {
return getMainClass(props, componentName)
})
const stickyTop = computed(() => {
return Number(getPx(props.offsetTop)) + Number(getPx(props.customNavHeight))
})
const styles = computed(() => {
const value: CSSProperties = {}
if (!props.disabled) {
if (cssSticky.value) {
value.position = 'sticky'
value.top = pxCheck(stickyTop.value)
value.zIndex = props.zIndex
}
else {
if (!fixed.value || height.value === 'auto') {
value.height = 'auto'
}
else {
value.height = `${height.value}px`
}
}
}
else {
value.position = 'static'
}
value.backgroundColor = props.bgColor
return getMainStyle(props, value)
})
const contentStyles = computed(() => {
const value: CSSProperties = {}
if (!cssSticky.value) {
value.position = fixed.value ? 'fixed' : 'static'
value.top = `${stickyTop.value}px`
value.left = `${left.value}px`
value.width = width.value === 'auto' ? 'auto' : `${width.value}px`
value.zIndex = props.zIndex
}
return value
})
function setFixed(top: number) {
// 判断是否出于吸顶条件范围
fixed.value = top <= stickyTop.value
}
const observer = shallowRef<UniApp.IntersectionObserver | null>(null)
function disconnectObserver() {
// 断掉观察,释放资源
if (observer.value == null)
return
observer.value.disconnect()
observer.value = null
}
function observeContent() {
// 先断掉之前的观察
disconnectObserver()
const contentObserver = uni.createIntersectionObserver({
// 检测的区间范围
thresholds: [0.95, 0.98, 1],
})
// 到屏幕顶部的高度时触发
contentObserver.relativeToViewport({
top: -stickyTop.value,
})
// 绑定观察的元素
contentObserver.observe(`#${elementId}`, (res) => {
setFixed(res.boundingClientRect.top)
})
observer.value = contentObserver
}
function initObserveContent() {
// 获取吸顶内容的高度用于在js吸顶模式时给父元素一个填充高度防止"塌陷"
useRect(elementId, instance).then((res) => {
left.value = res.left!
width.value = String(res.width)
height.value = String(res.height)
nextTick(() => {
observeContent()
})
})
}
// H5通过创建元素的形式嗅探是否支持css sticky
// 判断浏览器是否支持sticky属性
function checkCssStickyForH5() {
// 方法内进行判断,避免在其他平台生成无用代码
// #ifdef H5
const vendorList = ['', '-webkit-', '-ms-', '-moz-', '-o-']
const stickyElement = document.createElement('div')
for (let i = 0; i < vendorList.length; i++) {
stickyElement.style.position = `${vendorList[i]}sticky`
if (stickyElement.style.position !== '')
return true
}
return false
// #endif
}
// 在APP和微信小程序上通过uni.createSelectorQuery可以判断是否支持css sticky
function checkComputedStyle(): Promise<boolean> {
// 方法内进行判断,避免在其他平台生成无用代码
// #ifdef APP-VUE || MP-WEIXIN
return new Promise((resolve) => {
uni.createSelectorQuery()
.in(instance)
.select(`.${componentName}`)
.fields({
computedStyle: ['position'],
}, () => {})
.exec((res) => {
resolve(res[0].position === 'sticky')
})
})
// #endif
}
async function checkSupportCssSticky() {
// #ifdef H5
// H5一般都是现代浏览器是支持css sticky的这里使用创建元素嗅探的形式判断
if (checkCssStickyForH5())
cssSticky.value = true
// #endif
// 如果安卓版本高于8.0依然认为是支持css sticky的(因为安卓7在某些机型可能不支持sticky)
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.osName === 'android' && Number.parseInt(systemInfo.osVersion) > 8)
cssSticky.value = true
// APP-Vue和微信平台通过computedStyle判断是否支持css sticky
// #ifdef APP-VUE || MP-WEIXIN
cssSticky.value = await checkComputedStyle()
// #endif
// ios上从ios6开始都是支持css sticky的
if (systemInfo.osName === 'ios')
cssSticky.value = true
// nvue是支持css sticky的
// #ifdef APP-NVUE
cssSticky.value = true
// #endif
}
function init() {
// 判断使用的模式
checkSupportCssSticky()
// 如果不支持css sticky则使用js方案此方案性能比不上css方案
if (!cssSticky.value) {
if (!props.disabled) {
initObserveContent()
}
}
}
onMounted(() => {
init()
})
onUnmounted(() => {
disconnectObserver()
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-sticky`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view
:id="elementId"
:class="classes"
:style="[styles]"
>
<view
class="nut-sticky__content"
:style="[contentStyles]"
>
<slot />
</view>
</view>
</template>
<style lang="scss">
@import "./index";
</style>