ow
This commit is contained in:
1 parent
94d5fb2ec9
commit
6c86c59bdc
15 files changed
+429
-14
No files matched your search
@@ -1396,5 +1396,64 @@ namespace CloudBuilder.Master.Service
|
|||||||
}
|
}
|
||||||
catch (Exception ex) { Console.WriteLine($"[FamilyTreeExchange] 审计写入失败: {ex.Message}"); }
|
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);
|
FamilyTreeExchangeFileInfo[] ListBackups(string treeId);
|
||||||
int RestoreBackup(string targetTreeId, string backupFileName);
|
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(); }
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
|
<_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 />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -13,6 +13,7 @@ import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayDat
|
|||||||
import type { AdminRow } from "@/admin/admin-types";
|
import type { AdminRow } from "@/admin/admin-types";
|
||||||
import type { ExchangeScope } from "@/composables/useFamilyTreeExchange";
|
import type { ExchangeScope } from "@/composables/useFamilyTreeExchange";
|
||||||
import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo";
|
import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo";
|
||||||
|
import type { FamilyTreeStatisticsData } from "@/data/auto/familyTreeStatisticsData";
|
||||||
import { StorageUsageData } from "@/data/auto/storageUsageData";
|
import { StorageUsageData } from "@/data/auto/storageUsageData";
|
||||||
import { useLocale } from "uview-pro";
|
import { useLocale } from "uview-pro";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
@@ -477,10 +478,50 @@ const managerPermissions = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ===== 详情态重设计:三段式 Tab + 底部操作条 + 弹窗(仅浏览态;编辑态沿用旧内联布局) =====
|
// ===== 详情态重设计:三段式 Tab + 底部操作条 + 弹窗(仅浏览态;编辑态沿用旧内联布局) =====
|
||||||
/** 详情 tab:概览 / 成员 / 权限 */
|
/** 详情 tab:概览 / 成员 / 权限 / 统计 */
|
||||||
type DetailTab = "overview" | "members" | "perms";
|
type DetailTab = "overview" | "members" | "perms" | "stats";
|
||||||
const activeTab = ref<DetailTab>("overview");
|
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) */
|
/** 成员搜索关键字 + 性别筛选(支系无后端字段,暂不提供该 chip) */
|
||||||
type MemberFilter = "all" | "male" | "female";
|
type MemberFilter = "all" | "male" | "female";
|
||||||
const memberKeyword = ref("");
|
const memberKeyword = ref("");
|
||||||
@@ -559,6 +600,10 @@ const visibleChildren = computed(() => childMaps.value.filter(
|
|||||||
|
|
||||||
function switchTab(tab: DetailTab): void {
|
function switchTab(tab: DetailTab): void {
|
||||||
activeTab.value = tab;
|
activeTab.value = tab;
|
||||||
|
// 统计 tab 首次进入(或换谱后数据为空)懒加载一次,后续复用缓存
|
||||||
|
if (tab === "stats" && !statsData.value && !statsLoading.value) {
|
||||||
|
void fetchStatistics();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 成员行是否命中当前搜索 + 性别筛选(name 传姓+谱名连写,personId 供编号检索) */
|
/** 成员行是否命中当前搜索 + 性别筛选(name 传姓+谱名连写,personId 供编号检索) */
|
||||||
@@ -749,6 +794,9 @@ function resetDetail(): void {
|
|||||||
memberKeyword.value = "";
|
memberKeyword.value = "";
|
||||||
memberFilter.value = "all";
|
memberFilter.value = "all";
|
||||||
treeEditVisible.value = false;
|
treeEditVisible.value = false;
|
||||||
|
// 换谱时统计缓存一并清空,下次进 stats tab 重新懒加载(避免沿用上一谱数据)
|
||||||
|
statsData.value = null;
|
||||||
|
statsLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 载入详情:FindByCurrentPerson({ key, personId }),首载 personId 传空串(后端回默认族员) */
|
/** 载入详情:FindByCurrentPerson({ key, personId }),首载 personId 传空串(后端回默认族员) */
|
||||||
@@ -779,6 +827,10 @@ async function load(): Promise<void> {
|
|||||||
else if (activeTab.value === "members" && !data.value.IsManager && !data.value.IsMember) {
|
else if (activeTab.value === "members" && !data.value.IsManager && !data.value.IsMember) {
|
||||||
activeTab.value = "overview";
|
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> {
|
async function onOpenTree(row: FamilyTreeViewEntity): Promise<void> {
|
||||||
@@ -885,6 +937,8 @@ const personPickMode = ref<"current" | "spouse">("current");
|
|||||||
|
|
||||||
function pickCurrentPerson(): void {
|
function pickCurrentPerson(): void {
|
||||||
personPickMode.value = "current";
|
personPickMode.value = "current";
|
||||||
|
// 只能查本族族员:弹窗打开前把当前 TreeId 下发给 loader 作等值过滤
|
||||||
|
personPickConfig.fixedFilters.value = { TreeId: treeId.value.trim() };
|
||||||
personPickVisible.value = true;
|
personPickVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,6 +971,11 @@ async function setCurrentPerson(personId: string): Promise<void> {
|
|||||||
data.value = result;
|
data.value = result;
|
||||||
ensureLists();
|
ensureLists();
|
||||||
loaded.value = true;
|
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 {
|
function pickSpouse(): void {
|
||||||
personPickMode.value = "spouse";
|
personPickMode.value = "spouse";
|
||||||
|
// 选配偶同样限本族:与切换当前族员共用同一弹窗与过滤口径
|
||||||
|
personPickConfig.fixedFilters.value = { TreeId: treeId.value.trim() };
|
||||||
personPickVisible.value = true;
|
personPickVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1864,6 +1925,11 @@ defineExpose({
|
|||||||
<text>{{ t("FamilyTreePage.tabPerms") }}</text>
|
<text>{{ t("FamilyTreePage.tabPerms") }}</text>
|
||||||
<view v-if="pendingPermissions.length" class="ft-tab__dot" />
|
<view v-if="pendingPermissions.length" class="ft-tab__dot" />
|
||||||
</view>
|
</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>
|
</view>
|
||||||
|
|
||||||
<!-- ===== 概览 ===== -->
|
<!-- ===== 概览 ===== -->
|
||||||
@@ -2005,7 +2071,8 @@ defineExpose({
|
|||||||
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
|
<view v-if="!visibleSpouses.length" class="ft-empty"><text>{{ t("FamilyTreePage.spouseEmpty")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-for="record in visibleSpouses" :key="record.SpouseId" class="ft-mrow">
|
<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))">
|
:style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
|
||||||
<image v-if="personAvatar(record.SpouseId)" class="ft-av__img"
|
<image v-if="personAvatar(record.SpouseId)" class="ft-av__img"
|
||||||
:src="personAvatar(record.SpouseId)" mode="aspectFill" />
|
:src="personAvatar(record.SpouseId)" mode="aspectFill" />
|
||||||
@@ -2047,7 +2114,8 @@ defineExpose({
|
|||||||
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
|
<view v-if="!visibleChildren.length" class="ft-empty"><text>{{ t("FamilyTreePage.childEmpty")
|
||||||
}}</text></view>
|
}}</text></view>
|
||||||
<view v-for="record in visibleChildren" :key="record.ChildrenId" class="ft-mrow">
|
<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))">
|
:style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
|
||||||
<image v-if="personAvatar(record.ChildrenId)" class="ft-av__img"
|
<image v-if="personAvatar(record.ChildrenId)" class="ft-av__img"
|
||||||
:src="personAvatar(record.ChildrenId)" mode="aspectFill" />
|
:src="personAvatar(record.ChildrenId)" mode="aspectFill" />
|
||||||
@@ -2149,6 +2217,56 @@ defineExpose({
|
|||||||
</view>
|
</view>
|
||||||
</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" />
|
<view class="tree-page__detail-space" />
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
@@ -4409,6 +4527,106 @@ defineExpose({
|
|||||||
color: #bfc3c9;
|
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 列 ===== */
|
/* ===== 快捷操作 2 列 ===== */
|
||||||
.ft-quick {
|
.ft-quick {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -4489,6 +4707,12 @@ defineExpose({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 成员行头像作为「切换当前族员」可点区,按下反馈:微隔加背景,与行尾操作区拉开灰度,避免误判为无交互 */
|
||||||
|
.ft-av--tap {
|
||||||
|
opacity: 0.72;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
.ft-mrow__main {
|
.ft-mrow__main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -400,19 +400,30 @@ export function usePersonPickConfig() {
|
|||||||
// 成员编号列不显示:弹窗列表口径与族谱页统一用常用名辨识(rowKey 仍是 PersonId,仅不占行)
|
// 成员编号列不显示:弹窗列表口径与族谱页统一用常用名辨识(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();
|
const criteria = new FamilyPersonSearchCriteria();
|
||||||
criteria.GridColumns = [];
|
criteria.GridColumns = [];
|
||||||
criteria.page = page;
|
criteria.page = page;
|
||||||
criteria.limit = pageSize;
|
criteria.limit = pageSize;
|
||||||
criteria.orderby = FamilyPersonEntity.DB_GENEALOGY_NAME;
|
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) {
|
if (keyword) {
|
||||||
filters.push(
|
filterList.push(
|
||||||
containFilter(FamilyPersonEntity.DB_GENEALOGY_NAME, keyword),
|
containFilter(FamilyPersonEntity.DB_GENEALOGY_NAME, keyword),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
criteria.Filter = filters;
|
criteria.Filter = filterList;
|
||||||
const data = await familyPersonService.FindBySearchCriteria(criteria);
|
const data = await familyPersonService.FindBySearchCriteria(criteria);
|
||||||
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
|
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
|
||||||
import { ArticleEntity } from "./articleEntity";
|
import { ArticleEntity } from "./articleEntity";
|
||||||
export class ArticleDisplayData extends ArticleEntity {
|
export class ArticleDisplayData extends ArticleEntity
|
||||||
|
{
|
||||||
public AuthorLocalName: string = "";
|
public AuthorLocalName: string = "";
|
||||||
public AuthorAvatarUrl: string = "";
|
public AuthorAvatarUrl: string = "";
|
||||||
public CoverUrl: string = "";
|
public CoverUrl: string = "";
|
||||||
public IsOwner: boolean = false;
|
public IsOwner: boolean =false;
|
||||||
public IsMember: boolean = false;
|
public IsMember: boolean =false;
|
||||||
public IsManager: boolean = false;
|
public IsManager: boolean =false;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
import { ArticleFilenameEntity } from "./articleFilenameEntity";
|
import { ArticleFilenameEntity } from "./articleFilenameEntity";
|
||||||
export class ArticleFilenameDisplayData extends ArticleFilenameEntity {
|
export class ArticleFilenameDisplayData extends ArticleFilenameEntity
|
||||||
|
{
|
||||||
public ImageUrl: string = "";
|
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",
|
"tabOverview": "Overview",
|
||||||
"tabMembers": "Members",
|
"tabMembers": "Members",
|
||||||
"tabPerms": "Access",
|
"tabPerms": "Access",
|
||||||
|
"tabStats": "Stats",
|
||||||
"statMembers": "members",
|
"statMembers": "members",
|
||||||
"statManagers": "managers",
|
"statManagers": "managers",
|
||||||
"statPending": "pending",
|
"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",
|
"introTitle": "Brief",
|
||||||
"quickActions": "Quick Actions",
|
"quickActions": "Quick Actions",
|
||||||
"editProfile": "Edit Info",
|
"editProfile": "Edit Info",
|
||||||
|
|||||||
@@ -337,9 +337,20 @@
|
|||||||
"tabOverview": "概览",
|
"tabOverview": "概览",
|
||||||
"tabMembers": "成员",
|
"tabMembers": "成员",
|
||||||
"tabPerms": "权限",
|
"tabPerms": "权限",
|
||||||
|
"tabStats": "统计",
|
||||||
"statMembers": "位族人",
|
"statMembers": "位族人",
|
||||||
"statManagers": "管理员",
|
"statManagers": "管理员",
|
||||||
"statPending": "待审核",
|
"statPending": "待审核",
|
||||||
|
"statsTotal": "入树成员",
|
||||||
|
"statsGeneration": "世代层数",
|
||||||
|
"statsMale": "男",
|
||||||
|
"statsFemale": "女",
|
||||||
|
"statsPerGeneration": "各世代人数",
|
||||||
|
"statsColGeneration": "世代",
|
||||||
|
"statsColCount": "人数",
|
||||||
|
"statsGenerationUnit": "世",
|
||||||
|
"statsLoading": "统计加载中…",
|
||||||
|
"statsEmpty": "暂无统计数据",
|
||||||
"introTitle": "族谱简介",
|
"introTitle": "族谱简介",
|
||||||
"quickActions": "快捷操作",
|
"quickActions": "快捷操作",
|
||||||
"editProfile": "编辑资料",
|
"editProfile": "编辑资料",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { request } from "@/utils/request";
|
|||||||
import type { FamilyTreeResultData } from "@/data/auto/familyTreeResultData";
|
import type { FamilyTreeResultData } from "@/data/auto/familyTreeResultData";
|
||||||
import type { FamilyTreeExchangeData } from "@/data/auto/familyTreeExchangeData";
|
import type { FamilyTreeExchangeData } from "@/data/auto/familyTreeExchangeData";
|
||||||
import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo";
|
import type { FamilyTreeExchangeFileInfo } from "@/data/auto/familyTreeExchangeFileInfo";
|
||||||
|
import type { FamilyTreeStatisticsData } from "@/data/auto/familyTreeStatisticsData";
|
||||||
|
|
||||||
class FamilyTreeService{
|
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();
|
const familyTreeService= new FamilyTreeService();
|
||||||
|
|||||||
Reference in new issue
Block a user