修改完成待测试

This commit is contained in:
2026-09-13 17:45:47 +08:00
parent 9ad572907b
commit bbc024f7f2
80 changed files with 3844 additions and 1672 deletions
+166 -44
View File
@@ -55,7 +55,7 @@
<picker
:range="sexOptions.map((item) => item.label)"
:value="optionIndex(sexOptions, addForm.sex)"
@change="selectOption('sex', sexOptions, $event)"
@change="selectSex"
>
<view class="form-field form-field--picker">
<text>性别</text
@@ -168,19 +168,35 @@
type="number"
placeholder="可选正整数"
placeholder-class="form-placeholder"
@blur="loadRankOptions"
/>
<text v-else>首位成员固定为 1</text>
</view>
<picker
mode="date"
:value="addForm.birthDate"
@change="selectBirthDate"
v-if="rankOptions.length"
:range="rankPickerLabels"
:value="rankOptionIndex"
:disabled="rankOptionsState !== 'ready' || isSubmitting"
@change="selectRank"
>
<view class="form-field form-field--picker">
<text>出生日期</text
><text>{{ addForm.birthDate || "请选择" }}</text>
<text>排行称谓</text><text>{{ selectedRankName || "请选择" }}</text>
</view>
</picker>
<view v-else class="form-field form-field--disabled">
<text>排行称谓</text><text>{{ rankOptionStateLabel }}</text>
</view>
<text v-if="rankOptionsState === 'error'" class="field-error"
>排行配置暂未就绪请稍后重试</text
>
<view
class="form-field form-field--picker"
role="button"
aria-label="选择出生日期"
@click="openDatePicker('birthDate', '选择出生日期')"
>
<text>出生日期</text><text>{{ addForm.birthDate || "请选择" }}</text>
</view>
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, addForm.birthLunar)"
@@ -260,17 +276,15 @@
/>
</view>
<picker
<view
v-if="isDeceased"
mode="date"
:value="addForm.deathDate"
@change="selectDeathDate"
class="form-field form-field--picker"
role="button"
aria-label="选择逝世日期"
@click="openDatePicker('deathDate', '选择逝世日期')"
>
<view class="form-field form-field--picker">
<text>逝世日期</text
><text>{{ addForm.deathDate || "请选择" }}</text>
</view>
</picker>
<text>逝世日期</text><text>{{ addForm.deathDate || "请选择" }}</text>
</view>
<picker
v-if="isDeceased"
:range="lunarOptions.map((item) => item.label)"
@@ -338,17 +352,15 @@
<text>关系称谓</text><text>{{ optionLabel(relationVariantOptions, addForm.relationVariantCode) || "请选择" }}</text>
</view>
</picker>
<picker
<view
v-if="isDeceased"
mode="date"
:value="addForm.burialDate"
@change="selectBurialDate"
class="form-field form-field--picker"
role="button"
aria-label="选择安葬日期"
@click="openDatePicker('burialDate', '选择安葬日期')"
>
<view class="form-field form-field--picker">
<text>安葬日期</text
><text>{{ addForm.burialDate || "请选择" }}</text>
</view>
</picker>
<text>安葬日期</text><text>{{ addForm.burialDate || "请选择" }}</text>
</view>
<view v-if="isDeceased" class="form-field">
<text>安葬地</text>
<input
@@ -373,15 +385,6 @@
}}</text>
</view>
</picker>
<view class="form-field">
<text>排序值</text>
<input
v-model="addForm.sortOrder"
type="number"
placeholder="可选整数,值越小越靠前"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field form-field--summary">
<text>人物简介</text>
<textarea
@@ -427,6 +430,14 @@
</view>
</view>
<MemberDatePickerSheet
:visible="datePickerVisible"
:title="datePickerTitle"
:value="datePickerValue"
@cancel="closeDatePicker"
@confirm="confirmDatePicker"
/>
<AppDialog
:visible="discardDialogVisible"
eyebrow="尚未保存"
@@ -447,6 +458,7 @@ import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppDialog from "@/components/AppDialog.vue";
import MemberDatePickerSheet from "@/components/tree/MemberDatePickerSheet.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
@@ -486,10 +498,14 @@ const isAvatarUploading = ref(false);
const avatarFileName = ref("");
const avatarUploadError = ref("");
const discardDialogVisible = ref(false);
const datePickerVisible = ref(false);
const datePickerField = ref("");
const datePickerTitle = ref("选择日期");
const currentMember = ref(null);
const errorMessage = ref("");
const memberDetailRequestController = createRequestController();
const memberOptionsRequestController = createRequestController();
const rankOptionsRequestController = createRequestController();
const avatarUploadRequestController = createRequestController();
const memberCreationRequestController = createRequestController();
const sensitiveProfileRequestController = createRequestController();
@@ -503,6 +519,8 @@ const dictionaryRequestControllers = {
const memberCreationGuard = createNonIdempotentWriteGuard();
let pageActive = true;
let loadSequence = 0;
let rankLoadSequence = 0;
let rankOptionsContext = "";
const relationIntents = Object.freeze(
Object.fromEntries(
@@ -526,6 +544,7 @@ const addForm = reactive({
sex: "",
generation: "",
generationName: "",
rankId: "",
avatarOssId: "",
birthDate: "",
birthLunar: "",
@@ -548,7 +567,6 @@ const addForm = reactive({
personStatus: "",
biography: "",
remark: "",
sortOrder: "",
});
const fieldErrors = reactive({
name: "",
@@ -560,6 +578,8 @@ const fieldErrors = reactive({
});
const memberOptionsState = ref("loading");
const memberOptions = ref([]);
const rankOptionsState = ref("idle");
const rankOptions = ref([]);
const zodiacOptions = ref([]);
const educationOptions = ref([]);
const deathExpressionOptions = ref([]);
@@ -568,6 +588,7 @@ const spouseRelationVariantOptions = ref([]);
const isFirstMember = computed(() => mode.value === "first");
const isDeceased = computed(() => addForm.personStatus === "1");
const datePickerValue = computed(() => addForm[datePickerField.value] || "");
const activeRelationIntent = computed(
() => relationIntents[relationType.value] || null,
);
@@ -617,6 +638,23 @@ const formCopy = computed(() =>
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
);
const formNote = computed(() => "红色 * 为必填项,其余内容可按家谱记载补充。");
const rankPickerLabels = computed(() => [
"请选择排行称谓",
...rankOptions.value.map((item) => item.rankName),
]);
const rankOptionIndex = computed(() => {
const index = rankOptions.value.findIndex((item) => item.rankId === addForm.rankId);
return index < 0 ? 0 : index + 1;
});
const selectedRankName = computed(
() => rankOptions.value.find((item) => item.rankId === addForm.rankId)?.rankName || "",
);
const rankOptionStateLabel = computed(() => {
if (rankOptionsState.value === "loading") return "正在读取";
if (rankOptionsState.value === "error") return "暂时无法读取";
if (rankOptionsState.value === "idle") return "请先选择性别和世代";
return "当前条件暂无可选排行";
});
const resultCopy = computed(() => {
if (memberCreationOutcomeUnknown.value) {
return {
@@ -675,6 +713,7 @@ const loadCurrentMember = async () => {
});
if (!pageActive || activeLoad !== loadSequence) return;
currentMember.value = member;
applyRelationDefaults();
addState.value = "form";
} catch (error) {
if (!pageActive || activeLoad !== loadSequence || isRequestCancelled(error)) return;
@@ -753,6 +792,7 @@ onLoad((query) => {
void loadMemberOptions();
void loadBusinessOptions();
if (isFirstMember.value) {
applyRelationDefaults();
addState.value = "form";
return;
}
@@ -763,6 +803,7 @@ onUnload(() => {
loadSequence += 1;
memberDetailRequestController.abort();
memberOptionsRequestController.abort();
rankOptionsRequestController.abort();
avatarUploadRequestController.abort();
memberCreationRequestController.abort();
sensitiveProfileRequestController.abort();
@@ -772,10 +813,13 @@ onUnload(() => {
const requestBack = () =>
runBackGuard({
transientOpen: discardDialogVisible.value,
transientOpen: datePickerVisible.value || discardDialogVisible.value,
dirty: addState.value === "form" && hasDraft.value,
submitting: isSubmitting.value || isAvatarUploading.value,
"close-transient": cancelDiscard,
"close-transient": () => {
if (datePickerVisible.value) return closeDatePicker();
return cancelDiscard();
},
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
@@ -788,6 +832,7 @@ const clearError = (field) => {
const selectRelation = (event) => {
addForm.relation = relationOptions.value[Number(event.detail.value)] || "";
addForm.relationVariantCode = "";
applyRelationDefaults();
clearError("relation");
};
const optionIndex = findMemberOptionIndex;
@@ -795,6 +840,64 @@ const optionLabel = findMemberOptionLabel;
const selectOption = (field, options, event) => {
updateMemberFormOption(addForm, field, options, event);
};
const relationGeneration = () => {
const currentGeneration = Number(currentMember.value?.generation);
if (!Number.isSafeInteger(currentGeneration) || currentGeneration < 1) return "";
if ([memberRelationTypes.FATHER, memberRelationTypes.MOTHER].includes(selectedRelationType.value)) {
return String(Math.max(1, currentGeneration - 1));
}
if ([memberRelationTypes.SON, memberRelationTypes.DAUGHTER].includes(selectedRelationType.value)) {
return String(currentGeneration + 1);
}
return String(currentGeneration);
};
const applyRelationDefaults = () => {
addForm.generation = isFirstMember.value ? "1" : relationGeneration();
if ([memberRelationTypes.FATHER, memberRelationTypes.SON].includes(selectedRelationType.value)) {
addForm.sex = "0";
} else if ([memberRelationTypes.MOTHER, memberRelationTypes.DAUGHTER].includes(selectedRelationType.value)) {
addForm.sex = "1";
}
rankOptionsContext = "";
addForm.rankId = "";
void loadRankOptions();
};
const loadRankOptions = async () => {
const generation = Number(isFirstMember.value ? 1 : addForm.generation);
const sex = String(addForm.sex || "");
const context = `${generation}:${sex}`;
const activeLoad = ++rankLoadSequence;
if (!Number.isSafeInteger(generation) || generation < 1 || !["0", "1", "2"].includes(sex)) {
rankOptionsContext = "";
rankOptions.value = [];
rankOptionsState.value = "idle";
addForm.rankId = "";
return;
}
if (context !== rankOptionsContext) addForm.rankId = "";
rankOptionsContext = context;
rankOptionsState.value = "loading";
try {
const options = await lineageApi.getRankOptions(genealogyId.value, generation, sex, {
requestController: rankOptionsRequestController,
});
if (!pageActive || activeLoad !== rankLoadSequence) return;
rankOptions.value = options;
rankOptionsState.value = "ready";
} catch (error) {
if (!pageActive || activeLoad !== rankLoadSequence || isRequestCancelled(error)) return;
rankOptions.value = [];
rankOptionsState.value = "error";
}
};
const selectSex = (event) => {
selectOption("sex", sexOptions, event);
void loadRankOptions();
};
const selectRank = (event) => {
const selectedIndex = Number(event.detail.value) - 1;
addForm.rankId = rankOptions.value[selectedIndex]?.rankId || "";
};
const selectBindingMode = (event) => {
updateMemberBindingMode(addForm, event);
fieldErrors.bindingMode = "";
@@ -835,14 +938,26 @@ const uploadAvatar = async () => {
if (pageActive) isAvatarUploading.value = false;
}
};
const selectBirthDate = (event) => {
addForm.birthDate = event.detail.value || "";
const openDatePicker = (field, title) => {
datePickerField.value = field;
datePickerTitle.value = title;
datePickerVisible.value = true;
};
const selectDeathDate = (event) => {
addForm.deathDate = event.detail.value || "";
const closeDatePicker = () => {
datePickerVisible.value = false;
};
const selectBurialDate = (event) => {
addForm.burialDate = event.detail.value || "";
const confirmDatePicker = (value) => {
if (!datePickerField.value) return closeDatePicker();
addForm[datePickerField.value] = value;
fieldErrors.dates = "";
closeDatePicker();
};
const localToday = () => {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const validateAddForm = () => {
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
@@ -858,8 +973,15 @@ const validateAddForm = () => {
addForm.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(addForm.email.trim())
? "请填写有效的电子邮箱"
: "";
const today = localToday();
fieldErrors.dates =
addForm.birthDate && addForm.deathDate && addForm.deathDate < addForm.birthDate
addForm.birthDate && addForm.birthDate > today
? "出生日期不能晚于今天"
: addForm.deathDate && addForm.deathDate > today
? "逝世日期不能晚于今天"
: addForm.burialDate && addForm.burialDate > today
? "安葬日期不能晚于今天"
: addForm.birthDate && addForm.deathDate && addForm.deathDate < addForm.birthDate
? "逝世日期不能早于出生日期"
: addForm.deathDate && addForm.burialDate && addForm.burialDate < addForm.deathDate
? "安葬日期不能早于逝世日期"
@@ -884,6 +1006,7 @@ const submitAdd = async () => {
aliasName: addForm.aliasName,
sex: addForm.sex,
generationName: addForm.generationName,
...(addForm.rankId ? { rankId: addForm.rankId } : {}),
avatarOssId: addForm.avatarOssId,
birthDate: addForm.birthDate,
birthLunar: addForm.birthLunar,
@@ -908,7 +1031,6 @@ const submitAdd = async () => {
personStatus: addForm.personStatus,
biography: addForm.biography,
remark: addForm.remark,
sortOrder: addForm.sortOrder,
...(isFirstMember.value
? { generation: 1 }
: {
+142 -42
View File
@@ -49,7 +49,7 @@
<picker
:range="sexOptions.map((item) => item.label)"
:value="optionIndex(sexOptions, editForm.sex)"
@change="selectOption('sex', sexOptions, $event)"
@change="selectSex"
>
<view class="form-field form-field--picker">
<text>性别</text
@@ -148,8 +148,28 @@
type="number"
placeholder="正整数"
placeholder-class="form-placeholder"
@blur="loadRankOptions"
/>
</view>
<picker
v-if="rankOptions.length"
:range="rankPickerLabels"
:value="rankOptionIndex"
:disabled="rankOptionsState !== 'ready'"
@change="selectRank"
>
<view class="form-field form-field--picker">
<text>排行称谓</text>
<text>{{ selectedRankName || "请选择" }}</text>
</view>
</picker>
<view v-else class="form-field form-field--disabled">
<text>排行称谓</text>
<text>{{ rankOptionStateLabel }}</text>
</view>
<text v-if="rankOptionsState === 'error'" class="field-error"
>排行配置暂未就绪请稍后重试</text
>
<picker
:range="personOptionLabels"
:value="personOptionIndex(editForm.fatherId)"
@@ -170,16 +190,15 @@
><text>{{ personOptionLabel(editForm.motherId) }}</text></view
>
</picker>
<picker
mode="date"
:value="editForm.birthDate"
@change="selectDate('birthDate', $event)"
<view
class="form-field form-field--picker"
role="button"
aria-label="选择出生日期"
@click="openDatePicker('birthDate', '选择出生日期')"
>
<view class="form-field form-field--picker"
><text>出生日期</text
><text>{{ editForm.birthDate || "未填写" }}</text></view
>
</picker>
<text>出生日期</text>
<text>{{ editForm.birthDate || "未填写" }}</text>
</view>
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, editForm.birthLunar)"
@@ -257,17 +276,16 @@
placeholder-class="form-placeholder"
/>
</view>
<picker
<view
v-if="isDeceased"
mode="date"
:value="editForm.deathDate"
@change="selectDate('deathDate', $event)"
class="form-field form-field--picker"
role="button"
aria-label="选择离世日期"
@click="openDatePicker('deathDate', '选择离世日期')"
>
<view class="form-field form-field--picker"
><text>离世日期</text
><text>{{ editForm.deathDate || "在世或未填写" }}</text></view
>
</picker>
<text>离世日期</text>
<text>{{ editForm.deathDate || "在世或未填写" }}</text>
</view>
<picker
v-if="isDeceased"
:range="lunarOptions.map((item) => item.label)"
@@ -329,17 +347,16 @@
class="form-note"
>遗传病史属于敏感健康信息仅在已授权时读取和保存</text
>
<picker
<view
v-if="isDeceased"
mode="date"
:value="editForm.burialDate"
@change="selectDate('burialDate', $event)"
class="form-field form-field--picker"
role="button"
aria-label="选择安葬日期"
@click="openDatePicker('burialDate', '选择安葬日期')"
>
<view class="form-field form-field--picker"
><text>安葬日期</text
><text>{{ editForm.burialDate || "未填写" }}</text></view
>
</picker>
<text>安葬日期</text>
<text>{{ editForm.burialDate || "未填写" }}</text>
</view>
<view v-if="isDeceased" class="form-field">
<text>安葬地</text>
<input
@@ -361,15 +378,6 @@
}}</text>
</view>
</picker>
<view class="form-field">
<text>排序值</text>
<input
v-model="editForm.sortOrder"
type="number"
placeholder="可选整数,值越小越靠前"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field form-field--summary">
<text>人物简介</text>
<textarea
@@ -419,6 +427,14 @@
</view>
</view>
<MemberDatePickerSheet
:visible="datePickerVisible"
:title="datePickerTitle"
:value="datePickerValue"
@cancel="closeDatePicker"
@confirm="confirmDatePicker"
/>
<AppDialog
:visible="discardDialogVisible"
eyebrow="资料尚未保存"
@@ -439,6 +455,7 @@ import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppDialog from "@/components/AppDialog.vue";
import MemberDatePickerSheet from "@/components/tree/MemberDatePickerSheet.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
@@ -476,11 +493,15 @@ const isAvatarUploading = ref(false);
const avatarFileName = ref("");
const avatarUploadError = ref("");
const discardDialogVisible = ref(false);
const datePickerVisible = ref(false);
const datePickerField = ref("");
const datePickerTitle = ref("选择日期");
const errorMessage = ref("");
const failedAction = ref("load");
const memberDetailRequestController = createRequestController();
const personOptionsRequestController = createRequestController();
const memberOptionsRequestController = createRequestController();
const rankOptionsRequestController = createRequestController();
const avatarUploadRequestController = createRequestController();
const memberUpdateRequestController = createRequestController();
const sensitiveProfileRequestController = createRequestController();
@@ -491,6 +512,8 @@ const dictionaryRequestControllers = {
};
let pageActive = true;
let loadSequence = 0;
let rankLoadSequence = 0;
let rankOptionsContext = "";
const originalMember = ref(null);
const baseline = ref("");
@@ -504,6 +527,7 @@ const editForm = reactive({
sex: "",
generation: "",
generationName: "",
rankId: "",
fatherId: "",
motherId: "",
avatarOssId: "",
@@ -541,6 +565,8 @@ const lunarOptions = memberFormOptions.lunar;
const personStatusOptions = memberFormOptions.personStatus;
const bindingModeOptions = memberFormOptions.bindingMode;
const personOptions = ref([{ label: "不选择", value: "" }]);
const rankOptionsState = ref("idle");
const rankOptions = ref([]);
const personOptionLabels = computed(() =>
personOptions.value.map((item) => item.label),
);
@@ -565,6 +591,26 @@ const memberOptionLabels = computed(() =>
memberBindingOptions.value.map((item) => item.label),
);
const isDeceased = computed(() => editForm.personStatus === "1");
const datePickerValue = computed(() => editForm[datePickerField.value] || "");
const rankOptionIndex = computed(() => {
const index = rankOptions.value.findIndex((item) => item.rankId === editForm.rankId);
return index < 0 ? 0 : index + 1;
});
const rankPickerLabels = computed(() => [
"请选择排行称谓",
...rankOptions.value.map((item) => item.rankName),
]);
const selectedRankName = computed(
() =>
rankOptions.value.find((item) => item.rankId === editForm.rankId)?.rankName ||
(editForm.rankId === originalMember.value?.rankId ? originalMember.value?.rankName : "") ||
"",
);
const rankOptionStateLabel = computed(() => {
if (rankOptionsState.value === "loading") return "正在读取";
if (rankOptionsState.value === "error") return "暂时无法读取";
return selectedRankName.value || "暂无可选排行";
});
const formSnapshot = computed(() => JSON.stringify(editForm));
const hasValidContext = computed(() =>
@@ -654,6 +700,7 @@ const loadMember = async () => {
sex: member.sex || "",
generation: member.generation || "",
generationName: member.generationName || "",
rankId: member.rankId || "",
fatherId:
member.relatives.find((item) => item.relation === "父亲")?.id || "",
motherId:
@@ -696,6 +743,8 @@ const loadMember = async () => {
errorMessage.value = "";
failedAction.value = "load";
editState.value = "form";
rankOptionsContext = `${Number(editForm.generation)}:${String(editForm.sex || "")}`;
void loadRankOptions();
} catch (error) {
if (!pageActive || activeLoad !== loadSequence || isRequestCancelled(error)) return;
originalMember.value = null;
@@ -720,7 +769,7 @@ const loadPersonOptions = async () => {
return null;
}
const name = item.name;
return { value: personOptionId, label: `${name}${personOptionId}` };
return { value: personOptionId, label: name };
})
.filter(Boolean);
personOptions.value = [{ label: "不选择", value: "" }, ...options];
@@ -779,6 +828,32 @@ const loadMemberOptions = async () => {
memberOptionsState.value = "error";
}
};
const loadRankOptions = async () => {
const generation = Number(editForm.generation);
const sex = String(editForm.sex || "");
const context = `${generation}:${sex}`;
const activeLoad = ++rankLoadSequence;
if (!Number.isSafeInteger(generation) || generation < 1 || !["0", "1", "2"].includes(sex)) {
rankOptions.value = [];
rankOptionsState.value = "idle";
return;
}
if (context !== rankOptionsContext) editForm.rankId = "";
rankOptionsContext = context;
rankOptionsState.value = "loading";
try {
const options = await lineageApi.getRankOptions(genealogyId.value, generation, sex, {
requestController: rankOptionsRequestController,
});
if (!pageActive || activeLoad !== rankLoadSequence) return;
rankOptions.value = options;
rankOptionsState.value = "ready";
} catch (error) {
if (!pageActive || activeLoad !== rankLoadSequence || isRequestCancelled(error)) return;
rankOptions.value = [];
rankOptionsState.value = "error";
}
};
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
@@ -802,6 +877,7 @@ onUnload(() => {
memberDetailRequestController.abort();
personOptionsRequestController.abort();
memberOptionsRequestController.abort();
rankOptionsRequestController.abort();
avatarUploadRequestController.abort();
memberUpdateRequestController.abort();
sensitiveProfileRequestController.abort();
@@ -811,10 +887,13 @@ onUnload(() => {
const requestBack = () =>
runBackGuard({
transientOpen: discardDialogVisible.value,
transientOpen: datePickerVisible.value || discardDialogVisible.value,
dirty: isDirty.value,
submitting: isSubmitting.value || isAvatarUploading.value,
"close-transient": cancelDiscard,
"close-transient": () => {
if (datePickerVisible.value) return closeDatePicker();
return cancelDiscard();
},
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
@@ -829,6 +908,16 @@ const optionLabel = findMemberOptionLabel;
const selectOption = (field, options, event) => {
updateMemberFormOption(editForm, field, options, event);
};
const selectSex = (event) => {
const previousSex = editForm.sex;
selectOption("sex", sexOptions, event);
if (editForm.sex !== previousSex) editForm.rankId = "";
void loadRankOptions();
};
const selectRank = (event) => {
const selectedIndex = Number(event.detail.value) - 1;
editForm.rankId = rankOptions.value[selectedIndex]?.rankId || "";
};
const selectBindingMode = (event) => {
updateMemberBindingMode(editForm, event);
fieldErrors.bindingMode = "";
@@ -880,9 +969,19 @@ const uploadAvatar = async () => {
if (pageActive) isAvatarUploading.value = false;
}
};
const selectDate = (field, event) => {
editForm[field] = event.detail.value || "";
const openDatePicker = (field, title) => {
datePickerField.value = field;
datePickerTitle.value = title;
datePickerVisible.value = true;
};
const closeDatePicker = () => {
datePickerVisible.value = false;
};
const confirmDatePicker = (value) => {
if (!datePickerField.value) return closeDatePicker();
editForm[datePickerField.value] = value;
fieldErrors.dates = "";
closeDatePicker();
};
const validateEditForm = () => {
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
@@ -936,6 +1035,7 @@ const saveMember = async () => {
? Number(editForm.generation)
: undefined,
generationName: editForm.generationName,
rankId: editForm.rankId,
fatherId: editForm.fatherId,
motherId: editForm.motherId,
avatarOssId: editForm.avatarOssId,
+55 -309
View File
@@ -1,346 +1,92 @@
<template>
<view
class="rank-page"
:class="{
'rank-state--loading': rankState === 'loading',
'rank-state--form': rankState === 'form',
'rank-state--error': rankState === 'error',
}"
>
<view class="rank-redirect-page">
<ModulePageBackground module="tree" />
<view class="rank-page__header"
><PageHeader title="调整排行" custom-back @back="requestBack"
/></view>
<view class="rank-panel">
<PageHeader title="编辑成员" />
<view class="rank-redirect-panel">
<AppLoading
v-if="rankState === 'loading'"
text="正在读取成员资料"
description="请稍候,正在确认待调整人物。"
v-if="redirectState === 'loading'"
text="正在打开成员资料"
description="排行称谓已合并到成员编辑页面。"
/>
<view v-else-if="rankState === 'form' && member" class="rank-form">
<text class="form-eyebrow">同辈排行</text>
<text class="form-title">调整{{ member.name }}的排行</text>
<text class="form-copy"
>这里只调整这位成员的排行同辈成员的整体排序暂不能在这里修改</text
>
<view class="member-context">
<text>当前成员</text
><text> {{ member.generation }} · {{ member.branch }}</text>
</view>
<view class="rank-field">
<text>排序值</text>
<input
v-model="sortOrder"
type="number"
:disabled="savingRank"
placeholder="请输入整数,值越小越靠前"
placeholder-class="rank-placeholder"
@input="rankError = ''"
/>
</view>
<text v-if="rankError" class="rank-error">{{ rankError }}</text>
<view
class="form-action"
:class="{ 'form-action--disabled': savingRank }"
@click="saveRank"
>
<image
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="scaleToFill"
/>
<text>{{ savingRank ? "正在保存…" : "保存排行" }}</text>
</view>
</view>
<view v-else class="rank-result">
<text class="form-eyebrow">暂时无法打开成员页面</text>
<text class="form-title">暂时无法读取成员资料</text>
<text class="form-copy">{{ errorMessage }}</text>
<view class="form-action" @click="goBack">
<image
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="scaleToFill"
/>
<text>返回世系树</text>
<view v-else class="rank-redirect-error">
<text class="rank-redirect-error__title">暂时无法打开成员资料</text>
<text class="rank-redirect-error__copy">{{ errorMessage }}</text>
<view class="rank-redirect-error__action" @click="goBack">
<text>返回上一页</text>
</view>
</view>
</view>
<AppDialog
:visible="discardVisible"
eyebrow="未保存修改"
title="放弃排行修改?"
message="当前排序值还没有保存。"
confirm-text="确认放弃"
cancel-text="继续编辑"
show-cancel
:close-on-mask="false"
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { lineageApi } from "@/services/api/lineage-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation/gateway.js";
import { goBack, returnTo } from "@/utils/navigation/gateway.js";
const rankState = ref("loading");
const genealogyId = ref("");
const personId = ref("");
const member = ref(null);
const redirectState = ref("loading");
const errorMessage = ref("");
const sortOrder = ref("");
const savingRank = ref(false);
const rankError = ref("");
const committedSortOrder = ref("");
const sortOrderBaseline = ref("");
const discardVisible = ref(false);
const memberRankReadRequestController = createRequestController();
const memberRankSaveRequestController = createRequestController();
let pageActive = true;
let loadSequence = 0;
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const isDirty = computed(() =>
rankState.value === "form" && sortOrder.value !== sortOrderBaseline.value,
);
const loadMember = async () => {
const activeLoad = ++loadSequence;
rankState.value = "loading";
onLoad(async (query) => {
const genealogyId = String(query?.genealogyId || "");
const personId = String(query?.personId || "");
if (!/^[1-9]\d*$/.test(genealogyId) || !/^[1-9]\d*$/.test(personId)) {
redirectState.value = "error";
errorMessage.value = "页面参数已经失效,请从成员资料重新进入。";
return;
}
try {
const personDetail = await lineageApi.getPerson(genealogyId.value, personId.value, {
requestController: memberRankReadRequestController,
});
if (!pageActive || activeLoad !== loadSequence) return;
member.value = personDetail;
sortOrder.value = personDetail.sortOrder === null || personDetail.sortOrder === undefined
? ""
: String(personDetail.sortOrder);
sortOrderBaseline.value = sortOrder.value;
committedSortOrder.value = "";
errorMessage.value = "";
rankState.value = "form";
await returnTo("T05", { genealogyId, personId });
} catch (error) {
if (!pageActive || activeLoad !== loadSequence || isRequestCancelled(error)) return;
member.value = null;
errorMessage.value =
getRequestErrorMessage(error, "当前成员资料暂不可用,请返回世系树后重试。");
rankState.value = "error";
redirectState.value = "error";
errorMessage.value = error?.message || "请返回后重新打开成员资料。";
}
};
const saveRank = async () => {
if (savingRank.value || !member.value) return;
const normalizedSortOrder = String(sortOrder.value).trim();
if (!/^-?\d+$/.test(normalizedSortOrder)) {
rankError.value = "请填写整数,例如 1、2、3。";
return;
}
const numericSortOrder = Number(normalizedSortOrder);
if (!Number.isSafeInteger(numericSortOrder)) {
rankError.value = "排序值超出可保存范围";
return;
}
savingRank.value = true;
rankError.value = "";
try {
if (committedSortOrder.value !== normalizedSortOrder) {
await lineageApi.updatePersonSortOrder(
genealogyId.value,
personId.value,
numericSortOrder,
{ requestController: memberRankSaveRequestController },
);
if (!pageActive) return;
committedSortOrder.value = normalizedSortOrder;
sortOrderBaseline.value = normalizedSortOrder;
}
await returnTo("T01", {
genealogyId: genealogyId.value,
selectedId: personId.value,
});
} catch (error) {
if (pageActive && !isRequestCancelled(error)) {
rankError.value = committedSortOrder.value === normalizedSortOrder
? "排行已经保存,但页面返回失败。请再次点击保存重试返回。"
: getRequestErrorMessage(error, "排行尚未保存,请稍后重试。");
}
} finally {
if (pageActive) savingRank.value = false;
}
};
const requestBack = () => runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: savingRank.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
personId.value = String(query.personId || "");
if (
!/^[1-9]\d*$/.test(genealogyId.value) ||
!/^[1-9]\d*$/.test(personId.value) ||
query.mode !== "rank"
) {
rankState.value = "error";
errorMessage.value = "请从成员资料页重新进入排行调整。";
return;
}
void loadMember();
});
onUnload(() => {
pageActive = false;
loadSequence += 1;
memberRankReadRequestController.abort();
memberRankSaveRequestController.abort();
discardConfirmation.dispose();
});
onBackPress((event) => handleBackPress(event, requestBack));
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.rank-page {
display: flex;
.rank-redirect-page {
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.rank-page__header {
z-index: 3;
}
.rank-panel {
@include adaptive-tree-panel;
z-index: 2;
width: calc(100% - 32rpx);
margin: 18rpx auto calc(28rpx + env(safe-area-inset-bottom));
padding: 7.5% 8%;
}
.form-eyebrow {
display: block;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
letter-spacing: 3rpx;
}
.form-title {
display: block;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(19px, 34rpx, 24px);
font-weight: 700;
line-height: 1.35;
}
.form-copy {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.5;
}
.member-context,
.rank-field {
@include adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
align-items: center;
gap: 20rpx;
margin-top: 16rpx;
padding: 12rpx 22rpx;
}
.member-context text:first-child,
.rank-field text {
color: $ink;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
}
.member-context text:last-child {
min-width: 0;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.45;
text-align: right;
}
.rank-field input {
min-width: 0;
min-height: var(--app-touch-min);
padding: 0 14rpx;
border: 1rpx solid rgba(143, 108, 63, 0.34);
border-radius: 8rpx;
background: rgba(255, 253, 247, 0.8);
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
text-align: right;
}
.rank-placeholder { color: $ink-muted; }
.rank-error { display: block; margin-top: 12rpx; color: $brand-red; font-size: clamp(14px, 22rpx, 17px); }
.form-action {
display: grid;
width: 100%;
min-height: var(--app-touch-min);
margin-top: 22rpx;
}
.form-action--disabled { opacity: 0.58; pointer-events: none; }
.form-action image,
.form-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.form-action text {
.rank-redirect-panel {
position: relative;
z-index: 1;
padding: 48rpx 32rpx;
}
.rank-redirect-error {
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: clamp(15px, 24rpx, 18px);
flex-direction: column;
gap: 24rpx;
padding: 64rpx 32rpx;
text-align: center;
}
.rank-redirect-error__title {
color: $ink;
font-size: 36rpx;
font-weight: 700;
}
.rank-result {
margin-top: 30%;
text-align: center;
.rank-redirect-error__copy {
color: $ink-muted;
font-size: 28rpx;
line-height: 1.7;
}
.rank-result .form-eyebrow,
.rank-result .form-copy {
text-align: center;
}
.rank-result .form-action {
width: 420rpx;
max-width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.rank-panel {
width: calc(100% - 48rpx);
}
.rank-redirect-error__action {
min-width: 240rpx;
padding: 22rpx 36rpx;
border: 1rpx solid rgba($brand-red, 0.4);
color: $brand-red;
font-size: 30rpx;
}
</style>
+53 -88
View File
@@ -26,10 +26,9 @@
aria-label="回到当前成员"
hover-class="tree-action--pressed"
@click="recenterSelectedMember"
>回到当前</text
>回到当前</text
>
<text @click="treeState = 'landscape'">阅读提示</text>
<text @click="toRank">调整排行</text>
</view>
</view>
@@ -105,19 +104,14 @@
>
<view class="member-node__surface">
<view class="member-node__portrait">
<AppAvatar :sex="member.sex" />
<AppAvatar
:sex="member.sex"
:src="member.avatarFile?.accessUrl || ''"
/>
<text class="node-relation">{{ member.relation }}</text>
</view>
<image
class="member-node__divider"
src="/static/assets/foundation/transparent/auth-divider-knot.png"
mode="aspectFit"
/>
<view class="member-node__copy">
<text class="node-name">{{ member.name }}</text>
<text class="node-relation">{{
nodeRelationText(member)
}}</text>
<text class="node-years">{{ member.treeYears || member.years }}</text>
</view>
</view>
</view>
@@ -243,13 +237,6 @@ const memberActions = Object.freeze([
routeKey: "T04",
relationType: memberRelationTypes.SIBLING,
},
{
key: "ADJUST_RANK",
label: "调整排行",
group: "MANAGEMENT",
routeKey: "T06",
mode: "rank",
},
{
key: "ADD_SON",
label: "添加儿子",
@@ -282,10 +269,6 @@ const managementActions = computed(() =>
memberActions.filter((action) => action.group === "MANAGEMENT"),
);
const nodeRelationText = (member) => {
const branch = String(member.branch || "").replace(/字辈$/, "");
return [member.relation, branch].filter(Boolean).join(" · ");
};
const treeLayout = computed(() => createTreeLayout(members.value));
const layoutMembers = computed(() => treeLayout.value.members);
const treeMetrics = computed(() => treeLayout.value.metrics);
@@ -493,18 +476,6 @@ const toDirectory = () =>
openPage("T07", { genealogyId: genealogyId.value }, "T01");
const toPedigree = () =>
openPage("T02", { genealogyId: genealogyId.value }, "T01");
const toRank = () =>
selectedMember.value
? openPage(
"T06",
{
genealogyId: genealogyId.value,
personId: String(selectedMember.value.id),
mode: "rank",
},
"T01",
)
: Promise.resolve(false);
const openMemberPanel = (member) => {
selectedMember.value = member;
memberActionPanelVisible.value = true;
@@ -521,8 +492,6 @@ const openMemberAction = (action) => {
};
if (action.routeKey === "T04") {
params.relationType = action.relationType;
} else if (action.routeKey === "T06") {
params.mode = action.mode;
}
return openPage(action.routeKey, params, "T01");
};
@@ -584,7 +553,7 @@ const openMemberAction = (action) => {
}
.tree-stage--lineage {
display: grid;
grid-template-columns: 72rpx minmax(0, 1fr);
grid-template-columns: 112rpx minmax(0, 1fr);
align-items: start;
}
.tree-scroll {
@@ -595,7 +564,7 @@ const openMemberAction = (action) => {
.generation-rail {
display: grid;
z-index: 4;
width: 72rpx;
width: 112rpx;
height: 100%;
box-sizing: border-box;
border-right: 1rpx solid rgba(143, 108, 63, 0.2);
@@ -627,7 +596,7 @@ const openMemberAction = (action) => {
display: grid;
justify-self: center;
z-index: 3;
width: 62rpx;
width: 100rpx;
height: 104rpx;
color: #8f6c3f;
font-size: clamp(12px, 18rpx, 14px);
@@ -647,17 +616,17 @@ const openMemberAction = (action) => {
.generation-band__copy {
z-index: 1;
display: flex;
flex-direction: row;
flex-direction: column;
align-items: center;
justify-content: space-between;
gap: 0;
justify-content: center;
gap: 4rpx;
height: 100%;
padding: 0 4rpx;
padding: 8rpx 4rpx;
box-sizing: border-box;
background: rgba(255, 252, 244, 0.9);
}
.generation-band__copy text {
writing-mode: vertical-rl;
text-orientation: upright;
line-height: 1.35;
white-space: nowrap;
}
.generation-band__copy text:last-child {
@@ -677,8 +646,8 @@ const openMemberAction = (action) => {
align-self: start;
justify-self: start;
z-index: 2;
width: 160rpx;
height: 224rpx;
width: 154rpx;
height: 200rpx;
transform: translate(-50%, -50%);
text-align: center;
}
@@ -689,85 +658,81 @@ const openMemberAction = (action) => {
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 9rpx 8rpx 8rpx;
border: 1rpx solid #b78a42;
border-radius: 14rpx;
background: rgba(255, 252, 244, 0.96);
box-shadow: 0 5rpx 12rpx rgba(103, 72, 33, 0.12);
padding: 10rpx;
background: url("/static/assets/modules/tree/transparent/member-node-portrait.png") center / 100% 100% no-repeat;
}
.member-node__portrait {
position: relative;
display: flex;
width: 62rpx;
height: 62rpx;
width: 134rpx;
height: 126rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
border: 1rpx solid #c49a57;
border-radius: 50%;
border-radius: 2rpx;
background: #fffdf7;
overflow: hidden;
}
.member-node__divider {
width: 56rpx;
height: 14rpx;
margin-top: 3rpx;
}
.member-node__copy {
display: flex;
width: 100%;
margin-top: 1rpx;
min-height: 52rpx;
margin-top: 4rpx;
padding: 0;
align-items: center;
justify-content: center;
}
.node-name,
.node-relation,
.node-years {
.node-relation {
display: block;
}
.node-name {
display: -webkit-box;
color: #6b2419;
font-family: "STKaiti", "KaiTi", serif;
overflow: hidden;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
line-height: 1.1;
color: #d96529;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(14px, 24rpx, 18px);
font-weight: 500;
line-height: 1.15;
word-break: break-all;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
.node-relation {
position: absolute;
right: 50%;
bottom: 5rpx;
z-index: 1;
min-width: 54rpx;
max-width: 116rpx;
box-sizing: border-box;
overflow: hidden;
margin-top: 2rpx;
color: #644b2e;
font-size: clamp(12px, 18rpx, 14px);
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-years {
overflow: hidden;
margin-top: 2rpx;
color: #695b4c;
font-size: clamp(12px, 17rpx, 14px);
line-height: 1.2;
padding: 1rpx 6rpx;
transform: translateX(50%);
border: 1rpx solid #e66d24;
background: rgba(255, 255, 255, 0.96);
color: #e66d24;
font-size: clamp(12px, 22rpx, 15px);
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-node--spouse .member-node__surface {
border-color: #c39a61;
background: rgba(255, 250, 241, 0.98);
background-image: url("/static/assets/modules/tree/transparent/member-node-portrait.png");
}
.member-node--selected {
z-index: 3;
}
.member-node--selected .member-node__surface {
border-color: #c55344;
box-shadow: 0 6rpx 14rpx rgba(159, 23, 15, 0.16);
filter: drop-shadow(0 4rpx 5rpx rgba(159, 23, 15, 0.2));
}
.member-node--selected .member-node__portrait {
border-color: #c55344;
}
.member-node--selected .node-name {
color: $brand-red;
color: #d96529;
}
.tree-state-card {
display: grid;
+18 -30
View File
@@ -31,7 +31,7 @@
:show-scrollbar="false"
:style="pedigreeScrollStyle"
>
<view class="pedigree-sheet" :style="pedigreeSheetStyle">
<view class="pedigree-sheet" :style="getPedigreeSheetStyle(page)">
<view class="pedigree-column pedigree-column--legend">
<view class="pedigree-column__relation"><text>身份</text></view>
<view class="pedigree-column__name"><text>{{ page.label }}</text></view>
@@ -40,15 +40,14 @@
<view
v-for="(member, memberIndex) in page.members"
:key="member?.id || `empty-${pageIndex}-${memberIndex}`"
:key="member.id || `member-${pageIndex}-${memberIndex}`"
class="member-node"
:class="{
'member-node--blank': !member,
'member-node--selected': selectedId === String(member?.id || ''),
'member-node--spouse': Boolean(member?.spouseOf),
'member-node--selected': selectedId === String(member.id),
'member-node--spouse': Boolean(member.spouseOf),
}"
>
<view v-if="member" class="member-node__surface">
<view class="member-node__surface">
<view class="member-node__relation"><text>{{ memberRelation(member) }}</text></view>
<view
class="member-node__name"
@@ -65,22 +64,16 @@
:aria-label="`查看${member.name}的生平详情`"
@click.stop="openMemberDetail(member)"
>
<text class="node-relation">{{ member.branch }}</text>
<text class="node-years">{{ memberDetail(member) }}</text>
<text>{{ memberDetailText(member) }}</text>
</view>
</view>
<view v-else class="member-node__surface">
<view class="member-node__relation"></view>
<view class="member-node__name"></view>
<view class="member-node__copy"></view>
</view>
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
<view class="generation-rail" :style="pedigreeSheetStyle">
<view class="generation-rail" :style="pedigreeHeightStyle">
<view class="generation-band">
<image
class="generation-band__arrow generation-band__arrow--previous"
@@ -152,7 +145,7 @@ import { getRequestErrorMessage } from "@/services/api/request-error-message.js"
import { genealogyContext } from "@/utils/genealogy/context.js";
import { handleBackPress, openPage, returnTo } from "@/utils/navigation/gateway.js";
const PEDIGREE_PAGE_SIZE = 5;
const PEDIGREE_PAGE_SIZE = 3;
const genealogyId = ref("");
const treeState = ref("loading");
const treeLoadError = ref("");
@@ -198,14 +191,13 @@ const pedigreePages = computed(() => {
const branch = row.members.find((member) => member.branch)?.branch;
for (let index = 0; index < row.members.length; index += PEDIGREE_PAGE_SIZE) {
const pageMembers = row.members.slice(index, index + PEDIGREE_PAGE_SIZE);
while (pageMembers.length < PEDIGREE_PAGE_SIZE) pageMembers.push(null);
pages.push({
generation: row.generation,
label: row.label,
memberCount: row.members.length,
summary: [branch, `${row.members.length} 位成员`]
summary: [branch, `${row.members.length}位成员`]
.filter(Boolean)
.join(" · "),
.join("·"),
members: pageMembers,
});
}
@@ -219,14 +211,18 @@ const detailLength = computed(() =>
Math.max(
0,
...members.value.map((member) =>
Array.from(`${member.branch || ""}${memberDetail(member)}`).length,
Array.from(memberDetailText(member)).length,
),
),
);
const sheetHeight = computed(() => Math.max(1100, 720 + detailLength.value * 20));
const pedigreeSheetStyle = computed(() => ({
const pedigreeHeightStyle = computed(() => ({
height: `${sheetHeight.value}rpx`,
}));
const getPedigreeSheetStyle = (page) => ({
...pedigreeHeightStyle.value,
gridTemplateColumns: `104rpx repeat(${page.members.length}, minmax(0, 1fr))`,
});
const pedigreeScrollStyle = computed(() => ({
height: `${sheetHeight.value}rpx`,
}));
@@ -254,6 +250,8 @@ const handlePedigreePageChange = (event) => {
const memberRelation = (member) =>
member.spouseOf ? "配偶" : member.relation || "家谱成员";
const memberDetail = (member) => member.treeYears || member.years || "生卒待补";
const memberDetailText = (member) =>
[member.branch, memberDetail(member)].filter(Boolean).join("·");
const openMemberProfile = (member) =>
openPage(
"T03",
@@ -463,9 +461,6 @@ onUnload(() => {
.member-node__surface {
display: contents;
}
.member-node--blank {
pointer-events: none;
}
.member-node--selected .member-node__relation,
.member-node--selected .member-node__name,
.member-node--selected .member-node__copy {
@@ -477,13 +472,6 @@ onUnload(() => {
.member-node--spouse .member-node__relation {
color: #7a4d43;
}
.node-relation {
color: #675f57;
}
.node-years {
color: #3f3934;
text-combine-upright: all;
}
.generation-rail {
width: 92rpx;
border-left: 1rpx solid #e2e2e2;