From 6c86c59bdc3be82ccb75823f7e2af26399117dec Mon Sep 17 00:00:00 2001 From: owenchen Date: Fri, 23 Oct 2026 09:46:48 +0800 Subject: [PATCH] ow --- .../FamilyTreeService.auto.cs | 59 +++++ .../Data/FamilyTreeStatisticsData.cs | 37 +++ .../Service/IFamilyTreeService.auto.cs | 3 + .../Master/FamilyTreeRequest.auto.cs | 4 + .../Master/FamilyTreeController.auto.cs | 20 ++ .../FolderProfile3.pubxml.user | 2 +- .../family-tree-home/family-tree-home.vue | 232 +++++++++++++++++- .../src/composables/usePickerConfigs.ts | 19 +- .../src/data/auto/articleDisplayData.ts | 11 +- .../data/auto/articleFilenameDisplayData.ts | 5 +- .../auto/familyTreeGenerationCountData.ts | 7 + .../src/data/auto/familyTreeStatisticsData.ts | 11 + uniapp/youjiala/src/locale/lang/en-US.json | 11 + uniapp/youjiala/src/locale/lang/zh-CN.json | 11 + .../src/service/master/familyTreeService.ts | 11 + 15 files changed, 429 insertions(+), 14 deletions(-) create mode 100644 CloudBuilder/CloudBuilder.Master/Data/FamilyTreeStatisticsData.cs create mode 100644 uniapp/youjiala/src/data/auto/familyTreeGenerationCountData.ts create mode 100644 uniapp/youjiala/src/data/auto/familyTreeStatisticsData.ts diff --git a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs index 451d8ee..1609cde 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs @@ -1396,5 +1396,64 @@ namespace CloudBuilder.Master.Service } catch (Exception ex) { Console.WriteLine($"[FamilyTreeExchange] 审计写入失败: {ex.Message}"); } } + + /// + /// 族谱「统计」 tab 的聚合查询:总数/男/女/世代层数 + 各世代人数分布。 + /// 世代 = 始祖起 BFS 深度(始祖=1 世);未连到始祖的旁支不入世代桶,只计入 Total。 + /// 与 ListExchangeFiles/SaveExchangeFile 同套成员闸:非本族成员不能拉本族聚合数据。 + /// + [PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)] + public FamilyTreeStatisticsData GetStatistics(string treeId) + { + FamilyTreeStatisticsData data = new FamilyTreeStatisticsData(); + if (string.IsNullOrEmpty(treeId)) return data; + + FamilyTreeEntity d = repository.DetachedEntities.Where(x => x.TreeId == treeId).FirstOrDefault(); + if (d == null) throw new ValidatedException("族谱不存在."); + + var helper = new FamilyTreePermissionHelper(service); + if (!helper.IsMember(treeId) && !helper.IsManager(treeId)) + throw new ValidatedException("您不是该族谱成员."); + + IRepository repositoryPerson = repository.GetRepository>(); + IRepository repositoryChildrenMap = repository.GetRepository>(); + + FamilyPersonEntity[] persons = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId).ToArray(); + FamilyTreeChildrenMapEntity[] childrenMaps = repositoryChildrenMap.DetachedEntities.Where(x => x.TreeId == treeId).ToArray(); + + data.Total = persons.Length; + data.Male = persons.Count(x => x.Gender == "M"); + data.Female = persons.Count(x => x.Gender == "W"); + + // BFS 从始祖起逐层下钻,记录每人世代号;genByPerson 去重兼做环保护 + Dictionary genByPerson = new Dictionary(); + if (!string.IsNullOrEmpty(d.AncestorPersonId)) + { + Queue> q = new Queue>(); + q.Enqueue(new KeyValuePair(d.AncestorPersonId, 1)); + genByPerson[d.AncestorPersonId] = 1; + while (q.Count > 0) + { + var cur = q.Dequeue(); + foreach (var c in childrenMaps.Where(x => x.PersonId == cur.Key)) + { + if (string.IsNullOrEmpty(c.ChildrenId) || genByPerson.ContainsKey(c.ChildrenId)) continue; + genByPerson[c.ChildrenId] = cur.Value + 1; + q.Enqueue(new KeyValuePair(c.ChildrenId, cur.Value + 1)); + } + } + } + + data.GenerationCount = genByPerson.Count > 0 ? genByPerson.Values.Max() : 0; + + // 按世代升序聚合;未入桶的旁支不入世代分布(仅计 Total) + data.PerGeneration = genByPerson.Values + .GroupBy(g => g) + .OrderBy(grp => grp.Key) + .Select(grp => new FamilyTreeGenerationCountData { Generation = grp.Key, Count = grp.Count() }) + .ToArray(); + + return data; + } } } diff --git a/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeStatisticsData.cs b/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeStatisticsData.cs new file mode 100644 index 0000000..8474f84 --- /dev/null +++ b/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeStatisticsData.cs @@ -0,0 +1,37 @@ +using CloudBuilder.Core.Authorization; + +namespace CloudBuilder.Master.Data +{ + /// + /// 族谱「统计」tab 的聚合数据:总数/男/女/世代层数 + 各世代人数分布。 + /// 后端单次遍历算完(族员表 + 亲子边表 + 始祖 BFS),避免前端拉全表再算; + /// 世代 = 始祖起 BFS 深度(始祖=1 世),未连到始祖的旁支不入世代桶,只计入 Total。 + /// + [TypeScript] + public class FamilyTreeStatisticsData + { + /// 入树成员总数(family_person 按 TreeId 过滤后的行数) + public int Total { get; set; } + + /// 男性族员数(Gender=M) + public int Male { get; set; } + + /// 女性族员数(Gender=W,GenderPolicy 里女= W 而非 F) + public int Female { get; set; } + + /// 世代层数(本谱出现过的最大世代号;空谱为 0) + public int GenerationCount { get; set; } + + /// 各世代人数分布(按世代升序;空档世代也占位,图表柱不连续) + public FamilyTreeGenerationCountData[] PerGeneration { get; set; } = []; + } + + /// 单个世代的人数条目(世代号 + 该世代族员数) + [TypeScript] + public class FamilyTreeGenerationCountData + { + public int Generation { get; set; } + + public int Count { get; set; } + } +} diff --git a/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs b/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs index 33d61e5..8fe4e8f 100644 --- a/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs @@ -34,5 +34,8 @@ namespace CloudBuilder.Master.Service FamilyTreeExchangeFileInfo[] ListBackups(string treeId); int RestoreBackup(string targetTreeId, string backupFileName); + /// 族谱「统计」 tab 的聚合数据(总数/男女/世代层数/各世代人数);仅本族成员可拉 + FamilyTreeStatisticsData GetStatistics(string treeId); + } } diff --git a/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs b/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs index b45a91a..5020402 100644 --- a/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs +++ b/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs @@ -176,6 +176,10 @@ return Task.Run(() => serviceRequest.PostAsJsonAsync serviceRequest.PostAsJsonAsync("/FamilyTree/RestoreBackup", compositionData)).GetAwaiter().GetResult(); } + public FamilyTreeStatisticsData GetStatistics(string treeId) + { +return Task.Run(() => serviceRequest.PostAsJsonAsync("/FamilyTree/GetStatistics?treeId=" + treeId.ToString())).GetAwaiter().GetResult(); } + } } \ No newline at end of file diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs b/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs index 6407eb6..49c2993 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs +++ b/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs @@ -551,6 +551,26 @@ this.familyTreeService= familyTreeService; } } + [PermissionOperate(name: "FamilyTree", operate: "Find")] + [HttpPost] + public dynamic GetStatistics(string treeId) + { + try + { + + var result = familyTreeService.GetStatistics(treeId); + return result; + } + catch (ValidatedException vex) + { + return vex; + } + catch (Exception ex) + { + return new ServiceErrorException(ex); + } + } + } } \ No newline at end of file diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user b/CloudBuilder/CloudBuilder.Web.Entry/Properties/PublishProfiles/FolderProfile3.pubxml.user index 2571eca..695936e 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-10-22T06:27:02.4892588Z||;True|2026-10-21T21:22:01.1456473+08:00||;True|2026-10-20T15:45:39.3393674+08:00||;True|2026-10-17T20:49:21.0401213+08:00||;True|2026-09-23T13:19:13.0148919+08:00||;True|2026-09-23T13:16:07.8584304+08:00||;True|2026-09-22T23:03:13.6509491+08:00||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;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||; + True|2026-10-23T01:45:02.7814696Z||;True|2026-10-22T14:27:02.4892588+08:00||;True|2026-10-21T21:22:01.1456473+08:00||;True|2026-10-20T15:45:39.3393674+08:00||;True|2026-10-17T20:49:21.0401213+08:00||;True|2026-09-23T13:19:13.0148919+08:00||;True|2026-09-23T13:16:07.8584304+08:00||;True|2026-09-22T23:03:13.6509491+08:00||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;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/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue b/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue index 03b19e0..143bc07 100644 --- a/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue +++ b/uniapp/youjiala/src/components/family-tree-home/family-tree-home.vue @@ -13,6 +13,7 @@ import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayDat import type { AdminRow } from "@/admin/admin-types"; import type { ExchangeScope } from "@/composables/useFamilyTreeExchange"; import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo"; +import type { FamilyTreeStatisticsData } from "@/data/auto/familyTreeStatisticsData"; import { StorageUsageData } from "@/data/auto/storageUsageData"; import { useLocale } from "uview-pro"; import { computed, onMounted, ref } from "vue"; @@ -477,10 +478,50 @@ const managerPermissions = computed(() => ); // ===== 详情态重设计:三段式 Tab + 底部操作条 + 弹窗(仅浏览态;编辑态沿用旧内联布局) ===== -/** 详情 tab:概览 / 成员 / 权限 */ -type DetailTab = "overview" | "members" | "perms"; +/** 详情 tab:概览 / 成员 / 权限 / 统计 */ +type DetailTab = "overview" | "members" | "perms" | "stats"; const activeTab = ref("overview"); +/** 统计 tab 聚合数据:懒加载(首次切到 stats 才拉),loadedTree 变化时复位重拉 */ +const statsData = ref(null); +const statsLoading = ref(false); +/** 各世代分布里的最大人数(图表柱按此归一化到 100%) */ +const statsMaxGenerationCount = computed(() => + (statsData.value?.PerGeneration ?? []).reduce( + (m, g) => (g.Count > m ? g.Count : m), + 0, + ), +); + +/** 单世代条宽百分比:以最大世代人数为 100% 归一,零值给极小宽度避免不可见 */ +function genBarWidth(count: number): string { + const max = statsMaxGenerationCount.value; + if (!max || count <= 0) { + return "0%"; + } + return `${Math.max(2, Math.round((count / max) * 100))}%`; +} + +/** 拉取本谱统计:进统计 tab 时懒加载;失败回 null 走空态 */ +async function fetchStatistics(): Promise { + const key = String(treeId.value ?? "").trim(); + if (!key) { + statsData.value = null; + return; + } + statsLoading.value = true; + try { + statsData.value = await familyTreeService.GetStatistics(key); + } + catch (error) { + console.error(error); + statsData.value = null; + } + finally { + statsLoading.value = false; + } +} + /** 成员搜索关键字 + 性别筛选(支系无后端字段,暂不提供该 chip) */ type MemberFilter = "all" | "male" | "female"; const memberKeyword = ref(""); @@ -559,6 +600,10 @@ const visibleChildren = computed(() => childMaps.value.filter( function switchTab(tab: DetailTab): void { activeTab.value = tab; + // 统计 tab 首次进入(或换谱后数据为空)懒加载一次,后续复用缓存 + if (tab === "stats" && !statsData.value && !statsLoading.value) { + void fetchStatistics(); + } } /** 成员行是否命中当前搜索 + 性别筛选(name 传姓+谱名连写,personId 供编号检索) */ @@ -749,6 +794,9 @@ function resetDetail(): void { memberKeyword.value = ""; memberFilter.value = "all"; treeEditVisible.value = false; + // 换谱时统计缓存一并清空,下次进 stats tab 重新懒加载(避免沿用上一谱数据) + statsData.value = null; + statsLoading.value = false; } /** 载入详情:FindByCurrentPerson({ key, personId }),首载 personId 传空串(后端回默认族员) */ @@ -779,6 +827,10 @@ async function load(): Promise { else if (activeTab.value === "members" && !data.value.IsManager && !data.value.IsMember) { activeTab.value = "overview"; } + // 统计 tab 与成员 tab 同一把可见闸(非成员不可见,后端 GetStatistics 也会拒) + else if (activeTab.value === "stats" && !data.value.IsManager && !data.value.IsMember) { + activeTab.value = "overview"; + } } async function onOpenTree(row: FamilyTreeViewEntity): Promise { @@ -885,6 +937,8 @@ const personPickMode = ref<"current" | "spouse">("current"); function pickCurrentPerson(): void { personPickMode.value = "current"; + // 只能查本族族员:弹窗打开前把当前 TreeId 下发给 loader 作等值过滤 + personPickConfig.fixedFilters.value = { TreeId: treeId.value.trim() }; personPickVisible.value = true; } @@ -917,6 +971,11 @@ async function setCurrentPerson(personId: string): Promise { data.value = result; ensureLists(); loaded.value = true; + // 选定当前族员后自动切到「成员」tab:当前族员卡与夫妻/子女列表都在那里,免去二次点 tab; + // 普通用户无成员 tab 可见(与模板 v-if="data.IsManager || data.IsMember" 同源),保持原 tab + if (data.value.IsManager || data.value.IsMember) { + activeTab.value = "members"; + } } } @@ -1098,6 +1157,8 @@ async function onPersonEdited(person?: FamilyPersonDisplayData): Promise { // 已有成员里选配偶(快速建员之外的入口,对齐旧页):与族员搜索共用同一个底部弹窗 function pickSpouse(): void { personPickMode.value = "spouse"; + // 选配偶同样限本族:与切换当前族员共用同一弹窗与过滤口径 + personPickConfig.fixedFilters.value = { TreeId: treeId.value.trim() }; personPickVisible.value = true; } @@ -1864,6 +1925,11 @@ defineExpose({ {{ t("FamilyTreePage.tabPerms") }} + + + {{ t("FamilyTreePage.tabStats") }} + @@ -2005,7 +2071,8 @@ defineExpose({ {{ t("FamilyTreePage.spouseEmpty") }} - @@ -2047,7 +2114,8 @@ defineExpose({ {{ t("FamilyTreePage.childEmpty") }} - @@ -2149,6 +2217,56 @@ defineExpose({ + + + + + + {{ statsData?.Total ?? 0 }} + {{ t("FamilyTreePage.statsTotal") }} + + + {{ statsData?.GenerationCount ?? 0 }} + {{ t("FamilyTreePage.statsGeneration") }} + + + {{ statsData?.Male ?? 0 }} + {{ t("FamilyTreePage.statsMale") }} + + + {{ statsData?.Female ?? 0 }} + {{ t("FamilyTreePage.statsFemale") }} + + + + + + + {{ t("FamilyTreePage.statsPerGeneration") }} + + {{ t("FamilyTreePage.statsLoading") }} + {{ + t("FamilyTreePage.statsEmpty") }} + + + {{ t("FamilyTreePage.statsColGeneration") + }} + {{ t("FamilyTreePage.statsColCount") }} + + + + {{ gen.Generation }}{{ t("FamilyTreePage.statsGenerationUnit") + }} + {{ gen.Count }} + + + + + + + + @@ -4409,6 +4527,106 @@ defineExpose({ color: #bfc3c9; } +/* ===== 统计 tab:顶部四概览卡 + 各世代人数分布表 ===== */ +.ft-stat-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16rpx; + margin-bottom: 20rpx; +} + +.ft-stat-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 8rpx; + padding: 28rpx 8rpx; + border-radius: 24rpx; + background-color: #ffffff; + box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.06); +} + +.ft-stat-card__num { + font-size: 44rpx; + font-weight: 700; + line-height: 1.1; + color: var(--fg-primary, #d98a3d); +} + +.ft-stat-card__num--male { + color: #2b8fff; +} + +.ft-stat-card__num--female { + color: #eb2f96; +} + +.ft-stat-card__label { + font-size: 22rpx; + color: #8a8f99; +} + +/* 各世代表头:与行同列宽对齐(世代列固定 / 人数列固定 / 条占余下) */ +.ft-gen-head { + display: flex; + align-items: center; + padding: 12rpx 8rpx; + font-size: 22rpx; + color: #8a8f99; +} + +.ft-gen-head__col--gen, +.ft-gen-row__gen { + flex: none; + width: 96rpx; +} + +.ft-gen-head__col--cnt, +.ft-gen-row__cnt { + flex: none; + width: 96rpx; + text-align: right; +} + +.ft-gen-head__col--bar { + flex: 1; + margin-left: 16rpx; +} + +.ft-gen-row { + display: flex; + align-items: center; + padding: 14rpx 8rpx; + border-top: 1rpx solid rgba(0, 0, 0, 0.05); +} + +.ft-gen-row__gen { + font-size: 26rpx; + color: #303133; +} + +.ft-gen-row__cnt { + font-size: 26rpx; + font-weight: 600; + color: #303133; +} + +.ft-gen-bar { + flex: 1; + height: 20rpx; + margin-left: 16rpx; + overflow: hidden; + border-radius: 999rpx; + background-color: var(--fg-soft, #f0e6da); +} + +.ft-gen-bar__fill { + height: 100%; + border-radius: 999rpx; + background: linear-gradient(90deg, var(--fg-primary, #d98a3d), #f0b877); + transition: width 0.3s ease; +} + /* ===== 快捷操作 2 列 ===== */ .ft-quick { display: grid; @@ -4489,6 +4707,12 @@ defineExpose({ height: 100%; } +/* 成员行头像作为「切换当前族员」可点区,按下反馈:微隔加背景,与行尾操作区拉开灰度,避免误判为无交互 */ +.ft-av--tap { + opacity: 0.72; + transform: scale(0.96); +} + .ft-mrow__main { display: flex; flex: 1; diff --git a/uniapp/youjiala/src/composables/usePickerConfigs.ts b/uniapp/youjiala/src/composables/usePickerConfigs.ts index bd2f669..077991e 100644 --- a/uniapp/youjiala/src/composables/usePickerConfigs.ts +++ b/uniapp/youjiala/src/composables/usePickerConfigs.ts @@ -400,19 +400,30 @@ export function usePersonPickConfig() { // 成员编号列不显示:弹窗列表口径与族谱页统一用常用名辨识(rowKey 仍是 PersonId,仅不占行) ]); - async function loader(page: number, pageSize: number, keyword: string) { + async function loader( + page: number, + pageSize: number, + keyword: string, + filters: Record, + ) { const criteria = new FamilyPersonSearchCriteria(); criteria.GridColumns = []; criteria.page = page; criteria.limit = pageSize; criteria.orderby = FamilyPersonEntity.DB_GENEALOGY_NAME; - const filters: SearchFilter[] = []; + const filterList: SearchFilter[] = []; + // 固定过滤:按 TreeId 等值限定本族(由调用方设定,缺失则不限制) + if (filters.TreeId) { + filterList.push( + equalFilter(FamilyPersonEntity.DB_TREE_ID, filters.TreeId), + ); + } if (keyword) { - filters.push( + filterList.push( containFilter(FamilyPersonEntity.DB_GENEALOGY_NAME, keyword), ); } - criteria.Filter = filters; + criteria.Filter = filterList; const data = await familyPersonService.FindBySearchCriteria(criteria); return { rows: data?.Datas ?? [], total: data?.Total ?? 0 }; } diff --git a/uniapp/youjiala/src/data/auto/articleDisplayData.ts b/uniapp/youjiala/src/data/auto/articleDisplayData.ts index 34b983e..1c53880 100644 --- a/uniapp/youjiala/src/data/auto/articleDisplayData.ts +++ b/uniapp/youjiala/src/data/auto/articleDisplayData.ts @@ -1,9 +1,12 @@ + import { ArticleEntity } from "./articleEntity"; -export class ArticleDisplayData extends ArticleEntity { +export class ArticleDisplayData extends ArticleEntity +{ public AuthorLocalName: string = ""; public AuthorAvatarUrl: string = ""; public CoverUrl: string = ""; - public IsOwner: boolean = false; - public IsMember: boolean = false; - public IsManager: boolean = false; + public IsOwner: boolean =false; + public IsMember: boolean =false; + public IsManager: boolean =false; + } diff --git a/uniapp/youjiala/src/data/auto/articleFilenameDisplayData.ts b/uniapp/youjiala/src/data/auto/articleFilenameDisplayData.ts index 29556a0..2236da8 100644 --- a/uniapp/youjiala/src/data/auto/articleFilenameDisplayData.ts +++ b/uniapp/youjiala/src/data/auto/articleFilenameDisplayData.ts @@ -1,4 +1,7 @@ + import { ArticleFilenameEntity } from "./articleFilenameEntity"; -export class ArticleFilenameDisplayData extends ArticleFilenameEntity { +export class ArticleFilenameDisplayData extends ArticleFilenameEntity +{ public ImageUrl: string = ""; + } diff --git a/uniapp/youjiala/src/data/auto/familyTreeGenerationCountData.ts b/uniapp/youjiala/src/data/auto/familyTreeGenerationCountData.ts new file mode 100644 index 0000000..243b9af --- /dev/null +++ b/uniapp/youjiala/src/data/auto/familyTreeGenerationCountData.ts @@ -0,0 +1,7 @@ + +export class FamilyTreeGenerationCountData +{ + public Generation: number = 0; + public Count: number = 0; + +} diff --git a/uniapp/youjiala/src/data/auto/familyTreeStatisticsData.ts b/uniapp/youjiala/src/data/auto/familyTreeStatisticsData.ts new file mode 100644 index 0000000..2668f3e --- /dev/null +++ b/uniapp/youjiala/src/data/auto/familyTreeStatisticsData.ts @@ -0,0 +1,11 @@ + +import { FamilyTreeGenerationCountData } from "./familyTreeGenerationCountData"; +export class FamilyTreeStatisticsData +{ + public Total: number = 0; + public Male: number = 0; + public Female: number = 0; + public GenerationCount: number = 0; + public PerGeneration: FamilyTreeGenerationCountData[] = []; + +} diff --git a/uniapp/youjiala/src/locale/lang/en-US.json b/uniapp/youjiala/src/locale/lang/en-US.json index 6851401..77bb779 100644 --- a/uniapp/youjiala/src/locale/lang/en-US.json +++ b/uniapp/youjiala/src/locale/lang/en-US.json @@ -337,9 +337,20 @@ "tabOverview": "Overview", "tabMembers": "Members", "tabPerms": "Access", + "tabStats": "Stats", "statMembers": "members", "statManagers": "managers", "statPending": "pending", + "statsTotal": "Members", + "statsGeneration": "Generations", + "statsMale": "Male", + "statsFemale": "Female", + "statsPerGeneration": "Members per Generation", + "statsColGeneration": "Gen.", + "statsColCount": "Count", + "statsGenerationUnit": "G", + "statsLoading": "Loading statistics…", + "statsEmpty": "No statistics yet", "introTitle": "Brief", "quickActions": "Quick Actions", "editProfile": "Edit Info", diff --git a/uniapp/youjiala/src/locale/lang/zh-CN.json b/uniapp/youjiala/src/locale/lang/zh-CN.json index b905737..0843171 100644 --- a/uniapp/youjiala/src/locale/lang/zh-CN.json +++ b/uniapp/youjiala/src/locale/lang/zh-CN.json @@ -337,9 +337,20 @@ "tabOverview": "概览", "tabMembers": "成员", "tabPerms": "权限", + "tabStats": "统计", "statMembers": "位族人", "statManagers": "管理员", "statPending": "待审核", + "statsTotal": "入树成员", + "statsGeneration": "世代层数", + "statsMale": "男", + "statsFemale": "女", + "statsPerGeneration": "各世代人数", + "statsColGeneration": "世代", + "statsColCount": "人数", + "statsGenerationUnit": "世", + "statsLoading": "统计加载中…", + "statsEmpty": "暂无统计数据", "introTitle": "族谱简介", "quickActions": "快捷操作", "editProfile": "编辑资料", diff --git a/uniapp/youjiala/src/service/master/familyTreeService.ts b/uniapp/youjiala/src/service/master/familyTreeService.ts index 108f3b6..f280c33 100644 --- a/uniapp/youjiala/src/service/master/familyTreeService.ts +++ b/uniapp/youjiala/src/service/master/familyTreeService.ts @@ -5,6 +5,7 @@ import { request } from "@/utils/request"; import type { FamilyTreeResultData } from "@/data/auto/familyTreeResultData"; import type { FamilyTreeExchangeData } from "@/data/auto/familyTreeExchangeData"; import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo"; + import type { FamilyTreeStatisticsData } from "@/data/auto/familyTreeStatisticsData"; class FamilyTreeService{ @@ -227,6 +228,16 @@ class FamilyTreeService{ ) }; + async GetStatistics(data: string) { + return request( + { + url: "/FamilyTree/GetStatistics?treeId="+data, + method: "post", + data, + } + ) + }; + } const familyTreeService= new FamilyTreeService();