diff --git a/MiniApp/Skills/miniapp-dev/api-reference.md b/MiniApp/Skills/miniapp-dev/api-reference.md index ba65c17ed1..54194ddec9 100644 --- a/MiniApp/Skills/miniapp-dev/api-reference.md +++ b/MiniApp/Skills/miniapp-dev/api-reference.md @@ -297,6 +297,8 @@ app.onLocaleChange((locale) => { }); ``` +MiniApp 加载完成后会立即收到一次当前激活状态(Tab 在前台收到 `activate`,在后台收到 `deactivate`),之后每次 Tab 切入/切走时触发对应钩子。适合用来暂停/恢复轮询、动画等后台不需要的工作。 + ## 国际化 i18n ### `app.t(table, fallback)` — 多语言字符串挑选 diff --git a/src/web-ui/src/app/scenes/SceneViewport.tsx b/src/web-ui/src/app/scenes/SceneViewport.tsx index 369f4bdb30..a576926bae 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.tsx +++ b/src/web-ui/src/app/scenes/SceneViewport.tsx @@ -140,7 +140,7 @@ function renderScene( return ; default: if (typeof id === 'string' && id.startsWith('miniapp:')) { - return ; + return ; } return null; } diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx index 98b8b4d4cb..d3880c13bf 100644 --- a/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx +++ b/src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx @@ -33,9 +33,10 @@ const MINIAPP_REFRESH_EVENTS = [ interface MiniAppSceneProps { appId: string; + isActive?: boolean; } -const MiniAppScene: React.FC = ({ appId }) => { +const MiniAppScene: React.FC = ({ appId, isActive = true }) => { const openApp = useMiniAppStore((state) => state.openApp); const closeApp = useMiniAppStore((state) => state.closeApp); const markCustomizationActive = useMiniAppStore((state) => state.markCustomizationActive); @@ -206,7 +207,7 @@ const MiniAppScene: React.FC = ({ appId }) => { )} - + {customizePreview && (
diff --git a/src/web-ui/src/app/scenes/miniapps/components/MiniAppRunner.tsx b/src/web-ui/src/app/scenes/miniapps/components/MiniAppRunner.tsx index f543414ec2..574f3cfe43 100644 --- a/src/web-ui/src/app/scenes/miniapps/components/MiniAppRunner.tsx +++ b/src/web-ui/src/app/scenes/miniapps/components/MiniAppRunner.tsx @@ -11,11 +11,13 @@ import type { MiniAppRunScope } from '../customization/miniAppCustomizationTypes interface MiniAppRunnerProps { app: MiniApp; runScope?: MiniAppRunScope; + /** Whether the hosting scene tab is active; drives activate/deactivate lifecycle events. */ + isActive?: boolean; } -const MiniAppRunner: React.FC = ({ app, runScope }) => { +const MiniAppRunner: React.FC = ({ app, runScope, isActive = true }) => { const iframeRef = useRef(null); - useMiniAppBridge(iframeRef, app, runScope ?? { kind: 'active', appId: app.id }); + useMiniAppBridge(iframeRef, app, runScope ?? { kind: 'active', appId: app.id }, isActive); const writeCompiledHtml = useCallback(() => { const iframe = iframeRef.current; diff --git a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts index 0c6c2bd3e3..c3b2305578 100644 --- a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts +++ b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts @@ -4,7 +4,9 @@ * ai.* → Host AI client, agent.* → Host agent bridge (hidden subagent runs), * deck.renderPage → hidden host WebView slide rasterization (export), * clipboard.* → Host navigator.clipboard. - * Also handles bitfun/request-theme and pushes theme changes to the iframe. + * Also handles bitfun/request-theme and pushes theme changes to the iframe, + * and pushes activate/deactivate lifecycle events as the hosting scene tab + * gains/loses activation (consumed via app.onActivate/app.onDeactivate). */ import { useLayoutEffect, useRef, useEffect, RefObject } from 'react'; import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI'; @@ -37,6 +39,7 @@ export function useMiniAppBridge( iframeRef: RefObject, app: MiniApp, runScope: MiniAppRunScope, + isActive: boolean = true, ) { const { workspacePath } = useCurrentWorkspace(); const { theme: currentTheme } = useTheme(); @@ -47,6 +50,12 @@ export function useMiniAppBridge( workspacePathRef.current = workspacePath; const localeRef = useRef(currentLanguage); localeRef.current = currentLanguage; + const isActiveRef = useRef(isActive); + isActiveRef.current = isActive; + // The compiled bridge script announces readiness by sending bitfun/request-theme + // after registering its message listener; activation pushes before that point + // would be dropped by the still-loading iframe. + const bridgeReadyRef = useRef(false); const runScopeRef = useRef(runScope); runScopeRef.current = runScope; @@ -94,6 +103,11 @@ export function useMiniAppBridge( '*', ); } + bridgeReadyRef.current = true; + iframeRef.current?.contentWindow?.postMessage( + { type: 'bitfun:event', event: isActiveRef.current ? 'activate' : 'deactivate' }, + '*', + ); return; } @@ -394,6 +408,17 @@ export function useMiniAppBridge( ); }, [currentLanguage, iframeRef]); + // Push activation transitions (scene tab shown/hidden) to the iframe. + // Before the bridge handshake the initial state is delivered by the + // bitfun/request-theme handler above, so pre-handshake pushes are skipped. + useEffect(() => { + if (!bridgeReadyRef.current || !iframeRef.current?.contentWindow) return; + iframeRef.current.contentWindow.postMessage( + { type: 'bitfun:event', event: isActive ? 'activate' : 'deactivate' }, + '*', + ); + }, [isActive, iframeRef]); + // Listen for AI stream events from Tauri and forward them to the iframe. useEffect(() => { const currentAppId = app.id;