修改完成

This commit is contained in:
2026-07-21 07:53:03 +08:00
parent 01d246c47b
commit ff60119277
172 changed files with 7982 additions and 2999 deletions
+242 -3
View File
@@ -1,5 +1,244 @@
<!-- 页面编号T-04用途新增直系亲属与保存结果 -->
<template><TreeMemberForm kind="add" /></template>
<!-- 页面编号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 TreeMemberForm from "@/components/tree/TreeMemberForm.vue";
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>