165 lines
11 KiB
Vue
165 lines
11 KiB
Vue
<!-- 页面编号:T-01;用途:世系树阅读、同页提示、空态与失败状态(页面设计阶段使用本地模拟数据)。 -->
|
||
<template>
|
||
<view class="tree-page" :class="{
|
||
'tree-state--tree': treeState === 'tree',
|
||
'tree-state--landscape': treeState === 'landscape',
|
||
'tree-state--empty': treeState === 'empty',
|
||
'tree-state--error': treeState === 'error'
|
||
}">
|
||
<image class="tree-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
|
||
<view class="tree-page__header"><PageHeader title="世系树" action="成员目录" @action="toDirectory" /></view>
|
||
|
||
<view class="tree-toolbar">
|
||
<view class="tree-toolbar__title">
|
||
<text>汤氏家谱</text><text>主支 · 第 12—14 世</text>
|
||
</view>
|
||
<view class="tree-toolbar__actions">
|
||
<text @click="treeState = 'landscape'">阅读提示</text>
|
||
<text @click="toRelationship">关系维护</text>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="tree-scroll" scroll-x :show-scrollbar="false">
|
||
<view class="tree-canvas" :class="{ 'tree-canvas--state': treeState !== 'tree' }">
|
||
<image class="tree-canvas__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
|
||
|
||
<template v-if="treeState === 'tree'">
|
||
<view class="generation-label generation-label--one"><text>第十二世</text></view>
|
||
<view class="generation-label generation-label--two"><text>第十三世</text></view>
|
||
<view class="generation-label generation-label--three"><text>第十四世</text></view>
|
||
<view v-for="member in members" :key="member.id" class="member-node" :class="{ 'member-node--selected': selected?.id === member.id }" :style="nodeStyle(member)" @click="selected = member">
|
||
<image class="member-node__skin" src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
|
||
<text class="node-name">{{ member.name }}</text>
|
||
<text class="node-relation">{{ member.relation }}</text>
|
||
<text class="node-years">{{ member.years }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<view v-else class="tree-state-card">
|
||
<text class="tree-state-card__eyebrow">{{ stateCopy.eyebrow }}</text>
|
||
<text class="tree-state-card__title">{{ stateCopy.title }}</text>
|
||
<text class="tree-state-card__copy">{{ stateCopy.copy }}</text>
|
||
<view class="tree-state-card__action" @click="handleStateAction">
|
||
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
|
||
<text>{{ stateCopy.action }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view v-if="treeState === 'tree' && selected" class="member-sheet">
|
||
<image class="member-sheet__skin" src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
|
||
<view class="member-sheet__copy">
|
||
<text class="sheet-name">{{ selected.name }}</text>
|
||
<text class="sheet-meta">第 {{ selected.generation }} 世 · {{ selected.relation }} · {{ selected.branch }}</text>
|
||
</view>
|
||
<view class="sheet-actions">
|
||
<view class="sheet-action sheet-member-action" @click="toMember">
|
||
<image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" /><text>查看资料</text>
|
||
</view>
|
||
<view class="sheet-action" @click="toAddRelative">
|
||
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>添加亲属</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import PageHeader from '@/components/PageHeader.vue'
|
||
import { genealogyContext } from '@/utils/genealogy-context.js'
|
||
|
||
const genealogyId = ref('')
|
||
const treeState = ref('loading')
|
||
const selected = ref(null)
|
||
const members = ref([
|
||
{ id: 101, name: '汤文远', relation: '始祖', years: '1940—2012', generation: 12, branch: '主支', x: 50, y: 15 },
|
||
{ id: 102, name: '汤正国', relation: '长子', years: '1965—', generation: 13, branch: '长房', x: 31, y: 43 },
|
||
{ id: 103, name: '汤正华', relation: '次子', years: '1968—', generation: 13, branch: '二房', x: 69, y: 43 },
|
||
{ id: 104, name: '汤凯', relation: '长孙', years: '1992—', generation: 14, branch: '长房', x: 20, y: 72 },
|
||
{ id: 105, name: '汤悦', relation: '长孙女', years: '1995—', generation: 14, branch: '长房', x: 50, y: 72 },
|
||
{ id: 106, name: '汤晨', relation: '次孙', years: '1998—', generation: 14, branch: '二房', x: 80, y: 72 }
|
||
])
|
||
|
||
const stateCopy = computed(() => ({
|
||
landscape: { eyebrow: '横向阅读提示', title: '左右拖动查看完整世系', copy: '世系按代际从上到下排列;横向拖动画布可以查看同一世代的其他支系。', action: '进入世系图' },
|
||
empty: { eyebrow: '尚未建立世系', title: '从第一位先祖开始记录', copy: '录入首代成员后,即可继续补充子女、配偶与后代关系。', action: '录入首代成员' },
|
||
error: { eyebrow: '世系暂不可用', title: '暂时无法读取成员关系', copy: '请从当前家谱重新进入;已经录入的成员资料不会受到影响。', action: '重新查看' },
|
||
loading: { eyebrow: '正在整理世系', title: '请稍候', copy: '正在准备家谱成员关系。', action: '重新查看' }
|
||
}[treeState.value] || {}))
|
||
|
||
onLoad((query) => {
|
||
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || ''
|
||
if (query.state === 'landscape' || query.state === 'empty' || query.state === 'error') {
|
||
treeState.value = query.state
|
||
return
|
||
}
|
||
if (!genealogyId.value) {
|
||
treeState.value = 'empty'
|
||
return
|
||
}
|
||
genealogyContext.setCurrentGenealogyId(genealogyId.value)
|
||
selected.value = members.value.find((item) => String(item.id) === String(query.selectedId)) || members.value[0]
|
||
treeState.value = 'tree'
|
||
})
|
||
|
||
const nodeStyle = (member) => ({ left: `${member.x}%`, top: `${member.y}%` })
|
||
const handleStateAction = () => {
|
||
if (treeState.value === 'empty') {
|
||
uni.navigateTo({ url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value || 1001}&mode=first` })
|
||
return
|
||
}
|
||
selected.value = members.value[0]
|
||
treeState.value = 'tree'
|
||
}
|
||
const toMember = () => uni.navigateTo({ url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${selected.value.id}` })
|
||
const toDirectory = () => uni.navigateTo({ url: `/pages/tree/t07-member-directory?genealogyId=${genealogyId.value}` })
|
||
const toRelationship = () => uni.navigateTo({ url: `/pages/tree/t06-edit-relationship?genealogyId=${genealogyId.value}` })
|
||
const toAddRelative = () => uni.navigateTo({ url: `/pages/tree/t04-add-relative?genealogyId=${genealogyId.value}&personId=${selected.value.id}` })
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.tree-page { position: relative; height: 100vh; overflow: hidden; background: $paper; }
|
||
.tree-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
|
||
.tree-page__header, .tree-toolbar, .tree-scroll, .member-sheet { position: relative; z-index: 2; }
|
||
.tree-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 18rpx 28rpx 14rpx; }
|
||
.tree-toolbar__title text { display: block; }
|
||
.tree-toolbar__title text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 28rpx; font-weight: 700; }
|
||
.tree-toolbar__title text:last-child { margin-top: 5rpx; color: $ink-muted; font-size: 19rpx; }
|
||
.tree-toolbar__actions { display: flex; gap: 24rpx; color: $brand-red; font-size: 21rpx; }
|
||
.tree-scroll { width: 100%; height: calc(100vh - 330rpx); white-space: nowrap; }
|
||
.tree-canvas { position: relative; width: 1180rpx; height: 1040rpx; margin: 0 18rpx; }
|
||
.tree-canvas--state { width: calc(100vw - 36rpx); }
|
||
.tree-canvas__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||
.generation-label { position: absolute; left: 3.5%; z-index: 2; color: #92764f; font-size: 19rpx; writing-mode: vertical-rl; letter-spacing: 3rpx; }
|
||
.generation-label--one { top: 16%; }
|
||
.generation-label--two { top: 44%; }
|
||
.generation-label--three { top: 73%; }
|
||
.member-node { position: absolute; width: 190rpx; height: 132rpx; transform: translate(-50%, -50%); text-align: center; }
|
||
.member-node__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||
.node-name, .node-relation, .node-years { position: relative; z-index: 1; display: block; }
|
||
.node-name { padding-top: 22rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 29rpx; font-weight: 700; }
|
||
.node-relation { margin-top: 6rpx; color: $brand-red; font-size: 19rpx; }
|
||
.node-years { margin-top: 4rpx; color: $ink-muted; font-size: 17rpx; }
|
||
.member-node--selected .node-name { color: $brand-red; }
|
||
.tree-state-card { position: absolute; top: 28%; right: 9%; left: 9%; z-index: 2; text-align: center; white-space: normal; }
|
||
.tree-state-card__eyebrow { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 3rpx; }
|
||
.tree-state-card__title { display: block; margin-top: 14rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 36rpx; font-weight: 700; }
|
||
.tree-state-card__copy { display: block; margin-top: 18rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.7; }
|
||
.tree-state-card__action { position: relative; width: 380rpx; height: 76rpx; margin: 30rpx auto 0; }
|
||
.tree-state-card__action image { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||
.tree-state-card__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-sheet { position: fixed; right: 18rpx; bottom: calc(14rpx + env(safe-area-inset-bottom)); left: 18rpx; height: 178rpx; }
|
||
.member-sheet__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||
.member-sheet__copy { position: absolute; top: 34rpx; left: 44rpx; z-index: 1; }
|
||
.sheet-name { display: block; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
|
||
.sheet-meta { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 19rpx; }
|
||
.sheet-actions { position: absolute; top: 46rpx; right: 34rpx; z-index: 1; display: flex; gap: 10rpx; }
|
||
.sheet-action { position: relative; width: 130rpx; height: 62rpx; }
|
||
.sheet-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||
.sheet-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: $ink; font-size: 20rpx; font-weight: 700; }
|
||
.sheet-action:last-child text { color: #fff9ed; }
|
||
</style>
|