uni.getSystemInfoSync().windowHeight两次调用不一致

在小程序中,getSystemInfoSync()返回的screenHeight和windowHeight在不同阶段可能有不同的值。初次加载时,两者可能相等,但在onReady之后,windowHeight会减去顶部和底部导航栏的高度,表现为正常的页面视图高度。因此,当在页面初始化完成后调用getSystemInfoSync(),才能得到正确的windowHeight值。

getSystemInfoSync()返回的对象中有两个高度值,一个是

screenHeight:手机屏幕高度

windowHeight:小程序视图高度(除去原生的顶部和底部导航栏的高度)

第一次打开页面时拿到的值与第二次打开页面拿到的值不一致,我算了一下,两次值得差异就是导航栏得高度。。。

我开始是在onLoad方法里面调用getSystemInfoSync,拿到的windowHeightscreenHeight值一样。

onReady回调函数里面调用,等页面初始化完成,就能正常拿到windowHeight

<!-- pages/webview/webview.vue --> <template> <view class="webview-container"> <cover-view class="webview-wrapper"> <web-view :src="webviewPath" id="targetWebview"></web-view> </cover-view> <cover-view class="bottom-action-area"> <cover-view class="capture-btn" @click="captureWebview"> <cover-view class="btn-text">立即取证</cover-view> </cover-view> </cover-view> <!-- 预览组件 --> <cover-view v-if="showPreview" class="custom-preview"> <cover-view class="preview-header"> <cover-view class="title-container"> <cover-view class="preview-title">预览界面</cover-view> </cover-view> </cover-view> <cover-image :src="screenshotPath" class="preview-image" mode="aspectFit" @error="handleImageError" ></cover-image > <cover-view class="action-buttons"> <cover-view class="action-btn cancel-btn" @click="handleAction('放弃')">放弃</cover-view> <cover-view class="action-btn confirm-btn" @click="handleAction('固化')">固化</cover-view> </cover-view> <cover-view> </cover-view> </cover-view> </view> </template> <script setup lang="ts"> import { onLoad, onReady } from '@dcloudio/uni-app'; import html2canvas from 'html2canvas'; import { ref } from 'vue'; declare const plus: any; const webviewPath = ref(''); const ws = ref<any>(null); const screenshotPath = ref(''); const showPreview = ref(false); const platform = ref(''); const originalWebviewHeight = ref('auto'); const screenshotFilePath = ref(''); onLoad((options: any) => { if (options.url) { webviewPath.value = decodeURIComponent(options.url); } // 获取当前平台 const systemInfo = uni.getSystemInfoSync(); platform.value = systemInfo.platform; }); onReady(() => { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const currentWebview = currentPage.$getAppWebview(); setTimeout(() => { if (currentWebview.children().length > 0) { const wv = currentWebview.children()[0]; ws.value = wv; } }, 1000); }); const captureWebview = async () => { if (!ws.value) { uni.showToast({ title: 'WebView未准备好', icon: 'none' }); return; } uni.showLoading({ title:'正在取证中' }) // #ifdef APP-PLUS // #endif // #ifdef APP-H5 // #endif } const closePreview = () => { showPreview.value = false; } const handleAction = (action: string) => { switch(action) { case '放弃': closePreview(); break; case '固化': uni.saveImageToPhotosAlbum({ filePath: screenshotPath.value, success: () => { uni.showToast({ title: '证据已固化保存' }); // 调用上传函数 // uploadEvidence(); }, fail: (err) => { uni.showToast({ title: '保存失败: ' + err.errMsg, icon: 'none' }); } }); break; } } const handleImageError = (e: any) => { console.error('图片加载错误:', e); uni.showToast({ title: '图片加载错误', icon: 'none' }); } </script> <style> .webview-container { position: relative; width: 100%; height: 100vh; overflow: hidden; } .webview-wrapper { height: calc(100vh - 120rpx); width: 100%; overflow: hidden; } .bottom-action-area { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background-color: #007aff; padding: 30rpx; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); display: flex; justify-content: center; align-items: center; } .capture-btn { width: 100%; height: 90rpx; background-color: #007aff; border-radius: 45rpx; box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.2); display: flex; align-items: center; justify-content: center; } .btn-text { color: #ffffff; font-size: 32rpx; font-weight: 500; } /* 优化预览组件 */ .custom-preview { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; background-color: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; /* 垂直排列子元素 */ align-items: center; /* 关键修改:去掉顶部内边距,只保留底部内边距 */ padding: 0 0 40rpx 0; box-sizing: border-box; } .preview-header { width: 100%; height: 250rpx; /* 增加高度 */ background-color: #007aff; color: #ffffff; font-size: 34rpx; /* 增大字体 */ font-weight: bold; display: flex; align-items: center; justify-content: center; /* 添加圆角效果 */ border-top-left-radius: 16rpx; border-top-right-radius: 16rpx; } .preview-title { color: #ffffff; /* 白色字体 */ font-size: 28rpx; /* 字体大小适配40rpx高度 */ font-weight: bold; line-height: 40rpx; /* 与容器高度一致,实现垂直居中 */ width: 100%; text-align: center; } .preview-image { width: 100%; height: 100%; object-fit: contain; border: 1rpx solid rgba(255, 255, 255, 0.1); border-radius: 8rpx; background-color: #f5f5f5; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3); } .action-buttons { display: flex; width: 100%; /* 充满整个宽度 */ justify-content: space-between; padding: 30rpx 5%; /* 左右留5%间隙,上下30rpx内边距扩展背景区域 */ margin-top: auto; /* 借助flex布局推到最底部 */ background-color: #353336; /* 底部模块背景色 */ box-sizing: border-box; /* 确保padding影响宽度计算 */ } .action-btn { flex: 1; height: 90rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; padding: 0; margin: 0 15rpx; border: none; color: #ffffff; /* 文字色适配深色背景 */ font-size: 34rpx; font-weight: 500; line-height: 1; } .cancel-btn { color: #ffffff; /* 文字颜色 */ font-size: 34rpx; font-weight: 500; /* 确保文字本身无偏移 */ line-height: 1; /* 清除行高影响 */ text-align: center; /* 辅助居中(冗余保障) */ background-color: #353336; } .confirm-btn { color: #ffffff; /* 文字颜色 */ font-size: 34rpx; font-weight: 500; /* 确保文字本身无偏移 */ line-height: 1; /* 清除行高影响 */ text-align: center; /* 辅助居中(冗余保障) */ background-color: #353336; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .bottom-action-img{ position: fixed; top: 100; left: 0; right: 0; z-index: 100; background-color: #007aff; padding: 30rpx; display: flex; justify-content: center; align-items: center; } .action-img{ width: 100%; height: 90rpx; background-color: #007aff; display: flex; } </style> app设备中 将webview内容转换成pdf格式
08-09
<template> <view class="page-container"> <text class="title">Dropdown 下拉菜单示例</text> <!-- ==================== 示例1:基础用法 + 自定义触发器 ==================== --> <view class="demo-section"> <text class="label">选择分类:</text> <zy-popup-dropdown-menu v-model="selectedCategory" :data="categoryList" label-key="name" value-key="value" @change="handleChange" @open="onOpen" @close="onClose" > <template #reference="{ open }"> <!-- ✅ 必须添加 data-dropdown-trigger --> <view data-dropdown-trigger class="custom-trigger" @tap="open"> {{ selectedCategoryLabel }} <text class="arrow">▼</text> </view> </template> </zy-popup-dropdown-menu> </view> <!-- ==================== 示例2:城市选择(长文本测试) ==================== --> <view class="demo-section"> <text class="label">选择城市:</text> <zy-popup-dropdown-menu v-model="selectedCity" :data="cityList" label-key="name" value-key="code" @change="val => console.log('城市变更:', val)" > <template #reference="{ open }"> <view data-dropdown-trigger class="custom-trigger wide" @tap="open"> {{ selectedCityLabel || '请选择城市...' }} </view> </template> </zy-popup-dropdown-menu> </view> <!-- ==================== 示例3:默认触发器(最简写法)==================== --> <view class="demo-section"> <text class="label">直接使用默认按钮:</text> <zy-popup-dropdown-menu v-model="selectedTheme" :data="themeList" @change="val => uni.showToast({ title: `主题: ${val.label}`, icon: 'none' })" /> </view> <!-- ==================== 示例4:通过 ref 控制打开关闭 ==================== --> <view class="demo-section"> <text class="label">通过 Ref 手动控制:</text> <zy-popup-dropdown-menu ref="menuRef" v-model="selectedAction" :data="actionList" @change="val => console.log('操作:', val)" > <template #reference> <view class="custom-trigger" @tap="handleManualOpen"> {{ selectedActionLabel }} ▼ </view> </template> </zy-popup-dropdown-menu> <view class="button-group"> <button @tap="menuRef.open()">打开</button> <button @tap="menuRef.close()">关闭</button> <button @tap="menuRef.toggle()">切换</button> </view> </view> </view> </template> <script setup> import { ref, computed } from 'vue' // ---------------------- // 数据源 // ---------------------- // 分类列表 const categoryList = ref([ { name: '美食', value: 'food' }, { name: '旅游', value: 'travel' }, { name: '科技数码产品推荐', value: 'tech' }, { name: '健身运动与户外', value: 'fitness' }, { name: '摄影艺术创作', value: 'photo' } ]) // 城市列表(含长文本) const cityList = ref([ { name: '北京市', code: 'beijing' }, { name: '上海市', code: 'shanghai' }, { name: '广州市 - 南方重要经济中心', code: 'guangzhou' }, { name: '深圳市 - 创新科技之都', code: 'shenzhen' }, { name: '成都市 - 西部休闲文化名城,生活节奏慢', code: 'chengdu' } ]) // 主题列表 const themeList = ref([ { name: '浅色模式', value: 'light' }, { name: '深色模式', value: 'dark' }, { name: '自动切换', value: 'auto' } ]) // 操作列表 const actionList = ref([ { name: '编辑', value: 'edit' }, { name: '删除', value: 'delete' }, { name: '分享', value: 'share' }, { name: '导出数据', value: 'export' } ]) // ---------------------- // 当前选中值 // ---------------------- const selectedCategory = ref('travel') const selectedCity = ref('') const selectedTheme = ref('light') const selectedAction = ref('edit') // ---------------------- // 计算显示文本 // ---------------------- const selectedCategoryLabel = computed(() => { const item = categoryList.value.find(item => item.value === selectedCategory.value) return item ? item.name : '请选择...' }) const selectedCityLabel = computed(() => { const item = cityList.value.find(item => item.code === selectedCity.value) return item ? item.name : '请选择城市...' }) const selectedActionLabel = computed(() => { const item = actionList.value.find(item => item.value === selectedAction.value) return item ? item.name : '请选择操作' }) // ---------------------- // 事件处理 // ---------------------- const handleChange = ({ value, label }) => { uni.showToast({ title: `选择了: ${label}`, icon: 'none', duration: 1500 }) } const onOpen = currentValue => { console.log('[Dropdown] 打开事件,当前值:', currentValue) } const onClose = () => { console.log('[Dropdown] 已关闭') } // ---------------------- // Ref 手动控制 // ---------------------- const menuRef = ref(null) const handleManualOpen = () => { menuRef.value?.open() } </script> <style scoped> .page-container { padding: 20px; padding-top: 200px; background-color: #f8f9fa; min-height: 100vh; } .title { font-size: 18px; font-weight: bold; color: #1a1a1a; margin-bottom: 20px; display: block; } .demo-section { margin-bottom: 30px; } .label { display: block; font-size: 15px; color: #555; margin-bottom: 8px; } /* 自定义触发器样式 */ .custom-trigger { display: inline-flex; align-items: center; justify-content: space-between; width: 200px; padding: 10px 15px; background-color: #ffffff; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; color: #333; box-sizing: border-box; } .custom-trigger.wide { width: 250px; } .arrow { font-size: 12px; color: #999; } /* 按钮组 */ .button-group { display: flex; gap: 10px; margin-top: 10px; } button { font-size: 14px; padding: 0 12px; } </style> <template> <view class="dropdown-wrapper"> <!-- 插槽:触发器 --> <slot name="reference" :open="open" :close="close" :toggle="toggle"> <!-- 默认触发器(自动添加 data-dropdown-trigger) --> <view v-if="!$slots.reference" class="dropdown-trigger-default" data-dropdown-trigger @tap="open"> 点击选择 ▼ </view> <!-- 自定义内容包裹层(自动识别触发) --> <view v-else data-dropdown-trigger @tap="open"> <slot /> </view> </slot> <!-- 浮动下拉菜单 --> <view v-if="visible" ref="dropdownRef" class="zy-popup-dropdown-menu" :style="{ top: `${top}px`, left: `${left}px`, transform: position }" @touchmove.stop > <view class="dropdown-content"> <view v-for="item in props.data" :key="item[props.valueKey]" class="dropdown-item" :class="{ 'is-selected': props.modelValue === item[props.valueKey] }" @tap="handleSelect(item)" > <text class="item-label">{{ item[props.labelKey] }}</text> <text v-if="props.modelValue === item[props.valueKey]" class="icon-check"></text> </view> </view> </view> </view> </template> <script setup> import { ref, nextTick, onUnmounted } from 'vue' // ---------------------- // Props 定义 // ---------------------- const props = defineProps({ // 数据源 [{ label: 'xxx', value: '1' }] data: { type: Array, required: true }, // 当前选中值(v-model) modelValue: { type: [String, Number, null], default: null }, // 显示字段名 labelKey: { type: String, default: 'name' }, // 值字段名 valueKey: { type: String, default: 'value' } }) // ---------------------- // Emits // ---------------------- const emit = defineEmits(['update:modelValue', 'change', 'open', 'close']) // ---------------------- // 内部状态 // ---------------------- const visible = ref(false) const top = ref(0) const left = ref(0) const position = ref('translateY(8px)') const dropdownRef = ref(null) let observer = null // ---------------------- // 获取触发器位置(核心方法) // ---------------------- function getTriggerRect() { return new Promise(resolve => { const query = uni.createSelectorQuery() query.select('[data-dropdown-trigger]').boundingClientRect() query.exec(res => { const rect = res[0] if (rect) { console.log('[Dropdown] ✅ 成功获取触发器位置:', rect) resolve(rect) } else { console.warn('[Dropdown] ❌ 未找到 [data-dropdown-trigger] 元素,请检查插槽结构') resolve(null) } }) }) } // ---------------------- // 打开下拉框 // ---------------------- const open = async () => { if (visible.value) return const rect = await getTriggerRect() if (!rect) return // 获取窗口高度 const res = await new Promise(resolve => { uni.createSelectorQuery().selectViewport().scrollOffset().exec(resolve) }) const scrollInfo = res[0] const windowHeight = scrollInfo.windowHeight || scrollInfo.height const maxHeight = 486 const needUpward = windowHeight - rect.bottom < maxHeight // 设置位置 left.value = rect.left top.value = needUpward ? rect.top - maxHeight - 8 : rect.bottom + 8 position.value = needUpward ? 'translateY(-8px)' : 'translateY(8px)' visible.value = true emit('open', props.modelValue) nextTick(() => { bindOutsideClickListener() bindScrollListener() }) } // ---------------------- // 关闭 & 切换 & 选择 // ---------------------- const close = () => { if (!visible.value) return visible.value = false emit('close') removeListeners() } const toggle = () => { visible.value ? close() : open() } const handleSelect = item => { const value = item[props.valueKey] const label = item[props.labelKey] if (props.modelValue !== value) { emit('update:modelValue', value) emit('change', { value, label }) } close() } // ---------------------- // 外部点击关闭 // ---------------------- const bindOutsideClickListener = () => { const handler = e => { // 阻止事件冒泡时也能监听到页面点击 const path = e.path || [] const isInside = path.some(node => node?.dataset?.dropdownTrigger) if (!isInside) close() } uni.$on('onPageTap', handler) observer = { ...observer, cleanupTap: () => uni.$off('onPageTap', handler) } } // ---------------------- // 页面滚动关闭 // ---------------------- const bindScrollListener = () => { const scrollHandler = () => close() uni.$on('onPageScroll', scrollHandler) observer = { ...observer, cleanupScroll: () => uni.$off('onPageScroll', scrollHandler) } } // ---------------------- // 移除监听 // ---------------------- const removeListeners = () => { observer?.cleanupTap?.() observer?.cleanupScroll?.() observer = null } const openByEvent = async e => { const target = e.currentTarget || e.target if (!target) { console.error('[Dropdown] openByEvent 缺少 event 对象') return } const rect = await new Promise(resolve => { const query = uni.createSelectorQuery() query.select(`#${target.id}`).boundingClientRect() query.exec(res => resolve(res[0])) }) if (!rect) { // 回退:直接用 event 自带的信息 const { top, bottom, left, height } = e.detail?.offsetTop !== undefined ? e : target const fallbackRect = { top, bottom: bottom || top + height, left, width: target.offsetWidth || 80, height: target.offsetHeight || 40 } console.warn('[Dropdown] 使用 event 数据回退定位', fallbackRect) doOpen(fallbackRect) } else { doOpen(rect) } } // 抽离打开逻辑 const doOpen = rect => { const res = uni.getSystemInfoSync() const windowHeight = res.windowHeight const maxHeight = 486 const needUpward = windowHeight - rect.bottom < maxHeight left.value = rect.left top.value = needUpward ? rect.top - maxHeight - 8 : rect.bottom + 8 position.value = needUpward ? 'translateY(-8px)' : 'translateY(8px)' visible.value = true emit('open', props.modelValue) nextTick(() => { bindOutsideClickListener() bindScrollListener() }) } // ---------------------- // 暴露方法给父组件调用 // ---------------------- defineExpose({ open, close, toggle }) // ---------------------- // 卸载清理 // ---------------------- onUnmounted(() => { removeListeners() }) </script> <style scoped> /* 整体容器 */ .dropdown-wrapper { display: inline-block; } /* 默认触发器样式 */ .dropdown-trigger-default { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; background-color: #fff; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; color: #333; } /* 下拉菜单主体 */ .zy-popup-dropdown-menu { position: fixed; width: 215px; max-height: 486px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); z-index: 9999; overflow: hidden; } /* 内容区可滚动 */ .dropdown-content { height: 100%; max-height: 486px; overflow-y: auto; -webkit-overflow-scrolling: touch; } /* 每一项 */ .dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 15px; color: #333333; border-bottom: 1px solid #f5f5f5; } .dropdown-item:last-child { border-bottom: none; } /* 选中项样式 */ .dropdown-item.is-selected { color: #0f56d5; font-weight: 500; } /* 文本自动换行 */ .item-label { flex: 1; word-break: break-word; line-height: 1.4; text-align: left; } /* 对号图标 */ .icon-check { font-family: 'erda' !important; /* 可替换为 iconfont 字体 */ font-size: 16px; margin-left: 8px; color: #0f56d5; } </style> 报错zy-popup-dropdown-menu.js? [sm]:47 [Dropdown] ❌ 未找到 [data-dropdown-trigger] 元素,请检查插槽结构
10-15
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值