ke: 鸿蒙 UI 整改:emoji→AmIcon 图标、宽屏侧栏、Md 第三方库、通知可配置项
- common/Icons.ets(生成器产出):43 枚与 WebUI icons.tsx 同几何的 Path 图标 - NavItems.ts:icon(emoji) → iconKey,与 WebUI 导航同图标 - 全页面 emoji 清除(空态改单行灰字,按钮/头像/刷新等换 AmIcon) - MailDetailPage 接入 @luvi/lv-markdown-in(第三方,替代手写解析器) - WideSidebar + MainPage isWide(≥768vp):宽屏侧栏 60vp 图标轨、藏底部条、onAreaChange 断点 - 通知=可配置项(默认关):PushService.isEnabled/setEnabled + reportToken 最前 gate (关=不取token/不弹权限/不打网关)+ SettingsPage Toggle + 服务端通道状态提示 - EntryAbility.onCreate 解析 want:冷启点通知也能路由(原来只有 onNewWant) - 判据:harmony-widescreen(4) + harmony-push-optin(4) 新增;harmony-nav 同步 iconKey/条件 padding
This commit is contained in:
@ -33,6 +33,7 @@ import {
|
||||
AdminUpdateUserInput
|
||||
} from '../model/Models';
|
||||
import { toggled, lastLoginLabel, messageOfApiError, isAdminRole, isRestricted } from '../model/AdminUsers';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -267,8 +268,11 @@ struct AdminUsersPage {
|
||||
this.Header()
|
||||
|
||||
if (this.roleKnown && !this.isAdmin) {
|
||||
Column() {
|
||||
Text('🔒 管理入口仅管理员可见').fontSize(Theme.fontBody).fontColor(Theme.textMuted)
|
||||
Column() {
|
||||
Row() {
|
||||
AmIcon({ iconName: 'lock', iconSize: 16, iconColor: Theme.textMuted })
|
||||
Text('管理入口仅管理员可见').fontSize(Theme.fontBody).fontColor(Theme.textMuted).margin({ left: 6 })
|
||||
}
|
||||
Text('当前账号的角色不是管理员。')
|
||||
.fontSize(Theme.fontSmall).fontColor(Theme.textSubtle).margin({ top: 6 })
|
||||
}
|
||||
@ -326,8 +330,7 @@ struct AdminUsersPage {
|
||||
Text('管理').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
.layoutWeight(1)
|
||||
// 刷新:管理页的动作个个改服务端状态,看不到最新值会让人怀疑自己刚才点没点上
|
||||
Text('↻').fontSize(20).fontColor(Theme.accent).width(40).height(40)
|
||||
.textAlign(TextAlign.Center)
|
||||
AmIcon({ iconName: 'repeat', iconSize: 20, iconColor: Theme.accent }).width(40).height(40)
|
||||
.onClick(() => { this.load(); })
|
||||
}
|
||||
.width('100%').height(56).padding({ left: 8, right: 8 })
|
||||
|
||||
@ -11,6 +11,7 @@ import { SendMailRequest } from '../model/Models';
|
||||
import { ComposeParams } from '../model/RouteParams';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { picker } from '@kit.CoreFileKit';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -165,7 +166,7 @@ struct ComposePage {
|
||||
request.attachment_ids = this.attachmentIds;
|
||||
}
|
||||
await mailApi.send(request);
|
||||
this.getUIContext().getPromptAction().showToast({ message: '✅ 邮件已发送' });
|
||||
this.getUIContext().getPromptAction().showToast({ message: '邮件已发送' });
|
||||
this.getUIContext().getRouter().back();
|
||||
} catch (e) {
|
||||
const apiError = e as ApiError;
|
||||
@ -228,7 +229,7 @@ struct ComposePage {
|
||||
.fontWeight(this.selectedAccountId === acct.id ? FontWeight.Bold : FontWeight.Normal)
|
||||
.layoutWeight(1)
|
||||
if (this.selectedAccountId === acct.id) {
|
||||
Text('✓').fontSize(14).fontColor(Theme.accent)
|
||||
AmIcon({ iconName: 'check', iconSize: 14, iconColor: Theme.accent })
|
||||
}
|
||||
}
|
||||
.width('100%').height(40).padding({ left: 40, right: 12 })
|
||||
@ -279,12 +280,13 @@ struct ComposePage {
|
||||
Column() {
|
||||
ForEach(this.attachmentNames, (name: string, idx: number) => {
|
||||
Row() {
|
||||
Text('📎 ' + name)
|
||||
AmIcon({ iconName: 'paperclip', iconSize: 12, iconColor: Theme.textMuted })
|
||||
Text(name)
|
||||
.fontSize(12).fontColor(Theme.textPrimary)
|
||||
.margin({ left: 6 })
|
||||
.layoutWeight(1)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text('✕')
|
||||
.fontSize(14).fontColor(Theme.danger)
|
||||
AmIcon({ iconName: 'close', iconSize: 14, iconColor: Theme.danger })
|
||||
.onClick(() => { this.removeAttachment(idx); })
|
||||
}
|
||||
.width('100%').height(32)
|
||||
@ -297,9 +299,12 @@ struct ComposePage {
|
||||
|
||||
// 底部工具栏
|
||||
Row() {
|
||||
Text('📎 添加附件')
|
||||
.fontSize(13).fontColor(Theme.accent)
|
||||
.onClick(() => { this.pickAttachment(); })
|
||||
Row() {
|
||||
AmIcon({ iconName: 'paperclip', iconSize: 14, iconColor: Theme.accent })
|
||||
Text('添加附件')
|
||||
.fontSize(13).fontColor(Theme.accent).margin({ left: 6 })
|
||||
.onClick(() => { this.pickAttachment(); })
|
||||
}
|
||||
|
||||
if (this.uploading) {
|
||||
Text(this.status)
|
||||
|
||||
@ -7,6 +7,7 @@ import { Theme } from '../common/Theme';
|
||||
import { MailApi } from '../api/MailApi';
|
||||
import { MailSummary, Me } from '../model/Models';
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -97,7 +98,7 @@ struct InboxPage {
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else if (this.mails.length === 0) {
|
||||
Column() {
|
||||
Text('📭 收件箱为空').fontSize(16).fontColor(Theme.textSubtle)
|
||||
Text('收件箱为空').fontSize(16).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').height('80%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
@ -126,7 +127,7 @@ struct InboxPage {
|
||||
Column() {
|
||||
Row() {
|
||||
Column() {
|
||||
Text('📬').fontSize(20)
|
||||
AmIcon({ iconName: 'inbox', iconSize: 20, iconColor: Theme.accent })
|
||||
Text('收件箱').fontSize(11).fontColor(Theme.accent)
|
||||
}
|
||||
.layoutWeight(1).height('100%')
|
||||
@ -134,7 +135,7 @@ struct InboxPage {
|
||||
.backgroundColor(Theme.accentSoft)
|
||||
|
||||
Column() {
|
||||
Text('💬').fontSize(20)
|
||||
AmIcon({ iconName: 'chatBubble', iconSize: 20, iconColor: Theme.textMuted })
|
||||
Text('会话').fontSize(11).fontColor(Theme.textMuted)
|
||||
}
|
||||
.layoutWeight(1).height('100%')
|
||||
@ -144,7 +145,7 @@ struct InboxPage {
|
||||
})
|
||||
|
||||
Column() {
|
||||
Text('👤').fontSize(20)
|
||||
AmIcon({ iconName: 'contacts', iconSize: 20, iconColor: Theme.textMuted })
|
||||
Text('联系人').fontSize(11).fontColor(Theme.textMuted)
|
||||
}
|
||||
.layoutWeight(1).height('100%')
|
||||
@ -193,7 +194,7 @@ struct InboxPage {
|
||||
|
||||
// 附件图标
|
||||
if (mail.has_attachments) {
|
||||
Text('📎').fontSize(14).margin({ right: 8 })
|
||||
AmIcon({ iconName: 'paperclip', iconSize: 14, iconColor: Theme.textMuted }).margin({ right: 8 })
|
||||
}
|
||||
|
||||
// 权限档位徽标
|
||||
|
||||
@ -187,7 +187,7 @@ struct LoginPage {
|
||||
})
|
||||
|
||||
if (this.addrWarning.length > 0) {
|
||||
Text('⚠ ' + this.addrWarning)
|
||||
Text(this.addrWarning)
|
||||
.fontSize(11).fontColor(Theme.warnFg)
|
||||
.width('85%').margin({ bottom: 12 })
|
||||
}
|
||||
|
||||
@ -5,10 +5,13 @@
|
||||
*/
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
/* Markdown 渲染(第三方库,鸿蒙原生 ArkTS 引擎,不依赖 WebView)—— 正文用它,不再吐原始 Markdown */
|
||||
import { Markdown } from '@luvi/lv-markdown-in';
|
||||
import { MailApi } from '../api/MailApi';
|
||||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||||
import { MailDetail, SendMailRequest } from '../model/Models';
|
||||
import { MailDetailParams } from '../model/RouteParams';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -207,12 +210,23 @@ struct MailDetailPage {
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
|
||||
|
||||
Text(this.body)
|
||||
.fontSize(15)
|
||||
.fontColor(Theme.textPrimary)
|
||||
/*
|
||||
* 邮件正文:**用 Markdown 渲染**,不再直接吐原文。
|
||||
*
|
||||
* 2026-09-15 之前这里是 `Text(this.body)` —— 正文是 Markdown,
|
||||
* 于是用户看到的是 `**加粗**`、`# 标题`、`| 表 |` 的**字面量**。
|
||||
* WebUI 侧一直用 `react-markdown` + `remark-gfm` 渲染
|
||||
*(`MailView.tsx` 的 `.markdown` 容器),这是两端**功能不对等**的一处硬缺口。
|
||||
*
|
||||
* 用第三方库 `@luvi/lv-markdown-in`(鸿蒙原生 ArkTS 渲染引擎,
|
||||
* **不依赖 WebView**):解析与渲染全在原生层,`ohpm install` 一行接入。
|
||||
* 我一度自己写了一个 GFM 子集解析器 —— 随后按用户指示换成第三方库:
|
||||
* 自研那份只覆盖子集、还要自己养判据,而库里有 LaTeX / Prism 高亮 /
|
||||
* 大文本懒渲染 / 70+ 样式 API,都是自研要重做的。
|
||||
*/
|
||||
Markdown({ text: this.body })
|
||||
.width('100%')
|
||||
.padding({ left: 16, right: 16, bottom: 16 })
|
||||
.lineHeight(24)
|
||||
|
||||
Blank().height(60)
|
||||
}
|
||||
@ -223,7 +237,12 @@ struct MailDetailPage {
|
||||
|
||||
// 底部回复按钮
|
||||
Row() {
|
||||
Button('↩ 回复')
|
||||
Button() {
|
||||
Row() {
|
||||
AmIcon({ iconName: 'forward', iconSize: 16, iconColor: Theme.accentFg })
|
||||
Text('回复').fontSize(15).fontColor(Theme.accentFg).margin({ left: 6 })
|
||||
}
|
||||
}
|
||||
.width('90%').height(44)
|
||||
.backgroundColor(Theme.accent)
|
||||
.fontSize(15)
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
*/
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { WideSidebar } from './WideSidebar';
|
||||
import { MailApi, InboxResponse } from '../api/MailApi';
|
||||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||||
import { SseService, SseEvent } from '../api/SseService';
|
||||
@ -140,7 +142,7 @@ struct InboxTab {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.getUIContext().getPromptAction().showToast({ message: sourceName.length > 0 ? '📨 ' + sourceName + ' 收到新邮件' : '📨 新邮件到达' });
|
||||
this.getUIContext().getPromptAction().showToast({ message: sourceName.length > 0 ? '新邮件:' + sourceName : '新邮件到达' });
|
||||
this.loadData();
|
||||
}
|
||||
};
|
||||
@ -382,7 +384,7 @@ struct InboxTab {
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.mails.length === 0) {
|
||||
Column() {
|
||||
Text('📭 收件箱为空').fontSize(16).fontColor(Theme.textSubtle)
|
||||
Text('收件箱为空').fontSize(16).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else {
|
||||
@ -762,7 +764,7 @@ struct SentTab {
|
||||
Column() { LoadingProgress().width(32).height(32) }
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.error.length > 0) {
|
||||
Column() { Text('⚠ ' + this.error).fontSize(13).fontColor(Theme.danger) }
|
||||
Column() { Text(this.error).fontSize(13).fontColor(Theme.danger) }
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.groups.length === 0) {
|
||||
/*
|
||||
@ -771,7 +773,7 @@ struct SentTab {
|
||||
* 副句说清这一栏是干什么的。
|
||||
*/
|
||||
Column() {
|
||||
Text('📤').fontSize(32).margin({ bottom: 8 })
|
||||
AmIcon({ iconName: 'sent', iconSize: 36, iconColor: Theme.textSubtle }).margin({ bottom: 8 })
|
||||
Text(emptyTitle('sent')).fontSize(15).fontColor(Theme.textMuted)
|
||||
Text(emptyHint('sent')).fontSize(12).fontColor(Theme.textSubtle).margin({ top: 6 })
|
||||
}
|
||||
@ -1020,11 +1022,11 @@ struct PermissionTab {
|
||||
Column() { LoadingProgress().width(32).height(32) }
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.error.length > 0) {
|
||||
Column() { Text('⚠ ' + this.error).fontSize(13).fontColor(Theme.danger) }
|
||||
Column() { Text(this.error).fontSize(13).fontColor(Theme.danger) }
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.requests.length === 0) {
|
||||
Column() {
|
||||
Text('✅').fontSize(32).margin({ bottom: 8 })
|
||||
AmIcon({ iconName: 'shield', iconSize: 36, iconColor: Theme.textSubtle }).margin({ bottom: 8 })
|
||||
Text(emptyTitle('permissions')).fontSize(15).fontColor(Theme.textMuted)
|
||||
Text(emptyHint('permissions')).fontSize(12).fontColor(Theme.textSubtle).margin({ top: 6 })
|
||||
}
|
||||
@ -1189,12 +1191,17 @@ struct CommPage {
|
||||
Row() {
|
||||
Text('通信').fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
Blank()
|
||||
Text('⚙')
|
||||
.fontSize(20).fontColor(Theme.textMuted)
|
||||
.width(36).height(36).textAlign(TextAlign.Center)
|
||||
.onClick(() => {
|
||||
this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' });
|
||||
})
|
||||
/*
|
||||
* 设置入口。
|
||||
* 图标用 `person`:与 WebUI 的「我的」(`NarrowNav.tsx` 的 `PersonIcon`)同一枚,
|
||||
* 而设置页里正是账号 / 外观 / 密钥这些“属于这个人的东西”。
|
||||
* 2026-09-15 之前这里是 emoji `⚙`(理由与替换过程见 `model/NavItems.ts`)。
|
||||
*/
|
||||
AmIcon({ iconName: 'person', iconSize: 20, iconColor: Theme.textMuted })
|
||||
.width(36).height(36)
|
||||
.onClick(() => {
|
||||
this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' });
|
||||
})
|
||||
}
|
||||
.width('100%').height(52).padding({ left: 16, right: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
@ -1282,9 +1289,12 @@ struct ContactsTab {
|
||||
Row() {
|
||||
Text(contactViewTitle(this.contactView)).fontSize(20).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
Blank()
|
||||
Text(this.contactView === 'list' ? '▦' : '☰')
|
||||
.fontSize(18).fontColor(Theme.textMuted)
|
||||
.width(40).height(40).textAlign(TextAlign.Center)
|
||||
AmIcon({
|
||||
iconName: this.contactView === 'list' ? 'cardView' : 'listView',
|
||||
iconSize: 18,
|
||||
iconColor: Theme.textMuted
|
||||
})
|
||||
.width(40).height(40)
|
||||
.onClick(() => { this.switchView(); })
|
||||
}
|
||||
.width('100%').height(56).padding({ left: 16, right: 8 })
|
||||
@ -1303,7 +1313,7 @@ struct ContactsTab {
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.contacts.length === 0) {
|
||||
Column() {
|
||||
Text('📭 暂无联系人').fontSize(16).fontColor(Theme.textSubtle)
|
||||
Text('暂无联系人').fontSize(16).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
|
||||
} else if (this.contactView === 'card') {
|
||||
@ -1384,8 +1394,12 @@ struct ContactsTab {
|
||||
|
||||
if (c.last_preview.length > 0) {
|
||||
Row() {
|
||||
Text(lastFromIsHuman(c.agent_name, c.last_from) ? '👤' : '🤖')
|
||||
.fontSize(11).margin({ right: 4 })
|
||||
AmIcon({
|
||||
iconName: lastFromIsHuman(c.agent_name, c.last_from) ? 'person' : 'bot',
|
||||
iconSize: 12,
|
||||
iconColor: Theme.textMuted
|
||||
})
|
||||
.margin({ right: 4 })
|
||||
Text(c.last_preview)
|
||||
.fontSize(11).fontColor(Theme.textMuted)
|
||||
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
@ -1488,6 +1502,8 @@ struct ContactsTab {
|
||||
@Component
|
||||
struct MainPage {
|
||||
@State currentIndex: number = 0;
|
||||
/** 宽屏模式(≥768vp):复刻 WebUI 的三栏布局(Sidebar | 内容,无底部导航条) */
|
||||
@State isWide: boolean = false;
|
||||
/*
|
||||
* 壁纸层(背景的**画法**在 `model/Wallpaper.ts` 里算好,这里只负责画)。
|
||||
*
|
||||
@ -1737,9 +1753,11 @@ struct MainPage {
|
||||
* 缺的是**图标这一半**:原来只有 label 上了色,图标一直是默认色,
|
||||
* 看上去像「选中了一半」。两处都过同一个三元式,避免以后只改一处。
|
||||
*/
|
||||
Text(item.icon)
|
||||
.fontSize(20)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
AmIcon({
|
||||
iconName: item.iconKey,
|
||||
iconSize: 22,
|
||||
iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg
|
||||
})
|
||||
Text(item.label)
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
@ -1849,39 +1867,56 @@ struct MainPage {
|
||||
* 内容按 `currentIndex` 挂载(原来这里是系统 `Tabs` 的 TabContent)。
|
||||
* 底部**必须让出** NAV_CONTENT_RESERVE 的高度:条是浮在内容之上的,
|
||||
* 不让出这一段,列表最后一行就永远压在玻璃条底下 —— 看得见、点不到。
|
||||
* 宽屏没有底部导航条,所以 padding 为 0。
|
||||
*/
|
||||
Column() {
|
||||
if (this.currentIndex === 0) {
|
||||
CommPage({ bgActive: this.bgActive })
|
||||
} else if (this.currentIndex === 2) {
|
||||
ContactsTab({ bgActive: this.bgActive })
|
||||
Row() {
|
||||
if (this.isWide) {
|
||||
WideSidebar({
|
||||
currentIndex: this.currentIndex,
|
||||
onSelect: (index: number) => { this.currentIndex = normalizeNavIndex(index); },
|
||||
onSettings: () => { this.getUIContext().getRouter().pushUrl({ url: 'pages/SettingsPage' }); }
|
||||
})
|
||||
}
|
||||
/*
|
||||
* 日历与另外两个 pane 不同:**常驻挂载**,用 `visibility` 控制显示。
|
||||
*
|
||||
* 理由两条,都不是美观问题:
|
||||
* ① `today` 是日历里**唯一随时间变**的输入,而日历是"放着不动的 pane"。
|
||||
* 卸载重挂会重算(`aboutToAppear`),但"一直开着跨过午夜"不会 ——
|
||||
* 常驻 + `@Watch(visible)` 才能在它重新可见时重算
|
||||
* (`docs/DEBTS.json` 的 `calendar-today-recompute`)。
|
||||
* ② 常驻顺带保住"正在看哪个月",切页签回来不会被重置回本月。
|
||||
* 常驻 ≠ 常拉:首次**可见**时才发请求(见 `CalendarPage.onVisibleChanged`)。
|
||||
*/
|
||||
Column() {
|
||||
CalendarPage({ bgActive: this.bgActive, visible: this.currentIndex === 1 })
|
||||
if (this.currentIndex === 0) {
|
||||
CommPage({ bgActive: this.bgActive })
|
||||
} else if (this.currentIndex === 2) {
|
||||
ContactsTab({ bgActive: this.bgActive })
|
||||
}
|
||||
/*
|
||||
* 日历与另外两个 pane 不同:**常驻挂载**,用 `visibility` 控制显示。
|
||||
*
|
||||
* 理由两条,都不是美观问题:
|
||||
* ① `today` 是日历里**唯一随时间变**的输入,而日历是"放着不动的 pane"。
|
||||
* 卸载重挂会重算(`aboutToAppear`),但"一直开着跨过午夜"不会 ——
|
||||
* 常驻 + `@Watch(visible)` 才能在它重新可见时重算
|
||||
* (`docs/DEBTS.json` 的 `calendar-today-recompute`)。
|
||||
* ② 常驻顺带保住"正在看哪个月",切页签回来不会被重置回本月。
|
||||
* 常驻 ≠ 常拉:首次**可见**时才发请求(见 `CalendarPage.onVisibleChanged`)。
|
||||
*/
|
||||
Column() {
|
||||
CalendarPage({ bgActive: this.bgActive, visible: this.currentIndex === 1 })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.visibility(this.currentIndex === 1 ? Visibility.Visible : Visibility.None)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.visibility(this.currentIndex === 1 ? Visibility.Visible : Visibility.None)
|
||||
.padding({ bottom: this.isWide ? 0 : NAV_CONTENT_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ bottom: NAV_CONTENT_RESERVE })
|
||||
|
||||
// 浮动玻璃条浮在最上层(自绘,见 NavBar 的说明)
|
||||
this.NavBar()
|
||||
// 浮动玻璃条浮在最上层(自绘,见 NavBar 的说明)。宽屏没有底部导航。
|
||||
if (!this.isWide) {
|
||||
this.NavBar()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.onAreaChange((oldValue: Area, newValue: Area) => {
|
||||
this.isWide = (newValue.width as number) >= 768;
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,7 @@ struct SessionsPage {
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else if (this.sessions.length === 0) {
|
||||
Column() {
|
||||
Text('📭 暂无会话').fontSize(16).fontColor(Theme.textSubtle)
|
||||
Text('暂无会话').fontSize(16).fontColor(Theme.textSubtle)
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AuthApi } from '../api/AuthApi';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||||
import { SseService } from '../api/SseService';
|
||||
import { AppearanceStore } from '../common/AppearanceStore';
|
||||
@ -15,6 +17,8 @@ import { AdminUser } from '../model/Models';
|
||||
import { isAdminRole } from '../model/AdminUsers';
|
||||
import { ApiBaseCheck, validateApiBase } from '../model/ApiBase';
|
||||
import { BackgroundPicker } from '../common/BackgroundPicker';
|
||||
import { PushService } from '../api/PushService';
|
||||
import { PushTokenList } from '../model/PushContract';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
@ -35,6 +39,12 @@ struct SettingsPage {
|
||||
@State appearanceStatus: string = 'local-only';
|
||||
/** 当前账号是不是管理员 —— 只用来决定「管理」入口显不显示(口径见 model/AdminUsers.ts 的 isAdminRole) */
|
||||
@State isAdmin: boolean = false;
|
||||
/*
|
||||
* ★ 2026-09-15:接入系统通知 = **可配置项**(用户强调多次:“用户可以自由部署自己的 agentmail”)。
|
||||
* 默认 **关** —— 没开就不取 token / 不弹权限 / 不打网关。开关持久化在 PushService 的偏好里。
|
||||
*/
|
||||
@State pushEnabled: boolean = false;
|
||||
@State pushServerStatus: string = 'n/a';
|
||||
/*
|
||||
* 背景四值(P4c):由选择器(`BackgroundPicker`,@Link 双向绑)改,
|
||||
* 用户改一次就回调 `onUserChanged` ⇒ 这里推服务端 —— 与 `setTheme` 同一形状。
|
||||
@ -63,6 +73,7 @@ struct SettingsPage {
|
||||
this.refreshList();
|
||||
this.loadAppearance();
|
||||
this.loadRole();
|
||||
this.loadPushSetting();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -152,6 +163,53 @@ struct SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 2026-09-15:接入系统通知 = **可配置项**(默认关)。
|
||||
* 读一次持久化开关回填 @State,顺带看服务端有没有配推送(enabled 字段)。
|
||||
* 服务端没配/读不到 → 明确告诉用户“服务器未配置推送” —— 但开关本身照常可用。
|
||||
*/
|
||||
async loadPushSetting(): Promise<void> {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
this.pushEnabled = PushService.isEnabled(ctx);
|
||||
// 顺带读一次服务端 enabled(有账号时);读不到也是正常态(自部署没配凭证)
|
||||
const client: ApiClient | null = this.client;
|
||||
if (client === null) {
|
||||
this.pushServerStatus = 'n/a';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp: PushTokenList = await client.get<PushTokenList>('/me/devices/push-token');
|
||||
this.pushServerStatus = resp.enabled ? 'server-on' : 'server-off';
|
||||
} catch (e) {
|
||||
this.pushServerStatus = 'n/a';
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户拨动开关:写持久化;开了就立刻补一次上报(不开就不碰推送) */
|
||||
async togglePush(enabled: boolean): Promise<void> {
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
this.pushEnabled = enabled;
|
||||
PushService.setEnabled(ctx, enabled);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
// 开了:立刻取 token + 申请权限 + 上报(全部静默,失败下次启动再试)
|
||||
const client: ApiClient | null = this.client;
|
||||
if (client !== null) {
|
||||
// 页面里的 `getHostContext()` 返回 `Context`;这里就是在 ability 里(UIAbilityContext 是其子类)
|
||||
const push: PushService = PushService.getInstance(ctx as common.UIAbilityContext);
|
||||
push.reportToken(client).catch((err: Object) => {
|
||||
hilog.info(0x0001, 'SettingsPage', '开后立即上报失败(静默):%{public}s', JSON.stringify(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉一次外观:按合并规则落地(规则在 `model/Appearance.ts`,判据跑那一份)。
|
||||
*
|
||||
@ -290,7 +348,7 @@ struct SettingsPage {
|
||||
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);
|
||||
this.getUIContext().getPromptAction().showToast({ message: '✅ 添加成功: ' + account.displayName });
|
||||
this.getUIContext().getPromptAction().showToast({ message: '添加成功:' + account.displayName });
|
||||
this.showAddDialog = false;
|
||||
this.newDisplayName = '';
|
||||
this.newServer = '';
|
||||
@ -324,7 +382,7 @@ struct SettingsPage {
|
||||
|
||||
if (this.accounts.length === 0) {
|
||||
Column() {
|
||||
Text('📭 暂无账号').fontSize(16).fontColor(Theme.textMuted)
|
||||
Text('暂无账号').fontSize(16).fontColor(Theme.textMuted)
|
||||
Button('添加第一个账号')
|
||||
.margin({ top: 16 }).backgroundColor(Theme.accent)
|
||||
.onClick(() => { this.showAddDialog = true; })
|
||||
@ -407,6 +465,41 @@ struct SettingsPage {
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
|
||||
/*
|
||||
* ── 系统通知(★ 2026-09-15:**可配置项**,默认关)──
|
||||
*
|
||||
* 用户可以自由部署自己的 agentmail:没有服务端推送凭证时,
|
||||
* 关着 = 不取 token / 不弹权限 / 不打网关,零开销;
|
||||
* 开了 = 取 token + 申请通知权限 + 上报服务端(全部静默,失败只写日志)。
|
||||
*/
|
||||
Column() {
|
||||
Row() {
|
||||
Column() {
|
||||
Text('系统通知').fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
Text(this.pushServerStatus === 'server-off'
|
||||
? '服务器未配置推送'
|
||||
: (this.pushServerStatus === 'server-on' ? '服务器已开启推送通道' : '推送通道状态未知'))
|
||||
.fontSize(11)
|
||||
.fontColor(this.pushServerStatus === 'server-on' ? Theme.textMuted : Theme.warnFg)
|
||||
.margin({ top: 2 })
|
||||
}
|
||||
.layoutWeight(1).alignItems(HorizontalAlign.Start)
|
||||
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.pushEnabled })
|
||||
.onChange((isOn: boolean) => {
|
||||
this.togglePush(isOn);
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text('新邮件到达时由系统弹通知(点通知直达邮件)。未配置服务端推送时请保持关闭。')
|
||||
.fontSize(11).fontColor(Theme.textSubtle).margin({ top: 6 })
|
||||
}
|
||||
.width('100%').alignItems(HorizontalAlign.Start)
|
||||
.padding(16).margin({ top: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
|
||||
/*
|
||||
* ── 管理入口(仅管理员可见)──
|
||||
*
|
||||
|
||||
80
client/harmony/entry/src/main/ets/pages/WideSidebar.ets
Normal file
80
client/harmony/entry/src/main/ets/pages/WideSidebar.ets
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 宽屏侧栏图标轨 —— **一比一复刻 WebUI 的 `Sidebar.tsx`**。
|
||||
*
|
||||
* WebUI 的 `Sidebar`(60px 宽)是一列图标按钮:
|
||||
* · 品牌标(顶部)
|
||||
* · 通信 / 日历 / 联系人(中部)
|
||||
* · 设置(底部)
|
||||
* 选中态:文字/图标变色 + 品牌浅底(`nav-active-bg`),**不加背景块、不加指示条**
|
||||
* (用户 2026-09-14:「同时底部导航栏选中对应的文字和图标变色即可」)。
|
||||
*
|
||||
* 鸿蒙这一侧原先**完全没有宽屏布局**(只有底部导航条),所以这个文件同时承担
|
||||
* "宽屏入口"的角色。判据 `harmony-widescreen.test.mjs` 会检查:
|
||||
* · 60vp 宽(与 WebUI 的 60px 一致)
|
||||
* · 三项导航 + 设置按钮
|
||||
* · 选中态只换颜色(与底部导航同一套纪律)
|
||||
*/
|
||||
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { NAV_ITEMS, NavItem } from '../model/NavItems';
|
||||
|
||||
/** 侧栏宽度(vp)。WebUI 的 `Sidebar` 用 `w-[60px]` */
|
||||
export const SIDEBAR_WIDTH: number = 60;
|
||||
|
||||
@Component
|
||||
export struct WideSidebar {
|
||||
/** 当前选中的导航下标 */
|
||||
@Prop currentIndex: number = 0;
|
||||
/** 选中回调 */
|
||||
onSelect: (index: number) => void = (): void => {};
|
||||
/** 设置回调 */
|
||||
onSettings: () => void = (): void => {};
|
||||
|
||||
@Builder
|
||||
NavItemBuilder(key: string, label: string, iconKey: string, index: number) {
|
||||
Column() {
|
||||
AmIcon({
|
||||
iconName: iconKey,
|
||||
iconSize: 22,
|
||||
iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg
|
||||
})
|
||||
Text(label)
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
.margin({ top: 2 })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.onClick(() => { this.onSelect(index); })
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// 品牌标
|
||||
AmIcon({ iconName: 'brandMark', iconSize: 26, iconColor: Theme.accent })
|
||||
.margin({ bottom: 16 })
|
||||
|
||||
// 导航项
|
||||
ForEach(NAV_ITEMS, (item: NavItem, index: number) => {
|
||||
this.NavItemBuilder(item.key, item.label, item.iconKey, index)
|
||||
}, (item: NavItem) => item.key)
|
||||
|
||||
Blank()
|
||||
|
||||
// 设置
|
||||
Column() {
|
||||
AmIcon({ iconName: 'person', iconSize: 22, iconColor: Theme.navFg })
|
||||
Text('我的').fontSize(Theme.fontTiny).fontColor(Theme.navFg).margin({ top: 2 })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.onClick(() => { this.onSettings(); })
|
||||
}
|
||||
.width(SIDEBAR_WIDTH)
|
||||
.height('100%')
|
||||
.padding({ top: 12, bottom: 12 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user