refactor(harmony): 23 处废弃 API 换成 UIContext 写法(全局 promptAction.showToast 自 API 18 废弃)

SDK 里写得很清楚:`@ohos.promptAction.d.ts` 的全局 `showToast` 标着 `@deprecated since 18`,
替代品是 `UIContext.getPromptAction()`。仓库里有 23 处这种调用(6 个页面,历史遗留)——
这一轮既然在按"用系统方案"整理鸿蒙侧,就一次扫干净,并加判据挡住回潮。

- `promptAction.showToast(...)` → `this.getUIContext().getPromptAction().showToast(...)`(23 处)
- 清掉不再需要的 `promptAction` import(多个 → 只留 `router` 等)
- 新增判据:不得再用全局写法。防的不是这次,而是**新增页面照抄旧代码**这条回退路径 ——
  它编译照样通过、只在真机上行为不同。自检同时验"认得出旧写法"与"不误伤新写法"。
- 顺带把权限徽标那条"点它弹说明"的判据改成钉**非废弃**写法(原来是 `promptAction.showToast(`)。

变异验证:把 `SettingsPage` 的一处改回全局写法 → 判红并点出文件。

验证:`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0(harmony-logic 20 条)。
This commit is contained in:
2026-09-14 14:05:35 +08:00
parent 36f3183bba
commit c6aaf8c468
8 changed files with 71 additions and 27 deletions

View File

@ -20,7 +20,7 @@
*/ */
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
@ -338,7 +338,10 @@ test('徽标真的挂在界面上,且点它能看到那句说明(触屏没
const clickIdx = pageCode.indexOf('.onClick(', chipIdx); const clickIdx = pageCode.indexOf('.onClick(', chipIdx);
assert.ok(chipIdx > 0 && clickIdx > chipIdx && clickIdx - chipIdx < 600, '徽标上要有自己的 onClick'); assert.ok(chipIdx > 0 && clickIdx > chipIdx && clickIdx - chipIdx < 600, '徽标上要有自己的 onClick');
const chipClickBody = onClickBodyOf(pageCode, clickIdx); const chipClickBody = onClickBodyOf(pageCode, clickIdx);
assert.match(chipClickBody, /promptAction\.showToast\(/, '徽标的 onClick 里要弹说明(不是页面别处的 toast'); // 注意用**非废弃**的写法:全局 `promptAction.showToast` 自 API 18 起废弃
// SDK `@ohos.promptAction.d.ts` 的 `@deprecated since 18`
// 要的是 UIContext 上的那个 —— 这条断言顺带把废弃写法挡在门外。
assert.match(chipClickBody, /\.getPromptAction\(\)\.showToast\(/, '徽标的 onClick 里要弹说明UIContext 的非废弃写法)');
assert.match(chipClickBody, /permissionHint\(/, '弹出来的必须是那句说明'); assert.match(chipClickBody, /permissionHint\(/, '弹出来的必须是那句说明');
assert.match(pageCode, /enforcementLabel\(c\.permission_enforcement\)/, '说明里要带强制力标签'); assert.match(pageCode, /enforcementLabel\(c\.permission_enforcement\)/, '说明里要带强制力标签');
assert.match(pageCode, /permissionLabel\(mail\.permission_mode\)/, '收件箱行要用中文档位'); assert.match(pageCode, /permissionLabel\(mail\.permission_mode\)/, '收件箱行要用中文档位');
@ -349,3 +352,35 @@ test('徽标真的挂在界面上,且点它能看到那句说明(触屏没
'收件箱每封邮件里没有 permission_enforcement画强制力标记等于编一个"平台做到了什么"' '收件箱每封邮件里没有 permission_enforcement画强制力标记等于编一个"平台做到了什么"'
); );
}); });
test('★ 不得再用废弃的全局 promptAction.showToastAPI 18 起废弃,要走 UIContext', () => {
/*
* SDK 里写得很清楚:`@ohos.promptAction.d.ts` 的全局 `showToast` 标着
* `@deprecated since 18`,替代品是 `UIContext.getPromptAction()`。
* 本仓库原先有 **23 处**这种调用(不是我写的,是历史)—— 既然这一轮在按
* "用系统方案"整理鸿蒙侧,就顺手一次扫干净,并用判据挡住回潮:
* 新增页面照抄旧写法是最常见的回退路径,而它**编译照样通过**。
*/
const dir = join(ROOT, 'client/harmony/entry/src/main/ets');
const files = [];
const walk = d => {
for (const e of readdirSync(d, { withFileTypes: true })) {
const full = join(d, e.name);
if (e.isDirectory()) walk(full);
else if (/\.(ets|ts)$/.test(e.name)) files.push(full);
}
};
walk(dir);
assert.ok(files.length >= 10, `应扫到至少 10 个源文件,实际 ${files.length}`);
const bad = [];
for (const f of files) {
const src = readFileSync(f, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
// `getPromptAction().showToast(` 不算违规:它前面必须有 `get`
for (const m of src.matchAll(/(?<!get)promptAction\.showToast\(/g)) bad.push(f.slice(ROOT.length + 1));
}
assert.deepEqual(bad, [], `这些文件还在用废弃的全局 promptAction.showToast${bad.join('、')}`);
// 反向对照:自检正则要真能认出旧写法、且不误伤新写法
assert.ok(/(?<!get)promptAction\.showToast\(/.test('promptAction.showToast({ message: 1 })'), '自检:认不出旧写法');
assert.ok(!/(?<!get)promptAction\.showToast\(/.test('this.getUIContext().getPromptAction().showToast({})'), '自检:误伤了新写法');
});

View File

@ -9,7 +9,6 @@ import { MailApi } from '../api/MailApi';
import { AccountManager, AccountInfo } from '../api/AccountManager'; import { AccountManager, AccountInfo } from '../api/AccountManager';
import { SendMailRequest } from '../model/Models'; import { SendMailRequest } from '../model/Models';
import { ComposeParams } from '../model/RouteParams'; import { ComposeParams } from '../model/RouteParams';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit'; import { hilog } from '@kit.PerformanceAnalysisKit';
import { picker } from '@kit.CoreFileKit'; import { picker } from '@kit.CoreFileKit';
@ -92,7 +91,7 @@ struct ComposePage {
const mailApi: MailApi | null = this.createSelectedMailApi(ctx); const mailApi: MailApi | null = this.createSelectedMailApi(ctx);
if (mailApi === null) { if (mailApi === null) {
promptAction.showToast({ message: '请先选择发信账号' }); this.getUIContext().getPromptAction().showToast({ message: '请先选择发信账号' });
return; return;
} }
this.uploading = true; this.uploading = true;
@ -108,7 +107,7 @@ struct ComposePage {
hilog.info(0x0001, 'Compose', 'uploaded: %{public}s → %{public}s', name, attachmentId); hilog.info(0x0001, 'Compose', 'uploaded: %{public}s → %{public}s', name, attachmentId);
} catch (e) { } catch (e) {
const apiError = e as ApiError; const apiError = e as ApiError;
promptAction.showToast({ message: '上传失败: ' + name + ' - ' + apiError.message }); this.getUIContext().getPromptAction().showToast({ message: '上传失败: ' + name + ' - ' + apiError.message });
} }
} }
this.status = this.attachmentIds.length + ' 个附件已上传'; this.status = this.attachmentIds.length + ' 个附件已上传';
@ -133,11 +132,11 @@ struct ComposePage {
return; return;
} }
if (this.to.length === 0) { if (this.to.length === 0) {
promptAction.showToast({ message: '请填写收件人' }); this.getUIContext().getPromptAction().showToast({ message: '请填写收件人' });
return; return;
} }
if (this.subject.length === 0) { if (this.subject.length === 0) {
promptAction.showToast({ message: '请填写主题' }); this.getUIContext().getPromptAction().showToast({ message: '请填写主题' });
return; return;
} }
const ctx: Context | undefined = this.getUIContext().getHostContext(); const ctx: Context | undefined = this.getUIContext().getHostContext();
@ -146,7 +145,7 @@ struct ComposePage {
} }
const mailApi: MailApi | null = this.createSelectedMailApi(ctx); const mailApi: MailApi | null = this.createSelectedMailApi(ctx);
if (mailApi === null) { if (mailApi === null) {
promptAction.showToast({ message: '请先选择发信账号' }); this.getUIContext().getPromptAction().showToast({ message: '请先选择发信账号' });
return; return;
} }
@ -166,11 +165,11 @@ struct ComposePage {
request.attachment_ids = this.attachmentIds; request.attachment_ids = this.attachmentIds;
} }
await mailApi.send(request); await mailApi.send(request);
promptAction.showToast({ message: '✅ 邮件已发送' }); this.getUIContext().getPromptAction().showToast({ message: '✅ 邮件已发送' });
this.getUIContext().getRouter().back(); this.getUIContext().getRouter().back();
} catch (e) { } catch (e) {
const apiError = e as ApiError; const apiError = e as ApiError;
promptAction.showToast({ message: '发送失败: ' + apiError.message }); this.getUIContext().getPromptAction().showToast({ message: '发送失败: ' + apiError.message });
} finally { } finally {
this.sending = false; this.sending = false;
} }

View File

@ -6,7 +6,7 @@ import { ApiClient, ApiError } from '../api/ApiClient';
import { Theme } from '../common/Theme'; import { Theme } from '../common/Theme';
import { MailApi } from '../api/MailApi'; import { MailApi } from '../api/MailApi';
import { MailSummary, Me } from '../model/Models'; import { MailSummary, Me } from '../model/Models';
import { promptAction, router } from '@kit.ArkUI'; import { router } from '@kit.ArkUI';
@Entry @Entry
@Component @Component

View File

@ -10,7 +10,6 @@ import { AccountManager } from '../api/AccountManager';
import { SseService } from '../api/SseService'; import { SseService } from '../api/SseService';
import { Me } from '../model/Models'; import { Me } from '../model/Models';
import { DEFAULT_API_BASE, EMULATOR_HOST_BASE } from '../common/Config'; import { DEFAULT_API_BASE, EMULATOR_HOST_BASE } from '../common/Config';
import { promptAction } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit'; import { hilog } from '@kit.PerformanceAnalysisKit';
@Entry @Entry
@ -125,7 +124,7 @@ struct LoginPage {
const ae = e as ApiError; const ae = e as ApiError;
const msg: string = ae.code === 0 ? ae.message : (ae.message.length > 0 ? ae.message : '登录失败'); const msg: string = ae.code === 0 ? ae.message : (ae.message.length > 0 ? ae.message : '登录失败');
hilog.error(0x0001, 'LoginPage', 'login failed: code=%{public}d msg=%{public}s', ae.code, msg); hilog.error(0x0001, 'LoginPage', 'login failed: code=%{public}d msg=%{public}s', ae.code, msg);
promptAction.showToast({ message: msg }); this.getUIContext().getPromptAction().showToast({ message: msg });
} finally { } finally {
this.loading = false; this.loading = false;
} }

View File

@ -9,7 +9,6 @@ import { MailApi } from '../api/MailApi';
import { AccountManager, AccountInfo } from '../api/AccountManager'; import { AccountManager, AccountInfo } from '../api/AccountManager';
import { MailDetail, SendMailRequest } from '../model/Models'; import { MailDetail, SendMailRequest } from '../model/Models';
import { MailDetailParams } from '../model/RouteParams'; import { MailDetailParams } from '../model/RouteParams';
import { promptAction } from '@kit.ArkUI';
@Entry @Entry
@Component @Component
@ -103,10 +102,10 @@ struct MailDetailPage {
try { try {
await m.setPermissionMode(sid, mode); await m.setPermissionMode(sid, mode);
this.permissionMode = mode; this.permissionMode = mode;
promptAction.showToast({ message: '权限已切换为 ' + mode }); this.getUIContext().getPromptAction().showToast({ message: '权限已切换为 ' + mode });
} catch (e) { } catch (e) {
const ae = e as ApiError; const ae = e as ApiError;
promptAction.showToast({ message: '切换失败: ' + ae.message }); this.getUIContext().getPromptAction().showToast({ message: '切换失败: ' + ae.message });
} finally { } finally {
this.switchingPerm = false; this.switchingPerm = false;
} }
@ -306,10 +305,10 @@ struct MailDetailPage {
await m.send(req); await m.send(req);
this.showReplyBox = false; this.showReplyBox = false;
this.replyBody = ''; this.replyBody = '';
promptAction.showToast({ message: '回复已发送' }); this.getUIContext().getPromptAction().showToast({ message: '回复已发送' });
} catch (e) { } catch (e) {
const ae = e as ApiError; const ae = e as ApiError;
promptAction.showToast({ message: '发送失败: ' + ae.message }); this.getUIContext().getPromptAction().showToast({ message: '发送失败: ' + ae.message });
} finally { } finally {
this.sending = false; this.sending = false;
} }

View File

@ -26,7 +26,6 @@ import {
enforcementLabel enforcementLabel
} from '../model/MailGrouping'; } from '../model/MailGrouping';
import { MailDetailParams, ComposeParams } from '../model/RouteParams'; import { MailDetailParams, ComposeParams } from '../model/RouteParams';
import { promptAction } from '@kit.ArkUI';
/** 一页取多少封。取满了就要如实提示"可能还有更多"(服务端 total 是未读数,不是总封数)。 */ /** 一页取多少封。取满了就要如实提示"可能还有更多"(服务端 total 是未读数,不是总封数)。 */
const INBOX_PAGE_SIZE: number = 50; const INBOX_PAGE_SIZE: number = 50;
@ -89,7 +88,7 @@ struct InboxTab {
break; break;
} }
} }
promptAction.showToast({ message: sourceName.length > 0 ? '📨 ' + sourceName + ' 收到新邮件' : '📨 新邮件到达' }); this.getUIContext().getPromptAction().showToast({ message: sourceName.length > 0 ? '📨 ' + sourceName + ' 收到新邮件' : '📨 新邮件到达' });
this.loadData(); this.loadData();
} }
}; };
@ -737,7 +736,7 @@ struct ContactsTab {
.padding({ left: 5, right: 5, top: 1, bottom: 1 }) .padding({ left: 5, right: 5, top: 1, bottom: 1 })
.margin({ right: 6 }) .margin({ right: 6 })
.onClick(() => { .onClick(() => {
promptAction.showToast({ this.getUIContext().getPromptAction().showToast({
message: permissionHint(c.permission_mode, c.permission_enforcement) message: permissionHint(c.permission_mode, c.permission_enforcement)
+ '(强制力:' + enforcementLabel(c.permission_enforcement) + '', + '(强制力:' + enforcementLabel(c.permission_enforcement) + '',
duration: 6000 duration: 6000

View File

@ -7,7 +7,6 @@ import { Theme } from '../common/Theme';
import { AuthApi } from '../api/AuthApi'; import { AuthApi } from '../api/AuthApi';
import { AccountManager, AccountInfo } from '../api/AccountManager'; import { AccountManager, AccountInfo } from '../api/AccountManager';
import { SseService } from '../api/SseService'; import { SseService } from '../api/SseService';
import { promptAction } from '@kit.ArkUI';
@Entry @Entry
@Component @Component
@ -66,7 +65,7 @@ struct SettingsPage {
if (account !== null) { if (account !== null) {
client.setBase(account.server); client.setBase(account.server);
client.setToken(account.token); client.setToken(account.token);
promptAction.showToast({ message: '默认发信账号已设为 ' + account.displayName }); this.getUIContext().getPromptAction().showToast({ message: '默认发信账号已设为 ' + account.displayName });
} }
this.refreshList(); this.refreshList();
} }
@ -92,7 +91,7 @@ struct SettingsPage {
} }
} }
this.refreshList(); this.refreshList();
promptAction.showToast({ message: '账号已删除' }); this.getUIContext().getPromptAction().showToast({ message: '账号已删除' });
} }
async addNewAccount(): Promise<void> { async addNewAccount(): Promise<void> {
@ -106,7 +105,7 @@ struct SettingsPage {
const token: string = this.newToken.trim(); const token: string = this.newToken.trim();
const optionalUsername: string = this.newUsername.trim(); const optionalUsername: string = this.newUsername.trim();
if (displayName.length === 0 || server.length === 0 || token.length === 0) { if (displayName.length === 0 || server.length === 0 || token.length === 0) {
promptAction.showToast({ message: '请填写显示名称、Gateway 地址和 user_key' }); this.getUIContext().getPromptAction().showToast({ message: '请填写显示名称、Gateway 地址和 user_key' });
return; return;
} }
@ -119,7 +118,7 @@ struct SettingsPage {
const username: string = optionalUsername.length > 0 ? optionalUsername : user.username; const username: string = optionalUsername.length > 0 ? optionalUsername : user.username;
const account: AccountInfo = await manager.addAccount(server, username, token, displayName); const account: AccountInfo = await manager.addAccount(server, username, token, displayName);
SseService.getInstance().connectForAccount(account.id, account.server, account.token); SseService.getInstance().connectForAccount(account.id, account.server, account.token);
promptAction.showToast({ message: '✅ 添加成功: ' + account.displayName }); this.getUIContext().getPromptAction().showToast({ message: '✅ 添加成功: ' + account.displayName });
this.showAddDialog = false; this.showAddDialog = false;
this.newDisplayName = ''; this.newDisplayName = '';
this.newServer = ''; this.newServer = '';
@ -128,7 +127,7 @@ struct SettingsPage {
this.refreshList(); this.refreshList();
} catch (e) { } catch (e) {
const apiError = e as ApiError; const apiError = e as ApiError;
promptAction.showToast({ message: '验证失败: ' + apiError.message }); this.getUIContext().getPromptAction().showToast({ message: '验证失败: ' + apiError.message });
} finally { } finally {
this.adding = false; this.adding = false;
} }

View File

@ -543,3 +543,17 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排
深色模式下的观感都只有真机才看得到模拟器需人在命令行启动)。已验证的是 深色模式下的观感都只有真机才看得到模拟器需人在命令行启动)。已验证的是
`hvigorw assembleHap` BUILD SUCCESSFUL13 条跨端判据 + 4 条系统资源名判据全绿 `hvigorw assembleHap` BUILD SUCCESSFUL13 条跨端判据 + 4 条系统资源名判据全绿
四种变异都能判红`sys.*` 名字全部对着 SDK 名表核过判据持续盯着)。 四种变异都能判红`sys.*` 名字全部对着 SDK 名表核过判据持续盯着)。
### 7.14 顺手扫掉 23 处废弃 API全局 `promptAction.showToast`
SDK 里写着`@ohos.promptAction.d.ts` 的全局 `showToast` `@deprecated since 18`
替代品是 `UIContext.getPromptAction()`仓库里原有 **23 **这种调用历史遗留6 个页面)——
既然这一轮在按"用系统方案"整理鸿蒙侧就一次扫干净
`promptAction.showToast(...)` `this.getUIContext().getPromptAction().showToast(...)`
并清掉不再需要的 import
**判据**不得再用全局写法负向断言自检里同时验"认得出旧写法""不误伤新写法")。
这条防的不是这次而是**新增页面照抄旧代码**这条最常见的回退路径 —— 它编译照样通过
同一批整理里还没做的全局 `animateTo` 也要走 `getUIContext().animateTo(...)`
放到动效那一期一起改 —— 那一期本来就是"自定义 transition 系统 `curves`"。)