From 27284040cc261037ed3807a46f58b303a3d383ec Mon Sep 17 00:00:00 2001 From: owenchen Date: Mon, 19 Oct 2026 13:36:58 +0800 Subject: [PATCH] ow --- .../DmsFileService.auto.cs | 7 +- .../FamilyPersonService.auto.cs | 17 +- .../FamilyTreeService.auto.cs | 197 +- .../FamilyTreePermissionHelper.cs | 59 +- .../Data/FamilyTreeData.cs | 9 + .../CloudBuilderMasterRegistEntity.auto.cs | 1 + .../Entity/FamilyTreeEntity.auto.cs | 4 +- .../Service/IFamilyTreeService.auto.cs | 2 + .../Master/FamilyTreeRequest.auto.cs | 14 + .../Master/FamilyTreeController.auto.cs | 49 + .../admin-pick-popup/admin-pick-popup.vue | 45 +- .../family-tree-home/family-tree-home.vue | 1581 ++++++++++++++++- .../file-grid-list/file-grid-list.vue | 26 +- .../person-editor/person-editor.vue | 951 ++++++++++ .../person-quick-create.vue | 259 --- .../youjiala/src/composables/useDmsFiles.ts | 5 +- .../src/composables/usePickerConfigs.ts | 47 + .../youjiala/src/composables/useShareHall.ts | 5 +- .../youjiala/src/composables/useTrashFiles.ts | 5 +- .../youjiala/src/data/auto/familyTreeData.ts | 3 + .../youjiala/src/data/custom/fileBrowser.ts | 2 + uniapp/youjiala/src/locale/lang/en-US.json | 50 +- uniapp/youjiala/src/locale/lang/zh-CN.json | 50 +- uniapp/youjiala/src/md/admin-pages.md | 6 +- .../pages/security/family-person-enquiry.vue | 48 +- .../security/family-person-operation.vue | 4 +- uniapp/youjiala/src/router/routes.ts | 2 +- .../src/service/master/familyTreeService.ts | 20 + .../youjiala/src/static/icons/full-chart.svg | 1 + .../youjiala/src/static/icons/sub-chart.svg | 1 + 30 files changed, 3042 insertions(+), 428 deletions(-) create mode 100644 uniapp/youjiala/src/components/person-editor/person-editor.vue delete mode 100644 uniapp/youjiala/src/components/person-quick-create/person-quick-create.vue create mode 100644 uniapp/youjiala/src/static/icons/full-chart.svg create mode 100644 uniapp/youjiala/src/static/icons/sub-chart.svg diff --git a/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs index 1cd241c..b63e565 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/DmsFileService.auto.cs @@ -987,6 +987,10 @@ namespace CloudBuilder.Master.Service repositoryUpload.UpdateNow(upload); DmsFileEntity file = repository.DetachedEntities.Where(x => x.FileId == dmsFile.FileId).FirstOrDefault()!; + // 主表 OrgFileName 是列表显示/下载名的权威来源,重命名必须同步(历史上只改 upload 表,前端列表名字改不动); + // 注意只同步 OrgFileName,主表 FileName 是 FL 编号不能拿新名覆盖 + file.OrgFileName = dmsFile.FileName; + repository.UpdateNow(file); //修改目录档对应的名称 IRepository repositoryFolder = repository.GetRepository>(); DmsFolderEntity dmsFolder = repositoryFolder.DetachedEntities.Where(x => x.FolderName == file.FileName).FirstOrDefault()!; @@ -1683,7 +1687,8 @@ namespace CloudBuilder.Master.Service entity.OrderKey = dmsFile.OrderKey; entity.FileName = upload.UploadName; entity.FileType = FileTypePolicy.GetType(entity.OrderKey); - entity.OrgFileName = dmsFile.FileName; + // 主表必须存原始文件名(前端 multipart 字段名),不能存 dmsFile.FileName 物理分片名,否则下载时拿到的就是系统保存名 + entity.OrgFileName = dmsFile.OrgFileName; entity.FileSize = dmsFile.FileSize; entity.AllowDeleteIndc = entity.AllowRenameIndc = YesNoPolicy.YES; entity.IsDirectoryIndc = YesNoPolicy.NO; diff --git a/CloudBuilder/CloudBuilder.Master.Service/FamilyPersonService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/FamilyPersonService.auto.cs index 394370e..2eb08ed 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/FamilyPersonService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/FamilyPersonService.auto.cs @@ -116,6 +116,13 @@ namespace CloudBuilder.Master.Service throw new ValidatedException("记录已存在."); } + // 新增人没有既有 PersonId 可查支系,最小闸门:管理员或本谱已生效成员才能往谱里造人(防普通用户直接调 API 污染族谱) + var helper = new FamilyTreePermissionHelper(service); + if (!helper.IsManager(data.FamilyPerson.TreeId) && !helper.IsMember(data.FamilyPerson.TreeId)) + { + throw new ValidatedException("您没有新增权限."); + } + ValidateHelper.Validate(service, data.FamilyPerson, FamilyPersonEntity.DB_NAME_FAMILY_PERSON); d = new FamilyPersonEntity(); @@ -163,7 +170,15 @@ namespace CloudBuilder.Master.Service public void Delete(string key) { var ent = repository.DetachedEntities.Where(x => x.PersonId == key).FirstOrDefault()!; - if (ent != null) { repository.DeleteNow(ent); } + if (ent == null) return; + + // 删人与改人口径一致:需在该人所属支系内(管理员在 HasPermission 内短路为真) + if (!new FamilyTreePermissionHelper(service).HasPermission(ent.TreeId, ent.PersonId)) + { + throw new ValidatedException("您没有删除权限."); + } + + repository.DeleteNow(ent); } [PermissionOperate(name: MasterPermissionPolicy.FamilyPerson, operate: Operate.Delete)] diff --git a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs index 00d67de..82c00f6 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs @@ -131,6 +131,12 @@ namespace CloudBuilder.Master.Service data.IsManager = data.FamilyTreePermissions.Where(x => x.UserName == service.UserId && x.ManagerIndc == YesNoPolicy.YES).Any(); if (data.IsManager) data.IsCanAmend = true; + + // 成员口径:有绑定到自己且已生效的权限行即算(与锚点是否可编无关,供前端选项卡级控权);需在下方过滤名单前算,否则非管理员只剩待审行 + var selfValidRows = data.FamilyTreePermissions.Where(x => x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).ToArray(); + data.IsMember = selfValidRows.Any(); + // 自己绑定的族员(供前端切换「绑定账号/解除绑定」按钮),管理员无绑定概念不下发 + if (!data.IsManager) data.SelfBindPersonId = selfValidRows.Select(x => x.PersonId).FirstOrDefault() ?? ""; } if (!data.IsManager && data.FamilyTreePermissions.Any()) @@ -164,14 +170,16 @@ namespace CloudBuilder.Master.Service IRepository repositorySpouseMap = repository.GetRepository>(); - data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository>().DetachedEntities.Where(x => x.PersonId == currentPerson).OrderBy(x => x.Orderby).ToArray(); + data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository>().DetachedEntities.Where(x => x.PersonId == currentPerson && x.TreeId == d.TreeId).OrderBy(x => x.Orderby).ToArray(); //data.CurrentPerson.FamilyTreeParentMaps = repositoryChildrenMap.GetRepository>().DetachedEntities.Where(x => x.ChildrenId == key).OrderBy(x => x.Orderby).ToArray(); - data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository>().DetachedEntities.Where(x => x.PersonId == currentPerson).OrderBy(x => x.Orderby).ToArray(); + data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository>().DetachedEntities.Where(x => x.PersonId == currentPerson && x.TreeId == d.TreeId).OrderBy(x => x.Orderby).ToArray(); // 补充当前人员及其配偶、子女的人员信息(含头像地址),供谱系图卡片显示头像 List ps = new List(); if (!string.IsNullOrEmpty(currentPerson)) ps.Add(currentPerson); + // 始祖恒定并入下发集合:切换当前族员后,前端「始祖信息」卡仍需回查始祖档案(Distinct 去重) + if (!string.IsNullOrEmpty(d.AncestorPersonId)) ps.Add(d.AncestorPersonId); if (data.CurrentPerson.FamilyTreeSpouseMaps != null) { ps.AddRange(data.CurrentPerson.FamilyTreeSpouseMaps.Select(x => x.SpouseId).ToArray()); @@ -192,6 +200,10 @@ namespace CloudBuilder.Master.Service if (!data.IsCanAmend) data.IsCanAmend = new FamilyTreePermissionHelper(service).HasPermission(data.FamilyTree.TreeId, currentPerson); + // 逐个目标人的可编集合(关系行里的旁支靠它隐藏编辑/删除钮);管理员全量可编不限量不下发 + if (!data.IsManager && data.IsMember) + data.CanAmendPersonIds = new FamilyTreePermissionHelper(service).GetAmendablePersonIds(data.FamilyTree.TreeId).ToArray(); + return data; } @@ -297,9 +309,9 @@ namespace CloudBuilder.Master.Service if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0) data.CurrentPerson.FamilyTreeChildrenMaps.ToLookup(x => x.TreeId = d.TreeId); - SaveSpouseMaps(currentPerson, data.CurrentPerson.FamilyTreeSpouseMaps); + SaveSpouseMaps(d.TreeId, currentPerson, data.CurrentPerson.FamilyTreeSpouseMaps); //SaveParentMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeParentMaps); - SaveSonMaps(currentPerson, data.CurrentPerson.FamilyTreeChildrenMaps); + SaveSonMaps(d.TreeId, currentPerson, data.CurrentPerson.FamilyTreeChildrenMaps); return FindByCurrentPerson(d.TreeId, currentPerson); } @@ -316,30 +328,87 @@ namespace CloudBuilder.Master.Service public void RequestFamilyTreePermission(string treeId, string currentPersonId) { IRepository repositoryPermission = repository.GetRepository>(); - FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.PersonId == currentPersonId && service.UserId == x.UserName).FirstOrDefault()!; + // 一人一谱一行:PersonId 不再参与定位与绑定(申请锚点≠本人),查重按 TreeId+UserName + FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && service.UserId == x.UserName).FirstOrDefault()!; if (treePermissionEntity != null) return; treePermissionEntity = new FamilyTreePermissionEntity(); treePermissionEntity.TreeId = treeId; - treePermissionEntity.PersonId = currentPersonId; + // 申请时不绑定任何族员(旧口径把申请锚点——往往是始祖——绑了进去,导致成员误得大支系); + // 绑定时机:审批时按中文姓名同名唯一自动绑,或通过后由用户自行「绑定账号」 + treePermissionEntity.PersonId = ""; treePermissionEntity.UserName = service.UserId; treePermissionEntity.ManagerIndc = YesNoPolicy.NO; treePermissionEntity.ValidIndc = YesNoPolicy.NO; InsertFamilyTreePermission(treePermissionEntity); } + /// + /// PersonId 属复合主键 (PersonId,TreeId,UserName),EF 分离实体直接改主键列更新不可靠,换绑一律删旧行插新行 + /// + private void RebindFamilyTreePermission(FamilyTreePermissionEntity row, string personId) + { + IRepository repositoryPermission = repository.GetRepository>(); + + FamilyTreePermissionEntity fresh = new FamilyTreePermissionEntity(); + fresh.TreeId = row.TreeId; + fresh.UserName = row.UserName; + fresh.PersonId = personId; + fresh.ManagerIndc = row.ManagerIndc; + fresh.ValidIndc = row.ValidIndc; + + repositoryPermission.DeleteNow(row); + repositoryPermission.InsertNow(fresh); + } + + /// + /// 用户中文姓名(security_user.user_local_name);登录账号只是标识,不参与同名比较 + /// + private string GetUserLocalName(string userName) + { + IRepository repositoryUser = repository.GetRepository>(); + return repositoryUser.DetachedEntities.Where(x => x.UserName == userName).FirstOrDefault()?.UserLocalName ?? ""; + } + + /// + /// 同名口径:族员「姓+谱名」组合或常用名与用户中文姓名相同 + /// + private bool IsMatchedByLocalName(FamilyPersonEntity person, string localName) + { + if (string.IsNullOrEmpty(localName)) return false; + return (person.Surname ?? "") + (person.GenealogyName ?? "") == localName || person.CommonName == localName; + } + [PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)] [UnitOfWork] public void ApproveFamilyTreePermission(string treeId, string currentPersonId, string userName, string managerIndc) { IRepository repositoryPermission = repository.GetRepository>(); - FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.PersonId == currentPersonId && userName == x.UserName).FirstOrDefault()!; + // 行定位不再用 PersonId(申请行已不绑族员,旧数据也可能已换绑),TreeId+UserName 即唯一行 + FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && userName == x.UserName).FirstOrDefault()!; if (treePermissionEntity == null) return; treePermissionEntity.ManagerIndc = managerIndc; treePermissionEntity.ValidIndc = YesNoPolicy.YES; + // 通过为成员:不绑始祖;用户中文姓名与「姓+谱名」或常用名相同且全谱唯一才自动绑定,否则留空由用户自行「绑定账号」 + if (managerIndc != YesNoPolicy.YES) + { + string localName = GetUserLocalName(userName); + IRepository repositoryPerson = repository.GetRepository>(); + // 姓+谱名是运行时拼接,EF 翻不动就拉全谱内存筛(单谱人数量级小) + FamilyPersonEntity[] matches = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId).ToArray() + .Where(x => IsMatchedByLocalName(x, localName)).ToArray(); + string boundPersonId = matches.Length == 1 ? matches[0].PersonId : ""; + + if (treePermissionEntity.PersonId != boundPersonId) + { + RebindFamilyTreePermission(treePermissionEntity, boundPersonId); + return; + } + } + repositoryPermission.UpdateNow(treePermissionEntity); } @@ -348,28 +417,74 @@ namespace CloudBuilder.Master.Service public void RejectFamilyTreePermission(string treeId, string currentPersonId, string userName) { IRepository repositoryPermission = repository.GetRepository>(); - FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.PersonId == currentPersonId && userName == x.UserName).FirstOrDefault()!; + FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && userName == x.UserName).FirstOrDefault()!; if (treePermissionEntity == null) return; repositoryPermission.DeleteNow(treePermissionEntity); } - private void SaveSpouseMaps(string personId, FamilyTreeSpouseMapViewEntity[] maps) + /// + /// 绑定账号:把当前族员绑到自己已生效的权限行(仅限与登录中文姓名同名的族员); + /// 返回绑定后的详情数据(锚点切到刚绑的族员) + /// + [PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)] + [UnitOfWork] + public FamilyTreeData BindSelfPerson(string treeId, string personId) + { + IRepository repositoryPermission = repository.GetRepository>(); + FamilyTreePermissionEntity row = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).FirstOrDefault()!; + if (row == null) throw new ValidatedException("您还未通过审批,不能绑定账号."); + + IRepository repositoryPerson = repository.GetRepository>(); + FamilyPersonEntity person = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId && x.PersonId == personId).FirstOrDefault()!; + if (person == null) throw new ValidatedException("族员不存在."); + + // 同名口径:族员「姓+谱名」组合或常用名与登录中文姓名相同才可绑定(与管理员审批自动绑定同一把尺) + if (!IsMatchedByLocalName(person, GetUserLocalName(service.UserId))) + throw new ValidatedException("只能绑定与您的中文姓名同名的族员."); + + if (row.PersonId != personId) RebindFamilyTreePermission(row, personId); + + return FindByCurrentPerson(treeId, personId); + } + + /// + /// 解除绑定:把自己已生效权限行的 PersonId 置空(锚点回到始祖),解绑后可重新「绑定账号」 + /// + [PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)] + [UnitOfWork] + public FamilyTreeData UnbindSelfPerson(string treeId) + { + IRepository repositoryPermission = repository.GetRepository>(); + FamilyTreePermissionEntity row = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).FirstOrDefault()!; + if (row == null) throw new ValidatedException("您还未通过审批,不能解除绑定."); + + if (!string.IsNullOrEmpty(row.PersonId)) RebindFamilyTreePermission(row, ""); + + return FindByCurrentPerson(treeId, ""); + } + + private void SaveSpouseMaps(string treeId, string personId, FamilyTreeSpouseMapViewEntity[] maps) { IRepository repositoryMapView = repository.GetRepository>(); IRepository repositoryMap = repository.GetRepository>(); IRepository repositoryPerson = repository.GetRepository>(); FamilyPersonEntity familyPerson; - FamilyTreeSpouseMapViewEntity[] olds = repositoryMapView.Entities.Where(x => x.PersonId == personId).ToArray(); + FamilyTreeSpouseMapViewEntity[] olds = repositoryMapView.Entities.Where(x => x.TreeId == treeId && x.PersonId == personId).ToArray(); if (olds != null && olds.Length > 0) { foreach (FamilyTreeSpouseMapViewEntity old in olds) { - familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == old.PersonId).FirstOrDefault()!; - if (familyPerson != null) + // 仅当新载荷不再保留该配偶时才连带删其人员档案;原写法误用 old.PersonId(父辈本人), + // 会把父辈 family_person 行删掉,导致子女 view 联查丢行、SaveSonMaps 查不到旧行不删,重插同主键报 Duplicate entry + if (maps == null || !maps.Any(m => m.SpouseId == old.SpouseId)) { - repositoryPerson.DeleteNow(familyPerson); + familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == old.SpouseId).FirstOrDefault()!; + if (familyPerson != null) + { + repositoryPerson.DeleteNow(familyPerson); + } } repositoryMap.DeleteNow(new FamilyTreeSpouseMapEntity() @@ -412,22 +527,26 @@ namespace CloudBuilder.Master.Service } - private void SaveSonMaps(string personId, FamilyTreeChildrenMapViewEntity[] maps) + private void SaveSonMaps(string treeId, string personId, FamilyTreeChildrenMapViewEntity[] maps) { IRepository repositoryMapView = repository.GetRepository>(); IRepository repositoryMap = repository.GetRepository>(); IRepository repositoryPerson = repository.GetRepository>(); FamilyPersonEntity familyPerson; - FamilyTreeChildrenMapViewEntity[] olds = repositoryMapView.Entities.Where(x => x.PersonId == personId).ToArray(); + FamilyTreeChildrenMapViewEntity[] olds = repositoryMapView.Entities.Where(x => x.TreeId == treeId && x.PersonId == personId).ToArray(); if (olds != null && olds.Length > 0) { foreach (FamilyTreeChildrenMapViewEntity old in olds) { - familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == old.PersonId).FirstOrDefault()!; - if (familyPerson != null) + // 同 SaveSpouseMaps:只在该子女不再被新载荷保留时才连带删档案,且删的是子女而非父辈 + if (maps == null || !maps.Any(m => m.ChildrenId == old.ChildrenId)) { - repositoryPerson.DeleteNow(familyPerson); + familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == old.ChildrenId).FirstOrDefault()!; + if (familyPerson != null) + { + repositoryPerson.DeleteNow(familyPerson); + } } repositoryMap.DeleteNow(new FamilyTreeChildrenMapEntity() @@ -490,29 +609,42 @@ namespace CloudBuilder.Master.Service Validate(data.FamilyTree); - var has = new FamilyTreePermissionHelper(service).HasPermission(d.TreeId, d.AncestorPersonId); - - if (!has) - { - throw new ValidatedException("您没有修改权限."); - } - - ObjectCopyHelper.Copy(data.FamilyTree, d); - - repository.UpdateNow(d); + var helper = new FamilyTreePermissionHelper(service); var currentPerson = data.CurrentPerson != null ? data.CurrentPerson.FamilyPerson.PersonId : null; if (string.IsNullOrEmpty(currentPerson)) currentPerson = d.AncestorPersonId; + // 谱级口径:管理员才能改谱字段(名/简介/公开开关/挂始祖); + // 成员只准维护以自己当前族员为锚点的家支关系(锚点上溯两代内可能含始祖,HasPermission(始祖) 做不了谱级闸) + var isManager = helper.IsManager(d.TreeId); + if (!isManager) + { + if (!helper.HasPermission(d.TreeId, currentPerson)) + { + throw new ValidatedException("您没有修改权限."); + } + if (!string.IsNullOrEmpty(data.FamilyTree.AncestorPersonId) && data.FamilyTree.AncestorPersonId != d.AncestorPersonId) + { + throw new ValidatedException("您没有挂始祖权限."); + } + } + + if (isManager) + { + ObjectCopyHelper.Copy(data.FamilyTree, d); + + repository.UpdateNow(d); + } + if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0) data.CurrentPerson.FamilyTreeSpouseMaps.ToLookup(x => x.TreeId = d.TreeId); if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0) data.CurrentPerson.FamilyTreeChildrenMaps.ToLookup(x => x.TreeId = d.TreeId); - SaveSpouseMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeSpouseMaps); + SaveSpouseMaps(d.TreeId, data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeSpouseMaps); //SaveParentMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeParentMaps); - SaveSonMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeChildrenMaps); + SaveSonMaps(d.TreeId, data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeChildrenMaps); return FindByCurrentPerson(data.FamilyTree.TreeId, currentPerson); } @@ -525,9 +657,8 @@ namespace CloudBuilder.Master.Service var ent = repository.DetachedEntities.Where(x => x.TreeId == key).FirstOrDefault()!; if (ent == null) return; - var has = new FamilyTreePermissionHelper(service).HasPermission(ent.TreeId, ent.AncestorPersonId); - - if(!has) + // 删谱属谱级操作,仅管理员:成员锚点若贴近始祖,HasPermission 也会为真,不能做闸 + if (!new FamilyTreePermissionHelper(service).IsManager(ent.TreeId)) { throw new ValidatedException("您没有删除权限."); } diff --git a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeUtility/FamilyTreePermissionHelper.cs b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeUtility/FamilyTreePermissionHelper.cs index 3e6339e..9f0a7cc 100644 --- a/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeUtility/FamilyTreePermissionHelper.cs +++ b/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeUtility/FamilyTreePermissionHelper.cs @@ -29,6 +29,49 @@ namespace CloudBuilder.Master.Service.FamilyTreeUtility return GetPersonId(permissions, treeId).Where(x => x == personId).Any(); } + /// + /// 管理员口径:该谱系下此用户存在 ManagerIndc=Y 且已生效的权限行(谱级操作专属)。 + /// 注意与 HasPermission 区分:成员若绑始祖或始祖在其上溯两代内,HasPermission(始祖) 也会为真, + /// 不能拿它当谱级闸,否则这些成员能改谱/删谱 + /// + public bool IsManager(string treeId) + { + IRepository repositoryPermission = service.GetRepository>(); + + return repositoryPermission.DetachedEntities + .Where(x => x.UserName == service.UserId + && x.TreeId == treeId + && x.ManagerIndc == YesNoPolicy.YES + && x.ValidIndc == YesNoPolicy.YES) + .Any(); + } + + /// + /// 成员口径:该谱系下此用户存在已生效权限行(不分管理员/成员档), + /// 供「往谱里新增人」这类无既有 PersonId 可校验的操作做最小闸门 + /// + public bool IsMember(string treeId) + { + IRepository repositoryPermission = service.GetRepository>(); + + return repositoryPermission.DetachedEntities + .Where(x => x.UserName == service.UserId + && x.TreeId == treeId + && x.ValidIndc == YesNoPolicy.YES) + .Any(); + } + + /// + /// 当前用户在本谱可编的族员集合(家支主线,口径与 HasPermission 完全同源); + /// 供详情接口下发给前端做「逐个目标人」的编辑/删除钮判定(锚点级 IsCanAmend 盖不住关系行里的旁支) + /// + public List GetAmendablePersonIds(string treeId) + { + IRepository repositoryPermission = service.GetRepository>(); + FamilyTreePermissionEntity[] permissions = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).ToArray(); + return GetPersonId(permissions, treeId); + } + public List GetPersonId(FamilyTreePermissionEntity[] permissions, string treeId) { List ps = new List(); @@ -41,7 +84,7 @@ namespace CloudBuilder.Master.Service.FamilyTreeUtility IRepository repositorySpouseMap = service.GetRepository>(); - if (childrens == null || childrens.Length == 0) + if (spouses == null || spouses.Length == 0) spouses = repositorySpouseMap.DetachedEntities.Where(x => x.TreeId == treeId).ToArray(); foreach (var permission in permissions) @@ -63,12 +106,16 @@ namespace CloudBuilder.Master.Service.FamilyTreeUtility foreach (FamilyTreeChildrenMapEntity child in childrens.Where(x => x.PersonId == personId)) PackPersonIdByChildrenMaps(childrens, spouses, child.ChildrenId, ps); } - //父亲ID也有权限 - if (childrens != null && childrens.Where(x => x.ChildrenId == personId).Any()) + //向上延伸:最多两代(父、祖),含每一代配偶(家支口径;不含兄弟姐妹等旁支) + string cursor = personId; + for (int up = 0; up < 2 && childrens != null; up++) { - var p = childrens.Where(x => x.ChildrenId == personId).Select(x => x.PersonId).FirstOrDefault(); - if (!ps.Contains(p)) - ps.Add(p); + string father = childrens.Where(x => x.ChildrenId == cursor).Select(x => x.PersonId).FirstOrDefault(); + // ps 去重兼做环保护:父链成环或已收过即停 + if (string.IsNullOrEmpty(father) || ps.Contains(father)) break; + ps.Add(father); + PackPersonIdBySpouseMaps(spouses, father, ps); + cursor = father; } //配偶都有权限 PackPersonIdBySpouseMaps(spouses, personId, ps); diff --git a/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeData.cs b/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeData.cs index 4a914a6..2ab3bb9 100644 --- a/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeData.cs +++ b/CloudBuilder/CloudBuilder.Master/Data/FamilyTreeData.cs @@ -17,8 +17,17 @@ namespace CloudBuilder.Master.Data public FamilyPersonData? CurrentPerson { get; set; } public bool IsManager { get; set; } = false; + /// 已认证成员:存在绑定到自己且已生效(ValidIndc=YES)的权限行,前端据此控制成员/权限选项卡可见性 + public bool IsMember { get; set; } = false; + public bool IsCanAmend { get; set; } = false; + /// 自己已生效权限行绑定的族员 PersonId(空串=已过审但未绑定),前端据此切换「绑定账号/解除绑定」按钮 + public string SelfBindPersonId { get; set; } = ""; + + /// 当前用户在本谱可编的族员 PersonId 集合(家支主线,与 HasPermission 同源);管理员不限量不下发,前端按 IsManager 短路 + public string[] CanAmendPersonIds { get; set; } = []; + public FamilyTreePermissionUserViewEntity[]? FamilyTreePermissions { get; set; } } } diff --git a/CloudBuilder/CloudBuilder.Master/Entity/CloudBuilderMasterRegistEntity.auto.cs b/CloudBuilder/CloudBuilder.Master/Entity/CloudBuilderMasterRegistEntity.auto.cs index 8d0b871..694623c 100644 --- a/CloudBuilder/CloudBuilder.Master/Entity/CloudBuilderMasterRegistEntity.auto.cs +++ b/CloudBuilder/CloudBuilder.Master/Entity/CloudBuilderMasterRegistEntity.auto.cs @@ -113,6 +113,7 @@ builder.Entity().HasKey(c => new {c.PersonId,c.TreeI builder.Entity(eb => { eb.HasNoKey(); }); + //***END*** } } diff --git a/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeEntity.auto.cs b/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeEntity.auto.cs index 33aec8f..44b34b1 100644 --- a/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeEntity.auto.cs +++ b/CloudBuilder/CloudBuilder.Master/Entity/FamilyTreeEntity.auto.cs @@ -30,9 +30,9 @@ namespace CloudBuilder.Master.Entity [Column("brief")] public string? Brief { get; set; } [Column("ancestor_person_id")] - public string AncestorPersonId { get; set; } + public string? AncestorPersonId { get; set; } [Column("tree_cover_filename")] - public string TreeCoverFilename { get; set; } + public string? TreeCoverFilename { get; set; } [Column("public_indc")] public string PublicIndc { get; set; } diff --git a/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs b/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs index 67b448e..c3a4d43 100644 --- a/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs +++ b/CloudBuilder/CloudBuilder.Master/Service/IFamilyTreeService.auto.cs @@ -18,6 +18,8 @@ namespace CloudBuilder.Master.Service void RequestFamilyTreePermission(string treeId, string currentPermission); void ApproveFamilyTreePermission(string treeId, string currentPersonId, string userName, string managerIndc); void RejectFamilyTreePermission(string treeId, string currentPersonId, string userName); + FamilyTreeData BindSelfPerson(string treeId, string personId); + FamilyTreeData UnbindSelfPerson(string treeId); } } diff --git a/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs b/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs index f15dabc..bda491b 100644 --- a/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs +++ b/CloudBuilder/CloudBuilder.Request/Master/FamilyTreeRequest.auto.cs @@ -82,6 +82,20 @@ var compositionData = new ApproveFamilyTreePermissionCompositionData4{ treeId = { var compositionData = new RejectFamilyTreePermissionCompositionData3{ treeId = treeId, currentPersonId = currentPersonId, userName = userName }; Task.Run(() => serviceRequest.PostAsJsonAsync("/FamilyTree/RejectFamilyTreePermission", compositionData)).GetAwaiter().GetResult(); } + + public class BindSelfPersonCompositionData2 { + public string treeId { get; set; } + public string personId { get; set; } + + } + public FamilyTreeData BindSelfPerson(string treeId, string personId) + { +var compositionData = new BindSelfPersonCompositionData2{ treeId = treeId, personId = personId }; return Task.Run(() => serviceRequest.PostAsJsonAsync("/FamilyTree/BindSelfPerson", compositionData)).GetAwaiter().GetResult(); } + + public FamilyTreeData UnbindSelfPerson(string treeId) + { +return Task.Run(() => serviceRequest.PostAsJsonAsync("/FamilyTree/UnbindSelfPerson?treeId=" + treeId.ToString())).GetAwaiter().GetResult(); } + public FamilyTreeData Update(FamilyTreeData data) { return Task.Run(() => serviceRequest.PostAsJsonAsync("/FamilyTree/Update" , data)).GetAwaiter().GetResult(); } diff --git a/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs b/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs index 255031c..faf22de 100644 --- a/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs +++ b/CloudBuilder/CloudBuilder.Web.Entry/Controllers/Master/FamilyTreeController.auto.cs @@ -221,6 +221,55 @@ this.familyTreeService= familyTreeService; } } + + public class BindSelfPersonCompositionData + { + public string treeId { get; set; } + public string personId { get; set; } + + } + [PermissionOperate(name: "FamilyTree", operate: "Find")] + [HttpPost] + [UnitOfWork] + public dynamic BindSelfPerson(BindSelfPersonCompositionData compositionData) + { + try + { + + var result = familyTreeService.BindSelfPerson(compositionData.treeId,compositionData.personId); + return result; + } + catch (ValidatedException vex) + { + return vex; + } + catch (Exception ex) + { + return new ServiceErrorException(ex); + } + } + + [PermissionOperate(name: "FamilyTree", operate: "Find")] + [HttpPost] + [UnitOfWork] + public dynamic UnbindSelfPerson(string treeId) + { + try + { + + var result = familyTreeService.UnbindSelfPerson(treeId); + return result; + } + catch (ValidatedException vex) + { + return vex; + } + catch (Exception ex) + { + return new ServiceErrorException(ex); + } + } + [PermissionOperate(name: "FamilyTree", operate: "Update")] [HttpPost] [UnitOfWork] diff --git a/uniapp/youjiala/src/components/admin-pick-popup/admin-pick-popup.vue b/uniapp/youjiala/src/components/admin-pick-popup/admin-pick-popup.vue index 44b725c..6c35e87 100644 --- a/uniapp/youjiala/src/components/admin-pick-popup/admin-pick-popup.vue +++ b/uniapp/youjiala/src/components/admin-pick-popup/admin-pick-popup.vue @@ -136,9 +136,15 @@ function onSearch(): void { function toggleSelect(row: AdminRow): void { const key = keyOf(row); if (!props.multiple) { - // 单选:直接确认 - emit("confirm", [row]); - closePopup(); + // 单选:点行高亮选中(再点同一条取消),待「确定」回传——与多选同构,取消/确定栏常驻 + if (selectedKeys.value[0] === key) { + selectedKeys.value = []; + selectedPool.value = {}; + } + else { + selectedKeys.value = [key]; + selectedPool.value = { [key]: row }; + } return; } const idx = selectedKeys.value.indexOf(key); @@ -249,18 +255,21 @@ watch( - - - - + + + + {{ selectedKeys.length >= rows.length && rows.length > 0 ? t("AdminPage.unselectAllBut") : t("AdminPage.selectAllBut") }} @@ -379,6 +388,10 @@ $brand-green-light: #47a678; border-color: $brand-green; } +.pick-popup__check--radio { + border-radius: 50%; +} + .pick-popup__content { flex: 1; min-width: 0; 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 ceab2db..bd80f88 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 @@ -10,15 +10,23 @@ */ import type { LoadedTreeNode } from "@/components/family-tree-chart/types"; import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData"; +import type { AdminRow } from "@/admin/admin-types"; import { useLocale } from "uview-pro"; import { computed, onMounted, ref } from "vue"; -import { useAdminPick, useThemeGradient, useThemeTokens } from "@/composables"; +import { useThemeGradient, useThemeTokens } from "@/composables"; +import { usePersonPickConfig } from "@/composables/usePickerConfigs"; import { usePolicyOptions } from "@/composables/usePolicyOptions"; +import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity"; +import { FamilyPersonSearchCriteria } from "@/data/auto/familyPersonSearchCriteria"; +import { SearchFilter } from "@/data/auto/searchFilter"; +import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy"; +import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy"; import { FamilyTreeChildrenMapViewEntity } from "@/data/auto/familyTreeChildrenMapViewEntity"; import { FamilyTreeData } from "@/data/auto/familyTreeData"; import { FamilyTreeSearchCriteria } from "@/data/auto/familyTreeSearchCriteria"; import { FamilyTreeSpouseMapViewEntity } from "@/data/auto/familyTreeSpouseMapViewEntity"; import { FamilyTreeViewEntity } from "@/data/auto/familyTreeViewEntity"; +import familyPersonService from "@/service/master/familyPersonService"; import familyTreeService from "@/service/master/familyTreeService"; /** ready:组件挂载完成通知(时序口径见文件底部 onMounted 注释) */ @@ -118,6 +126,15 @@ const currentPersonName = computed(() => { const p = currentPerson.value; return p ? `${p.Surname ?? ""}${p.GenealogyName ?? ""}` : ""; }); +/** 当前族员是否就是自己权限行绑定的族员(切换「绑定账号/解除绑定」按钮) */ +const isSelfBound = computed(() => { + const pid = currentPerson.value?.PersonId ?? ""; + return !!pid && pid === (data.value.SelfBindPersonId ?? ""); +}); +/** 逐个目标人的可编判定:管理员全放行;成员按后端下发的家支集合(锚点级 IsCanAmend 盖不住关系行里的旁支,如兄弟) */ +function canAmendPerson(personId: string): boolean { + return !!data.value.IsManager || (data.value.CanAmendPersonIds ?? []).includes(personId); +} const spouseMaps = computed(() => data.value.CurrentPerson?.FamilyTreeSpouseMaps ?? []); const childMaps = computed(() => data.value.CurrentPerson?.FamilyTreeChildrenMaps ?? []); @@ -148,6 +165,25 @@ function personGender(personId?: string, fallback?: string): string { return persons.find(p => p?.PersonId === personId)?.Gender || fallback || ""; } +/** 关系对方常用名回查(成员列表行副信息用):无档案或无常用名回空串 */ +function personCommonName(personId?: string): string { + if (!personId) { + return ""; + } + const persons = (data.value.CurrentPerson?.FamilyTreePersons ?? []) as any[]; + return String(persons.find(p => p?.PersonId === personId)?.CommonName ?? ""); +} + +/** person-hero 芯片文案:优先常用名,未填回退成员编号(列表简化口径) */ +function personChipText(person?: { CommonName?: string; PersonId?: string } | null): string { + if (!person) { + return ""; + } + return person.CommonName + ? `${t("FamilyPersonPage.commonName")}: ${person.CommonName}` + : `${t("FamilyPersonPage.personId")}: ${person.PersonId ?? ""}`; +} + /** 谱系图同款性别配色:男蓝 / 女粉 */ function genderTheme(gender?: string): string { return genderIsMale(gender) ? "#1890ff" : "#eb2f96"; @@ -172,16 +208,23 @@ const fullChartVisible = ref(false); /** 快速建员弹层:三种用途共用一个组件实例(对齐旧 quickCreatePurpose) */ const qcVisible = ref(false); -const qcPurpose = ref<"spouse" | "child" | "ancestor">("spouse"); +const qcPurpose = ref<"spouse" | "child" | "ancestor" | "member">("spouse"); const qcTitle = computed(() => { if (qcPurpose.value === "spouse") { return t("FamilyTreePage.addSpouse"); } - return qcPurpose.value === "child" - ? t("FamilyTreePage.addChild") - : t("FamilyTreePage.addAncestor"); + if (qcPurpose.value === "child") { + return t("FamilyTreePage.addChild"); + } + return qcPurpose.value === "ancestor" + ? t("FamilyTreePage.addAncestor") + : t("FamilyTreePage.addMember"); }); +/** 成员信息修改弹层:与新增共用 person-editor,仅 mode="edit" 并带上要载入的 personId */ +const peEditVisible = ref(false); +const peEditPersonId = ref(""); + /** 关系行的就地展开(编辑表单可见性),key 用配偶/孩子 PersonId */ const expandedSpouses = ref>({}); const expandedChildren = ref>({}); @@ -209,6 +252,171 @@ const managerPermissions = computed(() => (data.value.FamilyTreePermissions ?? []).filter(item => item.ValidIndc === "Y"), ); +// ===== 详情态重设计:三段式 Tab + 底部操作条 + 弹窗(仅浏览态;编辑态沿用旧内联布局) ===== +/** 详情 tab:概览 / 成员 / 权限 */ +type DetailTab = "overview" | "members" | "perms"; +const activeTab = ref("overview"); + +/** 成员搜索关键字 + 性别筛选(支系无后端字段,暂不提供该 chip) */ +type MemberFilter = "all" | "male" | "female"; +const memberKeyword = ref(""); +const memberFilter = ref("all"); + +/** 族人档案弹层已取消(始祖卡单击不弹窗),相关状态/函数/样式/词条已同步清理 */ + +/** 邀请族人弹层已随底部分享入口取消(无后端接口占位),相关函数/样式/词条已同步清理,待邀请 service 就绪再重建 */ + +/** 编辑族谱信息弹层:只含 名称/始祖谱名/简介/对外公开 四字段,关系与成员在其他入口分开维护 */ +const treeEditVisible = ref(false); +/** 打开弹层时的四字段快照:取消时本地还原直改,免去重载带来的“处理中” */ +const treeEditSnapshot = ref({ TreeName: "", AncestorPersonGenealogyName: "", Brief: "", PublicIndc: "N" }); + +/** hero 统计「位族人」:全谱总人数(FindByCurrentPerson 只下发直系小集合,切当前族员会跳变,改走搜索链 Total 全量口径) */ +const treePersonCount = ref(0); +const memberCount = computed( + () => + treePersonCount.value || + (data.value.CurrentPerson?.FamilyTreePersons ?? []).length, +); + +/** 静默拉取本谱全量族人数:limit=1 只取 Total,不占 loading;失败回 0 走直系集合兜底 */ +async function fetchPersonCount(): Promise { + const key = String(treeId.value ?? "").trim(); + if (!key) { + treePersonCount.value = 0; + return; + } + try { + const criteria = new FamilyPersonSearchCriteria(); + criteria.GridColumns = []; + criteria.page = 1; + criteria.limit = 1; + const filter = new SearchFilter(); + filter.Column = FamilyPersonEntity.DB_TREE_ID; + filter.Type = SearchFilterTypePolicy.TEXT; + filter.Operator = SearchFilterOperatorPolicy.EQUAL_TO; + filter.Value = key; + criteria.Filter = [filter]; + const result = await familyPersonService.FindBySearchCriteria(criteria); + treePersonCount.value = result?.Total ?? 0; + } + catch { + treePersonCount.value = 0; + } +} + +/** 弹层里添加配偶/子女入口保留在关系卡卡头(行内直达动作),不再设档案弹层 */ + +/** 始祖档案(概览始祖信息卡):按 AncestorPersonId 回查,无则回 null */ +const ancestorPerson = computed(() => { + const id = tree.value.AncestorPersonId; + if (!id) { + return null; + } + const persons = (data.value.CurrentPerson?.FamilyTreePersons ?? []) as any[]; + return persons.find(p => p?.PersonId === id) ?? null; +}); + +/** 成员页搜索/筛选后的配偶、孩子可见行(命中判定走 matchMember) */ +const visibleSpouses = computed(() => spouseMaps.value.filter( + item => matchMember( + `${item.SpouseSurname ?? ""}${item.SpouseGenealogyName ?? ""}`, + personGender(item.SpouseId, item.Gender), + item.SpouseId ?? "", + ), +)); +const visibleChildren = computed(() => childMaps.value.filter( + item => matchMember( + `${item.ChildrenSurname ?? ""}${item.ChildrenGenealogyName ?? ""}`, + personGender(item.ChildrenId, item.Gender), + item.ChildrenId ?? "", + ), +)); + +function switchTab(tab: DetailTab): void { + activeTab.value = tab; +} + +/** 成员行是否命中当前搜索 + 性别筛选(name 传姓+谱名连写,personId 供编号检索) */ +function matchMember(name: string, gender: string, personId: string): boolean { + if (memberFilter.value === "male" && !genderIsMale(gender)) { + return false; + } + if (memberFilter.value === "female" && genderIsMale(gender)) { + return false; + } + const kw = memberKeyword.value.trim().toLowerCase(); + if (!kw) { + return true; + } + return `${name}`.toLowerCase().includes(kw) || `${personId}`.toLowerCase().includes(kw); +} + +/** 快捷区「添加族人」入口已取消(建员走关系卡头添加配偶/子女);openAddMember 随之删除 */ + +/** 复制文本(族谱编号 / 邀请链接) */ +function copyText(value: string): void { + if (!value) { + return; + } + uni.setClipboardData({ data: value }); +} + +/** 浏览态直接切换对外公开:改动即落库(旧内联编辑靠保存钮,此处无保存条故即时 Update) */ +async function onPublicToggle(value: boolean): Promise { + if (!data.value.IsCanAmend) { + toast(t("FamilyTreePage.noAmendTip")); + return; + } + tree.value.PublicIndc = value ? "Y" : "N"; + sanitizeTreePayload(); + await withBusy(async () => { + await familyTreeService.Update(data.value); + toast(t("AdminPage.savedTip")); + }); +} + +/** 打开「编辑族谱信息」弹层:浏览态四字段快改入口(无修改权限挡回),先记快照供取消还原 */ +function openTreeEditor(): void { + if (!isCreate.value && !data.value.IsCanAmend) { + toast(t("FamilyTreePage.noAmendTip")); + return; + } + treeEditSnapshot.value = { + TreeName: tree.value.TreeName ?? "", + AncestorPersonGenealogyName: tree.value.AncestorPersonGenealogyName ?? "", + Brief: tree.value.Brief ?? "", + PublicIndc: tree.value.PublicIndc ?? "N", + }; + treeEditVisible.value = true; +} + +/** 弹层保存:先做与 onSave 同款校验(失败保持弹层可继续改),通过则收起并复用 onSave 落库 */ +async function saveTreeEdit(): Promise { + if (!String(tree.value.TreeName ?? "").trim()) { + toast(t("FamilyTreePage.treeNameRequired")); + return; + } + if (isCreate.value && !String(tree.value.AncestorPersonId ?? "").trim()) { + toast(t("FamilyTreePage.ancestorRequired")); + return; + } + treeEditVisible.value = false; + await onSave(); +} + +/** 弹层取消:收起并按快照还原弹层里对 tree 的直改(纯本地,不发请求不转 loading) */ +function cancelTreeEdit(): void { + treeEditVisible.value = false; + const snap = treeEditSnapshot.value; + tree.value.TreeName = snap.TreeName; + tree.value.AncestorPersonGenealogyName = snap.AncestorPersonGenealogyName; + tree.value.Brief = snap.Brief; + tree.value.PublicIndc = snap.PublicIndc; +} + +/** 发送邀请入口已随邀请弹层取消 */ + /** 详情区分区卡折叠态(与媒体/共享页同款交互):默认全展开,点标题行切换 */ type FoldKey = "info" | "person" | "spouses" | "children" | "perms"; const folded = ref>({ @@ -275,8 +483,35 @@ function ensureLists(): void { } } +/** 保存载荷清洗:后端 FindByCurrentPerson 的关系行只按 PersonId 过滤不带 TreeId, + * 跨谱共享同一父/子时会混入他谱关系行,Update 把 TreeId 归一后插库撞主键 + * (Duplicate entry ... family_tree_children_map.PRIMARY);此处按当前谱过滤并按主键去重 */ +function sanitizeTreePayload(): void { + const cp = data.value.CurrentPerson; + if (!cp) { + return; + } + const keep = (rows: T[] | undefined, keyOf: (row: T) => string): T[] => { + const seen = new Set(); + return (rows ?? []).filter((row) => { + if (row.TreeId && treeId.value && row.TreeId !== treeId.value) { + return false; + } + const key = keyOf(row); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + }; + cp.FamilyTreeSpouseMaps = keep(cp.FamilyTreeSpouseMaps, row => `${row.PersonId}|${row.SpouseId}`); + cp.FamilyTreeChildrenMaps = keep(cp.FamilyTreeChildrenMaps, row => `${row.PersonId}|${row.ChildrenId}`); +} + function resetDetail(): void { data.value = new FamilyTreeData(); + treePersonCount.value = 0; ensureLists(); treeId.value = ""; loaded.value = false; @@ -285,6 +520,11 @@ function resetDetail(): void { loadedNodes.value = {}; expandedSpouses.value = {}; expandedChildren.value = {}; + // 浏览态重设计相关状态一并复位 + activeTab.value = "overview"; + memberKeyword.value = ""; + memberFilter.value = "all"; + treeEditVisible.value = false; } /** 载入详情:FindByCurrentPerson({ key, personId }),首载 personId 传空串(后端回默认族员) */ @@ -301,11 +541,20 @@ async function load(): Promise { data.value = result; ensureLists(); loaded.value = true; + // 全量族人数异步补拉(不阻塞详情渲染) + void fetchPersonCount(); // 无修改权限:强制回到查看模式(旧 onSearch 同款) if (!data.value.IsCanAmend && isEdit.value && !isCreate.value) { isEdit.value = false; toast(t("FamilyTreePage.noAmendTip")); } + // tab 级控权兜底:权限被回收/切换后仍停在不可见 tab 时踢回概览 + if (activeTab.value === "perms" && !data.value.IsManager) { + activeTab.value = "overview"; + } + else if (activeTab.value === "members" && !data.value.IsManager && !data.value.IsMember) { + activeTab.value = "overview"; + } } async function onOpenTree(row: FamilyTreeViewEntity): Promise { @@ -341,14 +590,6 @@ async function exitDetail(): Promise { await loadTrees(); } -function onAmend(): void { - if (!data.value.IsCanAmend) { - toast(t("FamilyTreePage.noAmendTip")); - return; - } - isEdit.value = true; -} - async function onCancelEdit(): Promise { if (isCreate.value || !loaded.value) { await exitDetail(); @@ -364,7 +605,13 @@ async function onSave(): Promise { toast(t("FamilyTreePage.treeNameRequired")); return; } + // 新建族谱必须先指定始祖(祖先):未新增则挡回(编辑既有族谱不重复校验) + if (isCreate.value && !String(tree.value.AncestorPersonId ?? "").trim()) { + toast(t("FamilyTreePage.ancestorRequired")); + return; + } ensureLists(); + sanitizeTreePayload(); await withBusy(async () => { const isUpdate = String(tree.value.CreatedBy ?? "").trim() !== ""; const result = await (isUpdate @@ -378,6 +625,8 @@ async function onSave(): Promise { isEdit.value = false; isCreate.value = false; loaded.value = true; + // 新建/保存可能增减族人(如建谱带始祖),重拉全量人数 + void fetchPersonCount(); toast(t("AdminPage.savedTip")); }); } @@ -404,15 +653,35 @@ function onPublicChange(value: boolean): void { } // ===== 当前族员切换(族员搜索 / 图上点人)===== -const personPicker = useAdminPick("person"); +// 族员搜索统一走 admin-pick-popup 底部弹窗:取消/确定与其他弹层同风格,不再跳整页 enquiry +const personPickConfig = usePersonPickConfig(); +const personPickVisible = ref(false); +/** 弹窗用途:current=切换当前族员,spouse=从已有成员里选配偶 */ +const personPickMode = ref<"current" | "spouse">("current"); function pickCurrentPerson(): void { - personPicker.open({ page: "familyPersonEnquiry" }, (rows) => { - const personId = String(rows[0]?.PersonId ?? ""); - if (personId) { - setCurrentPerson(personId); - } - }); + personPickMode.value = "current"; + personPickVisible.value = true; +} + +/** 弹窗确定/点行回传:按用途分流——切当前族员或挂配偶(pushSpouse 内含重复挡回) */ +function onPersonPicked(rows: AdminRow[]): void { + const row = rows[0]; + const personId = String(row?.PersonId ?? ""); + if (!personId) { + return; + } + if (personPickMode.value === "current") { + void setCurrentPerson(personId); + return; + } + pushSpouse({ + PersonId: personId, + Surname: String(row?.Surname ?? ""), + GenealogyName: String(row?.GenealogyName ?? ""), + CommonName: String(row?.CommonName ?? ""), + Gender: String(row?.Gender ?? ""), + } as FamilyPersonDisplayData); } async function setCurrentPerson(personId: string): Promise { @@ -467,6 +736,11 @@ function refreshParentOrderby(): void { function pushSpouse(person: FamilyPersonDisplayData): void { ensureLists(); + // 同一人重复挂配偶会撞 spouse_map 主键,挡回(快速建员/已有成员选入两个入口共用) + if (spouseMaps.value.some(item => item.SpouseId === person.PersonId)) { + toast(t("FamilyTreePage.dupMemberTip")); + return; + } const spouse = new FamilyTreeSpouseMapViewEntity(); spouse.TreeId = treeId.value; spouse.PersonId = currentPerson.value?.PersonId ?? ""; @@ -506,6 +780,11 @@ function refreshSonOrderby(): void { function pushChild(person: FamilyPersonDisplayData): void { ensureLists(); + // 同上:同一子女重复挂行会在保存时撞 children_map 主键 + if (childMaps.value.some(item => item.ChildrenId === person.PersonId)) { + toast(t("FamilyTreePage.dupMemberTip")); + return; + } const child = new FamilyTreeChildrenMapViewEntity(); child.TreeId = treeId.value; child.PersonId = currentPerson.value?.PersonId ?? ""; @@ -537,7 +816,7 @@ function moveChild(index: number, offset: number): void { } // ===== 快速建员回填(对齐旧 onPersonQuickCreated)===== -function onQuickCreated(person: FamilyPersonDisplayData): void { +async function onQuickCreated(person: FamilyPersonDisplayData): Promise { if (!person || !person.PersonId) { return; } @@ -547,34 +826,55 @@ function onQuickCreated(person: FamilyPersonDisplayData): void { else if (qcPurpose.value === "child") { pushChild(person); } - else { + else if (qcPurpose.value === "ancestor") { tree.value.AncestorPersonId = person.PersonId; tree.value.AncestorPersonGenealogyName = person.GenealogyName; } + else if (!isCreate.value && String(treeId.value ?? "").trim()) { + // 通用「添加族人」:person-editor 已落库,刷新详情让新族人进入列表 + await withBusy(() => load()); + } } function openQuickCreate(purpose: "spouse" | "child" | "ancestor"): void { + // 前置闸门:不可编(未绑定/当前锚点出支系)直接提示不弹层,避免弹完才报无权限、也防弹层里的非公开族人信息泄漏 + if (!data.value.IsCanAmend) { + toast(t("FamilyTreePage.noAmendTip")); + return; + } qcPurpose.value = purpose; qcVisible.value = true; } -// 已有成员里选配偶(快速建员之外的入口,对齐旧页) -const spousePicker = useAdminPick("spouse"); +/** 打开成员修改弹层:夫妻 / 孩子行与当前族员卡的「修改」图标都走这里 */ +function openEditPerson(personId: string): void { + if (!personId) { + return; + } + // 前置拦闸:按目标人逐个判(非按锚点),不可编直接提示不弹层,防弹完才报错与非公开信息泄漏 + if (!canAmendPerson(personId)) { + toast(t("FamilyTreePage.noAmendTip")); + return; + } + peEditPersonId.value = personId; + peEditVisible.value = true; +} +/** 修改保存后刷新:改的是本谱祖先就先回填去冗余的祖先谱名(新建/编辑两态都适用),已落库的族谱再重载详情 */ +async function onPersonEdited(person?: FamilyPersonDisplayData): Promise { + if (person && tree.value.AncestorPersonId === person.PersonId) { + tree.value.AncestorPersonGenealogyName = person.GenealogyName; + } + // 新建态族谱尚未落库(treeId 为空),load 取不到,跳过重载 + if (!isCreate.value && String(treeId.value ?? "").trim()) { + await withBusy(() => load()); + } +} + +// 已有成员里选配偶(快速建员之外的入口,对齐旧页):与族员搜索共用同一个底部弹窗 function pickSpouse(): void { - spousePicker.open({ page: "familyPersonEnquiry" }, (rows) => { - const row = rows[0]; - if (!row?.PersonId) { - return; - } - pushSpouse({ - PersonId: String(row.PersonId), - Surname: String(row.Surname ?? ""), - GenealogyName: String(row.GenealogyName ?? ""), - CommonName: String(row.CommonName ?? ""), - Gender: String(row.Gender ?? ""), - } as FamilyPersonDisplayData); - }); + personPickMode.value = "spouse"; + personPickVisible.value = true; } // ===== 孩子行的枚举编辑器 ===== @@ -583,11 +883,15 @@ function openRelationPicker(record: FamilyTreeChildrenMapViewEntity): void { relationPickerVisible.value = true; } -function onRelationConfirm(key: string): void { +async function onRelationConfirm(key: string): Promise { if (relationTarget.value) { relationTarget.value.RelationType = key; } relationPickerVisible.value = false; + // 浏览态(旧「管理关系」已取消):弹层里改完即落库;编辑态(新建谱)仍走「保存」钮 + if (!isEdit.value) { + await onSave(); + } } function openParentPicker(record: FamilyTreeChildrenMapViewEntity): void { @@ -595,15 +899,79 @@ function openParentPicker(record: FamilyTreeChildrenMapViewEntity): void { parentPickerVisible.value = true; } -function onParentConfirm(key: string): void { +async function onParentConfirm(key: string): Promise { if (parentTarget.value) { parentTarget.value.SpouseId = key; } parentPickerVisible.value = false; + if (!isEdit.value) { + await onSave(); + } +} + +// ===== 关系行快捷动作(配偶/子女行右侧直达,单击即落库,旧编辑态式交互)===== +type RelationKind = "spouse" | "child"; + +function relationIdOf(kind: RelationKind, record: any): string { + return String(kind === "spouse" ? record?.SpouseId : record?.ChildrenId); +} + +function relationNameOf(kind: RelationKind, record: any): string { + return kind === "spouse" + ? `${record?.SpouseSurname ?? ""}${record?.SpouseGenealogyName ?? ""}` + : `${record?.ChildrenSurname ?? ""}${record?.ChildrenGenealogyName ?? ""}`; +} + +/** 上移/下移关系行(配偶序/长幼序,落库到 Orderby);下标按全量列表回查,搜索筛选态下也不会错位 */ +async function onRelationMove(kind: RelationKind, record: any, offset: number): Promise { + if (!data.value.IsCanAmend) { + return; + } + const id = relationIdOf(kind, record); + const list = kind === "spouse" ? spouseMaps.value : childMaps.value; + const index = list.findIndex(item => relationIdOf(kind, item) === id); + if (index < 0 || index + offset < 0 || index + offset >= list.length) { + return; + } + if (kind === "spouse") { + moveSpouse(index, offset); + } + else { + moveChild(index, offset); + } + await onSave(); +} + +/** 解除关系:二次确认后删关系行并立即保存(只删关系行,不动人员档案) */ +async function onRelationRemove(kind: RelationKind, record: any): Promise { + if (!data.value.IsCanAmend) { + toast(t("FamilyTreePage.noAmendTip")); + return; + } + const id = relationIdOf(kind, record); + const ok = await confirmDialog( + t("FamilyTreePage.removeRelation"), + t("FamilyTreePage.removeRelationConfirm", { name: relationNameOf(kind, record) || id }), + ); + if (!ok) { + return; + } + if (kind === "spouse") { + deleteSpouse(id); + } + else { + deleteChild(id); + } + await onSave(); } // ===== 权限维护 ===== async function requestManager(): Promise { + // 入口常驻(快捷区/权限卡):已是管理员就不再发请求,直接提示 + if (data.value.IsManager) { + toast(t("FamilyTreePage.alreadyManagerTip")); + return; + } const personId = currentPerson.value?.PersonId ?? ""; if (!treeId.value || !personId) { return; @@ -616,6 +984,35 @@ async function requestManager(): Promise { load(); } +/** 绑定账号:把当前族员绑到自己已生效的权限行(后端校验同名,不符会直接报错提示) */ +async function bindCurrentPerson(): Promise { + const personId = String(currentPerson.value?.PersonId ?? "").trim(); + if (!treeId.value || !personId) { + toast(t("FamilyTreePage.bindNoPersonTip")); + return; + } + const result = await familyTreeService.BindSelfPerson({ treeId: treeId.value.trim(), personId }); + if (result?.FamilyTree) { + // 后端已把锚点切到刚绑族员,直接落数据;重拉全量人数与可编口径 + data.value = result; + ensureLists(); + } + toast(t("FamilyTreePage.bindSuccessTip")); +} + +/** 解除绑定:清空自己权限行的族员绑定(后端把锚点退回始祖) */ +async function unbindCurrentPerson(): Promise { + if (!treeId.value) { + return; + } + const result = await familyTreeService.UnbindSelfPerson(treeId.value.trim()); + if (result?.FamilyTree) { + data.value = result; + ensureLists(); + } + toast(t("FamilyTreePage.unbindSuccessTip")); +} + async function approvePermission( record: { PersonId: string; UserName: string }, managerIndc: string, @@ -746,7 +1143,246 @@ defineExpose({ - + + + + + @@ -770,15 +1406,6 @@ defineExpose({ - - - - - - - - - @@ -827,9 +1454,15 @@ defineExpose({ {{ t("FamilyTreePage.ancestor") }} {{ tree.AncestorPersonGenealogyName || "-" }} - + + + {{ t("FamilyTreePage.addAncestor") }} + + {{ t("FamilyTreePage.editAncestor") }} + @@ -865,20 +1498,24 @@ defineExpose({ + + + {{ t("FamilyPersonPage.editBut") }} + {{ t("FamilyTreePage.personSearch") }} - + {{ t("FamilyTreePage.pedigreeChart") }} - + {{ t("FamilyTreePage.ancestryChart") }} - + {{ t("FamilyTreePage.applyManage") }} @@ -934,6 +1571,8 @@ defineExpose({ +