This commit is contained in:
owenchen committed 2026-10-17 13:22:10 +08:00
1 parent b5d2a2ede0
commit aff58692bb
17 files changed
+1851 -240

No files matched your search

@@ -113,9 +113,53 @@ async function onRetry(): Promise<void> {
const data = ref<FamilyTreeData>(new FamilyTreeData());
const tree = computed(() => data.value.FamilyTree);
const currentPerson = computed(() => data.value.CurrentPerson?.FamilyPerson);
/** 当前族员显示名(旧系统同款:姓+谱名连写) */
const currentPersonName = computed(() => {
const p = currentPerson.value;
return p ? `${p.Surname ?? ""}${p.GenealogyName ?? ""}` : "";
});
const spouseMaps = computed(() => data.value.CurrentPerson?.FamilyTreeSpouseMaps ?? []);
const childMaps = computed(() => data.value.CurrentPerson?.FamilyTreeChildrenMaps ?? []);
/** 关系行头像查表:Spouse/Children 视图字段不带头像,回查全族人员档案拿 AvatarUrl(谱系图 getAvatar 同款口径) */
function personAvatar(personId?: string): string {
if (!personId) {
return "";
}
const persons = (data.value.CurrentPerson?.FamilyTreePersons ?? []) as any[];
return persons.find(p => p?.PersonId === personId)?.AvatarUrl ?? "";
}
/** 谱系图同款性别判定(family-tree-chart isMale 字面一致):空性别按男兜底 */
function genderIsMale(gender?: string): boolean {
if (!gender) {
return true;
}
const label = getPolicyText("GenderPolicy", gender).toLowerCase();
return label.includes("男") || label.includes("male") || gender === "M" || gender === "1";
}
/** 关系对方性别回查:关系视图的 Gender 是记录拥有者(丈夫/父亲)的字段(谱系图配色踩过的坑),优先拿对方档案真值 */
function personGender(personId?: string, fallback?: string): string {
if (!personId) {
return fallback ?? "";
}
const persons = (data.value.CurrentPerson?.FamilyTreePersons ?? []) as any[];
return persons.find(p => p?.PersonId === personId)?.Gender || fallback || "";
}
/** 谱系图同款性别配色:男蓝 / 女粉 */
function genderTheme(gender?: string): string {
return genderIsMale(gender) ? "#1890ff" : "#eb2f96";
}
/** 无头像时的占位圆样式:白底 + 性别同色描边(谱系图同款);有头像返回空不覆盖 */
function avatarFallbackStyle(hasAvatar: boolean, gender?: string): Record<string, string> {
return hasAvatar
? {}
: { backgroundColor: "#ffffff", border: `3rpx solid ${genderTheme(gender)}` };
}
const treeId = ref("");
const loaded = ref(false);
const isEdit = ref(false);
@@ -704,10 +748,10 @@ defineExpose({
</view>
<view class="tree-page__scroll-wrap">
<scroll-view scroll-y class="tree-page__scroll">
<!-- 头卡(与媒体/共享页同套):白底 + 主题浅底图标 + 编号灰字独立行 -->
<!-- 头卡(与媒体/共享页同套):白底 + 族谱树图标(与列表卡同款渐变底白树形) + 编号灰字独立行 -->
<view class="detail-hero">
<view class="detail-hero__avatar">
<u-icon name="account-fill" size="64" :color="primary" />
<image class="detail-hero__glyph" :src="treeGlyph" mode="aspectFit" />
</view>
<view class="detail-hero__main">
<text class="detail-hero__name">{{ tree.TreeName || t("FamilyTreePage.unnamed") }}</text>
@@ -768,11 +812,13 @@ defineExpose({
<textarea v-model="tree.Brief" class="detail-field__textarea" :disabled="!isEdit" :maxlength="1000"
:placeholder="t('FamilyTreePage.noBrief')" />
</view>
<!-- 封面文件暂时不用,先隐藏(保留代码便于日后恢复)
<view class="detail-field">
<text class="detail-field__label">{{ t("FamilyTreePage.treeCover") }}</text>
<input v-model="tree.TreeCoverFilename" class="detail-field__input" :disabled="!isEdit"
:maxlength="255">
</view>
-->
<view class="detail-field detail-field--row">
<text class="detail-field__label">{{ t("FamilyTreePage.publicIndc") }}</text>
<u-switch :model-value="tree.PublicIndc === 'Y'" :disabled="!isEdit" :active-color="primary"
@@ -795,17 +841,28 @@ defineExpose({
<u-icon :name="folded.person ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
</view>
<view v-show="!folded.person">
<view class="detail-field detail-field--row">
<text class="detail-field__label">{{ t("FamilyTreePage.gender") }}</text>
<text class="detail-field__value">{{ genderText(currentPerson?.Gender) || "-" }}</text>
</view>
<view class="detail-field detail-field--row">
<text class="detail-field__label">{{ t("FamilyTreePage.genealogyName") }}</text>
<text class="detail-field__value">{{ currentPerson?.GenealogyName || "-" }}</text>
</view>
<view class="detail-field detail-field--row">
<text class="detail-field__label">{{ t("FamilyTreePage.commonName") }}</text>
<text class="detail-field__value">{{ currentPerson?.CommonName || "-" }}</text>
<!-- 头像行(旧系统同款):圆头像 + 名字带性别 + 编号芯片与常用名,无头像时灰底人形占位 -->
<view class="person-hero">
<view class="person-hero__avatar"
:style="avatarFallbackStyle(!!currentPerson?.AvatarUrl, currentPerson?.Gender)">
<image v-if="currentPerson?.AvatarUrl" class="person-hero__img" :src="currentPerson.AvatarUrl"
mode="aspectFill" />
<u-icon v-else name="account" size="56" :color="genderTheme(currentPerson?.Gender)" />
</view>
<view class="person-hero__main">
<view class="person-hero__line">
<text class="person-hero__name">{{ currentPersonName || "-" }}</text>
<text class="person-hero__gender">{{ genderText(currentPerson?.Gender) || "-" }}</text>
</view>
<view class="person-hero__chips">
<text v-if="currentPerson?.PersonId" class="person-hero__chip">
<text v-if="currentPerson?.CommonName" class="person-hero__chip-name">
{{ currentPerson.CommonName }}
</text>
{{ `${t("FamilyPersonPage.personId")}: ${currentPerson.PersonId}` }}
</text>
</view>
</view>
</view>
<view class="op-btns">
<view class="op-btn" @tap="pickCurrentPerson">
@@ -853,9 +910,28 @@ defineExpose({
</view>
<view v-for="(record, index) in spouseMaps" :key="record.SpouseId" class="rel-item">
<view class="rel-head" @tap="expandedSpouses[record.SpouseId] = !expandedSpouses[record.SpouseId]">
<!-- 当前族员同款:圆头像 + 名字带性别 + 芯片(常用名值+成员编号) -->
<view class="rel-avatar"
:style="avatarFallbackStyle(!!personAvatar(record.SpouseId), personGender(record.SpouseId, record.Gender))">
<image v-if="personAvatar(record.SpouseId)" class="rel-avatar__img"
:src="personAvatar(record.SpouseId)" mode="aspectFill" />
<u-icon v-else name="account" size="40"
:color="genderTheme(personGender(record.SpouseId, record.Gender))" />
</view>
<view class="rel-text">
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
<text class="rel-sub">{{ genderText(record.Gender) }}</text>
<view class="person-hero__line">
<text class="rel-name">{{ record.SpouseSurname }}{{ record.SpouseGenealogyName }}</text>
<text class="person-hero__gender">{{ genderText(personGender(record.SpouseId, record.Gender))
}}</text>
</view>
<view class="person-hero__chips">
<text class="person-hero__chip">
<text v-if="record.SpouseCommonName" class="person-hero__chip-name">
{{ record.SpouseCommonName }}
</text>
{{ `${t("FamilyPersonPage.personId")}: ${record.SpouseId}` }}
</text>
</view>
</view>
<view class="rel-ops">
<template v-if="isEdit">
@@ -971,9 +1047,28 @@ defineExpose({
<view v-for="(record, index) in childMaps" :key="record.ChildrenId" class="rel-item">
<view class="rel-head"
@tap="expandedChildren[record.ChildrenId] = !expandedChildren[record.ChildrenId]">
<!-- 当前族员同款:圆头像 + 名字带性别 + 芯片(常用名值+成员编号) -->
<view class="rel-avatar"
:style="avatarFallbackStyle(!!personAvatar(record.ChildrenId), personGender(record.ChildrenId, record.Gender))">
<image v-if="personAvatar(record.ChildrenId)" class="rel-avatar__img"
:src="personAvatar(record.ChildrenId)" mode="aspectFill" />
<u-icon v-else name="account" size="40"
:color="genderTheme(personGender(record.ChildrenId, record.Gender))" />
</view>
<view class="rel-text">
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
<text class="rel-sub">{{ genderText(record.Gender) }}</text>
<view class="person-hero__line">
<text class="rel-name">{{ record.ChildrenSurname }}{{ record.ChildrenGenealogyName }}</text>
<text class="person-hero__gender">{{ genderText(personGender(record.ChildrenId, record.Gender))
}}</text>
</view>
<view class="person-hero__chips">
<text class="person-hero__chip">
<text v-if="record.ChildrenCommonName" class="person-hero__chip-name">
{{ record.ChildrenCommonName }}
</text>
{{ `${t("FamilyPersonPage.personId")}: ${record.ChildrenId}` }}
</text>
</view>
</view>
<view class="rel-ops">
<template v-if="isEdit">
@@ -1035,7 +1130,10 @@ defineExpose({
<u-icon :name="folded.perms ? 'arrow-right' : 'arrow-down'" size="26" color="#8f959e" />
</view>
<view v-show="!folded.perms">
<text class="perm-section-label">{{ t("FamilyTreePage.permPending") }}</text>
<view class="perm-section-head">
<text class="perm-section-label">{{ t("FamilyTreePage.permPending") }}</text>
<text class="perm-section-count">{{ pendingPermissions.length }}</text>
</view>
<view v-if="!pendingPermissions.length" class="detail-empty">
<text>{{ t("FamilyTreePage.permEmpty") }}</text>
</view>
@@ -1057,7 +1155,11 @@ defineExpose({
</view>
</view>
<text class="perm-section-label">{{ t("FamilyTreePage.permManager") }}</text>
<view class="perm-divider" />
<view class="perm-section-head">
<text class="perm-section-label">{{ t("FamilyTreePage.permManager") }}</text>
<text class="perm-section-count">{{ managerPermissions.length }}</text>
</view>
<view v-if="!managerPermissions.length" class="detail-empty">
<text>{{ t("FamilyTreePage.permEmpty") }}</text>
</view>
@@ -1358,10 +1460,95 @@ defineExpose({
width: 140rpx;
height: 140rpx;
border-radius: 32rpx;
background-color: var(--fg-soft, #eaf4ff);
background: var(--fg-gradient, linear-gradient(135deg, #4a9dff, #2b8fff));
flex-shrink: 0;
}
/* 树形字形(与列表卡同款内联 SVG data-URI) */
.detail-hero__glyph {
width: 84rpx;
height: 84rpx;
}
/* 当前族员头像行(旧系统 hero 同款):圆头像 + 名字 + 编号芯片 */
.person-hero {
display: flex;
flex-direction: row;
align-items: center;
gap: 24rpx;
/* 上距标题 24rpx;下距分隔线由 .op-btns 的 margin-top(24rpx) 提供,两侧视觉等距 */
padding: 24rpx 0 0;
}
.person-hero__avatar {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 112rpx;
height: 112rpx;
overflow: hidden;
background-color: var(--fg-soft, #eaf4ff);
border: 4rpx solid #ffffff;
border-radius: 50%;
box-shadow: 0 4rpx 14rpx rgba(31, 35, 41, 0.12);
}
.person-hero__img {
width: 100%;
height: 100%;
}
.person-hero__main {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 10rpx;
}
.person-hero__line {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 16rpx;
}
.person-hero__name {
font-size: 32rpx;
font-weight: 700;
color: #1f2329;
}
.person-hero__gender {
font-size: 26rpx;
color: #8f959e;
}
.person-hero__chips {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 12rpx;
}
.person-hero__chip {
padding: 4rpx 18rpx;
font-size: 22rpx;
color: var(--fg-primary, #2b8fff);
background-color: var(--fg-soft, #eaf4ff);
border-radius: 10rpx;
}
/* 芯片内的常用名值:排在编号前,同芯片浅底、深色字(旧系统同款合并位) */
.person-hero__chip-name {
margin-right: 24rpx;
font-size: 24rpx;
color: #1f2329;
}
.detail-hero__main {
display: flex;
flex: 1;
@@ -1594,12 +1781,33 @@ defineExpose({
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
/* 关系行圆头像(当前族员同款缩小一号):档案无头像时灰底人形占位 */
.rel-avatar {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
overflow: hidden;
background-color: var(--fg-soft, #eaf4ff);
border-radius: 50%;
}
.rel-avatar__img {
width: 100%;
height: 100%;
}
.rel-text {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
gap: 8rpx;
}
.rel-name {
@@ -1680,11 +1888,39 @@ defineExpose({
}
/* ===== 权限维护行 ===== */
/* 子标题分组头:加深字色 + 加粗 + 计数徽章,让分区标题清晰可辨(对齐参照图) */
.perm-section-head {
display: flex;
flex-direction: row;
align-items: center;
gap: 12rpx;
padding: 28rpx 0 8rpx;
}
/* 第二分区(管理名单)上方的通栏分隔带:负外边距 bleed 到卡片左右边,色带用主题软底把白卡横向"切开";
加 border-radius 让色带两端收圆角,圆角处透出白卡底,两分区过渡更柔和 */
.perm-divider {
height: 16rpx;
margin: 28rpx -28rpx 0;
background-color: var(--fg-soft, #f6efe8);
border-radius: 16rpx;
}
.perm-section-label {
display: block;
padding: 24rpx 0 4rpx;
font-size: 24rpx;
color: #8f959e;
font-size: 28rpx;
font-weight: 600;
color: #1f2329;
}
.perm-section-count {
min-width: 34rpx;
padding: 0 10rpx;
font-size: 22rpx;
line-height: 34rpx;
color: var(--fg-primary, #2b8fff);
text-align: center;
background-color: var(--fg-soft, #eaf4ff);
border-radius: 17rpx;
}
.perm-row {