This commit is contained in:
itcsx committed 2026-09-13 19:01:48 +08:00
1 parent 7f5bf3c68d
commit 816209fc35
37 files changed
+3236 -4515

No files matched your search

@@ -57,7 +57,9 @@ const getPolicyText = (options: PolicyOption[], value?: string | null) => {
const genderLabel = (g?: string) => getPolicyText(genderOptions, g);
const relationLabel = (r?: string) => getPolicyText(relationTypeOptions, r);
/** 性别为空时默认按男性处理 */
const isMale = (g?: string) => {
if (!g) return true;
const t = (genderLabel(g) ?? "").toLowerCase();
return t.includes("男") || t.includes("male") || g === "M" || g === "1";
};
@@ -66,6 +68,21 @@ const hasPerson = computed(() => !!props.data?.CurrentPerson?.FamilyPerson);
const rootPersonId = computed(() => props.data?.CurrentPerson?.FamilyPerson?.PersonId ?? "");
/** personId -> 头像访问地址(本人 / 全族人员 / 已加载族员) */
const avatarMap = computed(() => {
const map: Record<string, string> = {};
const root: any = props.data?.CurrentPerson?.FamilyPerson;
if (root?.PersonId && root?.AvatarUrl) map[root.PersonId] = root.AvatarUrl;
((props.data?.CurrentPerson?.FamilyTreePersons ?? []) as any[]).forEach((p: any) => {
if (p?.PersonId && p?.AvatarUrl) map[p.PersonId] = p.AvatarUrl;
});
Object.values((props.loadedNodes ?? {}) as Record<string, any>).forEach((n: any) => {
const p = n?.person;
if (p?.PersonId && p?.AvatarUrl) map[p.PersonId] = p.AvatarUrl;
});
return map;
});
/** HTML 字符串转义,防止人名中的特殊字符破坏卡片结构 */
const esc = (v?: string | null) =>
(v ?? "")
@@ -74,13 +91,22 @@ const esc = (v?: string | null) =>
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
/** 无头像时的默认头像图标(白底 + 性别色人像:男蓝 / 女粉) */
const USER_ICON_SVG =
'<svg viewBox="64 64 896 896" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"></path></svg>';
/** 自定义人员卡片 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 avatarColor = male ? "#1890ff" : "#eb2f96";
// 有头像显示头像,无头像显示默认头像(白底 + 性别色人像图标)
const avatar = data.avatarUrl
? `<img class="ftc-avatar" src="${esc(data.avatarUrl)}" alt="" onerror="this.style.visibility='hidden'" />`
: "";
const common = data.commonName
? `<div class="ftc-sub">常用名:${esc(data.commonName)}</div>`
: "";
@@ -92,11 +118,15 @@ const renderCard = (d: any) => {
const clickable = data.hasChildren || !data.loaded ? "ftc-clickable" : "";
return `
<div class="ftc-card ftc-card-lg ${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 class="ftc-main">
<div class="ftc-avatar-wrap" style="border-color:${theme};">
<span class="ftc-avatar-ph" style="color:${avatarColor};">${USER_ICON_SVG}</span>${avatar}
</div>
<div class="ftc-info">
<div class="ftc-name" title="${esc(data.name)}">${esc(data.name)}</div>
${common}
</div>
</div>
${common}
<div class="ftc-footer">
<div class="ftc-tag" style="color:${theme}; background:${theme}1a;">${esc(data.tag)}</div>
${actionTag}
@@ -139,7 +169,7 @@ const buildGraphData = () => {
if (!root) return { nodes, edges };
const CARD_WIDTH = 360;
const CARD_HEIGHT = 192;
const CARD_HEIGHT = 220;
const H_GAP = 60;
const V_GAP = 40;
const X_STEP = CARD_WIDTH + H_GAP; // 420,避免水平重叠
@@ -169,6 +199,7 @@ const buildGraphData = () => {
name: (p?.Surname ?? "") + (p?.GenealogyName ?? ""),
commonName: p?.CommonName,
gender: p?.Gender,
avatarUrl: p?.AvatarUrl ?? avatarMap.value[personId] ?? "",
hasChildren,
expanded,
loaded: desc.loaded,
@@ -183,9 +214,11 @@ const buildGraphData = () => {
style: { x: xBase + X_STEP + i * SPOUSE_GAP, y },
data: {
kind: "spouse",
personId: s.SpouseId,
tag: "配偶",
name: (s.SpouseSurname ?? "") + (s.SpouseGenealogyName ?? ""),
gender: s.Gender,
avatarUrl: avatarMap.value[s.SpouseId] ?? "",
loaded: true,
hasChildren: false,
expanded: false,
@@ -213,6 +246,7 @@ const buildGraphData = () => {
name: (c.ChildrenSurname ?? "") + (c.ChildrenGenealogyName ?? ""),
commonName: c.ChildrenCommonName,
gender: c.Gender,
avatarUrl: avatarMap.value[childId] ?? "",
hasChildren: false,
expanded: false,
loaded: false,
@@ -280,9 +314,9 @@ const initGraph = async () => {
node: {
type: "html",
style: {
size: [360, 192],
size: [360, 220],
dx: -180,
dy: -96,
dy: -110,
// 官方文档推荐函数形式,类型声明仅支持 string/HTMLElement,故断言
innerHTML: renderCard as any,
},
@@ -443,10 +477,6 @@ const onCancel = () => {
padding: 12px 16px;
}
.ftc-card-lg .ftc-ico {
font-size: 24px;
}
.ftc-card-lg .ftc-name {
font-size: 20px;
}
@@ -467,12 +497,63 @@ const onCancel = () => {
gap: 6px;
}
.ftc-ico {
font-size: 16px;
font-weight: 700;
/* 人员卡片:左侧头像 + 右侧姓名信息 */
.ftc-main {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.ftc-info {
flex: 1;
min-width: 0;
}
.ftc-avatar-wrap {
position: relative;
flex: none;
width: 40px;
height: 40px;
border: 2px solid #e8e8e8;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.ftc-card-lg .ftc-avatar-wrap {
width: 104px;
height: 104px;
}
/* 无头像占位:白底 + 性别色人像图标(男蓝 / 女粉,颜色由内联 style 指定) */
.ftc-avatar-ph {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #1890ff;
font-size: 20px;
line-height: 1;
}
.ftc-card-lg .ftc-avatar-ph {
font-size: 52px;
}
/* 头像图片绝对定位覆盖占位图标,加载失败时隐藏,露出下方默认头像 */
.ftc-avatar {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.ftc-name {
font-size: 14px;
font-weight: 600;