修改完成
This commit is contained in:
+220
-157
@@ -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;
|
||||
|
||||
+123
-239
@@ -1,4 +1,4 @@
|
||||
<!-- 页面编号:T-03;用途:成员档案详情与失败状态(页面设计阶段使用本地模拟数据)。 -->
|
||||
<!-- 页面编号:T-03;用途:按 personId 展示成员档案并进入其真实成员状态。 -->
|
||||
<template>
|
||||
<view
|
||||
class="member-page"
|
||||
@@ -8,73 +8,57 @@
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="member-page__header"
|
||||
><PageHeader
|
||||
title="成员档案"
|
||||
:action="memberState === 'detail' ? '编辑' : ''"
|
||||
@action="toEdit"
|
||||
/></view>
|
||||
<view class="member-page__header">
|
||||
<PageHeader title="成员档案" :action="memberState === 'detail' && canEdit ? '编辑' : ''" @action="toEdit" />
|
||||
</view>
|
||||
<view class="member-context">
|
||||
<text>{{ genealogyName }}</text>
|
||||
<text>{{ memberState === "detail" ? "成员身份与亲属关系" : "请重新选择成员" }}</text>
|
||||
</view>
|
||||
|
||||
<view class="member-panel">
|
||||
<image
|
||||
class="member-panel__skin"
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<AppLoading
|
||||
v-if="memberState === 'loading'"
|
||||
text="正在读取成员档案"
|
||||
description="请稍候,正在整理成员资料。"
|
||||
/>
|
||||
<view v-else-if="memberState === 'detail'" class="member-detail">
|
||||
<AppLoading v-if="memberState === 'loading'" text="正在读取成员档案" description="请稍候,正在整理成员资料。" />
|
||||
|
||||
<view v-else-if="memberState === 'detail' && member" class="member-detail">
|
||||
<view class="member-heading">
|
||||
<view class="member-heading__seal"
|
||||
><image
|
||||
src="/static/assets/modules/genealogy/transparent/current-seal-frame.png"
|
||||
mode="scaleToFill"
|
||||
/><text>{{ member.name.slice(0, 1) }}</text></view
|
||||
>
|
||||
<view
|
||||
><text>{{ member.name }}</text
|
||||
><text
|
||||
>第 {{ member.generation }} 世 · {{ member.relation }} ·
|
||||
{{ member.branch }}</text
|
||||
></view
|
||||
>
|
||||
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
|
||||
<view>
|
||||
<text>{{ member.name }}</text>
|
||||
<text>第 {{ member.generation }} 世 · {{ member.relation }} · {{ member.branch }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-status-entry" @click="openMemberState(member.status)">
|
||||
<text>{{ statusLabel }}</text>
|
||||
<text>{{ statusDescription }}</text>
|
||||
<text>查看</text>
|
||||
</view>
|
||||
|
||||
<text class="member-section-title">基本资料</text>
|
||||
<view v-for="item in details" :key="item.label" class="member-info-row">
|
||||
<image
|
||||
src="/static/assets/modules/tree/transparent/t07-search-input-frame.png"
|
||||
mode="scaleToFill"
|
||||
/><text>{{ item.label }}</text
|
||||
><text>{{ item.value }}</text>
|
||||
<text>{{ item.label }}</text><text>{{ item.value || "未填写" }}</text>
|
||||
</view>
|
||||
<text class="member-section-title member-section-title--relation"
|
||||
>亲属关系</text
|
||||
>
|
||||
|
||||
<text class="member-section-title member-section-title--relation">亲属关系</text>
|
||||
<view class="member-relatives">
|
||||
<text>长子 · 汤正国</text><text>次子 · 汤正华</text>
|
||||
</view>
|
||||
<view class="member-profile-action" @click="toEdit">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>完善成员档案</text>
|
||||
<view v-for="relative in member.relatives" :key="relative.id" @click="openRelative(relative)">
|
||||
<text>{{ relative.relation }}</text><text>{{ relative.name }}</text>
|
||||
</view>
|
||||
<text v-if="!member.relatives.length">尚未记录可查看的亲属</text>
|
||||
</view>
|
||||
|
||||
<view v-if="canEdit" class="member-profile-action" @click="toEdit"><text>完善成员档案</text></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="member-error">
|
||||
<text>成员档案暂不可用</text
|
||||
><text>请从世系树重新选择成员,当前没有可展示的个人资料。</text>
|
||||
<view class="member-profile-action" @click="toTree"
|
||||
><image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>返回世系树</text></view
|
||||
>
|
||||
<text>成员档案暂不可用</text>
|
||||
<text>{{ errorMessage }}</text>
|
||||
<view class="member-profile-action" @click="toTree"><text>返回世系树</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
@@ -82,203 +66,103 @@ import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const memberState = ref("loading");
|
||||
const member = ref({
|
||||
id: 101,
|
||||
name: "汤文远",
|
||||
generation: 12,
|
||||
relation: "始祖",
|
||||
branch: "主支",
|
||||
generationName: "文字辈",
|
||||
birthDate: "1940年3月",
|
||||
years: "1940—2012",
|
||||
});
|
||||
const details = computed(() => [
|
||||
const member = ref(null);
|
||||
const errorMessage = ref("");
|
||||
const genealogyName = ref("汤氏家谱");
|
||||
|
||||
const memberFixtures = {
|
||||
101: {
|
||||
id: 101, name: "汤文远", generation: 12, relation: "始祖", branch: "主支", generationName: "文字辈",
|
||||
birthDate: "1940年3月", years: "1940—2012", birthplace: "河南南阳", status: "deceased", canEdit: true,
|
||||
relatives: [{ id: 102, name: "汤正国", relation: "长子" }, { id: 103, name: "汤正华", relation: "次子" }],
|
||||
},
|
||||
102: {
|
||||
id: 102, name: "汤正国", generation: 13, relation: "长子", branch: "长房", generationName: "正字辈",
|
||||
birthDate: "1965年5月", years: "1965—", birthplace: "河南洛阳", status: "privacy", canEdit: true,
|
||||
relatives: [{ id: 101, name: "汤文远", relation: "父亲" }, { id: 104, name: "汤凯", relation: "长子" }],
|
||||
},
|
||||
103: {
|
||||
id: 103, name: "汤正华", generation: 13, relation: "次子", branch: "二房", generationName: "正字辈",
|
||||
birthDate: "", years: "资料受限", birthplace: "", status: "forbidden", canEdit: false,
|
||||
relatives: [{ id: 101, name: "汤文远", relation: "父亲" }],
|
||||
},
|
||||
};
|
||||
const details = computed(() => member.value ? [
|
||||
{ label: "字辈", value: member.value.generationName },
|
||||
{ label: "出生日期", value: member.value.birthDate },
|
||||
{ label: "出生日期", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthDate },
|
||||
{ label: "生卒信息", value: member.value.years },
|
||||
{ label: "祖居地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthplace },
|
||||
{ label: "所属支系", value: member.value.branch },
|
||||
]);
|
||||
] : []);
|
||||
const canEdit = computed(() => Boolean(member.value?.canEdit));
|
||||
const statusLabel = computed(() => ({ privacy: "隐私资料", deceased: "离世纪念", forbidden: "访问受限" })[member.value?.status] || "成员状态");
|
||||
const statusDescription = computed(() => ({
|
||||
privacy: "部分资料仅向授权成员展示",
|
||||
deceased: "查看生平保留与纪念资料说明",
|
||||
forbidden: "当前账号只能查看有限身份信息",
|
||||
})[member.value?.status] || "查看成员状态说明");
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value =
|
||||
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value)
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
memberState.value =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
: query.state === "error" || !personId.value
|
||||
? "error"
|
||||
: "detail";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
if (query.state === "loading") { memberState.value = "loading"; return; }
|
||||
if (query.state === "error" || !personId.value || !memberFixtures[personId.value]) {
|
||||
memberState.value = "error";
|
||||
errorMessage.value = !personId.value ? "没有指定成员,请从世系树重新选择。" : "这位成员不存在或已不属于当前家谱。";
|
||||
return;
|
||||
}
|
||||
member.value = { ...memberFixtures[personId.value] };
|
||||
memberState.value = "detail";
|
||||
});
|
||||
const toEdit = () =>
|
||||
|
||||
const toEdit = () => {
|
||||
if (!canEdit.value) { openMemberState("forbidden"); return; }
|
||||
uni.navigateTo({ url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${personId.value}` });
|
||||
};
|
||||
const openMemberState = (state) =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${personId.value}`,
|
||||
url: `/pages/tree/t08-member-states?genealogyId=${genealogyId.value}&personId=${personId.value}&state=${state}`,
|
||||
});
|
||||
const openRelative = (relative) =>
|
||||
uni.navigateTo({ url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${relative.id}` });
|
||||
const toTree = () => uni.navigateBack();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.member-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.member-page__header {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
.member-panel {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
height: min(650px, calc((100vw - 16px) * 1.5));
|
||||
margin: 18rpx auto 0;
|
||||
}
|
||||
.member-panel__skin {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-detail {
|
||||
position: absolute;
|
||||
inset: 7% 8%;
|
||||
}
|
||||
.member-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.member-heading__seal {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 84rpx;
|
||||
height: 108rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.member-heading__seal image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-heading__seal text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #fff8ec;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 33rpx;
|
||||
}
|
||||
.member-heading > view:last-child text {
|
||||
display: block;
|
||||
}
|
||||
.member-heading > view:last-child text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-heading > view:last-child text:last-child {
|
||||
margin-top: 8rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.member-section-title {
|
||||
display: block;
|
||||
margin: 20rpx 0 4rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.member-info-row {
|
||||
position: relative;
|
||||
height: 62rpx;
|
||||
margin-top: 7rpx;
|
||||
}
|
||||
.member-info-row image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-info-row text {
|
||||
position: absolute;
|
||||
top: 21rpx;
|
||||
z-index: 1;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.member-info-row text:nth-child(2) {
|
||||
left: 22rpx;
|
||||
color: $ink-muted;
|
||||
}
|
||||
.member-info-row text:last-child {
|
||||
right: 22rpx;
|
||||
color: $ink;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-section-title--relation {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.member-relatives {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.member-profile-action {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.member-profile-action image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.member-profile-action text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff9ed;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-error {
|
||||
position: absolute;
|
||||
top: 31%;
|
||||
right: 12%;
|
||||
left: 12%;
|
||||
text-align: center;
|
||||
}
|
||||
.member-error > text {
|
||||
display: block;
|
||||
}
|
||||
.member-error > text:first-child {
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 35rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.member-error > text:nth-child(2) {
|
||||
margin-top: 18rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.member-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
}
|
||||
.member-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 28rpx; box-sizing: border-box; background: $paper; }
|
||||
.member-page__header, .member-context, .member-panel { z-index: 2; }
|
||||
.member-context { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.member-panel { width: calc(100% - 32rpx); min-height: min(620px, calc((100vw - 16px) * 1.42)); margin: 16rpx auto 0; padding: 9% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.member-panel > .app-loading { margin-top: 30%; }
|
||||
.member-heading { display: flex; align-items: center; gap: 20rpx; }
|
||||
.member-heading__seal { display: flex; width: 74rpx; height: 74rpx; flex: 0 0 74rpx; align-items: center; justify-content: center; background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat; }
|
||||
.member-heading__seal text { color: #fff7e7; font-size: 28rpx; }
|
||||
.member-heading > view:last-child text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-heading > view:last-child text:last-child { font-size: 23rpx; }
|
||||
.member-heading > view:last-child text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 36rpx; font-weight: 700; }
|
||||
.member-status-entry { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14rpx; margin-top: 18rpx; padding: 18rpx 22rpx; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-status-entry text { color: $ink-muted; font-size: 22rpx; line-height: 1.4; }
|
||||
.member-status-entry text:first-child, .member-status-entry text:last-child { color: $brand-red; font-weight: 700; }
|
||||
.member-section-title { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; font-weight: 700; }
|
||||
.member-info-row { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 64rpx; align-items: center; gap: 18rpx; margin-top: 8rpx; padding: 8rpx 18rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-info-row text { color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-info-row text:last-child { color: $ink; text-align: right; }
|
||||
.member-relatives { margin-top: 8rpx; font-size: 23rpx; }
|
||||
.member-relatives > view { display: flex; justify-content: space-between; gap: 20rpx; padding: 12rpx 18rpx; color: $ink; font-size: 23rpx; }
|
||||
.member-relatives > view text:last-child { color: $brand-red; }
|
||||
.member-relatives > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
|
||||
.member-profile-action { display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / 100% 100% no-repeat; }
|
||||
.member-profile-action text { color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.member-error { margin-top: 30%; text-align: center; }
|
||||
.member-error > text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
|
||||
.member-error > text:nth-child(2) { font-size: 24rpx; }
|
||||
.member-error > text:first-child { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; }
|
||||
@media (min-width: 400px) { .member-context, .member-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,244 @@
|
||||
<!-- 页面编号:T-04;用途:新增直系亲属与保存结果。 -->
|
||||
<template><TreeMemberForm kind="add" /></template>
|
||||
<!-- 页面编号:T-04;用途:录入首位成员或为指定成员新增亲属。 -->
|
||||
<template>
|
||||
<view
|
||||
class="add-relative-page"
|
||||
:class="{
|
||||
'add-state--form': addState === 'form',
|
||||
'add-state--success': addState === 'success',
|
||||
'add-state--error': addState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="add-relative-page__header">
|
||||
<PageHeader :title="isFirstMember ? '录入首位成员' : '新增亲属'" />
|
||||
</view>
|
||||
|
||||
<view class="add-relative-panel">
|
||||
<view v-if="addState === 'form'" class="add-relative-form">
|
||||
<text class="form-eyebrow">{{ isFirstMember ? "建立世系起点" : "补全家族关系" }}</text>
|
||||
<text class="form-title">{{ formTitle }}</text>
|
||||
<text class="form-copy">{{ formCopy }}</text>
|
||||
|
||||
<view v-if="!isFirstMember" class="member-context">
|
||||
<text>当前成员</text><text>{{ currentMember.name }} · 第 {{ currentMember.generation }} 世</text>
|
||||
</view>
|
||||
|
||||
<view class="form-field">
|
||||
<text>姓名</text>
|
||||
<input
|
||||
v-model="addForm.name"
|
||||
maxlength="20"
|
||||
placeholder="请输入真实姓名"
|
||||
placeholder-class="form-placeholder"
|
||||
@input="clearError('name')"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
|
||||
|
||||
<picker
|
||||
v-if="!isFirstMember"
|
||||
:range="relationOptions"
|
||||
:value="relationIndex"
|
||||
@change="selectRelation"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>与本人关系</text>
|
||||
<text>{{ addForm.relation || "请选择亲属关系" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.relation" class="field-error">{{ fieldErrors.relation }}</text>
|
||||
|
||||
<picker :range="genderOptions" :value="genderIndex" @change="selectGender">
|
||||
<view class="form-field form-field--picker">
|
||||
<text>性别</text><text>{{ addForm.gender || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.gender" class="field-error">{{ fieldErrors.gender }}</text>
|
||||
|
||||
<picker mode="date" :value="addForm.birthDate" @change="selectBirthDate">
|
||||
<view class="form-field form-field--picker">
|
||||
<text>出生日期</text><text>{{ addForm.birthDate || "选填" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<view class="form-field form-field--summary">
|
||||
<text>简要说明</text>
|
||||
<textarea
|
||||
v-model="addForm.summary"
|
||||
auto-height
|
||||
maxlength="200"
|
||||
placeholder="选填:字辈、祖居地或身份说明"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<text class="form-note">{{ formNote }}</text>
|
||||
<view class="form-action" @click="submitAdd">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在保存…" : isFirstMember ? "保存首位成员" : "保存亲属" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="add-result">
|
||||
<text class="form-eyebrow">{{ addState === "success" ? "世系资料已更新" : "成员未保存" }}</text>
|
||||
<text class="form-title">{{ addState === "success" ? successTitle : "暂时无法保存成员" }}</text>
|
||||
<text class="form-copy">{{ addState === "success" ? successCopy : "当前填写内容仍保留,可返回修改后重试。" }}</text>
|
||||
<view class="form-action" @click="addState === 'success' ? returnToTree() : retryForm()">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ addState === "success" ? "返回世系树" : "返回修改" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardDialogVisible"
|
||||
eyebrow="尚未保存"
|
||||
title="要放弃本次填写吗"
|
||||
message="返回后,本次新增成员的内容不会保留。"
|
||||
cancel-text="继续填写"
|
||||
confirm-text="放弃并返回"
|
||||
show-cancel
|
||||
@close="discardDialogVisible = false"
|
||||
@cancel="discardDialogVisible = false"
|
||||
@confirm="discardAndBack"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const addState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const mode = ref("relative");
|
||||
const isSubmitting = ref(false);
|
||||
const discardDialogVisible = ref(false);
|
||||
let submitTimer = null;
|
||||
|
||||
const memberFixtures = {
|
||||
101: { id: 101, name: "汤文远", generation: 12 },
|
||||
102: { id: 102, name: "汤正国", generation: 13 },
|
||||
103: { id: 103, name: "汤正华", generation: 13 },
|
||||
};
|
||||
const relationOptions = ["长子", "次子", "女儿", "配偶", "兄弟", "姐妹"];
|
||||
const genderOptions = ["男", "女", "未说明"];
|
||||
const addForm = reactive({ name: "", relation: "", gender: "", birthDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", relation: "", gender: "" });
|
||||
|
||||
const isFirstMember = computed(
|
||||
() => mode.value === "first" || (!personId.value && mode.value !== "relative"),
|
||||
);
|
||||
const currentMember = computed(
|
||||
() => memberFixtures[personId.value] || { id: personId.value, name: "当前成员", generation: "待确认" },
|
||||
);
|
||||
const relationIndex = computed(() => Math.max(0, relationOptions.indexOf(addForm.relation)));
|
||||
const genderIndex = computed(() => Math.max(0, genderOptions.indexOf(addForm.gender)));
|
||||
const formTitle = computed(() =>
|
||||
isFirstMember.value ? "录入家谱中的第一位成员" : `为${currentMember.value.name}添加一位亲属`,
|
||||
);
|
||||
const formCopy = computed(() =>
|
||||
isFirstMember.value
|
||||
? "首位成员将成为世系起点,后续可从此人继续补充配偶、子女和后代。"
|
||||
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
|
||||
);
|
||||
const formNote = computed(() =>
|
||||
isFirstMember.value
|
||||
? "保存后进入世系树,并可继续为首位成员添加亲属。"
|
||||
: "保存后成员会出现在相应世代;详细生平可在成员档案中继续完善。",
|
||||
);
|
||||
const successTitle = computed(() =>
|
||||
isFirstMember.value ? `${addForm.name}已成为世系起点` : `${addForm.name}已加入世系`,
|
||||
);
|
||||
const successCopy = computed(() =>
|
||||
isFirstMember.value
|
||||
? "首位成员已保存,世系树现在可以继续向下补充。"
|
||||
: `${addForm.relation}关系已记录,返回后可查看新的成员节点。`,
|
||||
);
|
||||
const hasDraft = computed(() =>
|
||||
Object.values(addForm).some((value) => String(value).trim()),
|
||||
);
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
mode.value = query.mode === "first" ? "first" : "relative";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
addState.value = query.state === "success" ? "success" : query.state === "error" ? "error" : "form";
|
||||
});
|
||||
onUnload(() => {
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
});
|
||||
onBackPress(() => {
|
||||
if (discardDialogVisible.value) {
|
||||
discardDialogVisible.value = false;
|
||||
return true;
|
||||
}
|
||||
if (addState.value === "form" && hasDraft.value) {
|
||||
discardDialogVisible.value = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectRelation = (event) => {
|
||||
addForm.relation = relationOptions[Number(event.detail.value)] || "";
|
||||
clearError("relation");
|
||||
};
|
||||
const selectGender = (event) => {
|
||||
addForm.gender = genderOptions[Number(event.detail.value)] || "";
|
||||
clearError("gender");
|
||||
};
|
||||
const selectBirthDate = (event) => { addForm.birthDate = event.detail.value || ""; };
|
||||
const validateAddForm = () => {
|
||||
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
|
||||
fieldErrors.relation = isFirstMember.value || addForm.relation ? "" : "请选择与当前成员的关系";
|
||||
fieldErrors.gender = addForm.gender ? "" : "请选择性别或未说明";
|
||||
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.gender;
|
||||
};
|
||||
const submitAdd = () => {
|
||||
if (isSubmitting.value || !validateAddForm()) return;
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
addState.value = addForm.name.trim() === "失败" ? "error" : "success";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
};
|
||||
const retryForm = () => { addState.value = "form"; };
|
||||
const returnToTree = () => uni.navigateBack();
|
||||
const discardAndBack = () => {
|
||||
discardDialogVisible.value = false;
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.add-relative-page__header { z-index: 3; }
|
||||
.add-relative-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(680px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
.member-context, .form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
|
||||
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
|
||||
.form-field--summary { align-items: start; }
|
||||
.form-placeholder { color: #a79884; }
|
||||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||||
.form-note { text-align: center; }
|
||||
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
|
||||
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.add-result { margin-top: 30%; text-align: center; }
|
||||
.add-result .form-eyebrow, .add-result .form-copy { text-align: center; }
|
||||
.add-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
|
||||
@media (min-width: 400px) { .add-relative-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,193 @@
|
||||
<!-- 页面编号:T-05;用途:编辑成员资料与保存结果。 -->
|
||||
<template><TreeMemberForm kind="edit" /></template>
|
||||
<!-- 页面编号:T-05;用途:维护指定成员的身份与生平资料。 -->
|
||||
<template>
|
||||
<view
|
||||
class="edit-member-page"
|
||||
:class="{
|
||||
'edit-state--form': editState === 'form',
|
||||
'edit-state--success': editState === 'success',
|
||||
'edit-state--error': editState === 'error',
|
||||
'edit-state--no-permission': editState === 'no-permission',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="edit-member-page__header"><PageHeader title="编辑成员" /></view>
|
||||
<view class="edit-member-panel">
|
||||
<view v-if="editState === 'form'" class="edit-member-form">
|
||||
<text class="form-eyebrow">成员档案维护</text>
|
||||
<text class="form-title">完善{{ originalMember.name }}的生命记录</text>
|
||||
<text class="form-copy">基础身份用于世系展示,生平说明会显示在有权限查看的成员档案中。</text>
|
||||
|
||||
<view class="member-context">
|
||||
<text>成员身份</text><text>第 {{ originalMember.generation }} 世 · {{ originalMember.branch }}</text>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text>姓名</text>
|
||||
<input v-model="editForm.name" maxlength="20" placeholder="请输入姓名" placeholder-class="form-placeholder" @input="clearError('name')" />
|
||||
</view>
|
||||
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
|
||||
|
||||
<view class="form-field">
|
||||
<text>字辈</text>
|
||||
<input v-model="editForm.generationName" maxlength="12" placeholder="例如:文字辈" placeholder-class="form-placeholder" />
|
||||
</view>
|
||||
<picker mode="date" :value="editForm.birthDate" @change="selectDate('birthDate', $event)">
|
||||
<view class="form-field form-field--picker"><text>出生日期</text><text>{{ editForm.birthDate || "未填写" }}</text></view>
|
||||
</picker>
|
||||
<picker mode="date" :value="editForm.deathDate" @change="selectDate('deathDate', $event)">
|
||||
<view class="form-field form-field--picker"><text>离世日期</text><text>{{ editForm.deathDate || "在世或未填写" }}</text></view>
|
||||
</picker>
|
||||
<view class="form-field form-field--summary">
|
||||
<text>人物简介</text>
|
||||
<textarea v-model="editForm.summary" auto-height maxlength="500" placeholder="记录生平、迁徙或重要经历" placeholder-class="form-placeholder" />
|
||||
</view>
|
||||
<text v-if="fieldErrors.dates" class="field-error">{{ fieldErrors.dates }}</text>
|
||||
|
||||
<text class="form-note">隐私字段只向本人和具备维护权限的家谱管理员展示。</text>
|
||||
<view class="form-action" @click="saveMember">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在保存…" : "保存资料" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="edit-result">
|
||||
<text class="form-eyebrow">{{ resultCopy.eyebrow }}</text>
|
||||
<text class="form-title">{{ resultCopy.title }}</text>
|
||||
<text class="form-copy">{{ resultCopy.copy }}</text>
|
||||
<view class="form-action" @click="handleResultAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ resultCopy.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardDialogVisible"
|
||||
eyebrow="资料尚未保存"
|
||||
title="要放弃本次修改吗"
|
||||
message="返回后,本次对成员档案的修改不会保留。"
|
||||
cancel-text="继续编辑"
|
||||
confirm-text="放弃修改"
|
||||
show-cancel
|
||||
@close="discardDialogVisible = false"
|
||||
@cancel="discardDialogVisible = false"
|
||||
@confirm="discardAndBack"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const editState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const discardDialogVisible = ref(false);
|
||||
let submitTimer = null;
|
||||
|
||||
const memberFixtures = {
|
||||
101: { id: 101, name: "汤文远", generation: 12, generationName: "文字辈", branch: "主支", birthDate: "1940-03-01", deathDate: "2012-08-16", summary: "一生敦亲睦族,参与整理家族旧谱。" },
|
||||
102: { id: 102, name: "汤正国", generation: 13, generationName: "正字辈", branch: "长房", birthDate: "1965-05-12", deathDate: "", summary: "负责长房资料核对。" },
|
||||
103: { id: 103, name: "汤正华", generation: 13, generationName: "正字辈", branch: "二房", birthDate: "1968-09-03", deathDate: "", summary: "资料仍在补充。" },
|
||||
};
|
||||
const fallbackMember = { id: "", name: "当前成员", generation: "待确认", generationName: "", branch: "待确认", birthDate: "", deathDate: "", summary: "" };
|
||||
const originalMember = ref({ ...fallbackMember });
|
||||
const baseline = ref("");
|
||||
const editForm = reactive({ name: "", generationName: "", birthDate: "", deathDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", dates: "" });
|
||||
|
||||
const formSnapshot = computed(() => JSON.stringify(editForm));
|
||||
const isDirty = computed(
|
||||
() => editState.value === "form" && baseline.value && formSnapshot.value !== baseline.value,
|
||||
);
|
||||
const resultCopy = computed(() => ({
|
||||
success: { eyebrow: "成员资料已更新", title: `${editForm.name}的档案已保存`, copy: "返回成员档案后可以查看本次修改。", action: "返回成员档案" },
|
||||
error: { eyebrow: "资料未保存", title: "暂时无法保存成员档案", copy: "当前修改仍保留,可返回表单后重试。", action: "返回修改" },
|
||||
"no-permission": { eyebrow: "权限不足", title: "当前账号不能编辑这位成员", copy: "本人或具备成员维护权限的家谱管理员才能修改档案。", action: "返回成员档案" },
|
||||
}[editState.value] || {}));
|
||||
|
||||
const loadMember = (id) => {
|
||||
const member = memberFixtures[id] || { ...fallbackMember, id, name: "待核实成员" };
|
||||
originalMember.value = { ...member };
|
||||
Object.assign(editForm, {
|
||||
name: member.name,
|
||||
generationName: member.generationName,
|
||||
birthDate: member.birthDate,
|
||||
deathDate: member.deathDate,
|
||||
summary: member.summary,
|
||||
});
|
||||
baseline.value = formSnapshot.value;
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
loadMember(personId.value);
|
||||
editState.value = query.state === "success" ? "success" : query.state === "error" ? "error" : query.state === "no-permission" ? "no-permission" : !personId.value ? "error" : "form";
|
||||
});
|
||||
onUnload(() => { if (submitTimer) clearTimeout(submitTimer); });
|
||||
onBackPress(() => {
|
||||
if (discardDialogVisible.value) { discardDialogVisible.value = false; return true; }
|
||||
if (isDirty.value) { discardDialogVisible.value = true; return true; }
|
||||
return false;
|
||||
});
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectDate = (field, event) => {
|
||||
editForm[field] = event.detail.value || "";
|
||||
fieldErrors.dates = "";
|
||||
};
|
||||
const validateEditForm = () => {
|
||||
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
|
||||
fieldErrors.dates = editForm.birthDate && editForm.deathDate && editForm.deathDate < editForm.birthDate ? "离世日期不能早于出生日期" : "";
|
||||
return !fieldErrors.name && !fieldErrors.dates;
|
||||
};
|
||||
const saveMember = () => {
|
||||
if (isSubmitting.value || !validateEditForm()) return;
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
editState.value = editForm.name.trim() === "失败" ? "error" : "success";
|
||||
if (editState.value === "success") baseline.value = formSnapshot.value;
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (editState.value === "error") { editState.value = "form"; return; }
|
||||
uni.navigateBack();
|
||||
};
|
||||
const discardAndBack = () => {
|
||||
discardDialogVisible.value = false;
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-member-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.edit-member-page__header { z-index: 3; }
|
||||
.edit-member-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(690px, calc((100vw - 16px) * 1.52)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
.member-context, .form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-context text:first-child, .form-field > text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.member-context text:last-child, .form-field > text:last-child, .form-field input, .form-field textarea { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
|
||||
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
|
||||
.form-field--summary { align-items: start; }
|
||||
.form-placeholder { color: #a79884; }
|
||||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||||
.form-note { text-align: center; }
|
||||
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
|
||||
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.edit-result { margin-top: 30%; text-align: center; }
|
||||
.edit-result .form-eyebrow, .edit-result .form-copy { text-align: center; }
|
||||
.edit-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
|
||||
@media (min-width: 400px) { .edit-member-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,215 @@
|
||||
<!-- 页面编号:T-06;用途:编辑亲属关系、冲突提示与处理。 -->
|
||||
<template><TreeMemberForm kind="relation" /></template>
|
||||
<!-- 页面编号:T-06;用途:选择两位现有成员并校正其家族关系。 -->
|
||||
<template>
|
||||
<view
|
||||
class="relationship-page"
|
||||
:class="{
|
||||
'relationship-state--form': relationshipState === 'form',
|
||||
'relationship-state--success': relationshipState === 'success',
|
||||
'relationship-state--conflict': relationshipState === 'conflict',
|
||||
'relationship-state--error': relationshipState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="relationship-page__header"><PageHeader title="关系维护" /></view>
|
||||
<view class="relationship-panel">
|
||||
<view v-if="relationshipState === 'form'" class="relationship-form">
|
||||
<text class="form-eyebrow">亲属关系校正</text>
|
||||
<text class="form-title">确认两位成员的家族关系</text>
|
||||
<text class="form-copy">选择成员和关系类型后,先查看对世系的影响,再决定是否保存。</text>
|
||||
|
||||
<picker :range="memberLabels" :value="sourceIndex" @change="selectMember('sourceId', $event)">
|
||||
<view class="form-field form-field--picker"><text>当前成员</text><text>{{ memberName(relationshipForm.sourceId) || "请选择成员" }}</text></view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.sourceId" class="field-error">{{ fieldErrors.sourceId }}</text>
|
||||
|
||||
<picker :range="memberLabels" :value="targetIndex" @change="selectMember('targetId', $event)">
|
||||
<view class="form-field form-field--picker"><text>关联成员</text><text>{{ memberName(relationshipForm.targetId) || "请选择另一位成员" }}</text></view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.targetId" class="field-error">{{ fieldErrors.targetId }}</text>
|
||||
|
||||
<picker :range="relationshipOptions" :value="relationshipIndex" @change="selectRelationship">
|
||||
<view class="form-field form-field--picker"><text>关系类型</text><text>{{ relationshipForm.relationship || "请选择关系" }}</text></view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.relationship" class="field-error">{{ fieldErrors.relationship }}</text>
|
||||
|
||||
<view class="relationship-preview">
|
||||
<text>关系影响预览</text>
|
||||
<text>{{ relationshipPreview }}</text>
|
||||
</view>
|
||||
|
||||
<text class="form-note">父母子女关系会改变世系位置;配偶和兄弟姐妹关系不会自动改写现有父母。</text>
|
||||
<view class="form-action" @click="saveRelationship">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在校验…" : "校验并保存关系" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="relationship-result">
|
||||
<text class="form-eyebrow">{{ resultCopy.eyebrow }}</text>
|
||||
<text class="form-title">{{ resultCopy.title }}</text>
|
||||
<text class="form-copy">{{ resultCopy.copy }}</text>
|
||||
<view v-if="relationshipState === 'conflict'" class="result-actions">
|
||||
<view class="form-action form-action--secondary" @click="relationshipState = 'form'">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png" mode="scaleToFill" /><text>返回核对</text>
|
||||
</view>
|
||||
<view class="form-action" @click="conflictDialogVisible = true">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" /><text>查看规则</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="form-action" @click="handleResultAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ relationshipState === "success" ? "返回世系树" : "重新选择" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="conflictDialogVisible"
|
||||
eyebrow="关系校验规则"
|
||||
title="为什么不能保存这段关系"
|
||||
:message="conflictReason || '同一成员不能成为自己的亲属,也不能形成上下代循环或重复父母关系。'"
|
||||
@confirm="conflictDialogVisible = false"
|
||||
@close="conflictDialogVisible = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const relationshipState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const conflictDialogVisible = ref(false);
|
||||
const conflictReason = ref("");
|
||||
let submitTimer = null;
|
||||
|
||||
const memberOptions = [
|
||||
{ id: "101", name: "汤文远", generation: 12, parentId: "" },
|
||||
{ id: "102", name: "汤正国", generation: 13, parentId: "101" },
|
||||
{ id: "103", name: "汤正华", generation: 13, parentId: "101" },
|
||||
{ id: "104", name: "汤凯", generation: 14, parentId: "102" },
|
||||
{ id: "105", name: "汤悦", generation: 14, parentId: "102" },
|
||||
];
|
||||
const relationshipOptions = ["父子(当前成员为父)", "父女(当前成员为父)", "母子(当前成员为母)", "母女(当前成员为母)", "配偶", "兄弟姐妹"];
|
||||
const relationshipForm = reactive({ sourceId: "", targetId: "", relationship: "" });
|
||||
const fieldErrors = reactive({ sourceId: "", targetId: "", relationship: "" });
|
||||
const memberLabels = computed(() => memberOptions.map((item) => `${item.name} · 第 ${item.generation} 世`));
|
||||
const memberById = computed(() => new Map(memberOptions.map((item) => [item.id, item])));
|
||||
const sourceIndex = computed(() => Math.max(0, memberOptions.findIndex((item) => item.id === relationshipForm.sourceId)));
|
||||
const targetIndex = computed(() => Math.max(0, memberOptions.findIndex((item) => item.id === relationshipForm.targetId)));
|
||||
const relationshipIndex = computed(() => Math.max(0, relationshipOptions.indexOf(relationshipForm.relationship)));
|
||||
const memberName = (id) => memberById.value.get(String(id))?.name || "";
|
||||
const relationshipPreview = computed(
|
||||
() => relationshipForm.sourceId && relationshipForm.targetId && relationshipForm.relationship
|
||||
? `${memberName(relationshipForm.sourceId)}将以“${relationshipForm.relationship}”关联${memberName(relationshipForm.targetId)}。`
|
||||
: "完成三项选择后,这里会说明世系位置将如何变化。",
|
||||
);
|
||||
const resultCopy = computed(() => ({
|
||||
success: { eyebrow: "关系已保存", title: "世系关系已经更新", copy: relationshipPreview.value },
|
||||
conflict: { eyebrow: "发现关系冲突", title: "这段关系会造成世系矛盾", copy: conflictReason.value },
|
||||
error: { eyebrow: "关系未保存", title: "暂时无法完成关系调整", copy: "当前选择仍然保留,可返回后重新校验。" },
|
||||
}[relationshipState.value] || {}));
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
relationshipForm.sourceId = personId.value && memberById.value.has(String(personId.value)) ? String(personId.value) : memberOptions[0].id;
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
relationshipState.value = query.state === "success" ? "success" : query.state === "conflict" ? "conflict" : query.state === "error" ? "error" : "form";
|
||||
if (relationshipState.value === "conflict") conflictReason.value = "目标成员已经存在父级关系,请先核对原关系。";
|
||||
});
|
||||
onUnload(() => { if (submitTimer) clearTimeout(submitTimer); });
|
||||
|
||||
const clearFieldError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectMember = (field, event) => {
|
||||
relationshipForm[field] = memberOptions[Number(event.detail.value)]?.id || "";
|
||||
clearFieldError(field);
|
||||
};
|
||||
const selectRelationship = (event) => {
|
||||
relationshipForm.relationship = relationshipOptions[Number(event.detail.value)] || "";
|
||||
clearFieldError("relationship");
|
||||
};
|
||||
const isAncestor = (possibleAncestorId, memberId) => {
|
||||
let current = memberById.value.get(String(memberId));
|
||||
const visited = new Set();
|
||||
while (current?.parentId && !visited.has(current.id)) {
|
||||
if (current.parentId === String(possibleAncestorId)) return true;
|
||||
visited.add(current.id);
|
||||
current = memberById.value.get(current.parentId);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const validateRelationship = () => {
|
||||
fieldErrors.sourceId = relationshipForm.sourceId ? "" : "请选择当前成员";
|
||||
fieldErrors.targetId = relationshipForm.targetId ? "" : "请选择关联成员";
|
||||
fieldErrors.relationship = relationshipForm.relationship ? "" : "请选择关系类型";
|
||||
if (fieldErrors.sourceId || fieldErrors.targetId || fieldErrors.relationship) return false;
|
||||
if (relationshipForm.sourceId === relationshipForm.targetId) {
|
||||
conflictReason.value = "同一成员不能与自己建立亲属关系。";
|
||||
return false;
|
||||
}
|
||||
const isParentRelationship = relationshipForm.relationship.includes("当前成员为");
|
||||
const target = memberById.value.get(relationshipForm.targetId);
|
||||
if (isParentRelationship && isAncestor(relationshipForm.targetId, relationshipForm.sourceId)) {
|
||||
conflictReason.value = "保存后会形成上下代循环,请重新选择成员方向。";
|
||||
return false;
|
||||
}
|
||||
if (isParentRelationship && target?.parentId && target.parentId !== relationshipForm.sourceId) {
|
||||
conflictReason.value = `${target.name}已经存在父级成员,不能直接重复建立父母关系。`;
|
||||
return false;
|
||||
}
|
||||
conflictReason.value = "";
|
||||
return true;
|
||||
};
|
||||
const saveRelationship = () => {
|
||||
if (isSubmitting.value) return;
|
||||
if (!validateRelationship()) {
|
||||
if (conflictReason.value) relationshipState.value = "conflict";
|
||||
return;
|
||||
}
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
relationshipState.value = relationshipForm.relationship === "失败" ? "error" : "success";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (relationshipState.value === "error") { relationshipState.value = "form"; return; }
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.relationship-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
|
||||
.relationship-page__header { z-index: 3; }
|
||||
.relationship-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(650px, calc((100vw - 16px) * 1.46)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||||
.form-field { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.form-field text:first-child { color: $ink; font-size: 24rpx; font-weight: 700; }
|
||||
.form-field text:last-child { min-width: 0; color: $ink; font-size: 24rpx; line-height: 1.45; text-align: right; }
|
||||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||||
.relationship-preview { margin-top: 18rpx; padding: 20rpx 22rpx; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
|
||||
.relationship-preview text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.5; }
|
||||
.relationship-preview text:first-child { color: $brand-red; font-weight: 700; }
|
||||
.form-note { text-align: center; }
|
||||
.form-action { display: grid; width: 100%; min-height: 76rpx; margin-top: 18rpx; }
|
||||
.form-action image, .form-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.form-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.form-action--secondary text { color: $ink; }
|
||||
.relationship-result { margin-top: 28%; text-align: center; }
|
||||
.relationship-result .form-eyebrow, .relationship-result .form-copy { text-align: center; }
|
||||
.relationship-result > .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
|
||||
.result-actions { display: flex; gap: 14rpx; margin-top: 24rpx; }
|
||||
.result-actions .form-action { width: calc(50% - 7rpx); margin-top: 0; }
|
||||
@media (min-width: 400px) { .relationship-panel { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
v-if="directoryState === 'list' || directoryState === 'empty'"
|
||||
class="directory-search"
|
||||
>
|
||||
<image
|
||||
class="directory-search__frame"
|
||||
src="/static/assets/modules/tree/transparent/t07-search-input-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<input
|
||||
v-model="keyword"
|
||||
aria-label="成员搜索关键词"
|
||||
@@ -59,11 +54,6 @@
|
||||
class="directory-card"
|
||||
@click="openMember(item)"
|
||||
>
|
||||
<image
|
||||
class="directory-card__frame"
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="directory-card__copy">
|
||||
<text class="directory-card__name">{{ item.name }}</text>
|
||||
<text class="directory-card__meta"
|
||||
@@ -75,10 +65,6 @@
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="directory-state-card">
|
||||
<image
|
||||
src="/static/assets/modules/genealogy/transparent/list-slip-frame.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
><text>{{
|
||||
directoryState === "empty" ? "没有找到相关成员" : "成员目录暂不可用"
|
||||
@@ -167,9 +153,9 @@ const openMember = (item) =>
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.directory-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background: $paper;
|
||||
}
|
||||
@@ -177,7 +163,6 @@ const openMember = (item) =>
|
||||
.directory-context,
|
||||
.directory-search,
|
||||
.directory-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.directory-context {
|
||||
@@ -199,39 +184,33 @@ const openMember = (item) =>
|
||||
font-weight: 500;
|
||||
}
|
||||
.directory-search {
|
||||
display: grid;
|
||||
width: calc(100% - 48rpx);
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
margin: 20rpx auto 0;
|
||||
}
|
||||
.directory-search__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.directory-search input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 110rpx;
|
||||
bottom: 0;
|
||||
left: 26rpx;
|
||||
z-index: 1;
|
||||
height: 44px;
|
||||
grid-area: 1 / 1;
|
||||
min-height: 44px;
|
||||
margin-right: 110rpx;
|
||||
margin-left: 26rpx;
|
||||
color: $ink;
|
||||
font-size: 26rpx;
|
||||
line-height: 44px;
|
||||
}
|
||||
.directory-search__action {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 8rpx;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
grid-area: 1 / 1;
|
||||
justify-self: end;
|
||||
min-width: 88rpx;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8rpx;
|
||||
color: $brand-red;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
@@ -258,25 +237,17 @@ const openMember = (item) =>
|
||||
font-size: 23rpx;
|
||||
}
|
||||
.directory-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 196rpx;
|
||||
min-height: 196rpx;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 18rpx 28rpx;
|
||||
}
|
||||
.directory-card__frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.directory-card__copy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -285,21 +256,19 @@ const openMember = (item) =>
|
||||
justify-content: center;
|
||||
}
|
||||
.directory-card__name {
|
||||
overflow: hidden;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.directory-card__meta {
|
||||
overflow: hidden;
|
||||
margin-top: 7rpx;
|
||||
color: #62584c;
|
||||
font-size: 24rpx;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.directory-card__status {
|
||||
align-self: flex-start;
|
||||
@@ -311,23 +280,21 @@ const openMember = (item) =>
|
||||
line-height: 30rpx;
|
||||
}
|
||||
.directory-state-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
min-height: 220rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.directory-state-card > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 48rpx 12%;
|
||||
box-sizing: border-box;
|
||||
background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png")
|
||||
center / 100% 100% no-repeat;
|
||||
}
|
||||
.directory-state-card {
|
||||
margin-top: 70rpx;
|
||||
}
|
||||
.directory-state-card > view {
|
||||
position: absolute;
|
||||
inset: 24% 12%;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -351,7 +318,7 @@ const openMember = (item) =>
|
||||
}
|
||||
@media screen and (max-width: 340px) {
|
||||
.directory-card {
|
||||
height: 192rpx;
|
||||
min-height: 192rpx;
|
||||
padding-right: 22rpx;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 页面编号:T-08;用途:成员隐私、离世纪念与无权限状态说明。 -->
|
||||
<!-- 页面编号:T-08;用途:展示指定成员的隐私、纪念或无权限状态。 -->
|
||||
<template>
|
||||
<view
|
||||
class="member-status-page"
|
||||
@@ -6,220 +6,129 @@
|
||||
'member-status--privacy': statusState === 'privacy',
|
||||
'member-status--deceased': statusState === 'deceased',
|
||||
'member-status--forbidden': statusState === 'forbidden',
|
||||
'member-status--error': statusState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="member-status-page__header"
|
||||
><PageHeader title="成员状态"
|
||||
/></view>
|
||||
<view class="status-tabs">
|
||||
<view
|
||||
v-for="item in tabs"
|
||||
:key="item.value"
|
||||
class="status-tab"
|
||||
@click="statusState = item.value"
|
||||
>
|
||||
<image
|
||||
:src="
|
||||
statusState === item.value
|
||||
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
|
||||
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
|
||||
"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
:class="{ 'status-tab__text--active': statusState === item.value }"
|
||||
>{{ item.label }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="member-status-page__header"><PageHeader :title="pageTitle" /></view>
|
||||
|
||||
<view class="member-status-context">
|
||||
<text>{{ genealogyName }}</text>
|
||||
<text v-if="member">{{ member.name }} · 第 {{ member.generation }} 世 · {{ member.branch }}</text>
|
||||
<text v-else>未找到成员身份</text>
|
||||
</view>
|
||||
|
||||
<view class="status-card">
|
||||
<image
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view class="status-card__copy">
|
||||
<text>{{ activeStatus.eyebrow }}</text
|
||||
><text>{{ activeStatus.title }}</text
|
||||
><text>{{ activeStatus.copy }}</text>
|
||||
<text>{{ activeStatus.eyebrow }}</text>
|
||||
<text>{{ activeStatus.title }}</text>
|
||||
<text>{{ activeStatus.copy }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-guidance">
|
||||
<image
|
||||
src="/static/assets/modules/tree/transparent/t01-state-panel.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view>
|
||||
<text>{{ activeStatus.guideTitle }}</text>
|
||||
<text v-for="line in activeStatus.guides" :key="line">{{ line }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="status-action" @click="handleAction">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ activeStatus.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
const statusState = ref("privacy");
|
||||
const tabs = [
|
||||
{ value: "privacy", label: "隐私" },
|
||||
{ value: "deceased", label: "纪念" },
|
||||
{ value: "forbidden", label: "无权限" },
|
||||
];
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const statusState = ref("loading");
|
||||
const genealogyName = ref("汤氏家谱");
|
||||
const member = ref(null);
|
||||
|
||||
const memberFixtures = {
|
||||
101: { id: 101, name: "汤文远", generation: 12, branch: "主支", allowedStates: ["deceased"] },
|
||||
102: { id: 102, name: "汤正国", generation: 13, branch: "长房", allowedStates: ["privacy"] },
|
||||
103: { id: 103, name: "汤正华", generation: 13, branch: "二房", allowedStates: ["forbidden"] },
|
||||
};
|
||||
const states = {
|
||||
privacy: {
|
||||
eyebrow: "隐私成员",
|
||||
title: "敏感资料只向授权成员展示",
|
||||
copy: "姓名可保留在世系位置,出生信息、联系方式和生平内容根据权限隐藏。",
|
||||
guideTitle: "隐私展示原则",
|
||||
guides: [
|
||||
"本人可以查看和维护自己的完整档案",
|
||||
"谱主可协助处理身份与世系关系",
|
||||
"普通成员只看到被允许公开的内容",
|
||||
],
|
||||
copy: "该成员姓名和世系位置仍然保留,出生信息、联系方式和生平内容按权限隐藏。",
|
||||
guideTitle: "当前可见范围",
|
||||
guides: ["姓名、世代与所属支系可见", "联系方式和详细生平已隐藏", "本人或谱主可维护授权范围"],
|
||||
action: "返回成员档案",
|
||||
},
|
||||
deceased: {
|
||||
eyebrow: "离世成员",
|
||||
title: "在世系中保留温和的纪念状态",
|
||||
copy: "离世不会删除成员关系;档案可继续记录生平、影像和家人的追思内容。",
|
||||
eyebrow: "离世纪念",
|
||||
title: "这位家人的生命记录被温和保留",
|
||||
copy: "离世状态不会删除成员关系;有权限的家人仍可共同维护生卒年月、生平和追思资料。",
|
||||
guideTitle: "纪念资料范围",
|
||||
guides: [
|
||||
"生卒年月与世系关系继续保留",
|
||||
"生平内容由有权限家人共同维护",
|
||||
"敏感资料仍遵守原有隐私设置",
|
||||
],
|
||||
guides: ["生卒年月与世系关系继续保留", "生平内容由有权限家人维护", "敏感资料继续遵守原有隐私设置"],
|
||||
action: "返回成员档案",
|
||||
},
|
||||
forbidden: {
|
||||
eyebrow: "访问受限",
|
||||
title: "当前账号没有查看该档案的权限",
|
||||
copy: "为保护家人隐私,页面不会展示被隐藏的字段,也不会提供绕过权限的入口。",
|
||||
copy: "页面不会展示被隐藏字段,也不会提供绕过家谱权限的入口。",
|
||||
guideTitle: "如何申请查看",
|
||||
guides: [
|
||||
"先确认已经加入对应家谱",
|
||||
"联系谱主说明亲属关系和用途",
|
||||
"权限变更后重新进入成员档案",
|
||||
],
|
||||
guides: ["先确认已经加入对应家谱", "联系谱主说明亲属关系和用途", "权限变更后重新进入成员档案"],
|
||||
action: "返回我的家谱",
|
||||
},
|
||||
error: {
|
||||
eyebrow: "成员状态不可用",
|
||||
title: "没有找到要查看的成员",
|
||||
copy: "请从成员档案或世系树重新选择成员。",
|
||||
guideTitle: "重新进入方式",
|
||||
guides: ["从世系树选择成员节点", "从成员目录打开成员档案", "确认当前家谱仍然有效"],
|
||||
action: "返回上一页",
|
||||
},
|
||||
};
|
||||
const activeStatus = computed(() => states[statusState.value]);
|
||||
const activeStatus = computed(() => states[statusState.value] || states.error);
|
||||
const pageTitle = computed(() => statusState.value === "deceased" ? "成员纪念" : statusState.value === "privacy" ? "隐私资料" : "成员状态");
|
||||
|
||||
onLoad((query) => {
|
||||
statusState.value = states[query.state] ? query.state : "privacy";
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
member.value = memberFixtures[personId.value] || null;
|
||||
const requestedState = ["privacy", "deceased", "forbidden"].includes(query.state) ? query.state : "";
|
||||
statusState.value = member.value?.allowedStates.includes(requestedState) ? requestedState : "error";
|
||||
});
|
||||
|
||||
const handleAction = () => {
|
||||
if (statusState.value === "forbidden") {
|
||||
uni.reLaunch({ url: "/pages/genealogy/g01-my-genealogies" });
|
||||
return;
|
||||
}
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.member-status-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: $paper;
|
||||
}
|
||||
.member-status-page__header,
|
||||
.status-tabs,
|
||||
.status-card,
|
||||
.status-guidance {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
padding: 22rpx 24rpx 0;
|
||||
}
|
||||
.status-tab {
|
||||
position: relative;
|
||||
width: calc(33.333% - 7rpx);
|
||||
height: 62rpx;
|
||||
}
|
||||
.status-tab image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-tab text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-tab .status-tab__text--active {
|
||||
color: #fff9ed;
|
||||
}
|
||||
.status-card {
|
||||
width: calc(100% - 48rpx);
|
||||
height: calc((100vw - 24px) * 0.34286);
|
||||
min-height: 224rpx;
|
||||
margin: 24rpx auto 0;
|
||||
}
|
||||
.status-card > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-card__copy {
|
||||
position: absolute;
|
||||
inset: 18% 10%;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.status-card__copy text {
|
||||
display: block;
|
||||
}
|
||||
.status-card__copy text:first-child {
|
||||
color: $brand-red;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
.status-card__copy text:nth-child(2) {
|
||||
margin-top: 7rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-card__copy text:last-child {
|
||||
margin-top: 10rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status-guidance {
|
||||
width: calc(100% - 48rpx);
|
||||
height: min(400px, calc((100vw - 24px) * 0.9));
|
||||
margin: 18rpx auto 0;
|
||||
}
|
||||
.status-guidance > image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status-guidance > view {
|
||||
position: absolute;
|
||||
inset: 12% 11%;
|
||||
z-index: 1;
|
||||
}
|
||||
.status-guidance text {
|
||||
display: block;
|
||||
color: $ink-muted;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.status-guidance text:first-child {
|
||||
margin-bottom: 18rpx;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 31rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-guidance text:not(:first-child) {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.member-status-page { display: flex; min-height: 100vh; flex-direction: column; padding-bottom: 34rpx; box-sizing: border-box; background: $paper; }
|
||||
.member-status-page__header, .member-status-context, .status-card, .status-guidance, .status-action { z-index: 2; }
|
||||
.member-status-context { width: calc(100% - 32rpx); margin: 18rpx auto 0; padding: 16rpx 24rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
|
||||
.member-status-context text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.45; }
|
||||
.member-status-context text:first-child { color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.status-card { width: calc(100% - 32rpx); min-height: 330rpx; margin: 18rpx auto 0; padding: 12% 10%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
|
||||
.status-card__copy text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.55; text-align: center; }
|
||||
.status-card__copy text:first-child { color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||||
.status-card__copy text:nth-child(2) { margin-top: 12rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; }
|
||||
.status-card__copy text:last-child { margin-top: 14rpx; font-size: 24rpx; }
|
||||
.status-guidance { width: calc(100% - 48rpx); margin: 16rpx auto 0; padding: 24rpx 28rpx; box-sizing: border-box; background: url("/static/assets/modules/genealogy/transparent/list-slip-frame.png") center / 100% 100% no-repeat; }
|
||||
.status-guidance text { display: block; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
|
||||
.status-guidance text:first-child { margin-bottom: 8rpx; color: $ink; font-size: 26rpx; font-weight: 700; }
|
||||
.status-action { display: grid; width: calc(100% - 72rpx); min-height: 76rpx; margin: 22rpx auto 0; }
|
||||
.status-action image, .status-action text { grid-area: 1 / 1; width: 100%; height: 100%; }
|
||||
.status-action text { z-index: 1; display: flex; align-items: center; justify-content: center; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
@media (min-width: 400px) { .member-status-context, .status-card { width: calc(100% - 48rpx); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user