完成70%
This commit is contained in:
+121
-68
@@ -4,7 +4,7 @@
|
||||
class="add-relative-page"
|
||||
:class="{
|
||||
'add-state--form': addState === 'form',
|
||||
'add-state--preview': addState === 'preview',
|
||||
'add-state--loading': addState === 'loading',
|
||||
'add-state--error': addState === 'error',
|
||||
}"
|
||||
>
|
||||
@@ -14,7 +14,9 @@
|
||||
</view>
|
||||
|
||||
<view class="add-relative-panel">
|
||||
<view v-if="addState === 'form'" class="add-relative-form">
|
||||
<AppLoading v-if="addState === 'loading'" text="正在读取成员资料" description="请稍候,正在确认当前人物。" />
|
||||
|
||||
<view v-else-if="addState === 'form'" class="add-relative-form">
|
||||
<text class="form-eyebrow">{{ isFirstMember ? "建立世系起点" : "补全家族关系" }}</text>
|
||||
<text class="form-title">{{ formTitle }}</text>
|
||||
<text class="form-copy">{{ formCopy }}</text>
|
||||
@@ -48,13 +50,6 @@
|
||||
</picker>
|
||||
<text v-if="fieldErrors.relation" class="field-error">{{ fieldErrors.relation }}</text>
|
||||
|
||||
<picker :range="genderOptions" :value="genderIndex" @change="selectGender">
|
||||
<view class="form-field form-field--picker">
|
||||
<text>性别</text><text>{{ addForm.gender || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<text v-if="fieldErrors.gender" class="field-error">{{ fieldErrors.gender }}</text>
|
||||
|
||||
<picker mode="date" :value="addForm.birthDate" @change="selectBirthDate">
|
||||
<view class="form-field form-field--picker">
|
||||
<text>出生日期</text><text>{{ addForm.birthDate || "选填" }}</text>
|
||||
@@ -75,17 +70,17 @@
|
||||
<text class="form-note">{{ formNote }}</text>
|
||||
<view class="form-action" @click="submitAdd">
|
||||
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
|
||||
<text>{{ isSubmitting ? "正在校验…" : "生成本地预览" }}</text>
|
||||
<text>{{ isSubmitting ? "正在保存…" : "保存成员" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="add-result">
|
||||
<text class="form-eyebrow">{{ addState === "preview" ? "本地流程预览" : hasValidContext ? "成员未保存" : "成员入口无效" }}</text>
|
||||
<text class="form-title">{{ addState === "preview" ? previewTitle : hasValidContext ? "暂时无法校验成员" : "没有找到要关联的成员" }}</text>
|
||||
<text class="form-copy">{{ addState === "preview" ? previewCopy : hasValidContext ? "当前填写内容仍保留,可返回修改后重试。" : "请从世系树重新进入,页面不会创建临时成员身份。" }}</text>
|
||||
<view class="form-action" @click="addState === 'preview' ? returnToTree() : retryForm()">
|
||||
<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>{{ addState === "preview" ? "返回世系树(不保存)" : hasValidContext ? "返回修改" : "返回上一页" }}</text>
|
||||
<text>{{ resultCopy.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -108,46 +103,57 @@
|
||||
<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 { findTreeMemberFixture } from "@/data/mock.js";
|
||||
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
handleBackPress,
|
||||
returnTo,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const addState = ref("form");
|
||||
const addState = ref("loading");
|
||||
const genealogyId = ref("");
|
||||
const personId = ref("");
|
||||
const mode = ref("relative");
|
||||
const relationType = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const discardDialogVisible = ref(false);
|
||||
let submitTimer = null;
|
||||
const currentMember = ref(null);
|
||||
const errorMessage = ref("");
|
||||
const addRequestController = createRequestController();
|
||||
let loadSequence = 0;
|
||||
|
||||
const relationOptions = ["长子", "次子", "女儿", "配偶", "兄弟", "姐妹"];
|
||||
const genderOptions = ["男", "女", "未说明"];
|
||||
const addForm = reactive({ name: "", relation: "", gender: "", birthDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", relation: "", gender: "" });
|
||||
const relationIntents = Object.freeze({
|
||||
FATHER: { label: "父亲" },
|
||||
MOTHER: { label: "母亲" },
|
||||
SPOUSE: { label: "配偶" },
|
||||
SIBLING: { label: "兄弟姐妹" },
|
||||
SON: { label: "儿子" },
|
||||
DAUGHTER: { label: "女儿" },
|
||||
});
|
||||
const genericRelationTypes = Object.freeze(["FATHER", "MOTHER", "SPOUSE", "SIBLING", "SON", "DAUGHTER"]);
|
||||
const addForm = reactive({ name: "", relation: "", birthDate: "", summary: "" });
|
||||
const fieldErrors = reactive({ name: "", relation: "" });
|
||||
|
||||
const isFirstMember = computed(() => mode.value === "first");
|
||||
const currentMember = computed(() =>
|
||||
isFirstMember.value
|
||||
? null
|
||||
: findTreeMemberFixture(genealogyId.value, personId.value),
|
||||
);
|
||||
const activeRelationIntent = computed(() => relationIntents[relationType.value] || null);
|
||||
const relationOptions = computed(() => {
|
||||
const types = activeRelationIntent.value ? [relationType.value] : genericRelationTypes;
|
||||
return types.map((type) => relationIntents[type].label);
|
||||
});
|
||||
const relationLabel = computed(() => activeRelationIntent.value?.label || addForm.relation || "亲属关系");
|
||||
const hasValidContext = computed(
|
||||
() =>
|
||||
Boolean(genealogyId.value) &&
|
||||
(isFirstMember.value ? !personId.value : Boolean(currentMember.value)),
|
||||
);
|
||||
const relationIndex = computed(() => Math.max(0, relationOptions.indexOf(addForm.relation)));
|
||||
const genderIndex = computed(() => Math.max(0, genderOptions.indexOf(addForm.gender)));
|
||||
const relationIndex = computed(() => Math.max(0, relationOptions.value.indexOf(addForm.relation)));
|
||||
const formTitle = computed(() =>
|
||||
isFirstMember.value ? "录入家谱中的第一位成员" : `为${currentMember.value.name}添加一位亲属`,
|
||||
isFirstMember.value ? "录入家谱中的第一位成员" : `为${currentMember.value?.name || "当前成员"}添加${relationLabel.value}`,
|
||||
);
|
||||
const formCopy = computed(() =>
|
||||
isFirstMember.value
|
||||
@@ -155,16 +161,24 @@ const formCopy = computed(() =>
|
||||
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
|
||||
);
|
||||
const formNote = computed(() =>
|
||||
isFirstMember.value
|
||||
? "当前阶段只校验首位成员资料,不会修改世系树。"
|
||||
: "当前阶段只校验亲属资料,不会新增成员或改变世系关系。",
|
||||
);
|
||||
const previewTitle = computed(() =>
|
||||
isFirstMember.value ? `${addForm.name}可作为世系起点` : `${addForm.name}的亲属资料已通过本地校验`,
|
||||
);
|
||||
const previewCopy = computed(() =>
|
||||
`本地预览尚未提交服务器,返回后不会保存${isFirstMember.value ? "首位成员" : `${addForm.relation}关系`},世系树也不会变化。`,
|
||||
"本次只提交姓名、出生日期和人物简介;性别编码、头像和同辈排行不在当前写入范围。",
|
||||
);
|
||||
const resultCopy = computed(() => {
|
||||
if (hasValidContext.value) {
|
||||
return {
|
||||
eyebrow: "保存失败",
|
||||
title: `${relationLabel.value}尚未保存`,
|
||||
copy: errorMessage.value || "服务未确认本次写入,请检查填写后重试。",
|
||||
action: "返回修改",
|
||||
};
|
||||
}
|
||||
return {
|
||||
eyebrow: "成员入口无效",
|
||||
title: "没有找到要关联的成员",
|
||||
copy: errorMessage.value || "请从世系树重新进入。",
|
||||
action: "返回世系树",
|
||||
};
|
||||
});
|
||||
const hasDraft = computed(() =>
|
||||
Object.values(addForm).some((value) => String(value).trim()),
|
||||
);
|
||||
@@ -175,27 +189,58 @@ const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
const loadCurrentMember = async () => {
|
||||
const activeLoad = ++loadSequence;
|
||||
addState.value = "loading";
|
||||
try {
|
||||
const member = await appApi.getPerson(genealogyId.value, personId.value, {
|
||||
requestController: addRequestController,
|
||||
});
|
||||
if (activeLoad !== loadSequence) return;
|
||||
currentMember.value = member;
|
||||
addState.value = "form";
|
||||
} catch (error) {
|
||||
if (activeLoad !== loadSequence || isRequestCancelled(error)) return;
|
||||
currentMember.value = null;
|
||||
addState.value = "error";
|
||||
errorMessage.value = error?.message || "当前成员暂不可用。";
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
personId.value = String(query.personId || "");
|
||||
mode.value = query.mode === "first" ? "first" : "relative";
|
||||
addState.value = !hasValidContext.value || query.state === "error"
|
||||
? "error"
|
||||
: query.state === "preview"
|
||||
? "preview"
|
||||
: "form";
|
||||
relationType.value = String(query.relationType || "");
|
||||
if (!genealogyId.value || query.state === "error" || (!isFirstMember.value && !personId.value)) {
|
||||
addState.value = "error";
|
||||
errorMessage.value = "当前家谱或成员上下文无效,请从世系树重新进入。";
|
||||
return;
|
||||
}
|
||||
if (relationType.value && !activeRelationIntent.value) {
|
||||
addState.value = "error";
|
||||
errorMessage.value = "未知的亲属关系意图,本页不会推断或替换。";
|
||||
return;
|
||||
}
|
||||
if (activeRelationIntent.value) {
|
||||
addForm.relation = activeRelationIntent.value.label;
|
||||
}
|
||||
if (isFirstMember.value) {
|
||||
addState.value = "form";
|
||||
return;
|
||||
}
|
||||
void loadCurrentMember();
|
||||
});
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
loadSequence += 1;
|
||||
addRequestController.abort();
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardDialogVisible.value,
|
||||
dirty: (addState.value === "form" || addState.value === "preview") && hasDraft.value,
|
||||
dirty: addState.value === "form" && hasDraft.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
@@ -206,36 +251,44 @@ onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const clearError = (field) => { fieldErrors[field] = ""; };
|
||||
const selectRelation = (event) => {
|
||||
addForm.relation = relationOptions[Number(event.detail.value)] || "";
|
||||
addForm.relation = relationOptions.value[Number(event.detail.value)] || "";
|
||||
clearError("relation");
|
||||
};
|
||||
const selectGender = (event) => {
|
||||
addForm.gender = genderOptions[Number(event.detail.value)] || "";
|
||||
clearError("gender");
|
||||
};
|
||||
const selectBirthDate = (event) => { addForm.birthDate = event.detail.value || ""; };
|
||||
const validateAddForm = () => {
|
||||
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
|
||||
fieldErrors.relation = isFirstMember.value || addForm.relation ? "" : "请选择与当前成员的关系";
|
||||
fieldErrors.gender = addForm.gender ? "" : "请选择性别或未说明";
|
||||
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.gender;
|
||||
return !fieldErrors.name && !fieldErrors.relation;
|
||||
};
|
||||
const submitAdd = () => {
|
||||
const submitAdd = async () => {
|
||||
if (isSubmitting.value || !validateAddForm()) return;
|
||||
const submitSnapshot = Object.freeze({ ...addForm });
|
||||
isSubmitting.value = true;
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
addState.value = submitSnapshot.name.trim() === "失败" ? "error" : "preview";
|
||||
try {
|
||||
const payload = {
|
||||
name: addForm.name,
|
||||
birthDate: addForm.birthDate,
|
||||
biography: addForm.summary,
|
||||
...(isFirstMember.value ? {} : { relationName: relationLabel.value }),
|
||||
};
|
||||
if (isFirstMember.value) {
|
||||
await appApi.createPerson(genealogyId.value, payload, { requestController: addRequestController });
|
||||
} else {
|
||||
const type = activeRelationIntent.value
|
||||
? relationType.value
|
||||
: genericRelationTypes[relationOptions.value.indexOf(addForm.relation)];
|
||||
await appApi.createRelatedPerson(genealogyId.value, personId.value, type, payload, { requestController: addRequestController });
|
||||
}
|
||||
Object.assign(addForm, { name: "", relation: "", birthDate: "", summary: "" });
|
||||
await returnTo("T01", { genealogyId: genealogyId.value });
|
||||
} catch (error) {
|
||||
if (isRequestCancelled(error)) return;
|
||||
errorMessage.value = error?.message || "服务未确认本次写入。";
|
||||
addState.value = "error";
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
submitTimer = null;
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
};
|
||||
const retryForm = () => {
|
||||
if (!hasValidContext.value) return goBack();
|
||||
addState.value = "form";
|
||||
}
|
||||
};
|
||||
const handleResultAction = () => hasValidContext.value ? (addState.value = "form") : returnToTree();
|
||||
const returnToTree = async () => {
|
||||
const confirmed = hasDraft.value
|
||||
? await requestDiscardConfirmation()
|
||||
|
||||
Reference in New Issue
Block a user