<

{{title}}

{{file.summary.functions.pct}}%

Functions
{{file.summary.functions.covered}}/{{file.summary.functions.total}}

{{file.summary.branches.pct}}%

Branches
{{file.summary.branches.covered}}/{{file.summary.branches.total}}

{{file.summary.lines.pct}}%

Lines
{{file.summary.lines.covered}}/{{file.summary.lines.total}}
          
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
                
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; 
  } 
} 

              
Code coverage generated by bjc at {{date}}