Files
jiapuapp/pages/genealogy/g03-create-genealogy.vue
T
2026-07-16 07:58:53 +08:00

588 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号G-03用途创建家谱与录入首代人物的同路由两步流程 -->
<template>
<view class="flow-page">
<!-- 纸纹页头和山水均由完整位图承载流程内容只叠加文字和控件 -->
<image
class="page-paper-texture"
src="/static/assets/foundation/opaque/page-paper.jpg"
mode="scaleToFill"
/>
<image
class="page-footer-landscape"
src="/static/assets/foundation/transparent/footer-mountain-bamboo.png"
mode="scaleToFill"
/>
<view class="flow-header">
<image
class="flow-header__skin"
src="/static/assets/foundation/opaque/root-header-cinnabar.jpg"
mode="scaleToFill"
/>
<view class="flow-header__content">
<view class="flow-header__back" @click="goBack">
<image
class="flow-header__back-icon"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
<text>返回</text>
</view>
<text class="flow-header__title">{{ isAncestorStep ? "录入首代人物" : "创建家谱" }}</text>
<view class="flow-header__side" />
</view>
</view>
<view class="flow-content">
<view class="create-flow-panel">
<image
class="create-flow-panel__skin"
src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png"
mode="scaleToFill"
/>
<view v-if="!isAncestorStep" class="create-flow-panel__content">
<view class="flow-step-label"><text>第一步 · 立谱信息</text></view>
<text class="flow-heading">为家族立一部可传承的谱</text>
<text class="flow-note">家谱建立后可继续完善名称与访问规则由创建者维护</text>
<view class="flow-fields">
<view class="field-row">
<text>姓氏</text>
<input v-model="createForm.surname" maxlength="4" placeholder="如:汤" placeholder-class="placeholder" @input="clearFieldError('surname')" />
</view>
<text v-if="fieldErrors.surname" class="field-error">{{ fieldErrors.surname }}</text>
<view class="field-row">
<text>谱名</text>
<input v-model="createForm.name" maxlength="24" placeholder="如:汤氏家谱" placeholder-class="placeholder" @input="clearFieldError('name')" />
</view>
<text v-if="fieldErrors.name" class="field-error">{{ fieldErrors.name }}</text>
<view class="field-row">
<text>堂号</text>
<input v-model="createForm.hall" maxlength="12" placeholder="选填" placeholder-class="placeholder" />
</view>
<view class="field-row">
<text>所在地</text>
<input v-model="createForm.location" maxlength="30" placeholder="请选择或输入" placeholder-class="placeholder" @input="clearFieldError('location')" />
</view>
<text v-if="fieldErrors.location" class="field-error">{{ fieldErrors.location }}</text>
</view>
<view class="flow-rule">
<text class="flow-rule__label">访问规则</text>
<view class="flow-rule__options">
<view class="flow-rule__option" :class="{ 'flow-rule__option--active': createForm.visibility === 'MEMBER_ONLY' }" @click="createForm.visibility = 'MEMBER_ONLY'">仅成员可见</view>
<view class="flow-rule__option" :class="{ 'flow-rule__option--active': createForm.visibility === 'SEARCHABLE' }" @click="createForm.visibility = 'SEARCHABLE'">可搜索申请</view>
</view>
</view>
<text class="flow-rule__note">保护族人资料创建后可在家谱设置中开放搜索与申请加入</text>
<text v-if="createState === 'error'" class="flow-error">创建样式模拟失败请检查信息后重试</text>
<view class="flow-primary-action" hover-class="action-hover" @click="submitCreate">
<image
class="flow-primary-action__skin"
src="/static/assets/foundation/opaque/a01-primary-button.png"
mode="scaleToFill"
/>
<text class="flow-primary-action__copy">{{ createState === 'submitting' ? '正在创建…' : '创建并录入首代' }}</text>
</view>
</view>
<view v-else class="create-flow-panel__content">
<view class="flow-step-label"><text>第二步 · 首代人物</text></view>
<text class="flow-heading">家谱从这一位开始</text>
<text class="flow-note">可先录入姓名世代和简要生平其他资料后续随时补充</text>
<view class="flow-fields">
<view class="field-row">
<text>姓名</text>
<input v-model="ancestorForm.personName" maxlength="20" placeholder="请输入首代姓名" placeholder-class="placeholder" @input="clearFieldError('personName')" />
</view>
<text v-if="fieldErrors.personName" class="field-error">{{ fieldErrors.personName }}</text>
<view class="field-row">
<text>世代</text>
<text class="fixed-value">第一世</text>
</view>
<view class="field-row">
<text>出生日期</text>
<input v-model="ancestorForm.birthDate" placeholder="如:1940年" placeholder-class="placeholder" />
</view>
</view>
<text v-if="ancestorState === 'error'" class="flow-error">首代人物保存失败请稍后重试</text>
<view class="intro-field">
<text>生平简述</text>
<textarea v-model="ancestorForm.introduction" maxlength="200" placeholder="可选,记录家训、迁徙或重要经历" placeholder-class="placeholder" />
</view>
<view class="flow-primary-action" hover-class="action-hover" @click="submitAncestor">
<image
class="flow-primary-action__skin"
src="/static/assets/foundation/opaque/a01-primary-button.png"
mode="scaleToFill"
/>
<text class="flow-primary-action__copy">{{ ancestorState === 'submitting' ? '正在保存…' : '保存并进入家谱' }}</text>
</view>
</view>
</view>
</view>
<view v-if="duplicateReminderVisible" class="duplicate-reminder-layer" @click="closeDuplicateReminder">
<view class="duplicate-reminder" @click.stop>
<image class="duplicate-reminder__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="duplicate-reminder__content">
<text class="duplicate-reminder__title">先确认是否已有家谱</text>
<text class="duplicate-reminder__copy">同一家族可能已经建谱建议先搜索谱名地区和堂号避免重复创建</text>
<view class="duplicate-reminder__search" @click="searchExistingGenealogy">
<image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" />
<text>先搜索已有家谱</text>
</view>
<view class="duplicate-reminder__confirm" @click="confirmCreate">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>确认没有继续创建</text>
</view>
<view class="duplicate-reminder__cancel" @click="closeDuplicateReminder">返回修改</view>
</view>
</view>
</view>
<view v-if="ancestorState === 'success'" class="flow-success-layer">
<view class="flow-success-dialog">
<image class="flow-success-dialog__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="flow-success-dialog__content">
<text class="flow-success-dialog__title">家谱创建完成</text>
<text class="flow-success-dialog__copy">首代人物已保存接下来进入家谱总览继续完善资料</text>
<view class="flow-success-dialog__action" @click="enterOverview">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>进入家谱总览</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
const currentStep = ref("create");
const genealogyId = ref("");
const isSubmitting = ref(false);
const createState = ref("form");
const ancestorState = ref("form");
const duplicateReminderVisible = ref(false);
const fieldErrors = reactive({ surname: "", name: "", location: "", personName: "" });
let submitTimer = null;
const createForm = reactive({
surname: "",
name: "",
hall: "",
location: "",
visibility: "MEMBER_ONLY",
});
const ancestorForm = reactive({
personName: "",
generationNo: 1,
generationName: "一世",
sex: "0",
birthDate: "",
introduction: "",
});
const isAncestorStep = computed(() => currentStep.value === "ancestor");
const getFlowQuery = () => {
if (typeof location !== "undefined") {
return new URLSearchParams(location.hash.split("?")[1] || "");
}
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return currentPage?.options || {};
};
const syncFlowFromRoute = () => {
const query = getFlowQuery();
const isAncestorRoute = query.get
? query.get("step") === "ancestor"
: query.step === "ancestor";
const routeGenealogyId = query.get ? query.get("genealogyId") : query.genealogyId;
currentStep.value = isAncestorRoute ? "ancestor" : "create";
genealogyId.value = routeGenealogyId || (isAncestorStep.value ? "local-created-genealogy" : "");
};
onLoad(() => {
syncFlowFromRoute();
});
onShow(() => {
syncFlowFromRoute();
});
onMounted(() => {
if (typeof window !== "undefined") {
window.addEventListener("hashchange", syncFlowFromRoute);
}
});
onUnmounted(() => {
if (submitTimer) clearTimeout(submitTimer);
if (typeof window !== "undefined") {
window.removeEventListener("hashchange", syncFlowFromRoute);
}
});
const goBack = () => {
if (isAncestorStep.value) {
uni.redirectTo({ url: "/pages/genealogy/g03-create-genealogy" });
return;
}
uni.navigateBack();
};
const clearFieldError = (field) => { fieldErrors[field] = ""; };
const validateCreateForm = () => {
fieldErrors.surname = createForm.surname.trim() ? "" : "请填写姓氏";
fieldErrors.name = createForm.name.trim() ? "" : "请填写谱名";
fieldErrors.location = createForm.location.trim() ? "" : "请填写所在地";
return !fieldErrors.surname && !fieldErrors.name && !fieldErrors.location;
};
const submitCreate = () => {
if (isSubmitting.value) return;
createState.value = "form";
if (!validateCreateForm()) return;
duplicateReminderVisible.value = true;
};
const closeDuplicateReminder = () => { duplicateReminderVisible.value = false; };
const searchExistingGenealogy = () => uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
const confirmCreate = () => {
closeDuplicateReminder();
isSubmitting.value = true;
createState.value = "submitting";
submitTimer = setTimeout(() => {
isSubmitting.value = false;
if (createForm.name.trim() === "失败") {
createState.value = "error";
return;
}
const createdId = "local-created-genealogy";
uni.redirectTo({ url: `/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=${createdId}` });
}, 320);
};
const submitAncestor = () => {
if (isSubmitting.value) return;
if (!genealogyId.value) {
ancestorState.value = "error";
return;
}
if (!ancestorForm.personName.trim()) {
fieldErrors.personName = "请填写首代姓名";
return;
}
isSubmitting.value = true;
ancestorState.value = "submitting";
submitTimer = setTimeout(() => {
isSubmitting.value = false;
ancestorState.value = ancestorForm.personName.trim() === "失败" ? "error" : "success";
}, 320);
};
const enterOverview = () => uni.redirectTo({ url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${genealogyId.value}` });
</script>
<style scoped lang="scss">
.flow-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
background: #f9f6ef;
}
.page-paper-texture {
position: absolute;
top: 112rpx;
right: 0;
bottom: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
opacity: 0.74;
pointer-events: none;
}
.page-footer-landscape {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
height: 430rpx;
opacity: 0.42;
pointer-events: none;
}
.flow-header {
position: relative;
z-index: 3;
height: 112rpx;
overflow: hidden;
}
.flow-header__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.flow-header__content {
position: relative;
z-index: 1;
display: flex;
height: 100%;
align-items: center;
justify-content: space-between;
padding: 0 26rpx;
box-sizing: border-box;
}
.flow-header__back,
.flow-header__side {
display: flex;
width: 130rpx;
align-items: center;
}
.flow-header__back {
color: #fff3d9;
font-family: "STKaiti", "KaiTi", serif;
font-size: 27rpx;
}
.flow-header__back-icon {
width: 30rpx;
height: 30rpx;
margin-right: 4rpx;
transform: rotate(180deg);
}
.flow-header__title {
flex: 1;
color: #ffe4a7;
font-family: "STKaiti", "KaiTi", serif;
font-size: 39rpx;
font-weight: 700;
letter-spacing: 4rpx;
text-align: center;
}
.flow-content {
position: relative;
z-index: 2;
padding: 24rpx 32rpx 48rpx;
}
.create-flow-panel {
position: relative;
min-height: 1000rpx;
}
.create-flow-panel__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.create-flow-panel__content {
position: relative;
z-index: 1;
display: flex;
min-height: 1000rpx;
flex-direction: column;
box-sizing: border-box;
padding: 48rpx 50rpx 42rpx;
}
.flow-step-label {
color: #a33b2b;
font-family: "STKaiti", "KaiTi", serif;
font-size: 25rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.flow-heading {
display: block;
margin-top: 13rpx;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 39rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.flow-note,
.flow-rule__note {
display: block;
color: #786654;
font-size: 23rpx;
line-height: 1.65;
}
.flow-note {
min-height: 56rpx;
margin-top: 12rpx;
}
.flow-fields {
margin-top: 10rpx;
}
.field-row {
display: flex;
min-height: 86rpx;
align-items: center;
border-bottom: 1rpx solid rgba(181, 138, 75, 0.36);
}
.field-row > text {
width: 126rpx;
flex: 0 0 auto;
color: #786654;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
}
.field-row input {
min-width: 0;
flex: 1;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 28rpx;
}
.placeholder {
color: #b7aa97;
}
.field-error { display: block; margin-top: 3rpx; color: $brand-red; font-size: 20rpx; text-align: right; }
.fixed-value {
color: #a33b2b !important;
}
.flow-rule {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
}
.flow-rule__label {
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
font-weight: 700;
}
.flow-rule__options { display: flex; gap: 12rpx; }
.flow-rule__option { display: flex; min-height: 54rpx; align-items: center; padding: 0 10rpx; color: #786654; font-size: 21rpx; }
.flow-rule__option--active { color: $brand-red; font-weight: 700; }
.flow-rule__note {
margin-top: 8rpx;
}
.flow-error { display: block; margin-top: 10rpx; color: $brand-red; font-size: 21rpx; text-align: center; }
.intro-field {
margin-top: 18rpx;
}
.intro-field > text {
display: block;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
font-weight: 700;
}
.intro-field textarea {
width: 100%;
height: 142rpx;
margin-top: 8rpx;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 26rpx;
line-height: 1.6;
}
.flow-primary-action {
position: relative;
display: flex;
width: 100%;
height: 96rpx;
align-items: center;
justify-content: center;
margin-top: auto;
overflow: hidden;
}
.flow-primary-action__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.flow-primary-action__copy {
position: relative;
z-index: 1;
color: #fff9ec;
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.action-hover {
opacity: 0.82;
}
.duplicate-reminder-layer,
.flow-success-layer { position: fixed; z-index: 30; inset: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 42rpx; background: rgba(34, 20, 12, .6); }
.duplicate-reminder,
.flow-success-dialog { position: relative; width: 100%; max-width: 670rpx; min-height: 650rpx; }
.duplicate-reminder__skin,
.flow-success-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.duplicate-reminder__content,
.flow-success-dialog__content { position: relative; z-index: 1; display: flex; min-height: 650rpx; flex-direction: column; align-items: center; box-sizing: border-box; padding: 92rpx 64rpx 56rpx; text-align: center; }
.duplicate-reminder__title,
.flow-success-dialog__title { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 39rpx; font-weight: 700; }
.duplicate-reminder__copy,
.flow-success-dialog__copy { margin-top: 24rpx; color: $ink-muted; font-size: 24rpx; line-height: 40rpx; }
.duplicate-reminder__search,
.duplicate-reminder__confirm,
.flow-success-dialog__action { position: relative; display: flex; width: 100%; height: 92rpx; align-items: center; justify-content: center; overflow: hidden; }
.duplicate-reminder__search { margin-top: 38rpx; }
.duplicate-reminder__confirm { margin-top: 14rpx; }
.duplicate-reminder__search image,
.duplicate-reminder__confirm image,
.flow-success-dialog__action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.duplicate-reminder__search text,
.duplicate-reminder__confirm text,
.flow-success-dialog__action text { position: relative; z-index: 1; color: #fff9ec; font-size: 25rpx; font-weight: 700; }
.duplicate-reminder__search text { color: #7b4e24; }
.duplicate-reminder__cancel { display: flex; min-height: 68rpx; align-items: center; margin-top: auto; color: $ink-muted; font-size: 23rpx; }
.flow-success-dialog__action { margin-top: 44rpx; }
</style>