feat(share): ShareProvider 去SDK化三段式分享 + WeChatApi 仅留登录(分享重构 §4/§5/§7)
- WeChatApi:移除 SendMessageToWXResp/SendMessageToWXReq/WXMediaMessage/WXWebpageObject/WXImageObject/ErrCode/shareCb/sendShare;仅保留登录路径(sendAuth/handleWant/SendAuthResp) - ShareProvider:完全去 wechat_open_sdk 分享依赖,改「剪贴板+ShareGuideDialog→系统分享」三段式架构;保留 FriendsShare 注册、PHOTO_UPLOAD 链路、captureCanvas 截图、systemShare 兜底、乐观回传 sharesuccess;新增 type3 图片链接下载路径 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
15fadd7cdb
commit
e93d0627d4
@@ -1,45 +1,45 @@
|
||||
/**
|
||||
* 分享能力(契约 §8.1/§10.1,T-M3-13)。对齐原 Android「自定义三按钮分享面板」(微信/QQ/抖音)。
|
||||
* 分享能力(契约 §8.1/§10.1)。三端统一「剪贴板+指引窗 → 系统分享」,**不依赖任何分享 SDK**。
|
||||
*
|
||||
* H5 调 friendsSharetypeUrlToptitleDescript(data=sharetypeBean JSON)→ 弹自定义面板(entry 容器渲染)。
|
||||
* 用户点平台后经一次性 resultEvent 回投本 Provider:
|
||||
* - 微信(@tencent/wechat_open_sdk):
|
||||
* type=="2" → Canvas 截图图片分享(截当前激活 Web 的 canvas.toDataURL → 落盘 → WXImageObject.uri);
|
||||
* 其他 type → 网页分享(WXWebpageObject)。
|
||||
* scene:sharefriend=="1" → 好友(WXSceneSession),否则 → 朋友圈(WXSceneTimeline)。
|
||||
* 分享结果回传 H5:callHandler('sharesuccess', {success:2成功/3取消, type:1好友/2朋友圈})。
|
||||
* - QQ / 抖音(不集成对应 SDK,用 HarmonyOS 系统分享 @kit.ShareKit,让用户在系统面板选目标 app):
|
||||
* type=="1" → 文本(title+description);type=="2" → 截图图片;
|
||||
* type=="4"(抖音视频) → 视频(webpageUrl 为视频地址);else → 网页链接(webpageUrl)。**不回传 sharesuccess**。
|
||||
* H5 调 friendsSharetypeUrlToptitleDescript(data=sharetypeBean JSON):
|
||||
* - sharefriend=="2" → 直接微信指引窗(朋友圈语义,回传 type=2)。
|
||||
* - sharefriend=="1" → 弹自定义面板(微信/QQ/抖音/取消) → 选定渠道。
|
||||
* 选定渠道后按内容类型组装并写剪贴板,再弹 ShareGuideDialog:
|
||||
* - 文本(type1/4/其他):剪贴板 PLAIN_TEXT(title\ndescription,无 url);
|
||||
* - 图片(type2 截图 / type3 图片链接下载,低优先):剪贴板 PIXELMAP(尽力)。
|
||||
* 指引窗动作:打开App(openLink scheme,best-effort) / 用系统分享(systemText|systemImage,可靠) / 取消。
|
||||
* 仅微信乐观回传 sharesuccess:打开/系统分享→{success:2,type};取消→{success:3,type}。QQ/抖音不回传。
|
||||
*
|
||||
* ⚠ 微信运行期成功需在微信开放平台注册本 HarmonyOS 应用(bundleName+指纹 绑定 AppID)。
|
||||
* 截图/系统分享拉起/微信分享 均只能真机验证。
|
||||
* 注:图片"复制剪贴板再去对方App粘贴"能否成功取决于对方是否读 PIXELMAP(鸿蒙不可保证),
|
||||
* 故指引窗"用系统分享"(systemImage) 为可靠兜底;拉起 App 仅打开、不预填(需用户自行粘贴)。
|
||||
*/
|
||||
import { SendMessageToWXReq, WXMediaMessage, WXWebpageObject, WXImageObject, ErrCode } from '@tencent/wechat_open_sdk';
|
||||
import { systemShare } from '@kit.ShareKit';
|
||||
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
|
||||
import { fileUri } from '@kit.CoreFileKit';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { common, bundleManager } from '@kit.AbilityKit';
|
||||
import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
|
||||
import { image } from '@kit.ImageKit';
|
||||
import { util } from '@kit.ArkTS';
|
||||
import { BridgeController } from 'feature_bridge';
|
||||
import { InboundHandlers, OutboundHandlers, SharetypeBean, ShareSuccessResp } from 'contracts';
|
||||
import { EventBus, Logger, ShareEvents, SharePanelRequest, SharePanelResult, PhotoUploadPayload } from 'common';
|
||||
import { FileSystem } from 'platform';
|
||||
import {
|
||||
EventBus, Logger, ShareEvents,
|
||||
SharePanelRequest, SharePanelResult, ShareGuideRequest, ShareGuideResult, PhotoUploadPayload,
|
||||
} from 'common';
|
||||
import { FileSystem, Downloader } from 'platform';
|
||||
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
||||
import { WeChatApi } from '../wx/WeChatApi';
|
||||
import { buildShareText } from '../share/ShareText';
|
||||
|
||||
export class ShareProvider implements CapabilityProvider {
|
||||
/** 进程内实例计数:双 Web 槽各有一个 ShareProvider 实例,用它给 resultEvent 命名空间隔离。 */
|
||||
/** 进程内实例计数:双 Web 槽各一个实例,用它给 resultEvent 命名空间隔离。 */
|
||||
private static instanceCount: number = 0;
|
||||
readonly name: string = 'share';
|
||||
private readonly log: Logger = Logger.tag('ShareProvider');
|
||||
private readonly instanceId: number;
|
||||
private bridge: BridgeController | undefined = undefined;
|
||||
private ctx: CapabilityContext | undefined = undefined;
|
||||
/** 截图落盘 + resultEvent 命名计数器(实例自增,避免 Math.random 串扰)。 */
|
||||
/** 落盘 + resultEvent 命名计数器(实例自增,避免 Math.random 串扰)。 */
|
||||
private seq: number = 0;
|
||||
/** PHOTO_UPLOAD 订阅退订函数(onDestroy 调用,防双槽实例泄漏/串扰)。 */
|
||||
private photoUploadCancel: (() => void) | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
@@ -53,11 +53,10 @@ export class ShareProvider implements CapabilityProvider {
|
||||
bridge.registerHandler(InboundHandlers.FriendsShare, (data: string, _cb: (resp: string) => void) => {
|
||||
this.onShare(data);
|
||||
});
|
||||
// 第二条链路:H5 主动 POST 截图到 LocalUploadServer → emit PHOTO_UPLOAD → 微信图片分享。
|
||||
// 第二条链路:H5 主动 POST 截图到 LocalUploadServer → emit PHOTO_UPLOAD → 微信图片指引窗。
|
||||
this.photoUploadCancel = EventBus.on(ShareEvents.PHOTO_UPLOAD, (p) => this.onPhotoUpload(p));
|
||||
}
|
||||
|
||||
/** 容器销毁:退订 PHOTO_UPLOAD(双槽各一实例,须 per-subscriber 退订,勿 offAll)。 */
|
||||
onDestroy(): void {
|
||||
if (this.photoUploadCancel !== undefined) {
|
||||
this.photoUploadCancel();
|
||||
@@ -65,49 +64,10 @@ export class ShareProvider implements CapabilityProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* H5 POST 截图上传分享:仅当本槽桥激活时处理(非激活槽忽略,避免双实例对同一上传重复分享)。
|
||||
* type 语义同 sharefriend:"1" 好友 → WXSceneSession/回传 type=1;其他 → 朋友圈/回传 type=2。
|
||||
* 落盘成功走微信图片分享;微信未装 reportResult(3, ...);落盘失败仅 log(无网页可降级)。
|
||||
*/
|
||||
private onPhotoUpload(payload?: Object): void {
|
||||
const ctx = this.ctx;
|
||||
const bridge = this.bridge;
|
||||
if (ctx === undefined || bridge === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!bridge.isActive()) {
|
||||
return; // 非激活槽不处理,保证同一上传仅激活槽分享一次
|
||||
}
|
||||
if (payload === undefined) {
|
||||
return;
|
||||
}
|
||||
const up = payload as PhotoUploadPayload;
|
||||
if (up.imageBase64 === '') {
|
||||
this.log.w('photo upload empty image');
|
||||
return;
|
||||
}
|
||||
const friendType: number = up.type === '1' ? 1 : 2;
|
||||
const wx: WeChatApi = WeChatApi.getInstance();
|
||||
if (!wx.isWXInstalled()) {
|
||||
this.log.i('WeChat not installed (photo upload); report cancel');
|
||||
this.reportResult(3, friendType);
|
||||
return;
|
||||
}
|
||||
// saveDataUrlToFile 对无 data: 前缀的纯 base64 也兼容(indexOf(',')<0 → 整串当 base64)
|
||||
const filePath: string = this.saveDataUrlToFile(ctx.uiAbilityContext, up.imageBase64);
|
||||
if (filePath === '') {
|
||||
this.log.w('photo upload save failed');
|
||||
this.reportResult(3, friendType);
|
||||
return;
|
||||
}
|
||||
this.wechatImageByFriend(ctx.uiAbilityContext, wx, filePath, friendType);
|
||||
}
|
||||
// —— 入站 ——
|
||||
|
||||
/** 入站:校验 bean → 发起面板请求(一对一回投)。 */
|
||||
private onShare(data: string): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
if (this.ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
let bean: SharetypeBean;
|
||||
@@ -121,6 +81,11 @@ export class ShareProvider implements CapabilityProvider {
|
||||
this.log.w('share bean empty');
|
||||
return;
|
||||
}
|
||||
if (bean.sharefriend === '2') {
|
||||
// 朋友圈语义:不弹面板,直接微信指引窗(契约 §10.1)。
|
||||
this.startShare(bean, 'wechat');
|
||||
return;
|
||||
}
|
||||
this.seq += 1;
|
||||
const resultEvent: string = `share.result.${this.instanceId}.${this.seq}`;
|
||||
EventBus.once(resultEvent, (p) => this.onPlatformChosen(bean, p));
|
||||
@@ -128,189 +93,217 @@ export class ShareProvider implements CapabilityProvider {
|
||||
EventBus.emit(ShareEvents.SHOW_PANEL, req);
|
||||
}
|
||||
|
||||
/** 用户在面板选定平台后回投。 */
|
||||
private onPlatformChosen(bean: SharetypeBean, payload?: Object): void {
|
||||
const platform: string = payload !== undefined ? (payload as SharePanelResult).platform : 'cancel';
|
||||
if (platform === 'wechat') {
|
||||
this.shareToWeChat(bean);
|
||||
} else if (platform === 'qq') {
|
||||
this.shareViaSystem(bean, 'qq');
|
||||
} else if (platform === 'douyin') {
|
||||
this.shareViaSystem(bean, 'douyin');
|
||||
if (platform === 'wechat' || platform === 'qq' || platform === 'douyin') {
|
||||
this.startShare(bean, platform);
|
||||
}
|
||||
// cancel:不分享、不回传(对齐 Android 点空白关闭面板)。
|
||||
// cancel:不分享、不回传。
|
||||
}
|
||||
|
||||
// —— 微信 ——
|
||||
|
||||
/** sharefriend=="1" → 好友(回传 type=1);否则 → 朋友圈(回传 type=2)。对齐 Android scene 语义。 */
|
||||
private wechatReportType(bean: SharetypeBean): number {
|
||||
return bean.sharefriend === '1' ? 1 : 2;
|
||||
/** H5 POST 截图上传:仅激活槽处理,落盘后走微信图片指引窗。 */
|
||||
private onPhotoUpload(payload?: Object): void {
|
||||
const ctx = this.ctx;
|
||||
const bridge = this.bridge;
|
||||
if (ctx === undefined || bridge === undefined || !bridge.isActive() || payload === undefined) {
|
||||
return;
|
||||
}
|
||||
const up = payload as PhotoUploadPayload;
|
||||
if (up.imageBase64 === '') {
|
||||
this.log.w('photo upload empty image');
|
||||
return;
|
||||
}
|
||||
const bean: SharetypeBean = {
|
||||
sharefriend: up.type, type: '2', sharetype: '', webpageUrl: '', title: '', description: '',
|
||||
};
|
||||
const filePath: string = this.saveDataUrlToFile(ctx.uiAbilityContext, up.imageBase64);
|
||||
if (filePath === '') {
|
||||
this.log.w('photo upload save failed; report cancel');
|
||||
this.reportResult(3, up.type === '1' ? 1 : 2);
|
||||
return;
|
||||
}
|
||||
this.onImageReady(bean, 'wechat', filePath);
|
||||
}
|
||||
|
||||
private shareToWeChat(bean: SharetypeBean): void {
|
||||
// —— 组装 + 弹指引窗 ——
|
||||
|
||||
private startShare(bean: SharetypeBean, platform: string): void {
|
||||
if (bean.type === '2') {
|
||||
this.startCanvasImageShare(bean, platform);
|
||||
} else if (bean.type === '3') {
|
||||
this.startLinkImageShare(bean, platform);
|
||||
} else {
|
||||
this.startTextShare(bean, platform);
|
||||
}
|
||||
}
|
||||
|
||||
private startTextShare(bean: SharetypeBean, platform: string): void {
|
||||
this.writeClipboardText(buildShareText(bean.title, bean.description));
|
||||
this.guide(bean, platform, 'text', '');
|
||||
}
|
||||
|
||||
/** type2:截当前 Web canvas → 落盘 → 图片指引窗(捕获失败降级文本指引,非"图片转文本"语义)。 */
|
||||
private startCanvasImageShare(bean: SharetypeBean, platform: string): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const reportType: number = this.wechatReportType(bean);
|
||||
const wx: WeChatApi = WeChatApi.getInstance();
|
||||
if (!wx.isWXInstalled()) {
|
||||
this.log.i('WeChat not installed; report cancel');
|
||||
this.reportResult(3, reportType);
|
||||
const cap = ctx.captureCanvas;
|
||||
if (cap === undefined) {
|
||||
this.startTextShare(bean, platform);
|
||||
return;
|
||||
}
|
||||
if (bean.type === '2') {
|
||||
// Canvas 截图图片分享(失败降级网页)
|
||||
const cap = ctx.captureCanvas;
|
||||
if (cap === undefined) {
|
||||
this.wechatWebPage(ctx.uiAbilityContext, wx, bean, reportType);
|
||||
return;
|
||||
cap(bean.sharetype).then((dataUrl: string) => {
|
||||
const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(ctx.uiAbilityContext, dataUrl) : '';
|
||||
if (filePath !== '') {
|
||||
this.onImageReady(bean, platform, filePath);
|
||||
} else {
|
||||
this.log.w('canvas capture empty; fallback to text guide');
|
||||
this.startTextShare(bean, platform);
|
||||
}
|
||||
cap(bean.sharetype).then((dataUrl: string) => {
|
||||
const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(ctx.uiAbilityContext, dataUrl) : '';
|
||||
if (filePath !== '') {
|
||||
this.wechatImage(ctx.uiAbilityContext, wx, filePath, bean, reportType);
|
||||
} else {
|
||||
this.log.w('canvas capture empty; fallback to webpage share');
|
||||
this.wechatWebPage(ctx.uiAbilityContext, wx, bean, reportType);
|
||||
}
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`captureCanvas failed: ${e.message}; fallback to webpage share`);
|
||||
this.wechatWebPage(ctx.uiAbilityContext, wx, bean, reportType);
|
||||
});
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`captureCanvas failed: ${e.message}; fallback to text guide`);
|
||||
this.startTextShare(bean, platform);
|
||||
});
|
||||
}
|
||||
|
||||
/** type3(低优先,需确认 H5 是否使用):从 webpageUrl 下载图片 → 落盘 → 图片指引窗(失败降级文本)。 */
|
||||
private startLinkImageShare(bean: SharetypeBean, platform: string): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
this.wechatWebPage(ctx.uiAbilityContext, wx, bean, reportType);
|
||||
}
|
||||
|
||||
private wechatScene(bean: SharetypeBean): number {
|
||||
return this.wechatSceneByFriend(bean.sharefriend === '1' ? 1 : 2);
|
||||
}
|
||||
|
||||
/** friendType: 1 好友(WXSceneSession) / 2 朋友圈(WXSceneTimeline)。 */
|
||||
private wechatSceneByFriend(friendType: number): number {
|
||||
return friendType === 1 ? SendMessageToWXReq.WXSceneSession : SendMessageToWXReq.WXSceneTimeline;
|
||||
}
|
||||
|
||||
private wechatWebPage(ctx: common.UIAbilityContext, wx: WeChatApi, bean: SharetypeBean, reportType: number): void {
|
||||
const req: SendMessageToWXReq = new SendMessageToWXReq();
|
||||
req.scene = this.wechatScene(bean);
|
||||
const msg: WXMediaMessage = new WXMediaMessage();
|
||||
msg.title = bean.title;
|
||||
msg.description = bean.description;
|
||||
const web: WXWebpageObject = new WXWebpageObject();
|
||||
web.webpageUrl = bean.webpageUrl;
|
||||
msg.mediaObject = web;
|
||||
req.message = msg;
|
||||
wx.sendShare(ctx, req, (resp) => {
|
||||
this.reportResult(resp.errCode === ErrCode.ERR_OK ? 2 : 3, reportType);
|
||||
const url: string = bean.webpageUrl;
|
||||
if (url === '' || !(url.startsWith('http://') || url.startsWith('https://'))) {
|
||||
this.startTextShare(bean, platform);
|
||||
return;
|
||||
}
|
||||
this.seq += 1;
|
||||
const ext: string = url.toLowerCase().indexOf('.png') >= 0 ? '.png' : '.jpg';
|
||||
const savePath: string = `${ctx.uiAbilityContext.filesDir}/share/link_${this.seq}${ext}`;
|
||||
Downloader.download(url, savePath).then(() => {
|
||||
this.onImageReady(bean, platform, savePath);
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`link image download failed: ${e.message}; fallback to text guide`);
|
||||
this.startTextShare(bean, platform);
|
||||
});
|
||||
}
|
||||
|
||||
private wechatImage(ctx: common.UIAbilityContext, wx: WeChatApi, filePath: string,
|
||||
bean: SharetypeBean, reportType: number): void {
|
||||
this.wechatImageByFriend(ctx, wx, filePath, reportType);
|
||||
private onImageReady(bean: SharetypeBean, platform: string, filePath: string): void {
|
||||
this.writeClipboardImage(filePath);
|
||||
this.guide(bean, platform, 'image', filePath);
|
||||
}
|
||||
|
||||
/** 微信图片分享(友/圈由 friendType 决定);分享面板与 H5 POST 上传两条链路共用。
|
||||
* friendType 同时作为 sharesuccess 回传的 type(1 好友 / 2 朋友圈)。 */
|
||||
private wechatImageByFriend(ctx: common.UIAbilityContext, wx: WeChatApi, filePath: string, friendType: number): void {
|
||||
const req: SendMessageToWXReq = new SendMessageToWXReq();
|
||||
req.scene = this.wechatSceneByFriend(friendType);
|
||||
const msg: WXMediaMessage = new WXMediaMessage();
|
||||
const img: WXImageObject = new WXImageObject();
|
||||
img.uri = fileUri.getUriFromPath(filePath); // 沙箱文件 file uri,SDK 支持 jpeg/png
|
||||
msg.mediaObject = img;
|
||||
req.message = msg;
|
||||
wx.sendShare(ctx, req, (resp) => {
|
||||
this.reportResult(resp.errCode === ErrCode.ERR_OK ? 2 : 3, friendType);
|
||||
});
|
||||
private guide(bean: SharetypeBean, platform: string, kind: string, filePath: string): void {
|
||||
this.seq += 1;
|
||||
const resultEvent: string = `share.guide.${this.instanceId}.${this.seq}`;
|
||||
EventBus.once(resultEvent, (p) => this.onGuideAction(bean, platform, kind, filePath, p));
|
||||
const req: ShareGuideRequest = { platform, contentKind: kind, resultEvent };
|
||||
EventBus.emit(ShareEvents.SHOW_GUIDE, req);
|
||||
}
|
||||
|
||||
// —— QQ / 抖音:HarmonyOS 系统分享(不回传 sharesuccess)——
|
||||
|
||||
private shareViaSystem(bean: SharetypeBean, platform: string): void {
|
||||
private onGuideAction(bean: SharetypeBean, platform: string, kind: string, filePath: string, payload?: Object): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const uiCtx: common.UIAbilityContext = ctx.uiAbilityContext;
|
||||
if (bean.type === '2') {
|
||||
// 截图图片
|
||||
const cap = ctx.captureCanvas;
|
||||
if (cap === undefined) {
|
||||
this.systemText(uiCtx, bean, platform);
|
||||
return;
|
||||
const action: string = payload !== undefined ? (payload as ShareGuideResult).action : 'cancel';
|
||||
const reportType: number = bean.sharefriend === '1' ? 1 : 2;
|
||||
if (action === 'open') {
|
||||
this.openApp(uiCtx, platform);
|
||||
if (platform === 'wechat') {
|
||||
this.reportResult(2, reportType);
|
||||
}
|
||||
} else if (action === 'system') {
|
||||
if (kind === 'image' && filePath !== '') {
|
||||
this.systemImage(uiCtx, filePath, bean);
|
||||
} else {
|
||||
this.systemText(uiCtx, bean);
|
||||
}
|
||||
if (platform === 'wechat') {
|
||||
this.reportResult(2, reportType);
|
||||
}
|
||||
} else {
|
||||
// cancel
|
||||
if (platform === 'wechat') {
|
||||
this.reportResult(3, reportType);
|
||||
}
|
||||
cap(bean.sharetype).then((dataUrl: string) => {
|
||||
const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(uiCtx, dataUrl) : '';
|
||||
if (filePath !== '') {
|
||||
this.systemImage(uiCtx, filePath, bean);
|
||||
} else {
|
||||
this.log.w(`[${platform}] canvas capture empty; fallback to text`);
|
||||
this.systemText(uiCtx, bean, platform);
|
||||
}
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`[${platform}] captureCanvas failed: ${e.message}; fallback to text`);
|
||||
this.systemText(uiCtx, bean, platform);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (bean.type === '1') {
|
||||
this.systemText(uiCtx, bean, platform);
|
||||
return;
|
||||
}
|
||||
if (bean.type === '4') {
|
||||
// 抖音视频:webpageUrl 为视频地址
|
||||
this.systemVideo(uiCtx, bean);
|
||||
return;
|
||||
}
|
||||
// else:网页链接(title + description + webpageUrl)
|
||||
this.systemLink(uiCtx, bean);
|
||||
}
|
||||
|
||||
/** 文本分享(title + description,对齐 Android 文本拼接)。 */
|
||||
private systemText(uiCtx: common.UIAbilityContext, bean: SharetypeBean, _platform: string): void {
|
||||
const text: string = this.joinText(bean.title, bean.description);
|
||||
const record: systemShare.SharedRecord = { utd: utd.UniformDataType.PLAIN_TEXT, content: text !== '' ? text : ' ' };
|
||||
this.showSystemShare(uiCtx, record);
|
||||
// —— 拉起 App(best-effort,仅打开不预填)——
|
||||
|
||||
private schemeOf(platform: string): string {
|
||||
if (platform === 'qq') {
|
||||
return 'mqqapi://';
|
||||
}
|
||||
if (platform === 'douyin') {
|
||||
return 'snssdk1128://';
|
||||
}
|
||||
return 'weixin://';
|
||||
}
|
||||
|
||||
/** 网页链接分享。 */
|
||||
private systemLink(uiCtx: common.UIAbilityContext, bean: SharetypeBean): void {
|
||||
const url: string = bean.webpageUrl !== '' ? bean.webpageUrl : ' ';
|
||||
private openApp(uiCtx: common.UIAbilityContext, platform: string): void {
|
||||
const link: string = this.schemeOf(platform);
|
||||
let canOpen: boolean = false;
|
||||
try {
|
||||
canOpen = bundleManager.canOpenLink(link);
|
||||
} catch (e) {
|
||||
this.log.w(`canOpenLink failed: ${(e as BusinessError).message}`);
|
||||
return;
|
||||
}
|
||||
if (!canOpen) {
|
||||
this.log.i(`app not openable: ${link}`);
|
||||
return;
|
||||
}
|
||||
uiCtx.openLink(link, { appLinkingOnly: false }).catch((e: BusinessError) => {
|
||||
this.log.w(`openLink failed: code=${e.code} ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// —— 剪贴板 ——
|
||||
|
||||
private writeClipboardText(text: string): void {
|
||||
try {
|
||||
const data: pasteboard.PasteData =
|
||||
pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text !== '' ? text : ' ');
|
||||
pasteboard.getSystemPasteboard().setData(data)
|
||||
.catch((e: BusinessError) => this.log.w(`clipboard text failed: ${e.code} ${e.message}`));
|
||||
} catch (e) {
|
||||
this.log.w(`clipboard text init failed: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 图片写剪贴板(尽力;对方能否粘贴 PIXELMAP 不可保证,systemImage 才是可靠交付)。 */
|
||||
private writeClipboardImage(filePath: string): void {
|
||||
const source: image.ImageSource = image.createImageSource(filePath);
|
||||
source.createPixelMap().then((pm: image.PixelMap) => {
|
||||
try {
|
||||
const data: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_PIXELMAP, pm);
|
||||
pasteboard.getSystemPasteboard().setData(data)
|
||||
.catch((e: BusinessError) => this.log.w(`clipboard image failed: ${e.code} ${e.message}`));
|
||||
} catch (e) {
|
||||
this.log.w(`clipboard image init failed: ${(e as Error).message}`);
|
||||
}
|
||||
}).catch((e: BusinessError) => {
|
||||
this.log.w(`createPixelMap failed: ${e.code} ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// —— 系统分享(可靠兜底;文本不下发 url)——
|
||||
|
||||
private systemText(uiCtx: common.UIAbilityContext, bean: SharetypeBean): void {
|
||||
const text: string = buildShareText(bean.title, bean.description);
|
||||
const record: systemShare.SharedRecord = {
|
||||
utd: utd.UniformDataType.HYPERLINK,
|
||||
content: url,
|
||||
title: bean.title !== '' ? bean.title : undefined,
|
||||
description: bean.description !== '' ? bean.description : undefined,
|
||||
utd: utd.UniformDataType.PLAIN_TEXT, content: text !== '' ? text : ' ',
|
||||
};
|
||||
this.showSystemShare(uiCtx, record);
|
||||
}
|
||||
|
||||
/** 视频分享(webpageUrl 为视频地址;远程地址降级为链接分享)。 */
|
||||
private systemVideo(uiCtx: common.UIAbilityContext, bean: SharetypeBean): void {
|
||||
const path: string = bean.webpageUrl;
|
||||
if (path !== '' && (path.startsWith('/') || path.startsWith('file://')) && FileSystem.exists(this.toLocalPath(path))) {
|
||||
const local: string = this.toLocalPath(path);
|
||||
const typeId: string = utd.getUniformDataTypeByFilenameExtension(this.extOf(local), utd.UniformDataType.VIDEO);
|
||||
const record: systemShare.SharedRecord = {
|
||||
utd: typeId,
|
||||
uri: fileUri.getUriFromPath(local),
|
||||
title: bean.title !== '' ? bean.title : undefined,
|
||||
description: bean.description !== '' ? bean.description : undefined,
|
||||
};
|
||||
this.showSystemShare(uiCtx, record);
|
||||
return;
|
||||
}
|
||||
// 非本地视频文件:以链接形式分享地址(系统分享无法直接推送远程视频)
|
||||
this.systemLink(uiCtx, bean);
|
||||
}
|
||||
|
||||
/** 图片分享(沙箱文件)。 */
|
||||
private systemImage(uiCtx: common.UIAbilityContext, filePath: string, bean: SharetypeBean): void {
|
||||
const typeId: string = utd.getUniformDataTypeByFilenameExtension(this.extOf(filePath), utd.UniformDataType.IMAGE);
|
||||
const typeId: string =
|
||||
utd.getUniformDataTypeByFilenameExtension(this.extOf(filePath), utd.UniformDataType.IMAGE);
|
||||
const record: systemShare.SharedRecord = {
|
||||
utd: typeId,
|
||||
uri: fileUri.getUriFromPath(filePath),
|
||||
@@ -320,7 +313,6 @@ export class ShareProvider implements CapabilityProvider {
|
||||
this.showSystemShare(uiCtx, record);
|
||||
}
|
||||
|
||||
/** 拉起系统分享面板(失败 log 不崩溃)。 */
|
||||
private showSystemShare(uiCtx: common.UIAbilityContext, record: systemShare.SharedRecord): void {
|
||||
try {
|
||||
const data: systemShare.SharedData = new systemShare.SharedData(record);
|
||||
@@ -343,10 +335,7 @@ export class ShareProvider implements CapabilityProvider {
|
||||
this.bridge?.callHandler(OutboundHandlers.ShareSuccess, JSON.stringify(out));
|
||||
}
|
||||
|
||||
/**
|
||||
* dataURL("data:image/...;base64,XXXX")→ 解码 → 写沙箱 filesDir/share/shot_<seq>.jpg → 返回沙箱路径。
|
||||
* 失败返回空串(调用方降级)。
|
||||
*/
|
||||
/** dataURL/纯base64 → 解码 → 写沙箱 filesDir/share/shot_<seq>.jpg|png → 返回路径;失败返回空串。 */
|
||||
private saveDataUrlToFile(uiCtx: common.UIAbilityContext, dataUrl: string): string {
|
||||
try {
|
||||
const comma: number = dataUrl.indexOf(',');
|
||||
@@ -366,22 +355,6 @@ export class ShareProvider implements CapabilityProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private joinText(title: string, description: string): string {
|
||||
const parts: string[] = [];
|
||||
if (title !== '') {
|
||||
parts.push(title);
|
||||
}
|
||||
if (description !== '') {
|
||||
parts.push(description);
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
}
|
||||
|
||||
/** file:// 前缀 → 本地路径。 */
|
||||
private toLocalPath(p: string): string {
|
||||
return p.startsWith('file://') ? p.substring('file://'.length) : p;
|
||||
}
|
||||
|
||||
private extOf(path: string): string {
|
||||
const dot: number = path.lastIndexOf('.');
|
||||
const slash: number = path.lastIndexOf('/');
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/**
|
||||
* 微信开放平台 SDK 封装(@tencent/wechat_open_sdk,契约 §10.1/§10.2/§13)。单例。
|
||||
* 微信开放平台 SDK 封装(@tencent/wechat_open_sdk)。单例。**仅用于登录授权**(分享已去 SDK 化)。
|
||||
*
|
||||
* 职责:持有 WXApi(用 AppID 创建)、发起授权/分享请求、接收微信回调并路由到对应回调。
|
||||
* AppID 为公开标识,可入客户端;**AppSecret 绝不入端**(登录换 profile 须服务端,§13 红线)。
|
||||
*
|
||||
* 回调路径:微信处理完拉回宿主 App(默认 EntryAbility),EntryAbility.onNewWant/onCreate
|
||||
* 把 want 交给 handleWant() → SDK 解析 → onResp 路由到 authCb/shareCb。
|
||||
* 职责:持有 WXApi(用 AppID 创建)、发起授权请求、接收微信回调并路由到 authCb。
|
||||
* AppID 为公开标识,可入客户端;AppSecret 绝不入端(登录换 profile 须服务端,§13 红线)。
|
||||
*/
|
||||
import { common, Want } from '@kit.AbilityKit';
|
||||
import {
|
||||
WXAPIFactory, WXApi, WXApiEventHandler, BaseReq, BaseResp,
|
||||
SendAuthResp, SendMessageToWXResp,
|
||||
WXAPIFactory, WXApi, WXApiEventHandler, BaseReq, BaseResp, SendAuthResp,
|
||||
} from '@tencent/wechat_open_sdk';
|
||||
import { Logger } from 'common';
|
||||
|
||||
@@ -18,7 +14,6 @@ import { Logger } from 'common';
|
||||
export const WX_APP_ID: string = 'wxd2bd650e06bdfe58';
|
||||
|
||||
export type AuthRespCallback = (resp: SendAuthResp) => void;
|
||||
export type ShareRespCallback = (resp: SendMessageToWXResp) => void;
|
||||
|
||||
export class WeChatApi {
|
||||
private static readonly log: Logger = Logger.tag('WeChatApi');
|
||||
@@ -26,7 +21,6 @@ export class WeChatApi {
|
||||
private readonly api: WXApi;
|
||||
private readonly handler: WXApiEventHandler;
|
||||
private authCb: AuthRespCallback | undefined = undefined;
|
||||
private shareCb: ShareRespCallback | undefined = undefined;
|
||||
|
||||
private constructor() {
|
||||
this.api = WXAPIFactory.createWXAPI(WX_APP_ID);
|
||||
@@ -58,12 +52,6 @@ export class WeChatApi {
|
||||
this.api.sendReq(context, req);
|
||||
}
|
||||
|
||||
/** 发起分享;resp 经 cb 异步回调。 */
|
||||
sendShare(context: common.UIAbilityContext, req: BaseReq, cb: ShareRespCallback): void {
|
||||
this.shareCb = cb;
|
||||
this.api.sendReq(context, req);
|
||||
}
|
||||
|
||||
/** 由 EntryAbility 在 onNewWant/onCreate 调用,处理微信回调 want。 */
|
||||
handleWant(want: Want): void {
|
||||
try {
|
||||
@@ -80,12 +68,6 @@ export class WeChatApi {
|
||||
if (cb !== undefined) {
|
||||
cb(resp);
|
||||
}
|
||||
} else if (resp instanceof SendMessageToWXResp) {
|
||||
const cb = this.shareCb;
|
||||
this.shareCb = undefined;
|
||||
if (cb !== undefined) {
|
||||
cb(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user