待测试
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
v-for="connector in lineageConnectors"
|
||||
:key="connector.id"
|
||||
class="lineage-connector"
|
||||
:class="{ 'lineage-connector--spouse': connector.kind === 'spouse' }"
|
||||
:style="connector.style"
|
||||
/>
|
||||
|
||||
@@ -97,30 +98,33 @@
|
||||
:key="member.id"
|
||||
:id="`tree-member-${member.id}`"
|
||||
class="member-node"
|
||||
:class="{ 'member-node--selected': selected?.id === member.id }"
|
||||
:class="{
|
||||
'member-node--selected': selected?.id === member.id,
|
||||
'member-node--spouse': Boolean(member.spouseOf),
|
||||
}"
|
||||
:style="nodeGridStyle(member)"
|
||||
@click="openMemberPanel(member)"
|
||||
>
|
||||
<image
|
||||
class="member-node__skin"
|
||||
:src="
|
||||
selected?.id === member.id
|
||||
? '/static/assets/modules/tree/transparent/t01-member-node-selected.png'
|
||||
: '/static/assets/modules/tree/transparent/t01-member-node-standard.png'
|
||||
"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<image
|
||||
class="member-node__avatar"
|
||||
src="/static/assets/foundation/transparent/meta-member.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="member-node__copy">
|
||||
<text class="node-name">{{ member.name }}</text>
|
||||
<text class="node-relation"
|
||||
>{{ member.relation }} · {{ member.branch }}</text
|
||||
>
|
||||
<text class="node-years">{{ member.years }}</text>
|
||||
<view class="member-node__surface">
|
||||
<view class="member-node__portrait">
|
||||
<image
|
||||
class="member-node__avatar"
|
||||
src="/static/assets/foundation/transparent/tab-profile.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<image
|
||||
class="member-node__divider"
|
||||
src="/static/assets/foundation/transparent/auth-divider-knot.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="member-node__copy">
|
||||
<text class="node-name">{{ member.name }}</text>
|
||||
<text class="node-relation"
|
||||
>{{ nodeRelationText(member) }}</text
|
||||
>
|
||||
<text class="node-years">{{ member.years }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -168,7 +172,7 @@
|
||||
>
|
||||
<image
|
||||
class="member-action-profile__avatar"
|
||||
src="/static/assets/foundation/transparent/meta-member.png"
|
||||
src="/static/assets/foundation/transparent/tab-profile.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="member-action-profile__copy">
|
||||
@@ -202,7 +206,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
@@ -234,9 +238,11 @@ let treeLoadGeneration = 0;
|
||||
let pageActive = true;
|
||||
let skipNextShowRefresh = true;
|
||||
const GRID_UNIT = 5;
|
||||
const NODE_HALF_HEIGHT = 47;
|
||||
const MEMBER_GAP = 250;
|
||||
const GENERATION_GAP = 220;
|
||||
const NODE_HALF_WIDTH = 80;
|
||||
const NODE_HALF_HEIGHT = 94;
|
||||
const FAMILY_LINK_OFFSET = 18;
|
||||
const MEMBER_GAP = 178;
|
||||
const GENERATION_GAP = 260;
|
||||
const members = ref([]);
|
||||
|
||||
const memberActions = Object.freeze([
|
||||
@@ -257,6 +263,10 @@ const memberActions = Object.freeze([
|
||||
]);
|
||||
|
||||
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
|
||||
const nodeRelationText = (member) => {
|
||||
const branch = String(member.branch || "").replace(/字辈$/, "");
|
||||
return [member.relation, branch].filter(Boolean).join(" · ");
|
||||
};
|
||||
const layoutMembers = computed(() => {
|
||||
const generations = Array.from(
|
||||
new Set(members.value.map((member) => Number(member.generation))),
|
||||
@@ -269,12 +279,14 @@ const layoutMembers = computed(() => {
|
||||
.sort(
|
||||
(left, right) =>
|
||||
String(left.parentId || "").localeCompare(String(right.parentId || "")) ||
|
||||
String(left.spouseOf || left.id).localeCompare(String(right.spouseOf || right.id)) ||
|
||||
Number(Boolean(left.spouseOf)) - Number(Boolean(right.spouseOf)) ||
|
||||
String(left.id).localeCompare(String(right.id)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
const maxCount = Math.max(1, ...Array.from(groups.values()).map((group) => group.length));
|
||||
const canvasWidth = Math.max(720, maxCount * MEMBER_GAP + 100);
|
||||
const canvasWidth = Math.max(660, maxCount * MEMBER_GAP + 80);
|
||||
return generations.flatMap((generation, generationIndex) => {
|
||||
const group = groups.get(generation) || [];
|
||||
const occupiedWidth = Math.max(0, (group.length - 1) * MEMBER_GAP);
|
||||
@@ -282,7 +294,7 @@ const layoutMembers = computed(() => {
|
||||
return group.map((member, memberIndex) => ({
|
||||
...member,
|
||||
x: snapToGrid(startX + memberIndex * MEMBER_GAP),
|
||||
y: snapToGrid(135 + generationIndex * GENERATION_GAP),
|
||||
y: snapToGrid(200 + generationIndex * GENERATION_GAP),
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -290,7 +302,7 @@ const treeMetrics = computed(() => {
|
||||
const memberList = layoutMembers.value;
|
||||
const maxX = Math.max(0, ...memberList.map((member) => member.x));
|
||||
const maxY = Math.max(0, ...memberList.map((member) => member.y));
|
||||
const width = Math.max(720, snapToGrid(maxX + 210));
|
||||
const width = Math.max(660, snapToGrid(maxX + NODE_HALF_WIDTH + 50));
|
||||
const height = Math.max(640, snapToGrid(maxY + 250));
|
||||
return {
|
||||
width,
|
||||
@@ -345,9 +357,15 @@ const lineageConnectors = computed(() => {
|
||||
const memberById = new Map(
|
||||
layoutMembers.value.map((member) => [member.id, member]),
|
||||
);
|
||||
const spouseByPersonId = new Map();
|
||||
layoutMembers.value.forEach((member) => {
|
||||
if (member.spouseOf && memberById.has(member.spouseOf)) {
|
||||
spouseByPersonId.set(member.spouseOf, member);
|
||||
}
|
||||
});
|
||||
const childrenByParent = new Map();
|
||||
layoutMembers.value.forEach((member) => {
|
||||
if (!member.parentId || !memberById.has(member.parentId)) return;
|
||||
if (member.spouseOf || !member.parentId || !memberById.has(member.parentId)) return;
|
||||
const children = childrenByParent.get(member.parentId) || [];
|
||||
children.push(member);
|
||||
childrenByParent.set(member.parentId, children);
|
||||
@@ -356,18 +374,46 @@ const lineageConnectors = computed(() => {
|
||||
const connectors = [];
|
||||
childrenByParent.forEach((children, parentId) => {
|
||||
const parent = memberById.get(parentId);
|
||||
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
|
||||
const spouse = spouseByPersonId.get(parentId);
|
||||
const parentAnchorX = spouse
|
||||
? snapToGrid((parent.x + spouse.x) / 2)
|
||||
: parent.x;
|
||||
const parentBottom = parent.y + NODE_HALF_HEIGHT;
|
||||
const branchY = snapToGrid((parentBottom + childTop) / 2);
|
||||
const parentAnchorY = spouse
|
||||
? snapToGrid(parentBottom + FAMILY_LINK_OFFSET)
|
||||
: parentBottom;
|
||||
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
|
||||
const branchY = snapToGrid((parentAnchorY + childTop) / 2);
|
||||
const minX = Math.min(...children.map((child) => child.x));
|
||||
const maxX = Math.max(...children.map((child) => child.x));
|
||||
const verticalStyle = (x, top, bottom) => ({
|
||||
gridColumn: `${Math.round(x / GRID_UNIT) + 1} / span 1`,
|
||||
gridRow: `${Math.round(top / GRID_UNIT) + 1} / ${Math.round(bottom / GRID_UNIT) + 1}`,
|
||||
});
|
||||
const horizontalStyle = (left, right, y) => ({
|
||||
gridColumn: `${Math.round(left / GRID_UNIT) + 1} / ${Math.round(right / GRID_UNIT) + 2}`,
|
||||
gridRow: `${Math.round(y / GRID_UNIT) + 1} / span 1`,
|
||||
});
|
||||
if (spouse) {
|
||||
connectors.push({
|
||||
id: `${parentId}-${spouse.id}-family-parent`,
|
||||
kind: "family",
|
||||
style: verticalStyle(parent.x, parentBottom, parentAnchorY),
|
||||
});
|
||||
connectors.push({
|
||||
id: `${parentId}-${spouse.id}-family-spouse`,
|
||||
kind: "family",
|
||||
style: verticalStyle(spouse.x, spouse.y + NODE_HALF_HEIGHT, parentAnchorY),
|
||||
});
|
||||
connectors.push({
|
||||
id: `${parentId}-${spouse.id}-family-bridge`,
|
||||
kind: "family",
|
||||
style: horizontalStyle(Math.min(parent.x, spouse.x), Math.max(parent.x, spouse.x), parentAnchorY),
|
||||
});
|
||||
}
|
||||
connectors.push({
|
||||
id: `${parentId}-trunk`,
|
||||
style: verticalStyle(parent.x, parentBottom, branchY),
|
||||
style: verticalStyle(parentAnchorX, parentAnchorY, branchY),
|
||||
});
|
||||
connectors.push({
|
||||
id: `${parentId}-branch`,
|
||||
@@ -381,6 +427,23 @@ const lineageConnectors = computed(() => {
|
||||
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
|
||||
}));
|
||||
});
|
||||
layoutMembers.value.forEach((member) => {
|
||||
if (!member.spouseOf) return;
|
||||
const partner = memberById.get(member.spouseOf);
|
||||
if (!partner || partner.generation !== member.generation) return;
|
||||
if (childrenByParent.has(partner.id)) return;
|
||||
const left = Math.min(member.x, partner.x) + NODE_HALF_WIDTH;
|
||||
const right = Math.max(member.x, partner.x) - NODE_HALF_WIDTH;
|
||||
if (right <= left) return;
|
||||
connectors.push({
|
||||
id: `${partner.id}-${member.id}-spouse`,
|
||||
kind: "spouse",
|
||||
style: {
|
||||
gridColumn: `${Math.round(left / GRID_UNIT) + 1} / ${Math.round(right / GRID_UNIT) + 2}`,
|
||||
gridRow: `${Math.round(member.y / GRID_UNIT) + 1} / span 1`,
|
||||
},
|
||||
});
|
||||
});
|
||||
return connectors;
|
||||
});
|
||||
|
||||
@@ -444,6 +507,10 @@ const loadTree = async (query = {}) => {
|
||||
(item) => String(item.id) === String(query.selectedId),
|
||||
) || layoutMembers.value[0] || null;
|
||||
treeState.value = members.value.length ? "tree" : "empty";
|
||||
if (treeState.value === "tree" && selected.value) {
|
||||
await nextTick();
|
||||
recenterSelectedMember();
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
!pageActive ||
|
||||
@@ -481,22 +548,46 @@ const handleTreeScroll = (event) => {
|
||||
treeHasDrifted.value = Math.abs(scrollLeft - centeredTreeScrollLeft.value) > 80;
|
||||
};
|
||||
|
||||
const getFamilyPartner = (member) => {
|
||||
if (!member) return null;
|
||||
const partnerId = member.spouseOf || layoutMembers.value.find(
|
||||
(item) => String(item.spouseOf || "") === String(member.id),
|
||||
)?.id;
|
||||
const partner = layoutMembers.value.find(
|
||||
(item) => String(item.id) === String(partnerId || ""),
|
||||
);
|
||||
return partner && partner.generation === member.generation ? partner : null;
|
||||
};
|
||||
|
||||
const recenterSelectedMember = async () => {
|
||||
if (!selected.value) return;
|
||||
const spouse = getFamilyPartner(selected.value);
|
||||
ignoreTreeScroll = true;
|
||||
const [scrollRect, nodeRect] = await new Promise((resolve) => {
|
||||
const [scrollRect, nodeRect, spouseRect] = await new Promise((resolve) => {
|
||||
const query = uni.createSelectorQuery();
|
||||
query.select(".tree-scroll").boundingClientRect();
|
||||
query
|
||||
.select(`#tree-member-${selected.value.id}`)
|
||||
.boundingClientRect();
|
||||
if (spouse) {
|
||||
query.select(`#tree-member-${spouse.id}`).boundingClientRect();
|
||||
}
|
||||
query.exec(resolve);
|
||||
});
|
||||
if (scrollRect && nodeRect) {
|
||||
const targetLeft = spouseRect
|
||||
? Math.min(nodeRect.left, spouseRect.left)
|
||||
: nodeRect.left;
|
||||
const targetRight = spouseRect
|
||||
? Math.max(
|
||||
nodeRect.left + nodeRect.width,
|
||||
spouseRect.left + spouseRect.width,
|
||||
)
|
||||
: nodeRect.left + nodeRect.width;
|
||||
const centeredScrollLeft = Math.max(
|
||||
0,
|
||||
currentTreeScrollLeft.value +
|
||||
nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2),
|
||||
(targetLeft + targetRight) / 2 - (scrollRect.left + scrollRect.width / 2),
|
||||
);
|
||||
treeScrollLeft.value = centeredScrollLeft;
|
||||
currentTreeScrollLeft.value = centeredScrollLeft;
|
||||
@@ -613,7 +704,7 @@ const openMemberAction = (action) => {
|
||||
}
|
||||
.tree-stage--lineage {
|
||||
display: grid;
|
||||
grid-template-columns: 190rpx minmax(0, 1fr);
|
||||
grid-template-columns: 144rpx minmax(0, 1fr);
|
||||
align-items: start;
|
||||
}
|
||||
.tree-scroll {
|
||||
@@ -624,7 +715,7 @@ const openMemberAction = (action) => {
|
||||
.generation-rail {
|
||||
display: grid;
|
||||
z-index: 4;
|
||||
width: 190rpx;
|
||||
width: 144rpx;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
|
||||
@@ -635,7 +726,7 @@ const openMemberAction = (action) => {
|
||||
}
|
||||
.tree-canvas {
|
||||
display: grid;
|
||||
margin: 0 16rpx;
|
||||
margin: 0 12rpx;
|
||||
}
|
||||
.tree-canvas--state {
|
||||
display: block;
|
||||
@@ -655,7 +746,7 @@ const openMemberAction = (action) => {
|
||||
display: grid;
|
||||
justify-self: center;
|
||||
z-index: 3;
|
||||
width: 166rpx;
|
||||
width: 126rpx;
|
||||
height: 58rpx;
|
||||
color: #8f6c3f;
|
||||
font-size: 22rpx;
|
||||
@@ -679,7 +770,7 @@ const openMemberAction = (action) => {
|
||||
}
|
||||
.generation-band__copy text:last-child {
|
||||
margin-top: 1rpx;
|
||||
font-size: 19rpx;
|
||||
font-size: 18rpx;
|
||||
}
|
||||
.lineage-connector {
|
||||
align-self: stretch;
|
||||
@@ -687,36 +778,57 @@ const openMemberAction = (action) => {
|
||||
z-index: 1;
|
||||
background: #b78a42;
|
||||
}
|
||||
.lineage-connector--spouse {
|
||||
background: rgba(183, 117, 76, 0.82);
|
||||
}
|
||||
.member-node {
|
||||
display: grid;
|
||||
position: relative;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
z-index: 2;
|
||||
width: 224rpx;
|
||||
height: 86rpx;
|
||||
width: 160rpx;
|
||||
height: 188rpx;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.member-node__skin {
|
||||
.member-node__surface {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 9rpx 8rpx 8rpx;
|
||||
border: 1rpx solid #b78a42;
|
||||
border-radius: 14rpx;
|
||||
background: rgba(255, 252, 244, 0.96);
|
||||
box-shadow: 0 5rpx 12rpx rgba(103, 72, 33, 0.12);
|
||||
}
|
||||
.member-node__skin,
|
||||
.member-node__copy {
|
||||
grid-area: 1 / 1;
|
||||
.member-node__portrait {
|
||||
display: flex;
|
||||
width: 62rpx;
|
||||
height: 62rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #c49a57;
|
||||
border-radius: 50%;
|
||||
background: #fffdf7;
|
||||
overflow: hidden;
|
||||
}
|
||||
.member-node__avatar {
|
||||
grid-area: 1 / 1;
|
||||
z-index: 3;
|
||||
align-self: start;
|
||||
justify-self: start;
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
margin: 13rpx 0 0 18rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
.member-node__divider {
|
||||
width: 56rpx;
|
||||
height: 14rpx;
|
||||
margin-top: 3rpx;
|
||||
}
|
||||
.member-node__copy {
|
||||
z-index: 1;
|
||||
padding-left: 44rpx;
|
||||
width: 100%;
|
||||
margin-top: 1rpx;
|
||||
padding: 0;
|
||||
}
|
||||
.node-name,
|
||||
.node-relation,
|
||||
@@ -724,25 +836,48 @@ const openMemberAction = (action) => {
|
||||
display: block;
|
||||
}
|
||||
.node-name {
|
||||
padding-top: 5rpx;
|
||||
color: $ink;
|
||||
display: -webkit-box;
|
||||
color: #6b2419;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 25rpx;
|
||||
overflow: hidden;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
white-space: normal;
|
||||
}
|
||||
.node-relation {
|
||||
margin-top: 0;
|
||||
color: $brand-red;
|
||||
font-size: 21rpx;
|
||||
overflow: hidden;
|
||||
margin-top: 2rpx;
|
||||
color: #644b2e;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.node-years {
|
||||
margin-top: 0;
|
||||
color: $ink-muted;
|
||||
font-size: 20rpx;
|
||||
overflow: hidden;
|
||||
margin-top: 2rpx;
|
||||
color: #695b4c;
|
||||
font-size: 17rpx;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.member-node--spouse .member-node__surface {
|
||||
border-color: #c39a61;
|
||||
background: rgba(255, 250, 241, 0.98);
|
||||
}
|
||||
.member-node--selected {
|
||||
width: 236rpx;
|
||||
height: 94rpx;
|
||||
z-index: 3;
|
||||
}
|
||||
.member-node--selected .member-node__surface {
|
||||
border-color: #c55344;
|
||||
box-shadow: 0 6rpx 14rpx rgba(159, 23, 15, 0.16);
|
||||
}
|
||||
.member-node--selected .member-node__portrait {
|
||||
border-color: #c55344;
|
||||
}
|
||||
.member-node--selected .node-name {
|
||||
color: $brand-red;
|
||||
|
||||
Reference in New Issue
Block a user