252 行
7.2 KiB
Plaintext
252 行
7.2 KiB
Plaintext
<template>
|
|
<view style="flex: 1">
|
|
<uni-back-arrow-circle class="back" arrow-class="arrow"></uni-back-arrow-circle>
|
|
<scroll-view style="flex:1;" :refresher-enabled="true"
|
|
refresher-default-style="none" @refresherpulling="onRefresherpulling"
|
|
:refresher-threshold="300" :refresher-max-drag-distance="299"
|
|
@scroll="onScroll" :bounces="false"
|
|
>
|
|
<view class="header-wrapper" ref="headerWrapper">
|
|
<image class="header-img" ref="headerImg" src="https://web-ext-storage.dcloud.net.cn/hello-uni-app-x/pull-zoom-image-head-img.jpg" mode="scaleToFill"></image>
|
|
</view>
|
|
<view class="user-info" ref="userInfo">
|
|
<image class="avatar" src="/static/test-image/logo.png" mode="widthFix"></image>
|
|
<view class="user-text">
|
|
<text class="username">uni-app-x</text>
|
|
<text class="slogan">一次开发,多端覆盖</text>
|
|
</view>
|
|
</view>
|
|
<view class="content-list">
|
|
<view class="list-item" v-for="(item,index) in 30" :key="index">
|
|
<text class="item-text">{{item}}. 占位</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!--#ifdef APP -->
|
|
<view slot="refresher" class="header-pull-wrapper">
|
|
<image class="header-pull-img" ref="headerPullImg" src="https://web-ext-storage.dcloud.net.cn/hello-uni-app-x/pull-zoom-image-head-img.jpg" mode="scaleToFill"></image>
|
|
</view>
|
|
<!--#endif-->
|
|
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
/**
|
|
* @description: 支持下拉时缩放顶部封面图,并联动个人信息区域缩放与位移的个人主页页面。
|
|
* @logic: 页面监听滚动和下拉刷新过程中的位移变化,分别更新封面图缩放效果和用户信息区域的平移缩放样式。
|
|
* @optimization: 通过统一的元素样式更新方法复用封面图和信息区的 transform 计算,减少重复查询元素和样式设置逻辑。
|
|
*/
|
|
// Template refs
|
|
const headerPullImg = ref<UniElement | null>(null)
|
|
const headerImg = ref<UniElement | null>(null)
|
|
const userInfo = ref<UniElement | null>(null)
|
|
const headerWrapper = ref<UniElement | null>(null)
|
|
|
|
// 自动化测试
|
|
type PullZoomTestData = {
|
|
headerPullImgTransform: string
|
|
headerImgTransform: string
|
|
userInfoTransform: string
|
|
headerWrapperTransform: string
|
|
}
|
|
|
|
const data = reactive<PullZoomTestData>({
|
|
headerPullImgTransform: '',
|
|
headerImgTransform: '',
|
|
userInfoTransform: '',
|
|
headerWrapperTransform: ''
|
|
})
|
|
|
|
const $elementMap = new Map<string, UniElement>()
|
|
|
|
// 工具方法,用于快速设置 Element 的 style
|
|
function setElementStyle(refName : string, propertyName : string, propertyStyle : any) : void {
|
|
let element : UniElement | null = $elementMap.get(refName)
|
|
if (element == null) {
|
|
// Map ref name to corresponding ref variable
|
|
if (refName == 'header-pull-img') {
|
|
element = headerPullImg.value
|
|
} else if (refName == 'header-img') {
|
|
element = headerImg.value
|
|
} else if (refName == 'user-info') {
|
|
element = userInfo.value
|
|
} else if (refName == 'header-wrapper') {
|
|
element = headerWrapper.value
|
|
}
|
|
|
|
if (element != null) {
|
|
$elementMap.set(refName, element)
|
|
}
|
|
}
|
|
|
|
if (element != null) {
|
|
element.style.setProperty(propertyName, propertyStyle);
|
|
}
|
|
}
|
|
|
|
function updateTestTransform(refName : string, propertyStyle : string) : void {
|
|
if (refName == 'header-pull-img') {
|
|
data.headerPullImgTransform = propertyStyle
|
|
} else if (refName == 'header-img') {
|
|
data.headerImgTransform = propertyStyle
|
|
} else if (refName == 'user-info') {
|
|
data.userInfoTransform = propertyStyle
|
|
} else if (refName == 'header-wrapper') {
|
|
data.headerWrapperTransform = propertyStyle
|
|
}
|
|
}
|
|
|
|
function onScroll(e : ScrollEvent) {
|
|
const {scrollTop} = e.detail
|
|
let y : number = scrollTop - 110
|
|
let s : number = (100 - scrollTop/3)/100
|
|
if(y < 0){
|
|
y = 0
|
|
}
|
|
if(s < 0.7){
|
|
s = 0.7
|
|
}
|
|
let x : number = (1 - s) * -100
|
|
setElementStyle("user-info", "transform", `translate(${x}px, ${y + (s-1)* -50}px) scale(${s})`)
|
|
setElementStyle("header-wrapper", "transform", `translateY(${y}px)`)
|
|
}
|
|
|
|
function onRefresherpulling(e : RefresherEvent) {
|
|
// console.log('onRefresherpulling',e.detail.dy)
|
|
let pullingDistance : number = e.detail.dy
|
|
setElementStyle("header-pull-img", 'transform', `scale(${pullingDistance / 200 + 1})`)
|
|
setElementStyle("header-img", 'transform', `scale(${pullingDistance / 200 + 1})`)
|
|
}
|
|
|
|
// 供自动化测试直接验证滚动联动后的 transform 计算结果。
|
|
function jest_applyScroll(scrollTop : number) : void {
|
|
let y : number = scrollTop - 110
|
|
let s : number = (100 - scrollTop/3)/100
|
|
if(y < 0){
|
|
y = 0
|
|
}
|
|
if(s < 0.7){
|
|
s = 0.7
|
|
}
|
|
let x : number = (1 - s) * -100
|
|
updateTestTransform("user-info", `translate(${x}px, ${y + (s-1)* -50}px) scale(${s})`)
|
|
updateTestTransform("header-wrapper", `translateY(${y}px)`)
|
|
}
|
|
|
|
// 供自动化测试直接验证下拉缩放时两张头图的缩放结果。
|
|
function jest_applyPulling(pullingDistance : number) : void {
|
|
const scaleValue = `scale(${pullingDistance / 200 + 1})`
|
|
updateTestTransform("header-pull-img", scaleValue)
|
|
updateTestTransform("header-img", scaleValue)
|
|
}
|
|
|
|
defineExpose({
|
|
data,
|
|
jest_applyScroll,
|
|
jest_applyPulling
|
|
})
|
|
|
|
</script>
|
|
|
|
<style>
|
|
.header-pull-wrapper,
|
|
.header-wrapper
|
|
{
|
|
position: relative;
|
|
background: #5788cd;
|
|
height: 300px;
|
|
}
|
|
/* #ifdef APP-IOS || APP-HARMONY || (APP-ANDROID && VUE3-VAPOR) */
|
|
/** * TODO: 临时方案。
|
|
* 兼容性问题:在 iOS、鸿蒙以及 Android 蒸汽模式下,当 slot="refresher" 的元素设置为
|
|
* position: relative 时,渲染引擎仍会保留其在文档流中的物理占位,导致 scroll-view
|
|
* 内容底部出现多余空白。此处通过负 margin 抵消占位高度,以保证滚动区域计算正确。
|
|
*/
|
|
.header-pull-wrapper {
|
|
margin-bottom: -300px;
|
|
}
|
|
/* #endif */
|
|
|
|
.header-pull-img,
|
|
.header-img
|
|
{
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 600px;
|
|
}
|
|
|
|
.header-pull-img {
|
|
top: 0px;
|
|
}
|
|
|
|
.header-img {
|
|
bottom: -100px;
|
|
}
|
|
|
|
.header-wrapper {
|
|
z-index: 9;
|
|
height: 200px;
|
|
}
|
|
|
|
.user-info{
|
|
margin-top: -110px;
|
|
flex: 1;
|
|
padding: 15px;
|
|
flex-direction: row;
|
|
z-index: 10;
|
|
}
|
|
|
|
.avatar {
|
|
width: 75px;
|
|
height: 75px;
|
|
border-radius: 100px;
|
|
border: 3px solid #FFF;
|
|
}
|
|
|
|
.user-text {
|
|
flex-direction: column;
|
|
justify-content: space-around;
|
|
padding: 10px;
|
|
}
|
|
|
|
.username {
|
|
font-size: 26px;
|
|
color: #FFF;
|
|
}
|
|
|
|
.slogan {
|
|
font-size: 16px;
|
|
color: #FFF;
|
|
}
|
|
|
|
.content-list {
|
|
background-color: #FFF;
|
|
z-index: 1;
|
|
}
|
|
|
|
.list-item {
|
|
padding: 10px;
|
|
margin: 5px;
|
|
/* border-radius: 5px; */
|
|
border: 1px solid rgba(220, 220, 220, 0.3);
|
|
}
|
|
|
|
.item-text {
|
|
font-size: 14px;
|
|
color: #666;
|
|
line-height: 20px;
|
|
}
|
|
.back{
|
|
position: absolute;
|
|
top: var(--status-bar-height);
|
|
z-index: 10;
|
|
}
|
|
/* uni-back-arrow-circle组件 自定义样式 */
|
|
.arrow {
|
|
border-left-color: #FFFFFF !important;
|
|
border-bottom-color: #FFFFFF !important;
|
|
}
|
|
</style>
|