| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- <template>
- <view class="webview-page">
- <!-- #ifdef APP-PLUS -->
- <!-- App:web-view 为原生层会挡住普通 view 的点击,须用 cover-view 盖在上面 -->
- <web-view class="webview" :src="decodedUrl" @load="onEmbeddedWebviewLoad" />
- <cover-view class="header header--overlay" :style="headerStyle">
- <cover-view class="header-inner">
- <cover-view class="capsule">
- <cover-view
- class="capsule-btn"
- style="display: flex; align-items: center; justify-content: center; width: 46px; height: 28px"
- @tap="onCapsuleMoreMenu"
- >
- <!-- App:cover-view 内勿依赖 scoped;cover-image 宽高须内联 px -->
- <!-- App 端 cover-image 多数不支持 SVG,须用 PNG -->
- <cover-image
- style="width: 20px; height: 20px"
- src="/static/icons/capsule-more.png"
- mode="aspectFit"
- @tap="onCapsuleMoreMenu"
- />
- </cover-view>
- <cover-view class="capsule-divider" />
- <cover-view
- class="capsule-btn"
- style="display: flex; align-items: center; justify-content: center; width: 46px; height: 28px"
- @tap="onCapsuleClose"
- >
- <cover-image
- style="width: 20px; height: 20px"
- src="/static/icons/capsule-close.png"
- mode="aspectFit"
- @tap="onCapsuleClose"
- />
- </cover-view>
- </cover-view>
- </cover-view>
- </cover-view>
- <!-- #endif -->
- <!-- #ifndef APP-PLUS -->
- <view class="header" :style="headerStyle">
- <view class="header-inner">
- <view class="capsule">
- <view class="capsule-btn" @tap="onCapsuleMoreMenu">
- <image class="capsule-icon" src="/static/icons/capsule-more.svg" mode="aspectFit" />
- </view>
- <view class="capsule-divider" />
- <view class="capsule-btn" @tap="onCapsuleClose">
- <image class="capsule-icon" src="/static/icons/capsule-close.svg" mode="aspectFit" />
- </view>
- </view>
- </view>
- </view>
- <web-view class="webview" :src="decodedUrl" />
- <!-- #endif -->
- </view>
- </template>
- <script setup>
- import { computed, ref } from 'vue'
- import { onLoad, onReady, onShow } from '@dcloudio/uni-app'
- const decodedUrl = ref('')
- /** 用系统信息算顶栏 padding-top(部分端上 CSS env 不可靠) */
- function computeHeaderPaddingTopPx() {
- try {
- const si = uni.getSystemInfoSync()
- const insetTop =
- si.safeAreaInsets && typeof si.safeAreaInsets.top === 'number'
- ? si.safeAreaInsets.top
- : si.statusBarHeight || 0
- const r88 = uni.upx2px(88)
- const r24 = uni.upx2px(24)
- return Math.max(r88, r24 + insetTop)
- } catch (e) {
- return uni.upx2px(88)
- }
- }
- const headerPaddingTopPx = ref(computeHeaderPaddingTopPx())
- const headerStyle = computed(() => ({
- paddingTop: `${headerPaddingTopPx.value}px`
- }))
- onLoad((options) => {
- headerPaddingTopPx.value = computeHeaderPaddingTopPx()
- const rawUrl = options && options.url ? options.url : ''
- decodedUrl.value = rawUrl ? decodeURIComponent(rawUrl) : ''
- })
- /**
- * App 端 web-view 为原生组件,默认会铺满窗口,内嵌 H5 会与状态栏重叠。
- * 通过子 webview 的 setStyle 将内嵌区域限制在自定义顶栏下方。
- * 不可假定 children()[0] 为内嵌页,须遍历带 getURL 的子窗体。
- */
- // #ifdef APP-PLUS
- function findEmbeddedWebviewNative(currentWebview) {
- try {
- const list = currentWebview.children && currentWebview.children()
- if (!list || !list.length) return null
- for (let i = 0; i < list.length; i++) {
- const w = list[i]
- if (w && typeof w.setStyle === 'function' && typeof w.getURL === 'function') {
- return w
- }
- }
- for (let i = 0; i < list.length; i++) {
- const w = list[i]
- if (w && typeof w.setStyle === 'function') return w
- }
- } catch (e) {}
- return null
- }
- // #endif
- function positionEmbeddedWebview() {
- // #ifdef APP-PLUS
- const pages = getCurrentPages()
- const page = pages[pages.length - 1]
- if (!page || typeof page.$getAppWebview !== 'function') return
- const currentWebview = page.$getAppWebview()
- const apply = () => {
- const wv = findEmbeddedWebviewNative(currentWebview)
- if (!wv) return false
- uni.createSelectorQuery()
- .select('.header')
- .boundingClientRect((rect) => {
- if (!rect || rect.height == null) return
- const sys = uni.getSystemInfoSync()
- const winH = sys.windowHeight || sys.screenHeight
- const topPx = rect.height
- wv.setStyle({
- top: topPx,
- height: winH - topPx,
- left: 0,
- width: '100%'
- })
- })
- .exec()
- return true
- }
- const delays = [0, 50, 100, 200, 400, 800, 1200, 2000]
- delays.forEach((ms) => {
- setTimeout(() => {
- if (!apply()) {
- setTimeout(apply, 200)
- }
- }, ms)
- })
- // #endif
- }
- /** 内嵌页 load 完成后再缩窗,避免子 webview 尚未就绪时仍全屏挡在胶囊下 */
- function onEmbeddedWebviewLoad() {
- positionEmbeddedWebview()
- }
- onReady(() => {
- positionEmbeddedWebview()
- })
- onShow(() => {
- // 从后台返回时子 webview 可能重排,再对齐一次
- positionEmbeddedWebview()
- })
- /** 更多:底部菜单(当前含「用系统浏览器打开」);关闭:清栈并回到应用中心 Tab */
- function onCapsuleMoreMenu() {
- const url = decodedUrl.value
- console.log('[webview/capsule] more', { url })
- uni.showActionSheet({
- itemList: ['用系统浏览器打开'],
- success: (res) => {
- if (res.tapIndex !== 0) return
- openUrlInSystemBrowser(url)
- }
- })
- }
- function openUrlInSystemBrowser(url) {
- const u = String(url || '').trim()
- if (!u) {
- uni.showToast({ title: '链接无效', icon: 'none' })
- return
- }
- // #ifdef APP-PLUS
- try {
- plus.runtime.openURL(u)
- } catch (e) {
- uni.showToast({ title: '无法打开', icon: 'none' })
- }
- // #endif
- // #ifdef H5
- if (typeof window !== 'undefined' && window.open) {
- window.open(u, '_blank')
- }
- // #endif
- // #ifndef APP-PLUS || H5
- uni.showToast({ title: '当前环境不支持', icon: 'none' })
- // #endif
- }
- function onCapsuleClose() {
- console.log('[webview/capsule] close', { url: decodedUrl.value })
- uni.switchTab({ url: '/pages/app-center/index' })
- }
- </script>
- <style scoped>
- .webview-page {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background-color: #f8f8f8;
- }
- .header {
- padding: 0 24rpx 16rpx;
- padding-right: max(24rpx, constant(safe-area-inset-right));
- padding-right: max(24rpx, env(safe-area-inset-right));
- background-color: #ffffff;
- box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.06);
- }
- /* App:盖在原生 web-view 上,需脱离文档流避免布局错位 */
- .header--overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 99999;
- box-sizing: border-box;
- }
- .header-inner {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- width: 100%;
- }
- .capsule {
- display: flex;
- align-items: center;
- flex-shrink: 0;
- height: 52rpx;
- padding: 0 2rpx;
- background: rgba(0, 0, 0, 0.06);
- border: 1rpx solid rgba(0, 0, 0, 0.08);
- border-radius: 26rpx;
- }
- .capsule-btn {
- width: 52rpx;
- height: 52rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .capsule-icon {
- width: 30rpx;
- height: 30rpx;
- }
- .capsule-divider {
- width: 1rpx;
- height: 26rpx;
- background: rgba(0, 0, 0, 0.12);
- }
- .webview {
- flex: 1;
- }
- </style>
|