ow
This commit is contained in:
1 parent
b26bcfcd3b
commit
8285fb4090
9 files changed
+1370
-18
No files matched your search
@@ -25,6 +25,39 @@ namespace CloudBuilder.Master.Service
|
|||||||
this.service = service;
|
this.service = service;
|
||||||
repository = service.GetRepository<IRepository<FamilyTreeEntity>>();
|
repository = service.GetRepository<IRepository<FamilyTreeEntity>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
|
||||||
|
public FamilyTreeData FindAllByTreeId(string treeId)
|
||||||
|
{
|
||||||
|
FamilyTreeData data = new FamilyTreeData();
|
||||||
|
if (string.IsNullOrEmpty(treeId)) return data;
|
||||||
|
|
||||||
|
FamilyTreeEntity d = repository.DetachedEntities.Where(x => x.TreeId == treeId).FirstOrDefault();
|
||||||
|
|
||||||
|
if (d == null) return data;
|
||||||
|
|
||||||
|
data.FamilyTree = new FamilyTreeDisplayData(d);
|
||||||
|
data.CurrentPerson = new FamilyPersonData();
|
||||||
|
|
||||||
|
IRepository<FamilyPersonEntity> repositoryFamilyPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
|
||||||
|
|
||||||
|
var currentPerson = d.AncestorPersonId;
|
||||||
|
|
||||||
|
data.CurrentPerson.FamilyPerson = new FamilyPersonDisplayData(repositoryFamilyPerson.DetachedEntities.Where(x => x.PersonId == currentPerson).FirstOrDefault()!);
|
||||||
|
|
||||||
|
data.FamilyTree.AncestorPersonGenealogyName = data.CurrentPerson.FamilyPerson.GenealogyName;
|
||||||
|
|
||||||
|
IRepository<FamilyTreeChildrenMapViewEntity> repositoryChildrenMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>();
|
||||||
|
|
||||||
|
IRepository<FamilyTreeSpouseMapViewEntity> repositorySpouseMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>();
|
||||||
|
|
||||||
|
data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>().DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray();
|
||||||
|
|
||||||
|
data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
|
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
|
||||||
public FamilyTreeData FindByKey(string key)
|
public FamilyTreeData FindByKey(string key)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace CloudBuilder.Master.Service
|
|||||||
{
|
{
|
||||||
public interface IFamilyTreeService : IServiceBase<FamilyTreeData, string, FamilyTreeSearchCriteria>
|
public interface IFamilyTreeService : IServiceBase<FamilyTreeData, string, FamilyTreeSearchCriteria>
|
||||||
{
|
{
|
||||||
|
FamilyTreeData FindAllByTreeId(string treeId);
|
||||||
FamilyTreeData FindByCurrentPerson(string key, string personId);
|
FamilyTreeData FindByCurrentPerson(string key, string personId);
|
||||||
FamilyTreeResultData FindBySearchCriteria(FamilyTreeSearchCriteria tc);
|
FamilyTreeResultData FindBySearchCriteria(FamilyTreeSearchCriteria tc);
|
||||||
dynamic PrintBySearchCriteria(FamilyTreeSearchCriteria tc);
|
dynamic PrintBySearchCriteria(FamilyTreeSearchCriteria tc);
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public FamilyTreeRequest(IServiceRequest serviceRequest)
|
|||||||
this.serviceRequest = serviceRequest;
|
this.serviceRequest = serviceRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FamilyTreeData FindAllByTreeId(string treeId)
|
||||||
|
{
|
||||||
|
return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeData>("/FamilyTree/FindAllByTreeId?treeId=" + treeId.ToString())).GetAwaiter().GetResult(); }
|
||||||
|
|
||||||
public FamilyTreeData FindByKey(string key)
|
public FamilyTreeData FindByKey(string key)
|
||||||
{
|
{
|
||||||
return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeData>("/FamilyTree/FindByKey?key=" + key.ToString())).GetAwaiter().GetResult(); }
|
return Task.Run(() => serviceRequest.PostAsJsonAsync<FamilyTreeData>("/FamilyTree/FindByKey?key=" + key.ToString())).GetAwaiter().GetResult(); }
|
||||||
|
|||||||
@@ -26,6 +26,26 @@ public FamilyTreeController(IApplicationService service, IFamilyTreeService fami
|
|||||||
{
|
{
|
||||||
this.service = service;
|
this.service = service;
|
||||||
this.familyTreeService= familyTreeService;
|
this.familyTreeService= familyTreeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[PermissionOperate(name: "FamilyTree", operate: "Find")]
|
||||||
|
[HttpPost]
|
||||||
|
public dynamic FindAllByTreeId(string treeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var result = familyTreeService.FindAllByTreeId(treeId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (ValidatedException vex)
|
||||||
|
{
|
||||||
|
return vex;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ServiceErrorException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[PermissionOperate(name: "FamilyTree", operate: "Find")]
|
[PermissionOperate(name: "FamilyTree", operate: "Find")]
|
||||||
|
|||||||
Generated
+835
-12
File diff suppressed because it is too large.
Load diff
+5
-2
@@ -10,6 +10,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
|
"@antv/g6": "^5.1.1",
|
||||||
|
"@babel/runtime": "^7.24.0",
|
||||||
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.12.11",
|
"@types/node": "^20.12.11",
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"gsap": "^3.12.5",
|
"gsap": "^3.12.5",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
@@ -36,8 +40,7 @@
|
|||||||
"vue-router": "^4.0.13",
|
"vue-router": "^4.0.13",
|
||||||
"vue3-photo-preview": "^0.3.0",
|
"vue3-photo-preview": "^0.3.0",
|
||||||
"vue3-video-play": "^1.3.1",
|
"vue3-video-play": "^1.3.1",
|
||||||
"wow.js": "^1.2.2",
|
"wow.js": "^1.2.2"
|
||||||
"@simplewebauthn/browser": "^13.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/howler": "^2.2.11",
|
"@types/howler": "^2.2.11",
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
<template>
|
||||||
|
<dragModal v-model:open="visibleModel" width="85%" :title="title" :mask-closable="false" :fullscreen="fullscreen"
|
||||||
|
@cancel="onCancel">
|
||||||
|
<div ref="containerRef" v-if="hasPerson" class="family-chart-container"></div>
|
||||||
|
<a-empty v-else description="暂无当前族员数据" />
|
||||||
|
</dragModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||||
|
import { Graph } from "@antv/g6";
|
||||||
|
import dragModal from "@/components/basic/DragModal.vue";
|
||||||
|
import type { FamilyTreeData } from "@/data/auto/familyTreeData";
|
||||||
|
import { useLocalePolicy } from "@/hooks/useLocalePolicy";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: { type: Boolean, default: false },
|
||||||
|
data: { type: Object as () => FamilyTreeData, required: true },
|
||||||
|
title: { type: String, default: "谱系图" },
|
||||||
|
fullscreen: { type: Boolean, default: true },
|
||||||
|
loadedNodes: { type: Object, default: () => ({}) },
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["update:open", "cancel", "select-child"]);
|
||||||
|
|
||||||
|
const visibleModel = computed({
|
||||||
|
get: () => props.open,
|
||||||
|
set: (v: boolean) => emit("update:open", v),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 已展开的节点 personId 集合 */
|
||||||
|
const expandedIds = ref<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const { getPolicyOptions } = useLocalePolicy();
|
||||||
|
const genderOptions = getPolicyOptions("GenderPolicy", false);
|
||||||
|
const relationTypeOptions = getPolicyOptions("RelationTypePolicy", false);
|
||||||
|
|
||||||
|
interface PolicyOption {
|
||||||
|
BindValue: string;
|
||||||
|
DisplayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPolicyText = (options: PolicyOption[], value?: string | null) => {
|
||||||
|
const o = options.find((x) => x.BindValue === value);
|
||||||
|
return o ? o.DisplayName : value ?? "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const genderLabel = (g?: string) => getPolicyText(genderOptions, g);
|
||||||
|
const relationLabel = (r?: string) => getPolicyText(relationTypeOptions, r);
|
||||||
|
|
||||||
|
const isMale = (g?: string) => {
|
||||||
|
const t = (genderLabel(g) ?? "").toLowerCase();
|
||||||
|
return t.includes("男") || t.includes("male") || g === "M" || g === "1";
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasPerson = computed(() => !!props.data?.CurrentPerson?.FamilyPerson);
|
||||||
|
|
||||||
|
const rootPersonId = computed(() => props.data?.CurrentPerson?.FamilyPerson?.PersonId ?? "");
|
||||||
|
|
||||||
|
/** HTML 字符串转义,防止人名中的特殊字符破坏卡片结构 */
|
||||||
|
const esc = (v?: string | null) =>
|
||||||
|
(v ?? "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
|
||||||
|
/** 自定义人员卡片 HTML */
|
||||||
|
const renderCard = (d: any) => {
|
||||||
|
const data = d?.data ?? {};
|
||||||
|
const male = isMale(data.gender);
|
||||||
|
// 本人=橙色,男性=蓝色,女性=粉色
|
||||||
|
const theme = data.kind === "self" ? "#fa8c16" : male ? "#1890ff" : "#eb2f96";
|
||||||
|
const ico = male ? "♂" : "♀";
|
||||||
|
const common = data.commonName
|
||||||
|
? `<div class="ftc-sub">常用名:${esc(data.commonName)}</div>`
|
||||||
|
: "";
|
||||||
|
const actionTag = data.hasChildren
|
||||||
|
? `<div class="ftc-tag" style="color:#13c2c2; background:#13c2c21a;">${data.expanded ? "点击折叠" : "点击展开"}</div>`
|
||||||
|
: !data.loaded
|
||||||
|
? `<div class="ftc-tag" style="color:#722ed1; background:#722ed11a;">点击查看</div>`
|
||||||
|
: "";
|
||||||
|
const clickable = data.hasChildren || !data.loaded ? "ftc-clickable" : "";
|
||||||
|
return `
|
||||||
|
<div class="ftc-card ${clickable}" style="border-top: 3px solid ${theme};">
|
||||||
|
<div class="ftc-head">
|
||||||
|
<span class="ftc-ico" style="color:${theme};">${ico}</span>
|
||||||
|
<span class="ftc-name" title="${esc(data.name)}">${esc(data.name)}</span>
|
||||||
|
</div>
|
||||||
|
${common}
|
||||||
|
<div class="ftc-footer">
|
||||||
|
<div class="ftc-tag" style="color:${theme}; background:${theme}1a;">${esc(data.tag)}</div>
|
||||||
|
${actionTag}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 每个族员的描述:人员信息 + 配偶 + 孩子(根来自 data,其他来自 loadedNodes) */
|
||||||
|
const describePerson = (personId: string): any => {
|
||||||
|
if (personId === rootPersonId.value) {
|
||||||
|
const dp = props.data?.CurrentPerson;
|
||||||
|
if (!dp?.FamilyPerson) return null;
|
||||||
|
return {
|
||||||
|
person: dp.FamilyPerson,
|
||||||
|
spouses: dp.FamilyTreeSpouseMaps ?? [],
|
||||||
|
children: dp.FamilyTreeChildrenMaps ?? [],
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const loaded = (props.loadedNodes as Record<string, any>)?.[personId];
|
||||||
|
if (loaded) {
|
||||||
|
return {
|
||||||
|
person: loaded.person,
|
||||||
|
spouses: loaded.spouses ?? [],
|
||||||
|
children: loaded.children ?? [],
|
||||||
|
loaded: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 构建图数据:树形竖向排列,本人默认展开孩子,点击孩子可递归加载子树 */
|
||||||
|
const buildGraphData = () => {
|
||||||
|
const nodes: any[] = [];
|
||||||
|
const edges: any[] = [];
|
||||||
|
const root = rootPersonId.value;
|
||||||
|
if (!root) return { nodes, edges };
|
||||||
|
|
||||||
|
const Y_STEP = 116;
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
const walk = (personId: string, depth: number, xBase: number) => {
|
||||||
|
const desc = describePerson(personId);
|
||||||
|
if (!desc) return;
|
||||||
|
|
||||||
|
const isRoot = personId === root;
|
||||||
|
const expanded = expandedIds.value.has(personId);
|
||||||
|
const hasChildren = desc.children.length > 0;
|
||||||
|
const p = desc.person;
|
||||||
|
const x = xBase;
|
||||||
|
const y = cursor;
|
||||||
|
cursor += Y_STEP;
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: "n-" + personId,
|
||||||
|
style: { x, y },
|
||||||
|
data: {
|
||||||
|
kind: isRoot ? "self" : "member",
|
||||||
|
personId,
|
||||||
|
tag: isRoot ? "本人" : "族员",
|
||||||
|
name: (p?.Surname ?? "") + (p?.GenealogyName ?? ""),
|
||||||
|
commonName: p?.CommonName,
|
||||||
|
gender: p?.Gender,
|
||||||
|
hasChildren,
|
||||||
|
expanded,
|
||||||
|
loaded: desc.loaded,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 配偶与本人同一行,水平间隔 200 对齐
|
||||||
|
const SPOUSE_GAP = 200;
|
||||||
|
desc.spouses.forEach((s: any, i: number) => {
|
||||||
|
const sid = "s-" + personId + "-" + i;
|
||||||
|
nodes.push({
|
||||||
|
id: sid,
|
||||||
|
style: { x: x + 210 + i * SPOUSE_GAP, y },
|
||||||
|
data: {
|
||||||
|
kind: "spouse",
|
||||||
|
tag: "配偶",
|
||||||
|
name: (s.SpouseSurname ?? "") + (s.SpouseGenealogyName ?? ""),
|
||||||
|
gender: s.Gender,
|
||||||
|
loaded: true,
|
||||||
|
hasChildren: false,
|
||||||
|
expanded: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
edges.push({ source: "n-" + personId, target: sid, data: { kind: "spouse" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
// 孩子列与本人第一行的配偶列对齐(x + 210),即孩子位于配偶正下方,竖直跟第一行对齐
|
||||||
|
const childBaseX = x + 210;
|
||||||
|
|
||||||
|
desc.children.forEach((c: any) => {
|
||||||
|
const childId = c.ChildrenId;
|
||||||
|
if (describePerson(childId)) {
|
||||||
|
walk(childId, depth + 1, childBaseX);
|
||||||
|
} else {
|
||||||
|
nodes.push({
|
||||||
|
id: "n-" + childId,
|
||||||
|
style: { x: childBaseX, y: cursor },
|
||||||
|
data: {
|
||||||
|
kind: "child",
|
||||||
|
personId: childId,
|
||||||
|
tag: c.RelationType ? relationLabel(c.RelationType) : "子女",
|
||||||
|
name: (c.ChildrenSurname ?? "") + (c.ChildrenGenealogyName ?? ""),
|
||||||
|
commonName: c.CommonName,
|
||||||
|
gender: c.Gender,
|
||||||
|
hasChildren: false,
|
||||||
|
expanded: false,
|
||||||
|
loaded: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cursor += Y_STEP;
|
||||||
|
}
|
||||||
|
edges.push({ source: "n-" + personId, target: "n-" + childId, data: { kind: "parent" } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(root, 0, 0);
|
||||||
|
return { nodes, edges };
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement>();
|
||||||
|
let graph: any = null;
|
||||||
|
|
||||||
|
const destroyGraph = () => {
|
||||||
|
if (graph) {
|
||||||
|
graph.destroy();
|
||||||
|
graph = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initGraph = async () => {
|
||||||
|
destroyGraph();
|
||||||
|
await nextTick();
|
||||||
|
// 等待弹窗动画完成,确保容器有实际尺寸
|
||||||
|
await new Promise((r) => requestAnimationFrame(r));
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
|
const { nodes, edges } = buildGraphData();
|
||||||
|
if (!nodes.length) return;
|
||||||
|
|
||||||
|
graph = new Graph({
|
||||||
|
container: containerRef.value,
|
||||||
|
autoFit: "view",
|
||||||
|
data: { nodes, edges },
|
||||||
|
node: {
|
||||||
|
type: "html",
|
||||||
|
style: {
|
||||||
|
size: [180, 96],
|
||||||
|
dx: -90,
|
||||||
|
dy: -48,
|
||||||
|
// 官方文档推荐函数形式,类型声明仅支持 string/HTMLElement,故断言
|
||||||
|
innerHTML: renderCard as any,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
type: (d: any) => (d?.data?.kind === "spouse" ? "line" : "polyline"),
|
||||||
|
style: (d: any) => ({
|
||||||
|
// 夫妻关系=紫色虚线,亲子关系=灰色实线
|
||||||
|
stroke: d?.data?.kind === "spouse" ? "#722ed1" : "#a0a0a0",
|
||||||
|
lineWidth: 1.5,
|
||||||
|
lineDash: d?.data?.kind === "spouse" ? [4, 4] : undefined,
|
||||||
|
router: d?.data?.kind === "spouse" ? undefined : { type: "orth" },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
behaviors: ["drag-canvas", "zoom-canvas", "drag-element"],
|
||||||
|
});
|
||||||
|
graph.on("node:click", (event: any) => {
|
||||||
|
const id = event.target?.id ?? event.item?.getID?.();
|
||||||
|
if (typeof id !== "string" || !id.startsWith("n-")) return;
|
||||||
|
const personId = id.slice(2);
|
||||||
|
const isRoot = personId === rootPersonId.value;
|
||||||
|
if (isRoot) {
|
||||||
|
const children = props.data?.CurrentPerson?.FamilyTreeChildrenMaps ?? [];
|
||||||
|
if (children.length > 0) {
|
||||||
|
toggleExpanded(personId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (describePerson(personId)) {
|
||||||
|
toggleExpanded(personId);
|
||||||
|
} else {
|
||||||
|
emit("select-child", personId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await graph.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExpanded = (personId: string) => {
|
||||||
|
const set = new Set(expandedIds.value);
|
||||||
|
if (set.has(personId)) set.delete(personId);
|
||||||
|
else set.add(personId);
|
||||||
|
expandedIds.value = set;
|
||||||
|
initGraph();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(visibleModel, async (v) => {
|
||||||
|
if (v) {
|
||||||
|
// 第一个人(当前族员)默认展开孩子
|
||||||
|
expandedIds.value = new Set([rootPersonId.value].filter(Boolean));
|
||||||
|
await initGraph();
|
||||||
|
} else {
|
||||||
|
destroyGraph();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
async () => {
|
||||||
|
if (visibleModel.value) {
|
||||||
|
expandedIds.value = new Set([rootPersonId.value].filter(Boolean));
|
||||||
|
await initGraph();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.loadedNodes,
|
||||||
|
async (val) => {
|
||||||
|
if (!val || !visibleModel.value) return;
|
||||||
|
// 新加载的族员自动展开,方便查看其子树
|
||||||
|
const set = new Set(expandedIds.value);
|
||||||
|
Object.keys(val as Record<string, any>).forEach((k) => set.add(k));
|
||||||
|
expandedIds.value = set;
|
||||||
|
await initGraph();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => destroyGraph());
|
||||||
|
|
||||||
|
const onCancel = () => emit("cancel");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.family-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 72vh;
|
||||||
|
min-height: 480px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* G6 HTML 节点卡片样式(注入到 canvas 容器,使用独立前缀避免污染全局) */
|
||||||
|
.ftc-card {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-ico {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ftc-clickable:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,16 @@ import { request } from "@/utils/request";
|
|||||||
|
|
||||||
class FamilyTreeService{
|
class FamilyTreeService{
|
||||||
|
|
||||||
|
async FindAllByTreeId(data: string) {
|
||||||
|
return request<FamilyTreeData>(
|
||||||
|
{
|
||||||
|
url: "/FamilyTree/FindAllByTreeId?treeId="+data,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
async FindByKey(data: string) {
|
async FindByKey(data: string) {
|
||||||
return request<FamilyTreeData>(
|
return request<FamilyTreeData>(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,6 +91,11 @@
|
|||||||
<a-input-search required :disabled="!isEdit" style="width: 50%;"
|
<a-input-search required :disabled="!isEdit" style="width: 50%;"
|
||||||
v-model:value="data.FamilyTree.AncestorPersonId"
|
v-model:value="data.FamilyTree.AncestorPersonId"
|
||||||
@search="onModalPerson" />
|
@search="onModalPerson" />
|
||||||
|
<a-input-search style="flex: 1; min-width: 0;"
|
||||||
|
v-model:value="data.CurrentPerson.FamilyPerson.PersonId"
|
||||||
|
@search="onModalCurrentPerson" />
|
||||||
|
<a-button type="primary" :disabled="!hasData" :icon="h(ApartmentOutlined)"
|
||||||
|
@click.prevent="">祖谱系图</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -114,7 +119,7 @@
|
|||||||
<a-divider orientation="left" orientation-margin="0px" dashed style="background-color: silver">
|
<a-divider orientation="left" orientation-margin="0px" dashed style="background-color: silver">
|
||||||
</a-divider>
|
</a-divider>
|
||||||
<a-row :gutter="[16, 8]">
|
<a-row :gutter="[16, 8]">
|
||||||
<a-col :lg="12" :sm="24">
|
<a-col :lg="18" :sm="24">
|
||||||
<a-row :wrap="false">
|
<a-row :wrap="false">
|
||||||
<a-col flex="auto">
|
<a-col flex="auto">
|
||||||
<a-input-group compact style="display: flex; width: 100%;">
|
<a-input-group compact style="display: flex; width: 100%;">
|
||||||
@@ -134,6 +139,8 @@
|
|||||||
<a-input-search style="flex: 1; min-width: 0;"
|
<a-input-search style="flex: 1; min-width: 0;"
|
||||||
v-model:value="data.CurrentPerson.FamilyPerson.PersonId"
|
v-model:value="data.CurrentPerson.FamilyPerson.PersonId"
|
||||||
@search="onModalCurrentPerson" />
|
@search="onModalCurrentPerson" />
|
||||||
|
<a-button type="primary" :disabled="!hasData" :icon="h(ApartmentOutlined)"
|
||||||
|
@click.prevent="onShowChart">谱系图</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -281,8 +288,6 @@
|
|||||||
</a-row>
|
</a-row>
|
||||||
<a-row v-else :gutter="[16, 8]">
|
<a-row v-else :gutter="[16, 8]">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button type="primary" :icon="h(FileSearchOutlined)"
|
|
||||||
:disabled="!hasData" >谱系</a-button>
|
|
||||||
<a-button type="primary" v-if="canAmend" :icon="h(EditOutlined)"
|
<a-button type="primary" v-if="canAmend" :icon="h(EditOutlined)"
|
||||||
:disabled="!hasData" @click.prevent="onAmend">修改</a-button>
|
:disabled="!hasData" @click.prevent="onAmend">修改</a-button>
|
||||||
<a-popconfirm :disabled="!hasData" title="确定是否删除这条记录?" ok-text="是"
|
<a-popconfirm :disabled="!hasData" title="确定是否删除这条记录?" ok-text="是"
|
||||||
@@ -307,6 +312,8 @@
|
|||||||
<FamilyPersonEnquiry ref="personEnquiry"
|
<FamilyPersonEnquiry ref="personEnquiry"
|
||||||
@on-row-dblclick="handleSelectPerson"></FamilyPersonEnquiry>
|
@on-row-dblclick="handleSelectPerson"></FamilyPersonEnquiry>
|
||||||
</dragModal>
|
</dragModal>
|
||||||
|
<FamilyTreeChartModal v-model:open="chartVisible" :data="data" :loaded-nodes="loadedNodes" title="谱系图"
|
||||||
|
@select-child="onChartSelectChild" />
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -315,11 +322,12 @@ import { notification } from "ant-design-vue";
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import type { Dayjs } from "dayjs";
|
import type { Dayjs } from "dayjs";
|
||||||
import { DeleteOutlined, EditOutlined, ClearOutlined, FileSearchOutlined, FileAddOutlined, UndoOutlined, SaveOutlined, PlusSquareTwoTone, DownSquareTwoTone, UpSquareTwoTone } from "@ant-design/icons-vue";
|
import { DeleteOutlined, EditOutlined, ClearOutlined, FileSearchOutlined, FileAddOutlined, UndoOutlined, SaveOutlined, PlusSquareTwoTone, DownSquareTwoTone, UpSquareTwoTone, ApartmentOutlined } from "@ant-design/icons-vue";
|
||||||
import { PermissionContext, usePermissionContext } from "@/hooks/PermissionContext"
|
import { PermissionContext, usePermissionContext } from "@/hooks/PermissionContext"
|
||||||
import dragModal from "@/components/basic/DragModal.vue"
|
import dragModal from "@/components/basic/DragModal.vue"
|
||||||
import FamilyTreeEnquiry from "./FamilyTreeEnquiry.vue";
|
import FamilyTreeEnquiry from "./FamilyTreeEnquiry.vue";
|
||||||
import FamilyPersonEnquiry from "./FamilyPersonEnquiry.vue";
|
import FamilyPersonEnquiry from "./FamilyPersonEnquiry.vue";
|
||||||
|
import FamilyTreeChartModal from "@/components/master/FamilyTreeChartModal.vue";
|
||||||
import { FamilyTreeData } from "@/data/auto/familyTreeData";
|
import { FamilyTreeData } from "@/data/auto/familyTreeData";
|
||||||
import { FamilyTreeSpouseMapViewEntity } from "@/data/auto/familyTreeSpouseMapViewEntity";
|
import { FamilyTreeSpouseMapViewEntity } from "@/data/auto/familyTreeSpouseMapViewEntity";
|
||||||
import { FamilyTreeChildrenMapViewEntity } from "@/data/auto/familyTreeChildrenMapViewEntity";
|
import { FamilyTreeChildrenMapViewEntity } from "@/data/auto/familyTreeChildrenMapViewEntity";
|
||||||
@@ -350,6 +358,7 @@ const canDelete = ref<boolean>(false);
|
|||||||
const canCreate = ref<boolean>(false);
|
const canCreate = ref<boolean>(false);
|
||||||
|
|
||||||
const showModalType = ref<number>(0);
|
const showModalType = ref<number>(0);
|
||||||
|
const chartVisible = ref<boolean>(false);
|
||||||
|
|
||||||
const data = ref<FamilyTreeData>(new FamilyTreeData());
|
const data = ref<FamilyTreeData>(new FamilyTreeData());
|
||||||
const hasData = ref<boolean>(false);
|
const hasData = ref<boolean>(false);
|
||||||
@@ -588,6 +597,48 @@ const onModalSearch = (() => {
|
|||||||
showModalType.value = 0;
|
showModalType.value = 0;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onShowChart = (() => {
|
||||||
|
if (!hasData.value) return;
|
||||||
|
chartVisible.value = true;
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 谱系图中已加载的族员节点(personId -> { person, spouses, children }),不清空原 data,只追加 */
|
||||||
|
const loadedNodes = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
const onChartSelectChild = (async (personId: string) => {
|
||||||
|
if (!personId || !treeId.value) return;
|
||||||
|
if (loadedNodes.value[personId]) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const childData = await familyTreeService.FindByCurrentPerson({
|
||||||
|
key: treeId.value.trim(),
|
||||||
|
personId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (childData?.CurrentPerson?.FamilyPerson) {
|
||||||
|
loadedNodes.value = {
|
||||||
|
...loadedNodes.value,
|
||||||
|
[personId]: {
|
||||||
|
person: childData.CurrentPerson.FamilyPerson,
|
||||||
|
spouses: childData.CurrentPerson.FamilyTreeSpouseMaps ?? [],
|
||||||
|
children: childData.CurrentPerson.FamilyTreeChildrenMaps ?? [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notification.info({
|
||||||
|
message: '系统提示:',
|
||||||
|
description: '未找到该族员的谱系数据',
|
||||||
|
placement: "topRight",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const onSearch = (async () => {
|
const onSearch = (async () => {
|
||||||
|
|
||||||
@@ -626,6 +677,7 @@ const onReset = (() => {
|
|||||||
data.value.CurrentPerson.FamilyTreeChildrenMaps = [];
|
data.value.CurrentPerson.FamilyTreeChildrenMaps = [];
|
||||||
treeId.value = '';
|
treeId.value = '';
|
||||||
hasData.value = false;
|
hasData.value = false;
|
||||||
|
loadedNodes.value = {};
|
||||||
selectedSonIndex.value = -1;
|
selectedSonIndex.value = -1;
|
||||||
selectedParentIndex.value = -1;
|
selectedParentIndex.value = -1;
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user