From 1902e4e5bec57da7fdbbc8bca867c21cb84e6ecf Mon Sep 17 00:00:00 2001 From: lanterngamescn Date: Sat, 27 Jun 2026 15:31:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(share):=20=E6=88=AA=E5=9B=BE=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20componentSnapshot=20=E6=8A=93Web=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=83=8F=E7=B4=A0(=E4=BF=AEWebGL=E9=BB=91?= =?UTF-8?q?=E5=B1=8F)=20+=20=E5=89=AA=E8=B4=B4=E6=9D=BF=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=94=B9fd=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BridgeGameContainer: 大厅Web加 .id('web_lobby'),子游戏Web加 .id('web_subgame'), 供 componentSnapshot.get 按组件id定位 - WebSlot: captureCanvas 整体替换为 componentSnapshot.get + image.ImagePacker.packToData, 规避 WebGL canvas.toDataURL 黑屏;packToData 为 packing 的非废弃替代(§API13+); 删除 parseJsString(仅被旧 captureCanvas 内联用,新实现不需要) - ShareProvider: writeClipboardImage 改用 fileIo.openSync 取 fd 再 createImageSource(fd),修复传路径字符串可能失败问题;补 import fileIo API签名核对(devecocli docs): componentSnapshot.get(id, options) → Promise(deprecated API18,仍可用) ImagePacker.packToData(pixelMap, PackingOption) → Promise(API13+,WARN无error) Co-Authored-By: Claude Sonnet 4.6 --- .../main/ets/pages/BridgeGameContainer.ets | 2 + entry/src/main/ets/web/WebSlot.ets | 60 ++++++++----------- .../src/main/ets/providers/ShareProvider.ets | 14 ++++- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/entry/src/main/ets/pages/BridgeGameContainer.ets b/entry/src/main/ets/pages/BridgeGameContainer.ets index 53d4049..7cddd5d 100644 --- a/entry/src/main/ets/pages/BridgeGameContainer.ets +++ b/entry/src/main/ets/pages/BridgeGameContainer.ets @@ -347,6 +347,7 @@ export struct BridgeGameContainer { Stack() { // 大厅 Web(常驻) Web({ src: this.lobbySrc(), controller: this.controllerL }) + .id('web_lobby') .javaScriptAccess(true).domStorageAccess(true).fileAccess(true) .mixedMode(MixedMode.All).cacheMode(CacheMode.None) .geolocationAccess(true).zoomAccess(false) @@ -372,6 +373,7 @@ export struct BridgeGameContainer { // 子游戏 Web(临时,subgameUrl 非空时存在) if (this.subgameUrl !== '' && this.slotS !== undefined) { Web({ src: this.subgameUrl, controller: this.slotS.controller }) + .id('web_subgame') .javaScriptAccess(true).domStorageAccess(true).fileAccess(true) .mixedMode(MixedMode.All).cacheMode(CacheMode.None) .geolocationAccess(true).zoomAccess(false) diff --git a/entry/src/main/ets/web/WebSlot.ets b/entry/src/main/ets/web/WebSlot.ets index 15256ce..ccbe0e5 100644 --- a/entry/src/main/ets/web/WebSlot.ets +++ b/entry/src/main/ets/web/WebSlot.ets @@ -1,4 +1,7 @@ import { webview } from '@kit.ArkWeb'; +import { componentSnapshot } from '@kit.ArkUI'; +import { image } from '@kit.ImageKit'; +import { util } from '@kit.ArkTS'; import { common } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge'; @@ -225,46 +228,33 @@ export class WebSlot { } /** - * 截取本槽 Web 内的 canvas(截图分享)。脚本在 H5 内 toDataURL,返回 dataURL; - * runJavaScript 回调的 result 是 JSON 字符串(外层带引号),需 JSON.parse 去引号还原。 - * 非激活/出错一律返回空串(由 ShareProvider 降级网页分享,绝不抛错)。 + * 截取本槽 Web 组件已渲染像素(截图分享)。改用 componentSnapshot 抓组件合成结果, + * 规避 WebGL canvas.toDataURL 黑屏问题;打包为 image/jpeg 的 dataURL 返回。 + * 非激活/出错一律返回空串(由 ShareProvider 降级,绝不抛错)。canvasId 参数保留兼容、当前忽略。 */ private captureCanvas(canvasId: string): Promise { if (!this.active || this.disposed) { return Promise.resolve(''); } - const sel: string = canvasId !== '' - ? `document.getElementById(${JSON.stringify(canvasId)}).toDataURL('image/png')` - : `document.querySelector('canvas').toDataURL('image/jpeg',0.8)`; - const script: string = `(function(){try{return ${sel};}catch(e){return '';}})()`; - return new Promise((resolve: (v: string) => void) => { - try { - this.controller.runJavaScript(script, (err: BusinessError, result: string) => { - if (err) { - WebSlot.log.w(`[${this.role}] captureCanvas runJavaScript error: ${err.message}`); - resolve(''); - return; - } - resolve(WebSlot.parseJsString(result)); - }); - } catch (e) { - WebSlot.log.w(`[${this.role}] captureCanvas failed: ${(e as BusinessError).message}`); - resolve(''); - } - }); - } - - /** runJavaScript 返回值是 JSON 字符串(字符串结果外层带引号);去引号还原,异常返回空串。 */ - private static parseJsString(result: string): string { - if (result === undefined || result === null || result === '' || result === 'null') { - return ''; - } - try { - const parsed: Object = JSON.parse(result) as Object; - return typeof parsed === 'string' ? parsed as string : ''; - } catch (e) { - return ''; - } + const compId: string = `web_${this.role}`; + return componentSnapshot.get(compId, { waitUntilRenderFinished: true }) + .then((pm: image.PixelMap): Promise => { + const packer: image.ImagePacker = image.createImagePacker(); + return packer.packToData(pm, { format: 'image/jpeg', quality: 90 }) + .then((buf: ArrayBuffer): string => { + const b64: string = new util.Base64Helper().encodeToStringSync(new Uint8Array(buf)); + WebSlot.log.i(`[${this.role}] snapshot ok bytes=${buf.byteLength}`); + return `data:image/jpeg;base64,${b64}`; + }) + .finally(() => { + pm.release(); + packer.release(); + }); + }) + .catch((e: Object): string => { + WebSlot.log.w(`[${this.role}] componentSnapshot failed: ${(e as Error).message}`); + return ''; + }); } private flushPending(): void { diff --git a/feature_capabilities/src/main/ets/providers/ShareProvider.ets b/feature_capabilities/src/main/ets/providers/ShareProvider.ets index 6bdc4bb..68161f7 100644 --- a/feature_capabilities/src/main/ets/providers/ShareProvider.ets +++ b/feature_capabilities/src/main/ets/providers/ShareProvider.ets @@ -15,7 +15,7 @@ */ import { systemShare } from '@kit.ShareKit'; import { uniformTypeDescriptor as utd } from '@kit.ArkData'; -import { fileUri } from '@kit.CoreFileKit'; +import { fileUri, fileIo } from '@kit.CoreFileKit'; import { common, bundleManager } from '@kit.AbilityKit'; import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; import { image } from '@kit.ImageKit'; @@ -316,7 +316,14 @@ export class ShareProvider implements CapabilityProvider { /** 图片写剪贴板(尽力;对方能否粘贴 PIXELMAP 不可保证,systemImage 才是可靠交付)。 */ private writeClipboardImage(filePath: string): void { - const source: image.ImageSource = image.createImageSource(filePath); + let file: fileIo.File; + try { + file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY); + } catch (e) { + this.log.w(`clipboard image open failed: ${(e as Error).message}`); + return; + } + const source: image.ImageSource = image.createImageSource(file.fd); source.createPixelMap().then((pm: image.PixelMap) => { let data: pasteboard.PasteData; try { @@ -326,14 +333,15 @@ export class ShareProvider implements CapabilityProvider { pm.release(); return; } - // pm 须存活到 setData 完成(createData 可能持引用),故在 setData settle 后再 release。 pasteboard.getSystemPasteboard().setData(data) + .then(() => this.log.i('clipboard image set ok')) .catch((e: BusinessError) => this.log.w(`clipboard image failed: ${e.code} ${e.message}`)) .finally(() => pm.release()); }).catch((e: BusinessError) => { this.log.w(`createPixelMap failed: ${e.code} ${e.message}`); }).finally(() => { source.release(); + fileIo.closeSync(file); }); }