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();