feat: 完成前端业务闭环与后端联调

This commit is contained in:
2026-08-24 23:37:56 +08:00
parent c59e36f933
commit 9ad572907b
175 changed files with 10711 additions and 1740 deletions
+83 -20
View File
@@ -8,7 +8,7 @@
<text class="create-card__eyebrow">立谱信息</text>
<text class="create-card__title">为家族创建一部家谱</text>
<text class="create-card__note"
>创建成功后会回到我的家谱可在世系树中录入首位成员</text
>创建时会同步建立始迁祖人物创建成功后会回到我的家谱</text
>
<view class="field-row">
@@ -84,6 +84,37 @@
/>
</view>
<view class="field-row">
<text class="field-row__label"
><text class="required-mark">*</text>始迁祖</text
>
<input
v-model="form.firstAncestorName"
maxlength="30"
placeholder="请输入始迁祖姓名"
placeholder-class="placeholder"
@input="clearFieldError('firstAncestorName')"
/>
</view>
<text v-if="fieldErrors.firstAncestorName" class="field-error">{{
fieldErrors.firstAncestorName
}}</text>
<view class="owner-ancestor-field">
<view>
<text class="owner-ancestor-field__label">谱主本人就是始迁祖</text>
<text class="owner-ancestor-field__hint"
>仅本人确为始迁祖时开启开启后账号会绑定到该人物</text
>
</view>
<switch
:checked="form.ownerIsFirstAncestor"
color="#9f170f"
aria-label="谱主本人就是始迁祖"
@change="form.ownerIsFirstAncestor = $event.detail.value"
/>
</view>
<view class="field-row">
<text class="field-row__label">所在地</text>
<input
@@ -208,7 +239,6 @@ import {
import { genealogyApi } from "@/services/api/genealogy-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
import {
GENEALOGY_ACCESS_PRESET,
GENEALOGY_ACCESS_PRESET_OPTIONS,
@@ -229,6 +259,8 @@ const form = reactive({
surname: "",
genealogyName: "",
ancestralHall: "",
firstAncestorName: "",
ownerIsFirstAncestor: false,
originPlace: "",
addressDetail: "",
intro: "",
@@ -237,6 +269,7 @@ const form = reactive({
const fieldErrors = reactive({
surname: "",
genealogyName: "",
firstAncestorName: "",
regionCode: "",
});
const submitError = ref("");
@@ -248,7 +281,11 @@ const coverOssId = ref(null);
const coverFileName = ref("");
const coverUploadRequestController = createRequestController();
const genealogyCreateRequestController = createRequestController();
const genealogyCreateGuard = createNonIdempotentWriteGuard();
const createGenealogyRequestId = () =>
typeof globalThis.crypto?.randomUUID === "function"
? `app-genealogy-${globalThis.crypto.randomUUID()}`
: `app-genealogy-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const genealogyCreateRequestId = ref(createGenealogyRequestId());
const selectedRegion = ref(null);
const regionPickerTrail = ref([]);
const regionPickerDialog = ref(null);
@@ -262,6 +299,8 @@ const hasDraft = computed(
Object.entries(form).some(([key, value]) =>
key === "accessPreset"
? value !== GENEALOGY_ACCESS_PRESET.MEMBER_ONLY
: key === "ownerIsFirstAncestor"
? value === true
: String(value).trim(),
) ||
Boolean(selectedRegion.value?.regionCode) ||
@@ -282,12 +321,16 @@ const clearFieldError = (field) => {
const validate = () => {
fieldErrors.surname = form.surname.trim() ? "" : "请填写姓氏";
fieldErrors.genealogyName = form.genealogyName.trim() ? "" : "请填写谱名";
fieldErrors.firstAncestorName = form.firstAncestorName.trim()
? ""
: "请填写始迁祖姓名";
fieldErrors.regionCode = selectedRegion.value?.regionCode
? ""
: "请选择所在地区";
return (
!fieldErrors.surname &&
!fieldErrors.genealogyName &&
!fieldErrors.firstAncestorName &&
!fieldErrors.regionCode
);
};
@@ -354,7 +397,6 @@ const submitCreate = async () => {
if (!createdGenealogyId.value && !validate()) return;
let createPayload = null;
let createAttempt = null;
if (!createdGenealogyId.value) {
const access = toApiGenealogyAccess(form.accessPreset);
if (!access) {
@@ -366,18 +408,15 @@ const submitCreate = async () => {
genealogyName: form.genealogyName,
regionCode: selectedRegion.value.regionCode,
ancestralHall: form.ancestralHall,
firstAncestorName: form.firstAncestorName,
ownerIsFirstAncestor: form.ownerIsFirstAncestor,
requestId: genealogyCreateRequestId.value,
originPlace: form.originPlace,
addressDetail: form.addressDetail,
intro: form.intro,
coverOssId: coverOssId.value,
...access,
};
createAttempt = genealogyCreateGuard.begin(createPayload);
if (createAttempt === null) {
submitError.value =
"上次创建结果暂时无法确认,请先返回“我的家谱”检查,避免重复创建。";
return;
}
}
isSubmitting.value = true;
@@ -402,15 +441,6 @@ const submitCreate = async () => {
);
} catch (error) {
if (!pageActive) return;
if (
!createdGenealogyId.value &&
createAttempt &&
genealogyCreateGuard.recordFailure(createAttempt, error)
) {
submitError.value =
"创建结果暂时无法确认,请先返回“我的家谱”检查,避免重复创建。";
return;
}
if (isRequestCancelled(error)) return;
submitError.value = createdGenealogyId.value
? "家谱已经创建,但页面返回失败。请再次点击“返回我的家谱”,不要重复创建。"
@@ -439,7 +469,7 @@ onUnload(() => {
}
.page-content {
z-index: 1;
padding: 24rpx 32rpx 72rpx;
padding: 24rpx 32rpx calc(72rpx + env(safe-area-inset-bottom));
}
.create-card {
@include adaptive-genealogy-state-panel;
@@ -500,6 +530,7 @@ onUnload(() => {
}
.field-row input {
min-width: 0;
min-height: var(--app-touch-min);
flex: 1;
color: $ink;
font-size: clamp(16px, 28rpx, 20px);
@@ -552,6 +583,38 @@ onUnload(() => {
font-size: clamp(16px, 27rpx, 20px);
line-height: 1.55;
}
.owner-ancestor-field {
display: flex;
min-height: 112rpx;
align-items: center;
justify-content: space-between;
gap: 24rpx;
margin-top: 22rpx;
padding: 18rpx 20rpx;
border: 1rpx solid rgba(128, 89, 49, 0.34);
border-radius: 12rpx;
background: rgba(255, 252, 245, 0.7);
}
.owner-ancestor-field > view {
min-width: 0;
flex: 1;
}
.owner-ancestor-field__label,
.owner-ancestor-field__hint {
display: block;
}
.owner-ancestor-field__label {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 30rpx, 22px);
font-weight: 700;
}
.owner-ancestor-field__hint {
margin-top: 6rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.45;
}
.cover-field {
display: grid;
gap: 12rpx;