M3(T-M3-13): 分享面板对齐原工程——微信SDK + QQ/抖音系统分享
对齐 Android SharePanelHelper 流程:H5 friendsSharetypeUrlToptitleDescript → 原生弹自定义面板(微信/QQ/抖音) → 按所选平台分享。 - common ShareEvents:SHOW_PANEL + SharePanelRequest/Result;ShareProvider 经一次性 resultEvent 与 entry 容器一对一解耦(双槽实例隔离,避免广播串扰) - ShareProvider 重构:微信走 SDK(type2 Canvas截图→WXImageObject / 其他→WXWebpageObject; scene 好友/朋友圈;回传 sharesuccess);QQ/抖音走 @kit.ShareKit 系统分享(文本/图片/链接/视频, 不集成对应 SDK、不回传);截图失败统一降级 - 截图:CapabilityContext.captureCanvas(WebSlot 用 controller.runJavaScript 取 canvas toDataURL,非激活/出错返回空);FileSystem.writeBytes 落盘;fileUri 给 WXImageObject/系统分享 - entry SharePanel 组件 + BridgeGameContainer 顶层叠加;重复面板/销毁均取消回投防 once 悬挂 真机验证项:微信开放平台注册后的真分享、系统分享面板拉起、canvas 截图 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
01e4c0e99e
commit
5cbe5d4dd0
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 自定义分享面板(对齐原 Android SharePanelHelper:底部弹出 + 三按钮 微信/QQ/抖音 + 点空白关闭=取消)。
|
||||
*
|
||||
* 纯展示组件:由 BridgeGameContainer 以 @State visible 控制显隐,叠在 Stack 顶层。
|
||||
* 用户点平台 → onPick(platform);点空白/取消 → onPick('cancel')。无图标资源用文字按钮。
|
||||
* 不持业务逻辑、不直接发 EventBus——回投由容器统一经一次性 resultEvent 完成(避免组件耦合事件名)。
|
||||
*/
|
||||
@Component
|
||||
export struct SharePanel {
|
||||
/** 选择回调:'wechat' | 'qq' | 'douyin' | 'cancel'。 */
|
||||
onPick: (platform: string) => void = () => { };
|
||||
|
||||
build() {
|
||||
// 半透明遮罩:点击空白处取消
|
||||
Column() {
|
||||
Blank()
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
.onClick(() => this.onPick('cancel'))
|
||||
|
||||
// 内容区:点击不关闭(消费事件)
|
||||
Column() {
|
||||
Text('分享到')
|
||||
.fontSize(16)
|
||||
.fontColor('#333333')
|
||||
.margin({ top: 16, bottom: 16 })
|
||||
|
||||
Row() {
|
||||
this.platformButton('微信', 'wechat', '#07C160')
|
||||
this.platformButton('QQ', 'qq', '#12B7F5')
|
||||
this.platformButton('抖音', 'douyin', '#000000')
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.SpaceEvenly)
|
||||
.margin({ bottom: 12 })
|
||||
|
||||
Divider().color('#EEEEEE')
|
||||
|
||||
Text('取消')
|
||||
.fontSize(16)
|
||||
.fontColor('#666666')
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding({ top: 14, bottom: 14 })
|
||||
.onClick(() => this.onPick('cancel'))
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor(Color.White)
|
||||
.borderRadius({ topLeft: 16, topRight: 16 })
|
||||
.onClick(() => { /* 消费点击,避免穿透到遮罩 */ })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.backgroundColor('rgba(0,0,0,0.45)')
|
||||
}
|
||||
|
||||
@Builder
|
||||
platformButton(label: string, platform: string, color: string) {
|
||||
Column() {
|
||||
Circle({ width: 56, height: 56 }).fill(color)
|
||||
Text(label)
|
||||
.fontSize(13)
|
||||
.fontColor('#333333')
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
.onClick(() => this.onPick(platform))
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@ import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues } from '
|
||||
import { KvStore, LocalUploadServer } from 'platform';
|
||||
import { OutboundHandlers } from 'contracts';
|
||||
import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, SwitchGamePayload,
|
||||
BackGamePayload, Logger } from 'common';
|
||||
BackGamePayload, ShareEvents, SharePanelRequest, SharePanelResult, Logger } from 'common';
|
||||
import { BridgeGameParams, GenericWebParams, GenericWebResult, RouteName, AppEvents } from '../routes/AppRoutes';
|
||||
import { WebSlot } from '../web/WebSlot';
|
||||
import { SharePanel } from '../components/SharePanel';
|
||||
|
||||
/**
|
||||
* 大厅/子游戏容器(对应 webviewActivity,框架 §7.1/§7.5)。**双 Web 槽模型**:
|
||||
@@ -34,6 +35,10 @@ export struct BridgeGameContainer {
|
||||
private cancels: Array<() => void> = [];
|
||||
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
|
||||
@State private subgameUrl: string = '';
|
||||
/** 分享面板显隐(叠在 Stack 顶层,对齐 Android SharePanelHelper)。 */
|
||||
@State private sharePanelVisible: boolean = false;
|
||||
/** 当前分享面板的一次性回投事件名(用户选定平台后 emit 回 ShareProvider)。 */
|
||||
private shareResultEvent: string = '';
|
||||
|
||||
aboutToAppear(): void {
|
||||
if (AppEnv.isDebug()) {
|
||||
@@ -99,6 +104,32 @@ export struct BridgeGameContainer {
|
||||
this.cancels.push(EventBus.on(NavEvents.OPEN_GENERIC_WEB, (p?: EventPayload) => this.openGenericWeb(p)));
|
||||
this.cancels.push(EventBus.on(NavEvents.SWITCH_GAME, (p?: EventPayload) => this.switchGame(p)));
|
||||
this.cancels.push(EventBus.on(NavEvents.BACK_GAME, (p?: EventPayload) => this.backToLobby(p)));
|
||||
this.cancels.push(EventBus.on(ShareEvents.SHOW_PANEL, (p?: EventPayload) => this.showSharePanel(p)));
|
||||
}
|
||||
|
||||
/** 收到 ShareProvider 的 SHOW_PANEL:记下一次性回投事件名并弹出面板。 */
|
||||
private showSharePanel(p?: EventPayload): void {
|
||||
if (p === undefined) {
|
||||
return;
|
||||
}
|
||||
// 已有面板未关闭:先按取消回投旧请求,避免上一个 once 永不触发致 ShareProvider 悬挂。
|
||||
if (this.sharePanelVisible && this.shareResultEvent !== '') {
|
||||
this.emitShareResult('cancel');
|
||||
}
|
||||
const req = p as SharePanelRequest;
|
||||
this.shareResultEvent = req.resultEvent;
|
||||
this.sharePanelVisible = true;
|
||||
}
|
||||
|
||||
/** 用户选定平台/取消:回投并关闭面板。 */
|
||||
private emitShareResult(platform: string): void {
|
||||
const ev: string = this.shareResultEvent;
|
||||
this.shareResultEvent = '';
|
||||
this.sharePanelVisible = false;
|
||||
if (ev !== '') {
|
||||
const result: SharePanelResult = { platform };
|
||||
EventBus.emit(ev, result);
|
||||
}
|
||||
}
|
||||
|
||||
/** 路径 B:打开通用网页容器;关闭回传 → 当前激活槽 callHandler('getWebdata')。 */
|
||||
@@ -211,9 +242,22 @@ export struct BridgeGameContainer {
|
||||
})
|
||||
.onRenderExited((event: OnRenderExitedEvent) => this.slotS?.onRenderExited(event.renderExitReason))
|
||||
}
|
||||
|
||||
// 分享面板(顶层叠加;用户选平台/取消 → 一次性回投 ShareProvider)
|
||||
if (this.sharePanelVisible) {
|
||||
SharePanel({ onPick: (platform: string) => this.emitShareResult(platform) })
|
||||
}
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
}
|
||||
.hideTitleBar(true)
|
||||
.onBackPressed(() => {
|
||||
// 分享面板打开时,返回键关闭面板并按取消回投(对齐 Android 点空白关闭)
|
||||
if (this.sharePanelVisible) {
|
||||
this.emitShareResult('cancel');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export class WebSlot {
|
||||
kv,
|
||||
uploadServer: this.uploadServer,
|
||||
log: Logger.tag(`Cap-${this.role}`),
|
||||
captureCanvas: (canvasId: string): Promise<string> => this.captureCanvas(canvasId),
|
||||
};
|
||||
const registrar = new CapabilityRegistrar(buildCapabilities());
|
||||
registrar.registerAll(bridge, ctx);
|
||||
@@ -192,6 +193,49 @@ export class WebSlot {
|
||||
this.adapter = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取本槽 Web 内的 canvas(截图分享)。脚本在 H5 内 toDataURL,返回 dataURL;
|
||||
* runJavaScript 回调的 result 是 JSON 字符串(外层带引号),需 JSON.parse 去引号还原。
|
||||
* 非激活/出错一律返回空串(由 ShareProvider 降级网页分享,绝不抛错)。
|
||||
*/
|
||||
private captureCanvas(canvasId: string): Promise<string> {
|
||||
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<string>((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 '';
|
||||
}
|
||||
}
|
||||
|
||||
private flushPending(): void {
|
||||
if (this.pendingWebdata !== undefined) {
|
||||
this.bridgeCtrl?.callHandler(OutboundHandlers.GetWebData, this.pendingWebdata);
|
||||
|
||||
Reference in New Issue
Block a user