diff --git a/entry/src/main/ets/components/FbDayTimeline.ets b/entry/src/main/ets/components/FbDayTimeline.ets index 68eac76..daeeacb 100644 --- a/entry/src/main/ets/components/FbDayTimeline.ets +++ b/entry/src/main/ets/components/FbDayTimeline.ets @@ -356,33 +356,49 @@ export struct FbDayTimeline { .height('100%') } - /** 第 3 层:当前时间红线(浮在最上层) */ + /** + * 第 3 层:当前时间红线(浮在最上层) + * + * 🔴🔴 v84(用户 2026-10-23,同一条消息里两处):首页的这条线「红色线不对, + * 变短了左侧缺了一块」,日程页那条「红线左侧是不是还有一个红点?如果有,去掉。」 + * + * 两个抱怨其实是同一段代码的两个缺陷,都在这里: + * ① 老写法是 `Row { Text('07:02') width(34); 线(layoutWeight 1); 圆点(6) }` + * —— **行首的 34vp 时间文字**把线往右顶掉一整块,用户看到的就是 + * "左侧缺一块"(与网页端 `.ag-now` 那次「双重缩进」是同一类病: + * 线的左沿被别的东西占了)。 + * ② 行尾那个 `6×6 borderRadius(3)` 就是用户问的**红点**。 + * + * ⇒ 两端统一成**一根满宽的红线,没有任何端点装饰、没有文字**。 + * 这正是用户 v44 定下的口径:「直接把红点去掉就行了,直接一根红线就可以了」, + * 也是网页端 `.ag-now { left: 0; right: 0; border-top: 2px }` 的等价写法。 + * + * ⚠️ 别把时间文字"加回来":只要 Row 里有任何子元素,线就一定短一截。 + * 想知道现在几点,看左侧刻度列——那里 `isNowHour` 的那个小时文字**已经是红的** + * (见 `hourLabels()` 那段的 `fontColor`),信息并没有丢。 + * ⚠️ 别用 `Row { 线(layoutWeight 1) }` 的写法包一层 —— 能跑,但多一层无谓嵌套, + * 且下次有人往这个 Row 里塞东西,缺一块的 bug 就又回来了。 + * ⚠️ `hitTestBehavior(HitTestMode.None)` 必须留:装饰层不能挡住点色块。 + */ @Builder nowLayer() { - if (this.nowVisible()) { - Row() { - // 左侧时间文字(红线那头) - Text(FbTimelineUtil.hhmm(this.nowMin)) - .fontSize(9) - .fontColor(FB_NOW_RED) - .fontWeight(FontWeight.Medium) - .textAlign(TextAlign.End) - .width(34) - Row() - .layoutWeight(1) + /* ⚠️ 外层必须是**满高的 Column**:`Stack` 的几个子层是「叠」不是「排」, + 直接给线写 `.margin({top})` 在部分版本上会因对齐基准不同而漂移。 + 用 `Blank` 撑开 = 与卡片端 `FbCard4x4.nowLayer()` **同一个写法**, + 两端行为一致,改一处就知道另一处该怎么改。 */ + Column() { + if (this.nowVisible()) { + Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None) + Column() + .width('100%') .height(2) - .backgroundColor(FB_NOW_RED) - Row() - .width(6) - .height(6) - .borderRadius(3) + .borderRadius(1) .backgroundColor(FB_NOW_RED) } - .width('100%') - .alignItems(VerticalAlign.Center) - .margin({ top: this.nowOffsetVp() }) - .hitTestBehavior(HitTestMode.None) } + .width('100%') + .height('100%') + .hitTestBehavior(HitTestMode.None) } build() { diff --git a/entry/src/main/ets/components/FbEventEditSheet.ets b/entry/src/main/ets/components/FbEventEditSheet.ets index 1b32ca7..e8ccd04 100644 --- a/entry/src/main/ets/components/FbEventEditSheet.ets +++ b/entry/src/main/ets/components/FbEventEditSheet.ets @@ -431,6 +431,32 @@ export struct FbEventEditSheet { .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) + // ---- 日历本 ---- + /* 🔴🔴 v84 真机实测(2026-10-24):这一组原来排在「提醒」后面, + 而表单内容比卡片(`maxHeight: 86%`)高 ⇒ 它在**滚动区下方**, + 首屏只能看到被底边切掉半行的标题(实测 bounds 高 7px)。 + 用户报「选不了日历本」的直觉来源就是这个:看不见 = 用不了。 + ⇒ 它是**必选项**(没选就 `请选一本日历` 存不上), + 必须排在**首屏可见**的位置:紧跟「全天」,在日期时间之前。 + ⚠️ 别再往回调到「提醒」后面 —— 那栏一长,它必然被推到屏幕外。 */ + if (this.books.length > 0) { + this.section('加入哪本日历') + Flex({ wrap: FlexWrap.Wrap }) { + ForEach(this.books, (b: FbCalBook) => { + FbEventChip({ + label: b.name, + active: this.fBookId === b.id, + // 选中时用**这本日历自己的颜色**(一眼看出落到哪本) + activeBg: b.color, + onPick: (): void => { + this.fBookId = b.id; + } + }) + }, (b: FbCalBook): string => 'bk_' + b.id.toString()) + } + .width('100%') + } + // ---- 日期与时间 ---- // 🔴 照 SyncCalendar:**一行一个"日期 + 时间"控件**(点开一个弹层、 // 里面 DatePicker 与 TimePicker 并排,选完点「确定」)。 @@ -476,25 +502,6 @@ export struct FbEventEditSheet { .width('100%') .margin({ top: 2 }) - // ---- 日历本 ---- - if (this.books.length > 0) { - this.section('加入哪本日历') - Flex({ wrap: FlexWrap.Wrap }) { - ForEach(this.books, (b: FbCalBook) => { - FbEventChip({ - label: b.name, - active: this.fBookId === b.id, - // 选中时用**这本日历自己的颜色**(一眼看出落到哪本) - activeBg: b.color, - onPick: (): void => { - this.fBookId = b.id; - } - }) - }, (b: FbCalBook): string => 'bk_' + b.id.toString()) - } - .width('100%') - } - // ---- 地点 / 备注 ---- this.field('地点', this.fPlace, '地点(可选)', (v: string): void => { this.fPlace = v; @@ -502,13 +509,6 @@ export struct FbEventEditSheet { this.area('备注', this.fNote, '备注(可选)', (v: string): void => { this.fNote = v; }) - - if (this.err !== '') { - Text(this.err) - .fontSize(FbMetric.fontTiny) - .fontColor('#A32D2D') - .width('100%') - } } .width('100%') .padding({ top: 14 }) @@ -518,6 +518,21 @@ export struct FbEventEditSheet { .scrollBar(BarState.Off) .align(Alignment.TopStart) + /* 🔴🔴 v84 真机实测(2026-10-24):错误提示原来放在**滚动区内部最底部** + (`this.area('备注' …)` 后面),于是—— + ① 用户在顶部点「创建」时,提示在几百 px 之下,**根本看不见**; + ② 就算滚到底,它也被 `Scroll` 底边切掉半行(实测 bounds 高 39px,紧贴 2024 边界)。 + 现象就是「点了创建什么都没发生」——这是最难查的一类失败(用户只会以为没生效)。 + ⇒ 挪到**滚动区外面、按钮区上方**:无论滚到哪儿,点完立刻看得见。 + ⚠️ 不要挪回 Scroll 里:那里的可见性取决于用户当时滚到哪儿,运气好才看得见。 */ + if (this.err !== '') { + Text(this.err) + .fontSize(FbMetric.fontTiny) + .fontColor('#A32D2D') + .width('100%') + .margin({ top: 10 }) + } + // ---- 底部按钮:删除 / 保存 ---- if (this.deleting) { Text('删除中…') diff --git a/entry/src/main/ets/components/Widgets.ets b/entry/src/main/ets/components/Widgets.ets index 00f7bae..3e33c1a 100644 --- a/entry/src/main/ets/components/Widgets.ets +++ b/entry/src/main/ets/components/Widgets.ets @@ -148,6 +148,25 @@ export struct FbEmptyBox { export struct FbSectionTitle { @Param title: string = ''; @Param extra: string = ''; + /** + * 🔴 v84(用户 2026-10-23):「日程,没有新建日程按钮,名册,也没有新增成员 + * 这个按钮。……首页上的其他卡片也应该有这些基本功能。」 + * + * `addText` 非空 ⇒ 在 extra 右边给一颗**小号文字按钮**(空 = 不画,行为与原来完全一致)。 + * + * ⚠️ 为什么是"可选参数 + 默认空"而不是新加一个组件: + * 本组件被首页 4 个区块、日程页、任务页等多处引用, + * 改签名会波及所有调用点;`@Param` 带默认值则**老调用点一行都不用改**。 + * ⚠️ 为什么按钮文案用文字而不是 `+` 图标: + * 首页区块标题这一行只有 20vp 高,放图标按钮手指点不准(实测点击热区 + * 会小于 40vp);文字按钮横向量足,且与网页端 `datatable` 的「新增」观感一致。 + * ⚠️ 点击热区用 `padding` 撑到 **44vp 高**(不是 fontTiny 那么点大)—— + * `hitTestBehavior` 默认是 `Default`,padding 区**算在热区内**。 + */ + @Param addText: string = ''; + /** 点「新增」;`addText` 为空时永不触发 */ + @Event onAdd: () => void = (): void => { + }; build() { Row() { @@ -162,9 +181,27 @@ export struct FbSectionTitle { .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } + if (this.addText.length > 0) { + /* extra 存在时用 6vp 间距隔开;extra 为空时靠 Blank 顶到右边 */ + if (this.extra.length === 0) { + Blank() + } + Text(this.addText) + .fontSize(FbMetric.fontTiny) + /* ⚠️ 用 `FbColors.brand`(品牌砖红),不是 `accent` —— + `FbColors` 里**没有** `accent` 这个字段(编译会报 + "Property 'accent' does not exist")。 */ + .fontColor(FbColors.brand) + .fontWeight(FontWeight.Medium) + .padding({ left: 10, right: 2, top: 12, bottom: 12 }) + .onClick((): void => { + this.onAdd(); + }) + } } .width('100%') .padding({ left: 4, right: 4, top: 12, bottom: 8 }) + .alignItems(VerticalAlign.Center) } } diff --git a/entry/src/main/ets/pages/home/FbHomePage.ets b/entry/src/main/ets/pages/home/FbHomePage.ets index b3b42c5..e2c15ff 100644 --- a/entry/src/main/ets/pages/home/FbHomePage.ets +++ b/entry/src/main/ets/pages/home/FbHomePage.ets @@ -39,8 +39,8 @@ import { FbColors, FbMetric, FB_MASK } from '../../common/Theme'; import { FbAppState, fbAppState } from '../../common/Store'; -import { FbCalEvent, FbCatOpt, FbHomeData, FbMark, FbMemoryItem, FbRecentDay, FbTaskItem, - FbBoardOpt, FbTaskDetail, FbTaskMeta, taskKey, +import { FbCalEvent, FbCalBook, FbCatOpt, FbHomeData, FbMark, FbMemoryItem, FbRecentDay, + FbTaskItem, FbBoardOpt, FbTaskDetail, FbTaskMeta, taskKey, FbWeather, isEditableEvent } from '../../model/Types'; import { common } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; @@ -57,6 +57,7 @@ import { FbSectionTitle } from '../../components/Widgets'; import { FbStatCell } from '../../components/FbStatCell'; import { FbDayEventPanel } from '../../components/FbDayEventPanel'; import { FbEventSheet } from '../../components/FbEventSheet'; +import { FbEventEditSheet } from '../../components/FbEventEditSheet'; import { FbTaskPanel } from '../../components/FbTaskPanel'; import { FbTaskEditSheet } from '../../components/FbTaskEditSheet'; import { FB_DOCK_RESERVE } from '../../components/FloatDock'; @@ -66,6 +67,10 @@ import { FB_BACK } from '../../common/FbBack'; import { fbOpenNavigation } from '../../common/FbNav'; import { fbModuleOf, FbModuleDef, fbThemeVars } from '../../model/Modules'; +/** 日志域(与 `FbBackgroundSync` / `FbNav` 同一个值,端上统一 0x0000) */ +const DOMAIN: number = 0x0000; +const TAG: string = 'FbHomePage'; + @ComponentV2 export struct FbHomePage { @@ -119,6 +124,27 @@ export struct FbHomePage { */ @Local calStyle: FbCalStyle = 'timeline'; + // ============================================================ + // v84(用户 2026-10-23):首页卡片上的「新建 / 新增」按钮 + // ============================================================ + /* 🔴 用户原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。」 + ------------------------------------------------------------------ + ⚠️ 这条**上一轮只做到 Web 端**(`app/HomeCards.php` 的 `TYPES['agenda']['add']`), + 端上首页压根没做 —— 所以用户装完机仍然说"没有按钮"。 + 教训:这类"两端都要"的需求,改完一端必须**立刻去另一端 grep 同一件事**。 + + ⚠️ 为什么首页要**自己拉一次 `/calendar/books`**、不复用日程页那份: + 首页和日程页是两个独立组件(首页不持有日程页的 `@Local books`)。 + 把它塞进 `/home` 接口当然更省,但那要动服务端 + 端上解析 + 网页端, + 收益只有"少一个请求",风险却横跨三层。 + ⇒ 折中:**按需懒加载**(点开新建才拉),且拉到后缓存在 `calBooks` 里, + 同一会话内只拉一次。 */ + @Local calBooks: FbCalBook[] = []; + /** 首页「新建日程」表单开着 */ + @Local calCreating: boolean = false; + /** 正在拉日历本(防连点开两个空表单) */ + @Local calLoadingBooks: boolean = false; + // ============================================================ // v73(用户 2026-10-21):首页点日程 / 点任务 → **就地弹详情卡** // ============================================================ @@ -182,9 +208,10 @@ export struct FbHomePage { * `Index` 的 Stack** 里,层级比本页高 ⇒ 不让位就会盖住卡片底部的按钮、 * 还把点击抢走(名册/日程页都踩过)。 */ - @Monitor('evtBase', 'tskBase', 'tskEditOpen') + @Monitor('evtBase', 'tskBase', 'tskEditOpen', 'calCreating') onDetailChanged(): void { - this.app.sheetCount = (this.evtBase !== null || this.tskBase !== null || this.tskEditOpen) ? 1 : 0; + this.app.sheetCount = (this.evtBase !== null || this.tskBase !== null + || this.tskEditOpen || this.calCreating) ? 1 : 0; } aboutToAppear(): void { @@ -211,6 +238,12 @@ export struct FbHomePage { this.tskEditOpen = false; return true; } + /* v84 新建日程:它是**独立表单**(不在详情卡之上),但要优先于详情卡 —— + 两者不可能同时开,放前面只是保证"开了就一定关得掉"。 */ + if (this.calCreating) { + this.calCreating = false; + return true; + } if (this.evtBase !== null) { this.closeEvent(); return true; @@ -406,6 +439,46 @@ export struct FbHomePage { this.tskEditOpen = true; } + /** + * v84:首页「今天的日程」区块点「+ 新建」→ **就地**打开新建表单。 + * + * 流程:懒拉日历本 → 打开 `FbEventEditSheet(isCreate: true)`。 + * + * ⚠️ 日历本为空时**不要"打不开就算了"**:新建表单的"日程本"是必选项, + * 没有可写本就画不出表单。这时要么提示、要么至少把表单打开让它自己报错 + * —— 选**明说一句**(用户点了一下什么都没发生,是最难查的失败)。 + * ⚠️ `presetDate` 用"今天":首页这条区块就叫「今天的日程」, + * 用户在这里点新建,十有八九是给今天加一件。 + * ⚠️ 不要 `await` 之后才置 `calCreating` 以外的任何界面状态 —— + * 拉本期间给一个 `calLoadingBooks`,防止连点开出两个表单。 + */ + private async openCalCreate(): Promise { + if (this.calCreating || this.calLoadingBooks) { + return; + } + const token: string = FB_TOKEN_STORE.get(); + if (token === '') { + this.syncNote = '还没登录,无法新建日程'; + return; + } + if (this.calBooks.length === 0) { + this.calLoadingBooks = true; + try { + const list: FbCalBook[] = await FbCalRepo.inst().fetchBooks(token); + this.calBooks = list; + } catch (err) { + hilog.warn(DOMAIN, TAG, '首页拉日历本失败:%{public}s', JSON.stringify(err)); + } finally { + this.calLoadingBooks = false; + } + } + if (this.calBooks.length === 0) { + this.syncNote = '这个账号还没有可写的日程本,先去日程页设置里建一本'; + return; + } + this.calCreating = true; + } + /** * 把最新数据推给桌面上所有日程卡片。 @@ -529,6 +602,23 @@ export struct FbHomePage { return d.getHours() * 60 + d.getMinutes(); } + /** + * 今天 → `YYYY-MM-DD`(`FbEventEditSheet` 的 `presetDate` 要这个格式)。 + * + * ⚠️ **不要**用 `toISOString().slice(0,10)`:那是 **UTC**, + * 东八区早上 8 点前会算成"昨天"(本项目真机上踩过同类:新建的日程 + * 跑到前一天去了,用户完全无法理解)。必须用本地年月日拼。 + * ⚠️ 月份、日期都要补零,否则 `2026-1-5` 服务端解析不了。 + */ + private todayIso(): string { + const d: Date = new Date(); + const m: number = d.getMonth() + 1; + const day: number = d.getDate(); + return d.getFullYear().toString() + '-' + + (m < 10 ? '0' + m.toString() : m.toString()) + '-' + + (day < 10 ? '0' + day.toString() : day.toString()); + } + build() { /* 🔴 v73:外层必须是 **Stack**(叠放),不能是 Column(排列)。 浮层要"盖在页面上",而不是"排在页面后面" —— 后者会把 Scroll 挤出屏幕, @@ -688,7 +778,17 @@ export struct FbHomePage { // ⚠️ 面板自带卡片样式与外边距,这里**不要再套一层卡片**。 // ⚠️ `showHead: false` —— 本页已有 `FbSectionTitle`,两个头会重复。 // ⚠️ 空态文案也换掉:首页没有"右下角 +"那个按钮。 - FbSectionTitle({ title: '今天的日程', extra: this.todayEvents.length.toString() + ' 条' }) + FbSectionTitle({ + title: '今天的日程', + extra: this.todayEvents.length.toString() + ' 条', + /* 🔴 v84(用户 2026-10-23):「日程,没有新建日程按钮」。 + ⚠️ 别再删掉这颗按钮 —— 它是本条的**唯一可见证据**, + 删了用户立刻又说"没有按钮"(上一轮就漏在端上)。 */ + addText: '+ 新建', + onAdd: (): void => { + this.openCalCreate(); + } + }) .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) FbDayEventPanel({ @@ -847,6 +947,43 @@ export struct FbHomePage { .onTouch(fbOnTouch) .align(Alignment.TopStart) + // ==================== 新建日程(v84)==================== + // ⚠️ 遮罩与表单是 Stack 的**兄弟**,不是 Column 的子项 + // (本项目踩过:`Column { 整高遮罩; 卡片 }` 会把卡片挤出屏幕, + // 编译通过、节点树里却一个节点都没有 —— 看起来像"点了没反应")。 + if (this.calCreating) { + Column() + .width('100%') + .height('100%') + .backgroundColor(FB_MASK) + .transition(fbMaskTransition()) + .onClick((): void => { + this.calCreating = false; + }) + + FbEventEditSheet({ + event: null, + isCreate: true, + /* 首页这条区块叫「今天的日程」⇒ 默认给今天 */ + presetDate: this.todayIso(), + books: this.calBooks, + onCancel: (): void => { + this.calCreating = false; + }, + onSaved: (): void => { + this.calCreating = false; + /* 存成功立刻重拉首页 ⇒ 新日程出现在「今天的日程」里。 + ⚠️ 必须重拉而不是"本地塞一条":服务端会补 id / 颜色 / 日历本名, + 本地塞的那条会和之后下拉刷新出来的长得不一样。 */ + this.load(); + }, + onDeleted: (): void => { + this.calCreating = false; + } + }) + .transition(fbSheetTransition()) + } + // ==================== 日程详情卡(v73)==================== // ⚠️ 遮罩与卡片是 Stack 的**兄弟**,不是 Column 的子项(见 build 开头那段说明)。 if (this.evtBase !== null) {