修改完成55%

This commit is contained in:
2026-07-15 08:31:21 +08:00
parent 8c98936da5
commit 241a7af54c
98 changed files with 3269 additions and 782 deletions
+142 -48
View File
@@ -1,83 +1,177 @@
<!-- 公共组件全量页面视觉母版根据页面目录展示认证表单列表详情时间轴设置六类布局 -->
<!-- 公共组件F/R/N/M 剩余页面的高规格多任务母版内容独立视觉表面统一复用完整位图 -->
<template>
<view class="module-page" :class="`module-page--${page.template}`">
<!-- 背景层复用已验收的宣纸淡墨山水资产保持全量页面视觉一致 -->
<image class="module-page__paper" :src="isAuth ? '/static/assets/foundation/opaque/auth-page-paper.jpg' : '/static/assets/foundation/opaque/page-paper.jpg'" mode="aspectFill" />
<image class="module-page__scenery" src="/static/assets/foundation/transparent/auth-ink-scenery.png" mode="aspectFill" />
<PageHeader :title="page.title" :root="isRoot" :action="page.number" />
<view class="module-page" :class="[
`module-page--${page.template}`,
{
'module-state--ready': moduleState === 'ready',
'module-state--empty': moduleState === 'empty',
'module-state--success': moduleState === 'success',
'module-state--error': moduleState === 'error'
}
]">
<image class="module-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="module-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="module-page__header"><PageHeader :title="page.title" :action="page.number" /></view>
<view class="module-page__content" :class="{ 'module-page__content--tab': page.tab }">
<view v-if="moduleState === 'ready'" class="module-page__content">
<view class="page-intro">
<image class="page-intro__seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<view><text class="page-intro__number">{{ page.number }}</text><text class="page-intro__subtitle">{{ page.subtitle }}</text></view>
</view>
<!-- 表单与认证母版保留真实 input便于后续接入接口 -->
<view v-if="page.template === 'auth' || page.template === 'form'" class="form-card paper-card">
<view v-for="field in page.fields" :key="field" class="form-row"><text>{{ field }}</text><input :placeholder="`请输入${field}`" placeholder-class="form-placeholder" /></view>
<text v-if="page.note" class="form-note">{{ page.note }}</text>
<view class="primary-action" @click="showPending"><text>{{ page.action }}</text></view>
<view v-if="page.template === 'form'" class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body module-form">
<text class="module-heading">{{ page.title }}</text>
<text class="module-copy">{{ page.subtitle }}</text>
<view v-for="field in page.fields" :key="field" class="form-row">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><text>{{ field }}</text><input :placeholder="`请输入${field}`" placeholder-class="form-placeholder" />
</view>
<text v-if="page.note" class="form-note">{{ normalizedNote }}</text>
<view class="primary-action" @click="completeAction"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ page.action }}</text></view>
</view>
</view>
<!-- 列表母版用于家谱成员文章相册礼仪及记录 -->
<view v-else-if="page.template === 'list'" class="content-stack">
<view v-for="item in page.sections" :key="item[0]" class="list-card paper-card" @click="showPending"><view class="list-card__mark">谱</view><view class="list-card__copy"><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view><text class="list-card__arrow"></text></view>
<view class="primary-action primary-action--compact" @click="showPending"><text>{{ page.action }}</text></view>
<view v-for="item in page.sections" :key="item[0]" class="list-card" @click="openPreview(item)">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="list-card__copy"><text>{{ item[0] }}</text><text>{{ item[1] }}</text><text>点按查看详情</text></view>
</view>
<view class="primary-action primary-action--compact" @click="completeAction"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ page.action }}</text></view>
</view>
<!-- 详情母版强调一项主题信息与分段资料 -->
<view v-else-if="page.template === 'detail'" class="content-stack">
<view class="detail-hero paper-card"><text class="detail-hero__seal">{{ page.number.slice(0, 1) }}</text><text class="detail-hero__title">{{ page.title }}</text><text>{{ page.subtitle }}</text></view>
<view v-for="item in page.sections" :key="item[0]" class="detail-card paper-card"><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view>
<view class="primary-action" @click="showPending"><text>{{ page.action }}</text></view>
<view v-else-if="page.template === 'detail'" class="module-panel module-panel--detail">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body module-detail">
<text class="detail-eyebrow">{{ page.number }} · 家族档案</text>
<text class="module-heading">{{ page.title }}</text>
<text class="module-copy">{{ page.subtitle }}</text>
<view v-for="item in page.sections" :key="item[0]" class="detail-card">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><view><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view>
</view>
<view class="primary-action" @click="completeAction"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ page.action }}</text></view>
</view>
</view>
<!-- 时间轴母版用于申请成长和人生事件 -->
<view v-else-if="page.template === 'timeline'" class="timeline paper-card">
<view v-for="item in page.sections" :key="item[0]" class="timeline__item"><view class="timeline__dot"></view><view><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view></view>
<view class="primary-action primary-action--compact" @click="showPending"><text>{{ page.action }}</text></view>
<view v-else-if="page.template === 'timeline'" class="content-stack">
<view v-for="(item, index) in page.sections" :key="item[0]" class="timeline-row">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text> {{ index + 1 }} </text><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view>
</view>
<view class="primary-action primary-action--compact" @click="completeAction"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ page.action }}</text></view>
</view>
<!-- 设置母版提供清晰的分组入口和状态说明 -->
<view v-else-if="page.template === 'settings'" class="settings paper-card">
<view v-for="item in page.sections" :key="item[0]" class="settings__row" @click="showPending"><view><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view><text></text></view>
<view class="primary-action primary-action--compact" @click="showPending"><text>{{ page.action }}</text></view>
<view v-else-if="page.template === 'settings'" class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body module-settings">
<text class="module-heading">{{ page.title }}</text><text class="module-copy">{{ page.subtitle }}</text>
<view v-for="item in page.sections" :key="item[0]" class="settings-row" @click="openPreview(item)">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><view><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view><text>查看</text>
</view>
<view class="primary-action" @click="completeAction"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ page.action }}</text></view>
</view>
</view>
<!-- 待接入状态使用题签式留白避免裸页面 -->
<view v-else class="status-card paper-card"><text class="status-card__badge">{{ page.badge }}</text><text class="status-card__lead">{{ page.lead }}</text><text class="status-card__note">{{ page.note }}</text><view class="primary-action" @click="showPending"><text>{{ page.action }}</text></view></view>
<view v-else class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body status-card">
<text class="status-card__eyebrow">{{ page.badge }} · 服务说明</text><text class="status-card__lead">{{ page.lead }}</text><text class="status-card__note">{{ normalizedNote }}</text>
<view class="primary-action" @click="completeAction"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ page.action }}</text></view>
</view>
</view>
</view>
<view v-else class="module-page__content module-page__content--state">
<view class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body status-card">
<text class="status-card__eyebrow">{{ stateCopy.eyebrow }}</text><text class="status-card__lead">{{ stateCopy.title }}</text><text class="status-card__note">{{ stateCopy.copy }}</text>
<view class="primary-action" @click="moduleState = 'ready'"><image :src="moduleState === 'error' ? '/static/assets/foundation/opaque/a01-secondary-button.png' : '/static/assets/foundation/opaque/a01-primary-button.png'" mode="scaleToFill" /><text :class="{ 'primary-action__secondary': moduleState === 'error' }">{{ stateCopy.action }}</text></view>
</view>
</view>
</view>
<AppTabbar v-if="page.tab" :active="page.tab" />
</view>
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
import AppTabbar from '@/components/AppTabbar.vue'
import { pageCatalog } from '@/data/page-catalog.js'
const props = defineProps({ pageId: { type: String, required: true } })
const page = computed(() => pageCatalog[props.pageId])
const isAuth = computed(() => page.value.template === 'auth')
const isRoot = computed(() => false)
const showPending = () => uni.showToast({ title: '该功能正在完善中', icon: 'none' })
const query = (() => {
if (typeof location !== 'undefined') return Object.fromEntries(new URLSearchParams(location.hash.split('?')[1] || ''))
const pages = getCurrentPages(); return pages[pages.length - 1]?.options || {}
})()
const moduleState = ref('ready')
if (query.state === 'empty' || query.state === 'success' || query.state === 'error') moduleState.value = query.state
const normalizedNote = computed(() => (page.value.note || '').replace(/待接入|服务待接入|功能待接入/g, '将在后续功能阶段开放'))
const stateCopy = computed(() => ({
empty: { eyebrow: `${page.value.number} · 暂无内容`, title: `还没有${page.value.title}记录`, copy: '完成第一条记录后,内容会按时间或类别整理在这里。', action: page.value.action },
success: { eyebrow: '本地预览已保存', title: `${page.value.title}内容已更新`, copy: '当前候选已展示完整操作结果;正式数据将在接口阶段接入。', action: '继续查看' },
error: { eyebrow: '内容暂不可用', title: `暂时无法打开${page.value.title}`, copy: '请稍后重新进入,现有资料不会受到影响。', action: '重新查看' }
}[moduleState.value] || {}))
const completeAction = () => { moduleState.value = 'success' }
const openPreview = (item) => uni.showToast({ title: item[0], icon: 'none' })
</script>
<style scoped lang="scss">
.module-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.module-page__paper, .module-page__scenery { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.module-page__paper { opacity: .98; }.module-page__scenery { opacity: .32; }
.module-page__content { position: relative; z-index: 1; min-height: calc(100vh - 104rpx); padding: 34rpx 28rpx calc(64rpx + env(safe-area-inset-bottom)); box-sizing: border-box; }
.module-page__content--tab { padding-bottom: calc(190rpx + env(safe-area-inset-bottom)); }
.page-intro { display: flex; align-items: center; min-height: 104rpx; margin: 0 10rpx 26rpx; }
.page-intro__seal { width: 74rpx; height: 74rpx; margin-right: 14rpx; }.page-intro__number, .page-intro__subtitle { display: block; }
.page-intro__number { color: $brand-red; font-family: STKaiti, KaiTi, serif; font-size: 27rpx; font-weight: 700; letter-spacing: 3rpx; }.page-intro__subtitle { margin-top: 6rpx; color: $ink-muted; font-size: 22rpx; }
.paper-card { position: relative; border: 1rpx solid $gold-soft; background: rgba(255, 250, 240, .9); box-shadow: inset 0 0 0 6rpx rgba(255, 255, 255, .48); }
.form-card { padding: 16rpx 30rpx 34rpx; }.form-row { display: flex; align-items: center; min-height: 104rpx; border-bottom: 1rpx solid #ead9ba; }.form-row text { width: 142rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; }.form-row input { min-width: 0; flex: 1; color: $ink; font-size: 27rpx; }.form-placeholder { color: #b3a895; }.form-note { display: block; margin-top: 22rpx; color: $ink-muted; font-size: 21rpx; text-align: center; }
.primary-action { position: relative; display: flex; height: 100rpx; align-items: center; justify-content: center; margin-top: 34rpx; overflow: hidden; background-image: url('/static/assets/foundation/opaque/a01-primary-button.png'); background-position: center; background-repeat: no-repeat; background-size: 100% 100%; color: #fff9ec; font-family: STKaiti, KaiTi, serif; font-size: 34rpx; font-weight: 700; letter-spacing: 6rpx; }.primary-action--compact { height: 82rpx; font-size: 28rpx; }
.content-stack { display: flex; flex-direction: column; gap: 20rpx; }.list-card { display: flex; min-height: 132rpx; align-items: center; padding: 16rpx 22rpx; }.list-card__mark { display: flex; width: 74rpx; height: 86rpx; align-items: center; justify-content: center; margin-right: 18rpx; border: 2rpx solid $brand-red; color: $brand-red; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }.list-card__copy { min-width: 0; flex: 1; }.list-card__copy text, .detail-card text, .timeline__item text, .settings__row text { display: block; }.list-card__copy text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; }.list-card__copy text:last-child { margin-top: 9rpx; color: $ink-muted; font-size: 21rpx; }.list-card__arrow { color: $gold; font-size: 44rpx; }
.detail-hero { display: flex; flex-direction: column; align-items: center; padding: 38rpx 30rpx; text-align: center; }.detail-hero__seal { display: flex; width: 80rpx; height: 96rpx; align-items: center; justify-content: center; border: 2rpx solid $brand-red; color: $brand-red; font-family: STKaiti, KaiTi, serif; font-size: 38rpx; font-weight: 700; }.detail-hero__title { margin: 20rpx 0 12rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 40rpx; font-weight: 700; }.detail-hero > text:last-child { color: $ink-muted; font-size: 22rpx; }.detail-card { padding: 28rpx; }.detail-card text:first-child { color: $brand-red; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }.detail-card text:last-child { margin-top: 14rpx; color: $ink-muted; font-size: 25rpx; line-height: 1.8; }
.timeline { padding: 12rpx 28rpx 30rpx; }.timeline__item { position: relative; display: flex; gap: 22rpx; min-height: 112rpx; padding: 30rpx 0 0; }.timeline__item:not(:last-of-type)::before { position: absolute; top: 55rpx; left: 9rpx; width: 2rpx; height: 78rpx; background: $gold-soft; content: ''; }.timeline__dot { width: 20rpx; height: 20rpx; margin-top: 8rpx; border-radius: 50%; background: $brand-red; }.timeline__item text:first-child { color: $ink; font-size: 29rpx; font-weight: 700; }.timeline__item text:last-child { margin-top: 9rpx; color: $ink-muted; font-size: 22rpx; }
.settings { padding: 0 28rpx 28rpx; }.settings__row { display: flex; min-height: 116rpx; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #ead9ba; }.settings__row text:first-child { color: $ink; font-size: 29rpx; }.settings__row text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 21rpx; }.settings__row > text { color: $gold; font-size: 42rpx; }.status-card { display: flex; min-height: 540rpx; flex-direction: column; align-items: center; justify-content: center; padding: 40rpx; text-align: center; }.status-card__badge { display: flex; width: 116rpx; height: 136rpx; align-items: center; justify-content: center; border: 2rpx solid $brand-red; color: $brand-red; font-family: STKaiti, KaiTi, serif; font-size: 50rpx; font-weight: 700; }.status-card__lead { margin-top: 30rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 36rpx; font-weight: 700; }.status-card__note { margin-top: 16rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.7; }
.module-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
.module-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .12; pointer-events: none; }
.module-page__header, .module-page__content { position: relative; z-index: 2; }
.module-page__content { padding: 20rpx 24rpx 50rpx; }
.page-intro { display: flex; align-items: center; min-height: 86rpx; margin: 0 8rpx 14rpx; }
.page-intro__seal { width: 66rpx; height: 66rpx; margin-right: 13rpx; }
.page-intro__number, .page-intro__subtitle { display: block; }
.page-intro__number { color: $brand-red; font-size: 23rpx; font-weight: 700; letter-spacing: 3rpx; }
.page-intro__subtitle { margin-top: 5rpx; color: $ink-muted; font-size: 20rpx; }
.module-panel { position: relative; width: 100%; height: min(640px, calc((100vw - 24px) * 1.48)); }
.module-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.module-panel__body { position: absolute; inset: 7.5% 8%; }
.module-heading { display: block; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 34rpx; font-weight: 700; }
.module-copy { display: block; margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.form-row { position: relative; height: 76rpx; margin-top: 12rpx; }
.form-row image, .detail-card image, .settings-row image, .list-card > image, .timeline-row > image, .primary-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.form-row > text { position: absolute; top: 26rpx; left: 22rpx; z-index: 1; color: $ink; font-size: 21rpx; font-weight: 700; }
.form-row input { position: absolute; top: 0; right: 18rpx; bottom: 0; left: 164rpx; z-index: 1; height: 76rpx; color: $ink; font-size: 21rpx; line-height: 76rpx; }
.form-placeholder { color: #a79884; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 19rpx; text-align: center; }
.primary-action { position: relative; width: 100%; height: 76rpx; margin-top: 18rpx; }
.primary-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 24rpx; font-weight: 700; letter-spacing: 2rpx; }
.primary-action .primary-action__secondary { color: $ink; }
.primary-action--compact { width: 100%; }
.content-stack { display: flex; flex-direction: column; gap: 14rpx; }
.list-card, .timeline-row { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 220rpx; }
.list-card__copy { position: absolute; inset: 19% 9%; z-index: 1; }
.list-card__copy text { display: block; }
.list-card__copy text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }
.list-card__copy text:nth-child(2) { margin-top: 9rpx; color: $ink-muted; font-size: 20rpx; }
.list-card__copy text:last-child { margin-top: 9rpx; color: $brand-red; font-size: 18rpx; }
.detail-eyebrow { display: block; color: $brand-red; font-size: 21rpx; letter-spacing: 3rpx; }
.module-detail .module-heading { margin-top: 9rpx; }
.detail-card, .settings-row { position: relative; min-height: 88rpx; margin-top: 14rpx; }
.detail-card > view { position: relative; z-index: 1; padding: 18rpx 24rpx; }
.detail-card text, .settings-row text { display: block; }
.detail-card text:first-child { color: $brand-red; font-size: 21rpx; font-weight: 700; }
.detail-card text:last-child { margin-top: 7rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
.timeline-row > view { position: absolute; inset: 17% 9%; z-index: 1; }
.timeline-row text { display: block; }
.timeline-row text:first-child { color: $brand-red; font-size: 18rpx; letter-spacing: 2rpx; }
.timeline-row text:nth-child(2) { margin-top: 5rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }
.timeline-row text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; }
.settings-row > view { position: absolute; top: 19rpx; left: 22rpx; z-index: 1; }
.settings-row > view text:first-child { color: $ink; font-size: 22rpx; font-weight: 700; }
.settings-row > view text:last-child { margin-top: 5rpx; color: $ink-muted; font-size: 18rpx; }
.settings-row > text { position: absolute; top: 34rpx; right: 22rpx; z-index: 1; color: $brand-red; font-size: 18rpx; }
.status-card { top: 28%; text-align: center; }
.status-card__eyebrow { display: block; color: $brand-red; font-size: 21rpx; letter-spacing: 3rpx; }
.status-card__lead { display: block; margin-top: 14rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
.status-card__note { display: block; margin-top: 17rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.65; }
.status-card .primary-action { width: 420rpx; max-width: 100%; margin: 30rpx auto 0; }
.module-page__content--state { padding-top: 18rpx; }
@media (min-width: 400px) { .module-page__content { padding-right: 32rpx; padding-left: 32rpx; } }
</style>
+137
View File
@@ -0,0 +1,137 @@
<!-- T04-T06 共用成员表单仅复用视觉与交互骨架各页面通过 kind 拥有独立任务和文案 -->
<template>
<view class="member-form-page" :class="{
'form-state--form': formState === 'form',
'form-state--success': formState === 'success',
'form-state--conflict': formState === 'conflict',
'form-state--error': formState === 'error'
}">
<image class="member-form-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="member-form-page__header"><PageHeader :title="config.pageTitle" /></view>
<view class="member-form-panel">
<image class="member-form-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view v-if="formState === 'form'" class="member-form">
<text class="member-form__eyebrow">{{ config.eyebrow }}</text>
<text class="member-form__title">{{ config.title }}</text>
<text class="member-form__copy">{{ config.copy }}</text>
<view v-for="field in config.fields" :key="field.key" class="member-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>{{ field.label }}</text>
<input v-model="form[field.key]" :placeholder="field.placeholder" placeholder-class="member-placeholder" />
</view>
<text class="member-form__note">{{ config.note }}</text>
<view class="member-form-action" @click="saveForm">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ config.action }}</text>
</view>
</view>
<view v-else class="member-form-result">
<text class="member-form__eyebrow">{{ resultCopy.eyebrow }}</text>
<text class="member-form__title">{{ resultCopy.title }}</text>
<text class="member-form__copy">{{ resultCopy.copy }}</text>
<view v-if="formState === 'conflict'" class="conflict-actions">
<view class="member-form-action" @click="formState = 'form'">
<image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" /><text class="member-form-action__secondary">返回核对</text>
</view>
<view class="member-form-action" @click="showConflictHelp">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>查看冲突</text>
</view>
</view>
<view v-else class="member-form-action" @click="formState = 'form'">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ formState === 'success' ? '继续完善' : '重新填写' }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
const props = defineProps({ kind: { type: String, required: true } })
const configs = {
add: {
pageTitle: '新增亲属', eyebrow: '补全家族关系', title: '为汤文远添加一位亲属',
copy: '先确认与当前成员的关系,再录入基础身份信息。', action: '保存亲属', note: '保存后将在世系图中显示,可继续补充详细档案。',
fields: [
{ key: 'name', label: '姓名', placeholder: '请输入真实姓名' },
{ key: 'relation', label: '与本人关系', placeholder: '例如:长子、配偶' },
{ key: 'birthDate', label: '出生日期', placeholder: '例如:1992年' },
{ key: 'gender', label: '性别', placeholder: '请选择或输入' }
]
},
edit: {
pageTitle: '编辑成员', eyebrow: '成员档案维护', title: '完善汤文远的生命记录',
copy: '基础身份用于世系展示,生平信息可在成员档案中继续补充。', action: '保存资料', note: '隐私字段只向本人和有权限的家谱管理员展示。',
fields: [
{ key: 'name', label: '姓名', placeholder: '汤文远' },
{ key: 'generation', label: '字辈', placeholder: '例如:文' },
{ key: 'birthDate', label: '出生日期', placeholder: '1940年' },
{ key: 'summary', label: '人物简介', placeholder: '简要记录生平' }
]
},
relation: {
pageTitle: '关系维护', eyebrow: '亲属关系校正', title: '确认两位成员的家族关系',
copy: '调整关系会影响世系位置,保存前请核对成员和关系类型。', action: '保存关系', note: '若形成重复父母、代际倒置等情况,页面会先提示冲突。',
fields: [
{ key: 'source', label: '当前成员', placeholder: '汤文远' },
{ key: 'target', label: '关联成员', placeholder: '请选择家谱成员' },
{ key: 'relation', label: '关系类型', placeholder: '父子、配偶等' },
{ key: 'effectiveDate', label: '生效日期', placeholder: '选填' }
]
}
}
const config = computed(() => configs[props.kind])
const form = reactive({ name: '', relation: '', birthDate: '', gender: '', generation: '', summary: '', source: '', target: '', effectiveDate: '' })
const query = (() => {
if (typeof location !== 'undefined') return Object.fromEntries(new URLSearchParams(location.hash.split('?')[1] || ''))
const pages = getCurrentPages()
return pages[pages.length - 1]?.options || {}
})()
const formState = ref(query.state === 'success' ? 'success' : query.state === 'conflict' ? 'conflict' : query.state === 'error' ? 'error' : 'form')
const resultCopy = computed(() => ({
success: { eyebrow: '本地预览已更新', title: `${config.value.pageTitle}内容已保存`, copy: '返回世系树后可查看新的展示位置;正式数据将在接口阶段接入。' },
conflict: { eyebrow: '发现关系冲突', title: '这段关系会造成代际矛盾', copy: '目标成员已经存在父级关系,请先核对原关系,再决定是否调整。' },
error: { eyebrow: '内容未保存', title: '暂时无法完成这次操作', copy: '请返回成员档案重新进入,当前填写内容不会写入正式数据。' }
}[formState.value] || {}))
const saveForm = () => {
if (props.kind === 'relation' && (!form.target.trim() || !form.relation.trim())) {
formState.value = 'conflict'
return
}
formState.value = 'success'
}
const showConflictHelp = () => uni.showModal({ title: '关系冲突说明', content: '同一成员不能同时存在两个生父或形成上下代循环。请返回世系树核对原关系。', showCancel: false })
</script>
<style scoped lang="scss">
.member-form-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.member-form-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
.member-form-page__header { position: relative; z-index: 3; }
.member-form-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(640px, calc((100vw - 16px) * 1.48)); margin: 18rpx auto 0; }
.member-form-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-form, .member-form-result { position: absolute; inset: 7.5% 8%; }
.member-form__eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
.member-form__title { display: block; margin-top: 10rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 34rpx; font-weight: 700; }
.member-form__copy { display: block; margin-top: 9rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.member-field { position: relative; height: 78rpx; margin-top: 12rpx; }
.member-field image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-field > text { position: absolute; top: 27rpx; left: 22rpx; z-index: 1; color: $ink; font-size: 21rpx; font-weight: 700; }
.member-field input { position: absolute; top: 0; right: 18rpx; bottom: 0; left: 164rpx; z-index: 1; height: 78rpx; color: $ink; font-size: 21rpx; line-height: 78rpx; }
.member-placeholder { color: #a79884; }
.member-form__note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 19rpx; line-height: 1.45; text-align: center; }
.member-form-action { position: relative; width: 100%; height: 76rpx; margin-top: 17rpx; }
.member-form-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-form-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 24rpx; font-weight: 700; }
.member-form-action .member-form-action__secondary { color: $ink; }
.member-form-result { top: 29%; text-align: center; }
.member-form-result .member-form__eyebrow { text-align: center; }
.member-form-result .member-form__copy { margin-top: 20rpx; }
.member-form-result > .member-form-action { width: 420rpx; max-width: 100%; margin: 32rpx auto 0; }
.conflict-actions { display: flex; gap: 14rpx; margin-top: 30rpx; }
.conflict-actions .member-form-action { width: calc(50% - 7rpx); margin-top: 0; }
@media (min-width: 400px) { .member-form-panel { width: calc(100% - 48rpx); } }
</style>