Files
jiapuapp/pages/tree/edit-member.vue
T

1194 lines
39 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view
class="edit-member-page"
:class="{
'edit-state--form': editState === 'form',
'edit-state--loading': editState === 'loading',
'edit-state--error': editState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="edit-member-page__header"
><PageHeader title="编辑成员" custom-back @back="requestBack"
/></view>
<view class="edit-member-panel">
<AppLoading
v-if="editState === 'loading'"
text="正在读取成员资料"
description="请稍候,正在打开编辑资料。"
/>
<view v-else-if="editState === 'form'" class="edit-member-form">
<text class="form-eyebrow">成员档案维护</text>
<text class="form-title">完善{{ originalMember.name }}的生命记录</text>
<text class="form-copy"
>基础身份用于世系展示生平说明会显示在有权限查看的成员档案中</text
>
<view class="member-context">
<text>成员身份</text
><text
> {{ originalMember.generation }} ·
{{ originalMember.branch }}</text
>
</view>
<view class="form-field">
<text>姓名</text>
<input
v-model="editForm.name"
maxlength="20"
placeholder="请输入姓名"
placeholder-class="form-placeholder"
@input="clearError('name')"
/>
</view>
<text v-if="fieldErrors.name" class="field-error">{{
fieldErrors.name
}}</text>
<picker
:range="sexOptions.map((item) => item.label)"
:value="optionIndex(sexOptions, editForm.sex)"
@change="selectOption('sex', sexOptions, $event)"
>
<view class="form-field form-field--picker">
<text>性别</text
><text>{{
optionLabel(sexOptions, editForm.sex) || "请选择"
}}</text>
</view>
</picker>
<picker
:range="bindingModeOptions.map((item) => item.label)"
:value="optionIndex(bindingModeOptions, editForm.bindingMode)"
@change="selectBindingMode"
>
<view class="form-field form-field--picker">
<text>身份认领</text
><text>{{
optionLabel(bindingModeOptions, editForm.bindingMode)
}}</text>
</view>
</picker>
<text v-if="fieldErrors.bindingMode" class="field-error">{{
fieldErrors.bindingMode
}}</text>
<picker
v-if="editForm.bindingMode === 'SPECIFIED'"
:range="memberOptionLabels"
:value="memberOptionIndex(editForm.appUserId)"
:disabled="memberOptionsState !== 'ready' || !memberBindingOptions.length"
@change="selectBoundMember"
>
<view class="form-field form-field--picker">
<text><text class="required-mark">*</text>指定成员</text>
<text>{{ memberOptionLabel(editForm.appUserId) }}</text>
</view>
</picker>
<text
v-if="editForm.bindingMode === 'SPECIFIED' && memberOptionsState === 'loading'"
class="form-note"
>正在读取可绑定成员</text
>
<text
v-else-if="editForm.bindingMode === 'SPECIFIED' && memberOptionsState === 'error'"
class="field-error"
>可绑定成员暂不可用请稍后重试</text
>
<view class="form-field form-field--upload">
<text>头像</text>
<view>
<button
class="upload-button"
:disabled="isAvatarUploading || isSubmitting"
@click="uploadAvatar"
>
{{ isAvatarUploading ? "上传中…" : "选择图片" }}
</button>
<text v-if="avatarFileName" class="upload-receipt"
>已上传{{ avatarFileName }}</text
>
</view>
</view>
<text v-if="avatarUploadError" class="field-error">{{
avatarUploadError
}}</text>
<view class="form-field">
<text>表字</text>
<input
v-model="editForm.courtesyName"
maxlength="40"
placeholder="如族谱有记载可填写"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>别号</text>
<input
v-model="editForm.aliasName"
maxlength="40"
placeholder="别号、昵称或曾用名"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>字辈</text>
<input
v-model="editForm.generationName"
maxlength="12"
placeholder="例如:文字辈"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>世代</text>
<input
v-model="editForm.generation"
type="number"
placeholder="正整数"
placeholder-class="form-placeholder"
/>
</view>
<picker
:range="personOptionLabels"
:value="personOptionIndex(editForm.fatherId)"
@change="selectPerson('fatherId', $event)"
>
<view class="form-field form-field--picker"
><text>父亲</text
><text>{{ personOptionLabel(editForm.fatherId) }}</text></view
>
</picker>
<picker
:range="personOptionLabels"
:value="personOptionIndex(editForm.motherId)"
@change="selectPerson('motherId', $event)"
>
<view class="form-field form-field--picker"
><text>母亲</text
><text>{{ personOptionLabel(editForm.motherId) }}</text></view
>
</picker>
<picker
mode="date"
:value="editForm.birthDate"
@change="selectDate('birthDate', $event)"
>
<view class="form-field form-field--picker"
><text>出生日期</text
><text>{{ editForm.birthDate || "未填写" }}</text></view
>
</picker>
<picker
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, editForm.birthLunar)"
@change="selectOption('birthLunar', lunarOptions, $event)"
>
<view class="form-field form-field--picker">
<text>出生农历</text
><text>{{
optionLabel(lunarOptions, editForm.birthLunar) || "请选择"
}}</text>
</view>
</picker>
<view class="form-field">
<text>出生地</text>
<input
v-model="editForm.birthPlace"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker
:range="zodiacOptions.map((item) => item.label)"
:value="optionIndex(zodiacOptions, editForm.zodiac)"
@change="selectOption('zodiac', zodiacOptions, $event)"
>
<view class="form-field form-field--picker">
<text>生肖</text><text>{{ optionLabel(zodiacOptions, editForm.zodiac) || "请选择" }}</text>
</view>
</picker>
<view class="form-field">
<text>现居地</text>
<input
v-model="editForm.currentAddress"
maxlength="120"
placeholder="填写当前常住地区"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>联系电话</text>
<input
v-model="editForm.mobile"
maxlength="30"
placeholder="仅授权成员可见"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field">
<text>电子邮箱</text>
<input
v-model="editForm.email"
maxlength="254"
placeholder="仅授权成员可见"
placeholder-class="form-placeholder"
/>
</view>
<text v-if="fieldErrors.email" class="field-error">{{
fieldErrors.email
}}</text>
<picker
:range="educationOptions.map((item) => item.label)"
:value="optionIndex(educationOptions, editForm.education)"
@change="selectOption('education', educationOptions, $event)"
>
<view class="form-field form-field--picker">
<text>学历分类</text><text>{{ optionLabel(educationOptions, editForm.education) || "请选择" }}</text>
</view>
</picker>
<view class="form-field">
<text>职业</text>
<input
v-model="editForm.occupation"
maxlength="80"
placeholder="填写主要职业"
placeholder-class="form-placeholder"
/>
</view>
<picker
v-if="isDeceased"
mode="date"
:value="editForm.deathDate"
@change="selectDate('deathDate', $event)"
>
<view class="form-field form-field--picker"
><text>离世日期</text
><text>{{ editForm.deathDate || "在世或未填写" }}</text></view
>
</picker>
<picker
v-if="isDeceased"
:range="lunarOptions.map((item) => item.label)"
:value="optionIndex(lunarOptions, editForm.deathLunar)"
@change="selectOption('deathLunar', lunarOptions, $event)"
>
<view class="form-field form-field--picker">
<text>逝世农历</text
><text>{{
optionLabel(lunarOptions, editForm.deathLunar) || "请选择"
}}</text>
</view>
</picker>
<view v-if="isDeceased" class="form-field">
<text>享年</text>
<input
v-model="editForm.deathAge"
type="number"
placeholder="0 至 200 的整数"
placeholder-class="form-placeholder"
/>
</view>
<text v-if="fieldErrors.deathAge" class="field-error">{{
fieldErrors.deathAge
}}</text>
<view v-if="isDeceased" class="form-field">
<text>逝世地</text>
<input
v-model="editForm.deathPlace"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker
v-if="isDeceased"
:range="deathExpressionOptions.map((item) => item.label)"
:value="optionIndex(deathExpressionOptions, editForm.deathType)"
@change="selectOption('deathType', deathExpressionOptions, $event)"
>
<view class="form-field form-field--picker">
<text>逝世表述</text><text>{{ optionLabel(deathExpressionOptions, editForm.deathType) || "请选择" }}</text>
</view>
</picker>
<view
v-if="isDeceased && originalMember.canManageSensitiveMedicalHistory"
class="form-field form-field--summary"
>
<text>遗传病史</text>
<textarea
v-model="editForm.hereditaryMedicalHistory"
auto-height
maxlength="500"
placeholder="敏感信息,无明确依据可不填写"
placeholder-class="form-placeholder"
/>
</view>
<text
v-if="isDeceased && originalMember.canManageSensitiveMedicalHistory"
class="form-note"
>遗传病史属于敏感健康信息仅在已授权时读取和保存</text
>
<picker
v-if="isDeceased"
mode="date"
:value="editForm.burialDate"
@change="selectDate('burialDate', $event)"
>
<view class="form-field form-field--picker"
><text>安葬日期</text
><text>{{ editForm.burialDate || "未填写" }}</text></view
>
</picker>
<view v-if="isDeceased" class="form-field">
<text>安葬地</text>
<input
v-model="editForm.burialPlace"
placeholder="按家谱记载填写"
placeholder-class="form-placeholder"
/>
</view>
<picker
:range="personStatusOptions.map((item) => item.label)"
:value="optionIndex(personStatusOptions, editForm.personStatus)"
@change="selectOption('personStatus', personStatusOptions, $event)"
>
<view class="form-field form-field--picker">
<text>人物状态</text
><text>{{
optionLabel(personStatusOptions, editForm.personStatus) ||
"请选择"
}}</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
v-model="editForm.summary"
auto-height
maxlength="500"
placeholder="记录生平、迁徙或重要经历"
placeholder-class="form-placeholder"
/>
</view>
<view class="form-field form-field--summary">
<text>备注</text>
<textarea
v-model="editForm.remark"
auto-height
placeholder="记录其他需要保留的信息"
placeholder-class="form-placeholder"
/>
</view>
<text v-if="fieldErrors.dates" class="field-error">{{
fieldErrors.dates
}}</text>
<text class="form-note"
>私密信息仅你本人和可维护家谱的管理员可见</text
>
<view class="form-action" @click="saveMember">
<image
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="scaleToFill"
/>
<text>{{ isSubmitting ? "正在保存…" : "保存资料" }}</text>
</view>
</view>
<view v-else class="edit-result">
<text class="form-eyebrow">{{ resultCopy.eyebrow }}</text>
<text class="form-title">{{ resultCopy.title }}</text>
<text class="form-copy">{{ resultCopy.copy }}</text>
<view class="form-action" @click="handleResultAction">
<image
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="scaleToFill"
/>
<text>{{ resultCopy.action }}</text>
</view>
</view>
</view>
<AppDialog
:visible="discardDialogVisible"
eyebrow="资料尚未保存"
title="要放弃本次修改吗"
message="返回后,本次对成员档案的修改不会保留。"
cancel-text="继续编辑"
confirm-text="放弃修改"
show-cancel
:close-on-mask="false"
@cancel="cancelDiscard"
@confirm="confirmDiscard"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { genealogyMemberApi } from "@/services/api/genealogy-member-service.js";
import { businessDictionaryApi } from "@/services/api/business-dictionary-service.js";
import { lineageApi } from "@/services/api/lineage-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import {
isImagePickCancelled,
pickAndUploadImage,
} from "@/utils/media-upload.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import {
finishPage,
goBack,
handleBackPress,
runBackGuard,
} from "@/utils/navigation/gateway.js";
import {
findMemberOptionIndex,
findMemberOptionLabel,
memberFormOptions,
updateMemberBindingMode,
updateMemberFormOption,
} from "@/utils/tree/member-form.js";
const editState = ref("loading");
const genealogyId = ref("");
const personId = ref("");
const isSubmitting = ref(false);
const isAvatarUploading = ref(false);
const avatarFileName = ref("");
const avatarUploadError = ref("");
const discardDialogVisible = ref(false);
const errorMessage = ref("");
const failedAction = ref("load");
const memberDetailRequestController = createRequestController();
const personOptionsRequestController = createRequestController();
const memberOptionsRequestController = createRequestController();
const avatarUploadRequestController = createRequestController();
const memberUpdateRequestController = createRequestController();
const sensitiveProfileRequestController = createRequestController();
const dictionaryRequestControllers = {
zodiac: createRequestController(),
education: createRequestController(),
deathExpression: createRequestController(),
};
let pageActive = true;
let loadSequence = 0;
const originalMember = ref(null);
const baseline = ref("");
const committedMemberSnapshot = ref("");
const editForm = reactive({
appUserId: "",
bindingMode: "NONE",
name: "",
courtesyName: "",
aliasName: "",
sex: "",
generation: "",
generationName: "",
fatherId: "",
motherId: "",
avatarOssId: "",
birthDate: "",
birthLunar: "",
birthPlace: "",
zodiac: "",
currentAddress: "",
mobile: "",
email: "",
education: "",
occupation: "",
deathDate: "",
deathLunar: "",
deathAge: "",
deathPlace: "",
deathType: "",
hereditaryMedicalHistory: "",
burialDate: "",
burialPlace: "",
personStatus: "",
summary: "",
remark: "",
sortOrder: "",
});
const fieldErrors = reactive({
name: "",
dates: "",
bindingMode: "",
email: "",
deathAge: "",
});
const sexOptions = memberFormOptions.sex;
const lunarOptions = memberFormOptions.lunar;
const personStatusOptions = memberFormOptions.personStatus;
const bindingModeOptions = memberFormOptions.bindingMode;
const personOptions = ref([{ label: "不选择", value: "" }]);
const personOptionLabels = computed(() =>
personOptions.value.map((item) => item.label),
);
const memberOptionsState = ref("loading");
const memberOptions = ref([]);
const zodiacOptions = ref([]);
const educationOptions = ref([]);
const deathExpressionOptions = ref([]);
const originalSensitiveProfilePresent = ref(false);
const memberBindingOptions = computed(() => {
const options = memberOptions.value.slice();
if (
editForm.bindingMode === "SPECIFIED" &&
editForm.appUserId &&
!options.some((item) => item.value === editForm.appUserId)
) {
options.unshift({ value: editForm.appUserId, label: "当前已绑定成员" });
}
return options;
});
const memberOptionLabels = computed(() =>
memberBindingOptions.value.map((item) => item.label),
);
const isDeceased = computed(() => editForm.personStatus === "1");
const formSnapshot = computed(() => JSON.stringify(editForm));
const hasValidContext = computed(() =>
Boolean(
/^[1-9]\d*$/.test(genealogyId.value) &&
/^[1-9]\d*$/.test(personId.value) &&
originalMember.value,
),
);
const isDirty = computed(
() =>
editState.value === "form" &&
Boolean(baseline.value) &&
formSnapshot.value !== baseline.value,
);
const resultCopy = computed(
() =>
({
error:
failedAction.value === "navigate"
? {
eyebrow: "保存完成",
title: "成员档案已经保存",
copy:
errorMessage.value ||
"资料已保存,但页面暂时没有返回成员档案。",
action: "返回成员档案",
}
: failedAction.value === "save"
? {
eyebrow: "保存失败",
title: "成员档案尚未保存",
copy:
errorMessage.value || "资料尚未保存,请检查填写后重试。",
action: "返回修改",
}
: hasValidContext.value
? {
eyebrow: "资料读取失败",
title: "暂时无法读取成员档案",
copy: errorMessage.value || "请返回后重试。",
action: "重新读取",
}
: {
eyebrow: "暂时无法打开成员页面",
title: "没有找到要编辑的成员",
copy: errorMessage.value || "请从成员档案重新进入。",
action: "返回上一页",
},
})[editState.value] || {},
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardDialogVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const datePart = (value) => String(value || "").slice(0, 10);
const loadMember = async () => {
const activeLoad = ++loadSequence;
editState.value = "loading";
try {
const member = await lineageApi.getPerson(genealogyId.value, personId.value, {
requestController: memberDetailRequestController,
});
if (!pageActive || activeLoad !== loadSequence) return;
originalMember.value = member;
const deceased = member.personStatus === "1";
let sensitiveProfile = null;
if (member.canManageSensitiveMedicalHistory) {
sensitiveProfile = await lineageApi.getSensitiveProfile(
genealogyId.value,
personId.value,
{ requestController: sensitiveProfileRequestController },
);
if (!pageActive || activeLoad !== loadSequence) return;
}
originalSensitiveProfilePresent.value = sensitiveProfile?.present === true;
Object.assign(editForm, {
appUserId: member.appUserId || "",
bindingMode:
member.bindingMode || (member.appUserId ? "SPECIFIED" : "NONE"),
name: member.name,
courtesyName: member.courtesyName || "",
aliasName: member.aliasName || "",
sex: member.sex || "",
generation: member.generation || "",
generationName: member.generationName || "",
fatherId:
member.relatives.find((item) => item.relation === "父亲")?.id || "",
motherId:
member.relatives.find((item) => item.relation === "母亲")?.id || "",
avatarOssId: member.avatarOssId || "",
birthDate: datePart(member.birthDate),
birthLunar: member.birthLunar || "",
birthPlace: member.birthplace || "",
zodiac: member.zodiacCode || "",
currentAddress: member.currentAddress || "",
mobile: member.mobile || "",
email: member.email || "",
education: member.educationCode || "",
occupation: member.occupation || "",
deathDate: deceased ? datePart(member.deathDate) : "",
deathLunar: deceased ? member.deathLunar || "" : "",
deathAge: deceased ? member.deathAge ?? "" : "",
deathPlace: deceased ? member.deathPlace || "" : "",
deathType: deceased ? member.deathExpressionCode || "" : "",
hereditaryMedicalHistory:
deceased && member.canManageSensitiveMedicalHistory
? sensitiveProfile?.hereditaryMedicalHistory || ""
: "",
burialDate: deceased ? datePart(member.burialDate) : "",
burialPlace: deceased ? member.burialPlace || "" : "",
personStatus: member.personStatus || "",
summary: member.biography || "",
remark: member.remark || "",
sortOrder: member.sortOrder ?? "",
});
zodiacOptions.value = preserveHistoricalOption(zodiacOptions.value, member.zodiacCode, member.zodiac);
educationOptions.value = preserveHistoricalOption(educationOptions.value, member.educationCode, member.education);
deathExpressionOptions.value = preserveHistoricalOption(
deathExpressionOptions.value,
member.deathExpressionCode,
member.deathType,
);
baseline.value = formSnapshot.value;
committedMemberSnapshot.value = "";
errorMessage.value = "";
failedAction.value = "load";
editState.value = "form";
} catch (error) {
if (!pageActive || activeLoad !== loadSequence || isRequestCancelled(error)) return;
originalMember.value = null;
failedAction.value = "load";
errorMessage.value = getRequestErrorMessage(error, "成员资料暂不可用。");
editState.value = "error";
}
};
const loadPersonOptions = async () => {
try {
const rows = await lineageApi.getLineagePersonOptions(genealogyId.value, {
requestController: personOptionsRequestController,
});
if (!pageActive) return;
const options = rows
.map((item) => {
const personOptionId = item.id;
if (
!/^[1-9]\d*$/.test(personOptionId) ||
personOptionId === personId.value
) {
return null;
}
const name = item.name;
return { value: personOptionId, label: `${name}${personOptionId}` };
})
.filter(Boolean);
personOptions.value = [{ label: "不选择", value: "" }, ...options];
} catch (error) {
if (pageActive && !isRequestCancelled(error))
personOptions.value = [{ label: "暂无可选人物", value: "" }];
}
};
const preserveHistoricalOption = (options, value, label) => {
if (!value || options.some((item) => item.value === value)) return options;
return [{ value, label: label || value }, ...options];
};
const loadBusinessOptions = async () => {
try {
const [zodiacRows, educationRows, deathExpressionRows] = await Promise.all([
businessDictionaryApi.getBusinessDictionaryOptions("gen_zodiac", {
requestController: dictionaryRequestControllers.zodiac,
}),
businessDictionaryApi.getBusinessDictionaryOptions("gen_education_type", {
requestController: dictionaryRequestControllers.education,
}),
businessDictionaryApi.getBusinessDictionaryOptions("gen_death_expression", {
requestController: dictionaryRequestControllers.deathExpression,
}),
]);
if (!pageActive) return;
zodiacOptions.value = preserveHistoricalOption(zodiacRows, originalMember.value?.zodiacCode, originalMember.value?.zodiac);
educationOptions.value = preserveHistoricalOption(educationRows, originalMember.value?.educationCode, originalMember.value?.education);
deathExpressionOptions.value = preserveHistoricalOption(deathExpressionRows, originalMember.value?.deathExpressionCode, originalMember.value?.deathType);
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
zodiacOptions.value = preserveHistoricalOption([], originalMember.value?.zodiacCode, originalMember.value?.zodiac);
educationOptions.value = preserveHistoricalOption([], originalMember.value?.educationCode, originalMember.value?.education);
deathExpressionOptions.value = preserveHistoricalOption([], originalMember.value?.deathExpressionCode, originalMember.value?.deathType);
}
};
const loadMemberOptions = async () => {
memberOptionsState.value = "loading";
try {
const rows = await genealogyMemberApi.getMemberOptions(genealogyId.value, {
requestController: memberOptionsRequestController,
});
if (!pageActive) return;
memberOptions.value = rows
.filter((item) => item.eligible)
.map((item) => ({
value: item.appUserId,
label: item.relationName
? `${item.memberName} · ${item.relationName}`
: item.memberName,
}));
memberOptionsState.value = "ready";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
memberOptions.value = [];
memberOptionsState.value = "error";
}
};
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)
) {
editState.value = "error";
errorMessage.value = "这个页面已经过期,请从成员档案重新进入。";
return;
}
void loadMember();
void loadBusinessOptions();
void loadPersonOptions();
void loadMemberOptions();
});
onUnload(() => {
pageActive = false;
loadSequence += 1;
memberDetailRequestController.abort();
personOptionsRequestController.abort();
memberOptionsRequestController.abort();
avatarUploadRequestController.abort();
memberUpdateRequestController.abort();
sensitiveProfileRequestController.abort();
Object.values(dictionaryRequestControllers).forEach((controller) => controller.abort());
discardConfirmation.dispose();
});
const requestBack = () =>
runBackGuard({
transientOpen: discardDialogVisible.value,
dirty: isDirty.value,
submitting: isSubmitting.value || isAvatarUploading.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
onBackPress((event) => handleBackPress(event, requestBack));
const clearError = (field) => {
fieldErrors[field] = "";
};
const optionIndex = findMemberOptionIndex;
const optionLabel = findMemberOptionLabel;
const selectOption = (field, options, event) => {
updateMemberFormOption(editForm, field, options, event);
};
const selectBindingMode = (event) => {
updateMemberBindingMode(editForm, event);
fieldErrors.bindingMode = "";
};
const memberOptionIndex = (value) =>
Math.max(
0,
memberBindingOptions.value.findIndex((item) => item.value === value),
);
const memberOptionLabel = (value) =>
memberBindingOptions.value.find((item) => item.value === value)?.label ||
(memberOptionsState.value === "loading"
? "正在读取"
: memberBindingOptions.value.length
? "请选择可绑定成员"
: "暂无可绑定成员");
const selectBoundMember = (event) => {
editForm.appUserId =
memberBindingOptions.value[Number(event.detail.value)]?.value || "";
fieldErrors.bindingMode = "";
};
const personOptionIndex = (value) =>
Math.max(
0,
personOptions.value.findIndex((item) => item.value === value),
);
const personOptionLabel = (value) =>
personOptions.value.find((item) => item.value === value)?.label || "不选择";
const selectPerson = (field, event) => {
editForm[field] =
personOptions.value[Number(event.detail.value)]?.value || "";
};
const uploadAvatar = async () => {
if (isAvatarUploading.value || isSubmitting.value) return;
isAvatarUploading.value = true;
avatarUploadError.value = "";
try {
const receipt = await pickAndUploadImage({
requestController: avatarUploadRequestController,
});
if (!pageActive) return;
editForm.avatarOssId = receipt.ossId;
avatarFileName.value = receipt.fileName || "头像图片";
} catch (error) {
if (!pageActive) return;
if (!isImagePickCancelled(error) && !isRequestCancelled(error))
avatarUploadError.value = getRequestErrorMessage(error, "头像上传失败,请稍后重试");
} finally {
if (pageActive) isAvatarUploading.value = false;
}
};
const selectDate = (field, event) => {
editForm[field] = event.detail.value || "";
fieldErrors.dates = "";
};
const validateEditForm = () => {
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
fieldErrors.dates =
editForm.birthDate &&
editForm.deathDate &&
editForm.deathDate < editForm.birthDate
? "离世日期不能早于出生日期"
: editForm.deathDate &&
editForm.burialDate &&
editForm.burialDate < editForm.deathDate
? "安葬日期不能早于离世日期"
: "";
fieldErrors.email =
editForm.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(editForm.email.trim())
? "请填写有效的电子邮箱"
: "";
const deathAge = editForm.deathAge === "" ? null : Number(editForm.deathAge);
fieldErrors.deathAge =
deathAge !== null && (!Number.isSafeInteger(deathAge) || deathAge < 0 || deathAge > 200)
? "享年必须是 0 至 200 的整数"
: "";
fieldErrors.bindingMode =
editForm.bindingMode === "SPECIFIED" && !editForm.appUserId
? memberOptionsState.value === "error"
? "可绑定成员暂不可用,请稍后重试"
: "请选择可绑定成员"
: "";
return !Object.values(fieldErrors).some(Boolean);
};
const saveMember = async () => {
if (isSubmitting.value || isAvatarUploading.value || !validateEditForm())
return;
isSubmitting.value = true;
try {
const currentSnapshot = formSnapshot.value;
if (committedMemberSnapshot.value !== currentSnapshot) {
await lineageApi.updatePerson(
genealogyId.value,
personId.value,
{
bindingMode: editForm.bindingMode,
...(editForm.bindingMode === "SPECIFIED"
? { appUserId: editForm.appUserId }
: {}),
name: editForm.name,
courtesyName: editForm.courtesyName,
aliasName: editForm.aliasName,
sex: editForm.sex,
generation: editForm.generation
? Number(editForm.generation)
: undefined,
generationName: editForm.generationName,
fatherId: editForm.fatherId,
motherId: editForm.motherId,
avatarOssId: editForm.avatarOssId,
birthDate: editForm.birthDate,
birthLunar: editForm.birthLunar,
birthPlace: editForm.birthPlace,
zodiacCode: editForm.zodiac,
currentAddress: editForm.currentAddress,
mobile: editForm.mobile,
email: editForm.email,
educationCode: editForm.education,
occupation: editForm.occupation,
...(isDeceased.value
? {
deathDate: editForm.deathDate,
deathLunar: editForm.deathLunar,
deathAge: editForm.deathAge,
deathPlace: editForm.deathPlace,
deathExpressionCode: editForm.deathType,
burialDate: editForm.burialDate,
burialPlace: editForm.burialPlace,
}
: {}),
personStatus: editForm.personStatus,
biography: editForm.summary,
remark: editForm.remark,
sortOrder: editForm.sortOrder,
},
{ requestController: memberUpdateRequestController },
);
if (!pageActive) return;
if (originalMember.value.canManageSensitiveMedicalHistory) {
const sensitiveHistory = editForm.hereditaryMedicalHistory.trim();
if (sensitiveHistory) {
await lineageApi.saveSensitiveProfile(
genealogyId.value,
personId.value,
sensitiveHistory,
{ requestController: sensitiveProfileRequestController },
);
originalSensitiveProfilePresent.value = true;
} else if (originalSensitiveProfilePresent.value) {
await lineageApi.clearSensitiveProfile(
genealogyId.value,
personId.value,
{ requestController: sensitiveProfileRequestController },
);
originalSensitiveProfilePresent.value = false;
}
}
if (!pageActive) return;
committedMemberSnapshot.value = currentSnapshot;
baseline.value = currentSnapshot;
}
await finishPage(
"T03",
{
genealogyId: genealogyId.value,
personId: personId.value,
},
{
operation: "member-updated",
entityId: personId.value,
refresh: true,
},
);
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
const updateCommitted =
committedMemberSnapshot.value === formSnapshot.value;
failedAction.value = updateCommitted ? "navigate" : "save";
errorMessage.value = updateCommitted
? "资料已经保存,但页面返回失败。请重试返回成员档案。"
: getRequestErrorMessage(error, "资料尚未保存,请稍后重试。");
editState.value = "error";
} finally {
if (pageActive) isSubmitting.value = false;
}
};
const returnToMember = async () => {
const confirmed = isDirty.value ? await requestDiscardConfirmation() : true;
if (!confirmed) return false;
return goBack();
};
const handleResultAction = () => {
if (failedAction.value === "navigate") return saveMember();
if (failedAction.value === "save") {
editState.value = "form";
return;
}
if (editState.value === "error") {
if (!genealogyId.value || !personId.value) return returnToMember();
return loadMember();
}
editState.value = "form";
};
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.edit-member-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.edit-member-page__header {
z-index: 3;
}
.edit-member-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,
.form-note {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.5;
}
.member-context,
.form-field {
@include adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
align-items: center;
gap: 20rpx;
margin-top: 14rpx;
padding: 12rpx 22rpx;
}
.member-context text:first-child,
.form-field > text:first-child {
color: $ink;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
}
.member-context text:last-child,
.form-field > text:last-child,
.form-field input,
.form-field textarea {
min-width: 0;
color: $ink;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.45;
text-align: right;
}
.form-field textarea {
width: auto;
min-height: var(--app-touch-min);
text-align: left;
}
.form-field input {
min-height: var(--app-touch-min);
}
.form-field__hint,
.upload-receipt {
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.4;
text-align: right;
}
.form-field--upload > view {
display: grid;
justify-items: end;
gap: 6rpx;
}
.upload-button {
min-height: var(--app-touch-min);
margin: 0;
padding: 0 16rpx;
border: 1rpx solid rgba(159, 23, 15, 0.42);
border-radius: 8rpx;
background: transparent;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
}
.form-field--summary {
align-items: start;
min-height: 200rpx;
}
.form-placeholder {
color: #a79884;
}
.field-error {
display: block;
margin: 5rpx 18rpx 0;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
line-height: max(1.35em, clamp(17px, 32rpx, 22px));
}
.form-note {
text-align: center;
}
.form-action {
display: grid;
width: 100%;
min-height: var(--app-touch-min);
margin-top: 18rpx;
}
.form-action image,
.form-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.form-action text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
}
.edit-result {
margin-top: 30%;
text-align: center;
}
.edit-result .form-eyebrow,
.edit-result .form-copy {
text-align: center;
}
.edit-result .form-action {
width: 420rpx;
max-width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.edit-member-panel {
width: calc(100% - 48rpx);
}
}
</style>