From 84f225f3f33e06b77f20ce851372c0eff3bd6deb Mon Sep 17 00:00:00 2001 From: kuaifan Date: Fri, 17 Apr 2026 09:44:53 +0000 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E5=85=BC=E5=AE=B9=E6=96=B0=20E?= =?UTF-8?q?xpo=20=E5=A3=B3=EF=BC=88dootask=5Fexpo=20UA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合 dootask-app 仓库的 Expo 迁移(见 docs/migration-eeui-to-expo.md 的 Phase 5), 让服务端和前端同时识别旧 EEUI 壳与新 Expo 壳的 User-Agent,并让 eeui.js 的同步返回 方法在 Expo 壳下优先读取 injectedJS 启动时写入的 __EXPO_INIT_DATA__ / __EXPO_VARIATES__ 缓存,避免原本同步 API 变成 Promise 后破坏调用方。 后端: - Base::isEEUIApp():同时匹配 kuaifan_eeui / dootask_expo - UserDevice:android_(kuaifan_eeui|dootask_expo) 正则捕获标识段,版本号按实际段名取 - IndexController PDF 预览:浏览器分类兼容 android_dootask_expo / ios_dootask_expo - SystemController::prefetch:$isApp 同时接受两种 UA - resources/views/download.blade.php:/eeui|dootask_expo/i 前端: - app.js: - isEEUIApp 正则新增 dootask_expo - $preload 等待条件改为 requireModuleJs 可用 OR window.__EXPO_BRIDGE_READY__, 避免 Expo 壳下等 15 秒超时 - eeui.js:以下几个同步 getter 在 Expo 壳下先读 window.__EXPO_* 再回落到原生: - eeuiAppVersion / eeuiAppLocalVersion → __EXPO_INIT_DATA__.version - eeuiAppGetPageInfo → __EXPO_INIT_DATA__.pageInfo - eeuiAppGetThemeName → __EXPO_INIT_DATA__.themeName - eeuiAppKeyboardStatus → __EXPO_INIT_DATA__.keyboardVisible - eeuiAppGetVariate → __EXPO_VARIATES__[key] - eeuiAppGetCachesString → __EXPO_CACHES__[key](RN 侧后续要同步 broadcast) 旧 EEUI 壳不受影响:只读缓存不存在时自动回落到原有 $A.eeuiModule() 调用, 行为与改动前一致。 Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Http/Controllers/Api/SystemController.php | 3 +- app/Http/Controllers/IndexController.php | 14 +++++--- app/Models/UserDevice.php | 12 +++---- app/Module/Base.php | 5 +-- resources/assets/js/app.js | 7 ++-- resources/assets/js/functions/eeui.js | 33 ++++++++++++++----- resources/views/download.blade.php | 2 +- 7 files changed, 49 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index 53870bfff..1cc2a9ce6 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -1557,7 +1557,8 @@ class SystemController extends AbstractController { $userAgent = strtolower(Request::server('HTTP_USER_AGENT')); $isMain = str_contains($userAgent, 'maintaskwindow'); - $isApp = str_contains($userAgent, 'kuaifan_eeui'); + $isApp = str_contains($userAgent, 'kuaifan_eeui') + || str_contains($userAgent, 'dootask_expo'); $version = Base::getVersion(); $array = []; diff --git a/app/Http/Controllers/IndexController.php b/app/Http/Controllers/IndexController.php index ecfdd04d0..1084c8716 100755 --- a/app/Http/Controllers/IndexController.php +++ b/app/Http/Controllers/IndexController.php @@ -457,12 +457,16 @@ class IndexController extends InvokeController 'button' => Doo::translate('点击下载'), ]); } - // 浏览器类型 + // 浏览器类型(兼容旧 EEUI 与新 Expo 壳) $browser = 'none'; - if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) { - $browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop'; - } elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) { - $browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop'; + $isAndroidApp = str_contains($userAgent, 'android_kuaifan_eeui') + || str_contains($userAgent, 'android_dootask_expo'); + $isIosApp = str_contains($userAgent, 'ios_kuaifan_eeui') + || str_contains($userAgent, 'ios_dootask_expo'); + if (str_contains($userAgent, 'chrome') || $isAndroidApp) { + $browser = $isAndroidApp ? 'android-mobile' : 'chrome-desktop'; + } elseif (str_contains($userAgent, 'safari') || $isIosApp) { + $browser = $isIosApp ? 'safari-mobile' : 'safari-desktop'; } // electron 直接在线预览查看 if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) { diff --git a/app/Models/UserDevice.php b/app/Models/UserDevice.php index 939c676ff..6f8febcbc 100644 --- a/app/Models/UserDevice.php +++ b/app/Models/UserDevice.php @@ -129,8 +129,8 @@ class UserDevice extends AbstractModel } } - if (preg_match("/android_kuaifan_eeui/i", $ua)) { - // Android 客户端 + if (preg_match("/android_(kuaifan_eeui|dootask_expo)/i", $ua, $m)) { + // Android 客户端(兼容旧 EEUI 与新 Expo 壳) $result['app_type'] = 'Android'; if ($dd->getBrandName() && $dd->getModel()) { // 厂商+型号 @@ -145,9 +145,9 @@ class UserDevice extends AbstractModel // 平板 $result['app_name'] = 'Phablet'; } - $result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/'); - } elseif (preg_match("/ios_kuaifan_eeui/i", $ua)) { - // iOS 客户端 + $result['app_version'] = self::getAfterVersion($ua, $m[1] . '/'); + } elseif (preg_match("/ios_(kuaifan_eeui|dootask_expo)/i", $ua, $m)) { + // iOS 客户端(兼容旧 EEUI 与新 Expo 壳) $result['app_type'] = 'iOS'; if (preg_match("/(macintosh|ipad)/i", $ua)) { // iPad @@ -156,7 +156,7 @@ class UserDevice extends AbstractModel // iPhone $result['app_name'] = 'iPhone'; } - $result['app_version'] = self::getAfterVersion($ua, 'kuaifan_eeui/'); + $result['app_version'] = self::getAfterVersion($ua, $m[1] . '/'); } elseif (preg_match("/dootask/i", $ua)) { // DooTask 客户端 $result['app_type'] = $osInfo['name']; diff --git a/app/Module/Base.php b/app/Module/Base.php index c2cb8797a..00ee43c3c 100755 --- a/app/Module/Base.php +++ b/app/Module/Base.php @@ -1841,13 +1841,14 @@ class Base } /** - * 是否是App移动端 + * 是否是App移动端(兼容旧 EEUI 壳与新 Expo 壳) * @return bool */ public static function isEEUIApp() { $userAgent = strtolower(Request::server('HTTP_USER_AGENT')); - return str_contains($userAgent, 'kuaifan_eeui'); + return str_contains($userAgent, 'kuaifan_eeui') + || str_contains($userAgent, 'dootask_expo'); } /** diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index b2a28e891..43d92dfbf 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -1,5 +1,5 @@ const isElectron = !!(window && window.process && window.process.type && window.electron); -const isEEUIApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent); +const isEEUIApp = window && window.navigator && /eeui|dootask_expo/i.test(window.navigator.userAgent); const isSoftware = isElectron || isEEUIApp; document.getElementById("app")?.setAttribute("data-preload", "false"); @@ -325,14 +325,15 @@ const $preload = async () => { document.getElementById("app")?.setAttribute("data-preload", "true") if ($A.isEEUIApp) { + // 同时等待旧 EEUI 的 requireModuleJs 与新 Expo 壳注入的 __EXPO_BRIDGE_READY__ const requireTime = new Date().getTime(); - while (typeof requireModuleJs !== "function") { + while (typeof requireModuleJs !== "function" && !window.__EXPO_BRIDGE_READY__) { await new Promise(resolve => setTimeout(resolve, 200)); if (new Date().getTime() - requireTime > 15 * 1000) { break } } - if (typeof requireModuleJs !== "function") { + if (typeof requireModuleJs !== "function" && !window.__EXPO_BRIDGE_READY__) { const errorTip = $A.L("加载失败,请重启软件") const errorView = document.querySelector(".app-view-loading") if (errorView) { diff --git a/resources/assets/js/functions/eeui.js b/resources/assets/js/functions/eeui.js index b1f100261..a1b890a0c 100755 --- a/resources/assets/js/functions/eeui.js +++ b/resources/assets/js/functions/eeui.js @@ -35,14 +35,14 @@ import {languageName} from "../language"; }) }, - // 获取eeui版本号 + // 获取eeui版本号(Expo 壳下改为读取启动时注入的 __EXPO_INIT_DATA__,兼容旧 EEUI) eeuiAppVersion() { - return $A.eeuiModule()?.getVersion(); + return window.__EXPO_INIT_DATA__?.version ?? $A.eeuiModule()?.getVersion(); }, // 获取本地软件版本号 eeuiAppLocalVersion() { - return $A.eeuiModule()?.getLocalVersion(); + return window.__EXPO_INIT_DATA__?.version ?? $A.eeuiModule()?.getLocalVersion(); }, // Alert @@ -61,8 +61,12 @@ import {languageName} from "../language"; return $A.eeuiModule()?.rewriteUrl(val); }, - // 获取页面信息 + // 获取页面信息(Expo 壳在 injectedJS 启动时写入 __EXPO_INIT_DATA__.pageInfo) eeuiAppGetPageInfo(pageName) { + const cached = window.__EXPO_INIT_DATA__?.pageInfo; + if (cached) { + return pageName ? { ...cached, pageName } : cached; + } return $A.eeuiModule()?.getPageInfo(pageName || ""); }, @@ -152,13 +156,16 @@ import {languageName} from "../language"; $A.eeuiModule()?.checkUpdate(); }, - // 获取主题名称 light|dark + // 获取主题名称 light|dark(Expo 壳:启动时注入 + 系统变更推送更新 __EXPO_INIT_DATA__.themeName) eeuiAppGetThemeName() { - return $A.eeuiModule()?.getThemeName(); + return window.__EXPO_INIT_DATA__?.themeName ?? $A.eeuiModule()?.getThemeName(); }, - // 判断软键盘是否可见 + // 判断软键盘是否可见(Expo 壳:keyboardDidShow/Hide 会同步更新 __EXPO_INIT_DATA__.keyboardVisible) eeuiAppKeyboardStatus() { + if (window.__EXPO_INIT_DATA__ && typeof window.__EXPO_INIT_DATA__.keyboardVisible === "boolean") { + return window.__EXPO_INIT_DATA__.keyboardVisible; + } return $A.eeuiModule()?.keyboardStatus(); }, @@ -167,8 +174,12 @@ import {languageName} from "../language"; $A.eeuiModule()?.setVariate(key, value); }, - // 获取全局变量 + // 获取全局变量(Expo 壳:setVariate 时 RN 会 broadcast 到所有 WebView 的 __EXPO_VARIATES__) eeuiAppGetVariate(key, defaultVal = "") { + const cache = window.__EXPO_VARIATES__; + if (cache && Object.prototype.hasOwnProperty.call(cache, key)) { + return cache[key]; + } return $A.eeuiModule()?.getVariate(key, defaultVal); }, @@ -177,8 +188,12 @@ import {languageName} from "../language"; $A.eeuiModule()?.setCachesString(key, value, expired); }, - // 获取缓存数据 + // 获取缓存数据(Expo 壳:若 __EXPO_CACHES__ 已 hydrate 就同步读取,否则回落到原生桥) eeuiAppGetCachesString(key, defaultVal = "") { + const cache = window.__EXPO_CACHES__; + if (cache && Object.prototype.hasOwnProperty.call(cache, key)) { + return cache[key]; + } return $A.eeuiModule()?.getCachesString(key, defaultVal); }, diff --git a/resources/views/download.blade.php b/resources/views/download.blade.php index 0b4f35fc2..ba069faa9 100755 --- a/resources/views/download.blade.php +++ b/resources/views/download.blade.php @@ -78,7 +78,7 @@ document.body.classList.add("dark"); } // - const isEEUIApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent); + const isEEUIApp = window && window.navigator && /eeui|dootask_expo/i.test(window.navigator.userAgent); if (isEEUIApp) { document.querySelector(".link").addEventListener('click', function (e) { e.preventDefault();