588 lines
18 KiB
Vue
588 lines
18 KiB
Vue
<!-- 页面编号: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>
|