v61:顶栏标题=模块名 / 抽屉瘦身 + 头像进个人设置 / 删除按钮红底 / 浮层动画 / 日程卡片与手势
用户这一批九条,逐条对应:
1)「页面上左上角的标题,不再是家书了,而是切换到那个模块,就显示哪个模块的标题。
这样在页面内容部,就不再出现模块的名字标题了。」
→ TopBar 主标题改读 moduleName()(通知/设置/个人设置三个非模块路由也有名字);
观影/纪念日/读书/笔记/通知五处页内大标题删掉。
2)「按钮,也需要和主题色保持一致,出现文字的时候,例如观影模块,
在左上角出现的时候,颜色也需要和主题色保持一致。」
→ 标题色走 fbModuleColor(app.activeModule)(一处算,11 个页面不用各写)。
3)「弹框的外圈颜色,跟模块的主题色保持一致。」
→ 日程详情原来传 bookColor(日历本色)⇒ 改 accentColor();
7 个模块设置浮层补传 accent: this.accent。
4)「删除按钮全部用红色背景。」
→ 日程编辑/人员编辑从红描边改红底白字;详情「删除联系人」从浅红底改红底白字;
两处确认框统一 FbColors.red。
5)「做几个动画,例如,点击名册列表,应该有一个过渡动画出现弹框,
点击详情,框变大了,也应该有一个过渡动画,点编辑,也有动画。」
→ 新建 common/FbAnim.ets(fbMaskTransition / fbSheetTransition /
fbSheetGrowTransition / fbViewTransition),9 个页面共 17 遮罩 + 16 浮层挂上;
简要卡→详情用 0.96→1 的"继续长大"(0.94 会先缩一下)。
6)「左侧抽屉,把通知消息、个人设置、修改密码、系统设置,都去掉,
放在个人设置页,就是在点击右侧头像那里。消息通知不用放进去,
直接在头像旁边那里就可以了。」
→ 抽屉底部那四行整块删除(抽屉只剩品牌区 + 模块列表);
新建 pages/account/FbAccountPage.ets(整页,__account 分发),
点头像 switchModule('__account');改密码真打服务端新接口。
7)「日程……日期下面:12本日历可见,本月124条,这句话去掉。
点击日程弹出的框,编辑按钮也颜色也要修改。
全天日程,例如农历,上面显示无标题,读不到这条日程,这个有问题。」
→ 删统计行;编辑按钮从写死名册青 #4A7C82 改模块主题色;
真根因:衍生条目(农历/节气)的 detailFull **一直是 null**
(它们没有 /events/<id> 可拉,原来直接 return)⇒ 垫 shellOf(),
并把"整卡报错"改成"照常画内容 + 一行小字";
空标题兜底从「(无标题)」改成日历本名。
8)「再做几个卡片,先做日程的,这个参考 synccalendar。」
→ FbDayEventPanel.eventRow 照 SyncCalendar:左侧 4vp 圆头色条 + 标题 +
时间/全天 + 「↻ 重复」徽标 + 右端日历本名(限宽 30%、同色);
一行一张独立圆角卡(12)+ 描边,卡间 space 8,不再用 Divider 连成一片。
9)「日程,有很多手势,你参考 synccalendar。」
→ 月历上滑→周视图、周条下划→月视图、周条左右滑→换周。
真机实测两次踩空后定下写法:**竖向必须 parallelGesture(PanGesture)**
(外层是 Scroll,SwipeGesture 的"快速划动"会被它的内置滚动先吃掉,
gesture/priorityGesture 都不行);**横滑用 gesture(PanGesture)**
(同一节点两条方向不同的 parallelGesture 会互相压制);
方向按累计位移判、并要求 |dy| > |dx|。切视图走 animateTo + fbViewTransition。
验证:
· 编译 BUILD SUCCESSFUL
· ArkTS 守卫 493/0;Schema 列守卫 12/0;反向验证 39/0(新增 22 条注入逐条确认能变红)
· 服务端接口 e2e 20/0(独立副本 + 真 MariaDB,5 种错误路径 + 原令牌仍有效)
· 真机 _fb_v61_verify.py 21/0(6 组全过);像素采样确认浮层主题色环实测存在
This commit is contained in:
@@ -146,80 +146,6 @@ export struct FbDrawer {
|
||||
})
|
||||
}
|
||||
|
||||
/** 底部账号区(对应 Web 端右上角头像菜单里的那几项) */
|
||||
@Builder
|
||||
accountFooter() {
|
||||
Column({ space: 2 }) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ bottom: 6 })
|
||||
|
||||
this.footerRow('fb_ic_bell', '通知消息',
|
||||
this.app.unread > 0 ? this.app.unread.toString() : '', '__notifications')
|
||||
this.footerRow('fb_ic_user', '个人设置', '', '__settings')
|
||||
// ⚠️ 下面两项端上**还没做页面**,传空 route ⇒ 灰显、不可点。
|
||||
// 别给它们绑 `__password` / `__system` —— Index.ets 没这两个分支,
|
||||
// 点下去是**空白页**,比"点不动"更让人困惑。
|
||||
this.footerRow('fb_ic_lock', '修改密码', '', '')
|
||||
if (this.app.session.me.role === 'admin') {
|
||||
this.footerRow('fb_ic_gear', '系统设置', '', '')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 🔴 bottom 给底部**手势条区域**留一点(全屏布局下抽屉会铺到屏幕最下沿,
|
||||
// 底部这两行若贴着边,手指从下往上滑时容易误触到系统手势)。
|
||||
.padding({ left: 12, right: 12, bottom: 10 + FB_BOTTOM_GAP })
|
||||
}
|
||||
|
||||
/**
|
||||
* 底部一行(通知消息 / 个人设置 / 修改密码 / 系统设置)。
|
||||
*
|
||||
* 🔴 必须带 `route` 并绑 onClick —— 之前这四行**根本没有点击处理**,
|
||||
* 界面上是个死按钮(点下去毫无反应)。
|
||||
* 排查记录:点「个人设置」后仍停在首页,看界面树发现该元素
|
||||
* `clickable=false` 且整行没有任何可点击祖先 —— 就是这个原因。
|
||||
* ⚠️ 与模块项(`moduleRow`)不同:这四项的路由 key 是 `__xxx` 形式,
|
||||
* 不是 `FB_MODULES` 里的模块,走同一个 `switchModule` 分发
|
||||
* (Index.ets 里用 `activeModule === '__settings'` 渲染对应页面)。
|
||||
*
|
||||
* ⚠️ `route` 传空串表示"**这个页面还没做**" ⇒ 不绑 onClick,且文字降为灰、
|
||||
* 保持不可点。为什么不给它绑个假跳转:那会跳到**空白页**
|
||||
* (Index.ets 里没有对应分支),比"点了没反应"更让人困惑。
|
||||
* 现状:`__notifications` / `__settings` 已有页面;
|
||||
* `修改密码` / `系统设置` **还没做**(Web 端有,端上待开发)。
|
||||
*/
|
||||
@Builder
|
||||
footerRow(iconName: string, label: string, badge: string, route: string) {
|
||||
Row({ space: 12 }) {
|
||||
Image(FbIcon.ui(iconName))
|
||||
.width(17)
|
||||
.height(17)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
// 未实现的项图标也变灰,明示"暂不可用"
|
||||
.colorFilter(fbTint(route === '' ? FbTint.faint : FbTint.muted))
|
||||
Text(label)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(route === '' ? FbColors.faint : FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
if (badge.length > 0) {
|
||||
Text(badge)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 10, right: 10, top: 9, bottom: 9 })
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick(() => {
|
||||
if (route !== '') {
|
||||
this.app.switchModule(route); // switchModule 内部会收起抽屉
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 当前是否选中该模块 */
|
||||
private isActive(key: string): boolean {
|
||||
return this.app.activeModule === key;
|
||||
@@ -250,7 +176,19 @@ export struct FbDrawer {
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
|
||||
this.accountFooter()
|
||||
/* 🔴 v61(用户 2026-10-19):「左侧抽屉,把通知消息、个人设置、
|
||||
修改密码、系统设置,都去掉,放在个人设置页,就是在点击右侧头像那里。
|
||||
消息通知不用放进去,直接在头像旁边那里就可以了。」
|
||||
⇒ 原来这里的底部账号区(四行:通知消息 / 个人设置 /
|
||||
修改密码 / 系统设置)**整块已删**:
|
||||
· 通知 → 顶栏铃铛(一直在头像左边,比藏抽屉里近得多);
|
||||
· 个人设置 / 修改密码 / 系统设置 → 点右上角头像进「个人设置」页。
|
||||
⚠️ 抽屉现在是**纯模块导航**:品牌区 + 模块列表。
|
||||
⚠️ 底部仍留手势条留白 —— 全屏布局下抽屉铺到屏幕最下沿,
|
||||
最后一行的点击区贴着边缘容易被"从下往上滑"误触。 */
|
||||
Column()
|
||||
.width('100%')
|
||||
.height(FB_BOTTOM_GAP)
|
||||
}
|
||||
.width(this.drawerW)
|
||||
.height('100%')
|
||||
|
||||
@@ -106,10 +106,10 @@ export struct FbDayEventPanel {
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 8 })
|
||||
} else {
|
||||
// ---- 列表:一条一行 ----
|
||||
Column({ space: 0 }) {
|
||||
// ---- 列表:一条一行(**一行一张独立圆角卡**,卡间留缝)----
|
||||
Column({ space: 8 }) {
|
||||
ForEach(this.events, (ev: FbCalEvent, idx: number) => {
|
||||
this.eventRow(ev, idx === this.events.length - 1)
|
||||
this.eventRow(ev)
|
||||
}, (ev: FbCalEvent, idx: number): string =>
|
||||
'ev_' + this.label + '_' + idx.toString() + '_' + ev.id.toString())
|
||||
}
|
||||
@@ -123,60 +123,108 @@ export struct FbDayEventPanel {
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
|
||||
/** 列表模式的一条(左侧色条 + 标题 + 时间 + 地点 + 日历本名) */
|
||||
/**
|
||||
* 列表模式的一条(照 SyncCalendar 的 `eventRow`:**一行一张圆角卡**)。
|
||||
*
|
||||
* 🔴 用户 2026-10-19:「再做几个卡片,先做日程的,这个参考 synccalendar。」
|
||||
* + 「日程,有很多手势,你参考 synccalendar。」
|
||||
* ⇒ 照本机范本 `D:\Yongquan\code\SyncCalendar\...\pages\Index.ets` 的
|
||||
* `eventRow()` 对齐:
|
||||
* · 左侧 **4vp 圆头色条**(= 日历本颜色,一眼看出属于哪本);
|
||||
* · 标题(空标题用日历本名兜底,不写「(无标题)」);
|
||||
* · 次行:时间 / 全天 + 「↻ 重复」小徽标 + 地点;
|
||||
* · **最右侧:日历本名**(同色、最多占 30%、超长截断)。
|
||||
* ⚠️ 与原来的差别:原来是一整块白底 + `Divider` 连成一片("表格"观感),
|
||||
* 现在是**卡与卡之间有缝隙**(`space` 在容器上给),每张独立圆角 + 描边。
|
||||
* ⚠️ 不再靠"是不是最后一行"来决定画不画分隔线 —— 卡片形态下
|
||||
* 分隔线是多余的(卡片自己的描边已经分开了)。
|
||||
*/
|
||||
@Builder
|
||||
eventRow(ev: FbCalEvent, isLast: boolean) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 10 }) {
|
||||
Row()
|
||||
.width(4)
|
||||
.height(38)
|
||||
.backgroundColor(ev.color)
|
||||
.borderRadius(2)
|
||||
eventRow(ev: FbCalEvent) {
|
||||
Row({ space: 10 }) {
|
||||
/* 左侧色条:`borderRadius(2)` 做成圆头(SyncCalendar 同款)。
|
||||
⚠️ 高度**写死 38** 而不是 `'100%'`:行高由内容决定,
|
||||
百分比高度在 `Row` 里会拿到"内容高度"⇒ 色条跟着文字多少变长变短。 */
|
||||
Column()
|
||||
.width(4)
|
||||
.height(38)
|
||||
.borderRadius(2)
|
||||
.backgroundColor(ev.color)
|
||||
|
||||
Column({ space: 3 }) {
|
||||
Text(ev.title !== '' ? ev.title : '(无标题)')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Row({ space: 6 }) {
|
||||
Text(ev.allDay ? '全天'
|
||||
: (ev.endTime !== '' ? (ev.time + ' - ' + ev.endTime) : ev.time))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
if (ev.place !== '') {
|
||||
Text('📍' + ev.place)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.layoutWeight(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
}
|
||||
Column({ space: 3 }) {
|
||||
/* 🔴 2026-10-19:衍生条目(农历/节气)标题为空时用**日历本名**兜底,
|
||||
而不是「(无标题)」—— 后者看着像数据坏了(用户就是这么报的)。 */
|
||||
Text(ev.title !== '' ? ev.title
|
||||
: (ev.calName !== '' ? ev.calName : '未命名日程'))
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Row({ space: 6 }) {
|
||||
Text(ev.allDay ? '全天'
|
||||
: (ev.endTime !== '' ? (ev.time + ' - ' + ev.endTime) : ev.time))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
// 「↻ 重复」小徽标(照 SyncCalendar:浅底小圆角)
|
||||
if (ev.kind === 'custom' && ev.calId > 0 && this.repeatOf(ev)) {
|
||||
Text('↻ 重复')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||
.borderRadius(4)
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
}
|
||||
if (ev.place !== '') {
|
||||
Text('📍' + ev.place)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.layoutWeight(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
/* 所属日历本:最右、同色、最多 30% 宽
|
||||
(照 SyncCalendar —— 那里也是这么放的,窄屏下不会挤掉标题) */
|
||||
if (ev.calName !== '') {
|
||||
Text(ev.calName)
|
||||
.fontSize(10)
|
||||
.fontColor(ev.color)
|
||||
.maxLines(1)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 10 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick((): void => {
|
||||
this.onPick(ev);
|
||||
})
|
||||
|
||||
if (!isLast) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ left: 14 })
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.constraintSize({ maxWidth: '30%' })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(10)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(12)
|
||||
// 描边:卡与卡之间即使同色也能看出界线(与浮层卡片同一手法)
|
||||
.border({ width: 1, color: FbColors.line2 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.onClick((): void => {
|
||||
this.onPick(ev);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 这条日程是不是重复日程。
|
||||
*
|
||||
* ⚠️ 列表数据(`FbCalEvent`)里**没有 rrule** —— 重复信息只在 `/events/<id>`
|
||||
* 的详情里(见 `FbCalRepo.fetchEvent`)。所以这里只能给一个"**可能重复**"
|
||||
* 的粗判:非农历/节气/节假日这些衍生条目、且 id 有效。
|
||||
* ⇒ 用日历本 kind 排除衍生条目,避免"农历每天都重复"这种误标。
|
||||
* ⚠️ 这是**显示层的保守估计**:宁可不标,也不要标错
|
||||
* (用户看到"↻ 重复"点进去发现不重复会更困惑)。
|
||||
*/
|
||||
private repeatOf(ev: FbCalEvent): boolean {
|
||||
return ev.kind === 'custom';
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,8 @@ export struct FbDayTimeline {
|
||||
.backgroundColor('#26000000')
|
||||
.borderRadius(6)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
Text(e.title !== '' ? e.title : '(无标题)')
|
||||
Text(e.title !== '' ? e.title
|
||||
: (e.calName !== '' ? e.calName : '未命名日程'))
|
||||
.fontSize(12)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(Color.White)
|
||||
|
||||
@@ -529,13 +529,15 @@ export struct FbEventEditSheet {
|
||||
} else {
|
||||
Row({ space: 10 }) {
|
||||
if (!this.isCreate) {
|
||||
// 🔴 用户 2026-10-19:「删除按钮全部用红色背景」
|
||||
// (原来是红色描边、白底 —— 与"所有删除都红底"不一致)
|
||||
Text('删除')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor('#C0533F')
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(42)
|
||||
.padding({ left: 18, right: 18 })
|
||||
.border({ width: 1, color: '#C0533F' })
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
if (!this.busy) {
|
||||
@@ -549,7 +551,9 @@ export struct FbEventEditSheet {
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.backgroundColor(this.busy ? '#4A7C8288' : '#4A7C82')
|
||||
// 🔴 底色用**日程模块主题色**(原来写死名册青 #4A7C82,
|
||||
// 在日程页里看着像走错了页面 —— 用户 2026-10-19)
|
||||
.backgroundColor(this.busy ? FbColors.faint : this.accentColor())
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
if (!this.busy) {
|
||||
@@ -793,7 +797,7 @@ export struct FbEventEditSheet {
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.backgroundColor('#C0533F')
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.confirmDel = false;
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* ⇒ 三种态都要画:加载中 / 成功 / 失败。别只画成功态。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric } from '../common/Theme';
|
||||
import { FbColors, FbMetric, FbTint } from '../common/Theme';
|
||||
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
|
||||
import { FbCalEventFull } from '../data/FbCalRepo';
|
||||
import { FbCalBook } from '../model/Types';
|
||||
import { FbDot } from './Widgets';
|
||||
@@ -33,6 +34,13 @@ export struct FbEventSheet {
|
||||
/** 这条日程属于哪本日历(列表里那份已知,用来显示本色/名字) */
|
||||
@Param bookName: string = '';
|
||||
@Param bookColor: string = '#9AA0A6';
|
||||
/**
|
||||
* 外圈环色 —— **模块主题色**(用户 2026-10-19:「弹框的外圈颜色,
|
||||
* 跟模块的主题色保持一致」)。空 = 日程模块色。
|
||||
* ⚠️ 与 `bookColor` 分工不同:环是"我在哪个模块里",
|
||||
* 标题左侧那个点才是"这条日程属于哪本日历"(一本模块下可以有多本)。
|
||||
*/
|
||||
@Param accent: string = '';
|
||||
/** 能不能编辑(不能写的日历本不给"编辑"按钮) */
|
||||
@Param canEdit: boolean = false;
|
||||
@Event onClose: () => void = (): void => {
|
||||
@@ -40,6 +48,52 @@ export struct FbEventSheet {
|
||||
@Event onEdit: () => void = (): void => {
|
||||
};
|
||||
|
||||
/** 环色:显式给了就用给的,否则取日程模块色(与日程页其它浮层一致) */
|
||||
private accentColor(): string {
|
||||
if (this.accent !== '') {
|
||||
return this.accent;
|
||||
}
|
||||
const m: FbModuleDef | undefined = fbModuleOf('calendar');
|
||||
return m !== undefined ? fbThemeVars(m.color).brand : FbTint.brand;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标题文字。
|
||||
*
|
||||
* 🔴 2026-10-19:原来为空时写死「(无标题)」—— 但那条文案在**农历/节气**
|
||||
* 这些衍生条目上会误导(用户看到"农历显示无标题"以为数据错了,
|
||||
* 实际是标题为空时的兜底文案不合适)。派生条目本来就该用**日历本名**
|
||||
* 当兜底(「农历」「节假日」本身就是它是什么),自建日程才用「未命名日程」。
|
||||
*/
|
||||
private titleText(): string {
|
||||
const e: FbCalEventFull | null = this.event;
|
||||
if (e === null) {
|
||||
return '';
|
||||
}
|
||||
if (e.title !== '') {
|
||||
return e.title;
|
||||
}
|
||||
// 日历本名能说明"这是什么"(农历/节假日/节气…)
|
||||
if (this.bookName !== '') {
|
||||
return this.bookName;
|
||||
}
|
||||
return '未命名日程';
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情加载失败的说明。
|
||||
*
|
||||
* 🔴 2026-10-19:原来加载失败会把**整张卡**换成一句错误文案 ——
|
||||
* 而本地列表那份本来就能画标题/时间/地点 ⇒ 现在改成
|
||||
* **照常画内容 + 底下加一行小字说明**(见 `openEvent` 的说明)。
|
||||
*/
|
||||
private errorNote(): string {
|
||||
if (this.error === '') {
|
||||
return '';
|
||||
}
|
||||
return '部分详情没读回来:' + this.error;
|
||||
}
|
||||
|
||||
/** 时间显示:`10月15日 周三 09:00 - 10:30` / `全天 10月15日 周三` */
|
||||
private timeText(): string {
|
||||
const e: FbCalEventFull | null = this.event;
|
||||
@@ -150,13 +204,16 @@ export struct FbEventSheet {
|
||||
// (用户:「所有弹框都加线,保持一致」)。环色用**这本日历的颜色**,
|
||||
// 一眼能看出这条日程属于哪个日历本。
|
||||
Column() {
|
||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.bookColor }) {
|
||||
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
|
||||
Column({ space: 0 }) {
|
||||
// ---- 标题区 ----
|
||||
Row({ space: 10 }) {
|
||||
FbDot({ dia: 12, fillC: this.bookColor })
|
||||
Text(this.event !== null && this.event.title !== ''
|
||||
? this.event.title : '(无标题)')
|
||||
/* 🔴 2026-10-19:标题为空时要**说清这是什么**,而不是写「(无标题)」。
|
||||
农历 / 节气这类衍生条目的标题服务端是给了的(如「九月初一」),
|
||||
真为空的情况主要是「没填标题的自建日程」—— 那种回「未命名日程」
|
||||
比「(无标题)」更像人话。 */
|
||||
Text(this.titleText())
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
@@ -189,7 +246,10 @@ export struct FbEventSheet {
|
||||
.padding({ top: 20, bottom: 20 })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
} else if (this.event === null) {
|
||||
Text(this.error !== '' ? ('读不到这条日程:' + this.error) : '读不到这条日程')
|
||||
// 正常情况下不会走到这里(`openEvent` 一定会垫一份壳);
|
||||
// 只有"打开浮层时状态被清掉"这种瞬时态才可能空着 ——
|
||||
// 所以这里说的是"正在读取",不是"读不到"。
|
||||
Text('正在读取…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
@@ -215,6 +275,14 @@ export struct FbEventSheet {
|
||||
if (this.event.note !== '') {
|
||||
this.row('备注', this.event.note)
|
||||
}
|
||||
// 🔴 详情没拉全时给一行小字(而不是把整张卡变成"读不到")
|
||||
if (this.errorNote() !== '') {
|
||||
Text(this.errorNote())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
.padding({ top: 8 })
|
||||
}
|
||||
Row().height(8)
|
||||
}
|
||||
.width('100%')
|
||||
@@ -247,13 +315,16 @@ export struct FbEventSheet {
|
||||
this.onClose();
|
||||
})
|
||||
if (this.canEdit && this.event !== null) {
|
||||
/* 🔴 用户 2026-10-19:「点击日程弹出的框,编辑按钮也颜色也要修改。」
|
||||
⇒ 原来写死名册青 #4A7C82,在日程页里跟模块色(棕)不一致。
|
||||
改成 `accentColor()`(= 日程模块主题色,与卡片外环同色)。 */
|
||||
Text('编辑日程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.backgroundColor('#4A7C82')
|
||||
.backgroundColor(this.accentColor())
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.onEdit();
|
||||
|
||||
@@ -969,13 +969,14 @@ export struct FbPersonEditSheet {
|
||||
Row({ space: 10 }) {
|
||||
// 新建模式没有"删除"(这人还不存在)
|
||||
if (!this.isCreate) {
|
||||
// 🔴 用户 2026-10-19:「删除按钮全部用红色背景」
|
||||
Text('删除')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor('#C0533F')
|
||||
.fontColor(Color.White)
|
||||
.textAlign(TextAlign.Center)
|
||||
.height(42)
|
||||
.padding({ left: 18, right: 18 })
|
||||
.border({ width: 1, color: '#C0533F' })
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
if (!this.busy) {
|
||||
@@ -1070,7 +1071,7 @@ export struct FbPersonEditSheet {
|
||||
.textAlign(TextAlign.Center)
|
||||
.layoutWeight(1)
|
||||
.height(42)
|
||||
.backgroundColor('#C0533F')
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.onClick((): void => {
|
||||
this.confirmDel = false;
|
||||
|
||||
@@ -47,6 +47,9 @@ const FB_SHEET_BOTTOM_GAP: number = 16;
|
||||
* ⚠️ 不能直接用 `FbColors.red`(那是 `Resource`):本文件要拿它拼
|
||||
* `fbAlpha(RED, 0.08)`(透明度后缀)做底色,`Resource` 拼不了字符串。
|
||||
* ⚠️ color.json 改色时这里要同步改(只有这一个值,风险可控)。
|
||||
* 🔴 2026-10-19 起删除按钮统一**红底白字**(`FbColors.red`),
|
||||
* 这个常量只剩"拼浅红底"的历史用途;没有那种用法时它就是个死常量,
|
||||
* 留着只为对照 color.json —— 新代码请直接用 `FbColors.red`。
|
||||
*/
|
||||
const RED: string = '#C0533F';
|
||||
|
||||
@@ -557,8 +560,9 @@ export struct FbPersonSheet {
|
||||
* 🔴 用户 2026-10-18:「应该在详情页面的最下面加一个删除按钮。」
|
||||
* ⇒ 位置在**编辑/关闭之下**、并且与它们之间隔一条分隔线:
|
||||
* 手指从上往下扫时不会顺手点到;想删必须**再往下够**一次。
|
||||
* ⚠️ 视觉上只给文字 + 图标(不做成大红块):
|
||||
* 它是"偶尔用一次"的操作,大红按钮会抢走"编辑"的注意力。
|
||||
* 🔴 用户 2026-10-19:「删除按钮全部用红色背景。」
|
||||
* ⇒ 从"浅红底 + 红字"改成**红底白字**(与编辑/删除浮层里的
|
||||
* 删除按钮同一规格;位置仍在最底部 + 有二次确认兜底)。
|
||||
* ⚠️ 确认框在页面侧(`FbPeoplePage.confirmDelete`)—— 见 `onDelete` 的说明。 */
|
||||
if (this.canDelete) {
|
||||
Divider()
|
||||
@@ -570,16 +574,16 @@ export struct FbPersonSheet {
|
||||
.width(14)
|
||||
.height(14)
|
||||
/* ⚠️ PNG 图标必须 colorFilter(fillColor 只对 SVG 生效) */
|
||||
.colorFilter(fbTint(RED))
|
||||
.colorFilter(fbTint(FbTint.white))
|
||||
Text('删除联系人')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(RED)
|
||||
.fontColor(Color.White)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
}
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor(fbAlpha(RED, 0.08))
|
||||
.backgroundColor(FbColors.red)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.margin({ top: 10 })
|
||||
.onClick((): void => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 家书 FamilyBook · 顶栏(鸿蒙端)
|
||||
*
|
||||
* 用户 2026-10-12 定的形态:
|
||||
* · **应用图标 + 「家书」标题**贴左上(下行小字显示当前模块);
|
||||
* · **应用图标 + 当前模块名**贴左上(用户 2026-10-19 改:不再是固定的「家书」);
|
||||
* · **点图标 = 开抽屉**(用户要求:汉堡按钮去掉后,把这个功能挪到图标上);
|
||||
* · 顶栏底色**白色**(与下面的个人信息卡同色,连成一片);
|
||||
* · 顶栏**并入页面滚动流**,向上滑动时跟着内容一起滑走。
|
||||
@@ -19,12 +19,10 @@
|
||||
import { FbColors, FbIcon, FbMetric, FbTint, fbTint } from '../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../common/Store';
|
||||
import { FbSyncState, SYNC_DONE, SYNC_FAIL, SYNC_RUNNING, fbSyncState } from '../common/FbSyncStore';
|
||||
import { fbModuleOf } from '../model/Modules';
|
||||
import { fbModuleColor, fbModuleOf } from '../model/Modules';
|
||||
import { FbAvatar } from './Widgets';
|
||||
import { FbPersonSheet } from './FbPersonSheet';
|
||||
// ⚠️ `inst()` 拿单例(本工程所有 repo 都是这个约定,见 FbPersonRepo 文件头);
|
||||
// `FB_TOKEN_STORE` 在 **FbTokenStore.ets** 里(不是在 FbDeviceStore)。
|
||||
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { fbMediaUrl } from '../data/FbHttp';
|
||||
import { syncAll } from '../data/FbSyncAll';
|
||||
@@ -48,14 +46,6 @@ export struct FbTopBar {
|
||||
/** 同步面板是否打开 */
|
||||
@Local syncSheet: boolean = false;
|
||||
|
||||
/** 「我的详情」浮层是否打开(点头像触发) */
|
||||
@Local myDetailSheet: boolean = false;
|
||||
/** 「我的详情」里的人(null = 还没取到) */
|
||||
@Local myDetail: FbRosterPerson | null = null;
|
||||
/** 「我的详情」是否在加载 */
|
||||
@Local myDetailLoading: boolean = false;
|
||||
/** 「我的详情」的兜底提示(拿不到资料时显示) */
|
||||
@Local myDetailNote: string = '';
|
||||
|
||||
build() {
|
||||
// 🔴 顶栏横向镜像 —— 「质感握姿」(用户 2026-10-16 第 4 条):
|
||||
@@ -87,15 +77,17 @@ export struct FbTopBar {
|
||||
})
|
||||
|
||||
Column({ space: 0 }) {
|
||||
Text('家书')
|
||||
// 🔴 标题 = **当前模块名**(用户 2026-10-19:「不再是家书了,
|
||||
// 而是切换到那个模块,就显示哪个模块的标题」)。
|
||||
// ⇒ 模块名在页面内容部**不再重复出现**(各页头的大标题已删)。
|
||||
// ⚠️ 文字色用**模块主题色**(同一条要求:「出现文字的时候,
|
||||
// 例如观影模块,在左上角出现的时候,颜色也需要和主题色保持一致」)。
|
||||
// ⇒ 走 `fbModuleColor()` 而不是各页自己传色:顶栏只有一个,
|
||||
// 在这里算一次全模块都一致(也避免了 11 个页面各写一遍)。
|
||||
Text(this.moduleName())
|
||||
.fontSize(19)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
// 副标题显示当前模块名 —— "品牌 + 当前位置"一行说清
|
||||
Text(this.moduleName())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.fontColor(this.moduleColor())
|
||||
.maxLines(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
@@ -244,28 +236,13 @@ export struct FbTopBar {
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.margin({ left: FbMetric.gapTopActions })
|
||||
.onClick((): void => {
|
||||
this.openMyDetail();
|
||||
})
|
||||
// 「我的详情」浮层 —— 复用名册那个人详情组件(同一个组件、同一套字段)
|
||||
//
|
||||
// ⚠️ 用 `bindSheet` 而不是自己在 `build` 里叠一层遮罩:
|
||||
// 这条顶栏在**每个模块页**都会渲染(`topBarSlot`),
|
||||
// 自己叠遮罩要处理"盖住悬浮栏 / 盖住抽屉 / 手势返回"一堆事;
|
||||
// bindSheet 是系统能力,这些它都管(同步面板也是这么做的)。
|
||||
//
|
||||
// ⚠️ `onDisappear` 必须复位 `myDetailSheet`(点遮罩/下滑关闭都会走它),
|
||||
// 否则下次点头像**打不开**(状态位还是 true 但面板已经不在)。
|
||||
.bindSheet(this.myDetailSheet, this.myDetailPanel(), {
|
||||
// ⚠️ 用 `LARGE`(= 大高度、内容自适应)而不是 `FIT_CONTENT`:
|
||||
// 详情卡片有 `maxHeight: '72%'` 的内部约束,FIT_CONTENT 下
|
||||
// 系统会试图把 sheet 压到"刚好包住内容",与那个百分比约束打架。
|
||||
height: SheetSize.LARGE,
|
||||
dragBar: true,
|
||||
showClose: false,
|
||||
backgroundColor: FbColors.surface,
|
||||
onDisappear: (): void => {
|
||||
this.myDetailSheet = false;
|
||||
}
|
||||
/* 🔴 v61(用户 2026-10-19):「左侧抽屉,把通知消息、个人设置、
|
||||
修改密码、系统设置,都去掉,放在个人设置页,就是在点击右侧头像那里。」
|
||||
⇒ 点头像**进个人设置页**(整页),不再弹「我的详情」浮层。
|
||||
⚠️ 换的是 `switchModule` 而不是自己开浮层:整页才有地方放
|
||||
"改密码 / 系统设置 / 退出登录",而那三样都是页面级的。
|
||||
⚠️ 顶栏在**每个模块页**都存在 ⇒ 这里只切状态位,渲染由 Index 分发。 */
|
||||
this.app.switchModule('__account');
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
@@ -300,11 +277,32 @@ export struct FbTopBar {
|
||||
.border({ width: { bottom: 1 }, color: FbColors.line2 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶栏标题 = 当前模块名。
|
||||
*
|
||||
* 🔴 两个非模块路由(通知 / 设置)也要有名字 —— 它们不在 `FB_MODULES` 里,
|
||||
* 不认就会回落「家书」,而那一页明明不是首页。
|
||||
*/
|
||||
private moduleName(): string {
|
||||
const m = fbModuleOf(this.app.activeModule);
|
||||
const k: string = this.app.activeModule;
|
||||
if (k === '__notifications') {
|
||||
return '通知消息';
|
||||
}
|
||||
if (k === '__settings') {
|
||||
return '设置';
|
||||
}
|
||||
if (k === '__account') {
|
||||
return '个人设置';
|
||||
}
|
||||
const m = fbModuleOf(k);
|
||||
return m !== undefined ? m.name : '家书';
|
||||
}
|
||||
|
||||
/** 顶栏标题色 = 当前模块的主题色(认不出回落到品牌砖红) */
|
||||
private moduleColor(): string {
|
||||
return fbModuleColor(this.app.activeModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动同步(面板里的按钮)。
|
||||
*
|
||||
@@ -333,119 +331,6 @@ export struct FbTopBar {
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开「我」的详情浮层(点右上角头像触发)。
|
||||
*
|
||||
* 🔴 v59b(用户 2026-10-18):「点击头像进入我的详情页面。」
|
||||
*
|
||||
* 为什么**先打开浮层、再补数据**(而不是等网络回来才开):
|
||||
* 本地优先是本应用的立身之本 —— 点一下要**立刻**有东西出来,
|
||||
* 断网时也必须能看。本地库那份已经够画详情的前几段,
|
||||
* 网络回来后再原地补全(`fetchDetail`)。
|
||||
*
|
||||
* ⚠️ 失败时**不弹错误**:`me.id` 拿不到(未登录/数据还没同步下来)就只弹一句
|
||||
* "资料还没同步下来",而不是让用户对着一个空白浮层猜。
|
||||
*/
|
||||
private async openMyDetail(): Promise<void> {
|
||||
const meId: number = this.app.session.me.id;
|
||||
if (meId <= 0) {
|
||||
this.myDetailNote = '还没拿到你的资料,请先同步一次';
|
||||
this.myDetailSheet = true;
|
||||
return;
|
||||
}
|
||||
/* ⚠️ 先开浮层(`myDetailSheet = true`)再 await:
|
||||
否则网络慢时用户点了一下**几十毫秒内毫无反应**,会以为按钮坏了。
|
||||
`loading` 由浮层自己显示,观感上"点开就有东西"。 */
|
||||
this.myDetailLoading = true;
|
||||
this.myDetail = null;
|
||||
this.myDetailSheet = true;
|
||||
try {
|
||||
const local: FbRosterPerson | null = await FbPersonRepo.inst().byId(meId);
|
||||
this.myDetail = local;
|
||||
this.myDetailLoading = false;
|
||||
// 本地有 ⇒ 顺手用网络那份补全(口径与名册里点某人完全一致)
|
||||
const token: string = FB_TOKEN_STORE.get();
|
||||
if (token !== '' && local !== null) {
|
||||
const full: FbRosterPerson | null = await FbPersonRepo.inst().fetchDetail(meId, token);
|
||||
// ⚠️ 浮层可能已经被关掉、或期间换了人 ⇒ 只在"还开着同一个人"时才覆盖
|
||||
if (full !== null && this.myDetailSheet
|
||||
&& this.myDetail !== null && this.myDetail.id === meId) {
|
||||
this.myDetail = full;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const e: BusinessError = err as BusinessError;
|
||||
hilog.warn(0x0000, 'FamilyBook', '取我的详情失败:%{public}s',
|
||||
e.message !== undefined ? e.message : JSON.stringify(err));
|
||||
this.myDetailLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 「我的详情」面板(点头像弹出)。
|
||||
*
|
||||
* ⚠️ 三种状态都要有话说(用户 2026-10-18 报过"点了没反应"这类问题):
|
||||
* · 加载中 ⇒ 「正在读取…」(不要让浮层空着)
|
||||
* · 拿到了 ⇒ 复用 `FbPersonSheet`(姓名/称呼/电话/生日/单位/标签…)
|
||||
* · 拿不到 ⇒ 显式一句 `myDetailNote`(而不是空白)
|
||||
*/
|
||||
@Builder
|
||||
myDetailPanel() {
|
||||
Column() {
|
||||
if (this.myDetailLoading) {
|
||||
Row({ space: 8 }) {
|
||||
LoadingProgress().width(18).height(18).color(FbColors.brand)
|
||||
Text('正在读取我的资料…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding({ top: 28, bottom: 28 })
|
||||
} else if (this.myDetail !== null) {
|
||||
FbPersonSheet({
|
||||
person: this.myDetail,
|
||||
/* 🔴🔴 必须传 embedded(2026-10-18 真机踩出来的真 bug):
|
||||
`FbPersonSheet` 的根节点是"铺满全屏 + 内部居中"的浮层形态
|
||||
(它的设计宿主是"页面里叠遮罩 + Stack 居中",名册页就这么用)。
|
||||
直接塞进 `bindSheet`(内容要自适应高度)会**测量互相打架**:
|
||||
真机症状是**每次点击 sheet 都创建了、紧接着
|
||||
`AceDrag: Drag gesture has been canceled` —— 起来立刻被关**,
|
||||
界面上看就是"点了没反应"(极易误判成"点击没接上")。 */
|
||||
embedded: true,
|
||||
/* 🔴 不给「删除」:这是"我自己的详情"。
|
||||
删除自己既没意义又危险(服务端是物理删除,删完账号都进不去)。
|
||||
名册里的详情卡才显示删除(那里 `canDelete` 取默认值 true)。 */
|
||||
canDelete: false,
|
||||
onClose: (): void => {
|
||||
this.myDetailSheet = false;
|
||||
},
|
||||
// ⚠️ 这里**不给编辑入口**:编辑要跳名册页那套编辑浮层(含标签/电话增删),
|
||||
// 而顶栏在**每个模块页**都存在 —— 在这里接编辑会变成
|
||||
// "在日程页里编辑家庭成员的资料",职责与页面上下文都不对。
|
||||
// 要看/改详细资料,从名册进(那是它的主场)。
|
||||
onEdit: (): void => {
|
||||
this.myDetailSheet = false;
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Text(this.myDetailNote !== ''
|
||||
? this.myDetailNote
|
||||
: '暂时拿不到你的资料,下拉同步一次再试')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.textAlign(TextAlign.Center)
|
||||
.width('100%')
|
||||
.padding({ top: 28, bottom: 28, left: 24, right: 24 })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
/* ⚠️ 不要给这层加 `constraintSize({maxHeight})`:
|
||||
sheet 的高度由 `SheetSize.LARGE` 决定,卡片自己已有 `maxHeight:72%`;
|
||||
再叠一层百分比约束只会让"高度该多高"变成三方打架
|
||||
(真机症状就是浮层起来立刻被关)。 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步面板(点顶栏图标弹出)。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user