fix(日视图): 修复列表视图待办/全天区重复渲染;折叠改为每天独立 (#68 后续)

## 修的
### 1. 列表视图重复显示两遍(已修)
`DayTimelineView.build()` 里 `this.topBar()` 写在 `if (this.scrollable)` **之前**,
`else` 分支里又调了一次 → 列表视图(scrollable=false)渲染两遍。
删掉 else 里的那次重复调用。

### 2. 折叠状态"要收都收、要展都展"(已修)
原来 `topExpanded` 是 `DayTimelineView` 的 @State,
但列表视图里多个卡片是**同一个组件的多个实例**——
用户感知到的"统一联动"来自重复渲染的那两份共用状态。
移除重复渲染后,每个卡片实例各自独立。

## 未解决的(如实记录)
列表视图下 `topBar` 在 `DayTimelineView` 内部**渲染不出来**
(诊断日志确认 `todos=2` 有数据、组件被调用,但 dumpLayout 里查不到节点,
时间轴直接从 y=407 开始)。

已尝试且无效的方案:
- 放进 `timelineBody` 内部 → 被固定高度 viewH() 挤掉
- 放在 `DayTimelineView` 外层卡片里(新建 AgendaTopBar 组件)→ 仍不渲染
- `flexShrink(0)` 包裹 → 无效
- build 根节点从 `if` 改为 `Column` 包裹 → 无效

推测根因:列表视图卡片高度由 `DayTimelineView` 的 viewH() 决定,
外层 Column 加内容后 ListItem 高度测量异常,新增部分被裁掉。

## 当前状态
- 周/月视图:固定区 + 3条折叠 + 展开箭头 —— **已验收通过**(见 #68)
- 列表视图:重复渲染已修,但待办/全天区显示不出来 —— **未解决**

## 编译
BUILD SUCCESSFUL
This commit is contained in:
2026-11-05 16:37:39 +08:00
parent 2c565747ba
commit ab718fc00f
+169 -5
View File
@@ -4299,6 +4299,154 @@ class AgendaTimelineGroup {
timeline: DayTimeline = new DayTimeline(); timeline: DayTimeline = new DayTimeline();
} }
/**
* 列表视图的"待办 + 全天/跨天"区(#68)。
*
* 为什么做成独立组件:列表视图每天一张卡片,需要**每天独立折叠**
* (用户反馈:原来多个卡片共用一个折叠状态,"要收都收、要展都展")。
* 独立组件 = 独立 @State topExpanded,天然互不影响。
*
* 为什么放在 DayTimelineView 外面:后者内部时间轴是固定高度 viewH(),
* 在它里面插内容会被挤出去(实测渲染不出来)。
*/
@Component
struct AgendaTopBar {
@Prop todos: DisplayEvent[] = [];
@Prop allDay: DisplayEvent[] = [];
onPick: (e: DisplayEvent) => void = (e: DisplayEvent): void => {
};
onToggleTodo: (t: DisplayEvent) => void = (t: DisplayEvent): void => {
};
/** 本卡片的折叠状态(每天独立) */
@State private expanded: boolean = false;
/** 合并条目:待办在前、全天在后 */
private items(): TopBarItem[] {
const out: TopBarItem[] = [];
for (const td of this.todos) {
const it = new TopBarItem();
it.isTodo = true;
it.ev = td;
out.push(it);
}
for (const ev of this.allDay) {
const it = new TopBarItem();
it.isTodo = false;
it.ev = ev;
out.push(it);
}
return out;
}
private total(): number {
return this.todos.length + this.allDay.length;
}
private visible(): TopBarItem[] {
const all: TopBarItem[] = this.items();
if (this.expanded || all.length <= 3) {
return all;
}
return all.slice(0, 3);
}
build() {
Column() {
if (this.total() > 0) {
Column({ space: 4 }) {
ForEach(this.visible(), (item: TopBarItem) => {
if (item.isTodo) {
Row({ space: 8 }) {
Stack() {
if (item.ev.completed) {
Path()
.width(18)
.height(18)
.commands('M5 9.5 L7.5 12.5 L13 6.5')
.stroke('#FFFFFF')
.strokeWidth(2)
.strokeLineCap(LineCapStyle.Round)
.strokeLineJoin(LineJoinStyle.Round)
.fill(Color.Transparent)
}
}
.width(18)
.height(18)
.borderRadius(9)
.backgroundColor(item.ev.completed ? item.ev.color : 'rgba(0,0,0,0)')
.border({ width: 1.5, color: item.ev.color })
.onClick(() => {
this.onToggleTodo(item.ev);
})
Text(item.ev.title === '' ? '(无标题待办)' : item.ev.title)
.fontSize(12)
.fontColor($r('app.color.text_primary'))
.decoration({
type: item.ev.completed ? TextDecorationType.LineThrough : TextDecorationType.None
})
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.alignItems(VerticalAlign.Center)
.width('100%')
.height(24)
.padding({ left: 8, right: 8 })
.borderRadius(6)
.backgroundColor($r('app.color.card_bg'))
.border({ width: 0.5, color: $r('app.color.shadow_color') })
} else {
Row({ space: 6 }) {
Text(item.ev.isAllDay ? '全天' : '跨天')
.fontSize(9)
.fontColor('#FFFFFF')
.backgroundColor('#26000000')
.borderRadius(6)
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
Text(item.ev.title === '' ? '(无标题)' : item.ev.title)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.alignItems(VerticalAlign.Center)
.width('100%')
.height(24)
.padding({ left: 8, right: 8 })
.borderRadius(6)
.backgroundColor(item.ev.color)
.onClick(() => this.onPick(item.ev))
}
}, (item: TopBarItem) => `ab_${item.isTodo ? 't' : 'a'}_${item.ev.id}_${item.ev.completed ? 1 : 0}`)
if (this.total() > 3) {
Row({ space: 4 }) {
Text(this.expanded ? '收起' : `还有 ${this.total() - 3} 条`)
.fontSize(10)
.fontColor($r('app.color.text_hint'))
Text(this.expanded ? '⌃' : '⌄')
.fontSize(11)
.fontColor($r('app.color.text_hint'))
}
.width('100%')
.height(18)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.expanded = !this.expanded;
})
}
}
.width('100%')
.padding({ bottom: 4 })
}
}
.width('100%')
}
}
/** /**
* 顶部固定区(全天日程 + 待办)的一个条目(#68)。 * 顶部固定区(全天日程 + 待办)的一个条目(#68)。
* ArkTS 不允许内联对象字面量类型,必须用命名 class。 * ArkTS 不允许内联对象字面量类型,必须用命名 class。
@@ -4628,11 +4776,23 @@ struct DayTimelineView {
build() { build() {
Column() { Column() {
// 时间轴主体(含顶部"全天/跨天"条 —— 它们随内容一起滚动,向下滑时跟 0 点一起上移)。
// ⭐ 顶部固定区(待办 + 全天/跨天)#68:放在 Scroll **之外** → 不随时间轴滚动。 // ⭐ 顶部固定区(待办 + 全天/跨天)#68:放在 Scroll **之外** → 不随时间轴滚动。
// 原来它在 timelineBody 里,会被"自动滚到当前时刻"带走, // 原来它在 timelineBody 里,会被"自动滚到当前时刻"带走,
// 导致下午 16 点打开时全天日程看不见。 // 导致下午 16 点打开时全天日程看不见。
this.topBar() // ⚠️ 只对 scrollable(周/月视图)做固定区:
// 列表视图(scrollable=false)的卡片高度由"按日程起止截断的时间轴"决定,
// 在里面再插一个固定区会被固定高度的 timelineBody 挤到卡片外(实测红块跑到 y=620)。
// 列表视图逐天展开、不存在"自动滚到当前时刻"的问题,所以它继续把
// 待办+全天放在 timelineBody 内部跟随内容即可。
if (this.scrollable) {
Column() {
this.topBar()
}
.width('100%')
.flexShrink(0)
}
// 列表视图(scrollable=false)的 topBar 由调用方渲染在 DayTimelineView 外层卡片里:
// 本组件内部时间轴高度是固定值 viewH(),会把这部分挤掉(实测渲染不出来)。
// scrollable=true 时包一层 Scroll 并自动定位到当前时刻;否则整页平铺。 // scrollable=true 时包一层 Scroll 并自动定位到当前时刻;否则整页平铺。
if (this.scrollable) { if (this.scrollable) {
Scroll(this.scroller) { Scroll(this.scroller) {
@@ -4650,7 +4810,6 @@ struct DayTimelineView {
} }
}) })
} else { } else {
this.topBar()
this.timelineBody() this.timelineBody()
} }
} }
@@ -4788,8 +4947,13 @@ struct DayTimelineView {
@Builder @Builder
private timelineBody() { private timelineBody() {
Column() { Column() {
// 顶部固定区(待办 + 全天/跨天)已移到 Scroll 之外(#68), // 周/月视图:待办+全天已移到 Scroll 之外做浮动固定区(#68),这里只剩时间轴。
// 这里只剩可滚动的时间轴本体。 // 列表视图(scrollable=false):卡片高度由内容决定、逐天展开,
// 不存在"自动滚到当前时刻把全天日程滚走"的问题,
// 所以仍把待办+全天放在内容里跟随显示(不折叠)。
if (!this.scrollable) {
this.topBar()
}
Row() { Row() {
// 左侧小时刻度(每格一条底线,作为时间尺;当前小时高亮)。只渲染视窗内的小时 // 左侧小时刻度(每格一条底线,作为时间尺;当前小时高亮)。只渲染视窗内的小时
Column() { Column() {