import webview from '@ohos.web.webview';
import router from '@ohos.router';
import common from '@ohos.app.ability.common';
import window from '@ohos.window';
import { TencentQianWebViewConfig, StatusBarStyle } from './model/TencentQianWebViewConfig';
import { OnSignResult, OnSdkEvent } from './model/SignCallback';
import { SignResult } from './model/SignResult';
import { IntentBridge, IntentBridgePayload } from './internal/IntentBridge';
import {
TencentQianWebViewConfigurator,
TencentQianWebViewHandlers
} from './TencentQianWebViewConfigurator';
import { WebHostModel } from './internal/prewarm/WebHostModel';
import { WebNodeController } from './internal/prewarm/TencentQianWebNode';
import { HarmonyWebViewPool } from './internal/prewarm/HarmonyWebViewPool';
/**
* 内置容器页面在命名路由表中的注册名,与 `router_map.json` 一致。
*/
export const TENCENT_QIAN_WEBVIEW_PAGE_NAME: string = 'TencentQianWebViewPage';
/**
* SDK 内置容器页面,由 `TencentQianWebView.open()` 通过命名路由拉起。
*
* Web 组件改由离线组件(`BuilderNode` + `NodeContainer`)承载,与 Component / 预热池
* 共用同一 `@Builder`(`TencentQianWebNode`)。进度条 / 标题 / canGoBack 通过 `WebHostModel`
* 的回调驱动本页 `@State`(可靠触发 rebuild),规避跨 `BuilderNode` 的深层响应式不确定性。
*
* 提供顶栏、进度条、关闭确认对话框、状态栏样式控制、`qianapp://` 协议命中后自动关闭页面等能力。
*/
@Entry({ routeName: TENCENT_QIAN_WEBVIEW_PAGE_NAME })
@Component
struct TencentQianWebViewPage {
// ── 从 IntentBridge 取出的入参 ──
@State private url: string = '';
@State private config: TencentQianWebViewConfig = new TencentQianWebViewConfig();
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
private signResultCallback: OnSignResult = (_r): void => {
// no-op
};
private sdkEventCallback?: OnSdkEvent = undefined;
// ── Web 状态 ──
private controller: webview.WebviewController = new webview.WebviewController();
@State private handlers: TencentQianWebViewHandlers | null = null;
/** 承载离线 Web 组件的 NodeController。 */
@State private nodeController: WebNodeController | null = null;
/** 本页 Web 实例是否借自预热池(决定关闭时归还还是销毁)。 */
private fromPool: boolean = false;
@State private progress: number = 0;
@State private progressVisible: boolean = false;
@State private currentTitle: string = '';
@State private canGoBack: boolean = false;
// ── 防重复关闭 ──
private closing: boolean = false;
aboutToAppear(): void {
const payload: IntentBridgePayload | null = IntentBridge.peek();
if (payload === null) {
console.error('[TencentQianWebViewPage] IntentBridge has no payload, closing');
router.back();
return;
}
this.url = payload.url;
this.config = payload.config;
this.signResultCallback = (result: SignResult): void => {
// 关闭流程仅触发一次 onSignResult,避免双回调
if (this.closing) {
return;
}
this.closing = true;
payload.onSignResult(result);
try {
router.back();
} catch (_e) {
// ignore
}
IntentBridge.clear();
};
this.sdkEventCallback = payload.onSdkEvent;
// 优先复用预热池实例(UA 配置匹配时);否则自建离线组件
const pooled: WebNodeController | null =
HarmonyWebViewPool.obtain(this.config.injectUserAgent, this.config.userAgentExtra);
if (pooled !== null && pooled.getModel() !== null) {
this.reusePooled(pooled);
} else {
this.buildOwnNode();
}
this.applyStatusBar();
}
aboutToDisappear(): void {
// 释放网络状态监听
if (this.handlers !== null) {
try {
this.handlers.networkStateInjector.detach();
} catch (_e) {
// ignore
}
}
// 断开 model 对本容器的联动回调,防止池 model 长期持有已销毁容器引用(内存泄漏)。
if (this.nodeController !== null) {
const model: WebHostModel | null = this.nodeController.getModel();
if (model !== null) {
model.clearContainerListeners();
}
}
if (this.fromPool) {
// 借还池(同 Android):先解绑 NodeContainer,再归还(about:blank + clearHistory,不销毁)
this.nodeController = null;
HarmonyWebViewPool.recycle();
} else {
// 自建:销毁离线节点
if (this.nodeController !== null) {
this.nodeController.disposeNode();
this.nodeController = null;
}
}
// 页面销毁兜底,保证 IntentBridge 清空
if (!this.closing) {
this.closing = true;
IntentBridge.clear();
}
}
/** 复用预热池实例:用池 controller 生成真实 handlers,热替换 model.handlers/url 并重新激活。 */
private reusePooled(pooled: WebNodeController): void {
const model: WebHostModel = pooled.getModel()!;
this.controller = model.controller;
this.handlers = TencentQianWebViewConfigurator.apply(
this.controller,
this.context,
this.config,
this.signResultCallback,
this.sdkEventCallback
);
// detach 预热 handlers 的网络监听,切到真实 handlers
const old: TencentQianWebViewHandlers | null = model.handlers;
if (old !== null) {
try {
old.networkStateInjector.detach();
} catch (_e) {
// ignore
}
}
model.handlers = this.handlers;
model.url = this.url;
// 池实例已在预热时加载过 warmupUrl(历史栈残留);标记首个 onPageEnd 清历史,
// 使 signingUrl 成为唯一历史项,避免第一次返回退回预热页。
model.clearHistoryOnce = true;
// 标记来自池 + 绑定作废回调:借出态渲染进程崩溃时作废池实例(避免僵尸复用),
// 并同步容器 fromPool=false(后续关闭不再走归还)。对齐 Android。
model.fromPool = true;
model.onPoolInvalidateRequest = (): void => {
// 借出态崩溃:只脱池(不再被 obtain 复用 / 不再被 recycle 收回),**不销毁正在显示的活节点**。
// controller 崩溃后仍有效,容器在同一 controller 上 refresh() 原地恢复(鸿蒙无需分离节点/重建);
// 活节点由容器继续持有,关闭时(fromPool=false → disposeNode)再销毁收口。对齐 Android invalidateIfPooled。
HarmonyWebViewPool.unpoolBorrowed();
this.fromPool = false;
};
this.bindModelListeners(model);
this.nodeController = pooled;
this.fromPool = true;
// controller 已 attach(预热时),手动执行真实 handlers 的 attach(UA + 网络监听)
try {
this.handlers.onControllerAttached();
} catch (_e) {
// ignore
}
// 加载真实签署页(命中预热热身的连接/内核缓存)
try {
this.controller.loadUrl(this.url);
} catch (_e) {
// ignore
}
}
/** 自建离线组件(无池可复用时)。 */
private buildOwnNode(): void {
this.handlers = TencentQianWebViewConfigurator.apply(
this.controller,
this.context,
this.config,
this.signResultCallback,
this.sdkEventCallback
);
const model: WebHostModel = new WebHostModel(this.controller);
model.url = this.url;
model.handlers = this.handlers;
this.bindModelListeners(model);
const ctrl: WebNodeController = new WebNodeController();
ctrl.init(this.getUIContext(), model);
this.nodeController = ctrl;
this.fromPool = false;
}
/** 绑定进度/标题联动回调到 model(驱动本页 @State)。 */
private bindModelListeners(model: WebHostModel): void {
model.onProgressChanged = (progress: number, visible: boolean): void => {
this.progress = progress;
this.progressVisible = visible;
};
model.onTitleChanged = (title: string, canGoBack: boolean): void => {
this.currentTitle = title;
this.canGoBack = canGoBack;
};
// 渲染进程崩溃且 refresh/loadUrl 均无法恢复 → 关闭页面,避免持久白屏。
// 对齐 Android recoverFromRenderProcessGone 恢复失败 finish()。
model.onRenderUnrecoverable = (): void => {
this.doClose();
};
}
/**
* 状态栏样式应用。
*/
private async applyStatusBar(): Promise<void> {
try {
if (this.context === null) {
return;
}
const style: StatusBarStyle = this.config.statusBarStyle;
const isDark: boolean = style === 'DARK_CONTENT';
const win = await window.getLastWindow(this.context);
await win.setWindowSystemBarProperties({
statusBarContentColor: isDark ? '#000000' : '#FFFFFF'
});
} catch (_e) {
// 容错
}
}
/**
* 顶栏返回按钮 / 系统返回键处理。
*
* 实时查询 `accessBackward()` 判断是否可后退,而非依赖 `onPageEnd` 刷新的
* `canGoBack` @State —— SPA 的 `pushState` / `hashchange` 路由跳转不触发 `onPageEnd`,
* `canGoBack` 会陈旧,导致 H5 页内还能后退时却误弹退出。实时查询可覆盖
* 整页导航与 SPA 路由两种历史深度,退到 H5 首屏(不可后退)才走关闭流程。
*/
private handleBack(): void {
let canBack: boolean = false;
try {
canBack = this.controller.accessBackward();
} catch (_e) {
canBack = false;
}
if (canBack) {
try {
this.controller.backward();
} catch (_e) {
this.tryClose();
}
return;
}
this.tryClose();
}
/**
* 尝试关闭页面(若开启确认则先弹框)。
*/
private tryClose(): void {
if (this.config.confirmBeforeClose) {
this.showCloseConfirm();
return;
}
this.doClose();
}
/**
* 显示关闭确认对话框。
*/
private async showCloseConfirm(): Promise<void> {
const title: string = this.config.confirmCloseTitle.length > 0
? this.config.confirmCloseTitle
: '确认离开?';
const message: string = this.config.confirmCloseMessage.length > 0
? this.config.confirmCloseMessage
: '签署流程尚未完成,确认离开吗?';
try {
const result = await this.getUIContext().getPromptAction().showDialog({
title: title,
message: message,
buttons: [
{ text: '继续签署', color: '#999999' },
{ text: '离开', color: '#000000' }
]
});
if (result.index === 1) {
this.doClose();
}
} catch (_e) {
// ignore
}
}
/**
* 实际关闭页面。
*/
private doClose(): void {
if (this.closing) {
return;
}
this.closing = true;
IntentBridge.clear();
try {
router.back();
} catch (_e) {
// ignore
}
}
/**
* 系统返回键处理。返回 true = 已消费。
*/
onBackPress(): boolean {
this.handleBack();
return true;
}
build() {
Column() {
// ────────── 顶栏 ──────────
if (this.config.showTopBar) {
Row() {
// 返回按钮
Text('<')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.resolveTopBarTextColor())
.width(44)
.height('100%')
.textAlign(TextAlign.Center)
.onClick(() => this.handleBack());
// 标题
Text(this.resolveTopBarTitle())
.fontSize(17)
.fontWeight(FontWeight.Medium)
.fontColor(this.resolveTopBarTextColor())
.layoutWeight(1)
.textAlign(TextAlign.Center)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
// 关闭按钮
if (this.config.showCloseButton) {
Text('×')
.fontSize(24)
.fontColor(this.resolveTopBarTextColor())
.width(44)
.height('100%')
.textAlign(TextAlign.Center)
.onClick(() => this.tryClose());
} else {
Blank().width(44);
}
}
.width('100%')
.height(56)
.backgroundColor(this.resolveTopBarBackgroundColor());
}
// ────────── Web 组件 + 进度条(Stack 叠加)──────────
// 进度条浮在 Web 顶部,不占据布局空间,避免显隐时布局跳动(对齐 Android / iOS)。
Stack({ alignContent: Alignment.Top }) {
if (this.nodeController !== null) {
NodeContainer(this.nodeController)
.width('100%')
.height('100%');
} else {
Text('容器初始化失败,请重试')
.fontSize(14)
.fontColor('#999999')
.width('100%')
.height('100%')
.textAlign(TextAlign.Center);
}
if (this.progressVisible) {
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.height(2)
.width('100%')
.color('#1989FA')
.hitTestBehavior(HitTestMode.Transparent);
}
}
.layoutWeight(1)
.width('100%');
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF');
}
private resolveTopBarTitle(): string {
if (this.config.topBarTitle.length > 0) {
return this.config.topBarTitle;
}
return this.currentTitle.length > 0 ? this.currentTitle : '电子签署';
}
private resolveTopBarBackgroundColor(): number {
return this.config.topBarBackgroundColor !== null
? this.config.topBarBackgroundColor
: 0xFFFFFFFF;
}
private resolveTopBarTextColor(): number {
return this.config.topBarTextColor !== null
? this.config.topBarTextColor
: 0xFF000000;
}
}
|