Sfoglia il codice sorgente

低内存手机判断

liuq 2 settimane fa
parent
commit
8d9063000e

+ 2 - 0
App.vue

@@ -6,11 +6,13 @@
 	const USER_KEY = 'current_user'
 	// #ifdef APP-PLUS
 	import { scheduleAndroidApkUpdateCheck, maybeCheckAndroidApkUpdateByInterval } from './utils/appUpgrade'
+	import { maybeApplyAndroidWeakDeviceOpenUrlDefault } from './utils/openUrlPreference'
 	// #endif
 
 	export default {
 		onLaunch: async function() {
 			// #ifdef APP-PLUS
+			maybeApplyAndroidWeakDeviceOpenUrlDefault()
 			if (uni.getSystemInfoSync().platform === 'android') {
 				scheduleAndroidApkUpdateCheck()
 			}

+ 6 - 0
pages.json

@@ -18,6 +18,12 @@
 				"navigationBarTitleText": "修改密码"
 			}
 		},
+		{
+			"path": "pages/app-settings/index",
+			"style": {
+				"navigationBarTitleText": "应用设置"
+			}
+		},
 		{
 			"path": "pages/index/index",
 			"style": {

+ 2 - 6
pages/app-center/index.vue

@@ -112,6 +112,7 @@
 <script>
 	import UserAvatar from '../../components/UserAvatar.vue'
 	import { getToken, getLaunchpadApps, ssoLogin, getUserIdFromToken } from '../../utils/api'
+	import { openEmbeddedOrSystemBrowser } from '../../utils/openUrlPreference'
 
 	const USER_KEY = 'current_user'
 	const RECENT_APPS_KEY_PREFIX = 'launchpad_recent_apps_v1_'
@@ -445,12 +446,7 @@
 					// 登录成功并获得 redirect_url 后,把该应用记录为最新使用
 					this.recordRecentApp(app.id)
 
-					const pageUrl =
-						'/pages/webview/index?url=' +
-						encodeURIComponent(redirectUrl) +
-						'&title=' +
-						encodeURIComponent(app.name || '应用')
-					uni.navigateTo({ url: pageUrl })
+					openEmbeddedOrSystemBrowser(redirectUrl, app.name || '应用')
 				} catch (e) {
 					uni.showToast({ title: '打开失败', icon: 'none' })
 				} finally {

+ 107 - 0
pages/app-settings/index.vue

@@ -0,0 +1,107 @@
+<template>
+	<view class="app-settings-page">
+		<view class="card">
+			<view class="row">
+				<view class="label-block">
+					<text class="label">用手机浏览器打开链接</text>
+					<text class="sub">开启后,应用中心与聊天中的网页链接在手机系统浏览器中打开;关闭则使用应用内浏览器。</text>
+				</view>
+				<switch :checked="openInSystemBrowser" color="#259653" @change="onSwitchChange" />
+			</view>
+			<view v-if="deviceLines.length" class="snapshot-block">
+				<text v-for="(line, idx) in deviceLines" :key="idx" class="snapshot-line">{{ line }}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {
+		getOpenUrlInSystemBrowser,
+		setOpenUrlInSystemBrowser,
+		getOpenUrlDeviceSnapshot,
+		formatOpenUrlDeviceSnapshotLines,
+		refreshAndroidOpenUrlDeviceSnapshot
+	} from '../../utils/openUrlPreference'
+
+	export default {
+		data() {
+			return {
+				openInSystemBrowser: false,
+				deviceLines: []
+			}
+		},
+		onShow() {
+			this.openInSystemBrowser = getOpenUrlInSystemBrowser()
+			this.syncDeviceSnapshotDisplay()
+		},
+		methods: {
+			syncDeviceSnapshotDisplay() {
+				refreshAndroidOpenUrlDeviceSnapshot()
+				const snap = getOpenUrlDeviceSnapshot()
+				this.deviceLines = formatOpenUrlDeviceSnapshotLines(snap)
+			},
+			onSwitchChange(e) {
+				const v = !!(e && e.detail && e.detail.value)
+				this.openInSystemBrowser = v
+				setOpenUrlInSystemBrowser(v, { fromUser: true })
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.app-settings-page {
+		min-height: 100vh;
+		background-color: #f5f5f5;
+		padding: 24rpx;
+		box-sizing: border-box;
+	}
+	.card {
+		background: #fff;
+		border-radius: 16rpx;
+		overflow: hidden;
+	}
+	.row {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		gap: 24rpx;
+		padding: 28rpx 32rpx;
+		min-height: 112rpx;
+	}
+	.label-block {
+		flex: 1;
+		min-width: 0;
+	}
+	.label {
+		display: block;
+		font-size: 30rpx;
+		color: #333;
+		line-height: 1.4;
+	}
+	.sub {
+		display: block;
+		margin-top: 12rpx;
+		font-size: 24rpx;
+		color: #999;
+		line-height: 1.5;
+	}
+	.snapshot-block {
+		padding: 0 32rpx 28rpx 32rpx;
+		border-top: 1rpx solid #f0f0f0;
+	}
+	.snapshot-line {
+		display: block;
+		font-size: 24rpx;
+		color: #666;
+		line-height: 1.65;
+		margin-top: 8rpx;
+	}
+	.snapshot-line:first-child {
+		margin-top: 0;
+		font-size: 26rpx;
+		color: #333;
+		font-weight: 500;
+	}
+</style>

+ 3 - 12
pages/chat/index.vue

@@ -107,6 +107,7 @@ import { useMessages } from '../../composables/useMessages'
 import { useContacts } from '../../composables/useContacts'
 import { chatStore } from '../../store/chat'
 import { getMessageCallbackUrl, getToken, markHistoryReadAll } from '../../utils/api'
+import { openEmbeddedOrSystemBrowser } from '../../utils/openUrlPreference'
 import { fetchUnreadCountAndUpdateTabBar } from '../../composables/useUnreadBadge'
 
 const otherUserId = ref('')
@@ -331,21 +332,11 @@ async function openNotificationUrl(msg) {
 		const res = await getMessageCallbackUrl(token, msg.id)
 		const url = res.callback_url || res.callbackUrl || msg.actionUrl
 		if (url) {
-			const pageUrl =
-				'/pages/webview/index?url=' +
-				encodeURIComponent(url) +
-				'&title=' +
-				encodeURIComponent(msg.title || '详情')
-			uni.navigateTo({ url: pageUrl })
+			openEmbeddedOrSystemBrowser(url, msg.title || '详情')
 		}
 	} catch (e) {
 		if (msg.actionUrl) {
-			const pageUrl =
-				'/pages/webview/index?url=' +
-				encodeURIComponent(msg.actionUrl) +
-				'&title=' +
-				encodeURIComponent(msg.title || '详情')
-			uni.navigateTo({ url: pageUrl })
+			openEmbeddedOrSystemBrowser(msg.actionUrl, msg.title || '详情')
 		} else {
 			uni.showToast({ title: '打开失败', icon: 'none' })
 		}

+ 7 - 0
pages/profile/index.vue

@@ -56,6 +56,7 @@
 			>
 				检测更新
 			</view>
+			<view class="action-btn app-settings-btn" @click="onAppSettings">应用设置</view>
 			<view class="action-btn change-pwd-btn" @click="onChangePassword">修改密码</view>
 			<view class="logout-btn" @click="onLogout">退出登录</view>
 		</view>
@@ -185,6 +186,9 @@
 				manualCheckAndroidApkUpdate()
 				// #endif
 			},
+			onAppSettings() {
+				uni.navigateTo({ url: '/pages/app-settings/index' })
+			},
 			onChangePassword() {
 				uni.navigateTo({ url: '/pages/change-password/index' })
 			},
@@ -320,6 +324,9 @@
 	.check-update-btn {
 		color: #259653;
 	}
+	.app-settings-btn {
+		color: #333;
+	}
 	.change-pwd-btn {
 		color: #333;
 	}

+ 2 - 6
pages/search-center/index.vue

@@ -143,6 +143,7 @@
 import UserAvatar from '../../components/UserAvatar.vue'
 import SystemAvatar from '../../components/SystemAvatar.vue'
 import { getToken, getLaunchpadApps, searchUsers, ssoLogin } from '../../utils/api'
+import { openEmbeddedOrSystemBrowser } from '../../utils/openUrlPreference'
 import { chatStore } from '../../store/chat'
 
 export default {
@@ -305,12 +306,7 @@ export default {
 					uni.showToast({ title: '打开失败', icon: 'none' })
 					return
 				}
-				const pageUrl =
-					'/pages/webview/index?url=' +
-					encodeURIComponent(redirectUrl) +
-					'&title=' +
-					encodeURIComponent(app.name || '应用')
-				uni.navigateTo({ url: pageUrl })
+				openEmbeddedOrSystemBrowser(redirectUrl, app.name || '应用')
 			} catch (e) {
 				uni.showToast({ title: '打开失败', icon: 'none' })
 			} finally {

+ 334 - 0
utils/openUrlPreference.js

@@ -0,0 +1,334 @@
+/** 本地存储:为 true 时用系统浏览器打开链接,否则用内置 WebView */
+export const OPEN_URL_IN_SYSTEM_BROWSER_KEY = 'open_url_in_system_browser'
+
+/** 是否已做过「首次启动弱机默认」检测(全应用仅一次) */
+export const OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY = 'open_url_device_default_applied'
+
+/** 用户在「应用设置」里手动改过开关后为 true,不再自动覆盖 */
+export const OPEN_URL_USER_EXPLICIT_SET_KEY = 'open_url_user_explicit_set'
+
+/** 首次/刷新时写入的检测结果(JSON) */
+export const OPEN_URL_DEVICE_SNAPSHOT_KEY = 'open_url_device_snapshot'
+
+/** Android 12 对应 API 31 */
+const ANDROID_12_API_LEVEL = 31
+/** 物理内存低于此值(GB)视为弱机,默认用系统浏览器打开链接 */
+const WEAK_DEVICE_RAM_GB = 8
+
+/**
+ * Java Long / 任意桥接值为字节数
+ * @param {*} v
+ * @returns {number}
+ */
+function javaLongishToNumber(v) {
+	if (v == null || v === undefined) return 0
+	if (typeof v === 'number' && Number.isFinite(v)) return v
+	if (typeof v === 'string') {
+		const n = parseFloat(v)
+		return Number.isFinite(n) ? n : 0
+	}
+	try {
+		if (typeof plus !== 'undefined' && plus.android && plus.android.invoke) {
+			const lv = plus.android.invoke(v, 'longValue')
+			if (typeof lv === 'number' && Number.isFinite(lv)) return lv
+		}
+	} catch (e) {}
+	try {
+		const n = Number(v)
+		return Number.isFinite(n) ? n : 0
+	} catch (e2) {}
+	return 0
+}
+
+/**
+ * 读取 /proc/meminfo 中 MemTotal(kB)换算为字节(多数机型可用,作 ActivityManager 失败时的回退)
+ * @returns {number}
+ */
+function readMemTotalBytesFromProc() {
+	// #ifdef APP-PLUS
+	let br = null
+	try {
+		const FileReader = plus.android.importClass('java.io.FileReader')
+		const BufferedReader = plus.android.importClass('java.io.BufferedReader')
+		const fr = new FileReader('/proc/meminfo')
+		br = new BufferedReader(fr)
+		let line = br.readLine()
+		let guard = 0
+		while (line != null && guard < 400) {
+			guard++
+			const s = String(line)
+			if (s.indexOf('MemTotal:') === 0) {
+				const parts = s.trim().split(/\s+/)
+				const kb = parseInt(parts[1], 10)
+				if (Number.isFinite(kb) && kb > 0) return kb * 1024
+				return 0
+			}
+			line = br.readLine()
+		}
+	} catch (e) {
+	} finally {
+		try {
+			if (br) br.close()
+		} catch (e2) {}
+	}
+	// #endif
+	return 0
+}
+
+/**
+ * 物理内存总字节数(Android App)
+ * @returns {{ bytes: number, source: string }}
+ */
+function readAndroidTotalRamBytes() {
+	const empty = { bytes: 0, source: '' }
+	// #ifdef APP-PLUS
+	try {
+		const main = plus.android.runtimeMainActivity()
+		const ActivityManager = plus.android.importClass('android.app.ActivityManager')
+		const Context = plus.android.importClass('android.content.Context')
+		const MemoryInfo = plus.android.importClass('android.app.ActivityManager$MemoryInfo')
+		const am = main.getSystemService(Context.ACTIVITY_SERVICE)
+		const memInfo = new MemoryInfo()
+
+		try {
+			plus.android.invoke(am, 'getMemoryInfo', memInfo)
+		} catch (e) {
+			try {
+				am.getMemoryInfo(memInfo)
+			} catch (e2) {}
+		}
+
+		let raw = null
+		try {
+			raw = plus.android.invoke(memInfo, 'totalMem')
+		} catch (e) {}
+		if (raw == null || raw === undefined) {
+			try {
+				raw = memInfo.totalMem
+			} catch (e2) {}
+		}
+
+		const fromAm = javaLongishToNumber(raw)
+		if (fromAm > 0) return { bytes: fromAm, source: 'ActivityManager' }
+
+		const fromProc = readMemTotalBytesFromProc()
+		if (fromProc > 0) return { bytes: fromProc, source: '/proc/meminfo' }
+
+		return empty
+	} catch (e) {
+		const fromProc = readMemTotalBytesFromProc()
+		if (fromProc > 0) return { bytes: fromProc, source: '/proc/meminfo' }
+		return empty
+	}
+	// #endif
+	return empty
+}
+
+/**
+ * 读取 Android 设备信息(App 端)。用于快照与弱机判断。
+ * @returns {object|null}
+ */
+function collectAndroidSnapshot() {
+	// #ifdef APP-PLUS
+	try {
+		if (uni.getSystemInfoSync().platform !== 'android') return null
+
+		const out = {
+			platform: 'android',
+			at: Date.now(),
+			androidSdkInt: null,
+			androidRelease: '',
+			totalRamBytes: null,
+			totalRamGb: null,
+			ramSource: '',
+			weakOs: false,
+			weakRam: false,
+			weakDevice: false
+		}
+
+		const VERSION = plus.android.importClass('android.os.Build$VERSION')
+		out.androidSdkInt = Number(VERSION.SDK_INT)
+		out.androidRelease = String(VERSION.RELEASE || '')
+		out.weakOs = out.androidSdkInt < ANDROID_12_API_LEVEL
+
+		const ram = readAndroidTotalRamBytes()
+		if (ram.bytes > 0) {
+			out.totalRamBytes = ram.bytes
+			out.totalRamGb = Math.round((ram.bytes / (1024 * 1024 * 1024)) * 100) / 100
+			out.weakRam = out.totalRamGb < WEAK_DEVICE_RAM_GB
+			out.ramSource = ram.source || ''
+		}
+
+		out.weakDevice = !!(out.weakOs || out.weakRam)
+		return out
+	} catch (e) {
+		return {
+			platform: 'android',
+			at: Date.now(),
+			error: String((e && e.message) || e)
+		}
+	}
+	// #endif
+	return null
+}
+
+function persistSnapshot(snap) {
+	if (!snap) return
+	try {
+		uni.setStorageSync(OPEN_URL_DEVICE_SNAPSHOT_KEY, JSON.stringify(snap))
+	} catch (e) {}
+}
+
+/**
+ * 供应用设置页展示多行说明
+ * @param {object|null} s getOpenUrlDeviceSnapshot() 返回值
+ * @returns {string[]}
+ */
+export function formatOpenUrlDeviceSnapshotLines(s) {
+	if (!s || typeof s !== 'object') return []
+	if (s.platform === 'android') {
+		const lines = ['本机检测(已保存;进入本页会刷新内存与系统信息)']
+		if (s.androidSdkInt != null) {
+			lines.push(`Android API:${s.androidSdkInt}(低于 31 视为系统版本偏低)`)
+		}
+		lines.push(`Android 版本:${s.androidRelease || '—'}`)
+		if (s.totalRamGb != null) {
+			const src = s.ramSource ? `,来源:${s.ramSource}` : ''
+			lines.push(`物理内存:约 ${s.totalRamGb} GB(低于 8 GB 视为内存偏低)${src}`)
+		} else {
+			lines.push('物理内存:未能读取(ActivityManager 与 /proc/meminfo 均失败)')
+		}
+		lines.push(`弱机判定:${s.weakDevice ? '是' : '否'}(系统偏低或内存偏低)`)
+		if (s.error) lines.push(`读取异常:${s.error}`)
+		return lines
+	}
+	if (s.note) return [String(s.note)]
+	return []
+}
+
+/**
+ * 进入应用设置等页面时刷新并保存检测快照(不改变首次默认逻辑)。
+ */
+export function refreshAndroidOpenUrlDeviceSnapshot() {
+	// #ifdef APP-PLUS
+	const snap = collectAndroidSnapshot()
+	persistSnapshot(snap)
+	// #endif
+}
+
+/**
+ * @returns {object|null}
+ */
+export function getOpenUrlDeviceSnapshot() {
+	try {
+		const raw = uni.getStorageSync(OPEN_URL_DEVICE_SNAPSHOT_KEY)
+		if (raw == null || raw === '') return null
+		if (typeof raw === 'object') return raw
+		return JSON.parse(String(raw))
+	} catch (e) {
+		return null
+	}
+}
+
+function userHasExplicitOpenUrlPreference() {
+	try {
+		return !!uni.getStorageSync(OPEN_URL_USER_EXPLICIT_SET_KEY)
+	} catch (e) {
+		return false
+	}
+}
+
+/**
+ * 应用首次启动执行一次(见 App.vue onLaunch):
+ * 仅 Android:系统版本低于 12 或物理内存低于 8GB 时,若用户未在设置里手动改过,则默认开启「用手机浏览器打开」。
+ */
+export function maybeApplyAndroidWeakDeviceOpenUrlDefault() {
+	// #ifdef APP-PLUS
+	try {
+		if (uni.getStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY)) return
+
+		const platform = uni.getSystemInfoSync().platform
+		if (platform !== 'android') {
+			try {
+				uni.setStorageSync(OPEN_URL_DEVICE_SNAPSHOT_KEY, JSON.stringify({
+					platform,
+					at: Date.now(),
+					note: '弱机默认仅 Android 检测'
+				}))
+			} catch (e) {}
+			uni.setStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY, true)
+			return
+		}
+
+		const snap = collectAndroidSnapshot()
+		persistSnapshot(snap)
+
+		const weak =
+			snap &&
+			!snap.error &&
+			(snap.weakOs || snap.weakRam)
+
+		if (weak && !userHasExplicitOpenUrlPreference()) {
+			setOpenUrlInSystemBrowser(true)
+		}
+		uni.setStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY, true)
+	} catch (e) {
+		try {
+			uni.setStorageSync(OPEN_URL_DEVICE_DEFAULT_APPLIED_KEY, true)
+		} catch (e2) {}
+	}
+	// #endif
+}
+
+export function getOpenUrlInSystemBrowser() {
+	try {
+		const v = uni.getStorageSync(OPEN_URL_IN_SYSTEM_BROWSER_KEY)
+		return v === true || v === 'true' || v === 1
+	} catch (e) {
+		return false
+	}
+}
+
+/**
+ * @param {boolean} value
+ * @param {{ fromUser?: boolean }} [options] fromUser 为 true 时表示用户在应用设置中操作,之后不再被弱机默认覆盖
+ */
+export function setOpenUrlInSystemBrowser(value, options) {
+	try {
+		uni.setStorageSync(OPEN_URL_IN_SYSTEM_BROWSER_KEY, !!value)
+		if (options && options.fromUser) {
+			uni.setStorageSync(OPEN_URL_USER_EXPLICIT_SET_KEY, true)
+		}
+	} catch (e) {}
+}
+
+/**
+ * 按用户偏好:系统浏览器或内置 WebView。
+ * @param {string} url
+ * @param {string} [title] 内置 WebView 标题
+ */
+export function openEmbeddedOrSystemBrowser(url, title) {
+	const u = String(url || '').trim()
+	if (!u) return
+	const encTitle = encodeURIComponent(title || '页面')
+	const pageUrl =
+		'/pages/webview/index?url=' + encodeURIComponent(u) + '&title=' + encTitle
+
+	if (getOpenUrlInSystemBrowser()) {
+		// #ifdef APP-PLUS
+		try {
+			if (typeof plus !== 'undefined' && plus.runtime && typeof plus.runtime.openURL === 'function') {
+				plus.runtime.openURL(u)
+				return
+			}
+		} catch (e) {}
+		// #endif
+		// #ifdef H5
+		if (typeof window !== 'undefined' && typeof window.open === 'function') {
+			window.open(u, '_blank')
+			return
+		}
+		// #endif
+	}
+	uni.navigateTo({ url: pageUrl })
+}