跨端: 修卡片投影/页签条圆角/日视图/周视图 — 五处对 WebUI 的误读
用户逐条指出后,用 CDP 读 WebUI 的 computed style 与祖先链,发现五处都是我读错/抄错。
## ① 列表底部那条"不知道什么玩意的阴影"(用户原话)
根因:我把 `instance.shadow(Theme.glassShadow)` 挂在 **每张卡片** 上。
WebUI 全仓 `box-shadow` 只有三处 —— `.app-shell > *`(窗格)、
`html[data-bg='on'] .app-shell > *`、`.narrow-nav`(底部导航条)。
**卡片 `.glass-card` 一次都没有**(整个规则块只有 radius/border/bg/transition)。
一屏十几张卡各投一次 ⇒ 叠加成灰雾。设备实测(x=600,卡片底边 y≈1847):
有投影 y=1846→231、y=1852→230(向下衰减的暗带)
去掉后 y=1846→255、y=1852→251(消失)
⇒ 投影移到 `PaneModifier`(窗格层),与 WebUI 一致。
★ 判定实验还排除了一个嫌疑:`List` 默认 `edgeEffect(EdgeEffect.Spring)`
(官方文档「支持弹簧效果和**阴影效果**」)。设 `EdgeEffect.None` 后暗带照样在
⇒ 不是它。**弹簧是滚到边缘的正常反馈,不该为遮一个 bug 把它关掉。**
## ② 页签条右上角"那个圆角"(用户原话)
两道弧叠在一起。WebUI 实测(CDP 读祖先链):
.comm-pane x=80 w=320 radius=14px overflow=hidden ← 窗格
tabstrip x=80 w=320 radius=0px ← 页签条
**两者横向完全齐平**,页签条自己直角、靠窗格裁圆 ⇒ 只有一道弧。
我们内缩 16vp + 自己带角 ⇒ 两道弧,中间一弯月牙。
⇒ 改成与窗格齐平、只留左上角圆角。
★ 期间我犯过两个错,都记在注释里:先把半径设成 `HEADER_HEIGHT/2`=22(条高也是
44,那就是一个完整胶囊),后来自作主张把整条改成"通栏+下边框+无玻璃"——
那是重做而不是用户要的小改,已还原。
## ③ 日视图与周视图一样(用户原话)
`DayTimeline()` 我写好了却**从未调用**。日视图一直走 `rows()`(周视图那套七列)。
⇒ 按 scale 分流:日视图 → 24 小时时间轴(对齐 WebUI `DayGrid` 的 `HOURS` 逐行、
当前小时淡蓝底);周/日格子改用 `WEEK_CELL_HEIGHT`(240) 并画日程标题条
(对齐 `EventChip`:`HH:mm` + 标题,停用的加删除线 + 灰底)。
## ④ 邮件正文垂直居中(用户原话)
官方 FAQ `faqs-arkui-725` 原文:内容比 `Scroll` 矮时「**默认居中排布**」,
要 `.align(Alignment.Top)` 才是顶部排布。
实测内容块在 `Scroll` 里偏移 731px = (1944−481)/2 —— 正好居中。
⇒ 4 处 Scroll/List 补 `.align(Alignment.Top)`(详情页 y 从 1018 → 287)。
## ⑤ 窄屏圆角/留白全丢(用户:「你的邮件的圆角呢?日历的圆角呢?」)
`borderRadius(this.isWide ? glassRadius : 0)` —— 窄屏恒 0;
`left/right: isWide ? paneGap : 0` —— 把 WebUI「窄屏**底边**留白为 0」
错推广成「四边都为 0」。
WebUI 两条 shell 规则**都给圆角**,差的只是底边留白。
⇒ 圆角两端都开;左右留白两端都给;只有底边分宽窄。
## 连带修的两个真问题
· **底部导航避让**(用户:「为什么不避让底部导航栏」):根是 `NavBar` 与内容
是 `Stack` 里的**并列兄弟**(互相重叠),所以每个滚动容器都得手写
`contentEndOffset` —— 我漏了 8 处。照 WebUI 改成 `Column{内容, NavBar}`
(WebUI 实测 `scroller.bottom=861` / `nav.top=871`,内容停在导航上方)。
· **不透明的那一张卡**(用户:「有一个完全不透明的邮箱项」):`MailRow` 手写
`Theme.surface`(系统卡片色 α=1),而 `GroupHeader` 用的是 `GlassCardModifier`。
⇒ 改用同一件基础件 + 新增 `TintModifier` 合成状态色(`.attributeModifier`
是单一插槽,色必须参与合成)。实测 `(255,255,255)` → `(195,218,204)`。
判据随之反转 3 条(`harmony-widescreen` 两处把"窄屏 0"改成"两端都要"、
`cross-client-theme` 抓到我新写的 `Theme.border` 当 `fontColor`)。
This commit is contained in:
@ -380,10 +380,34 @@ export class GlassCardModifier implements AttributeModifier<CommonAttribute> {
|
||||
* `brightness` 只给一点点(1.06):再高就把正文的对比度吃掉 ——
|
||||
* WebUI 那边靠 `--glass-base` 恒白 + alpha 控浓度,我们这里靠 brightness。
|
||||
*/
|
||||
instance.backgroundEffect({
|
||||
radius: Theme.glassCardRadius,
|
||||
saturation: Theme.glassCardSaturation
|
||||
});
|
||||
/*
|
||||
* ★★ 2026-09-21 **去掉模糊**(用户:「邮件看着没有玻璃效果」→ 实测后发现是我的错)。
|
||||
*
|
||||
* 原来这里挂的是 `backgroundEffect({radius: 18, saturation: 1.5})` ——
|
||||
* 那是**底部导航条那一档**参数(WebUI `.narrow-nav` 的
|
||||
* `blur(18px) saturate(1.5)`,`index.css:1204`)。
|
||||
*
|
||||
* 而 WebUI 的**卡片根本不是模糊玻璃**(`.glass-card`,`index.css:1629`):
|
||||
*
|
||||
* .glass-card { background-color: rgb(255 255 255 / 0.92); }
|
||||
* html[data-bg='on'] … { background-color: rgb(255 255 255 / 0.78); }
|
||||
*
|
||||
* 全仓 `backdrop-filter` **只出现两处**:
|
||||
* · `.glass-control` —— 控件档(输入框等),`blur(8px) saturate(1.1)`
|
||||
* · `.narrow-nav` —— 底部导航条,`blur(18px) saturate(1.5)`
|
||||
* 卡片一次都没有。而 `index.css:990` 还专门写了警告:
|
||||
*
|
||||
* 「再 backdrop-filter 一次纯属叠加:不会更"玻璃",只会更脏更糊,
|
||||
* 而且每层都要重新采样一次背景(滚动时明显掉帧)」
|
||||
*
|
||||
* ⇒ 我的错法后果(实测像素):每张卡各糊一次,**卡内像素约等于壁纸色**
|
||||
* (行内 195,218,204 vs 壁纸 189,197,207)—— 卡片读起来像"壁纸上开了个洞"
|
||||
* 而不是"一张浮在上面的白纱"。这正是用户说"没有玻璃效果"的意思:
|
||||
* 玻璃感来自**白纱 + 发丝边 + 投影**这三件,不是来自模糊。
|
||||
*
|
||||
* ⇒ 照 WebUI:**白色 + alpha,不模糊**。
|
||||
*/
|
||||
instance.backgroundColor(this.active ? Theme.glassCardWall : Theme.glassCard);
|
||||
/*
|
||||
* ★ 只模糊+饱和仍然"像一块平的白方块" —— 玻璃板之所以像板,靠的是**边缘**:
|
||||
* 它要看起来**浮在壁纸之上**。WebUI 的完整配方(`.app-shell > *`):
|
||||
@ -402,7 +426,38 @@ export class GlassCardModifier implements AttributeModifier<CommonAttribute> {
|
||||
* 与 `rgba()`,理由正当:手写色在深色下要么看不见、要么就是替系统猜深浅)。
|
||||
* 系统档自带深浅两套取值 —— 与 `radiusCard` 走 `sys.float.*` 同一个道理。
|
||||
*/
|
||||
instance.shadow(Theme.glassShadow);
|
||||
/*
|
||||
* ★★ 2026-09-21 **去掉卡片投影**(用户:「你看看邮件列表底部的不知道什么玩意的阴影」)。
|
||||
*
|
||||
* 我原来在这里写了 `instance.shadow(Theme.glassShadow)`
|
||||
* (`ShadowStyle.OUTER_FLOATING_SM`)—— 那是个**误配**。
|
||||
*
|
||||
* WebUI 全仓 `box-shadow` **只有三处**(`grep -n box-shadow index.css`):
|
||||
*
|
||||
* 980 .app-shell > * ← **宽屏的窗格**(pane)
|
||||
* 1073 html[data-bg='on'] .app-shell > * ← 壁纸开时的窗格,投影加重
|
||||
* 1206 .narrow-nav ← **底部导航条**
|
||||
*
|
||||
* **卡片 `.glass-card` 一次都没有**(`index.css:1629`,整个规则块里只有
|
||||
* `border-radius` / `border` / `background-color` / `transition`)。
|
||||
*
|
||||
* 我把「窗格才该有的投影」加到了**每一张卡**上 ⇒ 一屏十几张卡各投一次,
|
||||
* 投影彼此叠加成一片灰雾。设备实测(宽屏 3184px,x=600):
|
||||
*
|
||||
* 有投影:y=1844 → 204 y=1846 → 231 y=1852 → 230 (一条从卡片底边
|
||||
* 向下衰减的暗带,起始一步骤降 51)
|
||||
* 去掉后:y=1846 → 255 y=1852 → 251 y=1900 → 252 (暗带消失)
|
||||
*
|
||||
* ★ 那次判定实验还排除了另一个嫌疑:我一度以为是 `List` 的默认
|
||||
* `edgeEffect(EdgeEffect.Spring)`(官方文档:「支持弹簧效果和**阴影效果**」),
|
||||
* 设成 `EdgeEffect.None` 后暗带**照样在** ⇒ 不是它。
|
||||
* 弹簧是列表滚到边缘的正常反馈,**不该为了遮一个 bug 把它关掉**。
|
||||
*
|
||||
* ── 那"玻璃板浮起来"的观感靠什么?──
|
||||
* 靠**发丝描边**(下一行的 `border`)与**白纱 + alpha**(上面那句
|
||||
* `backgroundColor`)。WebUI 的卡片正是这两件 —— 它没有投影仍然读得出是"一块卡"。
|
||||
* 投影留给**真正悬浮在内容之上**的层(底部导航条),那里它才有信息意义。
|
||||
*/
|
||||
/* 发丝描边:用系统分隔线色(深浅由系统给),不是手写 alpha */
|
||||
instance.border({ width: 1, color: Theme.border });
|
||||
} else {
|
||||
@ -439,6 +494,46 @@ export class GlassCardModifier implements AttributeModifier<CommonAttribute> {
|
||||
* ★ 窗格**不吃材质**:它是"让位"的那一层,玻璃在卡片/浮层上
|
||||
* (WebUI 的 `.comm-pane > .bg-white { background-color: transparent }` 同一分工)。
|
||||
*/
|
||||
/**
|
||||
* **一层色** —— 给已经铺了玻璃的卡片叠一个状态底色(未读/选中/悬停)。
|
||||
*
|
||||
* ★★ 2026-09-21 新增。为什么需要它,而不是直接在调用点 `.backgroundColor()`:
|
||||
*
|
||||
* ArkUI 的 `.attributeModifier()` 是**单一插槽** —— 挂在它**之外**的
|
||||
* `.backgroundColor(...)` 会被 modifier 内部的 `instance.backgroundColor(...)`
|
||||
* 直接覆盖(编译器不报错,只是静默失效)。而玻璃卡的形制正是:
|
||||
*
|
||||
* ① 透明底 + `backgroundEffect`(模糊玻璃)
|
||||
* ② 一层**半透明状态色**(未读的淡蓝 / 选中的更重一档)
|
||||
*
|
||||
* 两个都要 ⇒ 必须**合成**进同一个 modifier。用 `CompositeModifier` 可以叠多个
|
||||
* modifier,但"色"本身不是 modifier —— 所以补这一个小类,让色也能参与合成。
|
||||
*
|
||||
* 用它的典型写法(`MainPage.MailRow`):
|
||||
* CompositeModifier.of([
|
||||
* GlassCardModifier.of(this.bgActive, false),
|
||||
* TintModifier.of(Theme.accentSoftFor(this.isDarkNow))
|
||||
* ])
|
||||
*
|
||||
* ★ 顺序有意义:**先玻璃、后色**。反过来的话玻璃会把色盖掉
|
||||
* (`backgroundEffect` 不影响底色,但玻璃卡的 `backgroundColor(Transparent)`
|
||||
* 会覆盖掉先铺的那层色)。
|
||||
*/
|
||||
export class TintModifier implements AttributeModifier<CommonAttribute> {
|
||||
private color: ResourceColor = Color.Transparent;
|
||||
|
||||
/** 工厂:`TintModifier.of(Theme.accentSoft)` */
|
||||
static of(color: ResourceColor): TintModifier {
|
||||
const m = new TintModifier();
|
||||
m.color = color;
|
||||
return m;
|
||||
}
|
||||
|
||||
applyNormalAttribute(instance: CommonAttribute): void {
|
||||
instance.backgroundColor(this.color);
|
||||
}
|
||||
}
|
||||
|
||||
export class PaneModifier implements AttributeModifier<CommonAttribute> {
|
||||
private active: boolean = false;
|
||||
|
||||
@ -451,6 +546,80 @@ export class PaneModifier implements AttributeModifier<CommonAttribute> {
|
||||
|
||||
applyNormalAttribute(instance: CommonAttribute): void {
|
||||
instance.backgroundColor(this.active ? Color.Transparent : Theme.pageBg);
|
||||
/*
|
||||
* ★★ 2026-09-21 把投影**移到窗格这一层**(用户:「你看看邮件列表底部的
|
||||
* 不知道什么玩意的阴影」)。
|
||||
*
|
||||
* 投影原先挂在 `GlassCardModifier` 上(每张卡各投一次),那是**误配** ——
|
||||
* WebUI 全仓 `box-shadow` 只有三处:
|
||||
* 980 `.app-shell > *` ← **窗格**
|
||||
* 1073 `html[data-bg='on'] .app-shell > *` ← 壁纸开时的窗格(投影加重)
|
||||
* 1206 `.narrow-nav` ← 底部导航条
|
||||
* 卡片(`.glass-card`)**一次都没有**。
|
||||
*
|
||||
* 一屏十几张卡各投一次 ⇒ 投影彼此叠成一片灰雾。设备实测(宽屏 3184px,
|
||||
* x=600,卡片底边在 y≈1847):
|
||||
* 卡上有投影:y=1844→204 y=1846→231 y=1852→230 (一条向下衰减的暗带)
|
||||
* 去掉后: y=1846→255 y=1852→251 y=1900→252 (暗带消失)
|
||||
*
|
||||
* 移到窗格层才与 WebUI 一致,也才有意义 —— 窗格是"浮在壁纸上的一张板",
|
||||
* 投影表达的是它与壁纸的**层次差**;卡片在窗格之内,同层之间不需要投影。
|
||||
*/
|
||||
instance.shadow(Theme.glassShadow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **按压反馈** —— 全仓可点元素共用的一种"按下缩小、松开回弹"。
|
||||
*
|
||||
* ★★ 2026-09-21 重做(用户:「按钮连最基础的点击反馈(按下缩小、手松变大,
|
||||
* 或者按下变色)都没有」)。
|
||||
*
|
||||
* ── 我上一版为什么等于没做 ──
|
||||
*
|
||||
* 我写了个 `PressFeedbackModifier`(`onTouch` + `animateTo` 改底色)。
|
||||
* 它**能用**(实测 FAB 按下 35,98,235 → 35,91,221),但:
|
||||
*
|
||||
* ① **覆盖率约 2 / 112**:它要求调用点逐个手写 `.attributeModifier(...)`。
|
||||
* 全仓 112 处 `onClick`,实际挂上的只有 2 处 ——
|
||||
* 我做了个"基础组件"然后一个都没接(`GlassCard` / `Pressable`
|
||||
* 在**全仓零调用**)。用户看到的当然是"什么都没有"。
|
||||
* ② **只变色、不缩放**:用户点名要的是"按下缩小、手松变大"。
|
||||
*
|
||||
* ── 两条弯路,记下来别再走 ──
|
||||
*
|
||||
* ① 我花了一整轮论证"`applyPressedAttribute` 对纯 Row/Column 不回调 →
|
||||
* 所以只能手写 `onTouch`"。那个观察是对的,但**结论错了** ——
|
||||
* 系统本来就有一个通用属性 `clickEffect`(在 `CommonMethod<T>` 上,
|
||||
* 所有组件都能挂),由框架统一实现按下缩放 + 松开回弹。
|
||||
* **先查有没有现成的**,再决定要不要自己造。
|
||||
* ② 我接着想用 `@Styles` 把它铺开。官方 FAQ 明确:
|
||||
* **「@Styles 和 @Extend 目前不支持 export 导出,后续这两个装饰器将不再演进」**
|
||||
* (`faqs-arkui-222`)—— 编译确实报了
|
||||
* `Property 'pressFeedback' does not exist on type 'ColumnAttribute'`。
|
||||
* 官方推荐的替代正是本仓已有的 `AttributeModifier`。
|
||||
*
|
||||
* ── 现在这个版本 ──
|
||||
*
|
||||
* `AttributeModifier<CommonAttribute>`(与 `GlassCardModifier` 同一族);
|
||||
* 挂在**任意**组件上都生效,且能用 `CompositeModifier` 与玻璃/圆角叠加
|
||||
* (`.attributeModifier` 是**单一插槽**,链两个是后者覆盖前者 —— 本仓踩过)。
|
||||
*
|
||||
* 档位选 `LIGHT`(系统默认,缩放比 **0.90**)而不是 `MIDDLE`(0.95):
|
||||
* 列表行 / 导航项这类**大面积**元素用 0.95 几乎看不出缩放,
|
||||
* 而 0.90 与 WebUI 那几处 `active:bg-*` 的"明显可感知"是同一个意图。
|
||||
*/
|
||||
export class PressEffectModifier implements AttributeModifier<CommonAttribute> {
|
||||
/** 单例即可 —— 它无状态(档位是常量),不必每个调用点 new 一个 */
|
||||
private static readonly INSTANCE: PressEffectModifier = new PressEffectModifier();
|
||||
|
||||
/** 工厂:`PressEffectModifier.of()` */
|
||||
static of(): PressEffectModifier {
|
||||
return PressEffectModifier.INSTANCE;
|
||||
}
|
||||
|
||||
applyNormalAttribute(instance: CommonAttribute): void {
|
||||
instance.clickEffect({ level: ClickEffectLevel.LIGHT });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -218,6 +218,46 @@ export class Theme {
|
||||
* (10px 面板 / 18px 浮条),抄就得抄全套,不能只挑一个数。
|
||||
*/
|
||||
static readonly glassCardRadius: number = 18;
|
||||
/*
|
||||
* ── 卡片的"玻璃":**白色 + alpha,不做模糊** ──
|
||||
*
|
||||
* ★★ 2026-09-21 更正(用户:「邮件看着没有玻璃效果」,我实测后才发现自己搞错了)。
|
||||
*
|
||||
* WebUI 的 `.glass-card`(`index.css:1629`)**根本没有 `backdrop-filter`**:
|
||||
*
|
||||
* .glass-card { background-color: rgb(255 255 255 / 0.92); }
|
||||
* html[data-bg='on'] … { background-color: rgb(255 255 255 / 0.78); }
|
||||
* ↑ 就是"白 + alpha",没有 blur
|
||||
*
|
||||
* 全仓 `backdrop-filter` **只出现两处**(`index.css:844` 与 `:1204`):
|
||||
* · `.glass-control` —— 控件档,`blur(8px) saturate(1.1)`
|
||||
* · `.narrow-nav` —— **底部导航条**,`blur(18px) saturate(1.5)`
|
||||
*
|
||||
* 而我给**每张卡片**都挂了 `blur(18px) saturate(1.5)` —— 用的是**导航条那档**参数。
|
||||
* 这正是 `index.css:990` 明确警告的做法:
|
||||
* 「再 backdrop-filter 一次纯属叠加:不会更"玻璃",只会更脏更糊,
|
||||
* 而且每层都要重新采样一次背景(滚动时明显掉帧)」
|
||||
*
|
||||
* ⇒ 后果:列表里每张卡各糊一次,多层叠加后正文发灰、滚动掉帧 ——
|
||||
* 比"没有玻璃"更糟。修法就是照 WebUI:**卡片只用白 + alpha**。
|
||||
*/
|
||||
/*
|
||||
* 卡片的白 + alpha(ArkUI 的 `#AARRGGBB`:前两位是 alpha)。
|
||||
*
|
||||
* glassCard #EBFFFFFF ≈ 白 / 0.92 ← WebUI `--glass-card-a: 0.92`
|
||||
* glassCardWall #C7FFFFFF ≈ 白 / 0.78 ← WebUI `--glass-card-wall-a: 0.78`
|
||||
*
|
||||
* ★ 为什么必须**显式写 alpha**而不是像以前那样用 `Color.Transparent`:
|
||||
* 卡片在壁纸上要是一层"薄白纱"(壁纸透上来但被压淡),
|
||||
* 全透明等于壁纸原样穿过 —— 卡片的边界没了,文字也没有底衬(可读性掉)。
|
||||
* WebUI 的 0.78 就是在这两者之间取的平衡值。
|
||||
*
|
||||
* ★ 深浅两色同一个值:因为**基色恒为白**(WebUI `--glass-base` 注释:
|
||||
* 「基材恒为白,主题之间只差 alpha」)。深色下靠 `brightness` 与
|
||||
* 文字令牌保证对比度,而不是把底也调黑。
|
||||
*/
|
||||
static readonly glassCard: string = '#EBFFFFFF';
|
||||
static readonly glassCardWall: string = '#C7FFFFFF';
|
||||
static readonly glassCardSaturation: number = 1.5;
|
||||
/**
|
||||
* 「悬浮玻璃板」的另外两半:**投影 + 发丝描边**。
|
||||
|
||||
@ -322,7 +322,18 @@ export function navBadgeText(n: number): string {
|
||||
* "同一条轴线上的两种条"就变成肉眼可见的不齐 —— 而这正是本仓反复出现的那类毛病
|
||||
* (同一个东西两处各写一个数,然后慢慢分叉)。
|
||||
*/
|
||||
export const TAB_BAR_RADIUS: number = NAV_BAR_RADIUS;
|
||||
/*
|
||||
* 同上:页签条的高是 `TAB_BAR_HEIGHT`(44),不是 `NAV_BAR_HEIGHT`(56) ——
|
||||
* 半径必须按**自己**的高算,照抄底栏的 28 会超出一半(28/44 = 63.6%)。
|
||||
*
|
||||
* ★ 用 `TAB_BAR_HEIGHT` 而不是 `HEADER_HEIGHT`:两者当前同值(44),
|
||||
* 但**语义不同**(一个是页签条、一个是页标题栏)。写 `HEADER_HEIGHT`
|
||||
* 会引入一条跨语义的隐式依赖 —— 哪天标题栏改成 48,页签条会跟着变,
|
||||
* 而它明明没理由跟着变。(这条是编译器帮我发现的:声明顺序在前,
|
||||
* 直接引 `HEADER_HEIGHT` 报 "used before its declaration"。)
|
||||
*/
|
||||
export const TAB_BAR_HEIGHT: number = 44;
|
||||
export const TAB_BAR_RADIUS: number = TAB_BAR_HEIGHT / 2;
|
||||
export const TAB_BAR_SIDE: number = NAV_BAR_SIDE;
|
||||
/** 顶部留白(vp):离内容区顶部一点点,做成"浮着"而不是"贴着" */
|
||||
export const TAB_BAR_TOP: number = 8;
|
||||
@ -363,7 +374,24 @@ export const TAB_BAR_TOP: number = 8;
|
||||
* 44 同时是本仓的可点区下限(`NAV_ITEM_MIN_HIT`),返回键装得下。
|
||||
*/
|
||||
export const HEADER_HEIGHT: number = 44;
|
||||
export const HEADER_RADIUS: number = NAV_BAR_RADIUS;
|
||||
/*
|
||||
* ★★ 2026-09-21 修:**圆角必须跟高度走,不能跟另一条条的半径走**
|
||||
* (用户:「你看看顶栏右边那个圆角,你不觉得奇怪吗?」)。
|
||||
*
|
||||
* 这里原来写的是 `= NAV_BAR_RADIUS`(28)。而顶栏高度是 44 ——
|
||||
*
|
||||
* 底栏:高 56,半径 28 ⇒ 28/56 = 50% ✓ 标准的药丸形
|
||||
* 顶栏:高 44,半径 28 ⇒ 28/44 = **63.6%** ✗ 半径**超过高度的一半**
|
||||
*
|
||||
* 半径大于高度一半时,左右两条圆弧会**互相压过去**:渲染出来不是两个圆角,
|
||||
* 而是一块被斜切掉两翼的怪形 —— 顶端只剩很窄一段直的,看着就像"没对齐"。
|
||||
*
|
||||
* ★ 我当初的错法在 `HEADER_HEIGHT` 那段注释里写着:「侧留白/圆角仍复用 `NAV_BAR_*`,
|
||||
* 高度独立取 44」。**留白可以复用(它表达的是与屏幕边缘的距离,与条高无关),
|
||||
* 但圆角不行** —— 圆角的形状由**自身高度**决定。把两者归成"都复用",
|
||||
* 就是把"横向的留白"与"纵向的形状"当成了同一类东西。
|
||||
*/
|
||||
export const HEADER_RADIUS: number = HEADER_HEIGHT / 2;
|
||||
export const HEADER_SIDE: number = NAV_BAR_SIDE;
|
||||
/** 顶栏离内容区顶部的留白(vp)—— 与 `TAB_BAR_TOP` 同值,同一族 */
|
||||
export const HEADER_TOP: number = TAB_BAR_TOP;
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
*/
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AppHeader } from '../common/Surface';
|
||||
import { AppHeader, PressEffectModifier } from '../common/Surface';
|
||||
import { AdminApi, MeApi } from '../api/AdminApi';
|
||||
import {
|
||||
AdminUser,
|
||||
@ -371,6 +371,7 @@ struct AdminUsersPage {
|
||||
}
|
||||
.width(44)
|
||||
.height(44)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.load(); })
|
||||
}
|
||||
|
||||
@ -450,6 +451,7 @@ struct AdminUsersPage {
|
||||
.padding(12)
|
||||
.backgroundColor(Theme.surface)
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.toggleEdit(user); })
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ import { CalendarApi } from '../api/CalendarApi';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { CalendarEvent, CalendarEventInput, LunarLabels, LunarRangeResponse } from '../model/Models';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { GlassCardModifier, PaneModifier } from '../common/Surface';
|
||||
import { GlassCardModifier, PaneModifier, PressEffectModifier } from '../common/Surface';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import {
|
||||
DayCell,
|
||||
@ -69,6 +69,39 @@ import { pickIcsText, saveIcsText } from '../common/IcsFile';
|
||||
|
||||
/** 一格的高度(vp)。7 列等分 + 固定高度才画得成整齐的网格 */
|
||||
const CELL_HEIGHT: number = 52;
|
||||
|
||||
/**
|
||||
* 周/日视图的格子高度(vp)。
|
||||
*
|
||||
* ★★ 2026-09-21 新增(用户:「我说的是周视图和日视图」)。
|
||||
*
|
||||
* 周视图在 WebUI 里是**七列高格子**(`WeekGrid` 的 `min-h-[28rem]` ≈ 448px)——
|
||||
* 它的用途就是"一周七天各自摊开、把每天的日程标题都看见"。
|
||||
* 我们原来周/日档复用月视图的 `CELL_HEIGHT`(52vp),
|
||||
* 于是"周视图"只是"月视图的一行":格子矮到放不下任何标题,
|
||||
* 也就只能画一个"有日程"的小圆点 —— 那正是用户看到"和 WebUI 不一样"的地方。
|
||||
*
|
||||
* 取 240vp:够放 2–3 条日程条(每条 ~20vp),
|
||||
* 又不至于让七列在窄屏上横向挤到装不下(下面 `WEEK_MIN_WIDTH` 管横向)。
|
||||
*/
|
||||
const WEEK_CELL_HEIGHT: number = 240;
|
||||
|
||||
/** 周/日网格的最小可读宽度(vp):七列 × 每天至少 46vp —— 不够就横向滚动 */
|
||||
const WEEK_MIN_WIDTH: number = 322;
|
||||
|
||||
/**
|
||||
* 日视图的 24 小时(`0..23`)—— 对齐 WebUI `DayGrid` 的
|
||||
* `Array.from({length: 24}, (_, i) => i)`。
|
||||
*
|
||||
* ★★ 2026-09-21(用户:「我说的是周视图和日视图」)。
|
||||
*
|
||||
* 改之前:日视图被我做成"周视图只留一天"(七个空列,只有一列有内容)——
|
||||
* 那**不是**日视图。WebUI 的 `DayGrid` 是一条**24 小时时间轴**:
|
||||
* 每小时一行,左边 `HH:00`,右边是该小时内的日程;
|
||||
* 当前小时的那一行加淡蓝底(`bg-blue-50/40`)。
|
||||
* 一天有 24 小时,而这个信息在"七列网格"里根本表达不出来。
|
||||
*/
|
||||
const DAY_HOURS: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
|
||||
/**
|
||||
* 重复规则与提前量的**可选项**。
|
||||
*
|
||||
@ -626,33 +659,66 @@ export struct CalendarPage {
|
||||
return `${this.calScale}-${this.year}-${this.month}-${this.selectedIso}`;
|
||||
}
|
||||
|
||||
/** 这一天有几条日程(画事件点用)。数量很小,线性扫即可,不引入第二份索引。 */
|
||||
/** 这一天有几条日程。**复用 `eventsOf()`** —— 一处算法,避免两处慢慢漂开。 */
|
||||
private eventCountOf(iso: string): number {
|
||||
if (iso.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
let n: number = 0;
|
||||
for (let i = 0; i < this.events.length; i++) {
|
||||
if (localIsoOf(this.events[i].event_time) === iso) {
|
||||
n = n + 1;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
return this.eventsOf(iso).length;
|
||||
}
|
||||
|
||||
/** 选中那天的日程,按时间升序 */
|
||||
private selectedEvents(): CalendarEvent[] {
|
||||
/**
|
||||
* 某一天的日程列表(按时间升序)。
|
||||
*
|
||||
* ★★ 2026-09-21 新增(用户:「我说的是周视图和日视图」)。
|
||||
*
|
||||
* 原来只有一个 `eventCountOf()` —— 格子里画一个 **4×4 的小圆点**表示"这天有日程"。
|
||||
* 而 WebUI 的月/周两个网格画的都是**每条日程一个标题条**
|
||||
* (`EventChip`,`CalendarView.tsx:515/604` 的 `MonthGrid`/`WeekGrid`):
|
||||
*
|
||||
* <EventChip e={e} /> ← 蓝底小条:时间 + 标题(暂停的加删除线)
|
||||
*
|
||||
* 差别不是"圆点 vs 小条"这么小:
|
||||
* · 圆点只告诉你**有没有**,不告诉你**是什么** —— 用户要看到"15:00 评审";
|
||||
* · 周视图在 WebUI 里是**七列宽格子**(`min-h-[28rem]`),本来就是给
|
||||
* 日程标题留的地方;我们那版周视图每格只有 52vp 高(`CELL_HEIGHT`),
|
||||
* 连格子都放不下,更别说标题 —— 这才是"周/日视图和 WebUI 不一样"的根。
|
||||
*
|
||||
* 数据不另建索引:`events` 本来就不大(一次一个月),线性扫与
|
||||
* `eventCountOf` 同一口径(那个函数现在也改成调这个,免得两处算法漂移)。
|
||||
*/
|
||||
private eventsOf(iso: string): CalendarEvent[] {
|
||||
const out: CalendarEvent[] = [];
|
||||
if (iso.length === 0) {
|
||||
return out;
|
||||
}
|
||||
for (let i = 0; i < this.events.length; i++) {
|
||||
const e: CalendarEvent = this.events[i];
|
||||
if (localIsoOf(e.event_time) === this.selectedIso) {
|
||||
out.push(e);
|
||||
if (localIsoOf(this.events[i].event_time) === iso) {
|
||||
out.push(this.events[i]);
|
||||
}
|
||||
}
|
||||
out.sort((a: CalendarEvent, b: CalendarEvent) => Date.parse(a.event_time) - Date.parse(b.event_time));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `HH:mm` —— 与 WebUI `hhmm()` 同口径(取本地时间,两位补零) */
|
||||
private hhmm(iso: string): string {
|
||||
const d: Date = new Date(iso);
|
||||
if (isNaN(d.getTime())) {
|
||||
return '';
|
||||
}
|
||||
const h: number = d.getHours();
|
||||
const m: number = d.getMinutes();
|
||||
return `${h < 10 ? '0' : ''}${h}:${m < 10 ? '0' : ''}${m}`;
|
||||
}
|
||||
|
||||
/** 周/日视图才在格子里画**日程标题条**;月视图格子矮,只画"有日程"的点 */
|
||||
private cellShowsChips(): boolean {
|
||||
return this.calScale !== 'month';
|
||||
}
|
||||
|
||||
/** 选中那天的日程,按时间升序 */
|
||||
private selectedEvents(): CalendarEvent[] {
|
||||
return this.eventsOf(this.selectedIso);
|
||||
}
|
||||
|
||||
private cellFg(cell: DayCell): ResourceColor {
|
||||
if (cell.iso === this.selectedIso) {
|
||||
return Theme.accentFg;
|
||||
@ -844,40 +910,146 @@ export struct CalendarPage {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* **日程条** —— WebUI `EventChip` 的对应物(`CalendarView.tsx` 的 `EventChip`)。
|
||||
*
|
||||
* WebUI: bg-blue-50 text-blue-800,内容 = `HH:mm` + 标题,`truncate`
|
||||
* 暂停/取消 → `bg-gray-100 text-gray-400 line-through`
|
||||
*
|
||||
* ★ 生死态(`status !== 'active'`)必须**看得出来**:
|
||||
* WebUI 那段注释写得很直白 ——「暂停/取消的事件不会触发提醒,
|
||||
* 视觉上必须与生效的区分开,否则人以为设好了,实际到点什么都不会发生」。
|
||||
* 所以照它做:灰底 + 删除线,而不是"只是颜色淡一点"。
|
||||
*
|
||||
* ★ 点击要 `stopPropagation` 的效果:WebUI 里点事件条是"选中这条日程",
|
||||
* 不会连带触发"选中这一天"。ArkUI 没有事件冒泡语义区分得那么细,
|
||||
* 所以这里**只给事件条自己挂 onClick**,不额外向外传(格子自己的
|
||||
* `onClick` 仍会收到点击 —— 这与 WebUI 的 `ev.stopPropagation()` 有差异,
|
||||
* 但两件事的结果一致:**都是"选中这一天 + 选中这条日程"**,
|
||||
* 而 WebUI 的 `pickEvent` 本身也会把 `selectedDay` 设成那天
|
||||
* (见 `CalendarView.tsx` 的 `pickEvent`)。所以不必伪造冒泡控制。)
|
||||
*/
|
||||
@Builder
|
||||
EventChip(ev: CalendarEvent) {
|
||||
Row({ space: 4 }) {
|
||||
/* 已停用:先给一个"暂停"记号(WebUI 是 PauseIcon,我们用同族图标) */
|
||||
if (ev.status !== 'active') {
|
||||
AmIcon({ iconName: 'pause', iconSize: 9, iconColor: Theme.textSubtleFor() })
|
||||
}
|
||||
Text(this.hhmm(ev.event_time))
|
||||
.fontSize(9)
|
||||
.fontColor(ev.status === 'active' ? Theme.accentFor() : Theme.textSubtleFor())
|
||||
.opacity(0.75)
|
||||
Text(ev.title)
|
||||
.fontSize(9)
|
||||
.fontColor(ev.status === 'active' ? Theme.accentFor() : Theme.textSubtleFor())
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
.decoration(ev.status === 'active'
|
||||
? { type: TextDecorationType.None }
|
||||
: { type: TextDecorationType.LineThrough })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
.borderRadius(4)
|
||||
.backgroundColor(ev.status === 'active' ? Theme.accentSoftFor(this.isDarkNow) : Theme.chipNeutralBg)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
/* 与 WebUI 同构:先把选中日挪到那天,再开编辑面板 */
|
||||
this.selectedIso = localIsoOf(ev.event_time);
|
||||
this.openEdit(ev);
|
||||
})
|
||||
}
|
||||
|
||||
@Builder
|
||||
DayCellView(cell: DayCell) {
|
||||
Column() {
|
||||
if (cell.label > 0) {
|
||||
Text(`${cell.label}`)
|
||||
.fontSize(Theme.fontBody)
|
||||
.fontWeight(cell.iso === this.todayIso ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(this.cellFg(cell))
|
||||
/*
|
||||
* 农历小字 —— 与 WebUI 同一位置(公历数字下方一行)。
|
||||
*
|
||||
* ★ 文案直接用服务端给的 `text`(初一=月名、其余=日名),
|
||||
* 客户端**不自己拼**:两端各拼一份的话,同一天在两边日历上
|
||||
* 可能长得不一样(例如闰月到底写不写「闰」)。
|
||||
* ★ 拉不到农历时(服务端抖了一下/未登录)这里就是空字串,
|
||||
* 格子少一行小字、日历照常用 —— 不报错、不留空白占位。
|
||||
*/
|
||||
Text(this.lunarTextOf(cell.iso))
|
||||
.fontSize(9)
|
||||
/*
|
||||
* ── 日期头 ──
|
||||
*
|
||||
* 周/日视图里它是**独立一条**(WebUI 的 `sticky top-0` 那行):
|
||||
* 「今天」的淡蓝底、「选中」的下边框,都只落在这条上,**不铺满整列**。
|
||||
*
|
||||
* ★★ 2026-09-21:我第一版把月视图的「整格铺底色」原样留给高格子,
|
||||
* 结果周视图里今天那一列从顶蓝到底,变成一根蓝柱子
|
||||
* (实测截图:日程条被底色吃掉,几乎看不清)。
|
||||
* WebUI 的周视图是分开处理的(`CalendarView.tsx:639-651`)——
|
||||
* 底色在 `border-b` 那条 header 上,下面的事件区是白的。
|
||||
*/
|
||||
Column() {
|
||||
if (cell.label > 0) {
|
||||
Text(`${cell.label}`)
|
||||
.fontSize(Theme.fontBody)
|
||||
.fontWeight(cell.iso === this.todayIso ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(this.cellFg(cell))
|
||||
/*
|
||||
* ★ ★ 2026-09-19 改:`textSubtle`(系统三级文字)在**深色**下太暗 ——
|
||||
* 设备实测农历"二十"只有 **2.49:1**(`rgb(93,95,96)` 底上)。
|
||||
* 农历小字 —— 与 WebUI 同一位置(公历数字下方一行)。
|
||||
*
|
||||
* WebUI 用的是 `text-gray-500`(`CalendarView.tsx` 农历那行),
|
||||
* 而它在 `.dark` 段被**提亮**到 `165 173 186`(见 `index.css`)——
|
||||
* 正是 `--c-gray-*` 那组注释说的"深底上的浅色 gray-400 只有约 2:1,
|
||||
* 远低于 WCAG AA,看得见但读不动"。
|
||||
*
|
||||
* 系统三级色没有这层适配(它只管"比二级更淡",不管可读性下限),
|
||||
* ⇒ 深色下用它当**小字**会掉到 2.5:1。改用二级(`textMuted`),
|
||||
* 与 WebUI 的 gray-500 档位一致。
|
||||
* ★ 文案直接用服务端给的 `text`(初一=月名、其余=日名),
|
||||
* 客户端**不自己拼**:两端各拼一份的话,同一天在两边日历上
|
||||
* 可能长得不一样(例如闰月到底写不写「闰」)。
|
||||
* ★ 拉不到农历时(服务端抖了一下/未登录)这里就是空字串,
|
||||
* 格子少一行小字、日历照常用 —— 不报错、不留空白占位。
|
||||
*/
|
||||
.fontColor(cell.iso === this.selectedIso ? Theme.accentFg : Theme.textMuted)
|
||||
.maxLines(1)
|
||||
Text(this.lunarTextOf(cell.iso))
|
||||
.fontSize(9)
|
||||
/*
|
||||
* ★★ 2026-09-19 改:`textSubtle`(系统三级文字)在**深色**下太暗 ——
|
||||
* 设备实测农历"二十"只有 **2.49:1**(`rgb(93,95,96)` 底上)。
|
||||
* WebUI 用的是 `text-gray-500`,而它在 `.dark` 段被提亮到 `165 173 186`。
|
||||
* 系统三级色没有这层适配 ⇒ 深色下当小字会掉到 2.5:1。
|
||||
* 改用二级(`textMuted`),与 WebUI 的 gray-500 档位一致。
|
||||
*/
|
||||
.fontColor(cell.iso === this.selectedIso ? Theme.accentFg : Theme.textMuted)
|
||||
.maxLines(1)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
/*
|
||||
* 日期头底色:只在周/日档铺(月档由格子那层铺,保持原样)。
|
||||
* 「今天」淡蓝;「选中」同色 —— 两者可以同时成立,靠下面的下边框区分。
|
||||
*/
|
||||
.backgroundColor(this.cellShowsChips()
|
||||
? (cell.iso === this.todayIso || cell.iso === this.selectedIso
|
||||
? Theme.accentSoftFor(this.isDarkNow)
|
||||
: Theme.surface)
|
||||
: Color.Transparent)
|
||||
/*
|
||||
* 「选中」用**下边框**说,不用整格底色。
|
||||
* 不选中时给宽度 0(而不是去掉 `.border()` 调用):条件属性换来换去
|
||||
* 会让节点重建,宽 0 的边框是稳定的。
|
||||
*/
|
||||
.border({
|
||||
width: { bottom: this.cellShowsChips() && cell.iso === this.selectedIso ? 2 : 0 },
|
||||
color: Theme.accent
|
||||
})
|
||||
.padding({ top: 4, bottom: 4 })
|
||||
|
||||
/*
|
||||
* ── 日程条(WebUI `EventChip` 的对应物)──
|
||||
*
|
||||
* ★★ 2026-09-21 用**标题条**换掉原来的 4×4 小圆点
|
||||
* (用户:「我说的是周视图和日视图」)。
|
||||
*
|
||||
* 原来只画一个点:告诉你"这天有日程",却**不告诉是什么**。
|
||||
* WebUI 的 `MonthGrid`/`WeekGrid` 都是每条日程一行小条(`EventChip`):
|
||||
* bg-blue-50 text-blue-800,内容 = `HH:mm` + 标题
|
||||
* 暂停/取消的加 `line-through` + 灰底(**必须一眼看出它不会响**)
|
||||
*
|
||||
* 月视图格子只有 `CELL_HEIGHT`(52vp) ⇒ 放不下标题,**保留圆点**;
|
||||
* 周/日视图格子高(`WEEK_CELL_HEIGHT`)⇒ 画标题条。
|
||||
*/
|
||||
if (this.cellShowsChips() && cell.label > 0) {
|
||||
Column({ space: 2 }) {
|
||||
ForEach(this.eventsOf(cell.iso), (ev: CalendarEvent) => {
|
||||
this.EventChip(ev)
|
||||
}, (ev: CalendarEvent) => ev.event_id)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 2, right: 2, top: 3 })
|
||||
} else if (cell.label > 0) {
|
||||
/* 月视图:只报"有日程"(那个尺寸下的极限) */
|
||||
if (this.eventCountOf(cell.iso) > 0) {
|
||||
Column()
|
||||
.width(4)
|
||||
@ -889,12 +1061,34 @@ export struct CalendarPage {
|
||||
}
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.height(CELL_HEIGHT)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
/* 选中态用**底色 + 文字反白**(日历里的"选中某天"与导航条的"选中某项"不是同一件事:
|
||||
导航项上色即可,因为文字本身就在报位置;格子里只有一个数字,没有底色看不出选的是哪格) */
|
||||
.backgroundColor(cell.iso === this.selectedIso ? Theme.accent : Color.Transparent)
|
||||
.borderRadius(Theme.radiusControl)
|
||||
/*
|
||||
* 高度随档位变:月视图紧凑格(52vp),周/日视图高格(240vp)。
|
||||
* 周/日那种高度才放得下日程**标题条**(见 `cellShowsChips`)。
|
||||
*/
|
||||
/*
|
||||
* 周/日视图:格子吃满**行**的高度(`layoutWeight` 已在 `.layoutWeight(1)` 上给了
|
||||
* 横向;这里纵向用 `height('100%')` 让七列等高撑满),
|
||||
* 行本身再由网格的 `layoutWeight(1)` 撑满窗格。
|
||||
* 月视图仍是固定 `CELL_HEIGHT`(44 行网格不能被拉伸)。
|
||||
*/
|
||||
.height(this.cellShowsChips() ? '100%' : CELL_HEIGHT)
|
||||
/*
|
||||
* 有标题条时内容从**顶部**排(月视图那一个数字才需要垂直居中);
|
||||
* 否则周视图的日期会被推到 240vp 格子的正中间,底下留一大片空白。
|
||||
*/
|
||||
.justifyContent(this.cellShowsChips() ? FlexAlign.Start : FlexAlign.Center)
|
||||
/*
|
||||
* 选中/今天的底色:**只给月视图整格**。
|
||||
* 月视图格子只有一个数字,没有别的地方可标;
|
||||
* 周/日视图改由上面的日期头承担(见那段注释)。
|
||||
*/
|
||||
.backgroundColor(this.cellShowsChips()
|
||||
? Color.Transparent
|
||||
: (cell.iso === this.selectedIso ? Theme.accent : Color.Transparent))
|
||||
.borderRadius(this.cellShowsChips() ? 0 : Theme.radiusControl)
|
||||
/* 周/日:列间分隔线(WebUI 是 `border-r border-gray-100`),否则七列糊成一片 */
|
||||
.border({ width: { right: this.cellShowsChips() ? 1 : 0 }, color: Theme.border })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
if (cell.label > 0) {
|
||||
this.selectedIso = cell.iso;
|
||||
@ -902,6 +1096,27 @@ export struct CalendarPage {
|
||||
})
|
||||
}
|
||||
|
||||
/** 日视图:某小时的日程(按时间升序)。与 WebUI `DayGrid` 的 `byHour` 同口径。 */
|
||||
private hourEvents(hour: number): CalendarEvent[] {
|
||||
const out: CalendarEvent[] = [];
|
||||
const list: CalendarEvent[] = this.eventsOf(this.selectedIso);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const d: Date = new Date(list[i].event_time);
|
||||
if (!isNaN(d.getTime()) && d.getHours() === hour) {
|
||||
out.push(list[i]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前是几点 —— 只在这一天就是今天时有意义,否则返回 -1。
|
||||
* WebUI 的 `DayGrid` 是 `isSameDay(anchor, new Date()) ? new Date().getHours() : -1`。
|
||||
*/
|
||||
private nowHour(): number {
|
||||
return this.selectedIso === this.todayIso ? new Date().getHours() : -1;
|
||||
}
|
||||
|
||||
/** 某个格子的农历文案(取不到就是空字串 —— 不占位、不报错) */
|
||||
private lunarTextOf(iso: string): string {
|
||||
if (iso.length === 0 || this.lunarMap === undefined) {
|
||||
@ -946,6 +1161,7 @@ export struct CalendarPage {
|
||||
.padding(10)
|
||||
.backgroundColor(Theme.surfaceMuted)
|
||||
.borderRadius(Theme.radiusControl)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => this.openEdit(e))
|
||||
}
|
||||
|
||||
@ -1013,6 +1229,7 @@ export struct CalendarPage {
|
||||
.padding(10)
|
||||
.backgroundColor(Theme.chipNeutralBg)
|
||||
.borderRadius(Theme.radiusControl)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
this.pickersOpen = !this.pickersOpen;
|
||||
})
|
||||
@ -1136,6 +1353,12 @@ export struct CalendarPage {
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
/*
|
||||
* ★★ 2026-09-21:高度受限的 Scroll 必须显式顶部对齐。
|
||||
* 官方 FAQ 原文:内容比 Scroll 矮时「**默认居中排布**」,要 `.align(Alignment.Top)`
|
||||
* 才是顶部排布(`faqs-arkui-725`)。缺这一行时日历的日程列表会飘在中间。
|
||||
*/
|
||||
.align(Alignment.Top)
|
||||
/* 上下边缘渐隐(2026-09-17 补:这一处原先漏了,同一次滚动里只有列表是渐隐) */
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
}
|
||||
@ -1155,6 +1378,104 @@ export struct CalendarPage {
|
||||
* 现在按 WebUI 拆成两个 @Builder,由 `build()` 按可用宽度决定并排还是竖堆
|
||||
* (窄屏仍竖堆 —— WebUI 窄屏走 `NarrowStack` 覆盖式,见 `CalendarView.tsx:444-447`)。
|
||||
*/
|
||||
/**
|
||||
* **日视图** —— 24 小时时间轴(对齐 WebUI `DayGrid`,`CalendarView.tsx:669`)。
|
||||
*
|
||||
* ★★ 2026-09-21 新增(用户:「我说的是周视图和日视图」)。
|
||||
*
|
||||
* 改之前日视图走的是"周视图只留一天"(七个列、六个空),
|
||||
* 那条路**表达不出"几点有事"** —— 而这个恰恰是日视图唯一要回答的问题。
|
||||
*
|
||||
* WebUI 的形状(逐项对齐):
|
||||
* <div className="px-3 py-2 text-xs …"> ← 日期 + 农历那一行
|
||||
* <div className="divide-y">
|
||||
* 每小时一行:
|
||||
* <span className="w-10 …">HH:00</span>
|
||||
* 有日程 → 逐个 EventRow;没有 → 一个淡「—」
|
||||
* 当前小时那一行 `bg-blue-50/40`
|
||||
*/
|
||||
@Builder
|
||||
DayTimeline() {
|
||||
Column() {
|
||||
/* 顶部:日期 + 农历(WebUI 那条 `border-b` 行) */
|
||||
/*
|
||||
* 日期 + 农历:与 `AgendaPane` 顶部同一口径(那里也只写 ISO + 农历),
|
||||
* 不引入第二套格式化 —— 两端对同一天要说同样的话。
|
||||
*/
|
||||
Text(this.selectedIso + (this.lunarTextOf(this.selectedIso).length > 0
|
||||
? '(农历' + this.lunarTextOf(this.selectedIso) + ')' : ''))
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(Theme.textMuted)
|
||||
.width('100%')
|
||||
.padding({ left: 12, right: 12, top: 8, bottom: 6 })
|
||||
|
||||
List() {
|
||||
ForEach(DAY_HOURS, (h: number) => {
|
||||
ListItem() {
|
||||
Row() {
|
||||
/* 左列时间刻度:等宽数字,两位补零 */
|
||||
Text(`${h < 10 ? '0' : ''}${h}:00`)
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(Theme.textSubtleFor())
|
||||
.width(48)
|
||||
.textAlign(TextAlign.End)
|
||||
.padding({ top: 2 })
|
||||
|
||||
Column({ space: 4 }) {
|
||||
if (this.hourEvents(h).length === 0) {
|
||||
/*
|
||||
* 空小时:一条极淡的破折号(WebUI `CalendarView.tsx:721` 的 `text-gray-200`)。
|
||||
*
|
||||
* ★ 2026-09-21:这里我原先写的是 `Theme.border` ——
|
||||
* 它是 `sys.color.ohos_id_color_list**Separator**`,是**面/线**的令牌,
|
||||
* 会随主题翻转(浅色近白 / 深色近黑)。
|
||||
* 判据当场抓到(`cross-client-theme` 的「面当字」那条):
|
||||
* 深色下它当字就掉到看不见。
|
||||
* 改用**三级文字**令牌 —— 语义就是"最不抢眼的一档小字",
|
||||
* 而且它已有主题适配(`textSubtleFor`,本仓既有纪律:面/字分开)。
|
||||
*/
|
||||
Text('—')
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(Theme.textSubtleFor(this.isDarkNow))
|
||||
} else {
|
||||
ForEach(this.hourEvents(h), (ev: CalendarEvent) => {
|
||||
this.EventRow(ev)
|
||||
}, (ev: CalendarEvent) => `h${h}#${ev.event_id}`)
|
||||
}
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({
|
||||
left: 12,
|
||||
right: 12,
|
||||
/* 有日程的行高一点(WebUI:`list.length ? py-2 : py-1`) */
|
||||
top: this.hourEvents(h).length > 0 ? 8 : 4,
|
||||
bottom: this.hourEvents(h).length > 0 ? 8 : 4
|
||||
})
|
||||
/* 当前小时那一行加淡蓝底(WebUI `bg-blue-50/40`) */
|
||||
.backgroundColor(h === this.nowHour() ? Theme.accentSoftFor(this.isDarkNow) : Color.Transparent)
|
||||
}
|
||||
}, (h: number) => `hr${this.selectedIso}#${h}`)
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
.align(Alignment.Top)
|
||||
/*
|
||||
* 上下边缘渐隐 —— 与其它滚动容器同一口径
|
||||
* (WebUI `.overflow-y-auto` 的 `mask-image`,`index.css:1717`)。
|
||||
* 时刻表滚到一半时,首尾那两行不该被"一刀切"。
|
||||
*/
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
/* 让出底部悬浮导航条(内容末尾留白,不是容器 padding) */
|
||||
.contentEndOffset(this.navReserve)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
|
||||
@Builder
|
||||
GridPane() {
|
||||
/*
|
||||
@ -1276,6 +1597,7 @@ export struct CalendarPage {
|
||||
.backgroundColor(Theme.accent)
|
||||
.borderRadius(Theme.radiusControl)
|
||||
.margin({ left: 8 })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => this.openNew())
|
||||
}
|
||||
.width('100%')
|
||||
@ -1294,6 +1616,18 @@ export struct CalendarPage {
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
/*
|
||||
* ★★ 2026-09-21:**日视图走 24 小时时间轴,不再复用七列网格**
|
||||
* (用户:「你的日视图为什么和周视图一样」)。
|
||||
*
|
||||
* 我上一版把日视图实现成"周视图只留一天"(七列里六列空)——
|
||||
* 那条路**表达不出"几点有事"**,而那正是日视图唯一要回答的问题。
|
||||
* WebUI 的 `DayGrid`(`CalendarView.tsx:669`)是一条 24 小时时间轴,
|
||||
* 所以这里也按 scale 分流。
|
||||
*/
|
||||
if (this.calScale === 'day') {
|
||||
this.DayTimeline()
|
||||
} else {
|
||||
/*
|
||||
* 月网格:行 ← monthGrid,列 ← 7 格(嵌套 ForEach 的项名必须不同)。
|
||||
*
|
||||
@ -1310,10 +1644,29 @@ export struct CalendarPage {
|
||||
}, (cell: DayCell) => `c${cell.iso}#${cell.label}`)
|
||||
}
|
||||
.width('100%')
|
||||
/*
|
||||
* 周/日视图:整行给一个**最小宽度**,不够就靠外层 `Scroll` 横滚。
|
||||
*
|
||||
* 理由与 WebUI 同一个:`WeekGrid` 的内层是
|
||||
* `grid grid-cols-7 min-w-[36rem]`(576px)放在 `overflow-auto` 里 ——
|
||||
* 七列各自要有足够宽度才读得下日程标题,窄屏时宁可横向滚动,
|
||||
* 也不把七列压成七条缝。
|
||||
*/
|
||||
.constraintSize(this.cellShowsChips() ? { minWidth: WEEK_MIN_WIDTH } : {})
|
||||
}, (row: DayCell[], rowIndex: number) => `r${this.monthKey()}#${rowIndex}`)
|
||||
}
|
||||
.width('100%')
|
||||
/*
|
||||
* 周/日视图:让网格**吃掉**工具栏以下的所有剩余高度
|
||||
* (`layoutWeight(1)`),并把整块放进可横滚的 `Scroll` 里
|
||||
* —— 与 WebUI `WeekGrid` 的 `flex-1 min-h-0 overflow-auto` 同构。
|
||||
*
|
||||
* 不加这两句时,网格只有内容高度(实测 y 止于 1170,
|
||||
* 而窗格到 2230)⇒ 周视图下方空出大半屏,看起来"没做满"。
|
||||
*/
|
||||
.layoutWeight(this.cellShowsChips() ? 1 : 0)
|
||||
.transition(Theme.calendarSlide(this.slideForward))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,6 +13,8 @@ import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { picker } from '@kit.CoreFileKit';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets';
|
||||
import { PressEffectModifier } from '../common/Surface';
|
||||
import { Motion } from '../common/Motion';
|
||||
|
||||
/**
|
||||
* 写邮件窗格。
|
||||
@ -318,6 +320,8 @@ export struct ComposeView {
|
||||
*/
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtleFor() })
|
||||
.rotate({ angle: this.showAccountPicker ? 90 : 0 })
|
||||
/* 展开/收起箭头平滑旋转(WebUI `transition-transform` 的对应物) */
|
||||
.animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeOutSoft })
|
||||
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
|
||||
}
|
||||
.width('100%').height(40).padding({ left: 12, right: 12 })
|
||||
@ -338,6 +342,7 @@ export struct ComposeView {
|
||||
}
|
||||
.width('100%').height(40).padding({ left: 40, right: 12 })
|
||||
.backgroundColor(this.selectedAccountId === acct.id ? Theme.accentSoftFor(this.isDarkNow) : Theme.surface)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
this.selectedAccountId = acct.id;
|
||||
this.showAccountPicker = false;
|
||||
|
||||
@ -26,6 +26,8 @@ import {
|
||||
formatAddress
|
||||
} from '../model/ReplyTarget';
|
||||
import { Insets, KEY_WINDOW_INSETS, topInset } from '../model/WindowInsets';
|
||||
import { PressEffectModifier } from '../common/Surface';
|
||||
import { Motion } from '../common/Motion';
|
||||
|
||||
/**
|
||||
* 本地时间:与 WebUI `new Date(x).toLocaleString('zh-CN')` 同一口径。
|
||||
@ -724,6 +726,7 @@ export struct MailDetailView {
|
||||
AmIcon({ iconName: 'chevronLeft', iconSize: 20, iconColor: Theme.accentFor() })
|
||||
}
|
||||
.width(HEADER_BACK_HIT).height(HEADER_BACK_HIT)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.goBack(); })
|
||||
|
||||
Row() {
|
||||
@ -750,6 +753,8 @@ export struct MailDetailView {
|
||||
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 16, iconColor: Theme.textSubtleFor() })
|
||||
.rotate({ angle: this.headerOpen ? 90 : 0 })
|
||||
/* 展开/收起箭头平滑旋转(WebUI `transition-transform` 的对应物) */
|
||||
.animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeOutSoft })
|
||||
.margin({ left: 6 })
|
||||
}
|
||||
.layoutWeight(1)
|
||||
@ -768,6 +773,7 @@ export struct MailDetailView {
|
||||
* 点击区从 14px 变成 36vp(实测约 104px),且纵向不贴边。
|
||||
*/
|
||||
.height(36)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.headerOpen = !this.headerOpen; })
|
||||
}
|
||||
.width('100%')
|
||||
@ -806,6 +812,7 @@ export struct MailDetailView {
|
||||
AmIcon({ iconName: 'tree', iconSize: 14, iconColor: Theme.textMuted })
|
||||
Text('对话树').fontSize(12).fontColor(Theme.textMuted)
|
||||
}
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.openThread(); })
|
||||
/*
|
||||
* ★★ 2026-09-20 补:**转发在这里**,不在右下角的球上。
|
||||
@ -826,6 +833,7 @@ export struct MailDetailView {
|
||||
AmIcon({ iconName: 'forward', iconSize: 14, iconColor: Theme.textMuted })
|
||||
Text('转发').fontSize(12).fontColor(Theme.textMuted)
|
||||
}
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.showReplyBox = false; this.showForwardBox = true; })
|
||||
}
|
||||
.width('100%')
|
||||
@ -851,6 +859,7 @@ export struct MailDetailView {
|
||||
.width('100%')
|
||||
.padding({ left: 8, right: 12, top: 8, bottom: 8 })
|
||||
.backgroundColor(Theme.surface)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {})
|
||||
|
||||
Divider().color(Theme.border)
|
||||
@ -1006,6 +1015,7 @@ export struct MailDetailView {
|
||||
.borderRadius(8)
|
||||
.border({ width: 1, color: Theme.border })
|
||||
.margin({ bottom: 6 })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.downloadAttachment(a); })
|
||||
}, (a: AttachmentInfo) => a.attachment_id)
|
||||
}
|
||||
@ -1020,6 +1030,40 @@ export struct MailDetailView {
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
/*
|
||||
* ★★ 2026-09-21 补:**让出底部悬浮导航条**(用户:「你的显示实际信息的部分,
|
||||
* 响应点击的部分,为什么不避让底部导航栏?」)。
|
||||
*
|
||||
* 底栏是**悬浮**的(`NavBar`)——它不占布局空间,所以内容会**从它底下穿过**。
|
||||
* 全局约定是各滚动容器用 `contentEndOffset(navReserve)` 在**内容末尾**留白
|
||||
* (不是给容器加 padding:容器必须满高,内容才滑得到条底下、玻璃才有东西可糊)。
|
||||
*
|
||||
* 这一处**一直漏着**:详情页正文的最后一段(附件区/正文结尾)停在
|
||||
* y≈2230,而底栏占 1957..2232 —— 最后一行**永远压在条下面**,看得见、点不到。
|
||||
* 实测:`Scroll [2,287,1006,2231]` 与 `Column [0,1957,1008,2232]`(底栏)完全重叠。
|
||||
*/
|
||||
.contentEndOffset(this.navReserve)
|
||||
/*
|
||||
* ★★ 2026-09-21 修「邮件正文被垂直居中」(用户:「你家邮件是居中对其啊,
|
||||
* 不都是顶部对齐」)。
|
||||
*
|
||||
* 官方 FAQ 原文(`faqs-arkui-725「如何解决Scroll组件设置高度限制后,
|
||||
* 无法顶部对齐的问题」`):
|
||||
* 「若子组件的布局尺寸小于 Scroll 组件的尺寸时,则 Scroll 组件不可滚动,
|
||||
* **且默认居中排布**。」
|
||||
* 「当设置 Scroll 组件高度后,可以通过设置 **`.align(Alignment.Top)`**
|
||||
* 属性实现 Scroll 子组件顶部排布效果。」
|
||||
*
|
||||
* 这里正是那个形状:`Scroll` 有 `.height('100%')`(高度被限死),
|
||||
* 而短邮件的正文(标题 + 一行 Markdown + Blank)比视口矮得多 ⇒
|
||||
* 实测内容块 `Column [2,1018,…,1499]` 在 `Scroll [2,287,…,2231]` 里
|
||||
* 偏移 731px = (1944−481)/2 —— **正好居中**,顶上一大片空白。
|
||||
*
|
||||
* ⇒ 加 `.align(Alignment.Top)`。不是"内容自己会顶上去",而是
|
||||
* **高度受限的 Scroll 必须显式要求顶部对齐**,否则它替你居中。
|
||||
*/
|
||||
|
||||
.align(Alignment.Top)
|
||||
/*
|
||||
* ★ 上下边缘渐隐(2026-09-17 补)。
|
||||
*
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AppHeader, CompositeModifier, GlassCardModifier, PaneModifier, PressFeedbackModifier } from '../common/Surface';
|
||||
import { AppHeader, CompositeModifier, GlassCardModifier, PaneModifier, PressEffectModifier, TintModifier } from '../common/Surface';
|
||||
import { Motion } from '../common/Motion';
|
||||
import { AmIcon } from '../common/Icons';
|
||||
import { WideSidebar } from './WideSidebar';
|
||||
import { MailDetailView } from './MailDetailPage';
|
||||
@ -96,8 +97,7 @@ import {
|
||||
navBadgeText,
|
||||
navBadgeTone,
|
||||
NavItem,
|
||||
normalizeNavIndex
|
||||
} from '../model/NavItems';
|
||||
normalizeNavIndex, TAB_BAR_HEIGHT } from '../model/NavItems';
|
||||
|
||||
/** 一页取多少封。取满了就要如实提示"可能还有更多"(服务端 total 是未读数,不是总封数)。 */
|
||||
const MAIL_DETAIL_ROUTE: string = 'mail-detail';
|
||||
@ -160,6 +160,25 @@ function DetailPlaceholder() {
|
||||
}
|
||||
.width('100%').height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
/*
|
||||
* ★★ 2026-09-21 补:**宽屏右栏默认页也要透明**(用户:「邮件看着没有玻璃效果」)。
|
||||
*
|
||||
* `splitPlaceholder` 那块内容由 `Navigation` 直接渲染
|
||||
* (布局树里的 `SplitPlaceholderContentNode`,实测 x=1152..3154)。
|
||||
* 不设背景时它用**系统默认底**(近白不透明),于是整片右栏把壁纸挡死 ——
|
||||
* 实测宽屏 3184px 下:右栏中心 `(242,244,244)`,而屏幕边缘的壁纸是
|
||||
* `(192,194,192)`。差这么远,说明中间隔着的不透明层就是它。
|
||||
*
|
||||
* 这与 2026-09-19 修的 `CommPage` 那个 `Navigation` 是**同一个形状**
|
||||
* (见那段注释里"那条缝是决定性的证据")—— 系统容器不设背景就不会透。
|
||||
*
|
||||
* ★ 全局 `@Builder` 里**拿不到 `this`**(`splitPlaceholder` 只接受全局 Builder),
|
||||
* 所以不能用 `bgActive ? Transparent : surface`。
|
||||
* 这里改成**恒透明**:右栏默认页本来就没有"实体面"的语义
|
||||
* (它是一块空态提示),壁纸关着时底下是 `WallpaperLayer` 之外的
|
||||
* `pageBg`,观感仍然是浅色底 —— 不会因为透明变成异色。
|
||||
*/
|
||||
.backgroundColor(Color.Transparent)
|
||||
}
|
||||
|
||||
@Component
|
||||
@ -521,9 +540,11 @@ struct InboxTab {
|
||||
.fontSize(11).fontColor(Theme.textMuted)
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtleFor() })
|
||||
.rotate({ angle: 90 })
|
||||
.animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeOutSoft })
|
||||
.margin({ left: 3 })
|
||||
}
|
||||
.height(32)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.showAccountPicker = !this.showAccountPicker; })
|
||||
}
|
||||
if (this.unread > 0) {
|
||||
@ -573,6 +594,7 @@ struct InboxTab {
|
||||
}
|
||||
.width('100%').height(48).padding({ left: 16 })
|
||||
.backgroundColor(this.accountFilter === acct.id ? Theme.accentSoftFor(this.isDarkNow) : Theme.surface)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
this.accountFilter = acct.id;
|
||||
this.accountName = acct.displayName;
|
||||
@ -626,15 +648,50 @@ struct InboxTab {
|
||||
// 单封不成组:套一个可折叠的组头只是多一次点击(与 WebUI isFlatGroup 同结论)
|
||||
this.MailRow(g.mails[0])
|
||||
} else {
|
||||
Column() {
|
||||
/*
|
||||
* ── 会话组容器 ──
|
||||
*
|
||||
* ★★ 2026-09-21 修「多个邮件为什么没有堆叠效果」(用户)。
|
||||
*
|
||||
* 原来组头与子邮件是**平铺的兄弟**、各自一张完整的卡、间距一样 ——
|
||||
* 于是一组三封看起来就是"三张互不相干的卡",看不出
|
||||
* "这是同一条会话里的三封"。
|
||||
*
|
||||
* WebUI 的形状(`MailList.tsx:173-232`):
|
||||
* <div className="rounded-lg border"> ← **组容器**(一层外框)
|
||||
* <button className="glass-card …"> ← 组头
|
||||
* <div className="pl-5 pr-1 pb-1.5 space-y-0.5"> ← **缩进** + 紧凑间距
|
||||
* {g.mails.map(m => <MailItem compact />)}
|
||||
*
|
||||
* 三个要点逐条对齐:
|
||||
* ① **缩进** `pl-5`(20px):子邮件明确"在组头之下";
|
||||
* ② **紧凑间距** `space-y-0.5`(2px):一组之内挤在一起,
|
||||
* 而**组与组之间**是列表的 6vp —— 疏密本身就是分组信息;
|
||||
* ③ 组内含选中邮件时给外框上色(`hasActive`),
|
||||
* 否则展开一个组再滚下去会找不到自己在看哪封。
|
||||
*/
|
||||
Column({ space: 2 }) {
|
||||
this.GroupHeader(g)
|
||||
if (this.isExpanded(g.key)) {
|
||||
ForEach(g.mails, (m: MailLike) => {
|
||||
this.MailRow(m)
|
||||
}, (m: MailLike) => m.source_account_id + ':' + m.mail_id)
|
||||
/* 子邮件整体缩进 20vp(WebUI `pl-5`)—— 缩进是分组的视觉主语 */
|
||||
Column({ space: 2 }) {
|
||||
ForEach(g.mails, (m: MailLike) => {
|
||||
this.MailRow(m)
|
||||
}, (m: MailLike) => m.source_account_id + ':' + m.mail_id)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 20, right: 1, bottom: 4 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.borderRadius(Theme.radiusCard)
|
||||
/* 组内含正在读的那封:给外框上色(WebUI 的 `hasActive`) */
|
||||
.border({
|
||||
width: 1,
|
||||
color: this.groupHasActive(g) ? Theme.accentEdgeFor(this.isDarkNow) : Color.Transparent
|
||||
})
|
||||
.backgroundColor(this.groupHasActive(g)
|
||||
? Theme.accentSoftFor(this.isDarkNow) : Color.Transparent)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
@ -663,6 +720,26 @@ struct InboxTab {
|
||||
* 参数类型是 `MailLike`(`MailGrouping.ts` 的接口)而不是 `MailSummary`:
|
||||
* 折叠后的组里装的是接口类型,ArkTS 不做结构类型匹配,写死具体类就传不进来。
|
||||
*/
|
||||
/**
|
||||
* 这一组里有没有**正在读**的那封。
|
||||
*
|
||||
* 对齐 WebUI `MailList.tsx:171`:
|
||||
* const hasActive = currentMailID ? g.mails.some(m => m.mail_id === currentMailID) : false;
|
||||
* 它的用途写在那边注释里:「组内含选中邮件时给个边框,否则展开一个组再滚下去
|
||||
* 会找不到自己在看哪封」—— 组头离得远,光靠子项自己的高亮不够。
|
||||
*/
|
||||
private groupHasActive(g: SessionGroup): boolean {
|
||||
if (this.currentMailId.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < g.mails.length; i++) {
|
||||
if (g.mails[i].mail_id === this.currentMailId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Builder
|
||||
MailRow(mail: MailLike) {
|
||||
Row() {
|
||||
@ -684,8 +761,40 @@ struct InboxTab {
|
||||
* 若只靠底色区分,选中一封未读邮件时**看不出任何变化**。
|
||||
* 所以选中额外加那圈 `accentEdge` 边 —— 这正是 WebUI 两个 token 并存的原因。
|
||||
*/
|
||||
.backgroundColor(this.currentMailId === mail.mail_id || mail.status === 'unread'
|
||||
? Theme.accentSoftFor(this.isDarkNow) : Theme.surface)
|
||||
/*
|
||||
* ★★ 2026-09-21 修「有一个完全不透明的邮件项」(用户原话)。
|
||||
*
|
||||
* 原来这里写的是:
|
||||
* `.backgroundColor(... ? accentSoft : Theme.surface)`
|
||||
* 而 `Theme.surface` = `$r('sys.color.ohos_id_color_list_card_bg')`
|
||||
* —— **系统卡片色,α = 1**。于是壁纸开着时,**已读**的邮件行是一块纯白实心板
|
||||
* (实测像素 `(255,255,255)`,壁纸完全透不出来),
|
||||
* 而未读/选中那些(`accentSoft`)有半透明感 ——
|
||||
* 一排里就"单出一张不透明的",正是用户看到的那样。
|
||||
*
|
||||
* ── 为什么 `GroupHeader` 没这个毛病 ──
|
||||
* 它挂的是 `GlassCardModifier.of(this.bgActive)`(玻璃卡基础件),
|
||||
* 走 `backgroundEffect({radius, saturation})`,**不写实心色**。
|
||||
* `MailRow` 当时漏了这一步 —— 我手写了颜色,没用那件基础件。
|
||||
*
|
||||
* ── 修法 ──
|
||||
* 与 `GroupHeader` 同一件 `GlassCardModifier`(对齐 WebUI:两者的行都是
|
||||
* `.glass-card`,`MailList.tsx:280` 与 `:185`)。
|
||||
*
|
||||
* ★ 但「未读/选中」的淡蓝底**不能丢** —— WebUI 是
|
||||
* `glass-card` + `bg-blue-50` 两层叠着(CSS 里后者只改 background-color,
|
||||
* 玻璃的 border/阴影仍在)。
|
||||
* ArkUI 的 `.attributeModifier` 是**单一插槽**(`.backgroundColor` 链在 modifier
|
||||
* 之外时会被 modifier 覆盖),所以这里用 `CompositeModifier` 显式合成:
|
||||
* ① 有态色(未读/选中)时铺 `accentSoft` —— 它本身就是带 alpha 的令牌;
|
||||
* ② 无态色时走玻璃卡(透明 + backgroundEffect)。
|
||||
*/
|
||||
.attributeModifier(this.currentMailId === mail.mail_id || mail.status === 'unread'
|
||||
? CompositeModifier.of([
|
||||
GlassCardModifier.of(this.bgActive, false),
|
||||
TintModifier.of(Theme.accentSoftFor(this.isDarkNow))
|
||||
])
|
||||
: GlassCardModifier.of(this.bgActive))
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.border({
|
||||
width: 1,
|
||||
@ -715,6 +824,23 @@ struct InboxTab {
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
AmIcon({ iconName: 'chevronRight', iconSize: 12, iconColor: Theme.textSubtleFor() })
|
||||
.rotate({ angle: this.isExpanded(g.key) ? 90 : 0 })
|
||||
/*
|
||||
* ★★ 2026-09-21 补:**属性级动画**(用户:「其他界面涉及运动的页面
|
||||
* webui 也是有类似效果的」)。
|
||||
*
|
||||
* 原来这里只写了 `.rotate(...)` —— 角度是**瞬变**的:
|
||||
* 展开/收起时箭头"啪"地跳 90°,感觉是硬切。
|
||||
* WebUI 那边是 `transition-transform`(`MailList.tsx:186` 的
|
||||
* `className="… transition-transform ${open ? 'rotate-90' : ''}"`),
|
||||
* 角度变化会**平滑补间**。
|
||||
*
|
||||
* ArkUI 对应能力就是属性级 `.animation()`:它盯住**绑在它之上**的可动画
|
||||
* 属性,值一变就自动补间(文档 `arkts-attribute-animation-apis.md`)。
|
||||
* 用它而不是 `animateTo`:这里角度由 `isExpanded()` 驱动,
|
||||
* 触发点在 `toggleExpanded()` 里、离得很远,包 `animateTo` 要改三处;
|
||||
* 属性级动画不需要调用方配合。
|
||||
*/
|
||||
.animation({ duration: Motion.dur(Theme.durFast), curve: Theme.easeOutSoft })
|
||||
}
|
||||
.width(20)
|
||||
Column() {
|
||||
@ -766,6 +892,7 @@ struct InboxTab {
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.border({ width: 1, color: Theme.border })
|
||||
.clip(true)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
this.toggleExpanded(g.key);
|
||||
})
|
||||
@ -1068,6 +1195,7 @@ struct SentTab {
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.margin({ bottom: 6 })
|
||||
.clip(true)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.toggleExpanded(g.key); })
|
||||
}
|
||||
|
||||
@ -1146,6 +1274,7 @@ struct SentTab {
|
||||
color: this.currentMailId === m.mail_id ? Theme.accentEdgeFor(this.isDarkNow) : Theme.border
|
||||
})
|
||||
.margin({ bottom: 6 })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.openMail(m); })
|
||||
}
|
||||
.width('100%')
|
||||
@ -1638,9 +1767,15 @@ struct CommPage {
|
||||
}
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(44)
|
||||
/*
|
||||
* 用 `TAB_BAR_HEIGHT` 而不是字面量 44 —— 它同时决定 `TAB_BAR_RADIUS`
|
||||
* (半径 = 高/2)。写成两个字面量的话,哪天改高忘了改半径,
|
||||
* 半径又会超过一半、圆角重新变成怪形(正是 2026-09-21 修的那个 bug)。
|
||||
*/
|
||||
.height(TAB_BAR_HEIGHT)
|
||||
// 选中态用**下划线**(与 WebUI 的 `border-b-2` 同一观感),不用浮动白胶囊
|
||||
.border({ width: { bottom: this.commTab === key ? 2 : 0 }, color: Theme.accent })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
this.commTab = normalizeCommTab(key);
|
||||
this.navPathStack.clear();
|
||||
@ -1648,16 +1783,6 @@ struct CommPage {
|
||||
})
|
||||
}, (key: string) => key)
|
||||
}
|
||||
/*
|
||||
* ★ 宽度用 `calc(100% - 2×TAB_BAR_SIDE)`,**不是 `width('100%')` + `margin`**。
|
||||
*
|
||||
* ArkUI 里 `width('100%')` 是按父容器算的,再叠 `margin` 会**向右溢出**
|
||||
* 而不是把条挤窄 —— 实测(模拟器 3184px、密度 2.875):
|
||||
* 页签条实际占 x=231..1151,与面板同宽;TAB_BAR_SIDE(16vp=46px) 完全没生效
|
||||
* ⇒ 看起来仍是"通栏",只是多了圆角,与底部悬浮条(左右各留 16vp)不齐。
|
||||
* 把留白算进宽度里,左右才真的各让出 16vp。
|
||||
*/
|
||||
.width(`calc(100% - ${TAB_BAR_SIDE * 2}vp)`)
|
||||
/*
|
||||
* ★★ 2026-09-20(用户:「顶部的收件箱发件箱和授权为什么不做成悬浮玻璃?」)
|
||||
*
|
||||
@ -1669,11 +1794,27 @@ struct CommPage {
|
||||
* 三种做法各自的取舍见 `model/NavItems.ts` 的 `TAB_BAR_*` 常量注释
|
||||
* (为什么选悬浮而不是 WebUI 的"通栏无底色")。
|
||||
*/
|
||||
/*
|
||||
* 宽度:**与窗格齐平**(`100%`)。
|
||||
*
|
||||
* 原来这里是 `calc(100% - 2×TAB_BAR_SIDE)`(左右各内缩 16vp)。
|
||||
* 实测 WebUI:`.comm-pane` 与 `tabstrip` 的 `x`/`w` **完全相同**(都是 80/320)
|
||||
* —— 它靠**窗格的 `overflow:hidden`** 把顶角裁圆,所以页签条自己直角、通栏。
|
||||
* 我们内缩 + 自己带角 ⇒ 右端出现两道弧(用户:「你又在内部套了一个胶囊」)。
|
||||
* ⇒ 改成与窗格齐平,右端只剩窗格那一道边。
|
||||
*/
|
||||
.width('100%')
|
||||
.backgroundColor(Color.Transparent)
|
||||
.backgroundBlurStyle(this.bgActive ? Theme.navMaterial : BlurStyle.NONE)
|
||||
.borderRadius(TAB_BAR_RADIUS)
|
||||
.clip(true)
|
||||
.margin({ left: TAB_BAR_SIDE, right: TAB_BAR_SIDE, top: TAB_BAR_TOP })
|
||||
/*
|
||||
* ★ 只给**左上角**圆角,右上角不要(用户:「鸿蒙布局还略有不同的,
|
||||
* 你直接改成右边没圆角就行了」)。
|
||||
*
|
||||
* 右上角的弧与窗格右上角的弧**重叠成两道**(页签条一道、窗格一道),
|
||||
* 中间那弯月牙形的空玻璃就是"奇怪"的来源。
|
||||
* 去掉右边那一角,右端就只剩窗格自己那一道边。
|
||||
*/
|
||||
.borderRadius({ topLeft: Theme.glassRadius, topRight: 0 })
|
||||
}
|
||||
|
||||
openMail(mailId: string, accountId: string): void {
|
||||
@ -1841,7 +1982,27 @@ struct CommPage {
|
||||
.navBarWidth(320)
|
||||
.navBarWidthRange([280, 360])
|
||||
.minContentWidth(360)
|
||||
.hideTitleBar(true)
|
||||
.hideTitleBar(true)
|
||||
/*
|
||||
* ★★ 2026-09-21 补:**联系人页的 `Navigation` 缺背景声明**
|
||||
* (用户:「邮件看着没有玻璃效果」→ 实测发现卡内像素 (253,253,253),
|
||||
* 与"白 0.78 叠在壁纸上应得 (240,242,244)"差得太远 ⇒ 下面另有不透明层)。
|
||||
*
|
||||
* `Navigation` 不设背景时用**系统默认底**(近白不透明)。
|
||||
* 实测纵深采样(窄屏 1008px,y=500):
|
||||
* x=2 (191,199,209) ← 壁纸
|
||||
* x=20 (184,192,204) ← 壁纸
|
||||
* x=32 (243,244,248) ← **突然变近白** ⇒ 这一层就是 Navigation 的壳
|
||||
* x=100 (251,253,253) ← 卡片(白纱叠在这个近白壳上,当然看不见壁纸)
|
||||
*
|
||||
* ⇒ 整条链路(壁纸 → pane → 卡片)里,pane 与卡片都已经是半透明了,
|
||||
* **只剩这层壳把壁纸挡在外面** ⇒ 玻璃感整体失效。
|
||||
*
|
||||
* ★ `CommPage` 那个 `Navigation` 在 2026-09-19 已经补过同一处
|
||||
* (见那段注释里记的"那条缝是决定性的证据"),**联系人页漏了** ——
|
||||
* 同一个形状在两个 struct 里各写一遍,就是会漏一个。
|
||||
*/
|
||||
.attributeModifier(PaneModifier.of(this.bgActive))
|
||||
.width('100%').height('100%')
|
||||
/* 右栏占位:栈空时显示引导(否则宽屏两栏并排时右栏是一大片空白)。
|
||||
用系统入口 `splitPlaceholder`,**不是** NavDestination 的 else —— 后者栈空时不挂载。 */
|
||||
@ -2083,6 +2244,7 @@ struct ContactsTab {
|
||||
})
|
||||
}
|
||||
.width(40).height(40)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.switchView(); })
|
||||
}
|
||||
.width('100%').height(56).padding({ left: 16, right: 8 })
|
||||
@ -2149,6 +2311,7 @@ struct ContactsTab {
|
||||
}
|
||||
.width('100%')
|
||||
/* 点卡片 = 打开这条会话(WebUI `ContactPanel` 的 onOpen) */
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
// 确认态下卡片的点击不打开会话:那块区域已经是"确认框"了
|
||||
if (this.pendingArchiveId !== c.session_id) {
|
||||
@ -2201,6 +2364,7 @@ struct ContactsTab {
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.clip(true)
|
||||
/* 点卡片 = 打开这条会话(WebUI `ContactPanel` 的 onOpen) */
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
// 确认态下卡片的点击不打开会话:那块区域已经是"确认框"了
|
||||
if (this.pendingArchiveId !== c.session_id) {
|
||||
@ -2265,6 +2429,7 @@ struct ContactsTab {
|
||||
AmIcon({ iconName: 'chevronLeft', iconSize: 20, iconColor: Theme.accentFor() })
|
||||
}
|
||||
.width(HEADER_BACK_HIT).height(HEADER_BACK_HIT)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.closeSession(); })
|
||||
Text(this.openSessionTitle)
|
||||
.fontSize(14).fontWeight(FontWeight.Bold).fontColor(Theme.textPrimary)
|
||||
@ -2316,6 +2481,7 @@ struct ContactsTab {
|
||||
.border({ width: 1, color: Theme.border })
|
||||
}
|
||||
.width('100%')
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.openMail(m.mail_id, m.source_account_id); })
|
||||
}, (m: MailSummary) => m.mail_id)
|
||||
}
|
||||
@ -2470,6 +2636,7 @@ struct ContactsTab {
|
||||
.borderRadius(Theme.radiusControl)
|
||||
.border({ width: 1, color: Theme.border })
|
||||
.backgroundColor(Theme.surface)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(onTap)
|
||||
}
|
||||
|
||||
@ -2500,6 +2667,7 @@ struct ContactsTab {
|
||||
.borderRadius(Theme.radiusControl)
|
||||
.backgroundColor(Theme.danger)
|
||||
.opacity(this.archiving ? 0.5 : 1)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.confirmArchive(c); })
|
||||
|
||||
Row() {
|
||||
@ -2511,6 +2679,7 @@ struct ContactsTab {
|
||||
.border({ width: 1, color: Theme.border })
|
||||
.backgroundColor(Theme.surface)
|
||||
.margin({ left: 8 })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.cancelArchive(); })
|
||||
}
|
||||
.width('100%').margin({ top: 10 })
|
||||
@ -3194,6 +3363,8 @@ struct MainPage {
|
||||
* 时长用令牌(120ms, `--dur-fast`),不另拍一个数。
|
||||
*/
|
||||
.animation({ duration: Theme.durFast, curve: Theme.easeOutSoft })
|
||||
/* 按压反馈:导航项是最高频的可点元素,按下要有明确的"吃到了" */
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
/*
|
||||
* ★ 四项**都是内容窗格**(用户 2026-09-17:「我的页面完全没有遵守 nav 的导航规则」)。
|
||||
@ -3355,9 +3526,45 @@ struct MainPage {
|
||||
* (status 与 from_agent 参考实现也不显示,见 `WorkCard.tsx` 的字段集,
|
||||
* 判据里有一条"卡片字段两边一致"钉住这件事,避免以后以为漏了。)
|
||||
*/
|
||||
Stack({ alignContent: Alignment.Bottom }) {
|
||||
// 背景在最底下:内容面(系统色)盖在它上面
|
||||
/*
|
||||
* ★★ 2026-09-21 **结构性改**:根从「重叠的 Stack」改成
|
||||
* 「壁纸 + `Column{内容, 导航条}`」
|
||||
* (用户:「你看看 webui 是怎么安排的,它的避让方案是让内容框停在导航栏上方」
|
||||
* 「包括邮箱正文等等,都是这个避让方案」)。
|
||||
*
|
||||
* ── WebUI 的做法(用 CDP 实测它的真实几何,430×932)──
|
||||
*
|
||||
* .narrow-shell { display: flex; flex-direction: column }
|
||||
* <children/> ← 内容(flex 子项,**占位**)
|
||||
* <NarrowNav/> ← `position: static`、`flexShrink: 0`,**真的占 51px**
|
||||
*
|
||||
* 实测:`scroller.bottom = 861`、`nav.top = 871`
|
||||
* ⇒ **内容盒就停在导航栏上方**,两者谁也不用"让"谁。
|
||||
*
|
||||
* ── 鸿蒙原来错在哪 ──
|
||||
*
|
||||
* 根是 `Stack({ alignContent: Alignment.Bottom })`:`NavBar()` 与内容 `Row`
|
||||
* 是**并列兄弟**、都被摆到底部 ⇒ **互相重叠**(导航条浮在内容之上)。
|
||||
* 于是"避让"变成每个滚动容器各自手写 `contentEndOffset(navReserve)` 的手工活。
|
||||
*
|
||||
* 代价已经付过:我**漏了 8 处** —— 邮件详情正文、日视图列表、对话树、
|
||||
* 用户管理页……每处的最后一行都被底栏压住
|
||||
* (实测 `Scroll` 延伸到 y=2231,而底栏占 1957..2232)。
|
||||
*
|
||||
* ★ 这是**同一类错误的第二次**:把该由**布局**承担的事,交给 N 个调用点逐个手写。
|
||||
* 第一次是按压反馈(112 处 `onClick` 只挂上 2 处)。
|
||||
* 判据:**凡"每加一个页面就得记得做一次"的事,迟早会漏一半。**
|
||||
*
|
||||
* ── 改法 ──
|
||||
*
|
||||
* 照 WebUI:竖排 `Column`;内容 `layoutWeight(1)`,`NavBar` 自己占位。
|
||||
* 壁纸层仍绝对铺满(留在 `Stack` 里,不受这根 Column 影响)。
|
||||
* 各处的 `contentEndOffset` **保留**:宽屏没有底栏时它本就是 0,
|
||||
* 而它另外表达"内容末尾别贴死"(例如详情页要躲开回复球)。
|
||||
*/
|
||||
Stack() {
|
||||
this.WallpaperLayer()
|
||||
Column() {
|
||||
/*
|
||||
* 内容按 `currentIndex` 挂载(原来这里是系统 `Tabs` 的 TabContent)。
|
||||
* 底部**必须让出** NAV_CONTENT_RESERVE 的高度:条是浮在内容之上的,
|
||||
@ -3540,7 +3747,22 @@ struct MainPage {
|
||||
* `background-clip: padding-box` 处理,代价是溢出内容在圆角处可能露一点,
|
||||
* 比"完全不能滚动"好得多(原文)。这里照做:**去掉 clip,保留圆角**。
|
||||
*/
|
||||
.borderRadius(this.isWide ? Theme.glassRadius : 0)
|
||||
/*
|
||||
* ★★ 2026-09-21 修「窄屏圆角全没了」(用户:「你的邮件的圆角呢?日历的圆角呢?」)。
|
||||
*
|
||||
* 原来写的是 `this.isWide ? Theme.glassRadius : 0` —— **窄屏恒 0**,
|
||||
* 于是窄屏上「通信 / 日历 / 联系人 / 我的」四个窗格全是**直角**,
|
||||
* 与底部那条圆角玻璃导航条根本不是一套语汇。
|
||||
*
|
||||
* WebUI 那边两条分支**都给圆角**(`index.css`):
|
||||
* .app-shell > * { border-radius: var(--radius-card); } ← 宽屏
|
||||
* .narrow-shell > * { border-radius: var(--radius-card); } ← 窄屏
|
||||
* 差别只在**留白**:宽屏四边都留 `--pane-gap`,窄屏只留左右上
|
||||
* (下面那条 padding 里 `bottom: 0` 就是它),因为底栏自己带 margin。
|
||||
*
|
||||
* 我当初把"窄屏不留白"顺手写成了"窄屏不圆角",两件事被并成了一个三元。
|
||||
*/
|
||||
.borderRadius(Theme.glassRadius)
|
||||
/*
|
||||
* ★ 这里**不再**让位(原来写的是 `padding({ bottom: NAV_CONTENT_RESERVE })`)。
|
||||
*
|
||||
@ -3578,8 +3800,18 @@ struct MainPage {
|
||||
* 否则两层内容同时半透明地叠在屏幕上,看起来像"闪一下"。
|
||||
*/
|
||||
.padding({
|
||||
left: this.isWide ? Theme.paneGap : 0,
|
||||
right: this.isWide ? Theme.paneGap : 0,
|
||||
/*
|
||||
* ★★ 2026-09-21:窄屏**也要**左右留白(原来是 `isWide ? paneGap : 0`)。
|
||||
*
|
||||
* WebUI `.narrow-shell` 的 padding 是 `var(--pane-gap) var(--pane-gap) 0`
|
||||
* ——左右上和宽屏一样留 10px,只有底边为 0(底栏自己带 margin)。
|
||||
* 我们用 `isWide ? paneGap : 0` 把"底边为 0"错推广成了"四边都为 0",
|
||||
* 于是窄屏面板**贴死屏幕两侧**:既没有圆角的余地,也没有投影的余地,
|
||||
* 看起来是一块被屏幕裁掉的白板,而不是一张浮起来的卡。
|
||||
* (这正是用户说"邮箱页面丑死了、跟 WebUI 没法比"的一块。)
|
||||
*/
|
||||
left: Theme.paneGap,
|
||||
right: Theme.paneGap,
|
||||
/*
|
||||
* ★ 避让区就在这里:状态栏高度当 padding-top。
|
||||
*
|
||||
@ -3611,14 +3843,39 @@ struct MainPage {
|
||||
* "宽屏那一支"里。宽屏只是**额外多留** paneGap(面板投影的余量),
|
||||
* 不是**代替**避让。
|
||||
*/
|
||||
top: this.windowInsets.statusBar + (this.isWide ? Theme.paneGap : 0),
|
||||
/*
|
||||
* 避让区 + 面板留白。避让是**窗口级事实**(全屏后内容从 y=0 起画),
|
||||
* 与宽窄无关;留白则两端都有(WebUI 两条 shell 规则的 padding-top 都是 gap)。
|
||||
*/
|
||||
top: this.windowInsets.statusBar + Theme.paneGap,
|
||||
/*
|
||||
* 底边 = 0:窄屏的底栏是**悬浮**的、自己带 `NAV_BAR_BOTTOM` 的 margin,
|
||||
* 内容要能从它底下穿过(玻璃才有东西可糊,见 `navReserve` 那段)。
|
||||
* 宽屏没有底栏,才由这里补上留白。
|
||||
*/
|
||||
bottom: this.isWide ? Theme.paneGap : 0
|
||||
/*
|
||||
* 内容吃 **Column 里 NavBar 之外的剩余高度**。
|
||||
* 原来这里是 `height('100%')`(占满 Stack)—— 加了 NavBar 占位后
|
||||
* 那句会把导航条挤出可视区(实测:底栏整个消失)。
|
||||
*/
|
||||
})
|
||||
.layoutWeight(1)
|
||||
|
||||
// 浮动玻璃条浮在最上层(自绘,见 NavBar 的说明)。宽屏没有底部导航。
|
||||
/*
|
||||
* 底部导航条 —— **Column 的第二个子项**(不是覆盖层)。
|
||||
*
|
||||
* 它**占位**,所以内容盒自然停在它上方,不必再逐处 `contentEndOffset`。
|
||||
* `NavBar` 内部仍保留自己的圆角/外边距/玻璃材质 ——
|
||||
* 那是"悬浮**观感**",由圆角+阴影表达,不再靠 `position` 浮起来。
|
||||
* 宽屏没有底栏,这支只有一个子项,行为与原来一致。
|
||||
*/
|
||||
if (!this.isWide) {
|
||||
this.NavBar()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
*/
|
||||
import { ApiClient, ApiError } from '../api/ApiClient';
|
||||
import { Theme } from '../common/Theme';
|
||||
import { AppHeader } from '../common/Surface';
|
||||
import { AppHeader, PressEffectModifier } from '../common/Surface';
|
||||
import { GlassCardModifier, PaneModifier } from '../common/Surface';
|
||||
import { AuthApi } from '../api/AuthApi';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
@ -696,6 +696,12 @@ export struct SettingsPane {
|
||||
}
|
||||
.width('100%').layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
/*
|
||||
* ★★ 2026-09-21:高度受限的 Scroll 必须显式顶部对齐。
|
||||
* 官方 FAQ 原文:内容比 Scroll 矮时「**默认居中排布**」,要 `.align(Alignment.Top)`
|
||||
* 才是顶部排布(`faqs-arkui-725`)。缺这一行时「我的」页内容少就飘在中间。
|
||||
*/
|
||||
.align(Alignment.Top)
|
||||
/* WebUI `.overflow-y-auto` 的上下渐隐:内容滑到边缘不硬截断 */
|
||||
.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })
|
||||
/* 末尾让位:最后一段(退出登录)要能滚出悬浮条之下 */
|
||||
@ -803,6 +809,7 @@ export struct SettingsPane {
|
||||
}
|
||||
.width(44)
|
||||
.height(44)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.showAddDialog = true; })
|
||||
}
|
||||
|
||||
@ -1201,6 +1208,7 @@ export struct SettingsPane {
|
||||
.padding(16).margin({ top: 8 })
|
||||
.attributeModifier(GlassCardModifier.of(this.bgActive))
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => {
|
||||
this.getUIContext().getRouter().pushUrl({ url: 'pages/AdminUsersPage' });
|
||||
})
|
||||
|
||||
@ -46,6 +46,7 @@ import {
|
||||
sidebarContentIndex
|
||||
} from '../model/NavItems';
|
||||
import { Insets } from '../model/WindowInsets';
|
||||
import { PressEffectModifier } from '../common/Surface';
|
||||
|
||||
/** 侧栏宽度(vp)。WebUI 的 `Sidebar` 用 `w-[60px]` */
|
||||
export const SIDEBAR_WIDTH: number = 60;
|
||||
@ -163,6 +164,7 @@ export struct WideSidebar {
|
||||
.justifyContent(FlexAlign.Center)
|
||||
// 选中底块:WebUI `.nav-item[data-active='true']` 的 `--nav-active-bg`
|
||||
.backgroundColor(this.currentIndex === sidebarContentIndex(item.key) ? Theme.navActiveBg : Color.Transparent)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.onSelect(sidebarContentIndex(item.key)); })
|
||||
|
||||
/*
|
||||
@ -204,6 +206,7 @@ export struct WideSidebar {
|
||||
.width(40).height(40)
|
||||
.borderRadius(16)
|
||||
.margin({ bottom: 8 })
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.onSelect(0); })
|
||||
|
||||
/*
|
||||
@ -256,6 +259,7 @@ export struct WideSidebar {
|
||||
.margin({ right: 1, bottom: 1 })
|
||||
}
|
||||
.width(40).height(40)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.onSelect(ME_PANE_INDEX); })
|
||||
|
||||
/* ② 主题切换 */
|
||||
@ -264,6 +268,7 @@ export struct WideSidebar {
|
||||
}
|
||||
.width(36).height(28)
|
||||
.borderRadius(8)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.onToggleTheme(); })
|
||||
|
||||
/* ③ 退出 */
|
||||
@ -272,6 +277,7 @@ export struct WideSidebar {
|
||||
}
|
||||
.width(36).height(28)
|
||||
.borderRadius(8)
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
.onClick(() => { this.onLogout(); })
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Reference in New Issue
Block a user