待测试

This commit is contained in:
rain
2026-07-27 17:17:31 +08:00
parent 1eae3bbef4
commit 04287e6777
35 changed files with 4238 additions and 22750 deletions
+184 -14
View File
@@ -27,6 +27,34 @@
</view>
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
<view class="form-field">
<text>人物编号</text>
<input v-model="editForm.personNo" maxlength="40" placeholder="不填则由服务端生成" placeholder-class="form-placeholder" />
</view>
<picker :range="sexOptions.map((item) => item.label)" :value="optionIndex(sexOptions, editForm.sex)" @change="selectOption('sex', sexOptions, $event)">
<view class="form-field form-field--picker">
<text>性别</text><text>{{ optionLabel(sexOptions, editForm.sex) || "请选择" }}</text>
</view>
</picker>
<picker :range="bindingModeOptions.map((item) => item.label)" :value="optionIndex(bindingModeOptions, editForm.bindingMode)" @change="selectBindingMode">
<view class="form-field form-field--picker">
<text>身份认领</text><text>{{ optionLabel(bindingModeOptions, editForm.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>
<view class="form-field">
<text>别名</text>
<input v-model="editForm.aliasName" placeholder="别名或曾用名" placeholder-class="form-placeholder" />
@@ -35,13 +63,24 @@
<text>字辈</text>
<input v-model="editForm.generationName" maxlength="12" placeholder="例如:文字辈" placeholder-class="form-placeholder" />
</view>
<view class="form-field">
<text>世代</text>
<input v-model="editForm.generation" type="number" placeholder="正整数" placeholder-class="form-placeholder" />
</view>
<picker :range="personOptionLabels" :value="personOptionIndex(editForm.fatherId)" @change="selectPerson('fatherId', $event)">
<view class="form-field form-field--picker"><text>父亲</text><text>{{ personOptionLabel(editForm.fatherId) }}</text></view>
</picker>
<picker :range="personOptionLabels" :value="personOptionIndex(editForm.motherId)" @change="selectPerson('motherId', $event)">
<view class="form-field form-field--picker"><text>母亲</text><text>{{ personOptionLabel(editForm.motherId) }}</text></view>
</picker>
<picker mode="date" :value="editForm.birthDate" @change="selectDate('birthDate', $event)">
<view class="form-field form-field--picker"><text>出生日期</text><text>{{ editForm.birthDate || "未填写" }}</text></view>
</picker>
<view class="form-field">
<text>出生农历</text>
<input v-model="editForm.birthLunar" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
</view>
<picker :range="lunarOptions.map((item) => item.label)" :value="optionIndex(lunarOptions, editForm.birthLunar)" @change="selectOption('birthLunar', lunarOptions, $event)">
<view class="form-field form-field--picker">
<text>出生农历</text><text>{{ optionLabel(lunarOptions, editForm.birthLunar) || "请选择" }}</text>
</view>
</picker>
<view class="form-field">
<text>出生地</text>
<input v-model="editForm.birthPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
@@ -49,10 +88,11 @@
<picker mode="date" :value="editForm.deathDate" @change="selectDate('deathDate', $event)">
<view class="form-field form-field--picker"><text>离世日期</text><text>{{ editForm.deathDate || "在世或未填写" }}</text></view>
</picker>
<view class="form-field">
<text>逝世农历</text>
<input v-model="editForm.deathLunar" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
</view>
<picker :range="lunarOptions.map((item) => item.label)" :value="optionIndex(lunarOptions, editForm.deathLunar)" @change="selectOption('deathLunar', lunarOptions, $event)">
<view class="form-field form-field--picker">
<text>逝世农历</text><text>{{ optionLabel(lunarOptions, editForm.deathLunar) || "请选择" }}</text>
</view>
</picker>
<view class="form-field">
<text>逝世地</text>
<input v-model="editForm.deathPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
@@ -61,6 +101,19 @@
<text>安葬地</text>
<input v-model="editForm.burialPlace" placeholder="按家谱记载填写" placeholder-class="form-placeholder" />
</view>
<picker :range="personStatusOptions.map((item) => item.label)" :value="optionIndex(personStatusOptions, editForm.personStatus)" @change="selectOption('personStatus', personStatusOptions, $event)">
<view class="form-field form-field--picker">
<text>人物状态</text><text>{{ optionLabel(personStatusOptions, editForm.personStatus) || "请选择" }}</text>
</view>
</picker>
<view class="form-field">
<text>排序值</text>
<input v-model="editForm.sortOrder" type="number" placeholder="可选整数,值越小越靠前" placeholder-class="form-placeholder" />
</view>
<view class="form-field">
<text>关系名称</text>
<input v-model="editForm.relationName" maxlength="30" placeholder="可选关系显示名称" placeholder-class="form-placeholder" />
</view>
<view class="form-field form-field--summary">
<text>人物简介</text>
<textarea v-model="editForm.summary" auto-height maxlength="500" placeholder="记录生平、迁徙或重要经历" placeholder-class="form-placeholder" />
@@ -112,6 +165,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 {
goBack,
@@ -123,18 +177,30 @@ const editState = ref("loading");
const genealogyId = ref("");
const personId = ref("");
const isSubmitting = ref(false);
const isAvatarUploading = ref(false);
const avatarFileName = ref("");
const avatarUploadError = ref("");
const discardDialogVisible = ref(false);
const errorMessage = ref("");
const failedAction = ref("load");
const editRequestController = createRequestController();
const personOptionsRequestController = createRequestController();
let loadSequence = 0;
const originalMember = ref(null);
const baseline = ref("");
const editForm = reactive({
appUserId: "",
bindingMode: "NONE",
personNo: "",
name: "",
aliasName: "",
sex: "",
generation: "",
generationName: "",
fatherId: "",
motherId: "",
avatarOssId: "",
birthDate: "",
birthLunar: "",
birthPlace: "",
@@ -142,10 +208,39 @@ const editForm = reactive({
deathLunar: "",
deathPlace: "",
burialPlace: "",
personStatus: "",
summary: "",
remark: "",
sortOrder: "",
relationName: "",
});
const fieldErrors = reactive({ name: "", dates: "", bindingMode: "" });
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 appUserBindingHint = computed(() => {
if (editForm.bindingMode === "NONE") return "不绑定账号,保存时不提交业务用户 ID";
if (editForm.bindingMode === "SELF") return "由服务端从 Token 绑定当前登录账号,保存时不提交业务用户 ID";
return editForm.appUserId ? "保留当前已认领的指定业务用户" : "需先选择指定业务用户;当前没有可用候选接口";
});
const fieldErrors = reactive({ name: "", dates: "" });
const formSnapshot = computed(() => JSON.stringify(editForm));
const hasValidContext = computed(() =>
@@ -170,6 +265,7 @@ const discardConfirmation = createDiscardConfirmation((visible) => {
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const datePart = (value) => String(value || "").slice(0, 10);
const loadMember = async () => {
const activeLoad = ++loadSequence;
@@ -181,18 +277,29 @@ const loadMember = async () => {
if (activeLoad !== loadSequence) return;
originalMember.value = member;
Object.assign(editForm, {
appUserId: member.appUserId || "",
bindingMode: member.bindingMode || (member.appUserId ? "SPECIFIED" : "NONE"),
personNo: member.personNo || "",
name: member.name,
aliasName: member.aliasName || "",
sex: member.sex || "",
generation: member.generation || "",
generationName: member.generationName || "",
birthDate: member.birthDate || "",
fatherId: member.relatives.find((item) => item.relation === "父亲")?.id || "",
motherId: member.relatives.find((item) => item.relation === "母亲")?.id || "",
avatarOssId: member.avatarOssId || "",
birthDate: datePart(member.birthDate),
birthLunar: member.birthLunar || "",
birthPlace: member.birthplace || "",
deathDate: member.deathDate || "",
deathDate: datePart(member.deathDate),
deathLunar: member.deathLunar || "",
deathPlace: member.deathPlace || "",
burialPlace: member.burialPlace || "",
personStatus: member.personStatus || "",
summary: member.biography || "",
remark: member.remark || "",
sortOrder: member.sortOrder ?? "",
relationName: member.relationName || "",
});
baseline.value = formSnapshot.value;
errorMessage.value = "";
@@ -206,6 +313,22 @@ const loadMember = async () => {
editState.value = "error";
}
};
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) || value === personId.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 || "");
@@ -216,10 +339,12 @@ onLoad((query) => {
return;
}
void loadMember();
void loadPersonOptions();
});
onUnload(() => {
loadSequence += 1;
editRequestController.abort();
personOptionsRequestController.abort();
discardConfirmation.dispose();
});
@@ -227,7 +352,7 @@ const requestBack = () =>
runBackGuard({
transientOpen: discardDialogVisible.value,
dirty: isDirty.value,
submitting: isSubmitting.value,
submitting: isSubmitting.value || isAvatarUploading.value,
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
@@ -236,6 +361,34 @@ const requestBack = () =>
onBackPress((event) => handleBackPress(event, requestBack));
const clearError = (field) => { fieldErrors[field] = ""; };
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) => {
editForm[field] = options[Number(event.detail.value)]?.value || "";
};
const selectBindingMode = (event) => {
editForm.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) => {
editForm[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: editRequestController });
editForm.avatarOssId = receipt.ossId;
avatarFileName.value = receipt.fileName || "头像图片";
} catch (error) {
if (!isImagePickCancelled(error) && !isRequestCancelled(error)) avatarUploadError.value = error?.message || "头像上传失败,请稍后重试";
} finally {
isAvatarUploading.value = false;
}
};
const selectDate = (field, event) => {
editForm[field] = event.detail.value || "";
fieldErrors.dates = "";
@@ -243,16 +396,27 @@ const selectDate = (field, event) => {
const validateEditForm = () => {
fieldErrors.name = editForm.name.trim() ? "" : "请填写成员姓名";
fieldErrors.dates = editForm.birthDate && editForm.deathDate && editForm.deathDate < editForm.birthDate ? "离世日期不能早于出生日期" : "";
return !fieldErrors.name && !fieldErrors.dates;
fieldErrors.bindingMode = editForm.bindingMode === "SPECIFIED" && !editForm.appUserId
? "当前未提供可选业务用户,不能指定认领"
: "";
return !fieldErrors.name && !fieldErrors.dates && !fieldErrors.bindingMode;
};
const saveMember = async () => {
if (isSubmitting.value || !validateEditForm()) return;
if (isSubmitting.value || isAvatarUploading.value || !validateEditForm()) return;
isSubmitting.value = true;
try {
await appApi.updatePerson(genealogyId.value, personId.value, {
bindingMode: editForm.bindingMode,
...(editForm.bindingMode === "SPECIFIED" ? { appUserId: editForm.appUserId } : {}),
personNo: editForm.personNo,
name: editForm.name,
aliasName: editForm.aliasName,
sex: editForm.sex,
generation: editForm.generation ? Number(editForm.generation) : undefined,
generationName: editForm.generationName,
fatherId: editForm.fatherId,
motherId: editForm.motherId,
avatarOssId: editForm.avatarOssId,
birthDate: editForm.birthDate,
birthLunar: editForm.birthLunar,
birthPlace: editForm.birthPlace,
@@ -260,8 +424,11 @@ const saveMember = async () => {
deathLunar: editForm.deathLunar,
deathPlace: editForm.deathPlace,
burialPlace: editForm.burialPlace,
personStatus: editForm.personStatus,
biography: editForm.summary,
remark: editForm.remark,
sortOrder: editForm.sortOrder,
relationName: editForm.relationName,
}, { requestController: editRequestController });
baseline.value = formSnapshot.value;
await goBack();
@@ -307,6 +474,9 @@ const handleResultAction = () => {
.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__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; }