ow
This commit is contained in:
1 parent
479762b57e
commit
27284040cc
30 files changed
+3042
-428
No files matched your search
@@ -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<DmsFolderEntity> repositoryFolder = repository.GetRepository<IRepository<DmsFolderEntity>>();
|
||||
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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<FamilyTreeSpouseMapViewEntity> repositorySpouseMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>();
|
||||
|
||||
data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>().DetachedEntities.Where(x => x.PersonId == currentPerson).OrderBy(x => x.Orderby).ToArray();
|
||||
data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>().DetachedEntities.Where(x => x.PersonId == currentPerson && x.TreeId == d.TreeId).OrderBy(x => x.Orderby).ToArray();
|
||||
|
||||
//data.CurrentPerson.FamilyTreeParentMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.ChildrenId == key).OrderBy(x => x.Orderby).ToArray();
|
||||
data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.PersonId == currentPerson).OrderBy(x => x.Orderby).ToArray();
|
||||
data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.PersonId == currentPerson && x.TreeId == d.TreeId).OrderBy(x => x.Orderby).ToArray();
|
||||
|
||||
// 补充当前人员及其配偶、子女的人员信息(含头像地址),供谱系图卡片显示头像
|
||||
List<string> ps = new List<string>();
|
||||
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<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PersonId 属复合主键 (PersonId,TreeId,UserName),EF 分离实体直接改主键列更新不可靠,换绑一律删旧行插新行
|
||||
/// </summary>
|
||||
private void RebindFamilyTreePermission(FamilyTreePermissionEntity row, string personId)
|
||||
{
|
||||
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户中文姓名(security_user.user_local_name);登录账号只是标识,不参与同名比较
|
||||
/// </summary>
|
||||
private string GetUserLocalName(string userName)
|
||||
{
|
||||
IRepository<SecurityUserEntity> repositoryUser = repository.GetRepository<IRepository<SecurityUserEntity>>();
|
||||
return repositoryUser.DetachedEntities.Where(x => x.UserName == userName).FirstOrDefault()?.UserLocalName ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同名口径:族员「姓+谱名」组合或常用名与用户中文姓名相同
|
||||
/// </summary>
|
||||
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<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
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<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
|
||||
// 姓+谱名是运行时拼接,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<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
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)
|
||||
/// <summary>
|
||||
/// 绑定账号:把当前族员绑到自己已生效的权限行(仅限与登录中文姓名同名的族员);
|
||||
/// 返回绑定后的详情数据(锚点切到刚绑的族员)
|
||||
/// </summary>
|
||||
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
|
||||
[UnitOfWork]
|
||||
public FamilyTreeData BindSelfPerson(string treeId, string personId)
|
||||
{
|
||||
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
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<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解除绑定:把自己已生效权限行的 PersonId 置空(锚点回到始祖),解绑后可重新「绑定账号」
|
||||
/// </summary>
|
||||
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
|
||||
[UnitOfWork]
|
||||
public FamilyTreeData UnbindSelfPerson(string treeId)
|
||||
{
|
||||
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
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<FamilyTreeSpouseMapViewEntity> repositoryMapView = repository.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>();
|
||||
IRepository<FamilyTreeSpouseMapEntity> repositoryMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
|
||||
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
|
||||
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<FamilyTreeChildrenMapViewEntity> repositoryMapView = repository.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>();
|
||||
IRepository<FamilyTreeChildrenMapEntity> repositoryMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
|
||||
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
|
||||
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("您没有删除权限.");
|
||||
}
|
||||
|
||||
+53
-6
@@ -29,6 +29,49 @@ namespace CloudBuilder.Master.Service.FamilyTreeUtility
|
||||
return GetPersonId(permissions, treeId).Where(x => x == personId).Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员口径:该谱系下此用户存在 ManagerIndc=Y 且已生效的权限行(谱级操作专属)。
|
||||
/// 注意与 HasPermission 区分:成员若绑始祖或始祖在其上溯两代内,HasPermission(始祖) 也会为真,
|
||||
/// 不能拿它当谱级闸,否则这些成员能改谱/删谱
|
||||
/// </summary>
|
||||
public bool IsManager(string treeId)
|
||||
{
|
||||
IRepository<FamilyTreePermissionEntity> repositoryPermission = service.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
|
||||
return repositoryPermission.DetachedEntities
|
||||
.Where(x => x.UserName == service.UserId
|
||||
&& x.TreeId == treeId
|
||||
&& x.ManagerIndc == YesNoPolicy.YES
|
||||
&& x.ValidIndc == YesNoPolicy.YES)
|
||||
.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 成员口径:该谱系下此用户存在已生效权限行(不分管理员/成员档),
|
||||
/// 供「往谱里新增人」这类无既有 PersonId 可校验的操作做最小闸门
|
||||
/// </summary>
|
||||
public bool IsMember(string treeId)
|
||||
{
|
||||
IRepository<FamilyTreePermissionEntity> repositoryPermission = service.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
|
||||
return repositoryPermission.DetachedEntities
|
||||
.Where(x => x.UserName == service.UserId
|
||||
&& x.TreeId == treeId
|
||||
&& x.ValidIndc == YesNoPolicy.YES)
|
||||
.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户在本谱可编的族员集合(家支主线,口径与 HasPermission 完全同源);
|
||||
/// 供详情接口下发给前端做「逐个目标人」的编辑/删除钮判定(锚点级 IsCanAmend 盖不住关系行里的旁支)
|
||||
/// </summary>
|
||||
public List<string> GetAmendablePersonIds(string treeId)
|
||||
{
|
||||
IRepository<FamilyTreePermissionEntity> repositoryPermission = service.GetRepository<IRepository<FamilyTreePermissionEntity>>();
|
||||
FamilyTreePermissionEntity[] permissions = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).ToArray();
|
||||
return GetPersonId(permissions, treeId);
|
||||
}
|
||||
|
||||
public List<string> GetPersonId(FamilyTreePermissionEntity[] permissions, string treeId)
|
||||
{
|
||||
List<string> ps = new List<string>();
|
||||
@@ -41,7 +84,7 @@ namespace CloudBuilder.Master.Service.FamilyTreeUtility
|
||||
|
||||
IRepository<FamilyTreeSpouseMapEntity> repositorySpouseMap = service.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -17,8 +17,17 @@ namespace CloudBuilder.Master.Data
|
||||
public FamilyPersonData? CurrentPerson { get; set; }
|
||||
public bool IsManager { get; set; } = false;
|
||||
|
||||
/// <summary>已认证成员:存在绑定到自己且已生效(ValidIndc=YES)的权限行,前端据此控制成员/权限选项卡可见性</summary>
|
||||
public bool IsMember { get; set; } = false;
|
||||
|
||||
public bool IsCanAmend { get; set; } = false;
|
||||
|
||||
/// <summary>自己已生效权限行绑定的族员 PersonId(空串=已过审但未绑定),前端据此切换「绑定账号/解除绑定」按钮</summary>
|
||||
public string SelfBindPersonId { get; set; } = "";
|
||||
|
||||
/// <summary>当前用户在本谱可编的族员 PersonId 集合(家支主线,与 HasPermission 同源);管理员不限量不下发,前端按 IsManager 短路</summary>
|
||||
public string[] CanAmendPersonIds { get; set; } = [];
|
||||
|
||||
public FamilyTreePermissionUserViewEntity[]? FamilyTreePermissions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,7 @@ builder.Entity<FamilyTreePermissionEntity>().HasKey(c => new {c.PersonId,c.TreeI
|
||||
builder.Entity<FamilyTreePermissionUserViewEntity>(eb => { eb.HasNoKey(); });
|
||||
|
||||
|
||||
|
||||
//***END***
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,20 @@ var compositionData = new ApproveFamilyTreePermissionCompositionData4{
treeId =
|
||||
{
|
||||
var compositionData = new RejectFamilyTreePermissionCompositionData3{ ␍treeId = treeId, ␍currentPersonId = currentPersonId, ␍userName = userName }; ␍ Task.Run(() => serviceRequest.PostAsJsonAsync<RejectFamilyTreePermissionCompositionData3, dynamic>("/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<BindSelfPersonCompositionData2, FamilyTreeData>("/FamilyTree/BindSelfPerson", compositionData)).GetAwaiter().GetResult(); }
|
||||
|
||||
public FamilyTreeData UnbindSelfPerson(string treeId)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeData>("/FamilyTree/UnbindSelfPerson?treeId=" + treeId.ToString())).GetAwaiter().GetResult(); }
|
||||
|
||||
public FamilyTreeData Update(FamilyTreeData data)
|
||||
{
|
||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeData,FamilyTreeData>("/FamilyTree/Update" , data)).GetAwaiter().GetResult(); }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
<template v-else>
|
||||
<view v-for="row in rows" :key="keyOf(row)" class="pick-popup__row"
|
||||
:class="{ 'is-selected': isSelected(row) }" @tap="toggleSelect(row)">
|
||||
<!-- 多选勾选框 -->
|
||||
<view v-if="props.multiple" class="pick-popup__check" :class="{ 'is-on': isSelected(row) }">
|
||||
<!-- 选择指示:多选方勾、单选圆圈,选中绿底白勾 -->
|
||||
<view class="pick-popup__check" :class="{ 'is-on': isSelected(row), 'pick-popup__check--radio': !props.multiple }">
|
||||
<u-icon v-if="isSelected(row)" name="checkbox-mark" size="24" color="#ffffff" />
|
||||
</view>
|
||||
<view class="pick-popup__content">
|
||||
<text class="pick-popup__main">
|
||||
{{ getTitle(row) }}
|
||||
</text>
|
||||
<view v-for="col in getSubtitles()" :key="col.prop" class="pick-popup__sub">
|
||||
<text class="pick-popup__sub-label">{{ col.label }}</text>
|
||||
<text class="pick-popup__sub-value">{{ row[col.prop] ?? "" }}</text>
|
||||
</view>
|
||||
<!-- row 插槽:调用方可自绘行内容(如族谱页成员行:头像+姓名带性别+常用名);不传则走默认标题+副字段 -->
|
||||
<slot name="row" :row="row">
|
||||
<text class="pick-popup__main">
|
||||
{{ getTitle(row) }}
|
||||
</text>
|
||||
<view v-for="col in getSubtitles()" :key="col.prop" class="pick-popup__sub">
|
||||
<text class="pick-popup__sub-label">{{ col.label }}</text>
|
||||
<text class="pick-popup__sub-value">{{ row[col.prop] ?? "" }}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -272,10 +281,10 @@ watch(
|
||||
</template>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏(多选时) -->
|
||||
<view v-if="props.multiple" class="pick-popup__actions">
|
||||
<view class="pick-popup__action-left" @tap="toggleSelectAll">
|
||||
<text class="pick-popup__toggle">
|
||||
<!-- 底部操作栏(单选/多选常驻:取消、确定交互统一) -->
|
||||
<view class="pick-popup__actions">
|
||||
<view class="pick-popup__action-left">
|
||||
<text v-if="props.multiple" class="pick-popup__toggle" @tap="toggleSelectAll">
|
||||
{{ selectedKeys.length >= rows.length && rows.length > 0 ? t("AdminPage.unselectAllBut") :
|
||||
t("AdminPage.selectAllBut") }}
|
||||
</text>
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import type { PropType } from "vue";
|
||||
import type { FileItem, FileKind } from "@/data/custom/fileBrowser";
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useThemeTokens } from "@/composables/useThemeGradient";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -58,6 +58,18 @@ function visualOf(kind: FileKind): { icon: string; color: string } | null {
|
||||
return kind === "folder" ? null : kindVisual[kind];
|
||||
}
|
||||
|
||||
// 缩略图加载失败的卡记住失败态,回退类型图标(图被清理/静态目录无权限时不致白块)
|
||||
const thumbFailed = ref(new Set<number>());
|
||||
|
||||
function onThumbError(file: FileItem): void {
|
||||
thumbFailed.value.add(file.id);
|
||||
}
|
||||
|
||||
/** 可显缩略图的地址:仅图片类型且后端 Url(Scale 压缩图)有值、未报错 */
|
||||
function thumbSrcOf(file: FileItem): string {
|
||||
return file.kind === "image" && file.thumbUrl && !thumbFailed.value.has(file.id) ? file.thumbUrl : "";
|
||||
}
|
||||
|
||||
function isSelected(file: FileItem): boolean {
|
||||
return props.selectedIds.includes(file.id);
|
||||
}
|
||||
@@ -108,6 +120,9 @@ function onCardTap(file: FileItem): void {
|
||||
<view v-if="file.kind === 'folder'" class="fgl-card__folder" :style="folderStyle">
|
||||
<view class="fgl-card__folder-tab" :style="folderTabStyle" />
|
||||
</view>
|
||||
<!-- 图片卡:后端 Url 指向 Scale 原图压缩缩略(旧系统宫格同款),失败回退图标 -->
|
||||
<image v-else-if="thumbSrcOf(file)" class="fgl-card__thumb" :src="thumbSrcOf(file)"
|
||||
mode="aspectFill" @error="onThumbError(file)" />
|
||||
<u-icon v-else :name="visualOf(file.kind)!.icon" size="88" :color="visualOf(file.kind)!.color" />
|
||||
<!-- 锁仅目录显示(旧系统:文件不挂锁):绿开锁=有权限,灰闭锁=无权限;
|
||||
蓝分享标=已被收藏共享,两者文件/目录都可显示;
|
||||
@@ -235,6 +250,15 @@ function onCardTap(file: FileItem): void {
|
||||
border-radius: 10rpx 10rpx 0 0;
|
||||
}
|
||||
|
||||
/* 图片缩略图:尺寸对齐类型图标区(u-icon 88),圆角卡化 */
|
||||
.fgl-card__thumb {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
margin: 4rpx 0;
|
||||
border-radius: 14rpx;
|
||||
background-color: rgba(31, 35, 41, 0.04);
|
||||
}
|
||||
|
||||
.fgl-card__info {
|
||||
width: 100%;
|
||||
padding: 0 12rpx;
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 家族成员编辑器(底部抽屉,新增 / 修改共用)
|
||||
*
|
||||
* 把独立详情页 family-person-operation 的完整表单(基本 / 家庭 / 工作 / 简介 + 头像上传 + 身份证 OCR 回填)
|
||||
* 收进一个弹层,供族谱页「新增夫妻 / 孩子 / 祖先」与关系行「修改」复用;创建走 Insert、编辑走 Update(按 CreatedBy 判定,旧页同款)。
|
||||
* 组件自带数据与保存:create 用 props.treeId 落谱(可为空,祖先/新谱未落库时先建人再挂关系),
|
||||
* edit 用 props.personId 经 FindByKey 载入。保存成功 emit("saved", person) 交调用方回填 / 刷新。
|
||||
* 性别是 GenderPolicy 枚举,用 option-picker 单选;头像 / 身份证上传口径与详情页完全一致。
|
||||
*/
|
||||
import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { datePart } from "@/admin/admin-date";
|
||||
import { buildChunkFileName, uploadFile } from "@/common/http.interceptor";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import { FamilyPersonData } from "@/data/auto/familyPersonData";
|
||||
import familyPersonService from "@/service/master/familyPersonService";
|
||||
|
||||
/** 单个字段描述:type 缺省为文本;switch/date/textarea/gender 走各自控件 */
|
||||
interface FieldDef {
|
||||
prop: string;
|
||||
label: string;
|
||||
type?: "switch" | "date" | "textarea" | "gender";
|
||||
required?: boolean;
|
||||
maxlength?: number;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
/** create=新增(Insert)/ edit=修改(Update) */
|
||||
mode: { type: String as () => "create" | "edit", default: "create" },
|
||||
/** edit 模式要载入并修改的成员主键 */
|
||||
personId: { type: String, default: "" },
|
||||
/** create 模式落到的族谱编号(可为空:祖先 / 新谱未落库时先建人) */
|
||||
treeId: { type: String, default: "" },
|
||||
/** create 模式姓的默认值(新增孩子传当前族员的姓;身份证 OCR 也按此姓拆分姓名) */
|
||||
defaultSurname: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "saved", person: FamilyPersonDisplayData): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
// 主色:确认钮 / 勾选 / 开关随主题(渐变浅档白字发糊,用纯色)
|
||||
const { primary } = useThemeTokens();
|
||||
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
|
||||
|
||||
const data = ref<FamilyPersonData>(new FamilyPersonData());
|
||||
const person = computed(() => data.value.FamilyPerson);
|
||||
|
||||
const saving = ref(false);
|
||||
const uploading = ref(false);
|
||||
const idcardUploading = ref(false);
|
||||
const genderPickerVisible = ref(false);
|
||||
|
||||
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
|
||||
const genderLabel = computed(() => getPolicyText("GenderPolicy", person.value.Gender));
|
||||
|
||||
/** 弹层内四分区 tab(与 sections 一一对应)与基本区「更多资料」展开态:每次打开复位 */
|
||||
const activeTab = ref(0);
|
||||
const moreExpanded = ref(false);
|
||||
/** 基本区收进「更多资料」的字段:别名/字/号/排行,默认收起不占主表单 */
|
||||
const BASIC_MORE_PROPS = ["Alias", "Zi", "Hao", "BirthOrder"];
|
||||
/** hero 名字行:姓+谱名实时拼接(未填时模板里回退到标题/用途名) */
|
||||
const heroName = computed(() => `${person.value.Surname ?? ""}${person.value.GenealogyName ?? ""}`);
|
||||
/** 当前 tab 要渲染的字段:基本区收起时剩主四行(姓/谱名/性别/常用名) */
|
||||
const visibleFields = computed<FieldDef[]>(() => {
|
||||
const fields = sections.value[activeTab.value]?.fields ?? [];
|
||||
if (activeTab.value !== 0 || moreExpanded.value) {
|
||||
return fields;
|
||||
}
|
||||
return fields.filter(f => !BASIC_MORE_PROPS.includes(f.prop));
|
||||
});
|
||||
/** 「更多资料」行右侧灰字提示:收起字段的 label 连写(别名 · 字 · 号 · 排行) */
|
||||
const moreHint = computed(() =>
|
||||
(sections.value[0]?.fields ?? [])
|
||||
.filter(f => BASIC_MORE_PROPS.includes(f.prop))
|
||||
.map(f => f.label)
|
||||
.join(" · "),
|
||||
);
|
||||
|
||||
/** 分区字段配置:字段与详情页同口径;title 仅作分段器短标签(tabXxx),与旧页分区标题解耦 */
|
||||
const sections = computed<Array<{ title: string; fields: FieldDef[] }>>(() => [
|
||||
{
|
||||
title: t("FamilyPersonPage.tabBasic"),
|
||||
fields: [
|
||||
{ prop: "Surname", label: t("FamilyPersonPage.surname"), required: true },
|
||||
{ prop: "GenealogyName", label: t("FamilyPersonPage.genealogyName"), required: true },
|
||||
{ prop: "Gender", label: t("FamilyPersonPage.gender"), type: "gender" },
|
||||
{ prop: "CommonName", label: t("FamilyPersonPage.commonName") },
|
||||
{ prop: "Alias", label: t("FamilyPersonPage.alias") },
|
||||
{ prop: "Zi", label: t("FamilyPersonPage.zi") },
|
||||
{ prop: "Hao", label: t("FamilyPersonPage.hao") },
|
||||
{ prop: "BirthOrder", label: t("FamilyPersonPage.birthOrder") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("FamilyPersonPage.tabFamily"),
|
||||
fields: [
|
||||
{ prop: "MaritalStatus", label: t("FamilyPersonPage.maritalStatus") },
|
||||
{ prop: "FatherId", label: t("FamilyPersonPage.fatherId") },
|
||||
{ prop: "MotherName", label: t("FamilyPersonPage.motherName") },
|
||||
{ prop: "School", label: t("FamilyPersonPage.school") },
|
||||
{ prop: "Major", label: t("FamilyPersonPage.major") },
|
||||
{ prop: "Education", label: t("FamilyPersonPage.education") },
|
||||
{ prop: "GraduationStatus", label: t("FamilyPersonPage.graduationStatus") },
|
||||
{ prop: "Duty", label: t("FamilyPersonPage.duty") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("FamilyPersonPage.tabWork"),
|
||||
fields: [
|
||||
{ prop: "Title", label: t("FamilyPersonPage.title") },
|
||||
{ prop: "LiveStatus", label: t("FamilyPersonPage.liveStatus") },
|
||||
{ prop: "BindUserId", label: t("FamilyPersonPage.bindUserId") },
|
||||
{ prop: "IsBindUser", label: t("FamilyPersonPage.bindUser"), type: "switch" },
|
||||
{ prop: "BirthText", label: t("FamilyPersonPage.birthText") },
|
||||
{ prop: "BirthDate", label: t("FamilyPersonPage.birthDate"), type: "date" },
|
||||
{ prop: "DeathText", label: t("FamilyPersonPage.deathText") },
|
||||
{ prop: "DeathDate", label: t("FamilyPersonPage.deathDate"), type: "date" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("FamilyPersonPage.tabIntro"),
|
||||
fields: [
|
||||
{ prop: "Introduction", label: t("FamilyPersonPage.introduction"), type: "textarea", maxlength: 2000 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// 展开时按模式装载:edit 拉详情,create 重置并带上目标族谱号
|
||||
watch(
|
||||
() => props.visible,
|
||||
(on) => {
|
||||
if (!on) {
|
||||
return;
|
||||
}
|
||||
if (props.mode === "edit" && props.personId) {
|
||||
loadForEdit(props.personId);
|
||||
}
|
||||
else {
|
||||
resetForCreate();
|
||||
}
|
||||
// 弹层重开永远从「基本资料」起,更多资料回到收起态
|
||||
activeTab.value = 0;
|
||||
moreExpanded.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
function resetForCreate(): void {
|
||||
data.value = new FamilyPersonData();
|
||||
data.value.FamilyPerson.TreeId = props.treeId || "";
|
||||
// 新增孩子姓随亲(父/母):预填传入的默认姓,用户可改
|
||||
data.value.FamilyPerson.Surname = props.defaultSurname || "";
|
||||
}
|
||||
|
||||
async function loadForEdit(id: string): Promise<void> {
|
||||
try {
|
||||
const result = await familyPersonService.FindByKey(id);
|
||||
if (result?.FamilyPerson?.PersonId) {
|
||||
data.value = result;
|
||||
}
|
||||
else {
|
||||
uni.showToast({ title: t("AdminPage.notExistTip"), icon: "none" });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
uni.showToast({ title: t("FilePage.actionFailed"), icon: "none" });
|
||||
}
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
/** 取字段的字符串值(Date/布尔等统一转串供控件绑定;null/undefined 归空串) */
|
||||
function fv(prop: string): string {
|
||||
const raw = (person.value as Record<string, any>)[prop];
|
||||
return raw === null || raw === undefined ? "" : String(raw);
|
||||
}
|
||||
|
||||
function setField(prop: string, value: unknown): void {
|
||||
(person.value as Record<string, any>)[prop] = value;
|
||||
}
|
||||
|
||||
function onFieldRowTap(field: FieldDef): void {
|
||||
if (field.type === "gender") {
|
||||
genderPickerVisible.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onGenderConfirm(key: string): void {
|
||||
person.value.Gender = key;
|
||||
genderPickerVisible.value = false;
|
||||
}
|
||||
|
||||
/** 头像上传:单图 -> DmsFile/UploadFile(FilePath 头 FamilyPersonAvatar);AvatarFilename 存分片物理名,与详情页同口径 */
|
||||
function onUploadAvatar(): void {
|
||||
if (uploading.value) {
|
||||
return;
|
||||
}
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: async (res: any) => {
|
||||
const filePath = String(res?.tempFilePaths?.[0] ?? "");
|
||||
const fileName = String(res?.tempFiles?.[0]?.name || `avatar-${Date.now()}.png`);
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
uploading.value = true;
|
||||
const physicalName = buildChunkFileName(fileName);
|
||||
await uploadFile({
|
||||
url: "/DmsFile/UploadFile",
|
||||
filePath,
|
||||
name: fileName,
|
||||
fileName: physicalName,
|
||||
header: { FilePath: "FamilyPersonAvatar" },
|
||||
});
|
||||
person.value.AvatarFilename = physicalName;
|
||||
person.value.AvatarUrl = filePath;
|
||||
uni.showToast({ title: t("FamilyPersonPage.avatarUploadedTip"), icon: "none" });
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
uni.showToast({ title: t("FamilyPersonPage.avatarUploadFailed"), icon: "none" });
|
||||
}
|
||||
finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onClearAvatar(): void {
|
||||
if (person.value.AvatarUrl.startsWith("blob:") && typeof URL !== "undefined") {
|
||||
URL.revokeObjectURL(person.value.AvatarUrl);
|
||||
}
|
||||
person.value.AvatarFilename = "";
|
||||
person.value.AvatarUrl = "";
|
||||
}
|
||||
|
||||
/** 身份证 OCR 性别文本反查 GenderPolicy 存储键:先按 key、再按 label 精确,最后含「男/女」模糊,全落空返回空串 */
|
||||
function matchGenderKey(text: string): string {
|
||||
const value = text.trim();
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
const options = getPolicyOptions("GenderPolicy");
|
||||
const exact = options.find(o => o.key === value || o.label === value);
|
||||
if (exact) {
|
||||
return exact.key;
|
||||
}
|
||||
const lower = value.toLowerCase();
|
||||
const wantMale = value.includes("男") || lower.includes("male");
|
||||
const wantFemale = value.includes("女") || lower.includes("female");
|
||||
if (!wantMale && !wantFemale) {
|
||||
return "";
|
||||
}
|
||||
const hit = options.find((o) => {
|
||||
const label = `${o.key} ${o.label}`.toLowerCase();
|
||||
return (wantMale && (o.label.includes("男") || label.includes("male")))
|
||||
|| (wantFemale && (o.label.includes("女") || label.includes("female")));
|
||||
});
|
||||
return hit?.key ?? "";
|
||||
}
|
||||
|
||||
/** 出生日期归一 yyyy-MM-dd(OCR 常给 "19791111",兼容带分隔符);不足 8 位数字返回空串 */
|
||||
function formatBirthToYmd(raw: string): string {
|
||||
const digits = String(raw ?? "").replace(/\D/g, "");
|
||||
if (digits.length < 8) {
|
||||
return "";
|
||||
}
|
||||
return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
/** 把 UploadIdCardFile 返回的识别信息回填:常用名/性别/出生日期回填各自列,其余无列项写入简介;有任一写入返回 true */
|
||||
function applyIdCardOcr(info: any): boolean {
|
||||
if (!info || typeof info !== "object") {
|
||||
return false;
|
||||
}
|
||||
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
|
||||
let filled = 0;
|
||||
|
||||
const name = String(pick("CommonName", "commonName") ?? "").trim();
|
||||
if (name) {
|
||||
person.value.CommonName = name;
|
||||
filled += 1;
|
||||
// 按现有姓拆分身份证姓名:姓不变,去掉姓后的余名填入空白谱名(已有谱名不覆盖;姓为空或名字不含姓时只回填常用名)
|
||||
const surname = String(person.value.Surname ?? "").trim();
|
||||
const rest = surname && name.startsWith(surname) ? name.slice(surname.length).trim() : "";
|
||||
if (rest && !String(person.value.GenealogyName ?? "").trim()) {
|
||||
person.value.GenealogyName = rest;
|
||||
filled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const genderKey = matchGenderKey(String(pick("Gender", "gender") ?? ""));
|
||||
if (genderKey) {
|
||||
person.value.Gender = genderKey;
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
const ymd = formatBirthToYmd(String(pick("BirthDate", "birthDate") ?? ""));
|
||||
if (ymd) {
|
||||
setField("BirthDate", ymd);
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
if (upsertIdCardIntro(info)) {
|
||||
filled += 1;
|
||||
}
|
||||
|
||||
return filled > 0;
|
||||
}
|
||||
|
||||
/** 无列身份信息格式化成「身份证识别信息」块追加到简介末尾(重识别先截旧块再追加,避免堆叠);无可写项返回 false */
|
||||
function upsertIdCardIntro(info: any): boolean {
|
||||
const pick = (pascal: string, camel: string): unknown => info[pascal] ?? info[camel];
|
||||
const labelMap: Array<[string, string, string]> = [
|
||||
["nation", "Nation", "nation"],
|
||||
["address", "Address", "address"],
|
||||
["idNumber", "IdNumber", "idNumber"],
|
||||
["issuer", "Issuer", "issuer"],
|
||||
["validity", "Validity", "validity"],
|
||||
];
|
||||
const lines: string[] = [];
|
||||
for (const [labelKey, pascal, camel] of labelMap) {
|
||||
const val = String(pick(pascal, camel) ?? "").trim();
|
||||
if (val) {
|
||||
lines.push(`${t(`FamilyPersonPage.${labelKey}`)}: ${val}`);
|
||||
}
|
||||
}
|
||||
if (!lines.length) {
|
||||
return false;
|
||||
}
|
||||
const title = t("FamilyPersonPage.idcardIntroTitle");
|
||||
const block = [title, ...lines].join("\n");
|
||||
let intro = String(person.value.Introduction ?? "");
|
||||
const idx = intro.indexOf(title);
|
||||
if (idx >= 0) {
|
||||
intro = intro.slice(0, idx).replace(/\s+$/, "");
|
||||
}
|
||||
person.value.Introduction = intro ? `${intro}\n\n${block}` : block;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 上传身份证:单图 -> 同源后端 DmsFile/UploadIdCardFile(落盘 idcard 目录 + server-to-server 调 OCR)-> 回填 */
|
||||
function onUploadIdCard(): void {
|
||||
if (idcardUploading.value) {
|
||||
return;
|
||||
}
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: async (res: any) => {
|
||||
const filePath = String(res?.tempFilePaths?.[0] ?? "");
|
||||
const fileName = String(res?.tempFiles?.[0]?.name || `idcard-${Date.now()}.jpg`);
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
idcardUploading.value = true;
|
||||
const physicalName = buildChunkFileName(fileName);
|
||||
person.value.IdCardFilename = physicalName;
|
||||
person.value.IdCardUrl = filePath;
|
||||
const result = await uploadFile({
|
||||
url: "/DmsFile/UploadIdCardFile",
|
||||
filePath,
|
||||
name: fileName,
|
||||
fileName: physicalName,
|
||||
header: { FilePath: "idcard" },
|
||||
formData: { side: "auto" },
|
||||
});
|
||||
const recognized = result?.Data ?? result?.data ?? result;
|
||||
if (applyIdCardOcr(recognized)) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.idcardUploadedTip"), icon: "none" });
|
||||
}
|
||||
else {
|
||||
uni.showToast({ title: t("FamilyPersonPage.idcardNoResult"), icon: "none" });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
uni.showToast({ title: t("FamilyPersonPage.idcardUploadFailed"), icon: "none" });
|
||||
}
|
||||
finally {
|
||||
idcardUploading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onClearIdCard(): void {
|
||||
if (person.value.IdCardUrl.startsWith("blob:") && typeof URL !== "undefined") {
|
||||
URL.revokeObjectURL(person.value.IdCardUrl);
|
||||
}
|
||||
person.value.IdCardFilename = "";
|
||||
person.value.IdCardUrl = "";
|
||||
}
|
||||
|
||||
/** 保存:姓 / 谱名必填;按 CreatedBy 有无决定 Update / Insert;成功回传 person 并关闭 */
|
||||
async function onSave(): Promise<void> {
|
||||
if (saving.value) {
|
||||
return;
|
||||
}
|
||||
if (!String(person.value.Surname ?? "").trim()) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.surnameRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!String(person.value.GenealogyName ?? "").trim()) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.genealogyNameRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
saving.value = true;
|
||||
const isUpdate = String(person.value.CreatedBy ?? "").trim() !== "";
|
||||
const result = await (isUpdate
|
||||
? familyPersonService.Update(data.value)
|
||||
: familyPersonService.Insert(data.value));
|
||||
const saved = result?.FamilyPerson || data.value.FamilyPerson;
|
||||
uni.showToast({ title: t("AdminPage.savedTip"), icon: "none" });
|
||||
emit("saved", saved);
|
||||
close();
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
uni.showToast({ title: t("FilePage.actionFailed"), icon: "none" });
|
||||
}
|
||||
finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24"
|
||||
:close-on-click-overlay="!saving && !uploading && !idcardUploading" @close="close">
|
||||
<view class="pe">
|
||||
<!-- hero(图示同款):左大圆头像点开选图,右侧名字实时拼接 + 谱名灰字 + 红字清除头像;新增未填名时回退用途标题 -->
|
||||
<view class="pe__hero">
|
||||
<view class="pe__avatar" @tap="onUploadAvatar">
|
||||
<image v-if="person.AvatarUrl" class="pe__avatar-img" :src="person.AvatarUrl" mode="aspectFill" />
|
||||
<u-icon v-else name="account" size="52" color="#c4c8cf" />
|
||||
<view class="pe__avatar-mask">
|
||||
<text v-if="uploading" class="pe__avatar-mask-text">
|
||||
{{ t("FamilyPersonPage.uploadingTip") }}
|
||||
</text>
|
||||
<u-icon v-else name="camera" size="26" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="pe__hero-main">
|
||||
<text class="pe__hero-name">{{ heroName || props.title || t('FamilyPersonPage.detailTitle') }}</text>
|
||||
<text class="pe__hero-genealogy">{{ t("FamilyPersonPage.genealogyName") }} {{ person.GenealogyName || "-" }}</text>
|
||||
<text v-if="person.AvatarUrl || person.AvatarFilename" class="pe__hero-clear" @tap="onClearAvatar">
|
||||
{{ t("FamilyPersonPage.clearAvatar") }}
|
||||
</text>
|
||||
</view>
|
||||
<!-- 公开资料开关从任职区移到 hero 右侧:一眼可见的整档隐私闸 -->
|
||||
<view class="pe__hero-privacy">
|
||||
<text class="pe__hero-privacy-label">{{ t("FamilyPersonPage.privacy") }}</text>
|
||||
<u-switch :model-value="person.Privacy === 'Y'" :active-color="primary"
|
||||
@change="setField('Privacy', $event ? 'Y' : 'N')" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四分段器:一次只看一个分区(基本/家庭学业/任职状态/简介) -->
|
||||
<view class="pe__tabs">
|
||||
<view v-for="(section, index) in sections" :key="section.title" class="pe__tab"
|
||||
:class="{ 'pe__tab--on': activeTab === index }" @tap="activeTab = index">
|
||||
<text>{{ section.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="pe__body">
|
||||
<!-- 当前分区字段:一行一字段,标签左 / 控件右 -->
|
||||
<view class="pe__card">
|
||||
<view v-for="field in visibleFields" :key="field.prop" class="pe__row"
|
||||
:class="{ 'pe__row--tap': field.type === 'gender', 'pe__row--area': field.type === 'textarea' }"
|
||||
@tap="onFieldRowTap(field)">
|
||||
<!-- 简介区不展 label:整卡只剩一块大输入区 -->
|
||||
<view v-if="field.type !== 'textarea'" class="pe__label">
|
||||
<text>{{ field.label }}</text>
|
||||
<text v-if="field.required" class="pe__required">
|
||||
*
|
||||
</text>
|
||||
</view>
|
||||
<view class="pe__control">
|
||||
<!-- 性别:枚举行点开 option-picker 回写 -->
|
||||
<view v-if="field.type === 'gender'" class="pe__picker">
|
||||
<text :class="{ 'pe__picker-empty': !genderLabel }">
|
||||
{{ genderLabel || t('FamilyPersonPage.genderPlaceholder') }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
|
||||
</view>
|
||||
<u-switch v-else-if="field.type === 'switch'" :model-value="fv(field.prop) === 'Y'"
|
||||
:active-color="primary" @change="setField(field.prop, $event ? 'Y' : 'N')" />
|
||||
<textarea v-else-if="field.type === 'textarea'" class="pe__textarea"
|
||||
:value="fv(field.prop)" :maxlength="field.maxlength ?? 2000"
|
||||
:placeholder="t('FamilyPersonPage.tabIntro')"
|
||||
@input="setField(field.prop, $event.detail.value)" />
|
||||
<picker v-else-if="field.type === 'date'" mode="date" :value="datePart(fv(field.prop))"
|
||||
@change="setField(field.prop, $event.detail.value)">
|
||||
<view class="pe__picker">
|
||||
<text :class="{ 'pe__picker-empty': !fv(field.prop) }">
|
||||
{{ datePart(fv(field.prop)) || t('AdminPage.notSetTip') }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="24" color="#c4c8cf" />
|
||||
</view>
|
||||
</picker>
|
||||
<input v-else class="pe__input" :value="fv(field.prop)"
|
||||
:maxlength="field.maxlength ?? 100" :placeholder="field.label"
|
||||
@input="setField(field.prop, $event.detail.value)">
|
||||
</view>
|
||||
</view>
|
||||
<!-- 基本区「更多资料」展开行:绿链 + 收起字段灰字提示,点开就地展开/收起 -->
|
||||
<view v-if="activeTab === 0" class="pe__row pe__row--tap" @tap="moreExpanded = !moreExpanded">
|
||||
<text class="pe__more-link">{{ t("FamilyPersonPage.moreInfo") }}</text>
|
||||
<view class="pe__picker">
|
||||
<text class="pe__more-hint">{{ moreHint }}</text>
|
||||
<u-icon :name="moreExpanded ? 'arrow-up' : 'arrow-down'" size="22" color="#8f959e" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 身份证上传块:仅基本区展示(图示同位),标题入卡内 -->
|
||||
<view v-if="activeTab === 0" class="pe__group">
|
||||
<view class="pe__card pe__idcard">
|
||||
<text class="pe__idcard-title">
|
||||
{{ t("FamilyPersonPage.idcardTitle") }}
|
||||
</text>
|
||||
<view class="pe__idcard-head">
|
||||
<text class="pe__idcard-hint">
|
||||
{{ t("FamilyPersonPage.idcardHint") }}
|
||||
</text>
|
||||
<text v-if="person.IdCardUrl || person.IdCardFilename" class="pe__idcard-clear"
|
||||
@tap="onClearIdCard">
|
||||
{{ t("FamilyPersonPage.idcardClear") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pe__idcard-thumb" @tap="onUploadIdCard">
|
||||
<image v-if="person.IdCardUrl" class="pe__idcard-thumb-img" :src="person.IdCardUrl"
|
||||
mode="aspectFill" />
|
||||
<view v-else class="pe__idcard-empty">
|
||||
<u-icon name="camera" size="40" color="#c4c8cf" />
|
||||
<text class="pe__idcard-empty-text">
|
||||
{{ t("FamilyPersonPage.idcardUpload") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pe__idcard-mask">
|
||||
<text v-if="idcardUploading" class="pe__idcard-mask-text">
|
||||
{{ t("FamilyPersonPage.uploadingTip") }}
|
||||
</text>
|
||||
<u-icon v-else-if="person.IdCardUrl" name="camera" size="30" color="#ffffff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pe__body-space" />
|
||||
</scroll-view>
|
||||
|
||||
<view class="pe__footer">
|
||||
<view class="pe__btn" @tap="close">
|
||||
<text class="pe__btn-text">
|
||||
{{ t("AdminPage.cancelBut") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pe__btn pe__btn--primary" :class="{ 'pe__btn--muted': saving }"
|
||||
:style="{ background: primary }" @tap="onSave">
|
||||
<text class="pe__btn-text pe__btn-text--primary">
|
||||
{{ saving ? t("AdminPage.savingTip") : t("AdminPage.saveBut") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pe__safe" />
|
||||
</view>
|
||||
|
||||
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
|
||||
:empty-text="t('FamilyPersonPage.genderEmpty')" :confirm-text="t('AdminPage.confirmBut')"
|
||||
@close="genderPickerVisible = false" @confirm="onGenderConfirm" />
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块(同 option-picker / person-quick-create) */
|
||||
.pe {
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
/* hero:左大圆头像 + 右侧名字/谱名/清除链,图示同款左对齐结构 */
|
||||
&__hero {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
padding: 4rpx 12rpx 24rpx;
|
||||
}
|
||||
|
||||
&__hero-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__hero-name {
|
||||
overflow: hidden;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__hero-genealogy {
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__hero-clear {
|
||||
font-size: 24rpx;
|
||||
color: #fa3534;
|
||||
}
|
||||
|
||||
/* hero 右侧:公开资料开关,小灰字题注 + 开关竖排 */
|
||||
&__hero-privacy {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
&__hero-privacy-label {
|
||||
font-size: 22rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
/* 四分段器:灰底轨道 + 选中白底胶囊(同详情页 tab 观感) */
|
||||
&__tabs {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background-color: #e8eaed;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 68rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 999rpx;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #5c626b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&--on {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(31, 35, 41, 0.08);
|
||||
|
||||
text {
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 头像:hero 左侧大圆图,点开选图上传,上传中遮罩转文字 */
|
||||
&__avatar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
border: 3rpx solid #e5e6eb;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__avatar-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
&__avatar-mask-text {
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&__body {
|
||||
/* 明确封顶高度:scroll-view 在 uni-app/H5 需确定高度才会内部滚动,否则撑满内容把底部钮顶出可视区(同 option-picker) */
|
||||
max-height: 56vh;
|
||||
}
|
||||
|
||||
&__group {
|
||||
margin-top: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 0 28rpx;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
/* 一行一字段:标签左、控件右,行间细线(末行无线) */
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
min-height: 92rpx;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #eef0f3;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--tap {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 简介行:竖排整宽无标签,拉高输入区 */
|
||||
&--area {
|
||||
display: block;
|
||||
|
||||
.pe__textarea {
|
||||
min-height: 360rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&__required {
|
||||
margin-left: 6rpx;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&__control {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: #1f2329;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&__picker-empty {
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
/* 「更多资料」行:左绿链 + 右灰字字段提示 + 箭头 */
|
||||
&__more-link {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
&__more-hint {
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
/* 身份证块 */
|
||||
&__idcard {
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
&__idcard-title {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&__idcard-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__idcard-hint {
|
||||
flex: 1;
|
||||
font-size: 22rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__idcard-clear {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fa3534;
|
||||
}
|
||||
|
||||
&__idcard-thumb {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 260rpx;
|
||||
overflow: hidden;
|
||||
background-color: #f7f8fa;
|
||||
border: 1rpx dashed #d4d7dd;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__idcard-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__idcard-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__idcard-empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__idcard-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&__idcard-mask-text {
|
||||
font-size: 22rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&__body-space {
|
||||
height: 16rpx;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* 底部按键统一规格(同 option-picker):等高圆角矩形 + 细描边,非胶囊 */
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e5e6eb;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
&--primary {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&--muted {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
|
||||
&--primary {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&__safe {
|
||||
flex-shrink: 0;
|
||||
height: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,259 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 快速新增家族成员(底部抽屉):旧 WebVue components/master/FamilyPersonQuickCreateModal.vue 的移动端等价物
|
||||
*
|
||||
* 族谱关系维护里「新增配偶 / 新增孩子 / 新增祖先」都调它:只填 姓* / 名(谱名)* / 性别*,
|
||||
* 落库走 FamilyPersonService.Insert(TreeId 由调用方带入),成功后 emit("created", person),
|
||||
* created 取 result?.FamilyPerson || payload.FamilyPerson,由页面按用途回填到对应关系列表。
|
||||
* 性别用 option-picker 单选(数据来自 GenderPolicy 语言包),与成员详情页同口径。
|
||||
*/
|
||||
import type { FamilyPersonDisplayData } from "@/data/auto/familyPersonDisplayData";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useThemeTokens } from "@/composables";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import { FamilyPersonData } from "@/data/auto/familyPersonData";
|
||||
import { FamilyPersonDisplayData as FamilyPersonDisplayDataCls } from "@/data/auto/familyPersonDisplayData";
|
||||
import familyPersonService from "@/service/master/familyPersonService";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
treeId: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "created", person: FamilyPersonDisplayData): void;
|
||||
}>();
|
||||
|
||||
const { t } = useLocale();
|
||||
// 确认钮纯色跟主题(渐变浅档白字发糊)
|
||||
const { primary } = useThemeTokens();
|
||||
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
|
||||
|
||||
const genderItems = computed(() => getPolicyOptions("GenderPolicy"));
|
||||
|
||||
const surname = ref("");
|
||||
const genealogyName = ref("");
|
||||
const gender = ref("");
|
||||
const saving = ref(false);
|
||||
const genderPickerVisible = ref(false);
|
||||
|
||||
const genderLabel = computed(() => getPolicyText("GenderPolicy", gender.value));
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(on) => {
|
||||
if (on) {
|
||||
surname.value = "";
|
||||
genealogyName.value = "";
|
||||
gender.value = "";
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function close(): void {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
function onGenderConfirm(key: string): void {
|
||||
gender.value = key;
|
||||
genderPickerVisible.value = false;
|
||||
}
|
||||
|
||||
async function onConfirm(): Promise<void> {
|
||||
if (!surname.value.trim()) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.surnameRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!genealogyName.value.trim()) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.genealogyNameRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!gender.value) {
|
||||
uni.showToast({ title: t("FamilyPersonPage.genderRequired"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = new FamilyPersonData();
|
||||
payload.FamilyPerson = new FamilyPersonDisplayDataCls();
|
||||
payload.FamilyPerson.Surname = surname.value.trim();
|
||||
payload.FamilyPerson.GenealogyName = genealogyName.value.trim();
|
||||
payload.FamilyPerson.Gender = gender.value;
|
||||
payload.FamilyPerson.TreeId = props.treeId || "";
|
||||
|
||||
try {
|
||||
saving.value = true;
|
||||
const result = await familyPersonService.Insert(payload);
|
||||
const created = result?.FamilyPerson || payload.FamilyPerson;
|
||||
emit("created", created);
|
||||
close();
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
uni.showToast({ title: t("FamilyPersonPage.quickCreateFailed"), icon: "none" });
|
||||
}
|
||||
finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<u-popup :model-value="props.visible" mode="bottom" round="24" :close-on-click-overlay="!saving" @close="close">
|
||||
<view class="pqc">
|
||||
<text class="pqc__title">
|
||||
{{ props.title || t('FamilyPersonPage.quickCreateTitle') }}
|
||||
</text>
|
||||
|
||||
<!-- 表单白卡(底部抽屉统一风格:灰底 + 白色圆角分块) -->
|
||||
<view class="pqc__card">
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.surname") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<u-input v-model="surname" :border="true" :maxlength="100"
|
||||
:placeholder="t('FamilyPersonPage.surnamePlaceholder')" />
|
||||
</view>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.genealogyName") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<u-input v-model="genealogyName" :border="true" :maxlength="100"
|
||||
:placeholder="t('FamilyPersonPage.genealogyNamePlaceholder')" />
|
||||
</view>
|
||||
|
||||
<view class="pqc__field">
|
||||
<text class="pqc__label">
|
||||
{{ t("FamilyPersonPage.gender") }}<text class="pqc__required">*</text>
|
||||
</text>
|
||||
<view class="pqc__picker" @tap="genderPickerVisible = true">
|
||||
<text :class="{ 'pqc__picker-empty': !genderLabel }">
|
||||
{{ genderLabel || t('FamilyPersonPage.genderPlaceholder') }}
|
||||
</text>
|
||||
<u-icon name="arrow-right" size="26" color="#c4c8cf" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pqc__footer">
|
||||
<view class="pqc__btn" @tap="close">
|
||||
<text class="pqc__btn-text">
|
||||
{{ t("AdminPage.cancelBut") }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="pqc__btn pqc__btn--primary" :class="{ 'pqc__btn--muted': saving }"
|
||||
:style="{ background: primary }" @tap="onConfirm">
|
||||
<text class="pqc__btn-text pqc__btn-text--primary">
|
||||
{{ saving ? t("AdminPage.savingTip") : t("AdminPage.confirmBut") }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pqc__safe" />
|
||||
</view>
|
||||
|
||||
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
|
||||
:empty-text="t('FamilyPersonPage.genderEmpty')" @close="genderPickerVisible = false"
|
||||
@confirm="onGenderConfirm" />
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 底部抽屉统一风格:浅灰面板底 + 白色圆角卡分块 */
|
||||
.pqc {
|
||||
padding: 32rpx 24rpx 0;
|
||||
background-color: #f2f3f5;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
padding-bottom: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__card {
|
||||
padding: 28rpx 28rpx 4rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&__field {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #646a73;
|
||||
}
|
||||
|
||||
&__required {
|
||||
margin-left: 2rpx;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&__picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 76rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1f2329;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
&__picker-empty {
|
||||
color: #8f959e;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
/* 底部按键统一规格(同 option-picker):等高圆角矩形 + 细描边,非胶囊 */
|
||||
&__btn {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 96rpx;
|
||||
border: 2rpx solid #e5e6eb;
|
||||
border-radius: 24rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
&--primary {
|
||||
border-color: transparent;
|
||||
background-color: #2b8fff;
|
||||
}
|
||||
|
||||
/* 提交中:纯色 + 透明度置灰(色相不脱节) */
|
||||
&--muted {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 32rpx;
|
||||
color: #5c626b;
|
||||
|
||||
&--primary {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&__safe {
|
||||
height: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -127,10 +127,13 @@ export function useDmsFiles() {
|
||||
|
||||
function toFileItem(record: DmsFileUploadViewDisplayData): FileItem {
|
||||
const time = parseTime(record.CreatedDatetime);
|
||||
const kind = kindOf(record);
|
||||
return {
|
||||
id: record.FileId,
|
||||
name: record.OrgFileName || record.FileName,
|
||||
kind: kindOf(record),
|
||||
kind,
|
||||
// 图片卡显 Scale 缩略图(后端 Url 字段已拼好,旧系统宫格同款)
|
||||
thumbUrl: kind === "image" ? record.Url || undefined : undefined,
|
||||
// 卡片元数据不显示用户:文件夹 sizeText 为空只剩时间,文件显示大小+时间
|
||||
// FileSize 单位是 KB(旧系统 DmsFileEnquiry 下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
|
||||
sizeText: formatSize(
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
import type { PickColumn } from "@/components/admin-pick-popup/admin-pick-popup.vue";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity";
|
||||
import { FamilyPersonSearchCriteria } from "@/data/auto/familyPersonSearchCriteria";
|
||||
import { SearchFilter } from "@/data/auto/searchFilter";
|
||||
import { SecurityActorEntity } from "@/data/auto/securityActorEntity";
|
||||
import { SecurityActorSearchCriteria } from "@/data/auto/securityActorSearchCriteria";
|
||||
@@ -26,6 +28,7 @@ import { SecurityUserEntity } from "@/data/auto/securityUserEntity";
|
||||
import { SecurityUserSearchCriteria } from "@/data/auto/securityUserSearchCriteria";
|
||||
import { SearchFilterOperatorPolicy } from "@/data/custom/searchFilterOperatorPolicy";
|
||||
import { SearchFilterTypePolicy } from "@/data/custom/searchFilterTypePolicy";
|
||||
import familyPersonService from "@/service/master/familyPersonService";
|
||||
import securityActorService from "@/service/security/securityActorService";
|
||||
import securityFunctionService from "@/service/security/securityFunctionService";
|
||||
import securityGroupService from "@/service/security/securityGroupService";
|
||||
@@ -378,3 +381,47 @@ export function useHelpFeaturePickConfig() {
|
||||
fixedFilters,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Family Person(族员选择:切换当前族员 / 选已有成员作配偶)=====
|
||||
export function usePersonPickConfig() {
|
||||
const { t } = useLocale();
|
||||
const fixedFilters = ref<Record<string, string>>({});
|
||||
|
||||
const columns = computed<PickColumn[]>(() => [
|
||||
{
|
||||
label: t("FamilyPersonPage.genealogyName"),
|
||||
prop: FamilyPersonEntity.GENEALOGY_NAME,
|
||||
},
|
||||
{ label: t("FamilyPersonPage.surname"), prop: FamilyPersonEntity.SURNAME },
|
||||
{
|
||||
label: t("FamilyPersonPage.commonName"),
|
||||
prop: FamilyPersonEntity.COMMON_NAME,
|
||||
},
|
||||
// 成员编号列不显示:弹窗列表口径与族谱页统一用常用名辨识(rowKey 仍是 PersonId,仅不占行)
|
||||
]);
|
||||
|
||||
async function loader(page: number, pageSize: number, keyword: string) {
|
||||
const criteria = new FamilyPersonSearchCriteria();
|
||||
criteria.GridColumns = [];
|
||||
criteria.page = page;
|
||||
criteria.limit = pageSize;
|
||||
criteria.orderby = FamilyPersonEntity.DB_GENEALOGY_NAME;
|
||||
const filters: SearchFilter[] = [];
|
||||
if (keyword) {
|
||||
filters.push(
|
||||
containFilter(FamilyPersonEntity.DB_GENEALOGY_NAME, keyword),
|
||||
);
|
||||
}
|
||||
criteria.Filter = filters;
|
||||
const data = await familyPersonService.FindBySearchCriteria(criteria);
|
||||
return { rows: data?.Datas ?? [], total: data?.Total ?? 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
loader,
|
||||
rowKey: FamilyPersonEntity.PERSON_ID,
|
||||
titleKey: FamilyPersonEntity.GENEALOGY_NAME,
|
||||
fixedFilters,
|
||||
};
|
||||
}
|
||||
@@ -76,10 +76,13 @@ export function useShareHall() {
|
||||
|
||||
function toFileItem(record: FamilyDmsFileViewDisplayData): FileItem {
|
||||
const time = parseTime(record.CreatedDatetime);
|
||||
const kind = kindOf(record);
|
||||
return {
|
||||
id: record.FileId,
|
||||
name: record.OrgFileName || record.FileName,
|
||||
kind: kindOf(record),
|
||||
kind,
|
||||
// 图片卡显 Scale 缩略图(后端 Url 字段已拼好,旧系统宫格同款)
|
||||
thumbUrl: kind === "image" ? record.Url || undefined : undefined,
|
||||
// FileSize 单位是 KB(旧系统下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
|
||||
sizeText: formatSize(
|
||||
record.FileSize ? record.FileSize * 1024 : undefined,
|
||||
|
||||
@@ -40,10 +40,13 @@ export function useTrashFiles() {
|
||||
/** 删除日志 -> 卡片条目:时间列展示的是删除时间(记录 CreatedDatetime),path 留原目录备查 */
|
||||
function toFileItem(record: DmsFileUploadDeleteLogViewDisplayData): FileItem {
|
||||
const time = parseTime(record.CreatedDatetime);
|
||||
const kind = kindOf(record);
|
||||
return {
|
||||
id: record.FileId,
|
||||
name: record.OrgFileName || record.FileName,
|
||||
kind: kindOf(record),
|
||||
kind,
|
||||
// 回收站也显缩略图(日志视图 Url 同口径拼的 Scale 图,文件删了图还在)
|
||||
thumbUrl: kind === "image" ? record.Url || undefined : undefined,
|
||||
// FileSize 单位是 KB(旧系统下载缓冲 (FileSize+1)*1024 字节佐证),先换算成字节
|
||||
sizeText: formatSize(
|
||||
record.FileSize ? record.FileSize * 1024 : undefined,
|
||||
|
||||
@@ -7,7 +7,10 @@ export class FamilyTreeData
|
||||
public FamilyTree: FamilyTreeDisplayData = new FamilyTreeDisplayData();
|
||||
public CurrentPerson: FamilyPersonData = new FamilyPersonData();
|
||||
public IsManager: boolean =false;
|
||||
public IsMember: boolean =false;
|
||||
public IsCanAmend: boolean =false;
|
||||
public SelfBindPersonId: string = "";
|
||||
public CanAmendPersonIds: string[]= [];
|
||||
public FamilyTreePermissions: FamilyTreePermissionUserViewEntity[] = [];
|
||||
|
||||
}
|
||||
@@ -28,6 +28,8 @@ export interface FileItem {
|
||||
permission?: "Y" | "N";
|
||||
/** 已被收藏共享(PermissionIndc=Y 且 FavoriteIndc=Y):显示蓝色共享角标 */
|
||||
shared?: boolean;
|
||||
/** 图片缩略图地址(后端 PackUrl 拼的 Scale 压缩图,旧系统宫格同款);仅 image 类型且有值时卡片显图,否则显类型图标 */
|
||||
thumbUrl?: string;
|
||||
}
|
||||
|
||||
/** 快捷功能入口的单项:图标 + 渐变底色,页面给什么画什么 */
|
||||
|
||||
@@ -161,12 +161,20 @@
|
||||
"genealogyName": "Genealogy Name",
|
||||
"commonName": "Common Name",
|
||||
"personSearch": "Member Search",
|
||||
"pedigreeChart": "Pedigree Chart",
|
||||
"ancestryChart": "Ancestry Chart",
|
||||
"pedigreeChart": "Branch Chart",
|
||||
"ancestryChart": "Full Chart",
|
||||
"chartEmpty": "No family tree data",
|
||||
"applyManage": "Apply for Manager",
|
||||
"bindAccount": "Bind Account",
|
||||
"unbindAccount": "Unbind",
|
||||
"unbindSuccessTip": "Binding with the member has been removed",
|
||||
"bindSuccessTip": "Current member has been bound to your account",
|
||||
"bindNoPersonTip": "Select the member to bind first",
|
||||
"requestSentTip": "Request submitted, waiting for approval",
|
||||
"alreadyManagerTip": "You are already a manager of this tree",
|
||||
"addAncestor": "Add Ancestor",
|
||||
"editAncestor": "Edit Ancestor",
|
||||
"ancestorRequired": "Please add an ancestor first",
|
||||
"spouseTitle": "Spouses",
|
||||
"addSpouse": "Add Spouse",
|
||||
"pickSpouse": "Pick Member",
|
||||
@@ -203,11 +211,42 @@
|
||||
"chipPublic": "Public",
|
||||
"chipPrivate": "Private",
|
||||
"searchPlaceholder": "Search tree name",
|
||||
"listTitle": "Family Trees"
|
||||
"listTitle": "Family Trees",
|
||||
"tabOverview": "Overview",
|
||||
"tabMembers": "Members",
|
||||
"tabPerms": "Access",
|
||||
"statMembers": "members",
|
||||
"statManagers": "managers",
|
||||
"statPending": "pending",
|
||||
"introTitle": "Brief",
|
||||
"quickActions": "Quick Actions",
|
||||
"editProfile": "Edit Info",
|
||||
"edit": "Edit",
|
||||
"addMember": "Add Member",
|
||||
"selectMember": "Select Current Member",
|
||||
"deleteTree": "Delete Tree",
|
||||
"deleteTreeHint": "All member profiles will be deleted too",
|
||||
"searchMember": "Search name, genealogy name or member ID",
|
||||
"filterAll": "All",
|
||||
"filterMale": "Male",
|
||||
"filterFemale": "Female",
|
||||
"ancestorCard": "Ancestor",
|
||||
"ancestorInfo": "Ancestor Info",
|
||||
"noAncestor": "No ancestor info yet",
|
||||
"createdAt": "Created",
|
||||
"publicHint": "When public, others can search and request to join",
|
||||
"addChildBtn": "Add Child",
|
||||
"viewFullChart": "View Full Chart",
|
||||
"editInfoTitle": "Edit Family Tree Info",
|
||||
"removeRelation": "Remove Relation",
|
||||
"removeRelationConfirm": "Remove the relation with {name}? It takes effect immediately after saving.",
|
||||
"dupMemberTip": "This member is already linked, do not add again"
|
||||
},
|
||||
"FamilyPersonPage": {
|
||||
"title": "Family Member Query",
|
||||
"detailTitle": "Member Detail",
|
||||
"editTitle": "Edit Member",
|
||||
"editBut": "Edit",
|
||||
"personId": "Member ID",
|
||||
"treeId": "Tree ID",
|
||||
"surname": "Surname",
|
||||
@@ -223,6 +262,11 @@
|
||||
"zi": "Courtesy Name (Zi)",
|
||||
"hao": "Art Name (Hao)",
|
||||
"birthOrder": "Birth Order",
|
||||
"moreInfo": "More Info",
|
||||
"tabBasic": "Basic",
|
||||
"tabFamily": "Family",
|
||||
"tabWork": "Career",
|
||||
"tabIntro": "Bio",
|
||||
"maritalStatus": "Marital Status",
|
||||
"fatherId": "Father ID",
|
||||
"motherName": "Mother Name",
|
||||
|
||||
@@ -161,12 +161,20 @@
|
||||
"genealogyName": "谱名",
|
||||
"commonName": "常用名",
|
||||
"personSearch": "族员搜索",
|
||||
"pedigreeChart": "谱系图",
|
||||
"ancestryChart": "祖谱系图",
|
||||
"pedigreeChart": "小系图",
|
||||
"ancestryChart": "全系图",
|
||||
"chartEmpty": "暂无族谱数据",
|
||||
"applyManage": "申请管理",
|
||||
"bindAccount": "绑定账号",
|
||||
"unbindAccount": "解除绑定",
|
||||
"unbindSuccessTip": "已解除与族员的绑定",
|
||||
"bindSuccessTip": "已把当前族员绑定到你的账号",
|
||||
"bindNoPersonTip": "请先选择要绑定的族员",
|
||||
"requestSentTip": "申请已提交,等待管理员审批",
|
||||
"alreadyManagerTip": "您已是本族谱管理员,无需申请",
|
||||
"addAncestor": "新增祖先",
|
||||
"editAncestor": "修改祖先",
|
||||
"ancestorRequired": "请先新增祖先",
|
||||
"spouseTitle": "夫妻记录",
|
||||
"addSpouse": "新增配偶",
|
||||
"pickSpouse": "选择成员",
|
||||
@@ -203,11 +211,42 @@
|
||||
"chipPublic": "公开",
|
||||
"chipPrivate": "私密",
|
||||
"searchPlaceholder": "搜索族谱名称",
|
||||
"listTitle": "有家族谱"
|
||||
"listTitle": "有家族谱",
|
||||
"tabOverview": "概览",
|
||||
"tabMembers": "成员",
|
||||
"tabPerms": "权限",
|
||||
"statMembers": "位族人",
|
||||
"statManagers": "管理员",
|
||||
"statPending": "待审核",
|
||||
"introTitle": "族谱简介",
|
||||
"quickActions": "快捷操作",
|
||||
"editProfile": "编辑资料",
|
||||
"edit": "编辑",
|
||||
"addMember": "添加族人",
|
||||
"selectMember": "选择当前族员",
|
||||
"deleteTree": "删除族谱",
|
||||
"deleteTreeHint": "会同时删除全部族人档案",
|
||||
"searchMember": "搜索姓名、谱名或成员编号",
|
||||
"filterAll": "全部",
|
||||
"filterMale": "男性",
|
||||
"filterFemale": "女性",
|
||||
"ancestorCard": "始祖",
|
||||
"ancestorInfo": "始祖信息",
|
||||
"noAncestor": "暂无始祖信息",
|
||||
"createdAt": "创建时间",
|
||||
"publicHint": "公开后他人可搜索并申请加入",
|
||||
"addChildBtn": "添加子女",
|
||||
"viewFullChart": "查看全系图",
|
||||
"editInfoTitle": "编辑族谱信息",
|
||||
"removeRelation": "解除关系",
|
||||
"removeRelationConfirm": "确定解除与「{name}」的关系吗?解除后立即保存生效",
|
||||
"dupMemberTip": "该成员已在关系中,请勿重复添加"
|
||||
},
|
||||
"FamilyPersonPage": {
|
||||
"title": "家族成员查询",
|
||||
"detailTitle": "成员详情",
|
||||
"editTitle": "修改成员",
|
||||
"editBut": "修改",
|
||||
"personId": "成员编号",
|
||||
"treeId": "族谱编号",
|
||||
"surname": "姓",
|
||||
@@ -223,6 +262,11 @@
|
||||
"zi": "字",
|
||||
"hao": "号",
|
||||
"birthOrder": "排行",
|
||||
"moreInfo": "更多资料",
|
||||
"tabBasic": "基本资料",
|
||||
"tabFamily": "家庭学业",
|
||||
"tabWork": "任职状态",
|
||||
"tabIntro": "简介",
|
||||
"maritalStatus": "婚姻状况",
|
||||
"fatherId": "父亲编号",
|
||||
"motherName": "母亲姓名",
|
||||
|
||||
@@ -58,13 +58,13 @@
|
||||
|
||||
- 组件 `components/admin-pick-popup/admin-pick-popup.vue`:`u-popup mode="bottom"`(80vh)+ `scroll-view` 手动分页(**不能用 z-paging**:它默认 `position:fixed` 撑满视口,在弹层内会打架);
|
||||
- Props `visible / title / columns / loader / rowKey / titleKey / multiple / fixedFilters / pageSize`,Emits `update:visible / close / confirm(rows)`;
|
||||
- 数据源在 `composables/usePickerConfigs.ts`:每个被选数据一个工厂函数(`usePermissionPickConfig`、`useDependentPickConfig`、`useActorPickConfig`、`useGroupPickConfig`、`useUserPickConfig`、`useFunctionPickConfig`、`useHelpFeaturePickConfig`),返回 `{ columns, loader, rowKey, titleKey, fixedFilters }`;
|
||||
- 数据源在 `composables/usePickerConfigs.ts`:每个被选数据一个工厂函数(`usePermissionPickConfig`、`useDependentPickConfig`、`useActorPickConfig`、`useGroupPickConfig`、`useUserPickConfig`、`useFunctionPickConfig`、`useHelpFeaturePickConfig`、`usePersonPickConfig`),返回 `{ columns, loader, rowKey, titleKey, fixedFilters }`;
|
||||
- 调用方只需 `const xxxConfig = useXxxPickConfig(); const showXxxPicker = ref(false);`,点入口置 `true`,`@confirm` 里做原来的去重合并逻辑。需要动态过滤(如扩展权限按 `dependent`)时写 `xxxConfig.fixedFilters.value = { dependent }`;
|
||||
- 单选点行即 `confirm` + 关闭;多选勾选后点确定;X / 遮罩 / 取消三条退路都通。
|
||||
- 单选/多选同构:点行高亮选中(单选圆圈、多选方勾,再点取消),底部「取消/确定」栏常驻,确定才 `confirm` + 关闭;多选另有全选钮;X / 遮罩 / 取消三条退路都通。
|
||||
|
||||
**security 域的 enquiry 页因此回归纯列表页**:`isPick` / `pickTarget` / `onLoad(channel)` / `deliver()` 全部删除,`:show-nav-back="true"` 恒定,`+` 新增入口不再被选择器态隐藏。
|
||||
|
||||
`useAdminPick` / `useAdminPickTarget` 机制本身**保留**:`family-person-enquiry` 仍作为「族员选择器」被首页 dashboard 以 `?channel=person` 调起。那条老路子的协议参数一律不做 `encodeURIComponent`(H5 侧 uni/vue-router 拼 URL 用 `encodeURI`,预编码会把 `%` 再编成 `%25`)。
|
||||
`useAdminPick` / `useAdminPickTarget` 机制目前**无任何调用方**:最后一个跳页选择(族谱首页「族员搜索 / 选配偶」带 `?channel=person` 调 `family-person-enquiry`)已改为调用方内嵌 `<admin-pick-popup>`(数据源 `usePersonPickConfig`),enquiry 页侧协议代码同步移除。机制文件暂保留备用;若日后恢复跳页选择,注意那条老路子的协议参数一律不做 `encodeURIComponent`(H5 侧 uni/vue-router 拼 URL 用 `encodeURI`,预编码会把 `%` 再编成 `%25`)。
|
||||
|
||||
> 命名订正:`actor-enquiry` 就是旧 `ActorEnquiry.vue` 的移植,早期草稿里的 `actor-pick` 是造错的概念——它现在只是普通查询页,选择能力在调用方的弹窗里。
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
* 保留:四条筛选(谱名/常用名/族谱编号 文本包含、性别 单选枚举)、每页 20 条、行点击进详情。
|
||||
* 旧页的新增/删除按钮本就被注释禁用,故这里不出新增入口、也不做批量删除,行为对齐。
|
||||
*
|
||||
* 兼作「族员选择器」:首页 dashboard 详情态的「族员搜索」带 ?channel=person 跳入,
|
||||
* 单选回传后调用方按 PersonId 切换当前族员(useAdminPick / useAdminPickTarget 协议)。
|
||||
* 本页不再承担选择器职责:族谱首页的「族员搜索 / 选配偶」已改为调用方内嵌
|
||||
* <admin-pick-popup> 底部弹窗(数据源 composables/usePickerConfigs 的 usePersonPickConfig),
|
||||
* 遗留的 useAdminPickTarget 协议代码已移除,与其他 enquiry 页同构。
|
||||
*/
|
||||
import type { AdminColumn, AdminQuery, AdminRow } from "@/admin/admin-types";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue";
|
||||
import { fillCriteria, makeFilterOption, toSelectOption } from "@/admin/admin-filter";
|
||||
import { useAdminList, useAdminPickTarget, useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { useAdminList, useAuthGuard, useThemeGradient, useThemeTokens } from "@/composables";
|
||||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||||
import { FamilyPersonEntity } from "@/data/auto/familyPersonEntity";
|
||||
import { FamilyPersonSearchCriteria } from "@/data/auto/familyPersonSearchCriteria";
|
||||
@@ -24,7 +24,6 @@ import familyPersonService from "@/service/master/familyPersonService";
|
||||
|
||||
const { t, currentLocale } = useLocale();
|
||||
const { getPolicyOptions, getPolicyText } = usePolicyOptions();
|
||||
const pickTarget = useAdminPickTarget();
|
||||
|
||||
// 统一风格接线(与族谱首页/媒体/共享页同套):主题渐变底 + 强调色 CSS 变量下发
|
||||
const { themeBackground } = useThemeGradient();
|
||||
@@ -36,9 +35,6 @@ const themeVars = computed(() => ({
|
||||
"--fg-shadow": accentShadow.value,
|
||||
}));
|
||||
|
||||
/** 选择器模式:带 channel 进来时点行回传而非跳详情 */
|
||||
const isPick = ref(false);
|
||||
|
||||
const LABEL_KEYS: Record<string, string> = {
|
||||
[FamilyPersonEntity.DB_GENEALOGY_NAME]: "FamilyPersonPage.genealogyName",
|
||||
[FamilyPersonEntity.DB_COMMON_NAME]: "FamilyPersonPage.commonName",
|
||||
@@ -133,26 +129,11 @@ function onSearch(): void {
|
||||
}
|
||||
|
||||
function onRowTap(row: AdminRow): void {
|
||||
if (isPick.value) {
|
||||
if (pickTarget.multiple.value) {
|
||||
return;
|
||||
}
|
||||
pickTarget.deliverOne(row);
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `${pageUrl("familyPersonOperation")}?id=${row.PersonId}&mode=view`,
|
||||
});
|
||||
}
|
||||
|
||||
function onConfirm(rows: AdminRow[]): void {
|
||||
if (!rows.length) {
|
||||
uni.showToast({ title: t("AdminPage.pickNoneTip"), icon: "none" });
|
||||
return;
|
||||
}
|
||||
pickTarget.deliver(rows);
|
||||
}
|
||||
|
||||
watch(currentLocale, () => {
|
||||
for (const option of bindOptions) {
|
||||
const key = LABEL_KEYS[option.BindValue];
|
||||
@@ -165,11 +146,6 @@ watch(currentLocale, () => {
|
||||
}
|
||||
});
|
||||
|
||||
onLoad((options) => {
|
||||
pickTarget.readOptions(options);
|
||||
isPick.value = String(options?.channel ?? "").trim().length > 0;
|
||||
});
|
||||
|
||||
useAuthGuard(() => {
|
||||
// 每次显示都重拉第一页:详情修改后返回能看到最新数据(navigateBack 不重新挂页面)
|
||||
nextTick(() => listRef.value?.refresh());
|
||||
@@ -179,16 +155,12 @@ useAuthGuard(() => {
|
||||
<template>
|
||||
<view class="page" :style="[{ background: themeBackground }, themeVars]">
|
||||
<admin-list ref="listRef" v-model:keyword="keyword" :nav-title="t('FamilyPersonPage.title')"
|
||||
:show-nav-back="!isPick" :columns="columns" :loader="state.query" :row-key="FamilyPersonEntity.PERSON_ID"
|
||||
title-key="GenealogyName" :selectable="isPick && pickTarget.multiple.value" :filter-count="filterCount"
|
||||
:filter-visible="filterVisible" :filter-items="draft" :search-token="searchToken"
|
||||
:keep-selection-on-reload="isPick" @search="onSearch" @open-filter="state.openFilter()"
|
||||
@close-filter="state.closeFilter()" @apply-filter="state.applyFilter($event)"
|
||||
@clear-filter="state.clearFiltersAndSearch()" @row-tap="onRowTap">
|
||||
<template v-if="isPick" #actions="{ rows }">
|
||||
<u-button type="primary" shape="circle" size="mini" :text="t('AdminPage.pickDoneBut')"
|
||||
@click="onConfirm(rows)" />
|
||||
</template>
|
||||
:show-nav-back="true" :columns="columns" :loader="state.query" :row-key="FamilyPersonEntity.PERSON_ID"
|
||||
title-key="GenealogyName" :filter-count="filterCount" :filter-visible="filterVisible"
|
||||
:filter-items="draft" :search-token="searchToken" @search="onSearch"
|
||||
@open-filter="state.openFilter()" @close-filter="state.closeFilter()"
|
||||
@apply-filter="state.applyFilter($event)" @clear-filter="state.clearFiltersAndSearch()"
|
||||
@row-tap="onRowTap">
|
||||
</admin-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -535,8 +535,8 @@ useAuthGuard(() => {
|
||||
</template>
|
||||
|
||||
<option-picker :visible="genderPickerVisible" :title="t('FamilyPersonPage.gender')" :items="genderItems"
|
||||
:empty-text="t('FamilyPersonPage.genderEmpty')" @close="genderPickerVisible = false"
|
||||
@confirm="onGenderConfirm" />
|
||||
:empty-text="t('FamilyPersonPage.genderEmpty')" :confirm-text="t('AdminPage.confirmBut')"
|
||||
@close="genderPickerVisible = false" @confirm="onGenderConfirm" />
|
||||
</template>
|
||||
</admin-form>
|
||||
</template>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const PAGE_PATHS = {
|
||||
helpFeatureEnquiry: "pages/security/help-feature-enquiry",
|
||||
helpFeatureOperation: "pages/security/help-feature-operation",
|
||||
// 族谱管理已融合进首页 dashboard 两态页,旧 family-tree-enquiry/operation 两页已退役(不在 PAGE_PATHS 登记)
|
||||
// 家族成员(短地址 /family-person-enquiry、/family-person-operation):旧 master/FamilyPerson 两页迁移,含族员选择器
|
||||
// 家族成员(短地址 /family-person-enquiry、/family-person-operation):旧 master/FamilyPerson 两页迁移;族员选择已改为调用方内嵌弹窗
|
||||
familyPersonEnquiry: "pages/security/family-person-enquiry",
|
||||
familyPersonOperation: "pages/security/family-person-operation",
|
||||
// 底部导航 tab 页(首页复用 dashboard):先各建一个简单测试页占位
|
||||
|
||||
@@ -86,6 +86,26 @@ class FamilyTreeService{
|
||||
)
|
||||
};
|
||||
|
||||
async BindSelfPerson(data: any) {
|
||||
return request<FamilyTreeData>(
|
||||
{
|
||||
url: "/FamilyTree/BindSelfPerson",
|
||||
method: "post",
|
||||
data,
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
async UnbindSelfPerson(data: string) {
|
||||
return request<FamilyTreeData>(
|
||||
{
|
||||
url: "/FamilyTree/UnbindSelfPerson?treeId="+data,
|
||||
method: "post",
|
||||
data,
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
async Update(data: FamilyTreeData) {
|
||||
return request<FamilyTreeData>(
|
||||
{
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M978.944 609.28C926.72 565.76 890.88 511.488 890.88 475.136c0-27.648 22.528-55.808 63.488-80.384 8.704-5.632 12.8-15.872 10.752-25.6-25.088-107.008-98.816-355.84-257.536-368.64-81.92-6.144-158.72 45.568-220.672 152.064-21.504 37.376-41.984 81.408-59.392 130.56-35.328-53.76-76.8-82.944-122.88-86.528-45.568-4.096-113.152 13.824-174.08 118.784-38.912 66.56-70.656 161.792-90.112 268.288-11.264 61.952 0.512 118.272 33.28 161.28 38.4 49.664 104.448 81.408 184.32 88.576v137.216c0 12.8 10.24 23.04 23.04 23.04 12.288 0 23.04-10.752 23.04-23.552V834.56c30.72-1.024 59.904-6.144 87.04-14.336 35.84-11.776 66.048-30.72 89.088-54.272 46.592 29.184 104.96 47.104 171.52 52.736h3.584v182.272c0 12.8 10.24 23.04 23.04 23.04s23.04-10.24 23.04-23.04v-181.248c57.344-1.536 111.616-13.312 156.672-34.816 68.096-32.768 112.64-85.504 128.512-152.576 2.048-8.192-0.512-17.408-7.68-23.04zM304.64 788.48v-258.048c0-12.8-10.24-23.04-23.04-23.04s-23.04 10.24-23.04 23.04v78.336L199.68 577.536c-11.264-6.144-25.6-1.536-31.232 9.728-6.144 11.264-1.536 25.6 9.728 31.232l79.872 42.496v126.464c-65.536-6.656-118.272-31.232-147.456-70.144-25.6-32.768-33.792-75.264-25.088-124.928 30.208-166.912 103.936-357.888 215.04-348.672 48.64 4.096 85.504 55.296 107.52 98.816-14.848 49.152-27.648 102.4-37.376 157.696-14.336 79.872 0.512 150.528 42.496 205.312 9.216 11.776 18.944 22.528 30.208 32.256-31.744 30.208-81.408 48.64-138.752 50.688z m397.312-14.848V504.832c3.072-1.536 5.632-3.072 7.68-5.632l116.736-132.096c8.704-9.728 7.168-24.064-2.048-32.768-9.728-8.704-24.064-7.168-32.768 2.048l-89.6 101.376V273.408c0-12.8-10.24-23.04-23.04-23.04s-23.04 10.24-23.04 23.04v338.944l-79.872-95.744c-8.704-10.24-23.04-11.776-32.768-3.584-10.24 8.704-11.776 23.04-3.584 32.768l104.96 125.952c2.56 3.584 6.656 6.144 10.752 7.168v93.184c-66.56-5.12-122.88-24.576-164.864-55.296l-0.512-0.512c-0.512 0-0.512-0.512-1.024-0.512-15.36-11.264-28.672-24.064-39.936-38.4-33.792-44.544-45.056-101.888-33.28-168.96 38.912-215.04 134.144-461.312 277.504-461.312h10.24c86.528 7.168 164.352 125.44 211.968 317.44-46.592 32.256-71.168 70.144-71.168 111.616 0 47.616 34.816 108.032 93.184 159.232-30.208 93.696-132.608 135.68-235.52 138.24z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M740.11 410.02c-21.24-50.2-50.79-105.07-87.83-163.08-62.83-98.38-126.61-173.13-127.24-173.87a16.978 16.978 0 0 0-12.92-5.94h-0.25c-5.05 0.07-9.81 2.39-12.99 6.32-0.66 0.82-66.61 82.76-130.48 184.64-86.93 138.66-128.96 246.79-124.92 321.38 3.2 59.05 22.96 114.54 57.14 160.49 34.38 46.22 80.78 79.33 134.18 95.75 8.98 2.76 18.5-2.28 21.26-11.26 2.76-8.98-2.28-18.5-11.26-21.26-46.44-14.28-86.86-43.17-116.89-83.54-30.18-40.57-47.63-89.68-50.47-142.03-3.6-66.44 37.75-170.59 119.59-301.18 46.11-73.57 93.32-136.57 115.42-165.19 21.68 26.75 66.76 84.46 111.38 154.37 56.08 87.87 122.92 213.49 122.92 312.92 0 52.11-16.72 101.44-48.34 142.67-30.64 39.94-74.02 69.45-122.16 83.1-9.04 2.56-14.29 11.96-11.72 21 2.12 7.49 8.94 12.37 16.35 12.37 1.54 0 3.1-0.21 4.65-0.65 55.14-15.63 104.81-49.41 139.87-95.12 36.22-47.22 55.37-103.72 55.37-163.38 0.01-48.04-13.67-104.74-40.66-168.51z"/><path d="M625.23 639.73c6.66-6.62 6.69-17.39 0.07-24.05s-17.39-6.69-24.05-0.07L529 687.42V512.1c1.68-0.74 3.27-1.75 4.69-3.07 0.23-0.21 23.16-21.47 45.79-43.28 13.37-12.88 24.06-23.46 31.76-31.45 14.72-15.26 19-20.9 19-28.86 0-9.39-7.61-17.01-17.01-17.01-7.04 0-13.09 4.28-15.67 10.38-6.09 8.08-36.6 37.96-68.57 68.04V345.44c0-9.39-7.61-17.01-17.01-17.01-9.39 0-17.01 7.61-17.01 17.01v229.79l-84.02-50.99c-8.03-4.88-18.49-2.31-23.36 5.72-4.87 8.03-2.31 18.49 5.72 23.36l101.67 61.7v324.85c0 9.39 7.61 17.01 17.01 17.01 9.39 0 17.01-7.61 17.01-17.01v-206.1c1.85-0.82 3.6-1.97 5.12-3.48l91.11-90.56z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Reference in new issue
Block a user