完成50%
This commit is contained in:
@@ -182,7 +182,9 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { getGenealogyFixtureAccess, listTreeMemberFixtures } from "@/data/mock.js";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
import { openPage } from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const treeState = ref("loading");
|
||||
@@ -202,61 +204,7 @@ const generationMeta = {
|
||||
13: { label: "第十三世", summary: "两房" },
|
||||
14: { label: "第十四世", summary: "三支" },
|
||||
};
|
||||
const members = ref([
|
||||
{
|
||||
id: 101,
|
||||
name: "汤文远",
|
||||
relation: "始祖",
|
||||
years: "1940—2012",
|
||||
generation: 12,
|
||||
branch: "主支",
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: "汤正国",
|
||||
relation: "长子",
|
||||
years: "1965—",
|
||||
generation: 13,
|
||||
parentId: 101,
|
||||
branch: "长房",
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: "汤正华",
|
||||
relation: "次子",
|
||||
years: "1968—",
|
||||
generation: 13,
|
||||
parentId: 101,
|
||||
branch: "二房",
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
name: "汤凯",
|
||||
relation: "长孙",
|
||||
years: "1992—",
|
||||
generation: 14,
|
||||
parentId: 102,
|
||||
branch: "长房",
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
name: "汤悦",
|
||||
relation: "长孙女",
|
||||
years: "1995—",
|
||||
generation: 14,
|
||||
parentId: 102,
|
||||
branch: "长房",
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
name: "汤晨",
|
||||
relation: "次孙",
|
||||
years: "1998—",
|
||||
generation: 14,
|
||||
parentId: 103,
|
||||
branch: "二房",
|
||||
},
|
||||
]);
|
||||
const members = ref([]);
|
||||
|
||||
const snapToGrid = (value) => Math.ceil(value / GRID_UNIT) * GRID_UNIT;
|
||||
const layoutMembers = computed(() => {
|
||||
@@ -404,8 +352,29 @@ const stateCopy = computed(
|
||||
);
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value =
|
||||
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
if (genealogyContext.isCurrentGenealogyInvalidated()) {
|
||||
genealogyId.value = "";
|
||||
members.value = [];
|
||||
treeState.value = "error";
|
||||
return;
|
||||
}
|
||||
genealogyId.value = String(
|
||||
query.genealogyId || genealogyContext.getCurrentGenealogyId() || "",
|
||||
);
|
||||
if (!genealogyId.value) {
|
||||
members.value = [];
|
||||
treeState.value = "empty";
|
||||
return;
|
||||
}
|
||||
const access = getGenealogyFixtureAccess(genealogyId.value);
|
||||
if (!["owner", "member"].includes(access.accessRole)) {
|
||||
genealogyContext.invalidateCurrentGenealogyId();
|
||||
genealogyId.value = "";
|
||||
members.value = [];
|
||||
treeState.value = "error";
|
||||
return;
|
||||
}
|
||||
members.value = listTreeMemberFixtures(genealogyId.value);
|
||||
if (
|
||||
query.state === "loading" ||
|
||||
["landscape", "empty", "error"].includes(query.state)
|
||||
@@ -413,16 +382,12 @@ onLoad((query) => {
|
||||
treeState.value = query.state;
|
||||
return;
|
||||
}
|
||||
if (!genealogyId.value) {
|
||||
treeState.value = "empty";
|
||||
return;
|
||||
}
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
selected.value =
|
||||
layoutMembers.value.find(
|
||||
(item) => String(item.id) === String(query.selectedId),
|
||||
) || layoutMembers.value[0];
|
||||
treeState.value = "tree";
|
||||
treeState.value = members.value.length ? "tree" : "empty";
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
@@ -471,30 +436,30 @@ const nodeGridStyle = (member) => ({
|
||||
});
|
||||
const handleStateAction = () => {
|
||||
if (treeState.value === "empty") {
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value || 1001}&mode=first`,
|
||||
});
|
||||
return;
|
||||
if (!genealogyId.value) return Promise.resolve(false);
|
||||
return openPage(
|
||||
"T04",
|
||||
{ genealogyId: genealogyId.value, mode: "first" },
|
||||
"T01",
|
||||
);
|
||||
}
|
||||
selected.value = layoutMembers.value[0];
|
||||
treeState.value = "tree";
|
||||
};
|
||||
const toMember = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${selected.value.id}`,
|
||||
});
|
||||
selected.value
|
||||
? openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")
|
||||
: Promise.resolve(false);
|
||||
const toDirectory = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t07-member-directory?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
openPage("T07", { genealogyId: genealogyId.value }, "T01");
|
||||
const toRelationship = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t06-edit-relationship?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
selected.value
|
||||
? openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")
|
||||
: Promise.resolve(false);
|
||||
const toAddRelative = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value}&personId=${selected.value.id}`,
|
||||
});
|
||||
selected.value
|
||||
? openPage("T04", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")
|
||||
: Promise.resolve(false);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,18 +2,27 @@
|
||||
<template>
|
||||
<view
|
||||
class="member-page"
|
||||
:data-current-person-id="personId"
|
||||
:data-trail-length="memberTrail.length"
|
||||
:data-trail-index="trailIndex"
|
||||
:class="{
|
||||
'member-state--detail': memberState === 'detail',
|
||||
'member-state--restricted': memberState === 'restricted',
|
||||
'member-state--error': memberState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="member-page__header">
|
||||
<PageHeader title="成员档案" :action="memberState === 'detail' && canEdit ? '编辑' : ''" @action="toEdit" />
|
||||
<PageHeader title="成员档案"
|
||||
:action="canPreviewEdit ? '编辑预览' : ''"
|
||||
custom-back
|
||||
@back="requestBack"
|
||||
@action="toEdit"
|
||||
/>
|
||||
</view>
|
||||
<view class="member-context">
|
||||
<text>{{ genealogyName }}</text>
|
||||
<text>{{ memberState === "detail" ? "成员身份与亲属关系" : "请重新选择成员" }}</text>
|
||||
<text>{{ memberContextDescription }}</text>
|
||||
</view>
|
||||
|
||||
<view class="member-panel">
|
||||
@@ -28,7 +37,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-status-entry" @click="openMemberState(member.status)">
|
||||
<view class="member-status-entry" @click="openMemberState">
|
||||
<text>{{ statusLabel }}</text>
|
||||
<text>{{ statusDescription }}</text>
|
||||
<text>查看</text>
|
||||
@@ -41,13 +50,36 @@
|
||||
|
||||
<text class="member-section-title member-section-title--relation">亲属关系</text>
|
||||
<view class="member-relatives">
|
||||
<view v-for="relative in member.relatives" :key="relative.id" @click="openRelative(relative)">
|
||||
<view
|
||||
v-for="relative in member.relatives"
|
||||
:key="relative.id"
|
||||
:data-person-id="relative.id"
|
||||
@click="openRelative(relative.id)"
|
||||
>
|
||||
<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 class="member-record-actions">
|
||||
<view role="button" aria-label="查看成长日志" @click="toGrowthJournal"><text>成长日志</text></view>
|
||||
<view role="button" aria-label="查看人生事服务状态" @click="toLifeEvents"><text>人生事(待开放)</text></view>
|
||||
</view>
|
||||
|
||||
<view v-if="canPreviewEdit" class="member-profile-action" @click="toEdit"><text>制作成员编辑预览</text></view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="memberState === 'restricted' && member" class="member-restricted">
|
||||
<view class="member-heading">
|
||||
<view class="member-heading__seal"><text>{{ member.name.slice(0, 1) }}</text></view>
|
||||
<view>
|
||||
<text>{{ member.name }}</text>
|
||||
<text>第 {{ member.generation }} 世 · {{ member.relation }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text>{{ restrictedCopy.title }}</text>
|
||||
<text>{{ restrictedCopy.copy }}</text>
|
||||
<view class="member-restricted-action" @click="toTree"><text>返回上一位成员</text></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="member-error">
|
||||
@@ -60,12 +92,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { findTreeMemberPresentationFixture } from "@/data/mock.js";
|
||||
import {
|
||||
consumeNavigationResult,
|
||||
handleBackPress,
|
||||
openPage,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
@@ -73,24 +111,9 @@ const memberState = ref("loading");
|
||||
const member = ref(null);
|
||||
const errorMessage = ref("");
|
||||
const genealogyName = ref("汤氏家谱");
|
||||
const memberTrail = reactive([]);
|
||||
const trailIndex = ref(-1);
|
||||
|
||||
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.status === "forbidden" ? "按权限隐藏" : member.value.birthDate },
|
||||
@@ -98,39 +121,147 @@ const details = computed(() => member.value ? [
|
||||
{ label: "祖居地", value: member.value.status === "forbidden" ? "按权限隐藏" : member.value.birthplace },
|
||||
{ label: "所属支系", value: member.value.branch },
|
||||
] : []);
|
||||
const canEdit = computed(() => Boolean(member.value?.canEdit));
|
||||
const canPreviewEdit = computed(() => memberState.value === "detail");
|
||||
const restrictedCopy = computed(() =>
|
||||
member.value?.status === "forbidden"
|
||||
? {
|
||||
title: "当前成员档案访问受限",
|
||||
copy: "页面只保留基础身份,不展示生平、居住地、支系、亲属或编辑入口。",
|
||||
}
|
||||
: {
|
||||
title: "部分成员资料未公开",
|
||||
copy: "页面只保留姓名、关系与世代,不展示其他资料或操作入口。",
|
||||
},
|
||||
);
|
||||
const statusLabel = computed(() => ({ privacy: "隐私资料", deceased: "离世纪念", forbidden: "访问受限" })[member.value?.status] || "成员状态");
|
||||
const statusDescription = computed(() => ({
|
||||
privacy: "部分资料仅向授权成员展示",
|
||||
deceased: "查看生平保留与纪念资料说明",
|
||||
forbidden: "当前账号只能查看有限身份信息",
|
||||
})[member.value?.status] || "查看成员状态说明");
|
||||
const memberContextDescription = computed(() => ({
|
||||
loading: "正在读取成员资料",
|
||||
detail: "成员身份与亲属关系",
|
||||
restricted: "隐私成员仅展示基础身份",
|
||||
error: "请重新选择成员",
|
||||
}[memberState.value] || "请重新选择成员"));
|
||||
|
||||
const loadMember = async (nextPersonId) => {
|
||||
const normalizedPersonId = String(nextPersonId || "");
|
||||
const fixture = findTreeMemberPresentationFixture(genealogyId.value, normalizedPersonId);
|
||||
if (!fixture) {
|
||||
errorMessage.value = "这位成员不存在或已不属于当前家谱。";
|
||||
return false;
|
||||
}
|
||||
const restricted = ["privacy", "forbidden"].includes(fixture.status);
|
||||
const relatives = restricted ? [] : fixture.relatives
|
||||
.map((relative) => {
|
||||
const relativeMember = findTreeMemberPresentationFixture(
|
||||
genealogyId.value,
|
||||
relative.personId,
|
||||
);
|
||||
return relativeMember
|
||||
? { id: relativeMember.id, name: relativeMember.name, relation: relative.relation }
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
personId.value = normalizedPersonId;
|
||||
member.value = {
|
||||
...fixture,
|
||||
relatives,
|
||||
};
|
||||
errorMessage.value = "";
|
||||
memberState.value = restricted ? "restricted" : "detail";
|
||||
return true;
|
||||
};
|
||||
|
||||
const initializeMemberTrail = async (initialPersonId) => {
|
||||
memberTrail.splice(0, memberTrail.length);
|
||||
trailIndex.value = -1;
|
||||
const normalizedPersonId = String(initialPersonId || "");
|
||||
const loaded = await loadMember(normalizedPersonId);
|
||||
if (!loaded) return false;
|
||||
memberTrail.splice(0, memberTrail.length, normalizedPersonId);
|
||||
trailIndex.value = 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
const openRelative = async (nextPersonId) => {
|
||||
const normalizedPersonId = String(nextPersonId || "");
|
||||
if (!normalizedPersonId || normalizedPersonId === personId.value) return false;
|
||||
|
||||
// 先读取目标成员,只有真实读取成功后才截断前进历史并推进轨迹;失败时
|
||||
// 当前成员、当前索引和既有历史全部保持不变。
|
||||
const loaded = await loadMember(normalizedPersonId);
|
||||
if (!loaded) return false;
|
||||
memberTrail.splice(trailIndex.value + 1);
|
||||
memberTrail.push(normalizedPersonId);
|
||||
trailIndex.value = memberTrail.length - 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
const popMemberTrail = async () => {
|
||||
while (trailIndex.value > 0) {
|
||||
const targetIndex = trailIndex.value - 1;
|
||||
const historicalPersonId = memberTrail[targetIndex];
|
||||
const loaded = await loadMember(historicalPersonId);
|
||||
if (loaded) {
|
||||
trailIndex.value = targetIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 历史成员可能已删除或失去权限。移除失效项后,当前成员在数组中的
|
||||
// 索引同步左移,再继续寻找更早的有效成员,绝不创建新的 T03 页面。
|
||||
memberTrail.splice(targetIndex, 1);
|
||||
trailIndex.value -= 1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
internalTrail: trailIndex.value > 0,
|
||||
"pop-internal-trail": popMemberTrail,
|
||||
});
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
if (query.state === "loading") { memberState.value = "loading"; return; }
|
||||
if (query.state === "error" || !personId.value || !memberFixtures[personId.value]) {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
const initialPersonId = String(query.personId || "");
|
||||
if (query.state === "loading") return;
|
||||
if (query.state === "error" || !genealogyId.value || !initialPersonId) {
|
||||
memberState.value = "error";
|
||||
errorMessage.value = !personId.value ? "没有指定成员,请从世系树重新选择。" : "这位成员不存在或已不属于当前家谱。";
|
||||
errorMessage.value = !initialPersonId
|
||||
? "没有指定成员,请从世系树重新选择。"
|
||||
: "当前家谱上下文无效,请重新进入。";
|
||||
return;
|
||||
}
|
||||
member.value = { ...memberFixtures[personId.value] };
|
||||
memberState.value = "detail";
|
||||
void initializeMemberTrail(initialPersonId).then((loaded) => {
|
||||
if (loaded) return;
|
||||
member.value = null;
|
||||
memberState.value = "error";
|
||||
});
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
const result = consumeNavigationResult("T03");
|
||||
if (result?.operation === "member-open-requested" && result.entityId) {
|
||||
void openRelative(result.entityId);
|
||||
}
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const toEdit = () => {
|
||||
if (!canEdit.value) { openMemberState("forbidden"); return; }
|
||||
uni.navigateTo({ url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${personId.value}` });
|
||||
if (!canPreviewEdit.value) return false;
|
||||
return openPage("T05", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
|
||||
};
|
||||
const openMemberState = (state) =>
|
||||
uni.navigateTo({
|
||||
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();
|
||||
const openMemberState = () =>
|
||||
openPage("T08", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
|
||||
const toGrowthJournal = () =>
|
||||
openPage("R08", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
|
||||
const toLifeEvents = () =>
|
||||
openPage("R09", { genealogyId: genealogyId.value, personId: personId.value }, "T03");
|
||||
const toTree = requestBack;
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -160,8 +291,17 @@ const toTree = () => uni.navigateBack();
|
||||
.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-record-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12rpx; margin-top: 20rpx; }
|
||||
.member-record-actions > view { @include adaptive.adaptive-scroll-button(secondary); display: flex; min-height: 72rpx; align-items: center; justify-content: center; padding: 8rpx 14rpx; text-align: center; }
|
||||
.member-record-actions text { color: $brand-red; font-size: 22rpx; font-weight: 700; line-height: 1.35; }
|
||||
.member-profile-action { @include adaptive.adaptive-scroll-button(primary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
|
||||
.member-profile-action text { color: #fff9ed; font-size: 24rpx; font-weight: 700; }
|
||||
.member-restricted { padding-top: 14rpx; }
|
||||
.member-restricted > text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; text-align: center; }
|
||||
.member-restricted > text:first-of-type { margin-top: 28rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 31rpx; font-weight: 700; }
|
||||
.member-restricted > text + text { margin-top: 12rpx; }
|
||||
.member-restricted-action { @include adaptive.adaptive-scroll-button(secondary); display: flex; min-height: 76rpx; align-items: center; justify-content: center; margin-top: 22rpx; }
|
||||
.member-restricted-action text { color: $brand-red; 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; }
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
class="add-relative-page"
|
||||
:class="{
|
||||
'add-state--form': addState === 'form',
|
||||
'add-state--success': addState === 'success',
|
||||
'add-state--preview': addState === 'preview',
|
||||
'add-state--error': addState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="add-relative-page__header">
|
||||
<PageHeader :title="isFirstMember ? '录入首位成员' : '新增亲属'" />
|
||||
<PageHeader :title="isFirstMember ? '录入首位成员' : '新增亲属'" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view class="add-relative-panel">
|
||||
@@ -75,17 +75,17 @@
|
||||
<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>
|
||||
<text>{{ isSubmitting ? "正在校验…" : "生成本地预览" }}</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()">
|
||||
<text class="form-eyebrow">{{ addState === "preview" ? "本地流程预览" : hasValidContext ? "成员未保存" : "成员入口无效" }}</text>
|
||||
<text class="form-title">{{ addState === "preview" ? previewTitle : hasValidContext ? "暂时无法校验成员" : "没有找到要关联的成员" }}</text>
|
||||
<text class="form-copy">{{ addState === "preview" ? previewCopy : hasValidContext ? "当前填写内容仍保留,可返回修改后重试。" : "请从世系树重新进入,页面不会创建临时成员身份。" }}</text>
|
||||
<view class="form-action" @click="addState === 'preview' ? returnToTree() : retryForm()">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ addState === "success" ? "返回世系树" : "返回修改" }}</text>
|
||||
<text>{{ addState === "preview" ? "返回世系树(不保存)" : hasValidContext ? "返回修改" : "返回上一页" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -98,9 +98,9 @@
|
||||
cancel-text="继续填写"
|
||||
confirm-text="放弃并返回"
|
||||
show-cancel
|
||||
@close="discardDialogVisible = false"
|
||||
@cancel="discardDialogVisible = false"
|
||||
@confirm="discardAndBack"
|
||||
:close-on-mask="false"
|
||||
@cancel="cancelDiscard"
|
||||
@confirm="confirmDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
@@ -111,7 +111,14 @@ 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";
|
||||
import { findTreeMemberFixture } from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
handleBackPress,
|
||||
returnTo,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const addState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
@@ -121,21 +128,21 @@ 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 isFirstMember = computed(() => mode.value === "first");
|
||||
const currentMember = computed(() =>
|
||||
isFirstMember.value
|
||||
? null
|
||||
: findTreeMemberFixture(genealogyId.value, personId.value),
|
||||
);
|
||||
const currentMember = computed(
|
||||
() => memberFixtures[personId.value] || { id: personId.value, name: "当前成员", generation: "待确认" },
|
||||
const hasValidContext = computed(
|
||||
() =>
|
||||
Boolean(genealogyId.value) &&
|
||||
(isFirstMember.value ? !personId.value : Boolean(currentMember.value)),
|
||||
);
|
||||
const relationIndex = computed(() => Math.max(0, relationOptions.indexOf(addForm.relation)));
|
||||
const genderIndex = computed(() => Math.max(0, genderOptions.indexOf(addForm.gender)));
|
||||
@@ -149,43 +156,54 @@ const formCopy = computed(() =>
|
||||
);
|
||||
const formNote = computed(() =>
|
||||
isFirstMember.value
|
||||
? "保存后进入世系树,并可继续为首位成员添加亲属。"
|
||||
: "保存后成员会出现在相应世代;详细生平可在成员档案中继续完善。",
|
||||
? "当前阶段只校验首位成员资料,不会修改世系树。"
|
||||
: "当前阶段只校验亲属资料,不会新增成员或改变世系关系。",
|
||||
);
|
||||
const successTitle = computed(() =>
|
||||
isFirstMember.value ? `${addForm.name}已成为世系起点` : `${addForm.name}已加入世系`,
|
||||
const previewTitle = computed(() =>
|
||||
isFirstMember.value ? `${addForm.name}可作为世系起点` : `${addForm.name}的亲属资料已通过本地校验`,
|
||||
);
|
||||
const successCopy = computed(() =>
|
||||
isFirstMember.value
|
||||
? "首位成员已保存,世系树现在可以继续向下补充。"
|
||||
: `${addForm.relation}关系已记录,返回后可查看新的成员节点。`,
|
||||
const previewCopy = computed(() =>
|
||||
`本地预览尚未提交服务器,返回后不会保存${isFirstMember.value ? "首位成员" : `${addForm.relation}关系`},世系树也不会变化。`,
|
||||
);
|
||||
const hasDraft = computed(() =>
|
||||
Object.values(addForm).some((value) => String(value).trim()),
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardDialogVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
personId.value = String(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";
|
||||
addState.value = !hasValidContext.value || query.state === "error"
|
||||
? "error"
|
||||
: query.state === "preview"
|
||||
? "preview"
|
||||
: "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 timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardDialogVisible.value,
|
||||
dirty: (addState.value === "form" || addState.value === "preview") && hasDraft.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectRelation = (event) => {
|
||||
addForm.relation = relationOptions[Number(event.detail.value)] || "";
|
||||
@@ -204,18 +222,26 @@ const validateAddForm = () => {
|
||||
};
|
||||
const submitAdd = () => {
|
||||
if (isSubmitting.value || !validateAddForm()) return;
|
||||
const submitSnapshot = Object.freeze({ ...addForm });
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
addState.value = addForm.name.trim() === "失败" ? "error" : "success";
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
addState.value = submitSnapshot.name.trim() === "失败" ? "error" : "preview";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
};
|
||||
const retryForm = () => { addState.value = "form"; };
|
||||
const returnToTree = () => uni.navigateBack();
|
||||
const discardAndBack = () => {
|
||||
discardDialogVisible.value = false;
|
||||
uni.navigateBack();
|
||||
const retryForm = () => {
|
||||
if (!hasValidContext.value) return goBack();
|
||||
addState.value = "form";
|
||||
};
|
||||
const returnToTree = async () => {
|
||||
const confirmed = hasDraft.value
|
||||
? await requestDiscardConfirmation()
|
||||
: true;
|
||||
if (!confirmed) return false;
|
||||
return returnTo("T01", { genealogyId: genealogyId.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
class="edit-member-page"
|
||||
:class="{
|
||||
'edit-state--form': editState === 'form',
|
||||
'edit-state--success': editState === 'success',
|
||||
'edit-state--preview': editState === 'preview',
|
||||
'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-page__header"><PageHeader title="编辑成员" custom-back @back="requestBack" /></view>
|
||||
<view class="edit-member-panel">
|
||||
<view v-if="editState === 'form'" class="edit-member-form">
|
||||
<text class="form-eyebrow">成员档案维护</text>
|
||||
@@ -45,7 +45,7 @@
|
||||
<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>
|
||||
<text>{{ isSubmitting ? "正在校验…" : "生成本地预览" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -68,9 +68,9 @@
|
||||
cancel-text="继续编辑"
|
||||
confirm-text="放弃修改"
|
||||
show-cancel
|
||||
@close="discardDialogVisible = false"
|
||||
@cancel="discardDialogVisible = false"
|
||||
@confirm="discardAndBack"
|
||||
:close-on-mask="false"
|
||||
@cancel="cancelDiscard"
|
||||
@confirm="confirmDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
@@ -81,7 +81,13 @@ 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";
|
||||
import { findTreeMemberPresentationFixture } from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
handleBackPress,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const editState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
@@ -90,54 +96,93 @@ 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 originalMember = ref(null);
|
||||
const baseline = ref("");
|
||||
const editForm = reactive({ name: "", generationName: "", birthDate: "", deathDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", dates: "" });
|
||||
|
||||
const formSnapshot = computed(() => JSON.stringify(editForm));
|
||||
const hasValidContext = computed(() =>
|
||||
Boolean(genealogyId.value && personId.value && originalMember.value),
|
||||
);
|
||||
const isDirty = computed(
|
||||
() => editState.value === "form" && baseline.value && formSnapshot.value !== baseline.value,
|
||||
() =>
|
||||
(editState.value === "form" || editState.value === "preview") &&
|
||||
Boolean(baseline.value) &&
|
||||
formSnapshot.value !== baseline.value,
|
||||
);
|
||||
const resultCopy = computed(() => ({
|
||||
success: { eyebrow: "成员资料已更新", title: `${editForm.name}的档案已保存`, copy: "返回成员档案后可以查看本次修改。", action: "返回成员档案" },
|
||||
error: { eyebrow: "资料未保存", title: "暂时无法保存成员档案", copy: "当前修改仍保留,可返回表单后重试。", action: "返回修改" },
|
||||
preview: { eyebrow: "本地流程预览", title: `${editForm.name}的资料已通过本地校验`, copy: "尚未提交服务器,返回成员档案后不会显示本次修改。", action: "返回成员档案(不保存)" },
|
||||
error: hasValidContext.value
|
||||
? { eyebrow: "资料未保存", title: "暂时无法保存成员档案", copy: "当前修改仍保留,可返回表单后重试。", action: "返回修改" }
|
||||
: { eyebrow: "成员入口无效", title: "没有找到要编辑的成员", copy: "请从成员档案重新进入,页面不会创建临时成员身份。", action: "返回上一页" },
|
||||
"no-permission": { eyebrow: "权限不足", title: "当前账号不能编辑这位成员", copy: "本人或具备成员维护权限的家谱管理员才能修改档案。", action: "返回成员档案" },
|
||||
}[editState.value] || {}));
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardDialogVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
const loadMember = (id) => {
|
||||
const member = memberFixtures[id] || { ...fallbackMember, id, name: "待核实成员" };
|
||||
const member = findTreeMemberPresentationFixture(genealogyId.value, id);
|
||||
if (!member) return false;
|
||||
originalMember.value = { ...member };
|
||||
Object.assign(editForm, {
|
||||
name: member.name,
|
||||
generationName: member.generationName,
|
||||
birthDate: member.birthDate,
|
||||
deathDate: member.deathDate,
|
||||
summary: member.summary,
|
||||
});
|
||||
if (["privacy", "forbidden"].includes(member.status)) {
|
||||
Object.assign(editForm, {
|
||||
name: member.name,
|
||||
generationName: "",
|
||||
birthDate: "",
|
||||
deathDate: "",
|
||||
summary: "",
|
||||
});
|
||||
} else {
|
||||
Object.assign(editForm, {
|
||||
name: member.name,
|
||||
generationName: member.generationName || "",
|
||||
birthDate: member.birthDate || "",
|
||||
deathDate: member.deathDate || "",
|
||||
summary: member.summary || "",
|
||||
});
|
||||
}
|
||||
baseline.value = formSnapshot.value;
|
||||
return true;
|
||||
};
|
||||
|
||||
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";
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
personId.value = String(query.personId || "");
|
||||
const loaded = Boolean(genealogyId.value && personId.value) && loadMember(personId.value);
|
||||
editState.value = !loaded
|
||||
? "error"
|
||||
: ["privacy", "forbidden"].includes(originalMember.value.status)
|
||||
? "no-permission"
|
||||
: query.state === "preview"
|
||||
? "preview"
|
||||
: query.state === "error"
|
||||
? "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;
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardDialogVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectDate = (field, event) => {
|
||||
editForm[field] = event.detail.value || "";
|
||||
@@ -150,21 +195,29 @@ const validateEditForm = () => {
|
||||
};
|
||||
const saveMember = () => {
|
||||
if (isSubmitting.value || !validateEditForm()) return;
|
||||
const submitSnapshot = Object.freeze({ ...editForm });
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
editState.value = editForm.name.trim() === "失败" ? "error" : "success";
|
||||
if (editState.value === "success") baseline.value = formSnapshot.value;
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
editState.value = submitSnapshot.name.trim() === "失败" ? "error" : "preview";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
};
|
||||
const returnToMember = async () => {
|
||||
const confirmed = isDirty.value
|
||||
? await requestDiscardConfirmation()
|
||||
: true;
|
||||
if (!confirmed) return false;
|
||||
return goBack();
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (editState.value === "error") { editState.value = "form"; return; }
|
||||
uni.navigateBack();
|
||||
};
|
||||
const discardAndBack = () => {
|
||||
discardDialogVisible.value = false;
|
||||
uni.navigateBack();
|
||||
if (editState.value === "error" && hasValidContext.value) {
|
||||
editState.value = "form";
|
||||
return;
|
||||
}
|
||||
return returnToMember();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
class="relationship-page"
|
||||
:class="{
|
||||
'relationship-state--form': relationshipState === 'form',
|
||||
'relationship-state--success': relationshipState === 'success',
|
||||
'relationship-state--preview': relationshipState === 'preview',
|
||||
'relationship-state--conflict': relationshipState === 'conflict',
|
||||
'relationship-state--error': relationshipState === 'error',
|
||||
}"
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="relationship-page__header"><PageHeader title="关系维护" /></view>
|
||||
<view class="relationship-page__header"><PageHeader title="关系维护" custom-back @back="requestBack" /></view>
|
||||
<view class="relationship-panel">
|
||||
<view v-if="relationshipState === 'form'" class="relationship-form">
|
||||
<text class="form-eyebrow">亲属关系校正</text>
|
||||
@@ -40,7 +40,7 @@
|
||||
<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>
|
||||
<text>{{ isSubmitting ? "正在校验…" : "生成本地预览" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</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>
|
||||
<text>{{ relationshipState === "preview" ? "返回世系树(不保存)" : hasValidContext ? "重新选择" : "返回上一页" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -68,42 +68,60 @@
|
||||
eyebrow="关系校验规则"
|
||||
title="为什么不能保存这段关系"
|
||||
:message="conflictReason || '同一成员不能成为自己的亲属,也不能形成上下代循环或重复父母关系。'"
|
||||
:close-on-mask="false"
|
||||
@confirm="conflictDialogVisible = false"
|
||||
@close="conflictDialogVisible = false"
|
||||
/>
|
||||
<AppDialog
|
||||
:visible="discardDialogVisible"
|
||||
eyebrow="关系尚未保存"
|
||||
title="要放弃本次关系调整吗"
|
||||
message="当前关系只在本页预览,确认返回后不会修改世系。"
|
||||
cancel-text="继续核对"
|
||||
confirm-text="放弃并返回"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@cancel="cancelDiscard"
|
||||
@confirm="confirmDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
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";
|
||||
import { listTreeMemberFixtures } from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
handleBackPress,
|
||||
returnTo,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const relationshipState = ref("form");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const conflictDialogVisible = ref(false);
|
||||
const discardDialogVisible = 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 memberOptions = computed(() => listTreeMemberFixtures(genealogyId.value));
|
||||
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 baseline = ref("");
|
||||
const memberLabels = computed(() => memberOptions.value.map((item) => `${item.name} · 第 ${item.generation} 世`));
|
||||
const memberById = computed(() => new Map(memberOptions.value.map((item) => [item.id, item])));
|
||||
const hasValidContext = computed(() =>
|
||||
Boolean(genealogyId.value && memberById.value.has(personId.value)),
|
||||
);
|
||||
const sourceIndex = computed(() => Math.max(0, memberOptions.value.findIndex((item) => item.id === relationshipForm.sourceId)));
|
||||
const targetIndex = computed(() => Math.max(0, memberOptions.value.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(
|
||||
@@ -111,25 +129,67 @@ const relationshipPreview = computed(
|
||||
? `${memberName(relationshipForm.sourceId)}将以“${relationshipForm.relationship}”关联${memberName(relationshipForm.targetId)}。`
|
||||
: "完成三项选择后,这里会说明世系位置将如何变化。",
|
||||
);
|
||||
const formSnapshot = computed(() => JSON.stringify(relationshipForm));
|
||||
const isDirty = computed(() =>
|
||||
Boolean(baseline.value) && formSnapshot.value !== baseline.value,
|
||||
);
|
||||
const resultCopy = computed(() => ({
|
||||
success: { eyebrow: "关系已保存", title: "世系关系已经更新", copy: relationshipPreview.value },
|
||||
preview: { eyebrow: "本地流程预览", title: "关系校验已经通过", copy: `${relationshipPreview.value} 尚未提交服务器,返回后世系不会变化。` },
|
||||
conflict: { eyebrow: "发现关系冲突", title: "这段关系会造成世系矛盾", copy: conflictReason.value },
|
||||
error: { eyebrow: "关系未保存", title: "暂时无法完成关系调整", copy: "当前选择仍然保留,可返回后重新校验。" },
|
||||
error: hasValidContext.value
|
||||
? { eyebrow: "关系未保存", title: "暂时无法完成关系调整", copy: "当前选择仍然保留,可返回后重新校验。" }
|
||||
: { eyebrow: "成员入口无效", title: "没有找到要调整的成员", copy: "请从世系树重新进入,页面不会回退到其他成员。" },
|
||||
}[relationshipState.value] || {}));
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardDialogVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
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";
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
personId.value = String(query.personId || "");
|
||||
relationshipForm.sourceId = hasValidContext.value ? personId.value : "";
|
||||
baseline.value = formSnapshot.value;
|
||||
relationshipState.value = !hasValidContext.value || query.state === "error"
|
||||
? "error"
|
||||
: query.state === "preview"
|
||||
? "preview"
|
||||
: query.state === "conflict"
|
||||
? "conflict"
|
||||
: "form";
|
||||
if (relationshipState.value === "conflict") conflictReason.value = "目标成员已经存在父级关系,请先核对原关系。";
|
||||
});
|
||||
onUnload(() => { if (submitTimer) clearTimeout(submitTimer); });
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const closeTransient = () => {
|
||||
if (conflictDialogVisible.value) {
|
||||
conflictDialogVisible.value = false;
|
||||
return;
|
||||
}
|
||||
cancelDiscard();
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: conflictDialogVisible.value || discardDialogVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": closeTransient,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const clearFieldError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectMember = (field, event) => {
|
||||
relationshipForm[field] = memberOptions[Number(event.detail.value)]?.id || "";
|
||||
relationshipForm[field] = memberOptions.value[Number(event.detail.value)]?.id || "";
|
||||
clearFieldError(field);
|
||||
};
|
||||
const selectRelationship = (event) => {
|
||||
@@ -174,16 +234,30 @@ const saveRelationship = () => {
|
||||
if (conflictReason.value) relationshipState.value = "conflict";
|
||||
return;
|
||||
}
|
||||
const submitSnapshot = Object.freeze({ ...relationshipForm });
|
||||
isSubmitting.value = true;
|
||||
submitTimer = setTimeout(() => {
|
||||
relationshipState.value = relationshipForm.relationship === "失败" ? "error" : "success";
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
relationshipState.value = submitSnapshot.relationship === "失败" ? "error" : "preview";
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
};
|
||||
const returnToTree = async () => {
|
||||
const confirmed = isDirty.value
|
||||
? await requestDiscardConfirmation()
|
||||
: true;
|
||||
if (!confirmed) return false;
|
||||
return returnTo("T01", { genealogyId: genealogyId.value });
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (relationshipState.value === "error") { relationshipState.value = "form"; return; }
|
||||
uni.navigateBack();
|
||||
if (relationshipState.value === "error") {
|
||||
if (!hasValidContext.value) return goBack();
|
||||
relationshipState.value = "form";
|
||||
return;
|
||||
}
|
||||
return returnToTree();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="directory-page__header"><PageHeader title="成员目录" /></view>
|
||||
<view class="directory-context">
|
||||
<view v-if="hasValidContext" class="directory-context">
|
||||
<text class="directory-context__name">汤氏家谱</text>
|
||||
<text class="directory-context__meta"
|
||||
>主支 · {{ members.length }} 位成员</text
|
||||
@@ -24,7 +24,7 @@
|
||||
<input
|
||||
v-model="keyword"
|
||||
aria-label="成员搜索关键词"
|
||||
placeholder="按姓名、字辈或支系查找"
|
||||
placeholder="按姓名、身份或世代查找"
|
||||
placeholder-class="directory-placeholder"
|
||||
@confirm="searchMembers"
|
||||
/>
|
||||
@@ -56,11 +56,8 @@
|
||||
>
|
||||
<view class="directory-card__copy">
|
||||
<text class="directory-card__name">{{ item.name }}</text>
|
||||
<text class="directory-card__meta"
|
||||
>第 {{ item.generation }} 世 · {{ item.generationName }} ·
|
||||
{{ item.branch }}</text
|
||||
>
|
||||
<text class="directory-card__status">{{ item.note }}</text>
|
||||
<text class="directory-card__meta">{{ memberMeta(item) }}</text>
|
||||
<text class="directory-card__status">{{ memberStatus(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -71,7 +68,7 @@
|
||||
}}</text
|
||||
><text>{{
|
||||
directoryState === "empty"
|
||||
? "换一个姓名、字辈或支系关键词再试。"
|
||||
? "换一个姓名、身份或世代关键词再试。"
|
||||
: "请从当前家谱重新进入,成员资料不会受到影响。"
|
||||
}}</text></view
|
||||
>
|
||||
@@ -80,7 +77,7 @@
|
||||
v-if="directoryState === 'error'"
|
||||
block
|
||||
type="secondary"
|
||||
label="重新查看"
|
||||
:label="hasValidContext ? '重新查看' : '返回上一页'"
|
||||
@click="retryDirectory"
|
||||
/>
|
||||
</view>
|
||||
@@ -93,63 +90,56 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { listTreeMemberPresentationFixtures } from "@/data/mock.js";
|
||||
import { goBack, openPage } from "@/utils/navigation.js";
|
||||
const genealogyId = ref("");
|
||||
const keyword = ref("");
|
||||
const directoryState = ref("loading");
|
||||
const members = [
|
||||
{
|
||||
id: 101,
|
||||
name: "汤文远",
|
||||
generation: 12,
|
||||
generationName: "文字辈",
|
||||
branch: "主支",
|
||||
note: "始祖 · 档案完整",
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
name: "汤正国",
|
||||
generation: 13,
|
||||
generationName: "正字辈",
|
||||
branch: "长房",
|
||||
note: "家谱管理员",
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
name: "汤正华",
|
||||
generation: 13,
|
||||
generationName: "正字辈",
|
||||
branch: "二房",
|
||||
note: "资料待补充",
|
||||
},
|
||||
];
|
||||
const hasValidContext = computed(() => Boolean(genealogyId.value));
|
||||
const members = computed(() => listTreeMemberPresentationFixtures(genealogyId.value));
|
||||
const isRestrictedMember = (member) =>
|
||||
["privacy", "forbidden"].includes(member.status);
|
||||
const memberMeta = (member) =>
|
||||
isRestrictedMember(member)
|
||||
? `第 ${member.generation} 世 · ${member.relation}`
|
||||
: `第 ${member.generation} 世 · ${member.generationName} · ${member.branch}`;
|
||||
const memberStatus = (member) =>
|
||||
isRestrictedMember(member)
|
||||
? member.status === "forbidden"
|
||||
? "访问受限"
|
||||
: "隐私资料"
|
||||
: member.note;
|
||||
const filteredMembers = computed(() => {
|
||||
const value = keyword.value.trim();
|
||||
if (!value) return members;
|
||||
return members.filter((item) =>
|
||||
`${item.name}${item.generationName}${item.branch}`.includes(value),
|
||||
if (!value) return members.value;
|
||||
return members.value.filter((item) =>
|
||||
`${item.name}${item.relation}${item.generation}${item.generationName || ""}${item.branch || ""}`.includes(value),
|
||||
);
|
||||
});
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
directoryState.value =
|
||||
query.state === "loading"
|
||||
!hasValidContext.value ? "error" : query.state === "loading"
|
||||
? "loading"
|
||||
: query.state === "empty"
|
||||
? "empty"
|
||||
: query.state === "error"
|
||||
? "error"
|
||||
: "list";
|
||||
: members.value.length
|
||||
? "list"
|
||||
: "empty";
|
||||
});
|
||||
const searchMembers = () => {
|
||||
directoryState.value = filteredMembers.value.length ? "list" : "empty";
|
||||
};
|
||||
const retryDirectory = () => {
|
||||
if (!hasValidContext.value) return goBack();
|
||||
directoryState.value = "list";
|
||||
};
|
||||
const openMember = (item) =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${item.id}`,
|
||||
});
|
||||
hasValidContext.value
|
||||
? openPage("T03", { genealogyId: genealogyId.value, personId: String(item.id) }, "T07")
|
||||
: Promise.resolve(false);
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
<ModulePageBackground module="tree" />
|
||||
<view class="member-status-page__header"><PageHeader :title="pageTitle" /></view>
|
||||
|
||||
<view class="member-status-context">
|
||||
<view v-if="hasValidContext" class="member-status-context">
|
||||
<text>{{ genealogyName }}</text>
|
||||
<text v-if="member">{{ member.name }} · 第 {{ member.generation }} 世 · {{ member.branch }}</text>
|
||||
<text v-if="member">{{ memberIdentityCopy }}</text>
|
||||
<text v-else>未找到成员身份</text>
|
||||
</view>
|
||||
|
||||
@@ -45,7 +45,8 @@ import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import ModulePageBackground from "@/components/ModulePageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
import { findTreeMemberPresentationFixture } from "@/data/mock.js";
|
||||
import { goBack, goRoot } from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
@@ -53,18 +54,13 @@ 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: ["姓名、世代与所属支系可见", "联系方式和详细生平已隐藏", "本人或谱主可维护授权范围"],
|
||||
guides: ["姓名、世代与家族关系可见", "联系方式和详细生平已隐藏", "本人或谱主可维护授权范围"],
|
||||
action: "返回成员档案",
|
||||
},
|
||||
deceased: {
|
||||
@@ -94,22 +90,33 @@ const states = {
|
||||
};
|
||||
const activeStatus = computed(() => states[statusState.value] || states.error);
|
||||
const pageTitle = computed(() => statusState.value === "deceased" ? "成员纪念" : statusState.value === "privacy" ? "隐私资料" : "成员状态");
|
||||
const hasValidContext = computed(() => Boolean(genealogyId.value && personId.value));
|
||||
const memberIdentityCopy = computed(() => {
|
||||
if (!member.value) return "";
|
||||
const identity = `${member.value.name} · 第 ${member.value.generation} 世`;
|
||||
return ["privacy", "forbidden"].includes(member.value.status)
|
||||
? `${identity} · ${member.value.relation}`
|
||||
: `${identity} · ${member.value.branch}`;
|
||||
});
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
|
||||
personId.value = query.personId || "";
|
||||
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
member.value = memberFixtures[personId.value] || null;
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
personId.value = String(query.personId || "");
|
||||
member.value = hasValidContext.value
|
||||
? findTreeMemberPresentationFixture(genealogyId.value, personId.value)
|
||||
: null;
|
||||
const requestedState = ["privacy", "deceased", "forbidden"].includes(query.state) ? query.state : "";
|
||||
statusState.value = member.value?.allowedStates.includes(requestedState) ? requestedState : "error";
|
||||
const derivedState = member.value?.status || "";
|
||||
statusState.value = member.value && (!requestedState || requestedState === derivedState)
|
||||
? requestedState || derivedState
|
||||
: "error";
|
||||
});
|
||||
|
||||
const handleAction = () => {
|
||||
if (statusState.value === "forbidden") {
|
||||
uni.reLaunch({ url: "/pages/genealogy/g01-my-genealogies" });
|
||||
return;
|
||||
return goRoot("G01");
|
||||
}
|
||||
uni.navigateBack();
|
||||
return goBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user