feat(日视图): 全天日程与待办改为浮动固定,最多3条可展开 (#68)
## 用户诉求
「打开日程之后时间会自动滚到下面(比如下午16点),全天日程被滚上去看不见了。
想做成浮动的,直接靠在日期下面,不管下面怎么滚都不动。
但条数多会盖住视野,所以最多显示3条,下面加个向下箭头,点击展开。」
## 改造
### 结构变化
改前:
Scroll { timelineBody() } ← 待办区+全天区+时间轴 全在滚动里
Column { 待办区 / 全天区 / Row(刻度+三层时间轴) }
改后:
Column {
topBar() ← 固定,不滚
Scroll { timelineBody() } ← 只剩时间轴
}
### 为什么用 Column 而不是 Stack 浮动
用户原话是"浮动",但实现上用 Column 顺序布局(固定区占位、时间轴在其下)
比 Stack 叠浮更稳妥:不遮挡、不需算偏移。视觉上同样是"贴着日期下方不动"。
这样避免了用户担心的"盖住一大片视野"——固定区本身占位,不会被内容压。
### 折叠机制
- 待办 + 全天/跨天**合并**成一个列表(否则两块各自3条会变6条)
- 折叠时最多 3 条
- 超过时显示「还有 N 条 ⌄」,点击展开为「收起 ⌃」
- 待办在前、全天日程在后
### 顺带修正
`allDayH`(原用于滚动定位时把全天区高度计入偏移)现在恒为 0,
因为该区已移出 Scroll。若不改,滚动定位会多偏移整个固定区高度。
## 真机验证(5NC0225B27004718)
创建 4 条今天截止的测试待办后:
| 验证项 | 实测 |
|---|---|
| 固定区位置 | 日期行 y=707 下方,依次 783/871/958 |
| 折叠生效 | 只显示 折叠测试1/2/3,第4条隐藏 ✅ |
| 合并计数 | 「还有 3 条」(6条-3条,含待办本的其他条目)✅ |
| 展开箭头 | `[595,1030] ⌄` 向下箭头 ✅ |
| 点击展开 | 显示全部 6 条,文字变「收起」、箭头变 `⌃` ✅ |
| 时间轴在其下 | 刻度 11:00 从 y=1104 开始,未被遮挡 ✅ |
## 编译
BUILD SUCCESSFUL
This commit is contained in:
@@ -4299,6 +4299,15 @@ class AgendaTimelineGroup {
|
|||||||
timeline: DayTimeline = new DayTimeline();
|
timeline: DayTimeline = new DayTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部固定区(全天日程 + 待办)的一个条目(#68)。
|
||||||
|
* ArkTS 不允许内联对象字面量类型,必须用命名 class。
|
||||||
|
*/
|
||||||
|
class TopBarItem {
|
||||||
|
isTodo: boolean = false; // true=待办(带勾选圈) false=全天/跨天日程(色块)
|
||||||
|
ev: DisplayEvent = new DisplayEvent();
|
||||||
|
}
|
||||||
|
|
||||||
/** 日程层纵向序列中的一行:空隙 / 冲突组。整点线与红线已移到另外两层,本层不含任何"线"。
|
/** 日程层纵向序列中的一行:空隙 / 冲突组。整点线与红线已移到另外两层,本层不含任何"线"。
|
||||||
* topRatio~botRatio = 该行覆盖的当日比例区间(0~1,乘整日高得 vp)。 */
|
* topRatio~botRatio = 该行覆盖的当日比例区间(0~1,乘整日高得 vp)。 */
|
||||||
class TimeRow {
|
class TimeRow {
|
||||||
@@ -4328,7 +4337,12 @@ struct DayTimelineView {
|
|||||||
onToggleTodo: (t: DisplayEvent) => void = (t: DisplayEvent): void => {}; // 勾选待办回调
|
onToggleTodo: (t: DisplayEvent) => void = (t: DisplayEvent): void => {}; // 勾选待办回调
|
||||||
private scroller: Scroller = new Scroller();
|
private scroller: Scroller = new Scroller();
|
||||||
@State private viewportH: number = 0; // 滚动视口高度(由 Scroll 实测,用于把"当前时刻"居中)
|
@State private viewportH: number = 0; // 滚动视口高度(由 Scroll 实测,用于把"当前时刻"居中)
|
||||||
@State private allDayH: number = 0; // 顶部"全天/跨天"区高度(它现在随内容滚动,居中定位要加上)
|
/** ⭐ 顶部固定区(全天日程 + 待办)是否展开(#68)
|
||||||
|
* 默认折叠:最多显示 3 条,超出显示向下箭头。折叠是为了避免浮动区盖住大片时间轴视野。 */
|
||||||
|
@State private topExpanded: boolean = false;
|
||||||
|
/** 顶部"全天/跨天"区高度 —— #68 起该区移出 Scroll(浮动固定),
|
||||||
|
* 不再参与滚动偏移计算,恒为 0。保留字段仅为兼容旧引用。 */
|
||||||
|
@State private allDayH: number = 0;
|
||||||
private pendingScroll: boolean = false;
|
private pendingScroll: boolean = false;
|
||||||
|
|
||||||
aboutToAppear(): void {
|
aboutToAppear(): void {
|
||||||
@@ -4340,6 +4354,49 @@ struct DayTimelineView {
|
|||||||
this.scrollToNow();
|
this.scrollToNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶部固定区要显示的条目(#68):**待办在前、全天/跨天在后**,合并成一个列表。
|
||||||
|
* 合并而不是分两块,是为了统一"最多显示 3 条"的限制 —— 否则两块各自 3 条会变 6 条。
|
||||||
|
*/
|
||||||
|
private topItems(): TopBarItem[] {
|
||||||
|
const out: TopBarItem[] = [];
|
||||||
|
for (const td of this.timeline.todos) {
|
||||||
|
const it = new TopBarItem();
|
||||||
|
it.isTodo = true;
|
||||||
|
it.ev = td;
|
||||||
|
out.push(it);
|
||||||
|
}
|
||||||
|
for (const ev of this.timeline.allDay) {
|
||||||
|
const it = new TopBarItem();
|
||||||
|
it.isTodo = false;
|
||||||
|
it.ev = ev;
|
||||||
|
out.push(it);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 顶部固定区总条数 */
|
||||||
|
private topCount(): number {
|
||||||
|
return this.timeline.todos.length + this.timeline.allDay.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 折叠时最多显示 3 条(#68);展开后显示全部 */
|
||||||
|
private topLimit(): number {
|
||||||
|
return this.topExpanded ? this.topCount() : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 折叠时是否还有被藏起来的条目(决定要不要画展开箭头) */
|
||||||
|
private topHasMore(): boolean {
|
||||||
|
return this.topCount() > 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取前 N 条(展开状态由 topLimit 决定) */
|
||||||
|
private topVisible(): TopBarItem[] {
|
||||||
|
const all: TopBarItem[] = this.topItems();
|
||||||
|
const lim: number = this.topLimit();
|
||||||
|
return all.length > lim ? all.slice(0, lim) : all;
|
||||||
|
}
|
||||||
|
|
||||||
/** 自动滚动:把"当前时刻红线"定位到可视区**垂直居中**
|
/** 自动滚动:把"当前时刻红线"定位到可视区**垂直居中**
|
||||||
* (目标 y = nowRatio * 整日高 − 视口高/2,夹取 ≥0)。仅今天且自带滚动时生效。
|
* (目标 y = nowRatio * 整日高 − 视口高/2,夹取 ≥0)。仅今天且自带滚动时生效。
|
||||||
* 首帧视口高度未知 → 先用估算值滚一次,等 onAreaChange 实测到高度后再校正一次。 */
|
* 首帧视口高度未知 → 先用估算值滚一次,等 onAreaChange 实测到高度后再校正一次。 */
|
||||||
@@ -4356,8 +4413,9 @@ struct DayTimelineView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const vp: number = this.viewportH > 0 ? this.viewportH : this.hourUnit * 8;
|
const vp: number = this.viewportH > 0 ? this.viewportH : this.hourUnit * 8;
|
||||||
// 内容顶部还有"全天/跨天"区(allDayH),且时间轴只从 viewTop 开始 → 都要计入偏移
|
// #68:全天/待办区已移出 Scroll(浮动固定),偏移里不再加 allDayH。
|
||||||
const y: number = this.allDayH + (TimelineUtil.nowRatio(this.nowMs) - this.viewTop()) * this.totalH() - vp / 2;
|
// 时间轴只从 viewTop 开始 → 直接用比例换算即可。
|
||||||
|
const y: number = (TimelineUtil.nowRatio(this.nowMs) - this.viewTop()) * this.totalH() - vp / 2;
|
||||||
setTimeout((): void => {
|
setTimeout((): void => {
|
||||||
this.scroller.scrollTo({ xOffset: 0, yOffset: y < 0 ? 0 : y });
|
this.scroller.scrollTo({ xOffset: 0, yOffset: y < 0 ? 0 : y });
|
||||||
}, 60);
|
}, 60);
|
||||||
@@ -4571,6 +4629,10 @@ struct DayTimelineView {
|
|||||||
build() {
|
build() {
|
||||||
Column() {
|
Column() {
|
||||||
// 时间轴主体(含顶部"全天/跨天"条 —— 它们随内容一起滚动,向下滑时跟 0 点一起上移)。
|
// 时间轴主体(含顶部"全天/跨天"条 —— 它们随内容一起滚动,向下滑时跟 0 点一起上移)。
|
||||||
|
// ⭐ 顶部固定区(待办 + 全天/跨天)#68:放在 Scroll **之外** → 不随时间轴滚动。
|
||||||
|
// 原来它在 timelineBody 里,会被"自动滚到当前时刻"带走,
|
||||||
|
// 导致下午 16 点打开时全天日程看不见。
|
||||||
|
this.topBar()
|
||||||
// scrollable=true 时包一层 Scroll 并自动定位到当前时刻;否则整页平铺。
|
// scrollable=true 时包一层 Scroll 并自动定位到当前时刻;否则整页平铺。
|
||||||
if (this.scrollable) {
|
if (this.scrollable) {
|
||||||
Scroll(this.scroller) {
|
Scroll(this.scroller) {
|
||||||
@@ -4588,6 +4650,7 @@ struct DayTimelineView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
this.topBar()
|
||||||
this.timelineBody()
|
this.timelineBody()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4598,19 +4661,27 @@ struct DayTimelineView {
|
|||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 时间轴主体:顶部"全天/跨天"条(随滚动一起移动)+ 左侧小时刻度 + 右侧按冲突组分行、组内按列并排 */
|
/**
|
||||||
|
* 顶部固定区(#68):待办 + 全天/跨天日程,**不随时间轴滚动**。
|
||||||
|
*
|
||||||
|
* 为什么加这个区:原来它们排在时间轴最前面,但打开日视图会自动滚到当前时刻
|
||||||
|
* (比如下午 16 点),于是全天日程被滚上去看不见了。改成浮动固定后,
|
||||||
|
* 不管时间轴怎么滚,它们都贴在日期行下方。
|
||||||
|
*
|
||||||
|
* 折叠机制(用户提的折中方案):全天+待办合计**最多显示 3 条**,
|
||||||
|
* 超出时显示向下箭头,点一下展开全部;避免条数多时盖住大片视野。
|
||||||
|
*/
|
||||||
@Builder
|
@Builder
|
||||||
private timelineBody() {
|
private topBar() {
|
||||||
Column() {
|
if (this.topCount() > 0) {
|
||||||
// ⭐ 当天截止待办区(#45):画在最顶,全天/跨天之上,随内容一起滚动。
|
Column({ space: 4 }) {
|
||||||
if (this.timeline.todos.length > 0) {
|
ForEach(this.topVisible(), (item: TopBarItem) => {
|
||||||
Column({ space: 4 }) {
|
if (item.isTodo) {
|
||||||
ForEach(this.timeline.todos, (td: DisplayEvent) => {
|
// ---- 待办行:勾选圈 + 标题 ----
|
||||||
Row({ space: 8 }) {
|
Row({ space: 8 }) {
|
||||||
// 勾选圈:点击切换完成(与待办列表同一回调)
|
// 矢量勾(#50):字符 ✓ 依赖字体,粗细/居中不稳定
|
||||||
// 同列表:矢量勾(#50)
|
|
||||||
Stack() {
|
Stack() {
|
||||||
if (td.completed) {
|
if (item.ev.completed) {
|
||||||
Path()
|
Path()
|
||||||
.width(18)
|
.width(18)
|
||||||
.height(18)
|
.height(18)
|
||||||
@@ -4625,15 +4696,17 @@ struct DayTimelineView {
|
|||||||
.width(18)
|
.width(18)
|
||||||
.height(18)
|
.height(18)
|
||||||
.borderRadius(9)
|
.borderRadius(9)
|
||||||
.backgroundColor(td.completed ? td.color : 'rgba(0,0,0,0)')
|
.backgroundColor(item.ev.completed ? item.ev.color : 'rgba(0,0,0,0)')
|
||||||
.border({ width: 1.5, color: td.color })
|
.border({ width: 1.5, color: item.ev.color })
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
this.onToggleTodo(td);
|
this.onToggleTodo(item.ev);
|
||||||
})
|
})
|
||||||
Text(td.title === '' ? '(无标题待办)' : td.title)
|
Text(item.ev.title === '' ? '(无标题待办)' : item.ev.title)
|
||||||
.fontSize(12)
|
.fontSize(12)
|
||||||
.fontColor($r('app.color.text_primary'))
|
.fontColor($r('app.color.text_primary'))
|
||||||
.decoration({ type: td.completed ? TextDecorationType.LineThrough : TextDecorationType.None })
|
.decoration({
|
||||||
|
type: item.ev.completed ? TextDecorationType.LineThrough : TextDecorationType.None
|
||||||
|
})
|
||||||
.maxLines(1)
|
.maxLines(1)
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
@@ -4645,26 +4718,16 @@ struct DayTimelineView {
|
|||||||
.borderRadius(6)
|
.borderRadius(6)
|
||||||
.backgroundColor($r('app.color.card_bg'))
|
.backgroundColor($r('app.color.card_bg'))
|
||||||
.border({ width: 0.5, color: $r('app.color.shadow_color') })
|
.border({ width: 0.5, color: $r('app.color.shadow_color') })
|
||||||
}, (td: DisplayEvent) => `todo_${this.timeline.dateKey}_${td.id}_${td.completed}`)
|
} else {
|
||||||
}
|
// ---- 全天/跨天日程:色块 ----
|
||||||
.width('100%')
|
|
||||||
.padding({ bottom: 6 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全天 / 跨天日程:同样用**色块**展示(与普通日程保持一致的视觉),
|
|
||||||
// 画在 0 点之前并**随内容一起滚动**(向下滑时跟 0 点一起上移,不再常驻顶部)
|
|
||||||
if (this.timeline.allDay.length > 0) {
|
|
||||||
Column({ space: 4 }) {
|
|
||||||
ForEach(this.timeline.allDay, (e: DisplayEvent) => {
|
|
||||||
Row({ space: 6 }) {
|
Row({ space: 6 }) {
|
||||||
Text(e.isAllDay ? '全天' : '跨天')
|
Text(item.ev.isAllDay ? '全天' : '跨天')
|
||||||
.fontSize(9)
|
.fontSize(9)
|
||||||
.fontColor('#FFFFFF')
|
.fontColor('#FFFFFF')
|
||||||
.backgroundColor('#26000000')
|
.backgroundColor('#26000000')
|
||||||
.borderRadius(6)
|
.borderRadius(6)
|
||||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||||
if (!e.writable) {
|
if (!item.ev.writable) {
|
||||||
// 只读标记:圆角矩形白框 + 白字(与色块上的「只读」小标同一套样式)
|
|
||||||
Text('只读')
|
Text('只读')
|
||||||
.fontSize(9)
|
.fontSize(9)
|
||||||
.fontColor('#FFFFFF')
|
.fontColor('#FFFFFF')
|
||||||
@@ -4672,43 +4735,61 @@ struct DayTimelineView {
|
|||||||
.borderRadius(4)
|
.borderRadius(4)
|
||||||
.padding({ left: 4, right: 4, top: 0, bottom: 0 })
|
.padding({ left: 4, right: 4, top: 0, bottom: 0 })
|
||||||
}
|
}
|
||||||
Text(e.title === '' ? '(无标题)' : e.title)
|
Text(item.ev.title === '' ? '(无标题)' : item.ev.title)
|
||||||
.fontSize(12)
|
.fontSize(12)
|
||||||
.fontWeight(FontWeight.Medium)
|
.fontWeight(FontWeight.Medium)
|
||||||
.fontColor('#FFFFFF')
|
.fontColor('#FFFFFF')
|
||||||
.maxLines(1)
|
.maxLines(1)
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
if (e.location !== '') {
|
if (item.ev.location !== '') {
|
||||||
Text(e.location)
|
Text(item.ev.location)
|
||||||
.fontSize(10)
|
.fontSize(10)
|
||||||
.fontColor('#D9FFFFFF')
|
.fontColor('#D9FFFFFF')
|
||||||
.maxLines(1)
|
.maxLines(1)
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
.constraintSize({ maxWidth: 120 })
|
.constraintSize({ maxWidth: 100 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alignItems(VerticalAlign.Center)
|
.alignItems(VerticalAlign.Center)
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(26)
|
.height(24)
|
||||||
.padding({ left: 8, right: 8 })
|
.padding({ left: 8, right: 8 })
|
||||||
.borderRadius(8)
|
.borderRadius(6)
|
||||||
// 只读不再用灰蒙版(已撤),只靠标题后的「只读」白框小标区分
|
.backgroundColor(item.ev.color)
|
||||||
.backgroundColor(e.color)
|
.onClick(() => this.onPick(item.ev))
|
||||||
.onClick(() => this.onPick(e))
|
|
||||||
}, (e: DisplayEvent) => `al_${this.timeline.dateKey}_${TimelineUtil.keyOf(e)}_${e.writable ? 1 : 0}_${e.location.length}`)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.padding({ bottom: 6 })
|
|
||||||
.onAreaChange((oldVal: Area, newVal: Area): void => {
|
|
||||||
const h: number = Number(newVal.height);
|
|
||||||
if (h > 0 && (h - this.allDayH > 1 || h - this.allDayH < -1)) {
|
|
||||||
this.allDayH = h;
|
|
||||||
this.tryScroll();
|
|
||||||
}
|
}
|
||||||
})
|
}, (item: TopBarItem) => `tb_${this.timeline.dateKey}_${item.isTodo ? 't' : 'a'}_${item.ev.id}_${item.ev.completed ? 1 : 0}`)
|
||||||
}
|
|
||||||
|
|
||||||
|
// 展开/收起箭头(合计超过 3 条时才出现)
|
||||||
|
if (this.topHasMore()) {
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
Text(this.topExpanded ? '收起' : `还有 ${this.topCount() - 3} 条`)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor($r('app.color.text_hint'))
|
||||||
|
Text(this.topExpanded ? '⌃' : '⌄')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor($r('app.color.text_hint'))
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(18)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
.onClick(() => {
|
||||||
|
this.topExpanded = !this.topExpanded;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ bottom: 6 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间轴主体:左侧小时刻度 + 右侧按冲突组分行、组内按列并排(顶部固定区已移出,#68) */
|
||||||
|
@Builder
|
||||||
|
private timelineBody() {
|
||||||
|
Column() {
|
||||||
|
// 顶部固定区(待办 + 全天/跨天)已移到 Scroll 之外(#68),
|
||||||
|
// 这里只剩可滚动的时间轴本体。
|
||||||
Row() {
|
Row() {
|
||||||
// 左侧小时刻度(每格一条底线,作为时间尺;当前小时高亮)。只渲染视窗内的小时
|
// 左侧小时刻度(每格一条底线,作为时间尺;当前小时高亮)。只渲染视窗内的小时
|
||||||
Column() {
|
Column() {
|
||||||
|
|||||||
Reference in New Issue
Block a user