待测试
This commit is contained in:
+186
-16
@@ -37,6 +37,34 @@
|
||||
</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"
|
||||
@@ -58,16 +86,28 @@
|
||||
<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>
|
||||
<view class="form-field">
|
||||
<text>出生农历</text>
|
||||
<input v-model="addForm.birthLunar" maxlength="40" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
|
||||
</view>
|
||||
<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" />
|
||||
@@ -78,10 +118,11 @@
|
||||
<text>逝世日期</text><text>{{ addForm.deathDate || "请选择" }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="form-field">
|
||||
<text>逝世农历</text>
|
||||
<input v-model="addForm.deathLunar" maxlength="40" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
|
||||
</view>
|
||||
<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" />
|
||||
@@ -90,6 +131,20 @@
|
||||
<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>
|
||||
@@ -153,6 +208,7 @@ 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,
|
||||
@@ -166,10 +222,14 @@ 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({
|
||||
@@ -181,11 +241,39 @@ const relationIntents = Object.freeze({
|
||||
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: "",
|
||||
@@ -193,10 +281,13 @@ const addForm = reactive({
|
||||
deathLunar: "",
|
||||
deathPlace: "",
|
||||
burialPlace: "",
|
||||
personStatus: "",
|
||||
biography: "",
|
||||
remark: "",
|
||||
sortOrder: "",
|
||||
relationName: "",
|
||||
});
|
||||
const fieldErrors = reactive({ name: "", relation: "" });
|
||||
const fieldErrors = reactive({ name: "", relation: "", bindingMode: "" });
|
||||
|
||||
const isFirstMember = computed(() => mode.value === "first");
|
||||
const activeRelationIntent = computed(() => relationIntents[relationType.value] || null);
|
||||
@@ -205,6 +296,11 @@ const relationOptions = computed(() => {
|
||||
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) &&
|
||||
@@ -239,7 +335,7 @@ const resultCopy = computed(() => {
|
||||
};
|
||||
});
|
||||
const hasDraft = computed(() =>
|
||||
Object.values(addForm).some((value) => String(value).trim()),
|
||||
Object.entries(addForm).some(([field, value]) => field !== "bindingMode" && String(value).trim()),
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardDialogVisible.value = visible;
|
||||
@@ -265,7 +361,22 @@ const loadCurrentMember = async () => {
|
||||
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 || "");
|
||||
@@ -284,6 +395,7 @@ onLoad((query) => {
|
||||
if (activeRelationIntent.value) {
|
||||
addForm.relation = activeRelationIntent.value.label;
|
||||
}
|
||||
void loadPersonOptions();
|
||||
if (isFirstMember.value) {
|
||||
addState.value = "form";
|
||||
return;
|
||||
@@ -293,6 +405,7 @@ onLoad((query) => {
|
||||
onUnload(() => {
|
||||
loadSequence += 1;
|
||||
addRequestController.abort();
|
||||
personOptionsRequestController.abort();
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
@@ -300,7 +413,7 @@ const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardDialogVisible.value,
|
||||
dirty: addState.value === "form" && hasDraft.value,
|
||||
submitting: isSubmitting.value,
|
||||
submitting: isSubmitting.value || isAvatarUploading.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
@@ -313,21 +426,58 @@ 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 ? "" : "请选择与当前成员的关系";
|
||||
return !fieldErrors.name && !fieldErrors.relation;
|
||||
fieldErrors.bindingMode = addForm.bindingMode === "SPECIFIED"
|
||||
? "当前未提供可选业务用户,不能指定认领"
|
||||
: "";
|
||||
return !fieldErrors.name && !fieldErrors.relation && !fieldErrors.bindingMode;
|
||||
};
|
||||
const submitAdd = async () => {
|
||||
if (isSubmitting.value || !validateAddForm()) return;
|
||||
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,
|
||||
@@ -335,10 +485,16 @@ const submitAdd = async () => {
|
||||
deathLunar: addForm.deathLunar,
|
||||
deathPlace: addForm.deathPlace,
|
||||
burialPlace: addForm.burialPlace,
|
||||
personStatus: addForm.personStatus,
|
||||
biography: addForm.biography,
|
||||
remark: addForm.remark,
|
||||
...(isFirstMember.value ? { generation: 1 } : {}),
|
||||
...(isFirstMember.value ? {} : { relationName: relationLabel.value }),
|
||||
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 });
|
||||
@@ -349,10 +505,17 @@ const submitAdd = async () => {
|
||||
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: "",
|
||||
@@ -360,9 +523,13 @@ const submitAdd = async () => {
|
||||
deathLunar: "",
|
||||
deathPlace: "",
|
||||
burialPlace: "",
|
||||
personStatus: "",
|
||||
biography: "",
|
||||
remark: "",
|
||||
sortOrder: "",
|
||||
relationName: "",
|
||||
});
|
||||
avatarFileName.value = "";
|
||||
await returnTo("T01", { genealogyId: genealogyId.value });
|
||||
} catch (error) {
|
||||
if (isRequestCancelled(error)) return;
|
||||
@@ -396,6 +563,9 @@ const returnToTree = async () => {
|
||||
.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, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; }
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user