修改完成

This commit is contained in:
2026-07-21 07:53:03 +08:00
parent 01d246c47b
commit ff60119277
172 changed files with 7982 additions and 2999 deletions
+220 -157
View File
@@ -25,13 +25,20 @@
</view>
</view>
<view class="tree-stage">
<view v-if="treeState === 'tree'" class="generation-rail">
<view
class="tree-stage"
:class="{ 'tree-stage--lineage': treeState === 'tree' }"
>
<view
v-if="treeState === 'tree'"
class="generation-rail"
:style="generationRailStyle"
>
<view
v-for="row in generationRows"
:key="row.generation"
class="generation-band"
:style="generationRowStyle(row)"
:style="generationBandStyle(row)"
>
<image
src="/static/assets/modules/genealogy/transparent/section-divider.png"
@@ -50,10 +57,12 @@
scroll-x
:scroll-left="initialScrollLeft"
:show-scrollbar="false"
:style="treeScrollStyle"
>
<view
class="tree-canvas"
:class="{ 'tree-canvas--state': treeState !== 'tree' }"
:style="treeMetricsStyle"
>
<AppLoading
v-if="treeState === 'loading'"
@@ -66,24 +75,19 @@
<text>同代分支可左右查看</text>
</view>
<view class="lineage-connector lineage-connector--root" />
<view class="lineage-connector lineage-connector--root-branch" />
<view class="lineage-connector lineage-connector--gen13-left" />
<view class="lineage-connector lineage-connector--gen13-right" />
<view class="lineage-connector lineage-connector--left-trunk" />
<view class="lineage-connector lineage-connector--left-branch" />
<view class="lineage-connector lineage-connector--gen14-left" />
<view class="lineage-connector lineage-connector--gen14-middle" />
<view class="lineage-connector lineage-connector--right-trunk" />
<view class="lineage-connector lineage-connector--right-branch" />
<view class="lineage-connector lineage-connector--gen14-right" />
<view
v-for="connector in lineageConnectors"
:key="connector.id"
class="lineage-connector"
:style="connector.style"
/>
<view
v-for="member in members"
v-for="member in layoutMembers"
:key="member.id"
class="member-node"
:class="{ 'member-node--selected': selected?.id === member.id }"
:style="nodeStyle(member)"
:style="nodeGridStyle(member)"
@click="selected = member"
>
<image
@@ -95,11 +99,13 @@
"
mode="scaleToFill"
/>
<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__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>
</view>
</template>
@@ -171,11 +177,15 @@ const genealogyId = ref("");
const treeState = ref("loading");
const selected = ref(null);
const initialScrollLeft = ref(90);
const generationRows = [
{ generation: 12, label: "第十二世", summary: "始祖", y: 135 },
{ generation: 13, label: "第十三世", summary: "两房", y: 350 },
{ generation: 14, label: "第十四世", summary: "三支", y: 650 },
];
const GRID_UNIT = 5;
const NODE_HALF_HEIGHT = 47;
const MEMBER_GAP = 250;
const GENERATION_GAP = 220;
const generationMeta = {
12: { label: "第十二世", summary: "始祖" },
13: { label: "第十三世", summary: "两房" },
14: { label: "第十四世", summary: "三支" },
};
const members = ref([
{
id: 101,
@@ -184,8 +194,6 @@ const members = ref([
years: "1940—2012",
generation: 12,
branch: "主支",
x: 450,
y: 135,
},
{
id: 102,
@@ -193,9 +201,8 @@ const members = ref([
relation: "长子",
years: "1965—",
generation: 13,
parentId: 101,
branch: "长房",
x: 275,
y: 350,
},
{
id: 103,
@@ -203,9 +210,8 @@ const members = ref([
relation: "次子",
years: "1968—",
generation: 13,
parentId: 101,
branch: "二房",
x: 625,
y: 350,
},
{
id: 104,
@@ -213,9 +219,8 @@ const members = ref([
relation: "长孙",
years: "1992—",
generation: 14,
parentId: 102,
branch: "长房",
x: 150,
y: 650,
},
{
id: 105,
@@ -223,9 +228,8 @@ const members = ref([
relation: "长孙女",
years: "1995—",
generation: 14,
parentId: 102,
branch: "长房",
x: 400,
y: 650,
},
{
id: 106,
@@ -233,12 +237,132 @@ const members = ref([
relation: "次孙",
years: "1998—",
generation: 14,
parentId: 103,
branch: "二房",
x: 690,
y: 650,
},
]);
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
const layoutMembers = computed(() => {
const generations = Array.from(
new Set(members.value.map((member) => Number(member.generation))),
).sort((left, right) => left - right);
const groups = new Map(
generations.map((generation) => [
generation,
members.value
.filter((member) => Number(member.generation) === generation)
.sort(
(left, right) =>
Number(left.parentId || 0) - Number(right.parentId || 0) ||
Number(left.id) - Number(right.id),
),
]),
);
const maxCount = Math.max(1, ...Array.from(groups.values()).map((group) => group.length));
const canvasWidth = Math.max(720, maxCount * MEMBER_GAP + 100);
return generations.flatMap((generation, generationIndex) => {
const group = groups.get(generation) || [];
const occupiedWidth = Math.max(0, (group.length - 1) * MEMBER_GAP);
const startX = (canvasWidth - occupiedWidth) / 2;
return group.map((member, memberIndex) => ({
...member,
x: snapToGrid(startX + memberIndex * MEMBER_GAP),
y: snapToGrid(135 + generationIndex * GENERATION_GAP),
}));
});
});
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 height = Math.max(640, snapToGrid(maxY + 250));
return {
width,
height,
columns: width / GRID_UNIT,
rows: height / GRID_UNIT,
};
});
const treeMetricsStyle = computed(() => ({
width: `${treeMetrics.value.width}rpx`,
height: `${treeMetrics.value.height}rpx`,
gridTemplateColumns: `repeat(${treeMetrics.value.columns}, ${GRID_UNIT}rpx)`,
gridTemplateRows: `repeat(${treeMetrics.value.rows}, ${GRID_UNIT}rpx)`,
}));
const generationRailStyle = computed(() => ({
height: `${treeMetrics.value.height}rpx`,
gridTemplateRows: `repeat(${treeMetrics.value.rows}, ${GRID_UNIT}rpx)`,
}));
const treeScrollStyle = computed(() => ({
height: `${treeMetrics.value.height}rpx`,
}));
const generationRows = computed(() => {
const groups = new Map();
layoutMembers.value.forEach((member) => {
const group = groups.get(member.generation) || [];
group.push(member);
groups.set(member.generation, group);
});
return Array.from(groups.entries())
.sort(([left], [right]) => left - right)
.map(([generation, group]) => {
const y = Math.min(...group.map((member) => member.y));
return {
generation,
label: generationMeta[generation]?.label || `${generation}`,
summary: generationMeta[generation]?.summary || `${group.length} 位成员`,
y,
};
});
});
const generationBandStyle = (row) => ({
gridRow: `${Math.max(1, Math.round((row.y - 88) / GRID_UNIT) + 1)} / span 12`,
});
const lineageConnectors = computed(() => {
const memberById = new Map(
layoutMembers.value.map((member) => [member.id, member]),
);
const childrenByParent = new Map();
layoutMembers.value.forEach((member) => {
if (!member.parentId || !memberById.has(member.parentId)) return;
const children = childrenByParent.get(member.parentId) || [];
children.push(member);
childrenByParent.set(member.parentId, children);
});
const connectors = [];
childrenByParent.forEach((children, parentId) => {
const parent = memberById.get(parentId);
const childTop = Math.min(...children.map((child) => child.y - NODE_HALF_HEIGHT));
const parentBottom = parent.y + NODE_HALF_HEIGHT;
const branchY = snapToGrid((parentBottom + 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}`,
});
connectors.push({
id: `${parentId}-trunk`,
style: verticalStyle(parent.x, parentBottom, branchY),
});
connectors.push({
id: `${parentId}-branch`,
style: {
gridColumn: `${Math.round(minX / GRID_UNIT) + 1} / ${Math.round(maxX / GRID_UNIT) + 2}`,
gridRow: `${Math.round(branchY / GRID_UNIT) + 1} / span 1`,
},
});
children.forEach((child) => connectors.push({
id: `${parentId}-${child.id}`,
style: verticalStyle(child.x, branchY, child.y - NODE_HALF_HEIGHT),
}));
});
return connectors;
});
const stateCopy = computed(
() =>
({
@@ -279,18 +403,15 @@ onLoad((query) => {
}
genealogyContext.setCurrentGenealogyId(genealogyId.value);
selected.value =
members.value.find(
layoutMembers.value.find(
(item) => String(item.id) === String(query.selectedId),
) || members.value[0];
) || layoutMembers.value[0];
treeState.value = "tree";
});
const nodeStyle = (member) => ({
left: `${member.x}rpx`,
top: `${member.y}rpx`,
});
const generationRowStyle = (row) => ({
top: `${row.y - 88}rpx`,
const nodeGridStyle = (member) => ({
gridColumn: `${member.x / 5 + 1}`,
gridRow: `${member.y / 5 + 1}`,
});
const handleStateAction = () => {
if (treeState.value === "empty") {
@@ -299,7 +420,7 @@ const handleStateAction = () => {
});
return;
}
selected.value = members.value[0];
selected.value = layoutMembers.value[0];
treeState.value = "tree";
};
const toMember = () =>
@@ -322,17 +443,16 @@ const toAddRelative = () =>
<style scoped lang="scss">
.tree-page {
position: relative;
display: grid;
height: 100vh;
grid-template-rows: auto auto minmax(0, 1fr);
overflow: hidden;
background: $paper;
}
.tree-page__header,
.tree-toolbar,
.tree-stage,
.member-sheet {
position: relative;
z-index: 2;
.tree-stage {
z-index: 1;
}
.tree-toolbar {
display: flex;
@@ -361,8 +481,13 @@ const toAddRelative = () =>
font-size: 23rpx;
}
.tree-stage {
position: relative;
height: calc(100vh - 272rpx);
min-height: 0;
overflow-y: auto;
}
.tree-stage--lineage {
display: grid;
grid-template-columns: 190rpx minmax(0, 1fr);
align-items: start;
}
.tree-scroll {
width: 100%;
@@ -370,40 +495,37 @@ const toAddRelative = () =>
white-space: nowrap;
}
.generation-rail {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: grid;
z-index: 4;
width: 190rpx;
height: 100%;
box-sizing: border-box;
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
}
.tree-scroll--lineage {
width: calc(100% - 190rpx);
margin-left: 190rpx;
min-width: 0;
}
.tree-canvas {
position: relative;
width: 900rpx;
height: 900rpx;
display: grid;
margin: 0 16rpx;
}
.tree-canvas--state {
display: block;
width: calc(100vw - 32rpx);
}
.lineage-pan-cue {
position: absolute;
top: 8rpx;
right: 22rpx;
grid-area: 1 / 1 / -1 / -1;
align-self: start;
justify-self: end;
z-index: 3;
margin: 8rpx 22rpx 0 0;
color: #9a7748;
font-size: 20rpx;
letter-spacing: 1rpx;
}
.generation-band {
position: absolute;
left: 12rpx;
display: grid;
justify-self: center;
z-index: 3;
width: 166rpx;
height: 58rpx;
@@ -411,14 +533,15 @@ const toAddRelative = () =>
font-size: 22rpx;
text-align: center;
}
.generation-band image,
.generation-band__copy {
grid-area: 1 / 1;
}
.generation-band image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.generation-band__copy {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
@@ -431,78 +554,15 @@ const toAddRelative = () =>
font-size: 19rpx;
}
.lineage-connector {
position: absolute;
align-self: stretch;
justify-self: stretch;
z-index: 1;
background: #b78a42;
}
.lineage-connector--root {
left: 449rpx;
top: 177rpx;
width: 2rpx;
height: 88rpx;
}
.lineage-connector--root-branch {
left: 275rpx;
top: 264rpx;
width: 350rpx;
height: 2rpx;
}
.lineage-connector--gen13-left {
left: 274rpx;
top: 264rpx;
width: 2rpx;
height: 44rpx;
}
.lineage-connector--gen13-right {
left: 624rpx;
top: 264rpx;
width: 2rpx;
height: 44rpx;
}
.lineage-connector--left-trunk {
left: 274rpx;
top: 392rpx;
width: 2rpx;
height: 149rpx;
}
.lineage-connector--left-branch {
left: 150rpx;
top: 540rpx;
width: 250rpx;
height: 2rpx;
}
.lineage-connector--gen14-left {
left: 149rpx;
top: 540rpx;
width: 2rpx;
height: 68rpx;
}
.lineage-connector--gen14-middle {
left: 399rpx;
top: 540rpx;
width: 2rpx;
height: 68rpx;
}
.lineage-connector--right-trunk {
left: 624rpx;
top: 392rpx;
width: 2rpx;
height: 149rpx;
}
.lineage-connector--right-branch {
left: 625rpx;
top: 540rpx;
width: 65rpx;
height: 2rpx;
}
.lineage-connector--gen14-right {
left: 689rpx;
top: 540rpx;
width: 2rpx;
height: 68rpx;
}
.member-node {
position: absolute;
display: grid;
align-self: start;
justify-self: start;
z-index: 2;
width: 224rpx;
height: 86rpx;
@@ -510,16 +570,19 @@ const toAddRelative = () =>
text-align: center;
}
.member-node__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.member-node__skin,
.member-node__copy {
grid-area: 1 / 1;
}
.member-node__copy {
z-index: 1;
}
.node-name,
.node-relation,
.node-years {
position: relative;
z-index: 1;
display: block;
}
.node-name {
@@ -547,22 +610,23 @@ const toAddRelative = () =>
color: $brand-red;
}
.tree-state-card {
position: relative;
display: grid;
z-index: 2;
width: calc(100% - 48rpx);
height: 360rpx;
min-height: 360rpx;
margin: 150rpx 24rpx 0;
text-align: center;
white-space: normal;
}
.tree-state-card__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.tree-state-card__skin,
.tree-state-card__content {
grid-area: 1 / 1;
}
.tree-state-card__content {
position: relative;
z-index: 1;
padding: 62rpx 58rpx 42rpx;
}
@@ -588,19 +652,20 @@ const toAddRelative = () =>
line-height: 1.65;
}
.tree-state-card__action {
position: relative;
display: grid;
width: 360rpx;
height: 70rpx;
min-height: 70rpx;
margin: 22rpx auto 0;
}
.tree-state-card__action image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.tree-state-card__action image,
.tree-state-card__action text {
grid-area: 1 / 1;
}
.tree-state-card__action text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
@@ -616,7 +681,7 @@ const toAddRelative = () =>
bottom: calc(14rpx + env(safe-area-inset-bottom));
left: 18rpx;
display: flex;
height: 240rpx;
min-height: 240rpx;
flex-direction: column;
padding: 38rpx 44rpx 24rpx;
box-sizing: border-box;
@@ -629,7 +694,6 @@ const toAddRelative = () =>
pointer-events: none;
}
.member-sheet__copy {
position: relative;
z-index: 1;
}
.sheet-name {
@@ -646,7 +710,6 @@ const toAddRelative = () =>
font-size: 22rpx;
}
.sheet-actions {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
@@ -667,7 +730,7 @@ const toAddRelative = () =>
gap: 14rpx;
}
.member-sheet {
height: 240rpx;
min-height: 240rpx;
padding-top: 32rpx;
padding-right: 36rpx;
padding-left: 36rpx;