完成70%

This commit is contained in:
2026-07-24 07:56:22 +08:00
parent bb6431b319
commit c7f278fe79
92 changed files with 4741 additions and 14440 deletions
+121 -68
View File
@@ -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()