/** * 家书 FamilyBook · 名册 / 家谱(鸿蒙端) * * 对应 Web 端 `templates/people/index.php`(名册)与 `templates/kin/tree.php`(家谱)。 * * 这两页在 Web 端是两个模块,但**看的是同一批人**:名册是「平铺的名单」(通讯录视角), * 家谱是「以我为中心的连线」(关系视角)。端上放在同一个文件里,用内部分段切换 —— * 手机上在两页之间来回跳没有意义,切一下就能对照看。 * * ⚠️ 家谱的树形图(Web 端是 HTML 折线)在手机窄屏上排不下。 * 端上改成**「以我为中心的三圈」**:我 → 直系(父母/配偶/子女)→ 其他亲属, * 每圈一张卡,卡里按人列。关系表达上等价,宽度可控。 */ import { FbColors, FbMetric } from '../../common/Theme'; import { FbAppState, fbAppState } from '../../common/Store'; import { FbGroup, FbKinRelation, FbRosterItem } from '../../model/Types'; import { FB_DEMO_KIN_RELATIONS, FB_DEMO_ROSTER, fbGroupRoster } from '../../model/DemoData'; import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets'; /** 家谱里一个人一圈的分组 */ interface FbKinRing { /** 圈名:我 / 直系 / 亲属 */ title: string; /** 说明 */ hint: string; /** 圈内的关系条目 */ rels: FbKinRelation[]; } @ComponentV2 export struct FbPeoplePage { /** * 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。 * 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。 */ @BuilderParam topBar: () => void; @Param app: FbAppState = fbAppState(); /** 子视图:名册(roster)/ 家谱(kin) */ @Local subView: string = 'roster'; /** 名册范围:member(家里的人)/ friend(亲友) */ @Local rosterScope: string = 'member'; /** 搜索词(本轮只做界面,不过滤) */ @Local keyword: string = ''; private all: FbRosterItem[] = FB_DEMO_ROSTER; private rels: FbKinRelation[] = FB_DEMO_KIN_RELATIONS; build() { Scroll() { Column() { this.topBar() // ---- 分段切换(名册 / 家谱)---- Row({ space: 4 }) { this.segBtn('名册', 'roster', '#A84A28') this.segBtn('家谱', 'kin', '#A84A28') } .padding(3) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusPill) .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) if (this.subView === 'roster') { this.rosterView() } else { this.kinView() } } .width('100%') } .width('100%') .height('100%') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .align(Alignment.TopStart) } // ============================================================ // 名册 // ============================================================ @Builder rosterView() { Column() { // 操作条 Row({ space: 8 }) { Row({ space: 6 }) { Image($r('app.media.fb_ic_search')) .width(15) .height(15) .renderMode(ImageRenderMode.Template) .fillColor(FbColors.faint) TextInput({ placeholder: '搜索姓名、手机、备注', text: this.keyword }) .placeholderColor(FbColors.faint) .placeholderFont({ size: FbMetric.fontSmall }) .fontSize(FbMetric.fontSmall) .fontColor(FbColors.ink) .backgroundColor(Color.Transparent) .padding(0) .layoutWeight(1) .onChange((v: string) => { this.keyword = v; }) } .layoutWeight(1) .height(36) .padding({ left: 12, right: 12 }) .backgroundColor(FbColors.inputBg) .borderRadius(FbMetric.radiusS) .alignItems(VerticalAlign.Center) FbPrimaryButton({ label: '添加', bg: '#4A7C82', 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) // 范围切换(家里的人 / 亲友) Row({ space: 8 }) { this.scopeChip('家里的人', 'member') this.scopeChip('亲友', 'friend') Blank() Text(this.byScope().length.toString() + ' 人') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } .width('100%') .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 6 }) .alignItems(VerticalAlign.Center) // 分组列表 List({ space: 0 }) { ForEach(this.groups(), (g: FbGroup) => { ListItemGroup({ header: this.groupHeader(g.title, g.count + ' 人') }) { ForEach(g.items, (p: FbRosterItem) => { ListItem() { this.personRow(p) } }, (p: FbRosterItem): string => 'pr_' + p.id.toString()) } }, (g: FbGroup): string => 'rg_' + g.key) } .layoutWeight(1) .width('100%') .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 }) .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) } .width('100%') .layoutWeight(1) } /** 一个人一行:头像 + 名字/称谓 + 标签 + 生日 */ @Builder personRow(p: FbRosterItem) { Row({ space: 12 }) { FbAvatar({ name: p.name, color: p.avatarColor, dia: 40, fontSizeVp: 16 }) Column({ space: 3 }) { Row({ space: 6 }) { Text(p.name) .fontSize(FbMetric.fontBody) .fontColor(FbColors.ink) .fontWeight(FontWeight.Medium) .maxLines(1) if (p.isAdmin) { FbTag({ text: '管理员', tone: 'brand' }) } if (p.kinship !== p.name) { FbTag({ text: p.kinship, tone: 'muted' }) } } .alignItems(VerticalAlign.Center) Row({ space: 8 }) { if (p.tags.length > 0) { ForEach(p.tags, (t: string) => { FbTag({ text: t, tone: 'blue' }) }, (t: string): string => 'tg_' + p.id.toString() + '_' + t) } if (p.birthday.length > 0) { Text('生日 ' + p.birthday) .fontSize(10) .fontColor(FbColors.faint) } } .alignItems(VerticalAlign.Center) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) Image($r('app.media.fb_ic_forward')) .width(13) .height(13) .renderMode(ImageRenderMode.Template) .fillColor(FbColors.faint) } .width('100%') .padding(14) .backgroundColor(FbColors.surface) .borderRadius(FbMetric.radius) .margin({ bottom: 8 }) .alignItems(VerticalAlign.Center) } // ============================================================ // 家谱 // ============================================================ @Builder kinView() { Column() { // 说明条 Row({ space: 8 }) { Column({ space: 2 }) { Text('以我为中心') .fontSize(FbMetric.fontBody) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Text(this.rels.length.toString() + ' 条连线 · 父母 / 配偶 / 子女填上就能长出来') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) FbPrimaryButton({ label: '补关系', bg: '#8A6B4B', icon: $r('app.media.fb_ic_plus') }) } .width('100%') .padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 }) .alignItems(VerticalAlign.Center) Scroll() { Column({ space: 0 }) { ForEach(this.rings(), (r: FbKinRing) => { this.ringCard(r) }, (r: FbKinRing): string => 'ring_' + r.title) // 完整连线表(家谱的「原始数据」,便于核对填得对不对) Column({ space: 0 }) { Row() { Text('全部连线') .fontSize(FbMetric.fontSmall) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Blank() Text(this.rels.length.toString() + ' 条') .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } .width('100%') .padding({ left: 4, right: 4, bottom: 8 }) Column({ space: 0 }) { ForEach(this.rels, (r: FbKinRelation, idx: number) => { Row({ space: 10 }) { Text(r.fromName) .fontSize(FbMetric.fontSmall) .fontColor(FbColors.muted) .maxLines(1) Image($r('app.media.fb_ic_forward')) .width(12) .height(12) .renderMode(ImageRenderMode.Template) .fillColor(FbColors.faint) Text(r.toName) .fontSize(FbMetric.fontSmall) .fontColor(FbColors.ink) .layoutWeight(1) .maxLines(1) FbTag({ text: r.label, tone: 'muted' }) } .width('100%') .padding({ top: 10, bottom: 10 }) .alignItems(VerticalAlign.Center) if (idx < this.rels.length - 1) { Divider().color(FbColors.line2) } }, (r: FbKinRelation): string => 'rel_' + r.id.toString()) } .width('100%') } .width('100%') .padding(FbMetric.cardPad) .backgroundColor(FbColors.surface) .borderRadius(FbMetric.radius) .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 }) .alignItems(HorizontalAlign.Start) } .width('100%') } .layoutWeight(1) .width('100%') .scrollBar(BarState.Off) .edgeEffect(EdgeEffect.Spring) .align(Alignment.TopStart) } .width('100%') .layoutWeight(1) } /** 一圈亲属卡 */ @Builder ringCard(r: FbKinRing) { Column({ space: 10 }) { Row({ space: 8 }) { Text(r.title) .fontSize(FbMetric.fontSmall) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Text(r.hint) .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) Blank() Text(r.rels.length.toString()) .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) } .width('100%') if (r.rels.length === 0) { Text('还没有连线') .fontSize(FbMetric.fontSmall) .fontColor(FbColors.faint) } else { Flex({ wrap: FlexWrap.Wrap }) { ForEach(r.rels, (rel: FbKinRelation) => { Row({ space: 6 }) { FbAvatar({ name: rel.toName, color: this.colorOf(rel.toId), dia: 28, fontSizeVp: 12 }) Column({ space: 1 }) { Text(rel.toName) .fontSize(FbMetric.fontSmall) .fontColor(FbColors.ink) .maxLines(1) Text(rel.label) .fontSize(10) .fontColor(FbColors.faint) } .alignItems(HorizontalAlign.Start) } .padding({ left: 6, right: 12, top: 5, bottom: 5 }) .backgroundColor(FbColors.surfaceAlt) .borderRadius(FbMetric.radiusPill) .margin({ right: 8, bottom: 8 }) .alignItems(VerticalAlign.Center) }, (rel: FbKinRelation): string => 'kr_' + rel.id.toString()) } .width('100%') } } .width('100%') .padding(FbMetric.cardPad) .backgroundColor(FbColors.surface) .borderRadius(FbMetric.radius) .margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 }) .alignItems(HorizontalAlign.Start) } // ============================================================ // 共用小件 // ============================================================ @Builder segBtn(label: string, key: string, color: string) { Text(label) .fontSize(FbMetric.fontSmall) .fontWeight(this.subView === key ? FontWeight.Medium : FontWeight.Normal) .fontColor(this.subView === key ? Color.White : FbColors.muted) .textAlign(TextAlign.Center) .height(30) .layoutWeight(1) .backgroundColor(this.subView === key ? color : Color.Transparent) .borderRadius(FbMetric.radiusPill) .onClick(() => { this.subView = key; }) } @Builder scopeChip(label: string, key: string) { Text(label) .fontSize(FbMetric.fontTiny) .fontColor(this.rosterScope === key ? Color.White : FbColors.muted) .backgroundColor(this.rosterScope === key ? '#4A7C82' : FbColors.inputBg) .padding({ left: 10, right: 10, top: 5, bottom: 5 }) .borderRadius(FbMetric.radiusPill) .onClick(() => { this.rosterScope = key; }) } @Builder groupHeader(title: string, count: string) { Row({ space: 8 }) { Text(title) .fontSize(FbMetric.fontSmall) .fontWeight(FontWeight.Medium) .fontColor(FbColors.ink) Text(count) .fontSize(FbMetric.fontTiny) .fontColor(FbColors.faint) Blank() } .width('100%') .padding({ left: 4, right: 4, top: 12, bottom: 8 }) .backgroundColor(FbColors.paper) } private byScope(): FbRosterItem[] { return this.all.filter((p: FbRosterItem): boolean => p.roster === this.rosterScope); } private groups(): FbGroup[] { return fbGroupRoster(this.byScope()); } /** 三圈:我 / 直系 / 其他亲属 */ private rings(): FbKinRing[] { const direct: string[] = ['parent', 'spouse', 'child']; return [ { title: '直系', hint: '父母 / 配偶 / 子女', rels: this.rels.filter((r: FbKinRelation): boolean => direct.indexOf(r.kind) >= 0) }, { title: '其他亲属', hint: '兄弟姐妹与姻亲', rels: this.rels.filter((r: FbKinRelation): boolean => direct.indexOf(r.kind) < 0) } ]; } /** 取某个人的头像色(从名册里查,查不到用默认砖红) */ private colorOf(id: number): string { const hit: FbRosterItem | undefined = this.all.find((p: FbRosterItem): boolean => p.id === id); return hit !== undefined ? hit.avatarColor : '#A84A28'; } }