Files
jiapuapp/pages/genealogy/g03-create-genealogy.vue
T

660 lines
17 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">
<GenealogyPageBackground />
<PageHeader
:title="isAncestorStep ? '录入首代人物' : '创建家谱'"
custom-back
@back="goBack"
/>
<view class="flow-content">
<view class="create-flow-panel">
<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"
>
<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>
<picker
mode="selector"
:range="sexOptions"
range-key="label"
@change="changeAncestorSex"
>
<view class="field-row">
<text>性别</text>
<text class="fixed-value">{{ sexLabel }}</text>
</view>
</picker>
<view class="field-row">
<text>出生日期</text>
<picker
mode="date"
:value="ancestorForm.birthDate"
start="1800-01-01"
:end="new Date().toISOString().slice(0, 10)"
@change="changeAncestorBirthDate"
>
<text class="fixed-value">{{
ancestorForm.birthDate || "请选择日期"
}}</text>
</picker>
</view>
</view>
<text v-if="ancestorState === 'error'" class="flow-error"
>首代人物保存失败请稍后重试</text
>
<view class="intro-field">
<text>生平简述</text>
<textarea
v-model="ancestorForm.introduction"
auto-height
maxlength="200"
placeholder="可选,记录家训、迁徙或重要经历"
placeholder-class="placeholder"
/>
</view>
<view
class="flow-primary-action"
hover-class="action-hover"
@click="submitAncestor"
>
<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>
<view class="duplicate-reminder__content">
<text class="duplicate-reminder__title">先确认是否已有家谱</text>
<text class="duplicate-reminder__copy"
>同一家族可能已经建谱建议先搜索谱名地区和堂号避免重复创建</text
>
<view
class="duplicate-reminder__search"
@click="searchExistingGenealogy"
>
<text>先搜索已有家谱</text>
</view>
<view class="duplicate-reminder__confirm" @click="confirmCreate">
<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">
<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">
<text>进入家谱总览</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, onUnmounted, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
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 sexOptions = [
{ value: "0", label: "男" },
{ value: "1", label: "女" },
{ value: "2", label: "暂不填写" },
];
const sexLabel = computed(
() =>
sexOptions.find((option) => option.value === ancestorForm.sex)?.label ||
"请选择",
);
const changeAncestorSex = (event) => {
ancestorForm.sex = sexOptions[Number(event.detail.value)]?.value || "2";
};
const changeAncestorBirthDate = (event) => {
ancestorForm.birthDate = event.detail.value;
};
const isAncestorStep = computed(() => currentStep.value === "ancestor");
const syncFlowFromRoute = (query = {}) => {
const isAncestorRoute = query?.step === "ancestor";
const routeGenealogyId = query?.genealogyId || "";
currentStep.value = isAncestorRoute ? "ancestor" : "create";
genealogyId.value =
routeGenealogyId || (isAncestorStep.value ? "local-created-genealogy" : "");
};
onLoad((query) => {
syncFlowFromRoute(query);
});
onUnmounted(() => {
if (submitTimer) clearTimeout(submitTimer);
});
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">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.flow-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: #f9f6ef;
}
.flow-content {
z-index: 2;
padding: 24rpx 32rpx 48rpx;
}
.create-flow-panel {
@include adaptive.adaptive-genealogy-state-panel;
min-height: 1000rpx;
}
.create-flow-panel__content {
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: 25rpx;
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: 24rpx;
line-height: 34rpx;
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: 23rpx;
}
.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: 25rpx;
line-height: 36rpx;
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%;
min-height: 142rpx;
margin-top: 8rpx;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 26rpx;
line-height: 1.6;
}
.flow-primary-action {
display: flex;
width: 100%;
min-height: 96rpx;
align-items: center;
justify-content: center;
margin-top: auto;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
.flow-primary-action__copy {
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, 0.6);
}
.duplicate-reminder,
.flow-success-dialog {
width: 100%;
max-width: 670rpx;
min-height: 650rpx;
background: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png")
center / contain no-repeat;
}
.duplicate-reminder__content,
.flow-success-dialog__content {
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 {
display: flex;
width: 100%;
min-height: 92rpx;
align-items: center;
justify-content: center;
}
.duplicate-reminder__search {
margin-top: 38rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
center / contain no-repeat;
}
.duplicate-reminder__confirm {
margin-top: 14rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
.duplicate-reminder__search text,
.duplicate-reminder__confirm text,
.flow-success-dialog__action text {
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;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png")
center / contain no-repeat;
}
</style>