Files
jiapuapp/pages/tree/t04-add-relative.vue
T
2026-07-21 07:53:08 +08:00

245 lines
11 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.
<!-- 页面编号T-04用途录入首位成员或为指定成员新增亲属 -->
<template>
<view
class="add-relative-page"
:class="{
'add-state--form': addState === 'form',
'add-state--success': addState === 'success',
'add-state--error': addState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="add-relative-page__header">
<PageHeader :title="isFirstMember ? '录入首位成员' : '新增亲属'" />
</view>
<view class="add-relative-panel">
<view v-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>
<view v-if="!isFirstMember" class="member-context">
<text>当前成员</text><text>{{ currentMember.name }} · {{ currentMember.generation }} </text>
</view>
<view class="form-field">
<text>姓名</text>
<input
v-model="addForm.name"
maxlength="20"
placeholder="请输入真实姓名"
placeholder-class="form-placeholder"
@input="clearError('name')"
/>
</view>
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
<picker
v-if="!isFirstMember"
:range="relationOptions"
:value="relationIndex"
@change="selectRelation"
>
<view class="form-field form-field--picker">
<text>与本人关系</text>
<text>{{ addForm.relation || "请选择亲属关系" }}</text>
</view>
</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>
</view>
</picker>
<view class="form-field form-field--summary">
<text>简要说明</text>
<textarea
v-model="addForm.summary"
auto-height
maxlength="200"
placeholder="选填:字辈、祖居地或身份说明"
placeholder-class="form-placeholder"
/>
</view>
<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 ? "正在保存…" : isFirstMember ? "保存首位成员" : "保存亲属" }}</text>
</view>
</view>
<view v-else class="add-result">
<text class="form-eyebrow">{{ addState === "success" ? "世系资料已更新" : "成员未保存" }}</text>
<text class="form-title">{{ addState === "success" ? successTitle : "暂时无法保存成员" }}</text>
<text class="form-copy">{{ addState === "success" ? successCopy : "当前填写内容仍保留,可返回修改后重试。" }}</text>
<view class="form-action" @click="addState === 'success' ? returnToTree() : retryForm()">
<image src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="scaleToFill" />
<text>{{ addState === "success" ? "返回世系树" : "返回修改" }}</text>
</view>
</view>
</view>
<AppDialog
:visible="discardDialogVisible"
eyebrow="尚未保存"
title="要放弃本次填写吗"
message="返回后,本次新增成员的内容不会保留。"
cancel-text="继续填写"
confirm-text="放弃并返回"
show-cancel
@close="discardDialogVisible = false"
@cancel="discardDialogVisible = false"
@confirm="discardAndBack"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppDialog from "@/components/AppDialog.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { genealogyContext } from "@/utils/genealogy-context.js";
const addState = ref("form");
const genealogyId = ref("");
const personId = ref("");
const mode = ref("relative");
const isSubmitting = ref(false);
const discardDialogVisible = ref(false);
let submitTimer = null;
const memberFixtures = {
101: { id: 101, name: "汤文远", generation: 12 },
102: { id: 102, name: "汤正国", generation: 13 },
103: { id: 103, name: "汤正华", generation: 13 },
};
const relationOptions = ["长子", "次子", "女儿", "配偶", "兄弟", "姐妹"];
const genderOptions = ["男", "女", "未说明"];
const addForm = reactive({ name: "", relation: "", gender: "", birthDate: "", summary: "" });
const fieldErrors = reactive({ name: "", relation: "", gender: "" });
const isFirstMember = computed(
() => mode.value === "first" || (!personId.value && mode.value !== "relative"),
);
const currentMember = computed(
() => memberFixtures[personId.value] || { id: personId.value, name: "当前成员", generation: "待确认" },
);
const relationIndex = computed(() => Math.max(0, relationOptions.indexOf(addForm.relation)));
const genderIndex = computed(() => Math.max(0, genderOptions.indexOf(addForm.gender)));
const formTitle = computed(() =>
isFirstMember.value ? "录入家谱中的第一位成员" : `为${currentMember.value.name}添加一位亲属`,
);
const formCopy = computed(() =>
isFirstMember.value
? "首位成员将成为世系起点,后续可从此人继续补充配偶、子女和后代。"
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
);
const formNote = computed(() =>
isFirstMember.value
? "保存后进入世系树,并可继续为首位成员添加亲属。"
: "保存后成员会出现在相应世代;详细生平可在成员档案中继续完善。",
);
const successTitle = computed(() =>
isFirstMember.value ? `${addForm.name}已成为世系起点` : `${addForm.name}已加入世系`,
);
const successCopy = computed(() =>
isFirstMember.value
? "首位成员已保存,世系树现在可以继续向下补充。"
: `${addForm.relation}关系已记录,返回后可查看新的成员节点。`,
);
const hasDraft = computed(() =>
Object.values(addForm).some((value) => String(value).trim()),
);
onLoad((query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || "";
personId.value = query.personId || "";
mode.value = query.mode === "first" ? "first" : "relative";
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value);
addState.value = query.state === "success" ? "success" : query.state === "error" ? "error" : "form";
});
onUnload(() => {
if (submitTimer) clearTimeout(submitTimer);
});
onBackPress(() => {
if (discardDialogVisible.value) {
discardDialogVisible.value = false;
return true;
}
if (addState.value === "form" && hasDraft.value) {
discardDialogVisible.value = true;
return true;
}
return false;
});
const clearError = (field) => { fieldErrors[field] = ""; };
const selectRelation = (event) => {
addForm.relation = relationOptions[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;
};
const submitAdd = () => {
if (isSubmitting.value || !validateAddForm()) return;
isSubmitting.value = true;
submitTimer = setTimeout(() => {
addState.value = addForm.name.trim() === "失败" ? "error" : "success";
isSubmitting.value = false;
submitTimer = null;
}, 280);
};
const retryForm = () => { addState.value = "form"; };
const returnToTree = () => uni.navigateBack();
const discardAndBack = () => {
discardDialogVisible.value = false;
uni.navigateBack();
};
</script>
<style scoped lang="scss">
.add-relative-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.add-relative-page__header { z-index: 3; }
.add-relative-panel { z-index: 2; width: calc(100% - 32rpx); min-height: min(680px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 28rpx; padding: 7.5% 8%; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t01-state-panel.png") center / 100% 100% no-repeat; }
.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 { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 78rpx; align-items: center; gap: 20rpx; margin-top: 14rpx; padding: 12rpx 22rpx; box-sizing: border-box; background: url("/static/assets/modules/tree/transparent/t07-search-input-frame.png") center / 100% 100% no-repeat; }
.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; }
.add-result { margin-top: 30%; text-align: center; }
.add-result .form-eyebrow, .add-result .form-copy { text-align: center; }
.add-result .form-action { width: 420rpx; max-width: 100%; margin-right: auto; margin-left: auto; }
@media (min-width: 400px) { .add-relative-panel { width: calc(100% - 48rpx); } }
</style>