完成40%
This commit is contained in:
@@ -1,69 +1,504 @@
|
||||
<!-- 页面编号:G-03;用途:创建家谱表单。 -->
|
||||
<!-- 页面编号:G-03;用途:创建家谱与录入首代人物的同路由两步流程。 -->
|
||||
<template>
|
||||
<view class="page-shell create-page">
|
||||
<PageHeader title="创建家谱" />
|
||||
<view class="form-paper">
|
||||
<view class="section-title">立谱信息</view>
|
||||
<text class="form-note">家谱建立后可继续完善,名称与访问规则由创建者维护。</text>
|
||||
<view class="field-row">
|
||||
<text>姓氏</text>
|
||||
<input v-model="form.surname" maxlength="4" placeholder="如:汤" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>谱名</text>
|
||||
<input v-model="form.name" maxlength="24" placeholder="如:四川武胜汤氏族谱" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>堂号</text>
|
||||
<input v-model="form.hall" maxlength="12" placeholder="选填" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>所在地</text>
|
||||
<input v-model="form.location" maxlength="30" placeholder="请选择或输入" placeholder-class="placeholder" />
|
||||
<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="privacy-paper">
|
||||
<view class="privacy-heading">
|
||||
<text class="section-title">访问规则</text>
|
||||
<text class="privacy-value">默认仅成员可见</text>
|
||||
</view>
|
||||
<text class="form-note">保护族人资料。创建者可在家谱设置中开放搜索与申请加入。</text>
|
||||
</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 class="bottom-action"><button class="primary-button" @click="submit">创建并录入首代</button></view>
|
||||
<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" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<text>谱名</text>
|
||||
<input v-model="createForm.name" maxlength="24" placeholder="如:汤氏家谱" placeholder-class="placeholder" />
|
||||
</view>
|
||||
<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" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flow-rule">
|
||||
<text class="flow-rule__label">访问规则</text>
|
||||
<text class="flow-rule__value">默认仅成员可见</text>
|
||||
</view>
|
||||
<text class="flow-rule__note">保护族人资料;创建后可在家谱设置中开放搜索与申请加入。</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">创建并录入首代</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" />
|
||||
</view>
|
||||
<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>
|
||||
|
||||
<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">保存并查看世系树</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import { appApi } from '@/utils/api.js'
|
||||
import { genealogyContext } from '@/utils/genealogy-context.js'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { appApi } from "@/utils/api.js";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
|
||||
const form = reactive({ surname: '', name: '', hall: '', location: '', visibility: 'MEMBER_ONLY' })
|
||||
const currentStep = ref("create");
|
||||
const genealogyId = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
const createForm = reactive({
|
||||
surname: "",
|
||||
name: "",
|
||||
hall: "",
|
||||
location: "",
|
||||
visibility: "MEMBER_ONLY",
|
||||
});
|
||||
const ancestorForm = reactive({
|
||||
personName: "",
|
||||
generationNo: 1,
|
||||
generationName: "一世",
|
||||
sex: "0",
|
||||
birthDate: "",
|
||||
introduction: "",
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.surname.trim() || !form.name.trim()) {
|
||||
uni.showToast({ title: '请填写姓氏和谱名', icon: 'none' })
|
||||
return
|
||||
const isAncestorStep = computed(() => currentStep.value === "ancestor");
|
||||
|
||||
const getFlowQuery = () => {
|
||||
if (typeof location !== "undefined") {
|
||||
return new URLSearchParams(location.hash.split("?")[1] || "");
|
||||
}
|
||||
const created = await appApi.createGenealogy({ ...form })
|
||||
genealogyContext.setCurrentGenealogyId(created.id)
|
||||
uni.redirectTo({ url: `/pages/genealogy/g04-first-ancestor?genealogyId=${created.id}` })
|
||||
}
|
||||
|
||||
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 ? genealogyContext.getCurrentGenealogyId() : "");
|
||||
|
||||
if (isAncestorStep.value && genealogyId.value) {
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
syncFlowFromRoute();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
syncFlowFromRoute();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("hashchange", syncFlowFromRoute);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
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 submitCreate = async () => {
|
||||
if (!createForm.surname.trim() || !createForm.name.trim()) {
|
||||
uni.showToast({ title: "请填写姓氏和谱名", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await appApi.createGenealogy({ ...createForm });
|
||||
genealogyContext.setCurrentGenealogyId(created.id);
|
||||
uni.redirectTo({
|
||||
url: `/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=${created.id}`,
|
||||
});
|
||||
};
|
||||
|
||||
const submitAncestor = async () => {
|
||||
if (isSubmitting.value) return;
|
||||
if (!genealogyId.value) {
|
||||
uni.showToast({ title: "未找到当前家谱", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!ancestorForm.personName.trim()) {
|
||||
uni.showToast({ title: "请填写首代姓名", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const person = await appApi.createPerson(genealogyId.value, {
|
||||
...ancestorForm,
|
||||
personName: ancestorForm.personName.trim(),
|
||||
});
|
||||
uni.redirectTo({
|
||||
url: `/pages/tree/t01-tree-overview?genealogyId=${genealogyId.value}&selectedId=${person.id}`,
|
||||
});
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || "保存失败,请稍后重试", icon: "none" });
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.create-page { padding-bottom: 136rpx; }
|
||||
.form-paper, .privacy-paper { margin: 28rpx; padding: 34rpx 30rpx; border: 1rpx solid $gold-soft; border-radius: 22rpx; background: $paper-white; }
|
||||
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.65; }
|
||||
.field-row { display: flex; align-items: center; min-height: 104rpx; border-bottom: 1rpx solid #eadfc9; }
|
||||
.field-row:last-child { border-bottom: 0; }
|
||||
.field-row text { width: 130rpx; color: $ink-muted; font-size: 28rpx; }
|
||||
.field-row input { flex: 1; color: $ink; font-size: 29rpx; }
|
||||
.placeholder { color: #b7aa97; }
|
||||
.privacy-heading { display: flex; align-items: center; justify-content: space-between; }
|
||||
.privacy-value { color: $brand-red; font-size: 24rpx; }
|
||||
.bottom-action { position: fixed; right: 0; bottom: 0; left: 0; padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); background: #fffaf0; border-top: 1rpx solid $gold-soft; }
|
||||
.flow-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: 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: 876rpx;
|
||||
}
|
||||
|
||||
.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: 876rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
.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__value {
|
||||
color: #a33b2b;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
.flow-rule__note {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user