kuaifan f479047fc9 feat(mobile): 主程序前端 nativeApp 桥接契约改造 + /share 接收页 + 子模块切到 Expo 工程
新移动端从 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) <noreply@anthropic.com>
2026-06-17 06:55:52 +00:00

382 lines
12 KiB
JavaScript
Vendored
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const isElectron = !!(window && window.process && window.process.type && window.electron);
// 新移动端在 UA 追加 DooTaskApp/<codeVersion>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");
import {languageName, switchLanguage as $L} from "./language";
import {isLocalHost} from "./components/Replace/utils";
import './functions/common'
import './functions/native-app'
import './functions/web'
import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'
import routes from './routes'
import VueRouter from 'vue-router'
import VueClipboard from 'vue-clipboard2'
import ViewUI from 'view-design-hi'
import store from './store/index'
import state from "./mixins/state"
import "../sass/app.scss";
Vue.use(Vuex);
Vue.use(VueRouter);
Vue.use(VueClipboard, {
config: {
autoSetContainer: true
}
});
Vue.use(ViewUI, {
modal: {
checkEscClose: true
}
});
import PageTitle from './components/PageTitle.vue'
import Loading from './components/Loading.vue'
import AutoTip from './components/AutoTip.vue'
import TagInput from './components/TagInput.vue'
import TableAction from './components/TableAction.vue'
import QuickEdit from './components/QuickEdit.vue'
import UserAvatar from './components/UserAvatar'
import Imgs from './components/Replace/Imgs'
import ImgView from './components/ImgView.vue'
import Scrollbar from './components/Scrollbar'
Vue.component('PageTitle', PageTitle);
Vue.component('Loading', Loading);
Vue.component('AutoTip', AutoTip);
Vue.component('TagInput', TagInput)
Vue.component('TableAction', TableAction);
Vue.component('QuickEdit', QuickEdit);
Vue.component('UserAvatar', UserAvatar);
Vue.component('Imgs', Imgs);
Vue.component('ImgView', ImgView);
Vue.component('Scrollbar', Scrollbar);
import {
Avatar,
Tooltip,
Popover,
Dropdown,
DropdownMenu,
DropdownItem,
} from 'element-sea';
Vue.component('EAvatar', Avatar);
Vue.component('ETooltip', Tooltip);
Vue.component('EPopover', Popover);
Vue.component('EDropdown', Dropdown);
Vue.component('EDropdownMenu', DropdownMenu);
Vue.component('EDropdownItem', DropdownItem);
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
// 路由方式
const routeMode = isLocalHost(window.location) ? 'hash' : 'history';
const router = new VueRouter({mode: routeMode, routes});
// 进度条配置
ViewUI.LoadingBar.config({
color: '#3fcc25',
failedColor: '#ff0000'
});
router.beforeEach((to, from, next) => {
if (!isSoftware) {
ViewUI.LoadingBar._timer && clearTimeout(ViewUI.LoadingBar._timer)
ViewUI.LoadingBar._timer = setTimeout(_ => {
ViewUI.LoadingBar._load = true;
ViewUI.LoadingBar.start();
}, 300)
}
store.commit('route/loading', true);
next();
});
router.afterEach(() => {
if (!isSoftware) {
ViewUI.LoadingBar._timer && clearTimeout(ViewUI.LoadingBar._timer)
if (ViewUI.LoadingBar._load === true) {
ViewUI.LoadingBar._load = false;
ViewUI.LoadingBar.finish();
}
}
store.commit('route/loading', false);
});
// 消息配置
ViewUI.Message.config({
duration: 2.5
});
// 加载路由
Vue.prototype.goForward = function(route, isReplace, autoBroadcast = true) {
if ($A.Ready && $A.isSubElectron && autoBroadcast) {
$A.Electron.sendMessage('broadcastCommand', {
channel: 'goForward',
payload: {route, isReplace},
});
$A.Electron.sendMessage('mainWindowActive');
return
}
// 处理路由格式
if (typeof route === 'string') {
if ($A.strExists(route, '/')) {
if (/^https?:\/\//.test(route)) {
if ($A.getDomain(route) === $A.mainDomain()) {
route = route.replace(/^https?:\/\/[^\/]+/, '');
} else {
// 处理外部链接
if (isReplace) {
window.location.replace(route);
} else {
window.location.href = route;
}
return;
}
}
route = { path: route };
} else {
route = { name: route };
}
}
// 初始化路由历史
if (app.$store.state.routeHistorys.length === 0) {
app.$store.state.routeHistorys.push(app.$route)
}
// 执行路由跳转
const routerMethod = isReplace ? 'replace' : 'push';
app.$router[routerMethod](route).then(to => {
if (isReplace) {
app.$store.state.routeHistorys.pop();
app.$store.state.routeHistorys.push(to);
} else {
const length = app.$store.state.routeHistorys.push(to)
length > 120 && app.$store.state.routeHistorys.splice(length - 100)
app.$store.state.routeHistoryLast = length >= 2 ? app.$store.state.routeHistorys[length - 2] : {};
}
}).catch(err => console.warn('路由跳转失败:', err));
};
// 返回路由
Vue.prototype.goBack = function () {
if (app.$store.state.routeHistorys.length > 1) {
app.$router.back();
//
app.$store.state.routeHistorys.pop();
const length = app.$store.state.routeHistorys.length;
app.$store.state.routeHistoryLast = length >= 2 ? app.$store.state.routeHistorys[length - 2] : {};
} else {
app.$router.replace({path: '/'}).catch(_ => {});
app.$store.state.routeHistorys = [];
app.$store.state.routeHistoryLast = {};
}
};
// 复制文本
Vue.prototype.copyText = function (obj) {
if (!$A.isJson(obj)) {
obj = {
text: obj,
success: "复制成功",
error: "复制失败"
}
}
if ($A.isMobileApp) {
$A.nativeAppCopyText(obj.text)
obj.success && $A.messageSuccess(obj.success)
return
}
app.$copyText(obj.text).then(_ => {
obj.success && $A.messageSuccess(obj.success)
}).catch(_ => {
obj.error && $A.messageError(obj.error)
})
};
// 全局对象/变量
$A.L = $L;
$A.Ready = false;
$A.Electron = null;
$A.Platform = "web";
$A.isMainElectron = false;
$A.isSubElectron = false;
$A.isMobileApp = isMobileApp;
$A.isElectron = isElectron;
$A.isSoftware = isSoftware;
$A.openLog = false;
if (isElectron) {
$A.Electron = window.electron;
$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 (isMobileApp) {
$A.Platform = /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent) ? "ios" : "android";
}
// 同步执行派遣
const dispatchId = $A.randomString(6) + "_" + Date.now().toString()
$A.syncPauses = new Map();
$A.syncDispatch = (action, data) => {
if (!$A.Ready) {
return false
}
if (!isElectron) {
return false
}
if ($A.syncPauses.has(action)) {
return false;
}
if (!$A.isJson(data)) {
return false
}
if (data.__sync__ === true) {
delete data.__sync__;
return false
}
$A.Electron?.sendMessage('broadcastCommand', {
channel: 'syncDispatch',
payload: {
dispatchId,
action,
data,
}
});
return true
};
$A.Electron?.listener('syncDispatch', async ({dispatchId: targetId, action, data}) => {
if (!$A.Ready) {
return
}
if (dispatchId === targetId) {
return
}
if (!$A.isJson(data)) {
return
}
data.__sync__ = true
await store.dispatch(action, data)
})
$A.Electron?.listener('goForward', ({route, isReplace}) => {
if (!$A.Ready) {
return
}
$A.goForward(route, isReplace, false)
})
// 绑定截图快捷键
$A.bindScreenshotKey = (data) => {
let key = "";
const screenshot_key = (data.screenshot_key || "").trim().toLowerCase()
if (screenshot_key) {
key = /macintosh|mac os x/i.test(navigator.userAgent) ? 'command' : 'ctrl'
key = `${key}+shift+${screenshot_key.toLowerCase()}`
}
$A.Electron.sendMessage('bindScreenshotKey', {key});
};
Vue.prototype.$A = $A;
Vue.prototype.$L = $L;
Vue.prototype.$Electron = $A.Electron;
Vue.prototype.$Platform = $A.Platform;
Vue.prototype.$isMainElectron = $A.isMainElectron;
Vue.prototype.$isSubElectron = $A.isSubElectron;
Vue.prototype.$isMobileApp = $A.isMobileApp;
Vue.prototype.$isSoftware = $A.isSoftware;
Vue.config.productionTip = false;
Vue.mixin(state)
let app;
const $init = async () => {
const action = await store.dispatch("init");
app = new Vue({
router,
store,
render: h => h(App),
template: '<App/>',
}).$mount('#app');
$A.apiCall = (...args) => store.dispatch("call", ...args);
$A.goForward = app.goForward;
$A.goBack = app.goBack;
$A.Message = app.$Message;
$A.Notice = app.$Notice;
$A.Modal = app.$Modal;
$A.Ready = true;
if (action === "handleClearCache") {
$A.messageSuccess("清除成功");
}
if (typeof window.LANGUAGE_DATA[`i_${languageName}`] !== "undefined") {
ViewUI.locale(window.LANGUAGE_DATA[`i_${languageName}`]);
}
}
const $preload = async () => {
document.getElementById("app")?.setAttribute("data-preload", "true")
if ($A.isMobileApp) {
// 等待新 RN 桥接传输层window.__nativeBridge就绪注入时机为 webview 加载内容前
const requireTime = new Date().getTime();
while (!window.__nativeBridge) {
await new Promise(resolve => setTimeout(resolve, 200));
if (new Date().getTime() - requireTime > 15 * 1000) {
break
}
}
if (!window.__nativeBridge) {
const errorTip = $A.L("加载失败,请重启软件")
const errorView = document.querySelector(".app-view-loading")
if (errorView) {
errorView.innerHTML = `<span style="color:#f00;font-size:18px;">${errorTip}</span>`
} else {
alert(errorTip)
}
return
}
// 新移动端单 WebView 套壳永远是首屏isFirstPage 恒 true旧 EEUI 是多 page 容器才需要判定)
store.state.isFirstPage = true
await store.dispatch("safeAreaInsets")
}
await store.dispatch("preload");
const hash = (window.location[routeMode === 'hash' ? 'hash' : 'pathname']).replace(/^[#\/\s]+/, '');
if (hash !== 'preload') {
await $init()
return
}
document.querySelector(".app-view-loading")?.setAttribute("data-visible", "false")
window.__initializeApp = async (loadHash) => {
if (/^https?:\/\//.test(loadHash)) {
let mainDomain = $A.mainDomain()
if (mainDomain === "public") {
mainDomain = $A.getDomain(await $A.IDBString("cacheServerUrl"))
}
const loadDomain = $A.getDomain(loadHash)
if (loadDomain !== mainDomain && loadDomain !== window.location.host) {
window.location.href = loadHash;
return;
}
loadHash = loadHash.replace(/^https?:\/\/[^\/]+/, '');
}
if (routeMode === 'hash') {
loadHash = `#/${loadHash.replace(/^[#\/\s]+/, '')}`;
}
window.history.replaceState(null, '', loadHash)
await $init()
}
}
$preload().catch(_ => {})