修改完成待测试
This commit is contained in:
+166
-44
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user