接手核对时发现:「不许写死颜色」那条判据**只挡得住枚举的 8 个旧值** —— 判据全绿的 同期,pages/ 里还留着 14 处另一套写死的色(Google/Material:#E8F0FE、#E8F5E9、 #FFF3E0、#D93025、#777777×2、#555555、#444444、#cccccc、#aaaaaa、 遮罩 #80000000×2、透明 #00000000×2)。枚举挡不住漂移,只有"类"能挡。 ## 改法 - 14 处全部换成令牌。新增 accentStrong / warnBg / warnFg(取值对齐 WebUI `:root` 的 blue-700 / amber-50 / amber-700)与 `Theme.permBg/permFg` —— 权限档位徽标与 WebUI 的 `PermissionChip.tsx` **同一映射**(plan=蓝 / workspace=绿 / full=琥珀); 原先写成 `full ? 绿 : 橙`(Material 色),与 WebUI **反着来**。 - 遮罩拆成 `overlayColor` + `overlayAlpha`(照 WebUI 的 `--bg-scrim` + `--bg-dim` 两段式):遮罩色要能随主题换向,色与透明度焊死成一个 `#AARRGGBB` 等于把枚举写回 代码。ArkUI 只认单值,故由 `Theme.overlay()` 组装。 - 判据从"枚举旧值"改成"按类挡":pages/ 下**一个裸色值都不许有**(含 8 位 `#AARRGGBB`),页面清单从硬编码 7 个文件名改成**扫目录** —— 旧写法下, 接下来要加的发件箱/授权/日历会自动逃出判据。另加一条判据:权限档位配色与 WebUI `:root` 变量逐一比对,防"看起来差不多"。 ## 验证 - 变异测试:往 `InboxPage.ets` 塞一个 `#E8F0FE` → 判据红;撤回 → 绿。 - cross-client 判据 6 → 8 条全绿;`hvigorw assembleHap` BUILD SUCCESSFUL。 - **视觉未验**(模拟器在本机文件沙箱下起不来,见 `docs/HARMONY-ALIGN-PLAN.md` 5.4), 未写成"已完成"。
271 lines
9.6 KiB
Plaintext
271 lines
9.6 KiB
Plaintext
/*
|
||
* AgentMail 鸿蒙客户端 — 设置页(账号管理)
|
||
* 多账号列表 / 添加新账号 / 删除 / 切换默认账号
|
||
*/
|
||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||
import { Theme } from '../common/Theme';
|
||
import { AuthApi } from '../api/AuthApi';
|
||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||
import { SseService } from '../api/SseService';
|
||
import { promptAction } from '@kit.ArkUI';
|
||
|
||
@Entry
|
||
@Component
|
||
struct SettingsPage {
|
||
@State accounts: AccountInfo[] = [];
|
||
@State activeId: string = '';
|
||
@State showAddDialog: boolean = false;
|
||
@State newDisplayName: string = '';
|
||
@State newServer: string = '';
|
||
@State newUsername: string = '';
|
||
@State newToken: string = '';
|
||
@State adding: boolean = false;
|
||
|
||
private client: ApiClient | null = null;
|
||
private acctMgr: AccountManager | null = null;
|
||
|
||
aboutToAppear(): void {
|
||
const ctx = this.getUIContext().getHostContext();
|
||
if (ctx !== undefined) {
|
||
this.client = ApiClient.getInstance(ctx);
|
||
this.acctMgr = AccountManager.getInstance(ctx);
|
||
this.acctMgr.load().then(() => {
|
||
this.refreshList();
|
||
});
|
||
}
|
||
}
|
||
|
||
refreshList(): void {
|
||
const manager: AccountManager | null = this.acctMgr;
|
||
if (manager === null) {
|
||
return;
|
||
}
|
||
this.accounts = manager.getAccounts();
|
||
this.activeId = manager.getActiveId();
|
||
}
|
||
|
||
normalizeServer(server: string): string {
|
||
let normalized: string = server.trim();
|
||
while (normalized.length > 0 && normalized.endsWith('/')) {
|
||
normalized = normalized.substring(0, normalized.length - 1);
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
async switchTo(accountId: string): Promise<void> {
|
||
const manager: AccountManager | null = this.acctMgr;
|
||
const client: ApiClient | null = this.client;
|
||
if (manager === null || client === null) {
|
||
return;
|
||
}
|
||
const switched: boolean = await manager.switchAccount(accountId);
|
||
if (!switched) {
|
||
return;
|
||
}
|
||
const account: AccountInfo | null = manager.getActiveAccount();
|
||
if (account !== null) {
|
||
client.setBase(account.server);
|
||
client.setToken(account.token);
|
||
promptAction.showToast({ message: '默认发信账号已设为 ' + account.displayName });
|
||
}
|
||
this.refreshList();
|
||
}
|
||
|
||
async removeAccount(accountId: string): Promise<void> {
|
||
const manager: AccountManager | null = this.acctMgr;
|
||
const client: ApiClient | null = this.client;
|
||
if (manager === null) {
|
||
return;
|
||
}
|
||
const removed: boolean = await manager.removeAccount(accountId);
|
||
if (!removed) {
|
||
return;
|
||
}
|
||
SseService.getInstance().disconnectAccount(accountId);
|
||
const active: AccountInfo | null = manager.getActiveAccount();
|
||
if (client !== null) {
|
||
if (active !== null) {
|
||
client.setBase(active.server);
|
||
client.setToken(active.token);
|
||
} else {
|
||
client.setToken('');
|
||
}
|
||
}
|
||
this.refreshList();
|
||
promptAction.showToast({ message: '账号已删除' });
|
||
}
|
||
|
||
async addNewAccount(): Promise<void> {
|
||
const manager: AccountManager | null = this.acctMgr;
|
||
const ctx = this.getUIContext().getHostContext();
|
||
if (manager === null || ctx === undefined || this.adding) {
|
||
return;
|
||
}
|
||
const displayName: string = this.newDisplayName.trim();
|
||
const server: string = this.normalizeServer(this.newServer);
|
||
const token: string = this.newToken.trim();
|
||
const optionalUsername: string = this.newUsername.trim();
|
||
if (displayName.length === 0 || server.length === 0 || token.length === 0) {
|
||
promptAction.showToast({ message: '请填写显示名称、Gateway 地址和 user_key' });
|
||
return;
|
||
}
|
||
|
||
this.adding = true;
|
||
try {
|
||
const validationClient: ApiClient = new ApiClient(ctx);
|
||
validationClient.setBase(server);
|
||
validationClient.setToken(token);
|
||
const user = await new AuthApi(validationClient).me();
|
||
const username: string = optionalUsername.length > 0 ? optionalUsername : user.username;
|
||
const account: AccountInfo = await manager.addAccount(server, username, token, displayName);
|
||
SseService.getInstance().connectForAccount(account.id, account.server, account.token);
|
||
promptAction.showToast({ message: '✅ 添加成功: ' + account.displayName });
|
||
this.showAddDialog = false;
|
||
this.newDisplayName = '';
|
||
this.newServer = '';
|
||
this.newUsername = '';
|
||
this.newToken = '';
|
||
this.refreshList();
|
||
} catch (e) {
|
||
const apiError = e as ApiError;
|
||
promptAction.showToast({ message: '验证失败: ' + apiError.message });
|
||
} finally {
|
||
this.adding = false;
|
||
}
|
||
}
|
||
|
||
build() {
|
||
Column() {
|
||
Row() {
|
||
Text('‹').fontSize(24).fontColor(Theme.accent).width(40).height(40)
|
||
.textAlign(TextAlign.Center)
|
||
.onClick(() => { this.getUIContext().getRouter().back(); })
|
||
Text('账号管理').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||
.layoutWeight(1)
|
||
Text('+').fontSize(24).fontColor(Theme.accent).width(40).height(40)
|
||
.textAlign(TextAlign.Center)
|
||
.onClick(() => { this.showAddDialog = true; })
|
||
}
|
||
.width('100%').height(56).padding({ left: 8, right: 8 })
|
||
.backgroundColor(Theme.surface)
|
||
|
||
Divider().color(Theme.border)
|
||
|
||
if (this.accounts.length === 0) {
|
||
Column() {
|
||
Text('📭 暂无账号').fontSize(16).fontColor(Theme.textMuted)
|
||
Button('添加第一个账号')
|
||
.margin({ top: 16 }).backgroundColor(Theme.accent)
|
||
.onClick(() => { this.showAddDialog = true; })
|
||
}
|
||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||
} else {
|
||
List({ space: 1 }) {
|
||
ForEach(this.accounts, (account: AccountInfo) => {
|
||
ListItem() {
|
||
this.AccountItem(account)
|
||
}
|
||
.height(72)
|
||
}, (account: AccountInfo) => account.id)
|
||
}
|
||
.width('100%').layoutWeight(1)
|
||
.divider({ strokeWidth: 1, color: Theme.border, startMargin: 16, endMargin: 16 })
|
||
}
|
||
|
||
if (this.showAddDialog) {
|
||
Column() {
|
||
Column()
|
||
.width('100%').layoutWeight(1)
|
||
.backgroundColor(Theme.overlay())
|
||
.onClick(() => { this.showAddDialog = false; })
|
||
|
||
Column() {
|
||
Text('添加新账号').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||
.margin({ bottom: 16 })
|
||
|
||
TextInput({ placeholder: '显示名称(必填)', text: this.newDisplayName })
|
||
.width('100%').height(44).margin({ bottom: 10 })
|
||
.onChange((value: string) => { this.newDisplayName = value; })
|
||
|
||
TextInput({ placeholder: 'Gateway 地址(必填)', text: this.newServer })
|
||
.width('100%').height(44).margin({ bottom: 10 })
|
||
.onChange((value: string) => { this.newServer = value; })
|
||
|
||
TextInput({ placeholder: '用户名(可选)', text: this.newUsername })
|
||
.width('100%').height(44).margin({ bottom: 10 })
|
||
.onChange((value: string) => { this.newUsername = value; })
|
||
|
||
TextInput({ placeholder: 'user_key(必填)', text: this.newToken })
|
||
.width('100%').height(44).margin({ bottom: 16 })
|
||
.type(InputType.Password)
|
||
.onChange((value: string) => { this.newToken = value; })
|
||
|
||
Row() {
|
||
Button('取消')
|
||
.width(80).height(36).backgroundColor(Theme.surfaceMuted).fontColor(Theme.textMuted)
|
||
.onClick(() => { this.showAddDialog = false; })
|
||
Blank()
|
||
Button(this.adding ? '验证中…' : '添加')
|
||
.width(80).height(36).backgroundColor(Theme.accent)
|
||
.enabled(!this.adding)
|
||
.onClick(() => { this.addNewAccount(); })
|
||
}
|
||
.width('100%')
|
||
}
|
||
.width('100%').height('68%')
|
||
.padding(16).backgroundColor(Theme.surface)
|
||
.borderRadius({ topLeft: 12, topRight: 12 })
|
||
}
|
||
.width('100%').height('100%')
|
||
.position({ x: 0, y: 0 })
|
||
}
|
||
}
|
||
.width('100%').height('100%')
|
||
.backgroundColor(Theme.pageBg)
|
||
}
|
||
|
||
@Builder
|
||
AccountItem(account: AccountInfo) {
|
||
Row() {
|
||
Column() {
|
||
Row() {
|
||
Text(account.displayName)
|
||
.fontSize(15).fontWeight(FontWeight.Bold)
|
||
.fontColor(account.id === this.activeId ? Theme.accent : Theme.textPrimary)
|
||
if (account.id === this.activeId) {
|
||
Text(' 默认')
|
||
.fontSize(11).fontColor(Theme.accent)
|
||
.backgroundColor(Theme.accentSoft).borderRadius(4)
|
||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||
.margin({ left: 6 })
|
||
}
|
||
}
|
||
.width('100%')
|
||
|
||
Text((account.username.length > 0 ? account.username + ' · ' : '') + account.server)
|
||
.fontSize(12).fontColor(Theme.textMuted)
|
||
.margin({ top: 3 })
|
||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||
}
|
||
.layoutWeight(1).height('100%')
|
||
.alignItems(HorizontalAlign.Start)
|
||
.justifyContent(FlexAlign.Center)
|
||
|
||
if (account.id !== this.activeId) {
|
||
Text('设为默认')
|
||
.fontSize(12).fontColor(Theme.accent)
|
||
.margin({ right: 12 })
|
||
.onClick(() => { this.switchTo(account.id); })
|
||
}
|
||
|
||
Text('删除')
|
||
.fontSize(12).fontColor(Theme.danger)
|
||
.onClick(() => { this.removeAccount(account.id); })
|
||
}
|
||
.width('100%').height('100%')
|
||
.padding({ left: 16, right: 16 })
|
||
.alignItems(VerticalAlign.Center)
|
||
.backgroundColor(account.id === this.activeId ? Theme.accentSoft : Theme.surface)
|
||
}
|
||
}
|