From f479047fc9e46dc641d1aa833ccde0c0c7877b95 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 17 Jun 2026 06:55:52 +0000 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E4=B8=BB=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=20nativeApp=20=E6=A1=A5=E6=8E=A5=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E6=94=B9=E9=80=A0=20+=20/share=20=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E9=A1=B5=20+=20=E5=AD=90=E6=A8=A1=E5=9D=97=E5=88=87=E5=88=B0?= =?UTF-8?q?=20Expo=20=E5=B7=A5=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新移动端从 EEUI(weex) 切到 Expo(react-native),UA 标识从 'eeui' 改为 'DooTaskApp', 桥接底层从全局 requireModuleJs 切到 window.__nativeBridge.call(method,args) Promise RPC。 约 52 个文件批量改名(256 处引用): - 平台识别:$A.isEEUIApp/Vue.prototype.$isEEUIApp → isMobileApp / $isMobileApp app.js 顶部 UA 正则 /eeui/i → /DooTaskApp/i app.js requireModuleJs 等待逻辑替换为 window.__nativeBridge 就绪判定 - 桥接 API:$A.eeuiAppXXX → $A.nativeAppXXX - functions/eeui.js 删除,重写为 functions/native-app.js 底层走 window.__nativeBridge.call;nativeAppSendMessage 把旧 sendMessage 的 各 action 拆解为独立 method 调用(updateTheme/windowSize/setBadge/vibrate/ callTel/picturePreview/videoPreview/startMeeting/updateMeetingInfo 等) - types/dootask-globals.d.ts:方法签名同步改名 - store/actions.js:清理 saveUserInfoBase 内旧的 userChatList/userUploadUrl sendMessage(二期由 /share 接收页直接调接口替代) 二期 /share 接收页(跨 App 分享): - pages/share.vue(新增):原生 Share Extension / Android Intent 通过 dootask://share?token=... 拉起 App 后跳本页; 调 nativeAppGetSharedPayload 取分享元数据; 复用 api/users/share/list 渲染会话列表; 用户选会话 → POST api/dialog/msg/sendfiles 上传 + sendtext 发文本/URL; 完成后 nativeAppClearSharedPayload 清理 + 跳目标会话 - routes.js 注册 /share 路由 - language/original-web.txt 加 share.vue 用到的文案 构建链路(electron/build.js): - appbuild app 子命令:前端产物从复制到 resources/mobile/src/public + EEUI docker 构建 改为同步到 resources/mobile/assets/web(由 plugins/withWebAssets 注入原生 bundle) - android-upload apk 路径:从 platforms/android/eeuiApp/... 改为 android/app/build/outputs/apk/release(Expo prebuild 标准布局) 子模块 resources/mobile 指针更新到新 Expo 工程提交(feat/mobile-expo-rewrite 分支)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- electron/build.js | 55 +-- language/original-web.txt | 7 + resources/assets/js/App.vue | 50 +-- resources/assets/js/app.js | 28 +- resources/assets/js/components/AutoTip.vue | 2 +- resources/assets/js/components/Loading.vue | 2 +- .../assets/js/components/MicroApps/index.vue | 6 +- .../assets/js/components/Mobile/Back.vue | 6 +- .../js/components/Mobile/Notification.vue | 2 +- .../assets/js/components/Mobile/Tabbar.vue | 2 +- .../js/components/PreviewImage/state.vue | 14 +- .../assets/js/components/Replace/utils.js | 2 +- .../assets/js/components/RightBottom.vue | 4 +- resources/assets/js/components/SearchBox.vue | 6 +- .../assets/js/components/TableAction.vue | 2 +- .../assets/js/components/UserAvatar/tip.vue | 2 +- resources/assets/js/components/UserSelect.vue | 4 +- resources/assets/js/functions/common.js | 4 +- resources/assets/js/functions/eeui.js | 381 ------------------ resources/assets/js/functions/native-app.js | 194 +++++++++ resources/assets/js/functions/web.js | 18 +- resources/assets/js/pages/login.vue | 6 +- resources/assets/js/pages/manage.vue | 10 +- .../assets/js/pages/manage/application.vue | 4 +- .../manage/components/ChatInput/index.vue | 24 +- .../pages/manage/components/DialogModal.vue | 2 +- .../pages/manage/components/DialogWrapper.vue | 14 +- .../pages/manage/components/FileContent.vue | 4 +- .../pages/manage/components/FileHistory.vue | 2 +- .../components/MeetingManager/index.vue | 6 +- .../components/MeetingManager/player.vue | 2 +- .../pages/manage/components/ProjectList.vue | 2 +- .../js/pages/manage/components/ProjectLog.vue | 2 +- .../pages/manage/components/ProjectPanel.vue | 10 +- .../manage/components/ProjectWorkflow.vue | 2 +- .../js/pages/manage/components/ReportEdit.vue | 4 +- .../js/pages/manage/components/TaskAdd.vue | 2 +- .../pages/manage/components/TaskAddSimple.vue | 4 +- .../manage/components/TaskContentHistory.vue | 2 +- .../js/pages/manage/components/TaskDetail.vue | 12 +- .../js/pages/manage/components/TaskModal.vue | 2 +- .../js/pages/manage/components/TaskRow.vue | 2 +- .../js/pages/manage/components/UserDetail.vue | 4 +- .../assets/js/pages/manage/dashboard.vue | 2 +- resources/assets/js/pages/manage/file.vue | 6 +- .../assets/js/pages/manage/messenger.vue | 10 +- .../assets/js/pages/manage/projectInvite.vue | 2 +- .../setting/components/SystemSetting.vue | 2 +- .../assets/js/pages/manage/setting/index.vue | 12 +- .../js/pages/manage/setting/keyboard.vue | 2 +- resources/assets/js/pages/share.vue | 334 +++++++++++++++ resources/assets/js/pages/single/file.vue | 2 +- .../assets/js/pages/single/taskContent.vue | 2 +- resources/assets/js/routes.js | 5 + resources/assets/js/store/actions.js | 44 +- resources/assets/js/utils/file.js | 2 +- resources/assets/js/utils/index.js | 4 +- resources/mobile | 2 +- types/dootask-globals.d.ts | 78 ++-- 59 files changed, 772 insertions(+), 650 deletions(-) delete mode 100755 resources/assets/js/functions/eeui.js create mode 100755 resources/assets/js/functions/native-app.js create mode 100644 resources/assets/js/pages/share.vue diff --git a/electron/build.js b/electron/build.js index 69c5d90ea..d20e77516 100644 --- a/electron/build.js +++ b/electron/build.js @@ -401,48 +401,15 @@ async function startBuild(data) { fs.writeFileSync(indexFile, indexString, 'utf8'); // if (data.id === 'app') { - const eeuiDir = path.resolve(__dirname, "../resources/mobile"); - const publicDir = path.resolve(__dirname, "../resources/mobile/src/public"); - const containerName = `dootask-eeui-${Date.now()}-${process.pid}`; - fse.removeSync(publicDir) - fse.copySync(electronDir, publicDir) - if (argv[3] === "publish") { - // Android config - const gradleFile = path.resolve(eeuiDir, "platforms/android/eeuiApp/local.properties") - let gradleResult = fs.existsSync(gradleFile) ? fs.readFileSync(gradleFile, 'utf8') : ""; - gradleResult = gradleResult.replace(/(versionCode|versionName)\s*=\s*(.+?)(\n|$)/g, '') - gradleResult += `versionCode = ${config.codeVerson}\nversionName = ${config.version}\n` - fs.writeFileSync(gradleFile, gradleResult, 'utf8') - // iOS config - const xcconfigFile = path.resolve(eeuiDir, "platforms/ios/eeuiApp/Config/Version.xcconfig") - let xcconfigResult = fs.existsSync(xcconfigFile) ? fs.readFileSync(xcconfigFile, 'utf8') : ""; - xcconfigResult = xcconfigResult.replace(/(VERSION_CODE|VERSION_NAME)\s*=\s*(.+?)(\n|$)/g, '') - xcconfigResult += `VERSION_CODE = ${config.codeVerson}\nVERSION_NAME = ${config.version}\n` - fs.writeFileSync(xcconfigFile, xcconfigResult, 'utf8') - } - if (['build', 'publish'].includes(argv[3])) { - child_process.execSync( - `docker run -d --name ${containerName} -v ${shellQuote(eeuiDir)}:/work -w /work kuaifan/eeui-cli:0.0.1 sleep infinity`, - {stdio: "ignore", cwd: "resources/mobile"} - ); - try { - if (!fs.existsSync(path.resolve(eeuiDir, "node_modules"))) { - child_process.execSync(`docker exec ${containerName} npm install`, {stdio: "inherit", cwd: "resources/mobile"}); - } - child_process.execSync(`docker exec ${containerName} node /work/scripts/patch-eeui-build.js`, {stdio: "inherit", cwd: "resources/mobile"}); - child_process.execSync(`docker exec ${containerName} eeui build --simple`, {stdio: "inherit", cwd: "resources/mobile"}); - } finally { - child_process.execSync(`docker rm -f ${containerName}`, {stdio: "ignore", cwd: "resources/mobile"}); - } - } else { - [ - path.resolve(publicDir, "../../platforms/ios/eeuiApp/bundlejs/eeui/public"), - path.resolve(publicDir, "../../platforms/android/eeuiApp/app/src/main/assets/eeui/public"), - ].some(dir => { - fse.removeSync(dir) - fse.copySync(electronDir, dir) - }) - } + // 新 Expo 移动端:将前端构建产物作为离线包同步到 resources/mobile/assets/web, + // 由 config plugin(withWebAssets) 在 prebuild 时注入原生 bundle,运行时本地静态服务 serve。 + const mobileDir = path.resolve(__dirname, "../resources/mobile"); + const webDir = path.resolve(mobileDir, "assets/web"); + fse.removeSync(webDir) + fse.copySync(electronDir, webDir) + console.log(`移动端离线包已同步: ${webDir}`); + // 版本号由 resources/mobile/app.config.ts 从根 package.json 动态读取,无需改原生工程; + // App 原生打包在 resources/mobile 下执行 expo prebuild + eas build(见 WS7)。 return; } const output = `dist/${data.id.replace(/\./g, '-')}/${platform}` @@ -560,7 +527,9 @@ if (["dev"].includes(argv[2])) { process.exit(1) } const client = r2.createR2Client() - const releaseDir = path.resolve(__dirname, "../resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release"); + // 新 Expo 移动端:apk 由 EAS Build 输出(或本地 expo prebuild + gradle assembleRelease)。 + // EAS 流水线产物路径示例:android/app/build/outputs/apk/release 或 EAS 远端构建直接上传。 + const releaseDir = path.resolve(__dirname, "../resources/mobile/android/app/build/outputs/apk/release"); if (!fs.existsSync(releaseDir)) { console.error("发布文件未找到") process.exit(1) diff --git a/language/original-web.txt b/language/original-web.txt index 6820838c2..665640aae 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2469,3 +2469,10 @@ AI任务分析 操作引导启动失败 最多只能添加(*)个 该标签已存在 +发送给 +搜索会话 +暂无会话 +缺少分享凭据 +分享内容不存在或已过期 +加载会话列表失败 +正在发送 diff --git a/resources/assets/js/App.vue b/resources/assets/js/App.vue index 0a1331047..4f253358e 100755 --- a/resources/assets/js/App.vue +++ b/resources/assets/js/App.vue @@ -217,11 +217,11 @@ export default { handler() { this.$store.dispatch("websocketConnection"); // - if (this.$isEEUIApp) { + if (this.$isMobileApp) { this.umengAliasTimer && clearTimeout(this.umengAliasTimer) if (this.userId > 0) { // 给 APP 发送初始化消息 - $A.eeuiAppSendMessage({ + $A.nativeAppSendMessage({ action: 'initApp', apiUrl: $A.apiUrl(''), userid: this.userId, @@ -231,7 +231,7 @@ export default { }); // 更新设备信息 - $A.eeuiAppGetDeviceInfo().then(async info => { + $A.nativeAppGetDeviceInfo().then(async info => { let deviceName = info.deviceName || info.modelName if (info.systemName === 'Android') { if ($A.strExists(info.modelName, info.brand)) { @@ -254,14 +254,14 @@ export default { // 设置友盟别名 this.umengAliasTimer = setTimeout(_ => { this.umengAliasTimer = null; - $A.eeuiAppSendMessage({ + $A.nativeAppSendMessage({ action: 'setUmengAlias', url: $A.apiUrl('users/umeng/alias') }); }, 6000) } else { // 删除友盟别名 - $A.eeuiAppSendMessage({ + $A.nativeAppSendMessage({ action: 'delUmengAlias', url: $A.apiUrl('users/umeng/alias') }); @@ -385,7 +385,7 @@ export default { }, onRouterViewMounted() { - document.documentElement.setAttribute("data-platform", $A.isElectron ? "desktop" : $A.isEEUIApp ? "app" : "web") + document.documentElement.setAttribute("data-platform", $A.isElectron ? "desktop" : $A.isMobileApp ? "app" : "web") }, /** @@ -649,12 +649,12 @@ export default { }, eeuiEvents() { - if (!this.$isEEUIApp) { + if (!this.$isMobileApp) { return; } // 隐藏快照 setTimeout(() => { - this.appActivated && $A.eeuiAppHideWebviewSnapshot() + this.appActivated && $A.nativeAppHideWebviewSnapshot() }, 500) // APP进入前台 window.__onAppActive = async () => { @@ -667,12 +667,12 @@ export default { this.autoTheme() $A.updateTimezone() - $A.eeuiAppHideWebviewSnapshot() + $A.nativeAppHideWebviewSnapshot() this.$store.dispatch("safeAreaInsets") const nowYmd = $A.daytz().format('YYYY-MM-DD') if (this.lastCheckUpgradeYmd != nowYmd) { this.lastCheckUpgradeYmd = nowYmd - $A.eeuiAppCheckUpdate(); + $A.nativeAppCheckUpdate(); } } // APP进入后台 @@ -683,12 +683,12 @@ export default { // 如果APP处于激活状态,则不显示快照 return; } - $A.eeuiAppGetWebviewSnapshot(ok => { + $A.nativeAppGetWebviewSnapshot(ok => { if (!ok || this.appActivated) { // 如果获取快照失败,或者APP处于激活状态,则不显示快照 return; } - $A.eeuiAppShowWebviewSnapshot() + $A.nativeAppShowWebviewSnapshot() }); }, 500); } @@ -714,7 +714,7 @@ export default { return; } else if (urlType === 1) { // 使用默认浏览器打开 - $A.eeuiAppOpenWeb(url); + $A.nativeAppOpenWeb(url); return; } // App 内置浏览器打开 @@ -741,7 +741,7 @@ export default { tourist_id: event.uuid, } }).then(({data}) => { - $A.eeuiAppSendMessage({ + $A.nativeAppSendMessage({ action: 'updateMeetingInfo', infos: { uuid: event.uuid, @@ -790,7 +790,7 @@ export default { } this.$store.state.keyboardShow = event.keyboardType === 'show'; this.$store.state.keyboardHeight = event.keyboardHeight; - $A.eeuiAppShakeToEditEnabled(this.$store.state.keyboardShow) + $A.nativeAppShakeToEditEnabled(this.$store.state.keyboardShow) } // 通知权限 window.__onNotificationPermissionStatus = (ret) => { @@ -801,22 +801,22 @@ export default { this.goForward({ path: (path || '').indexOf('/') !==0 ? "/" + path : path }); } // 发送网页尺寸 - $A.eeuiAppSendMessage({ + $A.nativeAppSendMessage({ action: 'windowSize', width: this.windowWidth, height: this.windowHeight, }); // 取消长按振动 - $A.eeuiAppSetHapticBackEnabled(false) + $A.nativeAppSetHapticBackEnabled(false) // 设置语言 - $A.eeuiAppSetCachesString("languageWebBack", this.$L("后退")) - $A.eeuiAppSetCachesString("languageWebForward", this.$L("前进")) - $A.eeuiAppSetCachesString("languageWebBrowser", this.$L("浏览器打开")) - $A.eeuiAppSetCachesString("languageWebRefresh", this.$L("刷新")) - $A.eeuiAppSetCachesString("updateDefaultTitle", this.$L("发现新版本")) - $A.eeuiAppSetCachesString("updateDefaultContent", this.$L("暂无更新介绍!")) - $A.eeuiAppSetCachesString("updateDefaultCancelText", this.$L("以后再说")) - $A.eeuiAppSetCachesString("updateDefaultUpdateText", this.$L("立即更新")) + $A.nativeAppSetCachesString("languageWebBack", this.$L("后退")) + $A.nativeAppSetCachesString("languageWebForward", this.$L("前进")) + $A.nativeAppSetCachesString("languageWebBrowser", this.$L("浏览器打开")) + $A.nativeAppSetCachesString("languageWebRefresh", this.$L("刷新")) + $A.nativeAppSetCachesString("updateDefaultTitle", this.$L("发现新版本")) + $A.nativeAppSetCachesString("updateDefaultContent", this.$L("暂无更新介绍!")) + $A.nativeAppSetCachesString("updateDefaultCancelText", this.$L("以后再说")) + $A.nativeAppSetCachesString("updateDefaultUpdateText", this.$L("立即更新")) }, otherEvents() { diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index b2a28e891..53e62ff68 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -1,6 +1,7 @@ const isElectron = !!(window && window.process && window.process.type && window.electron); -const isEEUIApp = window && window.navigator && /eeui/i.test(window.navigator.userAgent); -const isSoftware = isElectron || isEEUIApp; +// 新移动端在 UA 追加 DooTaskApp/(resources/mobile/src/webview/WebViewShell.tsx) +const isMobileApp = window && window.navigator && /DooTaskApp/i.test(window.navigator.userAgent); +const isSoftware = isElectron || isMobileApp; document.getElementById("app")?.setAttribute("data-preload", "false"); @@ -8,7 +9,7 @@ import {languageName, switchLanguage as $L} from "./language"; import {isLocalHost} from "./components/Replace/utils"; import './functions/common' -import './functions/eeui' +import './functions/native-app' import './functions/web' import Vue from 'vue' @@ -188,8 +189,8 @@ Vue.prototype.copyText = function (obj) { error: "复制失败" } } - if ($A.isEEUIApp) { - $A.eeuiAppCopyText(obj.text) + if ($A.isMobileApp) { + $A.nativeAppCopyText(obj.text) obj.success && $A.messageSuccess(obj.success) return } @@ -207,7 +208,7 @@ $A.Electron = null; $A.Platform = "web"; $A.isMainElectron = false; $A.isSubElectron = false; -$A.isEEUIApp = isEEUIApp; +$A.isMobileApp = isMobileApp; $A.isElectron = isElectron; $A.isSoftware = isSoftware; $A.openLog = false; @@ -216,7 +217,7 @@ if (isElectron) { $A.Platform = /macintosh|mac os x/i.test(navigator.userAgent) ? "mac" : "win"; $A.isMainElectron = /\s+MainTaskWindow\//.test(window.navigator.userAgent); $A.isSubElectron = /\s+SubTaskWindow\//.test(window.navigator.userAgent); -} else if (isEEUIApp) { +} else if (isMobileApp) { $A.Platform = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent) ? "ios" : "android"; } @@ -287,7 +288,7 @@ Vue.prototype.$Electron = $A.Electron; Vue.prototype.$Platform = $A.Platform; Vue.prototype.$isMainElectron = $A.isMainElectron; Vue.prototype.$isSubElectron = $A.isSubElectron; -Vue.prototype.$isEEUIApp = $A.isEEUIApp; +Vue.prototype.$isMobileApp = $A.isMobileApp; Vue.prototype.$isSoftware = $A.isSoftware; Vue.config.productionTip = false; @@ -324,15 +325,16 @@ const $init = async () => { const $preload = async () => { document.getElementById("app")?.setAttribute("data-preload", "true") - if ($A.isEEUIApp) { + if ($A.isMobileApp) { + // 等待新 RN 桥接传输层(window.__nativeBridge)就绪,注入时机为 webview 加载内容前 const requireTime = new Date().getTime(); - while (typeof requireModuleJs !== "function") { + while (!window.__nativeBridge) { await new Promise(resolve => setTimeout(resolve, 200)); if (new Date().getTime() - requireTime > 15 * 1000) { break } } - if (typeof requireModuleJs !== "function") { + if (!window.__nativeBridge) { const errorTip = $A.L("加载失败,请重启软件") const errorView = document.querySelector(".app-view-loading") if (errorView) { @@ -342,8 +344,8 @@ const $preload = async () => { } return } - const pageInfo = $A.eeuiAppGetPageInfo() || {}; - store.state.isFirstPage = pageInfo.pageName === 'firstPage' + // 新移动端单 WebView 套壳,永远是首屏;isFirstPage 恒 true(旧 EEUI 是多 page 容器才需要判定) + store.state.isFirstPage = true await store.dispatch("safeAreaInsets") } diff --git a/resources/assets/js/components/AutoTip.vue b/resources/assets/js/components/AutoTip.vue index 624b6ad7e..c895d65ea 100644 --- a/resources/assets/js/components/AutoTip.vue +++ b/resources/assets/js/components/AutoTip.vue @@ -4,7 +4,7 @@ :placement="placement" :effect="tooltipTheme" :delay="delay" - :disabled="$isEEUIApp || windowTouch || !showTooltip || disabled" + :disabled="$isMobileApp || windowTouch || !showTooltip || disabled" :max-width="tooltipMaxWidth" transfer> diff --git a/resources/assets/js/components/Loading.vue b/resources/assets/js/components/Loading.vue index 70703cb6a..0e023c12f 100644 --- a/resources/assets/js/components/Loading.vue +++ b/resources/assets/js/components/Loading.vue @@ -1,5 +1,5 @@