918 lines
27 KiB
Vue
918 lines
27 KiB
Vue
<!-- 页面编号:T-04;用途:录入首位成员或为指定成员新增亲属。 -->
|
||
<template>
|
||
<view
|
||
class="add-relative-page"
|
||
:class="{
|
||
'add-state--form': addState === 'form',
|
||
'add-state--loading': addState === 'loading',
|
||
'add-state--error': addState === 'error',
|
||
}"
|
||
>
|
||
<ModulePageBackground module="tree" />
|
||
<view class="add-relative-page__header">
|
||
<PageHeader
|
||
:title="isFirstMember ? '录入首位成员' : '新增亲属'"
|
||
custom-back
|
||
@back="requestBack"
|
||
/>
|
||
</view>
|
||
|
||
<view class="add-relative-panel">
|
||
<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>
|
||
|
||
<view v-if="!isFirstMember" class="member-context">
|
||
<text>当前成员</text
|
||
><text
|
||
>{{ currentMember.name }} · 第
|
||
{{ currentMember.generation }} 世</text
|
||
>
|
||
</view>
|
||
|
||
<view class="form-field">
|
||
<text><text class="required-mark">*</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>
|
||
|
||
<view class="form-field">
|
||
<text>人物编号</text>
|
||
<input
|
||
v-model="addForm.personNo"
|
||
maxlength="40"
|
||
placeholder="不填则由服务端生成"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<picker
|
||
:range="sexOptions.map((item) => item.label)"
|
||
:value="optionIndex(sexOptions, addForm.sex)"
|
||
@change="selectOption('sex', sexOptions, $event)"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>性别</text
|
||
><text>{{ optionLabel(sexOptions, addForm.sex) || "请选择" }}</text>
|
||
</view>
|
||
</picker>
|
||
<picker
|
||
:range="bindingModeOptions.map((item) => item.label)"
|
||
:value="optionIndex(bindingModeOptions, addForm.bindingMode)"
|
||
@change="selectBindingMode"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>身份认领</text
|
||
><text>{{
|
||
optionLabel(bindingModeOptions, addForm.bindingMode)
|
||
}}</text>
|
||
</view>
|
||
</picker>
|
||
<view class="form-field">
|
||
<text>业务用户</text>
|
||
<text class="form-field__hint">{{ appUserBindingHint }}</text>
|
||
</view>
|
||
<text v-if="fieldErrors.bindingMode" class="field-error">{{
|
||
fieldErrors.bindingMode
|
||
}}</text>
|
||
<view class="form-field form-field--upload">
|
||
<text>头像</text>
|
||
<view>
|
||
<button
|
||
class="upload-button"
|
||
:disabled="isAvatarUploading || isSubmitting"
|
||
@click="uploadAvatar"
|
||
>
|
||
{{ isAvatarUploading ? "上传中…" : "选择图片" }}
|
||
</button>
|
||
<text v-if="avatarFileName" class="upload-receipt"
|
||
>已上传:{{ avatarFileName }}</text
|
||
>
|
||
</view>
|
||
</view>
|
||
<text v-if="avatarUploadError" class="field-error">{{
|
||
avatarUploadError
|
||
}}</text>
|
||
|
||
<picker
|
||
v-if="!isFirstMember"
|
||
:range="relationOptions"
|
||
:value="relationIndex"
|
||
@change="selectRelation"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text><text class="required-mark">*</text>与本人关系</text>
|
||
<text>{{ addForm.relation || "请选择亲属关系" }}</text>
|
||
</view>
|
||
</picker>
|
||
<text v-if="fieldErrors.relation" class="field-error">{{
|
||
fieldErrors.relation
|
||
}}</text>
|
||
|
||
<view class="form-field">
|
||
<text>别名</text>
|
||
<input
|
||
v-model="addForm.aliasName"
|
||
maxlength="40"
|
||
placeholder="按家谱记载填写"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="form-field">
|
||
<text>字辈</text>
|
||
<input
|
||
v-model="addForm.generationName"
|
||
maxlength="12"
|
||
placeholder="按家谱记载填写"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="form-field">
|
||
<text>世代</text>
|
||
<input
|
||
v-if="!isFirstMember"
|
||
v-model="addForm.generation"
|
||
type="number"
|
||
placeholder="可选正整数"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
<text v-else>首位成员固定为 1</text>
|
||
</view>
|
||
<picker
|
||
:range="personOptionLabels"
|
||
:value="personOptionIndex(addForm.fatherId)"
|
||
@change="selectPerson('fatherId', $event)"
|
||
>
|
||
<view class="form-field form-field--picker"
|
||
><text>父亲</text
|
||
><text>{{ personOptionLabel(addForm.fatherId) }}</text></view
|
||
>
|
||
</picker>
|
||
<picker
|
||
:range="personOptionLabels"
|
||
:value="personOptionIndex(addForm.motherId)"
|
||
@change="selectPerson('motherId', $event)"
|
||
>
|
||
<view class="form-field form-field--picker"
|
||
><text>母亲</text
|
||
><text>{{ personOptionLabel(addForm.motherId) }}</text></view
|
||
>
|
||
</picker>
|
||
|
||
<picker
|
||
mode="date"
|
||
:value="addForm.birthDate"
|
||
@change="selectBirthDate"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>出生日期</text
|
||
><text>{{ addForm.birthDate || "请选择" }}</text>
|
||
</view>
|
||
</picker>
|
||
<picker
|
||
:range="lunarOptions.map((item) => item.label)"
|
||
:value="optionIndex(lunarOptions, addForm.birthLunar)"
|
||
@change="selectOption('birthLunar', lunarOptions, $event)"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>出生农历</text
|
||
><text>{{
|
||
optionLabel(lunarOptions, addForm.birthLunar) || "请选择"
|
||
}}</text>
|
||
</view>
|
||
</picker>
|
||
<view class="form-field">
|
||
<text>出生地</text>
|
||
<input
|
||
v-model="addForm.birthPlace"
|
||
maxlength="60"
|
||
placeholder="按家谱记载填写"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
|
||
<picker
|
||
mode="date"
|
||
:value="addForm.deathDate"
|
||
@change="selectDeathDate"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>逝世日期</text
|
||
><text>{{ addForm.deathDate || "请选择" }}</text>
|
||
</view>
|
||
</picker>
|
||
<picker
|
||
:range="lunarOptions.map((item) => item.label)"
|
||
:value="optionIndex(lunarOptions, addForm.deathLunar)"
|
||
@change="selectOption('deathLunar', lunarOptions, $event)"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>逝世农历</text
|
||
><text>{{
|
||
optionLabel(lunarOptions, addForm.deathLunar) || "请选择"
|
||
}}</text>
|
||
</view>
|
||
</picker>
|
||
<view class="form-field">
|
||
<text>逝世地</text>
|
||
<input
|
||
v-model="addForm.deathPlace"
|
||
maxlength="60"
|
||
placeholder="按家谱记载填写"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="form-field">
|
||
<text>安葬地</text>
|
||
<input
|
||
v-model="addForm.burialPlace"
|
||
maxlength="60"
|
||
placeholder="按家谱记载填写"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<picker
|
||
:range="personStatusOptions.map((item) => item.label)"
|
||
:value="optionIndex(personStatusOptions, addForm.personStatus)"
|
||
@change="selectOption('personStatus', personStatusOptions, $event)"
|
||
>
|
||
<view class="form-field form-field--picker">
|
||
<text>人物状态</text
|
||
><text>{{
|
||
optionLabel(personStatusOptions, addForm.personStatus) || "请选择"
|
||
}}</text>
|
||
</view>
|
||
</picker>
|
||
<view class="form-field">
|
||
<text>排序值</text>
|
||
<input
|
||
v-model="addForm.sortOrder"
|
||
type="number"
|
||
placeholder="可选整数,值越小越靠前"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="form-field">
|
||
<text>关系名称</text>
|
||
<input
|
||
v-if="isFirstMember"
|
||
v-model="addForm.relationName"
|
||
maxlength="30"
|
||
placeholder="可选关系显示名称"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
<text v-else>{{ relationLabel }}</text>
|
||
</view>
|
||
|
||
<view class="form-field form-field--summary">
|
||
<text>人物简介</text>
|
||
<textarea
|
||
v-model="addForm.biography"
|
||
auto-height
|
||
maxlength="500"
|
||
placeholder="按家谱记载填写"
|
||
placeholder-class="form-placeholder"
|
||
/>
|
||
</view>
|
||
<view class="form-field form-field--summary">
|
||
<text>备注</text>
|
||
<textarea
|
||
v-model="addForm.remark"
|
||
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 ? "正在保存…" : "保存成员" }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else class="add-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 {
|
||
isImagePickCancelled,
|
||
pickAndUploadImage,
|
||
} from "@/utils/resumable-image-upload.js";
|
||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||
import { handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js";
|
||
|
||
const addState = ref("loading");
|
||
const genealogyId = ref("");
|
||
const personId = ref("");
|
||
const mode = ref("relative");
|
||
const relationType = ref("");
|
||
const isSubmitting = ref(false);
|
||
const isAvatarUploading = ref(false);
|
||
const avatarFileName = ref("");
|
||
const avatarUploadError = ref("");
|
||
const discardDialogVisible = ref(false);
|
||
const currentMember = ref(null);
|
||
const errorMessage = ref("");
|
||
const addRequestController = createRequestController();
|
||
const personOptionsRequestController = createRequestController();
|
||
let loadSequence = 0;
|
||
|
||
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 sexOptions = Object.freeze([
|
||
{ label: "男", value: "0" },
|
||
{ label: "女", value: "1" },
|
||
{ label: "未知", value: "2" },
|
||
]);
|
||
const lunarOptions = Object.freeze([
|
||
{ label: "否", value: "0" },
|
||
{ label: "是", value: "1" },
|
||
]);
|
||
const personStatusOptions = Object.freeze([
|
||
{ label: "健在", value: "0" },
|
||
{ label: "已故", value: "1" },
|
||
{ label: "未知", value: "2" },
|
||
]);
|
||
const bindingModeOptions = Object.freeze([
|
||
{ label: "不绑定账号", value: "NONE" },
|
||
{ label: "绑定当前账号", value: "SELF" },
|
||
{ label: "绑定指定用户", value: "SPECIFIED" },
|
||
]);
|
||
const personOptions = ref([{ label: "不选择", value: "" }]);
|
||
const personOptionLabels = computed(() =>
|
||
personOptions.value.map((item) => item.label),
|
||
);
|
||
const addForm = reactive({
|
||
bindingMode: "NONE",
|
||
personNo: "",
|
||
name: "",
|
||
relation: "",
|
||
aliasName: "",
|
||
sex: "",
|
||
generation: "",
|
||
generationName: "",
|
||
fatherId: "",
|
||
motherId: "",
|
||
avatarOssId: "",
|
||
birthDate: "",
|
||
birthLunar: "",
|
||
birthPlace: "",
|
||
deathDate: "",
|
||
deathLunar: "",
|
||
deathPlace: "",
|
||
burialPlace: "",
|
||
personStatus: "",
|
||
biography: "",
|
||
remark: "",
|
||
sortOrder: "",
|
||
relationName: "",
|
||
});
|
||
const fieldErrors = reactive({ name: "", relation: "", bindingMode: "" });
|
||
|
||
const isFirstMember = computed(() => mode.value === "first");
|
||
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 appUserBindingHint = computed(() => {
|
||
if (addForm.bindingMode === "NONE") return "不绑定账号,不提交业务用户 ID";
|
||
if (addForm.bindingMode === "SELF")
|
||
return "由服务端从 Token 绑定当前登录账号,不提交业务用户 ID";
|
||
return "需先选择指定业务用户;当前没有可用候选接口";
|
||
});
|
||
const hasValidContext = computed(
|
||
() =>
|
||
Boolean(genealogyId.value) &&
|
||
(isFirstMember.value ? !personId.value : Boolean(currentMember.value)),
|
||
);
|
||
const relationIndex = computed(() =>
|
||
Math.max(0, relationOptions.value.indexOf(addForm.relation)),
|
||
);
|
||
const formTitle = computed(() =>
|
||
isFirstMember.value
|
||
? "录入家谱中的第一位成员"
|
||
: `为${currentMember.value?.name || "当前成员"}添加${relationLabel.value}`,
|
||
);
|
||
const formCopy = computed(() =>
|
||
isFirstMember.value
|
||
? "首位成员将成为世系起点,后续可从此人继续补充配偶、子女和后代。"
|
||
: "先确认新成员与当前成员的关系,再填写可核实的身份信息。",
|
||
);
|
||
const formNote = computed(() => "红色 * 为必填项,其余内容可按家谱记载补充。");
|
||
const resultCopy = computed(() => {
|
||
if (hasValidContext.value) {
|
||
return {
|
||
eyebrow: "保存失败",
|
||
title: `${isFirstMember.value ? "首位成员" : relationLabel.value}尚未保存`,
|
||
copy: errorMessage.value || "服务未确认本次写入,请检查填写后重试。",
|
||
action: "返回修改",
|
||
};
|
||
}
|
||
return {
|
||
eyebrow: "成员入口无效",
|
||
title: "没有找到要关联的成员",
|
||
copy: errorMessage.value || "请从世系树重新进入。",
|
||
action: "返回世系树",
|
||
};
|
||
});
|
||
const hasDraft = computed(() =>
|
||
Object.entries(addForm).some(
|
||
([field, value]) => field !== "bindingMode" && String(value).trim(),
|
||
),
|
||
);
|
||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||
discardDialogVisible.value = visible;
|
||
});
|
||
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 || "当前成员暂不可用。";
|
||
}
|
||
};
|
||
const loadPersonOptions = async () => {
|
||
try {
|
||
const rows = await appApi.getLineagePersonOptions(genealogyId.value, {
|
||
requestController: personOptionsRequestController,
|
||
});
|
||
const options = rows
|
||
.map((item) => {
|
||
const value = String(item?.personId ?? item?.id ?? "");
|
||
if (!/^[1-9]\d*$/.test(value)) return null;
|
||
const name = String(
|
||
item?.name || item?.personName || item?.personNo || "未命名人物",
|
||
);
|
||
return { value, label: `${name}(${value})` };
|
||
})
|
||
.filter(Boolean);
|
||
personOptions.value = [{ label: "不选择", value: "" }, ...options];
|
||
} catch (error) {
|
||
if (!isRequestCancelled(error))
|
||
personOptions.value = [{ label: "暂无可选人物", value: "" }];
|
||
}
|
||
};
|
||
onLoad((query) => {
|
||
genealogyId.value = String(query.genealogyId || "");
|
||
personId.value = String(query.personId || "");
|
||
mode.value = query.mode === "first" ? "first" : "relative";
|
||
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;
|
||
}
|
||
void loadPersonOptions();
|
||
if (isFirstMember.value) {
|
||
addState.value = "form";
|
||
return;
|
||
}
|
||
void loadCurrentMember();
|
||
});
|
||
onUnload(() => {
|
||
loadSequence += 1;
|
||
addRequestController.abort();
|
||
personOptionsRequestController.abort();
|
||
discardConfirmation.dispose();
|
||
});
|
||
|
||
const requestBack = () =>
|
||
runBackGuard({
|
||
transientOpen: discardDialogVisible.value,
|
||
dirty: addState.value === "form" && hasDraft.value,
|
||
submitting: isSubmitting.value || isAvatarUploading.value,
|
||
"close-transient": cancelDiscard,
|
||
"block-submitting": () => true,
|
||
"confirm-discard": requestDiscardConfirmation,
|
||
});
|
||
|
||
onBackPress((event) => handleBackPress(event, requestBack));
|
||
|
||
const clearError = (field) => {
|
||
fieldErrors[field] = "";
|
||
};
|
||
const selectRelation = (event) => {
|
||
addForm.relation = relationOptions.value[Number(event.detail.value)] || "";
|
||
clearError("relation");
|
||
};
|
||
const optionIndex = (options, value) =>
|
||
Math.max(
|
||
0,
|
||
options.findIndex((item) => item.value === value),
|
||
);
|
||
const optionLabel = (options, value) =>
|
||
options.find((item) => item.value === value)?.label || "";
|
||
const selectOption = (field, options, event) => {
|
||
addForm[field] = options[Number(event.detail.value)]?.value || "";
|
||
};
|
||
const selectBindingMode = (event) => {
|
||
addForm.bindingMode =
|
||
bindingModeOptions[Number(event.detail.value)]?.value || "NONE";
|
||
fieldErrors.bindingMode = "";
|
||
};
|
||
const personOptionIndex = (value) =>
|
||
Math.max(
|
||
0,
|
||
personOptions.value.findIndex((item) => item.value === value),
|
||
);
|
||
const personOptionLabel = (value) =>
|
||
personOptions.value.find((item) => item.value === value)?.label || "不选择";
|
||
const selectPerson = (field, event) => {
|
||
addForm[field] = personOptions.value[Number(event.detail.value)]?.value || "";
|
||
};
|
||
const uploadAvatar = async () => {
|
||
if (isAvatarUploading.value || isSubmitting.value) return;
|
||
isAvatarUploading.value = true;
|
||
avatarUploadError.value = "";
|
||
try {
|
||
const receipt = await pickAndUploadImage({
|
||
requestController: addRequestController,
|
||
});
|
||
addForm.avatarOssId = receipt.ossId;
|
||
avatarFileName.value = receipt.fileName || "头像图片";
|
||
} catch (error) {
|
||
if (!isImagePickCancelled(error) && !isRequestCancelled(error))
|
||
avatarUploadError.value = error?.message || "头像上传失败,请稍后重试";
|
||
} finally {
|
||
isAvatarUploading.value = false;
|
||
}
|
||
};
|
||
const selectBirthDate = (event) => {
|
||
addForm.birthDate = event.detail.value || "";
|
||
};
|
||
const selectDeathDate = (event) => {
|
||
addForm.deathDate = event.detail.value || "";
|
||
};
|
||
const validateAddForm = () => {
|
||
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
|
||
fieldErrors.relation =
|
||
isFirstMember.value || addForm.relation ? "" : "请选择与当前成员的关系";
|
||
fieldErrors.bindingMode =
|
||
addForm.bindingMode === "SPECIFIED"
|
||
? "当前未提供可选业务用户,不能指定认领"
|
||
: "";
|
||
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.bindingMode;
|
||
};
|
||
const submitAdd = async () => {
|
||
if (isSubmitting.value || isAvatarUploading.value || !validateAddForm())
|
||
return;
|
||
isSubmitting.value = true;
|
||
try {
|
||
const payload = {
|
||
bindingMode: addForm.bindingMode,
|
||
personNo: addForm.personNo,
|
||
name: addForm.name,
|
||
aliasName: addForm.aliasName,
|
||
sex: addForm.sex,
|
||
generationName: addForm.generationName,
|
||
fatherId: addForm.fatherId,
|
||
motherId: addForm.motherId,
|
||
avatarOssId: addForm.avatarOssId,
|
||
birthDate: addForm.birthDate,
|
||
birthLunar: addForm.birthLunar,
|
||
birthPlace: addForm.birthPlace,
|
||
deathDate: addForm.deathDate,
|
||
deathLunar: addForm.deathLunar,
|
||
deathPlace: addForm.deathPlace,
|
||
burialPlace: addForm.burialPlace,
|
||
personStatus: addForm.personStatus,
|
||
biography: addForm.biography,
|
||
remark: addForm.remark,
|
||
sortOrder: addForm.sortOrder,
|
||
...(isFirstMember.value
|
||
? { generation: 1, relationName: addForm.relationName }
|
||
: {
|
||
...(addForm.generation
|
||
? { generation: Number(addForm.generation) }
|
||
: {}),
|
||
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, {
|
||
bindingMode: "NONE",
|
||
personNo: "",
|
||
name: "",
|
||
relation: "",
|
||
aliasName: "",
|
||
sex: "",
|
||
generation: "",
|
||
generationName: "",
|
||
fatherId: "",
|
||
motherId: "",
|
||
avatarOssId: "",
|
||
birthDate: "",
|
||
birthLunar: "",
|
||
birthPlace: "",
|
||
deathDate: "",
|
||
deathLunar: "",
|
||
deathPlace: "",
|
||
burialPlace: "",
|
||
personStatus: "",
|
||
biography: "",
|
||
remark: "",
|
||
sortOrder: "",
|
||
relationName: "",
|
||
});
|
||
avatarFileName.value = "";
|
||
await returnTo("T01", { genealogyId: genealogyId.value });
|
||
} catch (error) {
|
||
if (isRequestCancelled(error)) return;
|
||
errorMessage.value = error?.message || "服务未确认本次写入。";
|
||
addState.value = "error";
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
const handleResultAction = () =>
|
||
hasValidContext.value ? (addState.value = "form") : returnToTree();
|
||
const returnToTree = async () => {
|
||
const confirmed = hasDraft.value ? await requestDiscardConfirmation() : true;
|
||
if (!confirmed) return false;
|
||
return returnTo("T01", { genealogyId: genealogyId.value });
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
|
||
|
||
.add-relative-page {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
flex-direction: column;
|
||
background: $paper;
|
||
}
|
||
.add-relative-page__header {
|
||
z-index: 3;
|
||
}
|
||
.add-relative-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;
|
||
}
|
||
.required-mark {
|
||
display: inline-block;
|
||
margin-right: 4rpx;
|
||
color: $brand-red;
|
||
font-size: 22rpx;
|
||
line-height: 1;
|
||
transform: translateY(-2rpx);
|
||
}
|
||
.form-field textarea {
|
||
width: auto;
|
||
min-height: 54rpx;
|
||
text-align: left;
|
||
}
|
||
.form-field__hint,
|
||
.upload-receipt {
|
||
color: $ink-muted;
|
||
font-size: 21rpx;
|
||
line-height: 1.4;
|
||
text-align: right;
|
||
}
|
||
.form-field--upload > view {
|
||
display: grid;
|
||
justify-items: end;
|
||
gap: 6rpx;
|
||
}
|
||
.upload-button {
|
||
min-height: 54rpx;
|
||
margin: 0;
|
||
padding: 0 16rpx;
|
||
border: 1rpx solid rgba(159, 23, 15, 0.42);
|
||
border-radius: 8rpx;
|
||
background: transparent;
|
||
color: $brand-red;
|
||
font-size: 22rpx;
|
||
}
|
||
.form-field--summary {
|
||
align-items: start;
|
||
min-height: 200rpx;
|
||
}
|
||
.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>
|