feat: 完成前端业务闭环与后端联调
This commit is contained in:
+270
-17
@@ -134,11 +134,20 @@
|
||||
}}</text>
|
||||
|
||||
<view class="form-field">
|
||||
<text>别名</text>
|
||||
<text>表字</text>
|
||||
<input
|
||||
v-model="addForm.courtesyName"
|
||||
maxlength="40"
|
||||
placeholder="如族谱有记载可填写"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text>别号</text>
|
||||
<input
|
||||
v-model="addForm.aliasName"
|
||||
maxlength="40"
|
||||
placeholder="按家谱记载填写"
|
||||
placeholder="别号、昵称或曾用名"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
@@ -193,6 +202,63 @@
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<picker
|
||||
:range="zodiacOptions.map((item) => item.label)"
|
||||
:value="optionIndex(zodiacOptions, addForm.zodiac)"
|
||||
@change="selectOption('zodiac', zodiacOptions, $event)"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>生肖</text><text>{{ optionLabel(zodiacOptions, addForm.zodiac) || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="form-field">
|
||||
<text>现居地</text>
|
||||
<input
|
||||
v-model="addForm.currentAddress"
|
||||
maxlength="120"
|
||||
placeholder="填写当前常住地区"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text>联系电话</text>
|
||||
<input
|
||||
v-model="addForm.mobile"
|
||||
maxlength="30"
|
||||
placeholder="仅授权成员可见"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text>电子邮箱</text>
|
||||
<input
|
||||
v-model="addForm.email"
|
||||
maxlength="254"
|
||||
placeholder="仅授权成员可见"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.email" class="field-error">{{
|
||||
fieldErrors.email
|
||||
}}</text>
|
||||
<picker
|
||||
:range="educationOptions.map((item) => item.label)"
|
||||
:value="optionIndex(educationOptions, addForm.education)"
|
||||
@change="selectOption('education', educationOptions, $event)"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>学历分类</text><text>{{ optionLabel(educationOptions, addForm.education) || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="form-field">
|
||||
<text>职业</text>
|
||||
<input
|
||||
v-model="addForm.occupation"
|
||||
maxlength="80"
|
||||
placeholder="填写主要职业"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<picker
|
||||
v-if="isDeceased"
|
||||
@@ -218,6 +284,18 @@
|
||||
}}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view v-if="isDeceased" class="form-field">
|
||||
<text>享年</text>
|
||||
<input
|
||||
v-model="addForm.deathAge"
|
||||
type="number"
|
||||
placeholder="0 至 200 的整数"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.deathAge" class="field-error">{{
|
||||
fieldErrors.deathAge
|
||||
}}</text>
|
||||
<view v-if="isDeceased" class="form-field">
|
||||
<text>逝世地</text>
|
||||
<input
|
||||
@@ -227,6 +305,50 @@
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<picker
|
||||
v-if="isDeceased"
|
||||
:range="deathExpressionOptions.map((item) => item.label)"
|
||||
:value="optionIndex(deathExpressionOptions, addForm.deathType)"
|
||||
@change="selectOption('deathType', deathExpressionOptions, $event)"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>逝世表述</text><text>{{ optionLabel(deathExpressionOptions, addForm.deathType) || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view v-if="isDeceased" class="form-field form-field--summary">
|
||||
<text>遗传病史</text>
|
||||
<textarea
|
||||
v-model="addForm.hereditaryMedicalHistory"
|
||||
auto-height
|
||||
maxlength="500"
|
||||
placeholder="敏感信息,无明确依据可不填写"
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="isDeceased" class="form-note"
|
||||
>遗传病史属于敏感健康信息,后端必须按权限返回并记录访问审计。</text
|
||||
>
|
||||
<picker
|
||||
v-if="relationVariantOptions.length"
|
||||
:range="relationVariantOptions.map((item) => item.label)"
|
||||
:value="optionIndex(relationVariantOptions, addForm.relationVariantCode)"
|
||||
@change="selectOption('relationVariantCode', relationVariantOptions, $event)"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>关系称谓</text><text>{{ optionLabel(relationVariantOptions, addForm.relationVariantCode) || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<picker
|
||||
v-if="isDeceased"
|
||||
mode="date"
|
||||
:value="addForm.burialDate"
|
||||
@change="selectBurialDate"
|
||||
>
|
||||
<view class="form-field form-field--picker">
|
||||
<text>安葬日期</text
|
||||
><text>{{ addForm.burialDate || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view v-if="isDeceased" class="form-field">
|
||||
<text>安葬地</text>
|
||||
<input
|
||||
@@ -236,6 +358,9 @@
|
||||
placeholder-class="form-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="fieldErrors.dates" class="field-error">{{
|
||||
fieldErrors.dates
|
||||
}}</text>
|
||||
<picker
|
||||
:range="personStatusOptions.map((item) => item.label)"
|
||||
:value="optionIndex(personStatusOptions, addForm.personStatus)"
|
||||
@@ -329,6 +454,7 @@ import {
|
||||
isRequestCancelled
|
||||
} from "@/services/api/request-controller.js";
|
||||
import { genealogyMemberApi } from "@/services/api/genealogy-member-service.js";
|
||||
import { businessDictionaryApi } from "@/services/api/business-dictionary-service.js";
|
||||
import { lineageApi } from "@/services/api/lineage-service.js";
|
||||
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
|
||||
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
|
||||
@@ -366,6 +492,14 @@ const memberDetailRequestController = createRequestController();
|
||||
const memberOptionsRequestController = createRequestController();
|
||||
const avatarUploadRequestController = createRequestController();
|
||||
const memberCreationRequestController = createRequestController();
|
||||
const sensitiveProfileRequestController = createRequestController();
|
||||
const dictionaryRequestControllers = {
|
||||
zodiac: createRequestController(),
|
||||
education: createRequestController(),
|
||||
deathExpression: createRequestController(),
|
||||
parentVariant: createRequestController(),
|
||||
spouseVariant: createRequestController(),
|
||||
};
|
||||
const memberCreationGuard = createNonIdempotentWriteGuard();
|
||||
let pageActive = true;
|
||||
let loadSequence = 0;
|
||||
@@ -387,6 +521,7 @@ const addForm = reactive({
|
||||
appUserId: "",
|
||||
name: "",
|
||||
relation: "",
|
||||
courtesyName: "",
|
||||
aliasName: "",
|
||||
sex: "",
|
||||
generation: "",
|
||||
@@ -395,18 +530,41 @@ const addForm = reactive({
|
||||
birthDate: "",
|
||||
birthLunar: "",
|
||||
birthPlace: "",
|
||||
zodiac: "",
|
||||
currentAddress: "",
|
||||
mobile: "",
|
||||
email: "",
|
||||
education: "",
|
||||
occupation: "",
|
||||
deathDate: "",
|
||||
deathLunar: "",
|
||||
deathAge: "",
|
||||
deathPlace: "",
|
||||
deathType: "",
|
||||
hereditaryMedicalHistory: "",
|
||||
relationVariantCode: "",
|
||||
burialDate: "",
|
||||
burialPlace: "",
|
||||
personStatus: "",
|
||||
biography: "",
|
||||
remark: "",
|
||||
sortOrder: "",
|
||||
});
|
||||
const fieldErrors = reactive({ name: "", relation: "", bindingMode: "" });
|
||||
const fieldErrors = reactive({
|
||||
name: "",
|
||||
relation: "",
|
||||
bindingMode: "",
|
||||
email: "",
|
||||
dates: "",
|
||||
deathAge: "",
|
||||
});
|
||||
const memberOptionsState = ref("loading");
|
||||
const memberOptions = ref([]);
|
||||
const zodiacOptions = ref([]);
|
||||
const educationOptions = ref([]);
|
||||
const deathExpressionOptions = ref([]);
|
||||
const parentRelationVariantOptions = ref([]);
|
||||
const spouseRelationVariantOptions = ref([]);
|
||||
|
||||
const isFirstMember = computed(() => mode.value === "first");
|
||||
const isDeceased = computed(() => addForm.personStatus === "1");
|
||||
@@ -422,12 +580,27 @@ const relationOptions = computed(() => {
|
||||
const relationLabel = computed(
|
||||
() => activeRelationIntent.value?.label || addForm.relation || "亲属关系",
|
||||
);
|
||||
const selectedRelationType = computed(() =>
|
||||
isFirstMember.value
|
||||
? ""
|
||||
: activeRelationIntent.value
|
||||
? relationType.value
|
||||
: genericRelationTypes[relationOptions.value.indexOf(addForm.relation)] || "",
|
||||
);
|
||||
const relationVariantOptions = computed(() => {
|
||||
if ([memberRelationTypes.FATHER, memberRelationTypes.MOTHER].includes(selectedRelationType.value)) {
|
||||
return parentRelationVariantOptions.value;
|
||||
}
|
||||
return selectedRelationType.value === memberRelationTypes.SPOUSE
|
||||
? spouseRelationVariantOptions.value
|
||||
: [];
|
||||
});
|
||||
const memberOptionLabels = computed(() =>
|
||||
memberOptions.value.map((item) => item.label),
|
||||
);
|
||||
const hasValidContext = computed(
|
||||
() =>
|
||||
Boolean(genealogyId.value) &&
|
||||
/^[1-9]\d*$/.test(genealogyId.value) &&
|
||||
(isFirstMember.value ? !personId.value : Boolean(currentMember.value)),
|
||||
);
|
||||
const relationIndex = computed(() =>
|
||||
@@ -532,14 +705,38 @@ const loadMemberOptions = async () => {
|
||||
memberOptionsState.value = "error";
|
||||
}
|
||||
};
|
||||
const loadBusinessOptions = async () => {
|
||||
try {
|
||||
const [zodiacRows, educationRows, deathRows, parentRows, spouseRows] = await Promise.all([
|
||||
businessDictionaryApi.getBusinessDictionaryOptions("gen_zodiac", { requestController: dictionaryRequestControllers.zodiac }),
|
||||
businessDictionaryApi.getBusinessDictionaryOptions("gen_education_type", { requestController: dictionaryRequestControllers.education }),
|
||||
businessDictionaryApi.getBusinessDictionaryOptions("gen_death_expression", { requestController: dictionaryRequestControllers.deathExpression }),
|
||||
businessDictionaryApi.getBusinessDictionaryOptions("gen_parent_relationship_variant", { requestController: dictionaryRequestControllers.parentVariant }),
|
||||
businessDictionaryApi.getBusinessDictionaryOptions("gen_spouse_relationship_variant", { requestController: dictionaryRequestControllers.spouseVariant }),
|
||||
]);
|
||||
if (!pageActive) return;
|
||||
zodiacOptions.value = zodiacRows;
|
||||
educationOptions.value = educationRows;
|
||||
deathExpressionOptions.value = deathRows;
|
||||
parentRelationVariantOptions.value = parentRows;
|
||||
spouseRelationVariantOptions.value = spouseRows;
|
||||
} catch (error) {
|
||||
if (!pageActive || isRequestCancelled(error)) return;
|
||||
zodiacOptions.value = [];
|
||||
educationOptions.value = [];
|
||||
deathExpressionOptions.value = [];
|
||||
parentRelationVariantOptions.value = [];
|
||||
spouseRelationVariantOptions.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 ||
|
||||
(!isFirstMember.value && !personId.value)
|
||||
!/^[1-9]\d*$/.test(genealogyId.value) ||
|
||||
(!isFirstMember.value && !/^[1-9]\d*$/.test(personId.value))
|
||||
) {
|
||||
addState.value = "error";
|
||||
errorMessage.value = "这个页面已经过期,请从世系树重新进入。";
|
||||
@@ -554,6 +751,7 @@ onLoad((query) => {
|
||||
addForm.relation = activeRelationIntent.value.label;
|
||||
}
|
||||
void loadMemberOptions();
|
||||
void loadBusinessOptions();
|
||||
if (isFirstMember.value) {
|
||||
addState.value = "form";
|
||||
return;
|
||||
@@ -567,6 +765,8 @@ onUnload(() => {
|
||||
memberOptionsRequestController.abort();
|
||||
avatarUploadRequestController.abort();
|
||||
memberCreationRequestController.abort();
|
||||
sensitiveProfileRequestController.abort();
|
||||
Object.values(dictionaryRequestControllers).forEach((controller) => controller.abort());
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
@@ -587,6 +787,7 @@ const clearError = (field) => {
|
||||
};
|
||||
const selectRelation = (event) => {
|
||||
addForm.relation = relationOptions.value[Number(event.detail.value)] || "";
|
||||
addForm.relationVariantCode = "";
|
||||
clearError("relation");
|
||||
};
|
||||
const optionIndex = findMemberOptionIndex;
|
||||
@@ -640,6 +841,9 @@ const selectBirthDate = (event) => {
|
||||
const selectDeathDate = (event) => {
|
||||
addForm.deathDate = event.detail.value || "";
|
||||
};
|
||||
const selectBurialDate = (event) => {
|
||||
addForm.burialDate = event.detail.value || "";
|
||||
};
|
||||
const validateAddForm = () => {
|
||||
fieldErrors.name = addForm.name.trim() ? "" : "请填写成员姓名";
|
||||
fieldErrors.relation =
|
||||
@@ -650,7 +854,22 @@ const validateAddForm = () => {
|
||||
? "可绑定成员暂不可用,请稍后重试"
|
||||
: "请选择可绑定成员"
|
||||
: "";
|
||||
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.bindingMode;
|
||||
fieldErrors.email =
|
||||
addForm.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(addForm.email.trim())
|
||||
? "请填写有效的电子邮箱"
|
||||
: "";
|
||||
fieldErrors.dates =
|
||||
addForm.birthDate && addForm.deathDate && addForm.deathDate < addForm.birthDate
|
||||
? "逝世日期不能早于出生日期"
|
||||
: addForm.deathDate && addForm.burialDate && addForm.burialDate < addForm.deathDate
|
||||
? "安葬日期不能早于逝世日期"
|
||||
: "";
|
||||
const deathAge = addForm.deathAge === "" ? null : Number(addForm.deathAge);
|
||||
fieldErrors.deathAge =
|
||||
deathAge !== null && (!Number.isSafeInteger(deathAge) || deathAge < 0 || deathAge > 200)
|
||||
? "享年必须是 0 至 200 的整数"
|
||||
: "";
|
||||
return !Object.values(fieldErrors).some(Boolean);
|
||||
};
|
||||
const submitAdd = async () => {
|
||||
if (isSubmitting.value || isAvatarUploading.value || !validateAddForm())
|
||||
@@ -661,6 +880,7 @@ const submitAdd = async () => {
|
||||
? { appUserId: addForm.appUserId }
|
||||
: {}),
|
||||
name: addForm.name,
|
||||
courtesyName: addForm.courtesyName,
|
||||
aliasName: addForm.aliasName,
|
||||
sex: addForm.sex,
|
||||
generationName: addForm.generationName,
|
||||
@@ -668,10 +888,23 @@ const submitAdd = async () => {
|
||||
birthDate: addForm.birthDate,
|
||||
birthLunar: addForm.birthLunar,
|
||||
birthPlace: addForm.birthPlace,
|
||||
deathDate: addForm.deathDate,
|
||||
deathLunar: addForm.deathLunar,
|
||||
deathPlace: addForm.deathPlace,
|
||||
burialPlace: addForm.burialPlace,
|
||||
zodiacCode: addForm.zodiac,
|
||||
currentAddress: addForm.currentAddress,
|
||||
mobile: addForm.mobile,
|
||||
email: addForm.email,
|
||||
educationCode: addForm.education,
|
||||
occupation: addForm.occupation,
|
||||
...(isDeceased.value
|
||||
? {
|
||||
deathDate: addForm.deathDate,
|
||||
deathLunar: addForm.deathLunar,
|
||||
deathAge: addForm.deathAge,
|
||||
deathPlace: addForm.deathPlace,
|
||||
deathExpressionCode: addForm.deathType,
|
||||
burialDate: addForm.burialDate,
|
||||
burialPlace: addForm.burialPlace,
|
||||
}
|
||||
: {}),
|
||||
personStatus: addForm.personStatus,
|
||||
biography: addForm.biography,
|
||||
remark: addForm.remark,
|
||||
@@ -685,6 +918,9 @@ const submitAdd = async () => {
|
||||
...(relationType.value === memberRelationTypes.SPOUSE
|
||||
? { relationName: relationLabel.value }
|
||||
: {}),
|
||||
...(addForm.relationVariantCode
|
||||
? { relationVariantCode: addForm.relationVariantCode }
|
||||
: {}),
|
||||
}),
|
||||
};
|
||||
const submittedRelationType = isFirstMember.value
|
||||
@@ -708,12 +944,13 @@ const submitAdd = async () => {
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
let createdPerson;
|
||||
if (isFirstMember.value) {
|
||||
await lineageApi.createPerson(genealogyId.value, payload, {
|
||||
createdPerson = await lineageApi.createPerson(genealogyId.value, payload, {
|
||||
requestController: memberCreationRequestController,
|
||||
});
|
||||
} else {
|
||||
await lineageApi.createRelatedPerson(
|
||||
createdPerson = await lineageApi.createRelatedPerson(
|
||||
genealogyId.value,
|
||||
personId.value,
|
||||
submittedRelationType,
|
||||
@@ -723,6 +960,19 @@ const submitAdd = async () => {
|
||||
}
|
||||
if (!pageActive) return;
|
||||
memberCreationCommitted.value = true;
|
||||
const sensitiveHistory = addForm.hereditaryMedicalHistory.trim();
|
||||
if (sensitiveHistory) {
|
||||
const createdPersonId = String(createdPerson?.personId || "");
|
||||
if (!/^[1-9]\d*$/.test(createdPersonId)) {
|
||||
throw new Error("成员已经保存,但响应缺少人物标识,敏感健康资料尚未保存。请返回成员档案补充。");
|
||||
}
|
||||
await lineageApi.saveSensitiveProfile(
|
||||
genealogyId.value,
|
||||
createdPersonId,
|
||||
sensitiveHistory,
|
||||
{ requestController: sensitiveProfileRequestController },
|
||||
);
|
||||
}
|
||||
await returnTo("T01", { genealogyId: genealogyId.value });
|
||||
} catch (error) {
|
||||
if (!pageActive) return;
|
||||
@@ -774,7 +1024,7 @@ const returnToTree = async () => {
|
||||
@include adaptive-tree-panel;
|
||||
z-index: 2;
|
||||
width: calc(100% - 32rpx);
|
||||
margin: 18rpx auto 28rpx;
|
||||
margin: 18rpx auto calc(28rpx + env(safe-area-inset-bottom));
|
||||
padding: 7.5% 8%;
|
||||
}
|
||||
.form-eyebrow {
|
||||
@@ -837,9 +1087,12 @@ const returnToTree = async () => {
|
||||
}
|
||||
.form-field textarea {
|
||||
width: auto;
|
||||
min-height: 54rpx;
|
||||
min-height: var(--app-touch-min);
|
||||
text-align: left;
|
||||
}
|
||||
.form-field input {
|
||||
min-height: var(--app-touch-min);
|
||||
}
|
||||
.form-field__hint,
|
||||
.upload-receipt {
|
||||
color: $ink-muted;
|
||||
@@ -853,7 +1106,7 @@ const returnToTree = async () => {
|
||||
gap: 6rpx;
|
||||
}
|
||||
.upload-button {
|
||||
min-height: 54rpx;
|
||||
min-height: var(--app-touch-min);
|
||||
margin: 0;
|
||||
padding: 0 16rpx;
|
||||
border: 1rpx solid rgba(159, 23, 15, 0.42);
|
||||
@@ -882,7 +1135,7 @@ const returnToTree = async () => {
|
||||
.form-action {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 76rpx;
|
||||
min-height: var(--app-touch-min);
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.form-action image,
|
||||
|
||||
Reference in New Issue
Block a user