|
|
@@ -2,7 +2,7 @@
|
|
|
<view class="webview-page">
|
|
|
<!-- #ifdef APP-PLUS -->
|
|
|
<!-- App:web-view 为原生层会挡住普通 view 的点击,须用 cover-view 盖在上面 -->
|
|
|
- <web-view class="webview" :src="decodedUrl" @load="onEmbeddedWebviewLoad" />
|
|
|
+ <web-view class="webview" :key="webviewKey" :src="webviewSrc" @load="onEmbeddedWebviewLoad" />
|
|
|
<cover-view class="header header--overlay" :style="headerStyle">
|
|
|
<cover-view class="header-inner">
|
|
|
<cover-view class="capsule">
|
|
|
@@ -51,16 +51,29 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
- <web-view class="webview" :src="decodedUrl" />
|
|
|
+ <web-view class="webview" :key="webviewKey" :src="webviewSrc" />
|
|
|
<!-- #endif -->
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed, nextTick, ref } from 'vue'
|
|
|
import { onLoad, onReady, onShow } from '@dcloudio/uni-app'
|
|
|
|
|
|
const decodedUrl = ref('')
|
|
|
+/** 传给 web-view 的地址;重新加载时会同步为原生子 webview 当前 getURL(含重定向后) */
|
|
|
+const webviewSrc = ref('')
|
|
|
+/** App 端每次 load 后刷新,供 getURL 暂不可用时回退 */
|
|
|
+const cachedCurrentUrl = ref('')
|
|
|
+/** 变更 key 以强制重建 web-view,实现整页重新加载 */
|
|
|
+const webviewKey = ref(0)
|
|
|
+
|
|
|
+function isUsableWebviewUrl(u) {
|
|
|
+ const s = String(u || '').trim()
|
|
|
+ if (!s) return false
|
|
|
+ if (/^about:blank$/i.test(s)) return false
|
|
|
+ return true
|
|
|
+}
|
|
|
|
|
|
/** 用系统信息算顶栏 padding-top(部分端上 CSS env 不可靠) */
|
|
|
function computeHeaderPaddingTopPx() {
|
|
|
@@ -86,7 +99,10 @@ const headerStyle = computed(() => ({
|
|
|
onLoad((options) => {
|
|
|
headerPaddingTopPx.value = computeHeaderPaddingTopPx()
|
|
|
const rawUrl = options && options.url ? options.url : ''
|
|
|
- decodedUrl.value = rawUrl ? decodeURIComponent(rawUrl) : ''
|
|
|
+ const initial = rawUrl ? decodeURIComponent(rawUrl) : ''
|
|
|
+ decodedUrl.value = initial
|
|
|
+ webviewSrc.value = initial
|
|
|
+ cachedCurrentUrl.value = initial
|
|
|
})
|
|
|
|
|
|
/**
|
|
|
@@ -95,16 +111,46 @@ onLoad((options) => {
|
|
|
* 不可假定 children()[0] 为内嵌页,须遍历带 getURL 的子窗体。
|
|
|
*/
|
|
|
// #ifdef APP-PLUS
|
|
|
-function findEmbeddedWebviewNative(currentWebview) {
|
|
|
+function urlsRoughlyMatch(a, b) {
|
|
|
+ const strip = (s) =>
|
|
|
+ String(s || '')
|
|
|
+ .trim()
|
|
|
+ .split('#')[0]
|
|
|
+ .replace(/\/$/, '')
|
|
|
+ const x = strip(a)
|
|
|
+ const y = strip(b)
|
|
|
+ if (!x || !y) return false
|
|
|
+ if (x === y) return true
|
|
|
+ try {
|
|
|
+ return new URL(x).origin === new URL(y).origin
|
|
|
+ } catch (e) {
|
|
|
+ return x.includes(y) || y.includes(x)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function findEmbeddedWebviewNative(currentWebview, preferredUrlHint) {
|
|
|
try {
|
|
|
const list = currentWebview.children && currentWebview.children()
|
|
|
if (!list || !list.length) return null
|
|
|
+ const candidates = []
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
const w = list[i]
|
|
|
if (w && typeof w.setStyle === 'function' && typeof w.getURL === 'function') {
|
|
|
- return w
|
|
|
+ candidates.push(w)
|
|
|
}
|
|
|
}
|
|
|
+ const hint = String(preferredUrlHint || '').trim()
|
|
|
+ if (candidates.length === 1) return candidates[0]
|
|
|
+ if (hint && candidates.length > 1) {
|
|
|
+ for (let i = candidates.length - 1; i >= 0; i--) {
|
|
|
+ try {
|
|
|
+ const u = candidates[i].getURL()
|
|
|
+ if (u && urlsRoughlyMatch(u, hint)) return candidates[i]
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ return candidates[candidates.length - 1]
|
|
|
+ }
|
|
|
+ if (candidates.length) return candidates[candidates.length - 1]
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
const w = list[i]
|
|
|
if (w && typeof w.setStyle === 'function') return w
|
|
|
@@ -114,14 +160,39 @@ function findEmbeddedWebviewNative(currentWebview) {
|
|
|
}
|
|
|
// #endif
|
|
|
|
|
|
-function positionEmbeddedWebview() {
|
|
|
+/** App 端返回子 webview 当前地址;其它端恒为空(无原生 getURL) */
|
|
|
+function getEmbeddedWebviewUrl() {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ try {
|
|
|
+ const pages = getCurrentPages()
|
|
|
+ const page = pages[pages.length - 1]
|
|
|
+ if (!page || typeof page.$getAppWebview !== 'function') return ''
|
|
|
+ const wv = findEmbeddedWebviewNative(page.$getAppWebview(), webviewSrc.value)
|
|
|
+ if (wv && typeof wv.getURL === 'function') {
|
|
|
+ const u = wv.getURL()
|
|
|
+ return u && String(u).trim() ? String(u).trim() : ''
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ // #endif
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+function syncCachedUrlFromEmbeddedNative() {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ const u = getEmbeddedWebviewUrl()
|
|
|
+ if (isUsableWebviewUrl(u)) cachedCurrentUrl.value = u
|
|
|
+ // #endif
|
|
|
+}
|
|
|
+
|
|
|
+function positionEmbeddedWebview(extraDelays) {
|
|
|
// #ifdef APP-PLUS
|
|
|
const pages = getCurrentPages()
|
|
|
const page = pages[pages.length - 1]
|
|
|
if (!page || typeof page.$getAppWebview !== 'function') return
|
|
|
const currentWebview = page.$getAppWebview()
|
|
|
+ const hint = webviewSrc.value
|
|
|
const apply = () => {
|
|
|
- const wv = findEmbeddedWebviewNative(currentWebview)
|
|
|
+ const wv = findEmbeddedWebviewNative(currentWebview, hint)
|
|
|
if (!wv) return false
|
|
|
uni.createSelectorQuery()
|
|
|
.select('.header')
|
|
|
@@ -140,7 +211,11 @@ function positionEmbeddedWebview() {
|
|
|
.exec()
|
|
|
return true
|
|
|
}
|
|
|
- const delays = [0, 50, 100, 200, 400, 800, 1200, 2000]
|
|
|
+ const baseDelays = [0, 50, 100, 200, 400, 800, 1200, 2000]
|
|
|
+ const delays =
|
|
|
+ Array.isArray(extraDelays) && extraDelays.length
|
|
|
+ ? [...new Set([...baseDelays, ...extraDelays])].sort((a, b) => a - b)
|
|
|
+ : baseDelays
|
|
|
delays.forEach((ms) => {
|
|
|
setTimeout(() => {
|
|
|
if (!apply()) {
|
|
|
@@ -151,54 +226,71 @@ function positionEmbeddedWebview() {
|
|
|
// #endif
|
|
|
}
|
|
|
|
|
|
-/** 内嵌页 load 完成后再缩窗,避免子 webview 尚未就绪时仍全屏挡在胶囊下 */
|
|
|
+/** 内嵌页 load 完成后再缩窗;并同步子 webview 真实 URL(302 / 前端路由后 getURL 才是当前页) */
|
|
|
function onEmbeddedWebviewLoad() {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ syncCachedUrlFromEmbeddedNative()
|
|
|
+ ;[200, 600, 1500].forEach((ms) => setTimeout(syncCachedUrlFromEmbeddedNative, ms))
|
|
|
+ // #endif
|
|
|
positionEmbeddedWebview()
|
|
|
}
|
|
|
|
|
|
+/** 子 webview 因 key 重建后需多轮缩窗,否则易保持全屏被顶栏遮挡 */
|
|
|
+function schedulePositionAfterWebviewRemount() {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ const extra = [
|
|
|
+ 10, 30, 80, 150, 300, 500, 900, 1500, 2500, 3500, 5000
|
|
|
+ ]
|
|
|
+ nextTick(() => {
|
|
|
+ positionEmbeddedWebview(extra)
|
|
|
+ setTimeout(() => positionEmbeddedWebview(extra), 0)
|
|
|
+ })
|
|
|
+ // #endif
|
|
|
+}
|
|
|
+
|
|
|
onReady(() => {
|
|
|
positionEmbeddedWebview()
|
|
|
})
|
|
|
|
|
|
onShow(() => {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ syncCachedUrlFromEmbeddedNative()
|
|
|
+ // #endif
|
|
|
// 从后台返回时子 webview 可能重排,再对齐一次
|
|
|
positionEmbeddedWebview()
|
|
|
})
|
|
|
|
|
|
-/** 更多:底部菜单(当前含「用系统浏览器打开」);关闭:清栈并回到应用中心 Tab */
|
|
|
+/** 更多:重新加载当前内嵌页;关闭:回到应用中心 Tab */
|
|
|
function onCapsuleMoreMenu() {
|
|
|
const url = decodedUrl.value
|
|
|
- console.log('[webview/capsule] more', { url })
|
|
|
+ console.log('[webview/capsule] reload menu', { url })
|
|
|
uni.showActionSheet({
|
|
|
- itemList: ['用系统浏览器打开'],
|
|
|
+ itemList: ['重新加载'],
|
|
|
success: (res) => {
|
|
|
if (res.tapIndex !== 0) return
|
|
|
- openUrlInSystemBrowser(url)
|
|
|
+ reloadEmbeddedPage()
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function openUrlInSystemBrowser(url) {
|
|
|
- const u = String(url || '').trim()
|
|
|
- if (!u) {
|
|
|
- uni.showToast({ title: '链接无效', icon: 'none' })
|
|
|
- return
|
|
|
- }
|
|
|
+function reloadEmbeddedPage() {
|
|
|
// #ifdef APP-PLUS
|
|
|
- try {
|
|
|
- plus.runtime.openURL(u)
|
|
|
- } catch (e) {
|
|
|
- uni.showToast({ title: '无法打开', icon: 'none' })
|
|
|
- }
|
|
|
+ syncCachedUrlFromEmbeddedNative()
|
|
|
// #endif
|
|
|
- // #ifdef H5
|
|
|
- if (typeof window !== 'undefined' && window.open) {
|
|
|
- window.open(u, '_blank')
|
|
|
+ const target =
|
|
|
+ getEmbeddedWebviewUrl() ||
|
|
|
+ cachedCurrentUrl.value ||
|
|
|
+ webviewSrc.value ||
|
|
|
+ decodedUrl.value
|
|
|
+ if (!isUsableWebviewUrl(target)) {
|
|
|
+ uni.showToast({ title: '链接无效', icon: 'none' })
|
|
|
+ return
|
|
|
}
|
|
|
- // #endif
|
|
|
- // #ifndef APP-PLUS || H5
|
|
|
- uni.showToast({ title: '当前环境不支持', icon: 'none' })
|
|
|
- // #endif
|
|
|
+ const url = String(target).trim()
|
|
|
+ webviewSrc.value = url
|
|
|
+ cachedCurrentUrl.value = url
|
|
|
+ webviewKey.value += 1
|
|
|
+ schedulePositionAfterWebviewRemount()
|
|
|
}
|
|
|
|
|
|
function onCapsuleClose() {
|