|
|
@@ -1,32 +1,209 @@
|
|
|
<template>
|
|
|
<view class="webview-page">
|
|
|
- <view class="header">
|
|
|
- <view class="back" @click="goBack">
|
|
|
- <text class="back-text">返回</text>
|
|
|
+ <!-- #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>
|
|
|
- <text class="title">{{ title }}</text>
|
|
|
</view>
|
|
|
<web-view class="webview" :src="decodedUrl" />
|
|
|
+ <!-- #endif -->
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from 'vue'
|
|
|
-import { onLoad } from '@dcloudio/uni-app'
|
|
|
+import { computed, ref } from 'vue'
|
|
|
+import { onLoad, onReady, onShow } from '@dcloudio/uni-app'
|
|
|
|
|
|
const decodedUrl = ref('')
|
|
|
-const title = 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) : ''
|
|
|
- if (options && options.title) {
|
|
|
- title.value = decodeURIComponent(options.title)
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 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()
|
|
|
})
|
|
|
|
|
|
-function goBack() {
|
|
|
- uni.navigateBack()
|
|
|
+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>
|
|
|
|
|
|
@@ -38,31 +215,54 @@ function goBack() {
|
|
|
background-color: #f8f8f8;
|
|
|
}
|
|
|
.header {
|
|
|
- height: 88rpx;
|
|
|
- padding: 0 24rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
+ 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);
|
|
|
}
|
|
|
-.back {
|
|
|
- padding-right: 24rpx;
|
|
|
- padding-left: 4rpx;
|
|
|
+/* 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%;
|
|
|
}
|
|
|
-.back-text {
|
|
|
- font-size: 28rpx;
|
|
|
- color: #259653;
|
|
|
+.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;
|
|
|
}
|
|
|
-.title {
|
|
|
- flex: 1;
|
|
|
- font-size: 32rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #111827;
|
|
|
+.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>
|
|
|
-
|