跨端: 顶栏不再自己铺白条 + 日历改左右两栏 + 常驻窗格动画真的会播(用户三处实测指出)
用户三条反馈,逐条对应:
① 「底栏数字为什么显示在图标下面?」
WebUI 的徽标是 `absolute top-1 right-[22%]`(脱离文档流、浮在图标右上角),
我写成了 `Column` 的第三个子节点 ⇒ 参与竖向布局、掉到文字下面。
改用 `Stack({ alignContent: Alignment.TopEnd })` 锚在**图标**上。
(顺带撞了 skill 里明写的坑:Stack 没有 `.justifyContent()`。)
② 「一个横着过去的白条,我真的服了」/「期望:融进背景」
WebUI 的顶栏**自身没有底色** —— 只有 `border-b border-gray-200`
(`ContactPanel.tsx:64`、`CommTabs.tsx:40`、`CalendarView.tsx:295`),
底色由所在面板给;壁纸开启时那层面板是玻璃色(`index.css:876`)。
鸿蒙三个窗格顶栏写死了 `Theme.surface`(实心白)⇒ 无论壁纸开没开,
顶上都是一条不通明白带。改成与**页面底**同一口径
(`bgActive ? Transparent : surface`)+ 补下边框。
③ 「日历页面和webui布局完全不同」
WebUI 是**左右两栏**(`CalendarView.tsx:452-457`):左 `flex-1` 网格、
右 `400px` 常驻面板(日程 / 编辑器 / 小时网格三态互斥)。
鸿蒙原来是**单栏竖堆**。重搭为两栏,`paneWide` 由 `.onAreaChange`
量本页**自己的**宽度(不是屏幕宽度 —— 宽屏下这一页已被侧栏占掉一截);
编辑器改占右栏位置(不再整页盖掉正在看的那个月)。
④ 「最严重的动画问题你一点也不该改」
日历是**常驻挂载**(`visibility` 控制,因为它里面 today 要随时间重算、
也要保住"正在看哪个月"),而 `.transition()` 只在**挂载/卸载**时触发
(SDK 原话 \"when it **appears and disappears**\")⇒ 挂在它上面的
`.transition(paneRiseIn())` **一帧也不会播**,切过去是硬弹。
WebUI 踩过同一个坑并把错法写进了 `index.css:1305-1320`
(「只挂了类,却没让触发窗口出现 ⇒ 类挂着、动画永远不播」),
它的解法是 `html.view-switch` 重放窗口。ArkUI 对应物是
`animateTo` + 显式 `calPaneIn` 属性(`opacity` + `translate`)。
★ reset 必须在 `animateTo` **外**:写进回调里会与同帧的 1 相抵,
渲染层只看得见最终值 ⇒ 动画退化成一个瞬移。
顺带修:
· 宽屏侧栏 = 3 项(通信/日历/**联系**)+ 底部一簇(头像/主题/退出),
与底栏的四项(含「我的」)**不是同一份清单** —— WebUI 的 Sidebar 与
NarrowNav 本就不同(`Sidebar.tsx:26-44` vs `NarrowNav.tsx:37-40`+143)。
新增 `NAV_SIDEBAR_ITEMS` / `ME_PANE_INDEX` / `SIDEBAR_ITEM_*`。
· 退出登录抽成 `api/Logout.ets` 的 `performLogout()`:侧栏底簇与「我的」页
两个入口必须做同一件事(尤其"先注销推送 token"那一步),复制一份就会不一致。
· 徽标 `'plain'` 档底色:WebUI 是石板灰 `--c-chrome-600`(#475569),
我写成与未读共用红色 ⇒ 「联系」的徽标看起来像"有未读"。
· 主题快捷开关(侧栏底簇):对齐 `ThemeToggleButton` —— 从 `system` 翻转时
落到**当前生效值的反面**(不是回 system;那可能毫无变化、让按钮看起来坏了)。
· 「浓度」→「压暗」+ 数值带单位(原先屏上印 `56.000000`;WebUI 是 `suffix="%"`)。
判据(8 个套件全绿:logic 28 / nav 18 / widescreen 7 / window 9 /
arkts 5 / contacts 5 / calendar 30 / system-api 5):
· `harmony-widescreen` ② 重写:**回读 `Sidebar.tsx` 数 `short:` 的个数**
要求鸿蒙同数,并断言底部一簇三键真的被调用(`this.onToggleTheme()` ——
第一版写成 `/onToggleTheme/`,变异测试当场证明它不咬:属性**声明**还在,
正则照样匹上)。新增 ⑧:三档色调各自底色,期望值从 `--c-chrome-600` 读出。
· `harmony-nav` 动画条重写:改判**机制真的存在且被驱动**
(旧断言 `.visibility(...).transition(...)` 锁的正是那个 bug ——
判据引自己写的注释当依据,就会把错误锁死)。新增 reset-在-animateTo-外
这条断言(否则动画退化成瞬移)。
· `harmony-nav` 设备条:`navItemsOf` 的宽屏过滤从"左边缘靠左 1/6"
改成"**整个盒子在侧栏轨道内**" —— 旧条件把日历网格的格子
(实测 `[229,511][366,794]`)也当成导航项,一屏数出 11~12 个。
新增 `navRailItemsOf`:导航轨贴顶、底部簇在屏底,按位置切一刀。
· `harmony-logic` 两处:从 `NAV_ITEMS` / `NAV_SIDEBAR_ITEMS`
**各自的数组**里取 label —— 原先把全文件 `label:` 一网打尽,
得到 7 个(4+3 混在一起),任何一边改对了它都会红。
设备实测(HATriple 三折叠,3184×2232):侧栏 3 项 + 底簇 / 底栏徽标回到
图标右上角 / 日历左右两栏与 WebUI 并排同构。
server/go.mod:补 2da38bb 漏提交的 lunar-go 依赖。
This commit is contained in:
@ -3,9 +3,9 @@
|
||||
*
|
||||
* WebUI 的侧栏(60px 宽)实际是:
|
||||
* · 品牌标(顶部,点它回收件箱)
|
||||
* · 通信 / 日历 / 联系 —— **每项 = 图标 + 文字标签**
|
||||
* · 通信 / 日历 / **联系** —— **三项**,每项 = 图标 + 文字标签
|
||||
* · 选中态:**浅蓝底块**(`--nav-active-bg: 219 234 254`)+ 文字/图标变色
|
||||
* · 底部一簇:账号头像(带连接状态点)/ 主题切换 / 退出
|
||||
* · 底部一簇:**账号头像(带连接状态点)/ 主题切换 / 退出**
|
||||
*
|
||||
* ★★ 2026-09-18 修:这个文件的上一版写着「WebUI 的 Sidebar 是**纯图标轨**
|
||||
* (无 label 文字)……选中态**不加背景块**」,并据此把侧栏做成了纯图标 + 只变色。
|
||||
@ -20,16 +20,31 @@
|
||||
* 而用户 2026-09-16 说的「底部导航栏不允许有文字」针对的是**底部导航栏**。
|
||||
* ⇒ 并排截图一眼可见的差异,被一段自己写的注释盖住了。
|
||||
*
|
||||
* 保留的差异(**有理由的**,不是漏做):WebUI 底部那一簇(头像/主题/退出)
|
||||
* 鸿蒙放在「我的」窗格与底栏里 —— 这里只放导航项,避免同一动作两个入口。
|
||||
*
|
||||
* 判据 `harmony-widescreen.test.mjs` 检查:60vp 宽、三项导航 + 设置、
|
||||
* 选中态有底块、导航项带文字标签。
|
||||
* ★★ 2026-09-19 修第二处(**同类错误的第二次**,用户:
|
||||
* 「你写的app和webui大面积不符,问题特别大」):
|
||||
* 上一版把**底栏的四项**(含「我的」)直接给侧栏用了。四項是对的,
|
||||
* 但**只对底栏** —— `NarrowNav.tsx:37-40` 才是四项;
|
||||
* 宽屏侧栏(`Sidebar.tsx:26-44`)是**三项**,
|
||||
* 「我的」是底部那个**头像按钮**(`Sidebar.tsx:186`),与主题切换/退出并列。
|
||||
* 并排截图里鸿蒙多出一个「我的」导航项、而 WebUI 那一格是头像 —— 同一个错法。
|
||||
* ⇒ 两份清单现在是分开的:`NAV_SIDEBAR_ITEMS`(3) 与 `NAV_ITEMS`(4)。
|
||||
*/
|
||||
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { NAV_CONTENT_ITEMS, NavItem, navBadgeCount, navBadgeText, navBadgeTone } from '../model/NavItems';
|
||||
import { SseService } from '../api/SseService';
|
||||
import { AccountManager, AccountInfo } from '../api/AccountManager';
|
||||
import {
|
||||
NAV_SIDEBAR_ITEMS,
|
||||
NavItem,
|
||||
ME_PANE_INDEX,
|
||||
SIDEBAR_ITEM_SIZE,
|
||||
SIDEBAR_ITEM_RADIUS,
|
||||
navBadgeCount,
|
||||
navBadgeText,
|
||||
navBadgeTone,
|
||||
sidebarContentIndex
|
||||
} from '../model/NavItems';
|
||||
import { Insets, topInset } from '../model/WindowInsets';
|
||||
|
||||
/** 侧栏宽度(vp)。WebUI 的 `Sidebar` 用 `w-[60px]` */
|
||||
@ -37,7 +52,7 @@ export const SIDEBAR_WIDTH: number = 60;
|
||||
|
||||
@Component
|
||||
export struct WideSidebar {
|
||||
/** 当前选中的导航下标 */
|
||||
/** 当前选中的导航下标(= 内容窗格下标,四项那套) */
|
||||
@Prop currentIndex: number = 0;
|
||||
/**
|
||||
* 窗口避让区(vp)。
|
||||
@ -51,42 +66,104 @@ export struct WideSidebar {
|
||||
* 徽标计数 —— 由 `CommPage` / `MainPage` 经 AppStorage 发布(单向:页面写、导航栏读)。
|
||||
*
|
||||
* `@StorageProp` 而不是 `@StorageLink`:侧栏**只读**,不该往外写。
|
||||
* 与底栏读的是**同一组键** —— 两处各算一套,数字迟早对不上(用户同时看得到它们)。
|
||||
* 与底栏读的是**同一组键** —— 两处各算一套,数字迟迟对不上(用户同时看得到它们)。
|
||||
*/
|
||||
@StorageProp('agentmail.nav.unread') navUnread: number = 0;
|
||||
@StorageProp('agentmail.nav.pending') navPending: number = 0;
|
||||
@StorageProp('agentmail.nav.contacts') navContacts: number = 0;
|
||||
/** 背景是否开启:开着=玻璃面板(blur + 0.88 白),关着=不透明白 */
|
||||
@Prop bgActive: boolean = false;
|
||||
/** 当前账号展示名(底部头像里的前两个字)—— 对齐 WebUI `Sidebar.tsx:190` */
|
||||
@Prop accountLabel: string = '';
|
||||
/** 当前是否深色(主题切换按钮的图标)—— 对齐 WebUI `ThemeToggleButton` */
|
||||
@Prop isDark: boolean = false;
|
||||
/** 选中回调 */
|
||||
onSelect: (index: number) => void = (): void => {};
|
||||
/** 主题切换(顶部快捷开关) */
|
||||
onToggleTheme: () => void = (): void => {};
|
||||
/** 退出登录 */
|
||||
onLogout: () => void = (): void => {};
|
||||
|
||||
/**
|
||||
* 侧栏导航项 = **图标 + 文字标签 + 选中底块**(三项都与 WebUI `Sidebar` 一致)。
|
||||
* SSE 连接状态 → 头像右下角那颗点(对齐 WebUI `ConnectionIndicator`)。
|
||||
*
|
||||
* 底块尺寸对齐 WebUI:`w-12 h-12 rounded-lg` = 48×48vp、圆角 8。
|
||||
* 状态由 `SseService` 提供(`getStatusForAccount`)。这里不做轮询:
|
||||
* `aboutToAppear` 时读一次 + 订阅变化就够 —— 转圈反而会在状态没变时
|
||||
* 让界面脏得比状态本身快。
|
||||
*/
|
||||
@State private sseStatus: string = 'disconnected';
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.refreshSseStatus();
|
||||
}
|
||||
|
||||
/** 当前活跃账号的连接状态(没有账号时就是断开) */
|
||||
private refreshSseStatus(): void {
|
||||
let accountId: string = '';
|
||||
const ctx = this.getUIContext().getHostContext();
|
||||
if (ctx !== undefined) {
|
||||
try {
|
||||
const active: AccountInfo | null = AccountManager.getInstance(ctx).getActiveAccount();
|
||||
accountId = active === null ? '' : active.id;
|
||||
} catch (e) {
|
||||
accountId = '';
|
||||
}
|
||||
}
|
||||
this.sseStatus = SseService.getInstance().getStatusForAccount(accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态 → 颜色。与 WebUI `ConnectionIndicator.tsx:26-31` 逐档对齐:
|
||||
* connecting 黄 / connected 绿 / reconnecting 橙 / disconnected 红。
|
||||
*
|
||||
* ★ 取近似色而不是新建一组常量:这四个是"状态点"专用的语义色,
|
||||
* 与正文/边框不是一族;Theme 里没有它们的位置就就地取,
|
||||
* 而不是把 `Theme.warnFg`(#B45309 深橙)拿来当"连接中"——那是文字色。
|
||||
*/
|
||||
private sseColorOf(status: string): string {
|
||||
if (status === 'connected') {
|
||||
return '#22C55E';
|
||||
}
|
||||
if (status === 'connecting') {
|
||||
return '#FACC15';
|
||||
}
|
||||
if (status === 'reconnecting') {
|
||||
return '#FB923C';
|
||||
}
|
||||
return '#F87171';
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏导航项 = **图标 + 文字标签 + 选中底块**(三项,与 WebUI `Sidebar` 一致)。
|
||||
*
|
||||
* 底块尺寸对齐 WebUI:`w-12 h-12 rounded-lg` = 48×48vp、圆角 8(`Sidebar.tsx:112`)。
|
||||
*
|
||||
* ★ 点击传**内容窗格下标**(`sidebarContentIndex`),不是侧栏的下标:
|
||||
* 侧栏只画三项,而 `currentIndex` 是四项那套(`me` = 3 由底部头像点)。
|
||||
* 直接把构图的 `index` 传出去会在「联系」以后错位 —— 这里恰好第三项都对,
|
||||
* 但依赖"两份清单前两项相同"是暗器,不能留。
|
||||
*/
|
||||
@Builder
|
||||
NavItemBuilder(key: string, label: string, iconKey: string, index: number) {
|
||||
SidebarItem(item: NavItem) {
|
||||
Stack({ alignContent: Alignment.TopEnd }) {
|
||||
Column() {
|
||||
AmIcon({
|
||||
iconName: iconKey,
|
||||
iconName: item.iconKey,
|
||||
iconSize: 22,
|
||||
iconColor: this.currentIndex === index ? Theme.navFgActive : Theme.navFg
|
||||
iconColor: this.currentIndex === sidebarContentIndex(item.key) ? Theme.navFgActive : Theme.navFg
|
||||
})
|
||||
Text(label)
|
||||
Text(item.label)
|
||||
.fontSize(10)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
.fontColor(this.currentIndex === sidebarContentIndex(item.key) ? Theme.navFgActive : Theme.navFg)
|
||||
.margin({ top: 2 })
|
||||
}
|
||||
.width(48)
|
||||
.height(48)
|
||||
.borderRadius(12)
|
||||
.width(SIDEBAR_ITEM_SIZE)
|
||||
.height(SIDEBAR_ITEM_SIZE)
|
||||
.borderRadius(SIDEBAR_ITEM_RADIUS)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
// 选中底块:WebUI `.nav-item[data-active='true']` 的 `--nav-active-bg`
|
||||
.backgroundColor(this.currentIndex === index ? Theme.navActiveBg : Color.Transparent)
|
||||
.onClick(() => { this.onSelect(index); })
|
||||
.backgroundColor(this.currentIndex === sidebarContentIndex(item.key) ? Theme.navActiveBg : Color.Transparent)
|
||||
.onClick(() => { this.onSelect(sidebarContentIndex(item.key)); })
|
||||
|
||||
/*
|
||||
* 徽标(与底栏、与 WebUI `Sidebar.tsx:88-95` 同一套口径)。
|
||||
@ -96,17 +173,19 @@ export struct WideSidebar {
|
||||
* 不是排在文字下面。放下面会把 48vp 的项撑高、方阵节奏乱掉
|
||||
* (第一版就是这么错的,截图一眼可见:那颗 3 掉到了"联系人"文字下面)。
|
||||
*/
|
||||
if (navBadgeText(navBadgeCount(key, this.navUnread, this.navPending, this.navContacts)).length > 0) {
|
||||
Text(navBadgeText(navBadgeCount(key, this.navUnread, this.navPending, this.navContacts)))
|
||||
if (navBadgeText(navBadgeCount(item.key, this.navUnread, this.navPending, this.navContacts)).length > 0) {
|
||||
Text(navBadgeText(navBadgeCount(item.key, this.navUnread, this.navPending, this.navContacts)))
|
||||
.fontSize(9).fontColor(Color.White)
|
||||
.backgroundColor(navBadgeTone(key, this.navPending) === 'perm' ? Theme.warnFg : Theme.danger)
|
||||
.backgroundColor(navBadgeTone(item.key, this.navPending) === 'perm'
|
||||
? Theme.warnFg
|
||||
: (navBadgeTone(item.key, this.navPending) === 'plain' ? Theme.badgePlain : Theme.danger))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 4, right: 4 })
|
||||
.margin({ top: 2, right: 2 })
|
||||
}
|
||||
}
|
||||
.width(48)
|
||||
.height(48)
|
||||
.width(SIDEBAR_ITEM_SIZE)
|
||||
.height(SIDEBAR_ITEM_SIZE)
|
||||
}
|
||||
|
||||
build() {
|
||||
@ -127,8 +206,13 @@ export struct WideSidebar {
|
||||
.margin({ bottom: 8 })
|
||||
.onClick(() => { this.onSelect(0); })
|
||||
|
||||
// 导航项:**只内容窗格**(前三项)—— 设置已单列在下面,
|
||||
// 若遍历 NAV_ITEMS 会因第 4 项「我的」出现**两个 person 图标**。
|
||||
/*
|
||||
* 导航项 —— **三项**(`NAV_SIDEBAR_ITEMS`),不含「我的」。
|
||||
*
|
||||
* 「我的」在 WebUI 是**底部头像按钮**(`Sidebar.tsx:186`),
|
||||
* 不是导航轨里的一项;把它放在这里会多出一个 person 图标
|
||||
* (弹与 `NAV_CONTENT_ITEMS` 重复,两者当时就是同一个错)。
|
||||
*/
|
||||
/*
|
||||
* 导航项**一簇贴顶**,不是等分铺满。
|
||||
*
|
||||
@ -138,8 +222,8 @@ export struct WideSidebar {
|
||||
* 三个图标散在 1200px 里 —— 并排一看就知道不是同一个东西。
|
||||
*/
|
||||
Column({ space: 4 }) {
|
||||
ForEach(NAV_CONTENT_ITEMS, (item: NavItem, index: number) => {
|
||||
this.NavItemBuilder(item.key, item.label, item.iconKey, index)
|
||||
ForEach(NAV_SIDEBAR_ITEMS, (item: NavItem) => {
|
||||
this.SidebarItem(item)
|
||||
}, (item: NavItem) => item.key)
|
||||
}
|
||||
.width('100%')
|
||||
@ -147,19 +231,53 @@ export struct WideSidebar {
|
||||
Blank()
|
||||
|
||||
/*
|
||||
* ★★ 2026-09-18 修:下面那一簇(原「设置」单列项)删掉了。
|
||||
* 底部一簇 —— 对齐 WebUI `Sidebar.tsx:183-212`:
|
||||
* ① 账号头像(前两字,右下角带 SSE 连接状态点)→ 进「我的」窗格
|
||||
* ② 主题切换(浅色/深色翻转)
|
||||
* ③ 退出登录
|
||||
*
|
||||
* 它原来是 `onSettings → pushUrl('pages/SettingsPage')` —— 那正是用户
|
||||
* 2026-09-17 报过的形状:「我的页面完全没有遵守 nav 的导航规则」
|
||||
* (push 一个独立 @Entry 页 ⇒ 侧栏/底栏整条消失,要先「返回」才能再切窗格)。
|
||||
* 底栏那一支当时改成了窗格(`currentIndex === 3`),**侧栏这一支漏了**,
|
||||
* 于是宽屏下同一个毛病还在(而宽屏此前从未真正运行过,所以没人看见)。
|
||||
*
|
||||
* 现在两侧统一:`NAV_CONTENT_ITEMS` 是**全部四项**(含「我的」),
|
||||
* 上面的 `ForEach` 已经把四项都渲染出来(含「我的」= `onSelect(3)` = 窗格),
|
||||
* 所以这里**不需要**再单列一个入口 —— 单列那个会与第 4 项重复,
|
||||
* 而重复的那一个还恰好是"推页"的那条老路。
|
||||
* ★ 上一版把这一簇删了,理由是"鸿蒙放在「我的」窗格与底栏里,避免同一动作两个入口"
|
||||
* —— 那条理由站不住:WebUI 同时有这两处(侧栏底部 **和** 「我的」页)。
|
||||
* “侧栏没有它”不是取舍,是没做。
|
||||
*/
|
||||
Column({ space: 4 }) {
|
||||
/* ① 头像(带连接状态点) */
|
||||
Stack({ alignContent: Alignment.BottomEnd }) {
|
||||
Text(this.accountLabel.length > 0 ? this.accountLabel.slice(0, 2) : '?')
|
||||
.fontSize(11)
|
||||
.fontColor(Theme.navFg)
|
||||
.width(36).height(36)
|
||||
.borderRadius(18)
|
||||
.backgroundColor(this.currentIndex === ME_PANE_INDEX ? Theme.navActiveBg : Theme.border)
|
||||
.textAlign(TextAlign.Center)
|
||||
/* 连接状态点:不遮挡文字,贴在右下角(WebUI `-bottom-0.5 -right-0.5`) */
|
||||
Circle({ width: 8, height: 8 })
|
||||
.fill(this.sseColorOf(this.sseStatus))
|
||||
.margin({ right: 1, bottom: 1 })
|
||||
}
|
||||
.width(40).height(40)
|
||||
.onClick(() => { this.onSelect(ME_PANE_INDEX); })
|
||||
|
||||
/* ② 主题切换 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
AmIcon({ iconName: this.isDark ? 'moon' : 'sun', iconSize: 16, iconColor: Theme.navFg })
|
||||
}
|
||||
.width(36).height(28)
|
||||
.borderRadius(8)
|
||||
.onClick(() => { this.onToggleTheme(); })
|
||||
|
||||
/* ③ 退出 */
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
AmIcon({ iconName: 'logout', iconSize: 16, iconColor: Theme.navFg })
|
||||
}
|
||||
.width(36).height(28)
|
||||
.borderRadius(8)
|
||||
.onClick(() => { this.onLogout(); })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 8 })
|
||||
.border({ width: { top: 1 }, color: Theme.border })
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
/*
|
||||
* ★ 2026-09-18 补一条**实测口径**(差点被我读错):
|
||||
|
||||
Reference in New Issue
Block a user