diff --git a/packages/parser/src/config/scriptConfig.ts b/packages/parser/src/config/scriptConfig.ts index 1e1db77e9..eee477795 100644 --- a/packages/parser/src/config/scriptConfig.ts +++ b/packages/parser/src/config/scriptConfig.ts @@ -1,4 +1,4 @@ -import {commandType} from '../interface/sceneInterface'; +import { commandType } from '../interface/sceneInterface'; export const SCRIPT_CONFIG = [ { scriptString: 'say', scriptType: commandType.say }, @@ -31,7 +31,10 @@ export const SCRIPT_CONFIG = [ { scriptString: 'setTextbox', scriptType: commandType.setTextbox }, { scriptString: 'setAnimation', scriptType: commandType.setAnimation }, { scriptString: 'playEffect', scriptType: commandType.playEffect }, - { scriptString: 'setTempAnimation', scriptType: commandType.setTempAnimation }, + { + scriptString: 'setTempAnimation', + scriptType: commandType.setTempAnimation, + }, // comment? { scriptString: 'setTransform', scriptType: commandType.setTransform }, { scriptString: 'setTransition', scriptType: commandType.setTransition }, @@ -39,6 +42,8 @@ export const SCRIPT_CONFIG = [ { scriptString: 'applyStyle', scriptType: commandType.applyStyle }, { scriptString: 'wait', scriptType: commandType.wait }, { scriptString: 'callSteam', scriptType: commandType.callSteam }, + { scriptString: 'createIframe', scriptType: commandType.createIframe }, + { scriptString: 'removeIframe', scriptType: commandType.removeIframe }, ]; export const ADD_NEXT_ARG_LIST = [ commandType.bgm, diff --git a/packages/parser/src/interface/sceneInterface.ts b/packages/parser/src/interface/sceneInterface.ts index 1b05024c8..937bc8dfc 100644 --- a/packages/parser/src/interface/sceneInterface.ts +++ b/packages/parser/src/interface/sceneInterface.ts @@ -40,6 +40,8 @@ export enum commandType { applyStyle, wait, callSteam, // 调用Steam功能 + createIframe, // 创建框架 + removeIframe, // 移除框架 } /** diff --git a/packages/webgal/src/Core/Modules/events.ts b/packages/webgal/src/Core/Modules/events.ts index 1d27b1af7..be7ba1d61 100644 --- a/packages/webgal/src/Core/Modules/events.ts +++ b/packages/webgal/src/Core/Modules/events.ts @@ -12,6 +12,8 @@ export class Events { public fullscreenDbClick = formEvent('fullscreen-dbclick'); public styleUpdate = formEvent('style-update'); public afterStyleUpdate = formEvent('after-style-update'); + public save = formEvent('save'); + public load = formEvent('load'); } const eventBus = mitt(); diff --git a/packages/webgal/src/Core/controller/gamePlay/startContinueGame.ts b/packages/webgal/src/Core/controller/gamePlay/startContinueGame.ts index e3998acd5..48b704643 100644 --- a/packages/webgal/src/Core/controller/gamePlay/startContinueGame.ts +++ b/packages/webgal/src/Core/controller/gamePlay/startContinueGame.ts @@ -10,6 +10,7 @@ import { restorePerform } from '@/Core/controller/storage/jumpFromBacklog'; import { hasFastSaveRecord, loadFastSaveGame } from '@/Core/controller/storage/fastSaveLoad'; import { WebGAL } from '@/Core/WebGAL'; +import { stageActions } from '@/store/stageReducer'; /** * 从头开始游戏 @@ -40,6 +41,7 @@ export async function continueGame() { await loadFastSaveGame(); return; } + webgalStore.dispatch(stageActions.resetIframe()); if ( WebGAL.sceneManager.sceneData.currentSentenceId === 0 && WebGAL.sceneManager.sceneData.currentScene.sceneName === 'start.txt' diff --git a/packages/webgal/src/Core/controller/scene/sceneInterface.ts b/packages/webgal/src/Core/controller/scene/sceneInterface.ts index 3e29d0ee2..54a766fb2 100644 --- a/packages/webgal/src/Core/controller/scene/sceneInterface.ts +++ b/packages/webgal/src/Core/controller/scene/sceneInterface.ts @@ -40,6 +40,8 @@ export enum commandType { applyStyle, wait, callSteam, // 调用Steam功能 + createIframe, // 创建框架 + removeIframe, // 移除框架 } /** diff --git a/packages/webgal/src/Core/controller/stage/resetStage.ts b/packages/webgal/src/Core/controller/stage/resetStage.ts index b72573834..bc0228c71 100644 --- a/packages/webgal/src/Core/controller/stage/resetStage.ts +++ b/packages/webgal/src/Core/controller/stage/resetStage.ts @@ -1,4 +1,4 @@ -import { initState, resetStageState, setStage } from '@/store/stageReducer'; +import { initState, resetStageState, setStage, stageActions } from '@/store/stageReducer'; import { webgalStore } from '@/store/store'; import cloneDeep from 'lodash/cloneDeep'; import { WebGAL } from '@/Core/WebGAL'; @@ -27,4 +27,7 @@ export const resetStage = (resetBacklog: boolean, resetSceneAndVar = true) => { if (!resetSceneAndVar) { webgalStore.dispatch(setStage({ key: 'GameVar', value: currentVars })); } + + // 清空frames + webgalStore.dispatch(stageActions.resetIframe()); }; diff --git a/packages/webgal/src/Core/controller/storage/loadGame.ts b/packages/webgal/src/Core/controller/storage/loadGame.ts index ae471ab7e..b1f729910 100644 --- a/packages/webgal/src/Core/controller/storage/loadGame.ts +++ b/packages/webgal/src/Core/controller/storage/loadGame.ts @@ -3,7 +3,7 @@ import { logger } from '../../util/logger'; import { sceneFetcher } from '../scene/sceneFetcher'; import { sceneParser } from '../../parser/sceneParser'; import { webgalStore } from '@/store/store'; -import { resetStageState } from '@/store/stageReducer'; +import { resetStageState, stageActions } from '@/store/stageReducer'; import { setVisibility } from '@/store/GUIReducer'; import { restorePerform } from './jumpFromBacklog'; import { stopAllPerform } from '@/Core/controller/gamePlay/stopAllPerform'; @@ -25,6 +25,7 @@ export const loadGame = (index: number) => { logger.debug('读取的存档数据', loadFile); // 加载存档 loadGameFromStageData(loadFile); + WebGAL.events.load.emit(index); }; export function loadGameFromStageData(stageData: ISaveData) { @@ -51,6 +52,8 @@ export function loadGameFromStageData(stageData: ISaveData) { // 强制停止所有演出 stopAllPerform(); + // 清空frames + webgalStore.dispatch(stageActions.resetIframe()); // 恢复backlog const newBacklog = loadFile.backlog; @@ -61,8 +64,19 @@ export function loadGameFromStageData(stageData: ISaveData) { // 恢复舞台状态 const newStageState = cloneDeep(loadFile.nowStageState); + // 保存iframes的持久化数据 + const iframePersistentData = new Map>(); + newStageState.iframes.forEach((iframe) => { + if (iframe.persistentData) { + iframePersistentData.set(iframe.id, iframe.persistentData); + } + }); + // iframes将被指令创建,我们不需要使用存档中的iframes + newStageState.iframes = []; const dispatch = webgalStore.dispatch; dispatch(resetStageState(newStageState)); + // 将持久化数据存储到全局变量中,供后续创建iframe时使用 + (window as any).__iframePersistentData = iframePersistentData; // 恢复演出 setTimeout(restorePerform, 0); diff --git a/packages/webgal/src/Core/controller/storage/saveGame.ts b/packages/webgal/src/Core/controller/storage/saveGame.ts index c729b334c..83c552230 100644 --- a/packages/webgal/src/Core/controller/storage/saveGame.ts +++ b/packages/webgal/src/Core/controller/storage/saveGame.ts @@ -17,6 +17,7 @@ export const saveGame = (index: number) => { const saveData: ISaveData = generateCurrentStageData(index); webgalStore.dispatch(saveActions.saveGame({ index, saveData })); dumpSavesToStorage(index, index); + WebGAL.events.save.emit(index); }; /** diff --git a/packages/webgal/src/Core/gameScripts/createIframe.ts b/packages/webgal/src/Core/gameScripts/createIframe.ts new file mode 100644 index 000000000..b9361fa0e --- /dev/null +++ b/packages/webgal/src/Core/gameScripts/createIframe.ts @@ -0,0 +1,141 @@ +import { ISentence } from '@/Core/controller/scene/sceneInterface'; +import { IPerform } from '@/Core/Modules/perform/performInterface'; +import { getBooleanArgByKey, getStringArgByKey } from '../util/getSentenceArg'; +import { IIFrame } from '@/store/stageInterface'; +import { webgalStore } from '@/store/store'; +import { stageActions } from '@/store/stageReducer'; +import { CSSProperties } from 'react'; + +const allSandboxProperties = { + 'allow-forms': 'allowForms', // 允许iframe内提交表单 + 'allow-scripts': 'allowScripts', // 允许iframe内执行JavaScript脚本(包括定时器、事件等) + 'allow-same-origin': 'allowSameOrigin', // 允许iframe内容拥有同源身份,可访问自身Cookie/LocalStorage等 + 'allow-top-navigation': 'allowTopNavigation', // 允许iframe内的链接跳转到父页面(主页面)的上下文 + 'allow-popups': 'allowPopups', // 允许iframe通过window.open()等方式弹出新窗口 + 'allow-modals': 'allowModals', // 允许iframe弹出模态窗口(如alert()、confirm()、prompt()) + 'allow-pointer-lock': 'allowPointerLock', // 允许iframe使用Pointer Lock API(如游戏鼠标锁定) + 'allow-popups-to-escape-sandbox': 'allowPopupsToEscapeSandbox', // 允许iframe弹出的新窗口不受当前沙箱限制 + 'allow-downloads': 'allowDownloads', // 允许iframe内触发文件下载操作 + 'allow-presentation': 'allowPresentation', // 允许iframe使用Presentation API(投屏/演示功能) + 'allow-top-navigation-by-user-activation': 'allowTopNavigationByUserActivation', // 仅允许用户主动触发(如点击)的顶级导航操作 + 'allow-storage-access-by-user-activation': 'allowStorageAccessByUserActivation', // 允许用户主动触发后访问父页面的存储权限 + 'allow-orientation-lock': 'allowOrientationLock', // 允许iframe使用Screen Orientation API锁定屏幕方向 +}; + +/** + * 创建框架 + * @param sentence + */ +export const createIframe = (sentence: ISentence): IPerform => { + const src = sentence.content; + const id = getStringArgByKey(sentence, 'id') ?? ''; + const wait = getBooleanArgByKey(sentence, 'wait') ?? false; + const hidden = getBooleanArgByKey(sentence, 'hidden') ?? false; + const returnValue = getStringArgByKey(sentence, 'returnValue') ?? null; + const width = getStringArgByKey(sentence, 'width') ?? undefined; + const height = getStringArgByKey(sentence, 'height') ?? undefined; + if (!id || !src) { + return { + performName: 'none', + duration: 0, + isHoldOn: false, + stopFunction: () => {}, + blockingNext: () => false, + blockingAuto: () => true, + stopTimeout: undefined, + }; + } + + // 可能后续会增加更多的样式,所以我们先定义一个空对象 + // 还没想好如何优雅的使用指令定义样式,所以暂时使用已知参数定义样式 + let styleCSSProperties: CSSProperties = {}; + if (hidden) { + styleCSSProperties.display = 'none'; + styleCSSProperties.opacity = 0; + } + + let rawSrc = src; + // 处理src + if (!rawSrc.startsWith('http://') && !rawSrc.startsWith('https://')) { + rawSrc = './game/' + rawSrc; + } + + // 查询所有参数(以@开头) + const args = sentence.args; + const injectArgs = + args.filter((arg) => arg.key.startsWith('@')).map((arg) => ({ key: arg.key.slice(1), value: arg.value })) ?? {}; + + const frameData: IIFrame = { + id, + src: rawSrc, + sandbox: '', + width, + height, + isActive: true, + wait, + returnValue, + injectArgs, + style: styleCSSProperties, + }; + + for (const [key, value] of Object.entries(allSandboxProperties)) { + const v = getStringArgByKey(sentence, value) ?? ''; + if (v) { + frameData.sandbox += key + ' '; + } + } + + webgalStore.dispatch(stageActions.addIframe(frameData)); + + // 如果需要等待iframe完成,则返回阻塞的perform + if (wait) { + let isCompleted = false; + // 监听iframe完成消息 + const handleFrameComplete = (event: MessageEvent) => { + if ( + event.data && + typeof event.data === 'object' && + event.data.type === 'webgal-frame-complete' && + event.data.frameId === id + ) { + isCompleted = true; + // 如果有returnValue,则存储到游戏变量中 + if (returnValue && event.data.returnValue !== undefined) { + webgalStore.dispatch( + stageActions.setStageVar({ + key: returnValue, + value: event.data.returnValue, + }), + ); + } + // 移除事件监听器 + window.removeEventListener('message', handleFrameComplete); + } + }; + + // 添加事件监听器 + window.addEventListener('message', handleFrameComplete); + + return { + performName: `frame-wait-${id}`, + duration: 0, + isHoldOn: true, + stopFunction: () => { + window.removeEventListener('message', handleFrameComplete); + }, + blockingNext: () => !isCompleted, + blockingAuto: () => !isCompleted, + stopTimeout: undefined, + }; + } + + return { + performName: 'none', + duration: 0, + isHoldOn: false, + stopFunction: () => {}, + blockingNext: () => false, + blockingAuto: () => true, + stopTimeout: undefined, + }; +}; diff --git a/packages/webgal/src/Core/gameScripts/removeIframe.ts b/packages/webgal/src/Core/gameScripts/removeIframe.ts new file mode 100644 index 000000000..6cf0aa43a --- /dev/null +++ b/packages/webgal/src/Core/gameScripts/removeIframe.ts @@ -0,0 +1,37 @@ +import { ISentence } from '@/Core/controller/scene/sceneInterface'; +import { IPerform } from '@/Core/Modules/perform/performInterface'; +import { stageActions } from '@/store/stageReducer'; +import { webgalStore } from '@/store/store'; +import { getBooleanArgByKey } from '../util/getSentenceArg'; + +/** + * 移除框架 + * @param sentence + */ +export const removeIframe = (sentence: ISentence): IPerform => { + const id = sentence.content; + const save = getBooleanArgByKey(sentence, 'save') ?? false; + if (!id) { + return { + performName: 'none', + duration: 0, + isHoldOn: false, + stopFunction: () => {}, + blockingNext: () => false, + blockingAuto: () => true, + stopTimeout: undefined, + }; + } + + webgalStore.dispatch(stageActions.removeIframe({ id, isActive: save })); + + return { + performName: 'none', + duration: 0, + isHoldOn: false, + stopFunction: () => {}, + blockingNext: () => false, + blockingAuto: () => true, + stopTimeout: undefined, + }; +}; diff --git a/packages/webgal/src/Core/parser/sceneParser.ts b/packages/webgal/src/Core/parser/sceneParser.ts index 85b3ebea3..a3bf33200 100644 --- a/packages/webgal/src/Core/parser/sceneParser.ts +++ b/packages/webgal/src/Core/parser/sceneParser.ts @@ -28,6 +28,8 @@ import { setTransition } from '@/Core/gameScripts/setTransition'; import { unlockBgm } from '@/Core/gameScripts/unlockBgm'; import { unlockCg } from '@/Core/gameScripts/unlockCg'; import { callSteam } from '@/Core/gameScripts/callSteam'; +import { createIframe } from '@/Core/gameScripts/createIframe'; +import { removeIframe } from '@/Core/gameScripts/removeIframe'; import { end } from '../gameScripts/end'; import { jumpLabel } from '../gameScripts/jumpLabel'; import { pixiInit } from '../gameScripts/pixi/pixiInit'; @@ -74,6 +76,8 @@ export const SCRIPT_TAG_MAP = defineScripts({ applyStyle: ScriptConfig(commandType.applyStyle, applyStyle, { next: true }), wait: ScriptConfig(commandType.wait, wait), callSteam: ScriptConfig(commandType.callSteam, callSteam, { next: true }), + createIframe: ScriptConfig(commandType.createIframe, createIframe), + removeIframe: ScriptConfig(commandType.removeIframe, removeIframe), }); export const SCRIPT_CONFIG: IConfigInterface[] = Object.values(SCRIPT_TAG_MAP); diff --git a/packages/webgal/src/Stage/Iframe/Iframe.tsx b/packages/webgal/src/Stage/Iframe/Iframe.tsx new file mode 100644 index 000000000..2fb810af3 --- /dev/null +++ b/packages/webgal/src/Stage/Iframe/Iframe.tsx @@ -0,0 +1,342 @@ +import { useEffect, useMemo, useRef, useCallback, SyntheticEvent } from 'react'; +import { IIFrame } from '@/store/stageInterface'; +import { RootState, webgalStore } from '@/store/store'; +import { useSelector } from 'react-redux'; +import { isEqual } from 'lodash'; +import { ReactiveWatcher, WebGalAPI, WebGalAPIEventsKeyNames } from './interface'; +import { setStageVar, stageActions } from '@/store/stageReducer'; +import { setScriptManagedGlobalVar } from '@/store/userDataReducer'; +import { WebGAL } from '@/Core/WebGAL'; +import { nextSentence as nextSentenceController } from '@/Core/controller/gamePlay/nextSentence'; + +export default function Iframe({ id, sandbox, src, width, height, wait, injectArgs, style }: IIFrame) { + const idString = `iframe-${id}`; + const iframeRef = useRef(null); + const stage = useSelector((state: RootState) => state.stage, isEqual); + const GUI = useSelector((state: RootState) => state.GUI, isEqual); + const userData = useSelector((state: RootState) => state.userData, isEqual); + const saveData = useSelector((state: RootState) => state.saveData, isEqual); + + // 尝试从全局变量中恢复持久化数据 + useEffect(() => { + const globalPersistentData = (window as any).__iframePersistentData; + if (globalPersistentData?.has(id)) { + const persistentData = globalPersistentData.get(id); + webgalStore.dispatch( + stageActions.updateIframePersistentData({ + id, + persistentData, + }), + ); + // 恢复后从全局变量中删除 + globalPersistentData.delete(id); + } + }, [id]); + + const store = useMemo(() => ({ stage, GUI, userData, saveData }), [stage, GUI, userData, saveData]); + + const watchersRef = useRef>(new Map()); + const watcherIdRef = useRef(0); + + // 通过webgal的EventBus触发,我们只做桥接 + const eventsMap = useMemo( + () => ({ save: WebGAL.events.save, load: WebGAL.events.load, sentence: WebGAL.events.userInteractNext }), + [], + ); + + // 获取嵌套属性的值 + const getNestedValue = useCallback((obj: any, path: string) => { + return path.split('.').reduce((acc, part) => acc?.[part], obj); + }, []); + + // 获取当前值的函数 + const getValue = useCallback( + (source: string | string[] | ((store: RootState) => any), state: RootState) => { + const isFunction = typeof source === 'function'; + const isArray = Array.isArray(source); + const isString = typeof source === 'string'; + + if (isFunction) { + return (source as (store: RootState) => any)(state); + } else if (isArray) { + const result: any = {}; + (source as string[]).forEach((key) => { + result[key] = getNestedValue(state, key); + }); + return result; + } else if (isString) { + return getNestedValue(state, source as string); + } + return state; + }, + [getNestedValue], + ); + + // 检测所有watcher监听的值的变化 + const watchedValues = useSelector((state: RootState) => { + const result: Record = {}; + watchersRef.current.forEach((watcher, watcherId) => { + result[watcherId] = getValue(watcher.source, state); + }); + return result; + }, isEqual); + + useEffect(() => { + watchersRef.current.forEach((watcher, watcherId) => { + const newValue = watchedValues[watcherId]; + const oldValue = watcher.oldValue; + + const hasChanged = !isEqual(newValue, oldValue); + + if (hasChanged) { + watcher.callback(newValue, oldValue); + // 更新watcher的oldValue + watchersRef.current.set(watcherId, { + ...watcher, + oldValue: newValue, + }); + } + }); + }, [watchedValues]); + + const apiInstance = useMemo((): WebGalAPI => { + const api: WebGalAPI = Object.create(null); + // 获取响应式状态的方法 + api.getReactiveStore = (source, callback, options = {}) => { + // 创建新的watcher ID + const watcherId = watcherIdRef.current++; + + // 获取当前值 + const currentValue = getValue(source, store); + + // 添加watcher到列表 + watchersRef.current.set(watcherId, { + source, + callback, + options, + oldValue: currentValue, + }); + + // 立即执行回调函数 + if (options.immediate) { + callback(currentValue, undefined); + } + + // 返回取消订阅的函数 + return () => { + watchersRef.current.delete(watcherId); + }; + }; + // 获取特定状态的方法 + api.getStageState = () => store.stage; + api.getGUIState = () => store.GUI; + api.getUserData = () => store.userData; + api.getSaveData = () => store.saveData; + // 操作 + api.isBlockSentence = () => { + let isBlockingNext = false; + WebGAL.gameplay.performController.performList.forEach((e) => { + if (e.blockingNext()) + // 阻塞且没有结束的演出 + isBlockingNext = true; + }); + return isBlockingNext; + }; + api.nextSentence = () => { + // 触发下一句执行 + nextSentenceController(); + }; + // 事件 + api.on = (event: WebGalAPIEventsKeyNames, callback: (data?: any) => void) => { + if (!eventsMap[event]) { + console.error(`无效的事件类型: ${event}`); + return; + } + eventsMap[event].on(callback); + }; + api.off = (event: WebGalAPIEventsKeyNames, callback: (data?: any) => void) => { + if (!eventsMap[event]) { + console.error(`无效的事件类型: ${event}`); + return; + } + eventsMap[event].off(callback); + }; + api.closeIframe = (key?: string) => { + if (key) { + webgalStore.dispatch(stageActions.removeIframe({ id: key })); + } else { + webgalStore.dispatch(stageActions.removeIframe({ id })); + } + }; + api.openIframe = (key?: string) => { + if (!key) { + console.warn('openIframe 需要指定 iframe 的 key'); + return; + } + // 从 stage.iframes 中查找指定 key 的 iframe + const iframe = stage.iframes.find((e) => e.id === key); + if (!iframe) { + console.error(`找不到 id 为 ${key} 的 iframe`); + return; + } + // 如果 iframe 已经存在且被关闭,则重新添加到 iframes 列表中 + const existingIndex = stage.iframes.findIndex((e) => e.id === key); + if (existingIndex === -1) { + webgalStore.dispatch(stageActions.addIframe(iframe)); + } + }; + api.getGameVar = (key: string) => store.stage.GameVar[key]; + api.getGlobalGameVar = (key: string) => store.userData.globalGameVar[key]; + api.setGameVar = (key: string, value: any) => webgalStore.dispatch(setStageVar({ key, value })); + api.setGlobalGameVar = (key: string, value: any) => webgalStore.dispatch(setScriptManagedGlobalVar({ key, value })); + api.complete = (returnValue?: any) => { + if (wait) { + window.parent.postMessage( + { + type: 'webgal-frame-complete', + frameId: id, + returnValue, + }, + window.location.origin, // 使用当前页面的源 + ); + } + }; + // 持久化数据方法 + api.getPersistentData = (key?: string) => { + const iframe = stage.iframes.find((e) => e.id === id); + if (!iframe?.persistentData) { + return key ? undefined : {}; + } + return key ? iframe.persistentData[key] : iframe.persistentData; + }; + api.setPersistentData = (key: string, value: any) => { + const iframe = stage.iframes.find((e) => e.id === id); + if (!iframe) { + console.error(`找不到id为${id}的iframe`); + return; + } + const currentData = iframe.persistentData || {}; + webgalStore.dispatch( + stageActions.updateIframePersistentData({ + id, + persistentData: { ...currentData, [key]: value }, + }), + ); + }; + api.clearPersistentData = (key?: string) => { + const iframe = stage.iframes.find((e) => e.id === id); + if (!iframe) { + console.error(`找不到id为${id}的iframe`); + return null; + } + if (key) { + // 清除指定key的数据 + if (iframe.persistentData?.[key]) { + const newData = { ...iframe.persistentData }; + delete newData[key]; + webgalStore.dispatch( + stageActions.updateIframePersistentData({ + id, + persistentData: newData, + }), + ); + } + } else { + // 清除所有持久化数据 + webgalStore.dispatch( + stageActions.updateIframePersistentData({ + id, + persistentData: {}, + }), + ); + } + }; + api.postIframeMessage = (key: string, data?: any) => { + // 查找目标iframe + const targetIframe = stage.iframes.find((e) => e.id === key); + if (!targetIframe) { + console.error(`找不到id为${key}的iframe`); + return; + } + + // 查找目标iframe的DOM元素 + const targetIframeElement = document.getElementById(`iframe-${key}`) as HTMLIFrameElement; + if (!targetIframeElement) { + console.error(`找不到id为iframe-${key}的iframe元素`); + return; + } + + // 向目标iframe发送消息 + try { + targetIframeElement.contentWindow?.postMessage( + { + type: 'webgal-iframe-message', + sourceId: id, + data, + }, + window.location.origin, + ); + } catch (e) { + console.error(`向iframe ${key} 发送消息失败:`, e); + } + }; + return api; + }, [store, id, wait]); + + const onError = useCallback((e: SyntheticEvent) => { + console.error('iframe加载失败', e); + // 加载失败,则移除该iframe + webgalStore.dispatch(stageActions.removeIframe({ id })); + }, []); + + if (!src || !id) { + return null; + } + + useEffect(() => { + const iframe = iframeRef.current; + if (iframeRef.current?.contentWindow) { + // 将apiInstance注入到window.webgal + Object.defineProperty(iframeRef.current.contentWindow, 'webgal', { + value: apiInstance, + configurable: true, + enumerable: true, + }); + + // 将injectArgs注入到window.webgal.params + if (injectArgs && Object.keys(injectArgs).length > 0) { + Object.defineProperty(apiInstance, 'params', { + value: injectArgs, + configurable: true, + enumerable: true, + }); + } + } + return () => { + if (iframe?.contentWindow) { + try { + delete (iframe.contentWindow as any).webgal; + } catch (e) { + // If the iframe has navigated to a different origin, this will fail. + // This is expected and can be ignored. + } + } + }; + }, [injectArgs]); + + return ( +