/** * 家书 FamilyBook · 「新建 / 编辑纪念日」浮层(鸿蒙端) * * 对应网页版 `templates/days/form.php`(网页 2026-10-11 起也是**弹框**形态)。 * * 🔴 用户 2026-10-20:「纪念日、新增纪念日、设置相关的功能。」 * ⇒ 端上「添加」按钮没有 `onTap`、列表行没有 `onClick` —— 都没有接。 * * 字段照服务端 `annivData()` 的契约:名称 / 按什么重复 / 日期(月+日)/ 分类 / 备注 / 关联人。 * * ⚠️⚠️ **只存月日,不存年**(`anniversaries` 表就那么设计的): * 日期控件选完只取 `month` / `day`,年份丢掉。所以这里的文案是 * 「几月几号」而不是「哪一天」—— 别让用户以为它记住了年份。 * * ⚠️ 分类是**自由文本**:服务端 `kind` 就是用户自己配的分类名 * (`Anniversary::kindsFull` 那份清单**没有 API**,端上取不到)。 * ⇒ 这里给几个预置名当快捷方式 + 一个可自己填的输入框; * 编辑老数据时若分类不在预置里,也要照原样显示出来(不能一保存就被清空)。 */ import { FbColors, FbMetric, FB_MASK, fbAlpha } from '../common/Theme'; import { FbSheetClose } from './FbSheetClose'; import { FbSheetRing } from './FbSheetRing'; import { FbDateTimeRow, FbDateTimePicker, fbToday } from './FbDateTimeField'; import { FbPersonPickSheet } from './FbPersonPickSheet'; import { FbRosterPerson } from '../data/FbPersonRepo'; import { FB_TOKEN_STORE } from '../data/FbTokenStore'; import { createDay, updateDay, deleteDay, FbDayDraft } from '../data/FbDayRepo'; import { FbApiResult } from '../data/FbHttp'; /** 重复方式的键(与服务端 `Anniversary::REPEAT` 一字不差) */ const REPEAT_KEYS: string[] = ['solar', 'lunar', 'once']; const REPEAT_LABELS: string[] = ['每年(阳历)', '每年(农历)', '只此一次']; /** 预置分类(服务端 `PRESET_KINDS` 的同一份;用户还能自己填) */ const PRESET_KINDS: string[] = ['生日', '结婚纪念日', '相识纪念日', '忌日', '节日', '其它']; @ComponentV2 export struct FbDayEditSheet { /** 模块主题色(纪念日 = 陶土橙) */ @Param accent: string = '#B4633C'; /** 正在编辑的纪念日 id(0 = 新建) */ @Param editingId: number = 0; @Param editingTitle: string = ''; @Param editingKind: string = ''; @Param editingNote: string = ''; /** solar / lunar / once */ @Param editingRepeat: string = 'solar'; @Param editingMonth: number = 1; @Param editingDay: number = 1; @Param editingPersonIds: number[] = []; /** 名册(选关联人用) */ @Param roster: FbRosterPerson[] = []; @Event onClose: () => void = (): void => {}; @Event onSaved: () => void = (): void => {}; @Event onDeleted: () => void = (): void => {}; @Local title: string = ''; @Local kind: string = ''; @Local note: string = ''; @Local repeat: string = 'solar'; @Local month: number = 1; @Local day: number = 1; /** 日期控件的载体(`YYYY-MM-DD`;年份无意义,只借它拿月日) */ @Local pickDate: string = ''; @Local pickOpen: boolean = false; @Local personIds: number[] = []; @Local personPickOpen: boolean = false; @Local saving: boolean = false; @Local confirmingDelete: boolean = false; @Local note2: string = ''; aboutToAppear(): void { this.title = this.editingTitle; this.kind = this.editingKind; this.note = this.editingNote; this.repeat = this.editingRepeat !== '' ? this.editingRepeat : 'solar'; this.month = this.editingMonth > 0 ? this.editingMonth : 1; this.day = this.editingDay > 0 ? this.editingDay : 1; this.personIds = this.editingPersonIds.slice(); /* 载体日期:用**今年**的这一天。年份只为了拼出一个合法 `YYYY-MM-DD` 给日期控件用,提交时只取月日(见文件头)。 */ const y: string = fbToday().substring(0, 4); this.pickDate = y + '-' + this.pad2(this.month) + '-' + this.pad2(this.day); } private pad2(v: number): string { return v < 10 ? ('0' + v.toString()) : v.toString(); } /** 下标 → 重复方式中文名(越界给「每年(阳历)」,不抛错) */ private repeatLabel(idx: number): string { return idx >= 0 && idx < REPEAT_LABELS.length ? REPEAT_LABELS[idx] : REPEAT_LABELS[0]; } /** 已选的关联人显示名(「全家的事」= 一个都没选) */ private peopleText(): string { if (this.personIds.length === 0) { return '全家的事'; } const names: string[] = []; for (const id of this.personIds) { const p: FbRosterPerson | undefined = this.roster.find((x: FbRosterPerson): boolean => x.id === id); if (p !== undefined) { names.push(p.shown !== '' ? p.shown : p.name); } } return names.length > 0 ? names.join('、') : ('已选 ' + this.personIds.length.toString() + ' 人'); } /** 分类清单:预置 + 当前值(老数据里的自定义分类不能丢) */ private kinds(): string[] { const out: string[] = []; for (const k of PRESET_KINDS) { out.push(k); } if (this.kind !== '' && out.indexOf(this.kind) < 0) { out.push(this.kind); } return out; } build() { /* 🔴🔴 根必须 `Stack`:遮罩与卡片要"叠",不能当 `Column` 子项"排" (排会把卡片挤出屏幕 —— 2026-10-20 真机事故,见 pitfalls.md) */ Stack({ alignContent: Alignment.Center }) { Column() .width('100%') .height('100%') .backgroundColor(FB_MASK) .onClick((): void => { this.onClose(); }) Column() { FbSheetRing({ radius: FbMetric.radius, ringColor: this.accent }) { Column({ space: 12 }) { // ---- 标题栏 ---- Row({ space: 8 }) { Column({ space: 2 }) { Text(this.editingId > 0 ? '编辑纪念日' : '新建纪念日') .fontSize(FbMetric.fontTitle) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) .maxLines(1) Text('只记几月几号,每年自动提醒') .fontSize(FbMetric.fontTiny) .fontColor(this.accent) .maxLines(1) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) .flexShrink(1) FbSheetClose({ onClose: (): void => { this.onClose(); }, color: this.accent }) } .width('100%') .alignItems(VerticalAlign.Center) Scroll() { Column({ space: 12 }) { // ---- 名称 ---- Row({ space: 8 }) { Text('叫什么') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) .width(44) TextInput({ text: this.title, placeholder: '如:结婚纪念日' }) .placeholderColor(FbColors.faint) .placeholderFont({ size: FbMetric.fontTiny }) .fontSize(FbMetric.fontSmall) .fontColor(FbColors.ink) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) .height(42) .layoutWeight(1) .padding({ left: 10, right: 10 }) .onChange((v: string): void => { this.title = v; this.note2 = ''; }) } .width('100%') .alignItems(VerticalAlign.Center) // ---- 重复方式 ---- Column({ space: 6 }) { Text('按什么重复') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) Row({ space: 8 }) { ForEach(REPEAT_KEYS, (k: string, idx: number) => { Text(this.repeatLabel(idx)) .fontSize(FbMetric.fontTiny) .fontColor(this.repeat === k ? '#FFFFFF' : FbColors.muted) .padding({ left: 12, right: 12, top: 7, bottom: 7 }) .backgroundColor(this.repeat === k ? this.accent : FbColors.inputBg) .borderRadius(FbMetric.radiusPill) .onClick((): void => { this.repeat = k; }) }, (k: string): string => 'rp_' + k) } } .width('100%') .alignItems(HorizontalAlign.Start) // ---- 日期(月 + 日)---- FbDateTimeRow({ label: '日期', date: this.pickDate, allDay: true, accent: this.accent, onTap: (): void => { this.pickOpen = true; } }) // ---- 分类 ---- Column({ space: 6 }) { Text('分类') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) Scroll() { Row({ space: 8 }) { ForEach(this.kinds(), (k: string) => { Text(k) .fontSize(FbMetric.fontTiny) .fontColor(this.kind === k ? '#FFFFFF' : FbColors.muted) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(this.kind === k ? this.accent : FbColors.inputBg) .borderRadius(FbMetric.radiusPill) .onClick((): void => { // 再点一下 = 取消分类(不分类是合法状态) this.kind = this.kind === k ? '' : k; }) }, (k: string): string => 'kd_' + k) } .padding({ right: 4 }) } .scrollable(ScrollDirection.Horizontal) .scrollBar(BarState.Off) .width('100%') } .width('100%') .alignItems(HorizontalAlign.Start) // ---- 关联人 ---- Row({ space: 8 }) { Text('关联人') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) .width(44) Text(this.peopleText()) .fontSize(FbMetric.fontSmall) .fontColor(this.personIds.length > 0 ? FbColors.ink : FbColors.muted) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) .height(42) .padding({ left: 12, right: 12 }) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) .onClick((): void => { this.personPickOpen = true; }) } .width('100%') .alignItems(VerticalAlign.Center) // ---- 备注 ---- Row({ space: 8 }) { Text('备注') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) .width(44) TextArea({ text: this.note, placeholder: '想记的细节(可空)' }) .placeholderColor(FbColors.faint) .placeholderFont({ size: FbMetric.fontTiny }) .fontSize(FbMetric.fontSmall) .fontColor(FbColors.ink) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) .height(72) .layoutWeight(1) .padding({ left: 10, right: 10, top: 8, bottom: 8 }) .onChange((v: string): void => { this.note = v; }) } .width('100%') .alignItems(VerticalAlign.Top) } .width('100%') .alignItems(HorizontalAlign.Start) } .constraintSize({ maxHeight: 400 }) .width('100%') .scrollBar(BarState.Off) .align(Alignment.TopStart) if (this.note2 !== '') { Text(this.note2) .fontSize(FbMetric.fontTiny) .fontColor(FbColors.red) .width('100%') .maxLines(3) } // ---- 底部:删除(左)+ 保存 ---- Row({ space: 10 }) { if (this.editingId > 0) { Text(this.confirmingDelete ? '真的删掉?' : '删除') .fontSize(FbMetric.fontBody) .fontWeight(FontWeight.Medium) .fontColor('#FFFFFF') .textAlign(TextAlign.Center) .height(44) .padding({ left: 16, right: 16 }) .borderRadius(FbMetric.radiusS) /* 🔴 v61:删除按钮统一红底白字。红值取自 color.json 的 `red` */ .backgroundColor(this.confirmingDelete ? '#C0533F' : fbAlpha('#C0533F', 0.86)) .onClick((): void => { if (this.confirmingDelete) { this.doDelete(); } else { this.confirmingDelete = true; } }) } Text(this.saving ? '保存中…' : (this.editingId > 0 ? '保存修改' : '记下来')) .fontSize(FbMetric.fontBody) .fontWeight(FontWeight.Medium) .fontColor(this.saving ? FbColors.faint : '#FFFFFF') .textAlign(TextAlign.Center) .layoutWeight(1) .height(44) .borderRadius(FbMetric.radiusS) .backgroundColor(fbAlpha(this.accent, this.saving ? 0.35 : 0.98)) .onClick((): void => { this.save(); }) } .width('100%') .alignItems(VerticalAlign.Center) } .width('100%') .padding({ left: 18, right: 18, top: 18, bottom: 18 }) .alignItems(HorizontalAlign.Start) } } .width('92%') // ---- 日期选择(只取月日)---- if (this.pickOpen) { FbDateTimePicker({ title: '几月几号', date: this.pickDate, allDay: true, accent: this.accent, onConfirm: (d: string, t: string): void => { /* ⚠️ 只留月/日:服务端存的就是月日(见文件头)。 `d` 是 `YYYY-MM-DD`,第 6-7 位是月、9-10 位是日。 */ if (d.length >= 10) { this.month = parseInt(d.substring(5, 7), 10); this.day = parseInt(d.substring(8, 10), 10); this.pickDate = d; } this.pickOpen = false; }, onCancel: (): void => { this.pickOpen = false; } }) } // ---- 选关联人(复用观影那套"搜索 + 暂存 + 确定")---- if (this.personPickOpen) { FbPersonPickSheet({ people: this.roster, selected: this.personIds, title: '这条纪念日和谁有关', accent: this.accent, onConfirm: (ids: number[]): void => { this.personIds = ids; this.personPickOpen = false; }, onClose: (): void => { this.personPickOpen = false; } }) } } .width('100%') .height('100%') .hitTestBehavior(HitTestMode.Transparent) } private async save(): Promise { if (this.saving) { return; } const t: string = this.title.trim(); if (t === '') { this.note2 = '纪念日得有个名字(如「结婚纪念日」)。'; return; } this.saving = true; this.note2 = ''; const d: FbDayDraft = { title: t, repeatType: this.repeat, month: this.month, day: this.day, kind: this.kind, note: this.note.trim(), personIds: this.personIds }; const token: string = FB_TOKEN_STORE.get(); const res: FbApiResult = this.editingId > 0 ? await updateDay(token, this.editingId, d) : await createDay(token, d); this.saving = false; if (!res.ok) { this.note2 = res.error !== '' ? res.error : '保存失败,请稍后再试。'; return; } this.onSaved(); } private async doDelete(): Promise { if (this.saving) { return; } this.saving = true; this.note2 = ''; const res: FbApiResult = await deleteDay(FB_TOKEN_STORE.get(), this.editingId); this.saving = false; this.confirmingDelete = false; if (!res.ok) { this.note2 = res.error !== '' ? res.error : '删除失败,请稍后再试。'; return; } this.onDeleted(); } }