1194 lines
39 KiB
Vue
1194 lines
39 KiB
Vue
<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>
|