From 8a13dfb836323dd27bac8e04a4dc075cbf5a4ec6 Mon Sep 17 00:00:00 2001 From: owenchen Date: Wed, 2 Sep 2026 10:59:06 +0800 Subject: [PATCH] ow --- .../FamilyTreeService.auto.cs | 25 + .../Data/FamilyPersonData.cs | 4 +- .../FolderProfile3.pubxml.user | 2 +- .../master/AncestryTreeChartModal.vue | 574 ++++++++++++++++++ .../master/FamilyTreeChartModal.vue | 150 ++++- .../src/views/master/FamilyTreeOperation.vue | 10 +- 6 files changed, 740 insertions(+), 25 deletions(-) create mode 100644 WebVue/src/components/master/AncestryTreeChartModal.vue diff --git a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs index b86a1a0..d924a85 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs @@ -13,6 +13,7 @@ using CloudBuilder.Security.Utility; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; namespace CloudBuilder.Master.Service { public class FamilyTreeService : IFamilyTreeService @@ -55,6 +56,24 @@ namespace CloudBuilder.Master.Service data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository>().DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray(); + List ps = new List(); + ps.Add(d.AncestorPersonId); + if (data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0) + { + ps.AddRange(data.CurrentPerson.FamilyTreeSpouseMaps.Select(x => x.SpouseId).ToArray()); + } + if (data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0) + { + ps.AddRange(data.CurrentPerson.FamilyTreeChildrenMaps.Select(x => x.ChildrenId).ToArray()); + } + + ps = ps.Distinct().ToList(); + FamilyPersonEntity[] families = repositoryFamilyPerson.DetachedEntities.Where(x => ps.AsQueryable().Contains(x.PersonId)).ToArray(); + if (families != null && families.Length > 0) + { + data.CurrentPerson.FamilyTreePersons = families.Select(x => new FamilyPersonDisplayData(x)).ToArray(); + } + return data; } @@ -346,6 +365,12 @@ namespace CloudBuilder.Master.Service var currentPerson = data.CurrentPerson != null ? data.CurrentPerson.FamilyPerson.PersonId : null; if (string.IsNullOrEmpty(currentPerson)) currentPerson = d.AncestorPersonId; + if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0) + data.CurrentPerson.FamilyTreeSpouseMaps.ToLookup(x => x.TreeId = d.TreeId); + + if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0) + data.CurrentPerson.FamilyTreeChildrenMaps.ToLookup(x => x.TreeId = d.TreeId); + SaveSpouseMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeSpouseMaps); //SaveParentMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeParentMaps); SaveSonMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeChildrenMaps); diff --git a/CloudBuilder/CloudBuilder.Master/Data/FamilyPersonData.cs b/CloudBuilder/CloudBuilder.Master/Data/FamilyPersonData.cs index 713ffe0..5587659 100644 --- a/CloudBuilder/CloudBuilder.Master/Data/FamilyPersonData.cs +++ b/CloudBuilder/CloudBuilder.Master/Data/FamilyPersonData.cs @@ -12,11 +12,9 @@ namespace CloudBuilder.Master.Data public class FamilyPersonData { public FamilyPersonDisplayData? FamilyPerson { get; set; } - + public FamilyPersonDisplayData[]? FamilyTreePersons { get; set; } public FamilyTreeSpouseMapViewEntity[]? FamilyTreeSpouseMaps { get; set; } - public FamilyTreeChildrenMapViewEntity[]? FamilyTreeParentMaps { get; set; } - public FamilyTreeChildrenMapViewEntity[]? FamilyTreeChildrenMaps { get; set; } } } diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user index ce90dd0..f25b4b1 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user +++ b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user @@ -3,7 +3,7 @@ <_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2 - True|2026-08-26T05:34:12.3805892Z||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||; + True|2026-09-02T02:58:16.8081797Z||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||; \ No newline at end of file diff --git a/WebVue/src/components/master/AncestryTreeChartModal.vue b/WebVue/src/components/master/AncestryTreeChartModal.vue new file mode 100644 index 0000000..a729c94 --- /dev/null +++ b/WebVue/src/components/master/AncestryTreeChartModal.vue @@ -0,0 +1,574 @@ + + + + + diff --git a/WebVue/src/components/master/FamilyTreeChartModal.vue b/WebVue/src/components/master/FamilyTreeChartModal.vue index a0a1109..2c5dee1 100644 --- a/WebVue/src/components/master/FamilyTreeChartModal.vue +++ b/WebVue/src/components/master/FamilyTreeChartModal.vue @@ -1,8 +1,18 @@ @@ -81,7 +91,7 @@ const renderCard = (d: any) => { : ""; const clickable = data.hasChildren || !data.loaded ? "ftc-clickable" : ""; return ` -
+
${ico} ${esc(data.name)} @@ -94,6 +104,9 @@ const renderCard = (d: any) => {
`; }; +/** 同级(配偶/孩子)按 Orderby 升序排列 */ +const byOrder = (a: any, b: any) => (a.Orderby ?? 0) - (b.Orderby ?? 0); + /** 每个族员的描述:人员信息 + 配偶 + 孩子(根来自 data,其他来自 loadedNodes) */ const describePerson = (personId: string): any => { if (personId === rootPersonId.value) { @@ -101,8 +114,8 @@ const describePerson = (personId: string): any => { if (!dp?.FamilyPerson) return null; return { person: dp.FamilyPerson, - spouses: dp.FamilyTreeSpouseMaps ?? [], - children: dp.FamilyTreeChildrenMaps ?? [], + spouses: [...(dp.FamilyTreeSpouseMaps ?? [])].sort(byOrder), + children: [...(dp.FamilyTreeChildrenMaps ?? [])].sort(byOrder), loaded: true, }; } @@ -110,8 +123,8 @@ const describePerson = (personId: string): any => { if (loaded) { return { person: loaded.person, - spouses: loaded.spouses ?? [], - children: loaded.children ?? [], + spouses: [...(loaded.spouses ?? [])].sort(byOrder), + children: [...(loaded.children ?? [])].sort(byOrder), loaded: true, }; } @@ -125,7 +138,13 @@ const buildGraphData = () => { const root = rootPersonId.value; if (!root) return { nodes, edges }; - const Y_STEP = 116; + const CARD_WIDTH = 360; + const CARD_HEIGHT = 192; + const H_GAP = 60; + const V_GAP = 40; + const X_STEP = CARD_WIDTH + H_GAP; // 420,避免水平重叠 + const Y_STEP = CARD_HEIGHT + V_GAP; // 232,避免垂直重叠 + const SPOUSE_GAP = X_STEP; let cursor = 0; const walk = (personId: string, depth: number, xBase: number) => { @@ -156,13 +175,12 @@ const buildGraphData = () => { }, }); - // 配偶与本人同一行,水平间隔 200 对齐 - const SPOUSE_GAP = 200; + // 配偶与本人同一行,水平间隔 X_STEP 对齐 desc.spouses.forEach((s: any, i: number) => { const sid = "s-" + personId + "-" + i; nodes.push({ id: sid, - style: { x: x + 210 + i * SPOUSE_GAP, y }, + style: { x: xBase + X_STEP + i * SPOUSE_GAP, y }, data: { kind: "spouse", tag: "配偶", @@ -177,8 +195,8 @@ const buildGraphData = () => { }); if (expanded) { - // 孩子列与本人第一行的配偶列对齐(x + 210),即孩子位于配偶正下方,竖直跟第一行对齐 - const childBaseX = x + 210; + // 孩子列与本人第一行的配偶列对齐(xBase + X_STEP),即孩子位于配偶正下方,竖直跟第一行对齐 + const childBaseX = xBase + X_STEP; desc.children.forEach((c: any) => { const childId = c.ChildrenId; @@ -193,7 +211,7 @@ const buildGraphData = () => { personId: childId, tag: c.RelationType ? relationLabel(c.RelationType) : "子女", name: (c.ChildrenSurname ?? "") + (c.ChildrenGenealogyName ?? ""), - commonName: c.CommonName, + commonName: c.ChildrenCommonName, gender: c.Gender, hasChildren: false, expanded: false, @@ -214,6 +232,28 @@ const buildGraphData = () => { const containerRef = ref(); let graph: any = null; +/** 卡片最小显示比例:世代过多时 fitView 会把整图缩得很小,卡片将看不清,低于该比例时放大 */ +const MIN_ZOOM = 0.4; + +/** 缩放按钮单次增量 */ +const ZOOM_STEP = 0.1; + +/** 单击按钮放大/缩小一点点(按增量叠加并限制在 zoomRange 内) */ +const zoomBy = (delta: number) => { + if (!graph) return; + const next = graph.getZoom() + delta; + graph.zoomTo(Math.min(Math.max(next, MIN_ZOOM), 10)); +}; + +/** 还原为适配整个画布 */ +const fitGraph = async () => { + if (!graph) return; + await graph.fitView(); + if (graph.getZoom() < MIN_ZOOM) { + await graph.zoomTo(MIN_ZOOM); + } +}; + const destroyGraph = () => { if (graph) { graph.destroy(); @@ -233,14 +273,16 @@ const initGraph = async () => { graph = new Graph({ container: containerRef.value, - autoFit: "view", + // 内容小于视口时不缩放(卡片保持原始大小),内容溢出时才适配 + autoFit: { type: "view", options: { when: "overflow", direction: "both" } }, + zoomRange: [MIN_ZOOM, 10], data: { nodes, edges }, node: { type: "html", style: { - size: [180, 96], - dx: -90, - dy: -48, + size: [360, 192], + dx: -180, + dy: -96, // 官方文档推荐函数形式,类型声明仅支持 string/HTMLElement,故断言 innerHTML: renderCard as any, }, @@ -276,6 +318,10 @@ const initGraph = async () => { } }); await graph.render(); + // 世代过多导致 fitView 缩得太小时,放大到最小显示比例,保证卡片可读 + if (graph.getZoom() < MIN_ZOOM) { + await graph.zoomTo(MIN_ZOOM); + } }; const toggleExpanded = (personId: string) => { @@ -322,20 +368,61 @@ watch( onBeforeUnmount(() => destroyGraph()); -const onCancel = () => emit("cancel"); +const onCancel = () => { + visibleModel.value = false; + emit("cancel"); +};