/** * 家书 FamilyBook · 纪念日模块(鸿蒙端) * * 对应 Web 端 `templates/days/index.php`。 * * Web 端的结构:**「即将到来」横条在顶**(30 天内的纪念日,含名册里每个人的生日), * 下面是全部纪念日的列表(可按类别筛)。 * * 端上照此,但把「即将到来」做成**大数字卡片**(还剩几天用大字), * 因为「还有几天」是这个模块唯一真正要一眼看到的信息。 */ import { FbColors, FbMetric } from '../../common/Theme'; import { FbAppState, fbAppState } from '../../common/Store'; import { FbAnniversaryItem, FbKinChip } from '../../model/Types'; import { FB_DEMO_ANNIVERSARIES } from '../../model/DemoData'; import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets'; import { FB_DOCK_RESERVE } from '../../components/FloatDock'; @ComponentV2 export struct FbDaysPage { /** * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 */ @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 模块主题色(纪念日 = 陶土橙) */ private accent: string = '#B4633C'; private all: FbAnniversaryItem[] = FB_DEMO_ANNIVERSARIES; /** 类别筛选('' = 全部) */ @Local kindFilter: string = ''; build() { Scroll() { Column() { this.topBar() // ---- 页头 ---- Row({ space: 8 }) { Column({ space: 2 }) { Text('纪念日') .fontSize(FbMetric.fontTitle) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Text('节日 / 纪念日 / 生日,都在这一条时间线上') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) FbPrimaryButton({ label: '添加', bg: this.accent, icon: $r('app.media.fb_ic_plus') }) FbIconButton({ icon: $r('app.media.fb_ic_gear'), tint: FbColors.muted }) } .width('100%') .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) .alignItems(VerticalAlign.Center) Scroll() { Column({ space: 0 }) { // ---- 即将到来(30 天内)---- Column({ space: 0 }) { Row({ space: 8 }) { Text('即将到来') .fontSize(FbMetric.fontSmall) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Blank() Text('30 天内') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } .width('100%') .padding({ left: 4, right: 4, bottom: 10 }) if (this.upcoming().length === 0) { Text('三十天内没有纪念日') .fontSize(FbMetric.fontSmall) .fontColor(FbColors.faint) } else { // 横向排列的「大数字」卡 Scroll() { Row({ space: 10 }) { ForEach(this.upcoming(), (a: FbAnniversaryItem) => { this.upcomingCard(a) }, (a: FbAnniversaryItem): string => 'up_' + a.id.toString()) } } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .width('100%') // 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏 .padding({ bottom: FB_DOCK_RESERVE }) } } .padding(FbMetric.cardPad) .backgroundColor(FbColors.surface) .borderRadius(FbMetric.radius) .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad }) .alignItems(HorizontalAlign.Start) // ---- 全部纪念日 ---- Row({ space: 8 }) { this.kindChip('全部', '') this.kindChip('生日', 'birthday') this.kindChip('纪念日', 'anniversary') this.kindChip('节日', 'holiday') } .width('100%') .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 14, bottom: 8 }) if (this.filtered().length === 0) { FbEmptyBox({ title: '这里还没有纪念日', hint: '名册里填上生日,它就会自动出现在这儿', icon: $r('app.media.fb_ic_days') }) } else { Column({ space: 0 }) { ForEach(this.filtered(), (a: FbAnniversaryItem, idx: number) => { this.annRow(a, idx === this.filtered().length - 1) }, (a: FbAnniversaryItem): string => 'ann_' + a.id.toString()) } .padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 8 }) .backgroundColor(FbColors.surface) .borderRadius(FbMetric.radius) .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 28 }) } } .width('100%') } .width('100%') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .align(Alignment.TopStart) } .width('100%') } .width('100%') .height('100%') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .align(Alignment.TopStart) } /** 即将到来的大数字卡 */ @Builder upcomingCard(a: FbAnniversaryItem) { Column({ space: 4 }) { if (a.daysLeft === 0) { Text('今天') .fontSize(24) .fontWeight(FontWeight.Medium) .fontColor(Color.White) } else { Row({ space: 1 }) { Text(a.daysLeft.toString()) .fontSize(26) .fontWeight(FontWeight.Medium) .fontColor(Color.White) Text('天') .fontSize(FbMetric.fontTiny) .fontColor('#FFFFFFCC') .margin({ bottom: 4 }) } .alignItems(VerticalAlign.Bottom) } Text(a.title) .fontSize(FbMetric.fontSmall) .fontColor(Color.White) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Text(a.date) .fontSize(10) .fontColor('#FFFFFFB3') } .width(96) .padding(14) .backgroundColor(this.accent) .borderRadius(FbMetric.radius) .alignItems(HorizontalAlign.Start) } /** 一行纪念日:日期 + 标题 + 相关人 + 天数 */ @Builder annRow(a: FbAnniversaryItem, isLast: boolean) { Column({ space: 0 }) { Row({ space: 12 }) { // 日期块 Column({ space: 0 }) { Text(this.monthDay(a.date)) .fontSize(FbMetric.fontSmall) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Text(this.yearText(a.date)) .fontSize(9) .fontColor(FbColors.faint) } .width(52) .alignItems(HorizontalAlign.Start) Column({ space: 4 }) { Row({ space: 6 }) { Text(a.title) .fontSize(FbMetric.fontBody) .fontColor(FbColors.ink) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) FbTag({ text: this.kindName(a.kind), tone: this.kindTone(a.kind) }) } .width('100%') .alignItems(VerticalAlign.Center) // 相关人(生日才有) if (a.people.length > 0) { Row({ space: 6 }) { ForEach(a.people, (p: FbKinChip) => { Row({ space: 3 }) { FbAvatar({ name: p.shown, color: p.avatarColor, dia: 16, fontSizeVp: 9 }) Text(p.shown) .fontSize(10) .fontColor(FbColors.muted) } .alignItems(VerticalAlign.Center) }, (p: FbKinChip): string => 'ap_' + a.id.toString() + '_' + p.id.toString()) } } } .alignItems(HorizontalAlign.Start) .layoutWeight(1) // 剩余天数 Text(a.daysLeft === 0 ? '今天' : a.daysLeft.toString() + ' 天') .fontSize(FbMetric.fontTiny) .fontWeight(a.daysLeft <= 7 ? FontWeight.Medium : FontWeight.Normal) .fontColor(a.daysLeft <= 7 ? this.accent : FbColors.faint) } .width('100%') .padding({ top: 12, bottom: 12 }) .alignItems(VerticalAlign.Center) if (!isLast) { Divider().color(FbColors.line2).margin({ left: 64 }) } } .width('100%') } @Builder kindChip(label: string, key: string) { Text(label) .fontSize(FbMetric.fontSmall) .fontColor(this.kindFilter === key ? Color.White : FbColors.muted) .backgroundColor(this.kindFilter === key ? this.accent : FbColors.inputBg) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .borderRadius(FbMetric.radiusPill) .onClick(() => { this.kindFilter = key; }) } /** 'MM-DD' → '10-17';'YYYY-MM-DD' → '11-08' */ private monthDay(date: string): string { const parts: string[] = date.split('-'); if (parts.length === 3) { return parts[1] + '-' + parts[2]; } return date; } /** 带年份的日期显示年份(如 '2019-11-08' → '2019'),不带则显示「每年」 */ private yearText(date: string): string { const parts: string[] = date.split('-'); if (parts.length === 3) { return parts[0]; } return '每年'; } private kindName(kind: string): string { if (kind === 'birthday') { return '生日'; } if (kind === 'holiday') { return '节日'; } if (kind === 'solar_term') { return '节气'; } return '纪念日'; } private kindTone(kind: string): string { if (kind === 'birthday') { return 'brand'; } if (kind === 'holiday') { return 'red'; } if (kind === 'solar_term') { return 'green'; } return 'blue'; } private upcoming(): FbAnniversaryItem[] { return this.all.filter((a: FbAnniversaryItem): boolean => a.daysLeft <= 30); } private filtered(): FbAnniversaryItem[] { if (this.kindFilter.length === 0) { return this.all; } return this.all.filter((a: FbAnniversaryItem): boolean => a.kind === this.kindFilter); } }