322 lines
14 KiB
Vue
322 lines
14 KiB
Vue
<!-- 页面编号:T-05;用途:维护指定成员的身份与生平资料。 -->
|
||
<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>
|
||
|
||
<view class="form-field">
|
||
<text>别名</text>
|
||
<input v-model="editForm.aliasName" 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>
|
||
<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>
|
||
<view class="form-field">
|
||
<text>出生农历</text>
|
||
<input v-model="editForm.birthLunar" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
|
||
</view>
|
||
<view class="form-field">
|
||
<text>出生地</text>
|
||
<input v-model="editForm.birthPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
|
||
</view>
|
||
<picker mode="date" :value="editForm.deathDate" @change="selectDate('deathDate', $event)">
|
||
<view class="form-field form-field--picker"><text>离世日期</text><text>{{ editForm.deathDate || "在世或未填写" }}</text></view>
|
||
</picker>
|
||
<view class="form-field">
|
||
<text>逝世农历</text>
|
||
<input v-model="editForm.deathLunar" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
|
||
</view>
|
||
<view class="form-field">
|
||
<text>逝世地</text>
|
||
<input v-model="editForm.deathPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
|
||
</view>
|
||
<view class="form-field">
|
||
<text>安葬地</text>
|
||
<input v-model="editForm.burialPlace" 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/a01-scroll-primary-v3.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/a01-scroll-primary-v3.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 { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||
import {
|
||
goBack,
|
||
handleBackPress,
|
||
runBackGuard,
|
||
} from "@/utils/navigation.js";
|
||
|
||
const editState = ref("loading");
|
||
const genealogyId = ref("");
|
||
const personId = ref("");
|
||
const isSubmitting = ref(false);
|
||
const discardDialogVisible = ref(false);
|
||
const errorMessage = ref("");
|
||
const failedAction = ref("load");
|
||
const editRequestController = createRequestController();
|
||
let loadSequence = 0;
|
||
|
||
const originalMember = ref(null);
|
||
const baseline = ref("");
|
||
const editForm = reactive({
|
||
name: "",
|
||
aliasName: "",
|
||
generationName: "",
|
||
birthDate: "",
|
||
birthLunar: "",
|
||
birthPlace: "",
|
||
deathDate: "",
|
||
deathLunar: "",
|
||
deathPlace: "",
|
||
burialPlace: "",
|
||
summary: "",
|
||
remark: "",
|
||
});
|
||
const fieldErrors = reactive({ name: "", dates: "" });
|
||
|
||
const formSnapshot = computed(() => JSON.stringify(editForm));
|
||
const hasValidContext = computed(() =>
|
||
Boolean(genealogyId.value && personId.value && originalMember.value),
|
||
);
|
||
const isDirty = computed(
|
||
() =>
|
||
editState.value === "form" &&
|
||
Boolean(baseline.value) &&
|
||
formSnapshot.value !== baseline.value,
|
||
);
|
||
const resultCopy = computed(() => ({
|
||
error: 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 loadMember = async () => {
|
||
const activeLoad = ++loadSequence;
|
||
editState.value = "loading";
|
||
try {
|
||
const member = await appApi.getPerson(genealogyId.value, personId.value, {
|
||
requestController: editRequestController,
|
||
});
|
||
if (activeLoad !== loadSequence) return;
|
||
originalMember.value = member;
|
||
Object.assign(editForm, {
|
||
name: member.name,
|
||
aliasName: member.aliasName || "",
|
||
generationName: member.generationName || "",
|
||
birthDate: member.birthDate || "",
|
||
birthLunar: member.birthLunar || "",
|
||
birthPlace: member.birthplace || "",
|
||
deathDate: member.deathDate || "",
|
||
deathLunar: member.deathLunar || "",
|
||
deathPlace: member.deathPlace || "",
|
||
burialPlace: member.burialPlace || "",
|
||
summary: member.biography || "",
|
||
remark: member.remark || "",
|
||
});
|
||
baseline.value = formSnapshot.value;
|
||
errorMessage.value = "";
|
||
failedAction.value = "load";
|
||
editState.value = "form";
|
||
} catch (error) {
|
||
if (activeLoad !== loadSequence || isRequestCancelled(error)) return;
|
||
originalMember.value = null;
|
||
failedAction.value = "load";
|
||
errorMessage.value = error?.message || "成员资料暂不可用。";
|
||
editState.value = "error";
|
||
}
|
||
};
|
||
|
||
onLoad((query) => {
|
||
genealogyId.value = String(query.genealogyId || "");
|
||
personId.value = String(query.personId || "");
|
||
if (!genealogyId.value || !personId.value || query.state === "error") {
|
||
editState.value = "error";
|
||
errorMessage.value = "当前家谱或成员上下文无效。";
|
||
return;
|
||
}
|
||
void loadMember();
|
||
});
|
||
onUnload(() => {
|
||
loadSequence += 1;
|
||
editRequestController.abort();
|
||
discardConfirmation.dispose();
|
||
});
|
||
|
||
const requestBack = () =>
|
||
runBackGuard({
|
||
transientOpen: discardDialogVisible.value,
|
||
dirty: isDirty.value,
|
||
submitting: isSubmitting.value,
|
||
"close-transient": cancelDiscard,
|
||
"block-submitting": () => true,
|
||
"confirm-discard": requestDiscardConfirmation,
|
||
});
|
||
|
||
onBackPress((event) => handleBackPress(event, requestBack));
|
||
|
||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||
const selectDate = (field, event) => {
|
||
editForm[field] = event.detail.value || "";
|
||
fieldErrors.dates = "";
|
||
};
|
||
const validateEditForm = () => {
|
||
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
|
||
fieldErrors.dates = editForm.birthDate && editForm.deathDate && editForm.deathDate < editForm.birthDate ? "离世日期不能早于出生日期" : "";
|
||
return !fieldErrors.name && !fieldErrors.dates;
|
||
};
|
||
const saveMember = async () => {
|
||
if (isSubmitting.value || !validateEditForm()) return;
|
||
isSubmitting.value = true;
|
||
try {
|
||
await appApi.updatePerson(genealogyId.value, personId.value, {
|
||
name: editForm.name,
|
||
aliasName: editForm.aliasName,
|
||
generationName: editForm.generationName,
|
||
birthDate: editForm.birthDate,
|
||
birthLunar: editForm.birthLunar,
|
||
birthPlace: editForm.birthPlace,
|
||
deathDate: editForm.deathDate,
|
||
deathLunar: editForm.deathLunar,
|
||
deathPlace: editForm.deathPlace,
|
||
burialPlace: editForm.burialPlace,
|
||
biography: editForm.summary,
|
||
remark: editForm.remark,
|
||
}, { requestController: editRequestController });
|
||
baseline.value = formSnapshot.value;
|
||
await goBack();
|
||
} catch (error) {
|
||
if (isRequestCancelled(error)) return;
|
||
failedAction.value = "save";
|
||
errorMessage.value = error?.message || "服务未确认本次修改。";
|
||
editState.value = "error";
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
const returnToMember = async () => {
|
||
const confirmed = isDirty.value
|
||
? await requestDiscardConfirmation()
|
||
: true;
|
||
if (!confirmed) return false;
|
||
return goBack();
|
||
};
|
||
const handleResultAction = () => {
|
||
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">
|
||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||
|
||
.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.adaptive-tree-panel; z-index: 2; width: calc(100% - 32rpx); margin: 18rpx auto 28rpx; padding: 7.5% 8%; }
|
||
.form-eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||
.form-title { display: block; margin-top: 10rpx; color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 34rpx; font-weight: 700; line-height: 1.35; }
|
||
.form-copy, .form-note { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
|
||
.member-context, .form-field { @include adaptive.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: 24rpx; 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: 24rpx; line-height: 1.45; text-align: right; }
|
||
.form-field textarea { width: auto; min-height: 54rpx; text-align: left; }
|
||
.form-field--summary { align-items: start; }
|
||
.form-placeholder { color: #a79884; }
|
||
.field-error { display: block; margin: 5rpx 18rpx 0; color: $brand-red; font-size: 22rpx; line-height: 32rpx; }
|
||
.form-note { text-align: center; }
|
||
.form-action { display: grid; width: 100%; min-height: 76rpx; 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: 24rpx; 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>
|