This commit is contained in:
owenchen committed 2026-10-23 09:46:48 +08:00
1 parent 94d5fb2ec9
commit 6c86c59bdc
15 files changed
+426 -11

No files matched your search

@@ -1396,5 +1396,64 @@ namespace CloudBuilder.Master.Service
}
catch (Exception ex) { Console.WriteLine($"[FamilyTreeExchange] 审计写入失败: {ex.Message}"); }
}
/// <summary>
/// 族谱「统计」 tab 的聚合查询:总数/男/女/世代层数 + 各世代人数分布。
/// 世代 = 始祖起 BFS 深度(始祖=1 世);未连到始祖的旁支不入世代桶,只计入 Total。
/// 与 ListExchangeFiles/SaveExchangeFile 同套成员闸:非本族成员不能拉本族聚合数据。
/// </summary>
[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<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
IRepository<FamilyTreeChildrenMapEntity> repositoryChildrenMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
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<string, int> genByPerson = new Dictionary<string, int>();
if (!string.IsNullOrEmpty(d.AncestorPersonId))
{
Queue<KeyValuePair<string, int>> q = new Queue<KeyValuePair<string, int>>();
q.Enqueue(new KeyValuePair<string, int>(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<string, int>(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;
}
}
}
@@ -0,0 +1,37 @@
using CloudBuilder.Core.Authorization;
namespace CloudBuilder.Master.Data
{
/// <summary>
/// 族谱「统计」tab 的聚合数据:总数/男/女/世代层数 + 各世代人数分布。
/// 后端单次遍历算完(族员表 + 亲子边表 + 始祖 BFS),避免前端拉全表再算;
/// 世代 = 始祖起 BFS 深度(始祖=1 世),未连到始祖的旁支不入世代桶,只计入 Total。
/// </summary>
[TypeScript]
public class FamilyTreeStatisticsData
{
/// <summary>入树成员总数(family_person 按 TreeId 过滤后的行数)</summary>
public int Total { get; set; }
/// <summary>男性族员数(Gender=M)</summary>
public int Male { get; set; }
/// <summary>女性族员数(Gender=W,GenderPolicy 里女= W 而非 F)</summary>
public int Female { get; set; }
/// <summary>世代层数(本谱出现过的最大世代号;空谱为 0)</summary>
public int GenerationCount { get; set; }
/// <summary>各世代人数分布(按世代升序;空档世代也占位,图表柱不连续)</summary>
public FamilyTreeGenerationCountData[] PerGeneration { get; set; } = [];
}
/// <summary>单个世代的人数条目(世代号 + 该世代族员数)</summary>
[TypeScript]
public class FamilyTreeGenerationCountData
{
public int Generation { get; set; }
public int Count { get; set; }
}
}
@@ -34,5 +34,8 @@ namespace CloudBuilder.Master.Service
FamilyTreeExchangeFileInfo[] ListBackups(string treeId);
int RestoreBackup(string targetTreeId, string backupFileName);
/// <summary>族谱「统计」 tab 的聚合数据(总数/男女/世代层数/各世代人数);仅本族成员可拉</summary>
FamilyTreeStatisticsData GetStatistics(string treeId);
}
}
@@ -176,6 +176,10 @@ return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeExchangeFileInf
{
var compositionData = new RestoreBackupCompositionData2{ ␍targetTreeId = targetTreeId, ␍backupFileName = backupFileName }; ␍return Task.Run(() => serviceRequest.PostAsJsonAsync<RestoreBackupCompositionData2, Int32>("/FamilyTree/RestoreBackup", compositionData)).GetAwaiter().GetResult(); }
public FamilyTreeStatisticsData GetStatistics(string treeId)
{
return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeStatisticsData>("/FamilyTree/GetStatistics?treeId=" + treeId.ToString())).GetAwaiter().GetResult(); }
}
}
@@ -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);
}
}
}
}
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
<History>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||;</History>
<History>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||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
@@ -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<DetailTab>("overview");
/** 统计 tab 聚合数据:懒加载(首次切到 stats 才拉),loadedTree 变化时复位重拉 */
const statsData = ref<FamilyTreeStatisticsData | null>(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<void> {
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<void> {
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<void> {
@@ -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<void> {
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<void> {
// 已有成员里选配偶(快速建员之外的入口,对齐旧页):与族员搜索共用同一个底部弹窗
function pickSpouse(): void {
personPickMode.value = "spouse";
// 选配偶同样限本族:与切换当前族员共用同一弹窗与过滤口径
personPickConfig.fixedFilters.value = { TreeId: treeId.value.trim() };
personPickVisible.value = true;
}
@@ -1864,6 +1925,11 @@ defineExpose({
<text>{{ t("FamilyTreePage.tabPerms") }}</text>
<view v-if="pendingPermissions.length" class="ft-tab__dot" />
</view>
<!-- 统计 tab:总数/男女/世代分布,管理员与已绑定成员可见(与后端 GetStatistics 成员闸同口径) -->
<view v-if="data.IsManager || data.IsMember" class="ft-tab"
:class="{ 'ft-tab--on': activeTab === 'stats' }" @tap="switchTab('stats')">
<text>{{ t("FamilyTreePage.tabStats") }}</text>
</view>
</view>
<!-- ===== 概览 ===== -->
@@ -2005,7 +2071,8 @@ defineExpose({
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
}}</text></view>
<view v-for="record in visibleSpouses" :key="record.SpouseId" class="ft-mrow">
<view class="ft-av"
<view class="ft-av" hover-class="ft-av--tap" :hover-stay-time="80"
@tap.stop="setCurrentPerson(record.SpouseId)"
:style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
<image v-if="personAvatar(record.SpouseId)" class="ft-av__img"
:src="personAvatar(record.SpouseId)" mode="aspectFill" />
@@ -2047,7 +2114,8 @@ defineExpose({
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
}}</text></view>
<view v-for="record in visibleChildren" :key="record.ChildrenId" class="ft-mrow">
<view class="ft-av"
<view class="ft-av" hover-class="ft-av--tap" :hover-stay-time="80"
@tap.stop="setCurrentPerson(record.ChildrenId)"
:style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
<image v-if="personAvatar(record.ChildrenId)" class="ft-av__img"
:src="personAvatar(record.ChildrenId)" mode="aspectFill" />
@@ -2149,6 +2217,56 @@ defineExpose({
</view>
</view>
<!-- ===== 统计 ===== -->
<view v-show="activeTab === 'stats'" class="ft-pane">
<!-- 顶部四概览卡:总数 / 世代层数 / 男 / 女(与参考图同序) -->
<view class="ft-stat-grid">
<view class="ft-stat-card">
<text class="ft-stat-card__num">{{ statsData?.Total ?? 0 }}</text>
<text class="ft-stat-card__label">{{ t("FamilyTreePage.statsTotal") }}</text>
</view>
<view class="ft-stat-card">
<text class="ft-stat-card__num">{{ statsData?.GenerationCount ?? 0 }}</text>
<text class="ft-stat-card__label">{{ t("FamilyTreePage.statsGeneration") }}</text>
</view>
<view class="ft-stat-card">
<text class="ft-stat-card__num ft-stat-card__num--male">{{ statsData?.Male ?? 0 }}</text>
<text class="ft-stat-card__label">{{ t("FamilyTreePage.statsMale") }}</text>
</view>
<view class="ft-stat-card">
<text class="ft-stat-card__num ft-stat-card__num--female">{{ statsData?.Female ?? 0 }}</text>
<text class="ft-stat-card__label">{{ t("FamilyTreePage.statsFemale") }}</text>
</view>
</view>
<!-- 各世代人数:表头 + 行(世代 / 人数 / 横向条形图,按最大人数归一) -->
<view class="ft-card">
<view class="ft-card__head">
<text class="ft-card__title">{{ t("FamilyTreePage.statsPerGeneration") }}</text>
</view>
<view v-if="statsLoading" class="ft-empty"><text>{{ t("FamilyTreePage.statsLoading") }}</text></view>
<view v-else-if="!statsData?.PerGeneration?.length" class="ft-empty"><text>{{
t("FamilyTreePage.statsEmpty") }}</text></view>
<block v-else>
<view class="ft-gen-head">
<text class="ft-gen-head__col ft-gen-head__col--gen">{{ t("FamilyTreePage.statsColGeneration")
}}</text>
<text class="ft-gen-head__col ft-gen-head__col--cnt">{{ t("FamilyTreePage.statsColCount") }}</text>
<text class="ft-gen-head__col ft-gen-head__col--bar" />
</view>
<view v-for="gen in statsData?.PerGeneration ?? []" :key="gen.Generation" class="ft-gen-row">
<text class="ft-gen-row__gen">{{ gen.Generation }}{{ t("FamilyTreePage.statsGenerationUnit")
}}</text>
<text class="ft-gen-row__cnt">{{ gen.Count }}</text>
<view class="ft-gen-bar">
<view class="ft-gen-bar__fill"
:style="{ width: genBarWidth(gen.Count) }" />
</view>
</view>
</block>
</view>
</view>
<view class="tree-page__detail-space" />
</scroll-view>
</view>
@@ -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;
@@ -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<string, string>,
) {
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 };
}
@@ -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;
}
@@ -1,4 +1,7 @@
import { ArticleFilenameEntity } from "./articleFilenameEntity";
export class ArticleFilenameDisplayData extends ArticleFilenameEntity {
export class ArticleFilenameDisplayData extends ArticleFilenameEntity
{
public ImageUrl: string = "";
}
@@ -0,0 +1,7 @@
export class FamilyTreeGenerationCountData
{
public Generation: number = 0;
public Count: number = 0;
}
@@ -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[] = [];
}
@@ -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",
@@ -337,9 +337,20 @@
"tabOverview": "概览",
"tabMembers": "成员",
"tabPerms": "权限",
"tabStats": "统计",
"statMembers": "位族人",
"statManagers": "管理员",
"statPending": "待审核",
"statsTotal": "入树成员",
"statsGeneration": "世代层数",
"statsMale": "男",
"statsFemale": "女",
"statsPerGeneration": "各世代人数",
"statsColGeneration": "世代",
"statsColCount": "人数",
"statsGenerationUnit": "世",
"statsLoading": "统计加载中…",
"statsEmpty": "暂无统计数据",
"introTitle": "族谱简介",
"quickActions": "快捷操作",
"editProfile": "编辑资料",
@@ -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<FamilyTreeStatisticsData>(
{
url: "/FamilyTree/GetStatistics?treeId="+data,
method: "post",
data,
}
)
};
}
const familyTreeService= new FamilyTreeService();