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}"); }
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user