mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-24 02:48:04 +00:00
feat(ohos): screensue 改用 Web 组件渲染 HTML(RichText 撑不住)
用户实测截图:推送内容把整段 HTML 源码当字符串显示(含 <style>、@keyframes、
内联 <svg>、radial-gradient)。RichText 只认极小标签子集,这些一律不渲染。
用户明确要求「引入 webview」。
## 修法
- 新增 `common/ScreensueHtml.ets`(从 BridgeCaps 抽出:后者加进 HTML 逻辑后
超 520 行,越了工程「单文件 ≤400 行」的约定;且「screensue 怎么解析/渲染」与
「设备能力怎么实现」本是两件事)。
- `ScreensuePage.ets`:HTML 走 **Web**,纯文本仍走 Text。
- 加载用 `loadData(base64)`:encoding 非 base64 时按 URL 规则转义,几 KB 的
完整文档会撞长度/转义问题。自写 `base64Utf8`(UTF-8 手编字节,含代理对合成)
—— 直接把 UTF-16 码元交给 Base64Helper 会让中文变乱码。
- 非完整文档补一层 shell(meta viewport + 主题前景色),完整文档原样加载。
## 顺带修掉一个真实缺陷(实测发现)
`looksLikeHtml` 旧判据要求「首个非空字符就是 '<'」。而 agent 传参常把整段文档
连引号一起给(`'<html>…'`)——截图里那个孤立的 `'` 就是这么来的,判据因此
**判否并退回纯文本**,所以看到的是源码。改为扫第一个「像标签开头」的 '<'
(跳过引号/前导文字),且只在其后紧跟字母或 '/' 时才算,避免误判 "a < b"。
Node 复刻同一算法验证了 7 个样例(含截图实况、<3 表情、比较符)。
## 安全(我因为引入 WebView 而必须自己把关)
内容来自 agent(第三方)。显式关闭:
`javaScriptAccess(false)` / `fileAccess(false)` / `domStorageAccess(false)` /
`onlineImageAccess(false)` / `zoomAccess(false)`。
★ **javaScriptAccess 的默认值是 true** —— 不显式关掉等于让远端内容在客户端执行脚本。
已实测取证:推入带 `<script>` 与 `<img onerror>` 的页面,屏上稳定显示 `JS-OFF`,
两条执行路径都没跑起来。
## 另一个实测发现的缺陷
`onControllerAttached` 只在挂载时触发一次,而 ScreensuePage 在 `if (visible)`
里常驻 —— 连续两条 screensue 只改 @Prop、组件不重建,Web 一直显示**上一条**
内容(实测:倒计时变成 33s 但画面还是旧 HTML)。改 `@Prop @Watch('onDataChanged')`
显式重载,并记录 attached 状态避免过早 loadData(会抛 17100001)。
## 验证(模拟器真机链路)
把工程内连接指向本机服务、开启「允许 agent 控制本机」授权,经
`device_ctl_cmdrun` → 设备桥 → screensue 推入用户截图里那段原样 HTML:
- 渲染成功:radial-gradient 背景、内联 SVG 兔子、CSS 发光文字、两行文案
(修复前同一段内容显示为满屏标签源码)
- 连推第二条 → 画面正确刷新为 SECOND PUSH
- JS 探测 → JS-OFF(脚本被拦)
模拟器只能装 unsigned 包(signed 报 READ_PASTEBOARD 授权失败,与既有记录一致)。
This commit is contained in:
@ -426,98 +426,3 @@ export function capDeviceInfo(deviceId: string, deviceName: string): CapResult {
|
||||
};
|
||||
return okResult(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
// ===== screensue 内容解析 =====
|
||||
// 服务端协议: screensue [秒] <内容>;0=常驻。
|
||||
|
||||
export interface ScreensuePayload {
|
||||
duration: number; // 秒;0 表示常驻直到用户关闭
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function parseScreensue(rawArgs: string): ScreensuePayload {
|
||||
const p: ScreensuePayload = { duration: 5, content: '' };
|
||||
const leadingSpaces: RegExp = new RegExp('^\\s+');
|
||||
const firstSpace: RegExp = new RegExp('\\s');
|
||||
let rest: string = rawArgs.replace(leadingSpaces, '');
|
||||
const splitAt: number = rest.search(firstSpace);
|
||||
if (splitAt > 0) {
|
||||
const first: string = rest.substring(0, splitAt);
|
||||
const digits: RegExp = new RegExp('^\\d+$');
|
||||
if (digits.test(first)) {
|
||||
p.duration = Math.min(parseInt(first, 10), 86400);
|
||||
rest = rest.substring(splitAt).replace(leadingSpaces, '');
|
||||
}
|
||||
}
|
||||
p.content = rest;
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 agent 下发的 screensue 内容是不是 HTML 片段。
|
||||
*
|
||||
* 服务端协议两侧都允许 HTML(localuse 的 local_screensue 在 Linux 用 browsh/w3m
|
||||
* 渲染 HTML;remotedevice 的工具说明写的就是「显示内容/HTML」)。此前鸿蒙端一律
|
||||
* 塞进 Text(),于是 HTML 被当成普通字符串原样显示成标签。
|
||||
*
|
||||
* 判据取「首个非空字符是 '<'」并且「存在配对的 '>'」且含字母/斜杠,
|
||||
* 避免把 "<3" 这类纯文本误判成标签;再排除纯文本里常见的比较式(如 "a < b")。
|
||||
*/
|
||||
export function looksLikeHtml(content: string): boolean {
|
||||
const trimmed: string = content.trim();
|
||||
if (trimmed.length === 0 || trimmed.charAt(0) !== '<') {
|
||||
return false;
|
||||
}
|
||||
const close: number = trimmed.indexOf('>');
|
||||
if (close < 0) {
|
||||
return false;
|
||||
}
|
||||
// 标签名必须是字母开头(</div>、<div>、<br/>),"< 3" 这类不是标签。
|
||||
const inner: string = trimmed.substring(1, close).trim();
|
||||
if (inner.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const head: string = inner.charAt(0) === '/' ? inner.substring(1) : inner;
|
||||
const tagName: RegExp = new RegExp('^[a-zA-Z][a-zA-Z0-9]*');
|
||||
return tagName.test(head);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一段内容包成可交给 RichText 渲染的最小 HTML 文档。
|
||||
*
|
||||
* 为什么用 RichText 而不是 Web:screensue 的内容是 agent 下发的第三方文本,
|
||||
* Web 组件默认带 JS 与网络能力(javaScriptAccess/fileAccess),等于让远端内容
|
||||
* 在客户端进程里执行脚本。RichText 只解析 HTML 子集、无脚本、无网络,
|
||||
* 对「给用户看一段内容」这个用途正好够用且更安全。
|
||||
*
|
||||
* RichText 用系统默认前景色,深色主题下会变成黑字看不见 —— 这里显式注入
|
||||
* 颜色/字号,让两种主题下都可读。
|
||||
*/
|
||||
export function screensueHtml(content: string, dark: boolean): string {
|
||||
const fg: string = dark ? '#E8ECF4' : '#1B2430';
|
||||
const trimmed: string = content.trim();
|
||||
// 已是完整文档就不要重复包 <html>(RichText 需要单一根节点才稳定)。
|
||||
const body: string = looksLikeHtml(trimmed) ? trimmed : escapeHtmlText(trimmed);
|
||||
return '<div style="color:' + fg + ';font-size:16px;line-height:1.6;'
|
||||
+ 'word-break:break-word;padding:2px">' + body + '</div>';
|
||||
}
|
||||
|
||||
/** HTML 转义(纯文本走 RichText 时用,避免内容里的 < > 被当成标签)。 */
|
||||
export function escapeHtmlText(s: string): string {
|
||||
let out: string = '';
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c: string = s.charAt(i);
|
||||
if (c === '&') {
|
||||
out += '&';
|
||||
} else if (c === '<') {
|
||||
out += '<';
|
||||
} else if (c === '>') {
|
||||
out += '>';
|
||||
} else if (c === '"') {
|
||||
out += '"';
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -9,9 +9,8 @@ import {
|
||||
capSpeakerUse,
|
||||
capDeviceInfo,
|
||||
capStatus,
|
||||
parseScreensue,
|
||||
ScreensuePayload,
|
||||
} from './BridgeCaps';
|
||||
import { parseScreensue, ScreensuePayload } from './ScreensueHtml';
|
||||
import { connStore } from './ConnStore';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
|
||||
|
||||
182
cmd/ohos/HomeAgent/entry/src/main/ets/common/ScreensueHtml.ets
Normal file
182
cmd/ohos/HomeAgent/entry/src/main/ets/common/ScreensueHtml.ets
Normal file
@ -0,0 +1,182 @@
|
||||
/**
|
||||
* screensue 载荷解析与 HTML 渲染(无 UI 依赖)。
|
||||
*
|
||||
* 从 BridgeCaps.ets 抽出:加进 HTML 检测/编码后那个文件超过 520 行,
|
||||
* 超出工程「单文件 ≤400 行」的约定;而「screensue 内容怎么解析、怎么渲染」
|
||||
* 与「设备能力怎么实现」本就是两件事。
|
||||
*/
|
||||
|
||||
import { util } from '@kit.ArkTS';
|
||||
|
||||
// ===== screensue 内容解析 =====
|
||||
// 服务端协议: screensue [秒] <内容>;0=常驻。
|
||||
|
||||
export interface ScreensuePayload {
|
||||
duration: number; // 秒;0 表示常驻直到用户关闭
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function parseScreensue(rawArgs: string): ScreensuePayload {
|
||||
const p: ScreensuePayload = { duration: 5, content: '' };
|
||||
const leadingSpaces: RegExp = new RegExp('^\\s+');
|
||||
const firstSpace: RegExp = new RegExp('\\s');
|
||||
let rest: string = rawArgs.replace(leadingSpaces, '');
|
||||
const splitAt: number = rest.search(firstSpace);
|
||||
if (splitAt > 0) {
|
||||
const first: string = rest.substring(0, splitAt);
|
||||
const digits: RegExp = new RegExp('^\\d+$');
|
||||
if (digits.test(first)) {
|
||||
p.duration = Math.min(parseInt(first, 10), 86400);
|
||||
rest = rest.substring(splitAt).replace(leadingSpaces, '');
|
||||
}
|
||||
}
|
||||
p.content = rest;
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 agent 下发的 screensue 内容是不是 HTML。
|
||||
*
|
||||
* 服务端两侧协议都允许 HTML(localuse 的 local_screensue 在 Linux 用 browsh/w3m
|
||||
* 渲染 HTML;remotedevice 的工具说明写的就是「显示内容/HTML」)。
|
||||
*
|
||||
* ★ 判据必须容忍前导杂质:实测 agent 常把整段文档连引号一起传进来
|
||||
* (`'<html>…</html>'`),而"首个非空字符必须是 '<'"的旧判据直接判否、
|
||||
* 退回纯文本渲染,用户看到的就是满屏标签源码(截图取证)。
|
||||
*
|
||||
* 所以这里扫到第一个「像标签开头」的 '<',不要求它在开头;但只有后面紧根
|
||||
* 字母或 '/' 时才认,避免把 "a < b" 这类文本里的比较符当标签。
|
||||
*/
|
||||
export function looksLikeHtml(content: string): boolean {
|
||||
return findHtmlStart(content) >= 0;
|
||||
}
|
||||
|
||||
/** 找到第一个「像标签开头」的 '<';没有则 -1。 */
|
||||
function findHtmlStart(content: string): number {
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content.charAt(i) !== '<') {
|
||||
continue;
|
||||
}
|
||||
const next: string = i + 1 < content.length ? content.charAt(i + 1) : '';
|
||||
if (next === '/') {
|
||||
const after: string = i + 2 < content.length ? content.charAt(i + 2) : '';
|
||||
if (isAsciiLetter(after)) {
|
||||
return i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isAsciiLetter(next)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isAsciiLetter(ch: string): boolean {
|
||||
if (ch.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const c: number = ch.charCodeAt(0);
|
||||
return (c >= 65 && c <= 90) || (c >= 97 && c <= 122);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出真正的 HTML 片段:剥掉 agent 误带的包裹引号,再从头截到第一个标签。
|
||||
*
|
||||
* 剥引号是必须的:不剥的话那个孤立的 `'` 会被 Web 当正文渲染出来
|
||||
* (截图上第一行就是它),而且它还会把后续判据带偏。返回 '' 表示不是 HTML。
|
||||
*/
|
||||
export function screensueHtmlDocument(content: string): string {
|
||||
let body: string = content.trim();
|
||||
// 反复剥成对的包裹引号(agent 把整段 HTML 当命令参数传时的常见形态)。
|
||||
while (body.length >= 2) {
|
||||
const first: string = body.charAt(0);
|
||||
const last: string = body.charAt(body.length - 1);
|
||||
if ((first === '\'' && last === '\'') || (first === '"' && last === '"')) {
|
||||
body = body.substring(1, body.length - 1).trim();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const idx: number = findHtmlStart(body);
|
||||
if (idx < 0) {
|
||||
return '';
|
||||
}
|
||||
if (idx > 0) {
|
||||
body = body.substring(idx);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 screensue 内容编成可直接交给 Web 组件 `loadData` 的 base64。
|
||||
*
|
||||
* 为什么必须上 Web(不再用 RichText):RichText 只认极小标签子集,
|
||||
* 对 <style>、CSS 动画、内联 SVG 一律不渲染 —— 实测 agent 推的是完整
|
||||
* HTML 文档(含 @keyframes 与 <svg>),RichText 下只能看到源码。用户明确要求引入 webview。
|
||||
*
|
||||
* 为什么用 base64 而不是明文 loadData:encoding 非 base64 时按 URL 规则转义,
|
||||
* 一个几 KB 的完整文档会撞上长度/转义问题;base64 是整篇加载的推荐方式,
|
||||
* 中文与引号、'#' 也不会被二次转义(自己手写 UTF-8 编码,见 base64Utf8)。
|
||||
*
|
||||
* 片段(非完整文档)补一层 shell:加 <meta viewport> 让窄屏排版正确,
|
||||
* 并注入主题前景色,避免深色主题下黑字不可见。返回 '' 表示不是 HTML(走纯文本渲染)。
|
||||
*/
|
||||
export function screensueWebData(content: string, dark: boolean): string {
|
||||
const fragment: string = screensueHtmlDocument(content);
|
||||
if (fragment.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (hasHtmlShell(fragment)) {
|
||||
// 已是完整文档:不再包壳,也不注入颜色(由页面自带样式决定)。
|
||||
return base64Utf8(fragment);
|
||||
}
|
||||
const fg: string = dark ? '#E8ECF4' : '#1B2430';
|
||||
const wrapped: string = '<!DOCTYPE html><html><head><meta charset="utf-8">'
|
||||
+ '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
||||
+ '<style>html,body{margin:0;padding:0}'
|
||||
+ 'body{padding:10px;color:' + fg + ';font-family:sans-serif;font-size:16px;'
|
||||
+ 'line-height:1.6;word-break:break-word;-webkit-text-size-adjust:100%}'
|
||||
+ 'img,svg,video{max-width:100%;height:auto}</style></head><body>'
|
||||
+ fragment + '</body></html>';
|
||||
return base64Utf8(wrapped);
|
||||
}
|
||||
|
||||
/** 内容是否已是完整 HTML 文档(有 <html> 或 <!DOCTYPE>),不必再包壳。 */
|
||||
function hasHtmlShell(s: string): boolean {
|
||||
const head: string = s.substring(0, 400).toLowerCase();
|
||||
return head.indexOf('<html') >= 0 || head.indexOf('<!doctype') >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 字符串 → base64。
|
||||
*
|
||||
* 不能把 UTF-16 码元直接交给 Base64Helper:那样中文会变成乱码。
|
||||
* 这里手写 UTF-8 字节序列(按码点,含代理对合成)后再编码。
|
||||
*/
|
||||
export function base64Utf8(s: string): string {
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
let code: number = s.charCodeAt(i);
|
||||
// 代理对(emoji 等)合成成一个码点。
|
||||
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < s.length) {
|
||||
const next: number = s.charCodeAt(i + 1);
|
||||
if (next >= 0xDC00 && next <= 0xDFFF) {
|
||||
code = ((code - 0xD800) << 10) + (next - 0xDC00) + 0x10000;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (code < 0x80) {
|
||||
bytes.push(code);
|
||||
} else if (code < 0x800) {
|
||||
bytes.push(0xC0 | (code >> 6), 0x80 | (code & 0x3F));
|
||||
} else if (code < 0x10000) {
|
||||
bytes.push(0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F));
|
||||
} else {
|
||||
bytes.push(0xF0 | (code >> 18), 0x80 | ((code >> 12) & 0x3F),
|
||||
0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F));
|
||||
}
|
||||
}
|
||||
const helper: util.Base64Helper = new util.Base64Helper();
|
||||
return helper.encodeToStringSync(new Uint8Array(bytes));
|
||||
}
|
||||
@ -2,7 +2,8 @@ import { ThemePalette, DARK_PALETTE, LIGHT_PALETTE, ANIM_NORMAL } from '../commo
|
||||
import { GradientBackground } from './GradientBackground';
|
||||
import { PageTopBar } from './PageTopBar';
|
||||
import { MotionBase } from './MotionBase';
|
||||
import { looksLikeHtml, screensueHtml } from '../common/BridgeCaps';
|
||||
import { looksLikeHtml, screensueWebData } from '../common/ScreensueHtml';
|
||||
import { webview } from '@kit.ArkWeb';
|
||||
|
||||
/**
|
||||
* agent 主动推送的前台内容页。
|
||||
@ -10,9 +11,15 @@ import { looksLikeHtml, screensueHtml } from '../common/BridgeCaps';
|
||||
* 调用方负责决定页面宽度:窄屏占满窗口,宽屏只占右侧内容栏,
|
||||
* 从而让左侧一级页面和主导航保持可见、可操作。
|
||||
*
|
||||
* 内容可能是纯文本,也可能是 HTML(服务端两侧协议都允许,见 BridgeCaps 的
|
||||
* screensueHtml)。HTML 用 RichText 渲染:它只解析 HTML 子集、无脚本、无网络,
|
||||
* 对 agent 下发的远端内容比 Web 组件安全(Web 默认带 JS 与网络能力)。
|
||||
* 内容可能是纯文本,也可能是 HTML(服务端两侧协议都允许,见 ScreensueHtml)。
|
||||
*
|
||||
* HTML 走 **Web 组件**(用户明确要求):agent 推的常是完整文档 —— 带 <style>
|
||||
* CSS 动画、内联 <svg>、radial-gradient 背景。RichText 只认极小标签子集,
|
||||
* 对这些一律不渲染,实测只能看到满屏源码。
|
||||
*
|
||||
* 安全:内容来自 agent(第三方),所以显式关掉 JS 与本地文件访问 ——
|
||||
* 注意 **javaScriptAccess 默认是 true**,不显式关掉等于让远端内容在客户端执行脚本。
|
||||
* 纯文本仍走 Text(无需开销,也不该把文本塞进 Web)。
|
||||
*/
|
||||
@Component
|
||||
export struct ScreensuePage {
|
||||
@ -22,11 +29,16 @@ export struct ScreensuePage {
|
||||
onClose: () => void = () => {
|
||||
};
|
||||
|
||||
/** 本轮内容是不是 HTML(决定走 RichText 还是 Text)。 */
|
||||
/** 内容是不是 HTML(决定走 Web 还是 Text)。 */
|
||||
private htmlMode(): boolean {
|
||||
return looksLikeHtml(this.pushedText);
|
||||
}
|
||||
|
||||
/** HTML 的 base64 载荷(空串表示不是 HTML)。 */
|
||||
private webData(): string {
|
||||
return screensueWebData(this.pushedText, this.isDark);
|
||||
}
|
||||
|
||||
build() {
|
||||
Stack({ alignContent: Alignment.Bottom }) {
|
||||
GradientBackground()
|
||||
@ -55,9 +67,12 @@ export struct ScreensuePage {
|
||||
|
||||
Column() {
|
||||
if (this.htmlMode()) {
|
||||
// HTML:RichText 自己排版,外层用带主题背景的卡片承载。
|
||||
RichText(screensueHtml(this.pushedText, this.isDark))
|
||||
// HTML:整篇交给 Web 渲染(base64 loadData,见 screensueWebData)。
|
||||
// 高度固定 420vp:Web 不参与父级自适应测量,给 height('100%')
|
||||
// 会在 Scroll 里塌成 0。内容区本身可滚。
|
||||
ScreenWebView({ data: this.webData(), isDark: this.isDark })
|
||||
.width('100%')
|
||||
.height(420)
|
||||
} else {
|
||||
Text(this.pushedText)
|
||||
.fontSize(16)
|
||||
@ -117,3 +132,66 @@ export struct ScreensuePage {
|
||||
return this.isDark ? DARK_PALETTE : LIGHT_PALETTE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 承载 screensue HTML 的 Web 视图(独立组件,便于按内容变化重建控制器)。
|
||||
*
|
||||
* 为什么单开一个组件而不是直接在 ScreensuePage 里放 Web:
|
||||
* `WebviewController` 与 Web 组件是一对一绑定的,必须等组件挂载(onControllerAttached)
|
||||
* 才能真正 loadData;把它隔离在这里,ScreensuePage 只管布局与倒计时。
|
||||
*/
|
||||
@Component
|
||||
struct ScreenWebView {
|
||||
// @Watch 挂在这里是必须的:ScreensuePage 在 `if (screensueVisible)` 里常驻,
|
||||
// 第二次 screensue 只会改这个 @Prop 而不会重建组件,而 onControllerAttached
|
||||
// 只在挂载时触发一次 —— 不 watch 就会一直显示上一条推送的内容。
|
||||
// 实测:连推两条不同 HTML,倒计时变了、Web 里还是旧画面。
|
||||
@Prop @Watch('onDataChanged') data: string = '';
|
||||
@Prop isDark: boolean = true;
|
||||
private controller: webview.WebviewController = new webview.WebviewController();
|
||||
/** 控制器是否已与 Web 组件关联(过早 loadData 会抛 17100001)。 */
|
||||
private attached: boolean = false;
|
||||
|
||||
/** data 变化时重新加载(组件不重建,必须显式刷新)。 */
|
||||
onDataChanged(): void {
|
||||
if (this.attached) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Web({ src: '', controller: this.controller })
|
||||
// ★ 内容来自 agent(第三方):显式关闭脚本与本地文件访问。
|
||||
// javaScriptAccess 的默认值是 true,不写这一行等于放任远端内容执行脚本。
|
||||
.javaScriptAccess(false)
|
||||
.fileAccess(false)
|
||||
.domStorageAccess(false)
|
||||
.onlineImageAccess(false)
|
||||
.imageAccess(true) // 保留内联/数据 URI 图片(不联网)
|
||||
.zoomAccess(false) // 禁手势缩放,避免与外层滚动打架
|
||||
.horizontalScrollBarAccess(false)
|
||||
.verticalScrollBarAccess(false)
|
||||
.darkMode(WebDarkMode.Off)
|
||||
.backgroundColor(Color.Transparent)
|
||||
// 控制器挂载完才 loadData:过早调用会抛 17100001(控制器未与组件关联)。
|
||||
.onControllerAttached(() => {
|
||||
// 挂载完成才允许 loadData;此前的变更由 onDataChanged 记着,这里补一次。
|
||||
this.attached = true;
|
||||
this.load();
|
||||
})
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
/** 以 base64 整篇加载(空串直接跳过,避免 Web 显示错误页)。 */
|
||||
private load(): void {
|
||||
if (this.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.controller.loadData(this.data, 'text/html', 'base64');
|
||||
} catch (e) {
|
||||
// 加载失败不该把整页带崩:保持空白,用户仍能看到顶栏与关闭按钮。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,8 @@ import { GradientBackground } from '../components/GradientBackground';
|
||||
import { ScreensuePage } from '../components/ScreensuePage';
|
||||
import { registerScreensueHandler } from '../common/BridgeRouter';
|
||||
import { markForegroundBridgeUIReady } from '../common/DeviceBridgeSession';
|
||||
import { ScreensuePayload, snapshotComponentId } from '../common/BridgeCaps';
|
||||
import { snapshotComponentId } from '../common/BridgeCaps';
|
||||
import { ScreensuePayload } from '../common/ScreensueHtml';
|
||||
import { window, display } from '@kit.ArkUI';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user