修改完成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
+140 -41
View File
@@ -1,44 +1,119 @@
<!-- 页面编号T-01用途世系树总览 -->
<!-- 页面编号T-01用途世系树阅读同页提示空态与失败状态页面设计阶段使用本地模拟数据 -->
<template>
<view class="tree-page">
<PageHeader title="世系树" action="树形视图" />
<view class="tree-toolbar"><view class="branch-select">当前家谱世系</view><view class="tree-tools"><text @click="toDirectory">查找</text><text @click="toRelationship">编辑</text></view></view>
<scroll-view class="tree-scroll" scroll-x scroll-y>
<view class="tree-canvas">
<view v-if="!members.length" class="tree-empty">{{ loadError || '暂未录入族人' }}</view>
<view v-for="member in members" :key="member.id" class="member-node" :class="{ selected: selected && selected.id === member.id }" :style="nodeStyle(member)" @click="selected = member"><text class="node-name">{{ member.name }}</text><text class="node-relation">{{ member.relation }}</text><text class="node-years">{{ member.years }}</text></view>
<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>主支 · 1214 </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="selected" class="member-sheet"><view><text class="sheet-name">{{ selected.name }}</text><text class="sheet-meta">第 {{ selected.generation }} 世 · {{ selected.relation }} · {{ selected.branch }}</text></view><view class="sheet-actions"><text @click="toMember">查看资料</text><text @click="toAddRelative">添加亲属</text></view></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 { ref } from 'vue'
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const members = ref([])
const selected = ref(null)
const genealogyId = ref('')
const loadError = 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 loadTree = async (query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) { loadError.value = '未找到当前家谱。'; return }
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
members.value = await appApi.getTree(genealogyId.value)
selected.value = members.value.find((item) => String(item.id) === String(query.selectedId)) || members.value[0] || null
} catch (error) {
loadError.value = error.message || '世系树加载失败。'
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'
})
onLoad(loadTree)
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}` })
@@ -46,20 +121,44 @@ const toAddRelative = () => uni.navigateTo({ url: `/pages/tree/t04-add-relative?
</script>
<style scoped lang="scss">
.tree-page { height: 100vh; overflow: hidden; background: $paper; }
.tree-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 28rpx; border-bottom: 1rpx solid $gold-soft; background: #fffaf0; }
.branch-select { padding: 12rpx 18rpx; border: 1rpx solid $gold; color: $ink; font-size: 25rpx; }
.tree-tools { display: flex; gap: 24rpx; color: $brand-red; font-size: 26rpx; }
.tree-scroll { height: calc(100vh - 382rpx); white-space: nowrap; }
.tree-canvas { position: relative; width: 1180rpx; height: 1120rpx; background: $paper; }
.tree-empty { padding: 240rpx 0; color: $ink-muted; font-size: 28rpx; text-align: center; }
.member-node { position: absolute; width: 150rpx; min-height: 148rpx; padding: 20rpx 12rpx; box-sizing: border-box; border: 2rpx solid $gold-soft; background: #fffaf0; transform: translateX(-50%); white-space: normal; text-align: center; }
.member-node.selected { border-color: $brand-red; box-shadow: 0 0 0 6rpx #f2dfcf; }
.node-name { display: block; color: $ink; font-family: serif; font-size: 32rpx; font-weight: 700; }
.node-relation { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
.node-years { display: block; margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; }
.member-sheet { position: fixed; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: space-between; padding: 26rpx 28rpx calc(26rpx + env(safe-area-inset-bottom)); border-top: 1rpx solid $gold; background: $navy; color: #fff8ec; }
.sheet-name { display: block; font-family: serif; font-size: 36rpx; font-weight: 700; }
.sheet-meta { display: block; margin-top: 8rpx; color: #dfc590; font-size: 21rpx; }
.sheet-actions { display: flex; flex-direction: column; gap: 12rpx; color: #f3dba7; font-size: 24rpx; text-align: right; }
.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>
+73 -46
View File
@@ -1,59 +1,86 @@
<!-- 页面编号T-03用途成员档案详情 -->
<!-- 页面编号T-03用途成员档案详情与失败状态页面设计阶段使用本地模拟数据 -->
<template>
<view class="page-shell member-page">
<PageHeader title="成员档案" action="编辑" @action="toEdit" />
<view v-if="member" class="member-hero"><view class="member-seal">{{ member.name.slice(0, 1) }}</view><view><text class="member-name">{{ member.name }}</text><text class="member-role"> {{ member.generation }} · {{ member.relation }} · 家谱成员</text></view></view>
<view v-else class="state-copy">{{ loadError || '正在载入成员档案…' }}</view>
<template v-if="member">
<view class="profile-card paper-card"><text class="section-title">基本资料</text><view v-for="item in details" :key="item.label" class="info-row"><text>{{ item.label }}</text><text>{{ item.value }}</text></view></view>
<view class="profile-card paper-card"><text class="section-title">亲属关系</text><text class="bio">亲属关系将在编辑功能接入后完整维护</text></view>
<view v-if="member.introduction" class="profile-card paper-card"><text class="section-title">生平事迹</text><text class="bio">{{ member.introduction }}</text></view>
</template>
<view class="member-page" :class="{ 'member-state--detail': memberState === 'detail', 'member-state--error': memberState === 'error' }">
<image class="member-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="member-page__header"><PageHeader title="成员档案" :action="memberState === 'detail' ? '编辑' : ''" @action="toEdit" /></view>
<view class="member-panel">
<image class="member-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view v-if="memberState === 'detail'" class="member-detail">
<view class="member-heading">
<view class="member-heading__seal"><image src="/static/assets/modules/genealogy/transparent/current-seal-frame.png" mode="scaleToFill" /><text>{{ member.name.slice(0, 1) }}</text></view>
<view><text>{{ member.name }}</text><text> {{ member.generation }} · {{ member.relation }} · {{ member.branch }}</text></view>
</view>
<text class="member-section-title">基本资料</text>
<view v-for="item in details" :key="item.label" class="member-info-row">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><text>{{ item.label }}</text><text>{{ item.value }}</text>
</view>
<text class="member-section-title member-section-title--relation">亲属关系</text>
<view class="member-relatives">
<text>长子 · 汤正国</text><text>次子 · 汤正华</text>
</view>
<view class="member-profile-action" @click="toEdit">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>完善成员档案</text>
</view>
</view>
<view v-else class="member-error">
<text>成员档案暂不可用</text><text>请从世系树重新选择成员当前没有可展示的个人资料</text>
<view class="member-profile-action" @click="toTree"><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 { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const member = ref(null)
const loadError = ref('')
const genealogyId = ref('')
const details = computed(() => member.value ? [
{ label: '字辈', value: member.value.generationName || `${member.value.generation}` },
{ label: '出生日期', value: member.value.birthDate || '待补' },
{ label: '生卒信息', value: member.value.years || '待补' },
{ label: '所属支系', value: member.value.branch || '主支' }
] : [])
onLoad(async (query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value || !query.personId) { loadError.value = '成员参数不完整,请从世系树进入。'; return }
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
member.value = await appApi.getPerson(genealogyId.value, query.personId)
if (!member.value) loadError.value = '未找到该成员。'
} catch (error) {
loadError.value = error.message || '成员档案加载失败。'
}
const personId = ref('')
const memberState = ref('loading')
const member = ref({ id: 101, name: '汤文远', generation: 12, relation: '始祖', branch: '主支', generationName: '文字辈', birthDate: '1940年3月', years: '1940—2012' })
const details = computed(() => [
{ label: '字辈', value: member.value.generationName },
{ label: '出生日期', value: member.value.birthDate },
{ label: '生卒信息', value: member.value.years },
{ label: '所属支系', value: member.value.branch }
])
onLoad((query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() || ''
personId.value = query.personId || ''
if (genealogyId.value) genealogyContext.setCurrentGenealogyId(genealogyId.value)
memberState.value = query.state === 'error' || !personId.value ? 'error' : 'detail'
})
const toEdit = () => uni.navigateTo({ url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${member.value?.id || ''}` })
const toEdit = () => uni.navigateTo({ url: `/pages/tree/t05-edit-member?genealogyId=${genealogyId.value}&personId=${personId.value}` })
const toTree = () => uni.navigateBack()
</script>
<style scoped lang="scss">
.member-page { padding-bottom: 36rpx; }
.member-hero { display: flex; align-items: center; gap: 24rpx; padding: 38rpx 34rpx; background: $navy; }
.member-seal { display: flex; width: 92rpx; height: 92rpx; align-items: center; justify-content: center; border: 3rpx solid #e0bd77; border-radius: 50%; color: #f4daa1; font-family: serif; font-size: 46rpx; }
.member-name { display: block; color: #fff8ec; font-family: serif; font-size: 46rpx; font-weight: 700; }
.member-role { display: block; margin-top: 10rpx; color: #e0c993; font-size: 23rpx; }
.state-copy { padding: 80rpx 40rpx; color: $ink-muted; font-size: 27rpx; text-align: center; }
.profile-card { margin: 26rpx 28rpx 0; padding: 30rpx; }
.info-row { display: flex; justify-content: space-between; padding: 25rpx 0; border-bottom: 1rpx solid #eadfc9; color: $ink-muted; font-size: 27rpx; }
.info-row text:last-child { color: $ink; }
.info-row:last-child { border-bottom: 0; }
.bio { display: block; margin-top: 22rpx; color: $ink-muted; font-size: 27rpx; line-height: 1.9; }
.member-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.member-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
.member-page__header { position: relative; z-index: 3; }
.member-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(650px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 0; }
.member-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-detail { position: absolute; inset: 7% 8%; }
.member-heading { display: flex; align-items: center; gap: 20rpx; }
.member-heading__seal { position: relative; display: flex; width: 84rpx; height: 108rpx; align-items: center; justify-content: center; }
.member-heading__seal image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-heading__seal text { position: relative; z-index: 1; color: #fff8ec; font-family: 'STKaiti', 'KaiTi', serif; font-size: 33rpx; }
.member-heading > view:last-child text { display: block; }
.member-heading > view:last-child text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 38rpx; font-weight: 700; }
.member-heading > view:last-child text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; }
.member-section-title { display: block; margin: 20rpx 0 4rpx; color: $brand-red; font-size: 22rpx; font-weight: 700; letter-spacing: 2rpx; }
.member-info-row { position: relative; height: 62rpx; margin-top: 7rpx; }
.member-info-row image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-info-row text { position: absolute; top: 21rpx; z-index: 1; font-size: 20rpx; }
.member-info-row text:nth-child(2) { left: 22rpx; color: $ink-muted; }
.member-info-row text:last-child { right: 22rpx; color: $ink; font-weight: 700; }
.member-section-title--relation { margin-top: 18rpx; }
.member-relatives { display: flex; justify-content: space-between; color: $ink; font-size: 20rpx; }
.member-profile-action { position: relative; width: 100%; height: 72rpx; margin-top: 20rpx; }
.member-profile-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.member-profile-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-error { position: absolute; top: 31%; right: 12%; left: 12%; text-align: center; }
.member-error > text { display: block; }
.member-error > text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 35rpx; font-weight: 700; }
.member-error > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.65; }
@media (min-width: 400px) { .member-panel { width: calc(100% - 48rpx); } }
</style>
+3 -3
View File
@@ -1,3 +1,3 @@
<!-- 页面编号T-04用途新增直系亲属 -->
<template><ModulePage page-id="t04" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号T-04用途新增直系亲属与保存结果 -->
<template><TreeMemberForm kind="add" /></template>
<script setup>import TreeMemberForm from '@/components/tree/TreeMemberForm.vue'</script>
+3 -3
View File
@@ -1,3 +1,3 @@
<!-- 页面编号T-05用途编辑成员资料 -->
<template><ModulePage page-id="t05" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号T-05用途编辑成员资料与保存结果 -->
<template><TreeMemberForm kind="edit" /></template>
<script setup>import TreeMemberForm from '@/components/tree/TreeMemberForm.vue'</script>
+3 -3
View File
@@ -1,3 +1,3 @@
<!-- 页面编号T-06用途编辑亲属关系冲突提示 -->
<template><ModulePage page-id="t06" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号T-06用途编辑亲属关系冲突提示与处理 -->
<template><TreeMemberForm kind="relation" /></template>
<script setup>import TreeMemberForm from '@/components/tree/TreeMemberForm.vue'</script>
+72 -3
View File
@@ -1,3 +1,72 @@
<!-- 页面编号T-07用途成员目录与搜索 -->
<template><ModulePage page-id="t07" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号T-07用途成员目录搜索空态与失败状态 -->
<template>
<view class="directory-page" :class="{ 'directory-state--list': directoryState === 'list', 'directory-state--empty': directoryState === 'empty', 'directory-state--error': directoryState === 'error' }">
<image class="directory-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="directory-page__header"><PageHeader title="成员目录" /></view>
<view class="directory-search">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<input v-model="keyword" placeholder="按姓名、字辈或支系查找" placeholder-class="directory-placeholder" @confirm="searchMembers" />
<text @click="searchMembers">查找</text>
</view>
<view class="directory-content">
<template v-if="directoryState === 'list'">
<view class="directory-summary"><text>家谱成员</text><text>{{ filteredMembers.length }} · 按世代排列</text></view>
<view v-for="item in filteredMembers" :key="item.id" class="directory-card" @click="openMember(item)">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="directory-card__copy"><text>{{ item.name }}</text><text> {{ item.generation }} · {{ item.generationName }} · {{ item.branch }}</text><text>{{ item.note }}</text></view>
</view>
</template>
<view v-else class="directory-state-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text>{{ directoryState === 'empty' ? '没有找到相关成员' : '成员目录暂不可用' }}</text><text>{{ directoryState === 'empty' ? '换一个姓名、字辈或支系关键词再试。' : '请从当前家谱重新进入,成员资料不会受到影响。' }}</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'
const genealogyId = ref('')
const keyword = ref('')
const directoryState = ref('loading')
const members = [
{ id: 101, name: '汤文远', generation: 12, generationName: '文字辈', branch: '主支', note: '始祖 · 档案完整' },
{ id: 102, name: '汤正国', generation: 13, generationName: '正字辈', branch: '长房', note: '家谱管理员' },
{ id: 103, name: '汤正华', generation: 13, generationName: '正字辈', branch: '二房', note: '资料待补充' }
]
const filteredMembers = computed(() => {
const value = keyword.value.trim()
if (!value) return members
return members.filter((item) => `${item.name}${item.generationName}${item.branch}`.includes(value))
})
onLoad((query) => { genealogyId.value = query.genealogyId || ''; directoryState.value = query.state === 'empty' ? 'empty' : query.state === 'error' ? 'error' : 'list' })
const searchMembers = () => { directoryState.value = filteredMembers.value.length ? 'list' : 'empty' }
const openMember = (item) => uni.navigateTo({ url: `/pages/tree/t03-member-profile?genealogyId=${genealogyId.value}&personId=${item.id}` })
</script>
<style scoped lang="scss">
.directory-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.directory-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
.directory-page__header, .directory-search, .directory-content { position: relative; z-index: 2; }
.directory-search { width: calc(100% - 48rpx); height: 84rpx; margin: 20rpx auto 0; }
.directory-search image { position: absolute; inset: 0; width: 100%; height: 100%; }
.directory-search input { position: absolute; top: 0; right: 110rpx; bottom: 0; left: 26rpx; z-index: 1; height: 84rpx; color: $ink; font-size: 22rpx; line-height: 84rpx; }
.directory-search > text { position: absolute; top: 29rpx; right: 28rpx; z-index: 1; color: $brand-red; font-size: 22rpx; font-weight: 700; }
.directory-placeholder { color: #a79884; }
.directory-content { padding: 22rpx 24rpx 50rpx; }
.directory-summary { display: flex; justify-content: space-between; margin: 0 8rpx 18rpx; }
.directory-summary text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 29rpx; font-weight: 700; }
.directory-summary text:last-child { color: $ink-muted; font-size: 20rpx; }
.directory-card, .directory-state-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 220rpx; margin-bottom: 16rpx; }
.directory-card > image, .directory-state-card > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.directory-card__copy { position: absolute; inset: 20% 9%; z-index: 1; }
.directory-card__copy text { display: block; }
.directory-card__copy text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 29rpx; font-weight: 700; }
.directory-card__copy text:nth-child(2) { margin-top: 10rpx; color: $ink-muted; font-size: 21rpx; }
.directory-card__copy text:last-child { margin-top: 9rpx; color: $brand-red; font-size: 19rpx; }
.directory-state-card { margin-top: 70rpx; }
.directory-state-card > view { position: absolute; inset: 24% 12%; z-index: 1; text-align: center; }
.directory-state-card text { display: block; }
.directory-state-card text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 30rpx; font-weight: 700; }
.directory-state-card text:last-child { margin-top: 14rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.5; }
</style>
+67 -3
View File
@@ -1,3 +1,67 @@
<!-- 页面编号T-08用途成员离世隐私隐藏与无权限状态 -->
<template><ModulePage page-id="t08" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号T-08用途成员隐私离世纪念与无权限状态说明 -->
<template>
<view class="member-status-page" :class="{
'member-status--privacy': statusState === 'privacy',
'member-status--deceased': statusState === 'deceased',
'member-status--forbidden': statusState === 'forbidden'
}">
<image class="member-status-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="member-status-page__header"><PageHeader title="成员状态" /></view>
<view class="status-tabs">
<view v-for="item in tabs" :key="item.value" class="status-tab" @click="statusState = item.value">
<image :src="statusState === item.value ? '/static/assets/foundation/opaque/a01-primary-button.png' : '/static/assets/foundation/opaque/a01-secondary-button.png'" mode="scaleToFill" />
<text :class="{ 'status-tab__text--active': statusState === item.value }">{{ item.label }}</text>
</view>
</view>
<view class="status-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="status-card__copy">
<text>{{ activeStatus.eyebrow }}</text><text>{{ activeStatus.title }}</text><text>{{ activeStatus.copy }}</text>
</view>
</view>
<view class="status-guidance">
<image src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view>
<text>{{ activeStatus.guideTitle }}</text>
<text v-for="line in activeStatus.guides" :key="line">{{ line }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
const statusState = ref('privacy')
const tabs = [{ value: 'privacy', label: '隐私' }, { value: 'deceased', label: '纪念' }, { value: 'forbidden', label: '无权限' }]
const states = {
privacy: { eyebrow: '隐私成员', title: '敏感资料只向授权成员展示', copy: '姓名可保留在世系位置,出生信息、联系方式和生平内容根据权限隐藏。', guideTitle: '隐私展示原则', guides: ['本人可以查看和维护自己的完整档案', '谱主可协助处理身份与世系关系', '普通成员只看到被允许公开的内容'] },
deceased: { eyebrow: '离世成员', title: '在世系中保留温和的纪念状态', copy: '离世不会删除成员关系;档案可继续记录生平、影像和家人的追思内容。', guideTitle: '纪念资料范围', guides: ['生卒年月与世系关系继续保留', '生平内容由有权限家人共同维护', '敏感资料仍遵守原有隐私设置'] },
forbidden: { eyebrow: '访问受限', title: '当前账号没有查看该档案的权限', copy: '为保护家人隐私,页面不会展示被隐藏的字段,也不会提供绕过权限的入口。', guideTitle: '如何申请查看', guides: ['先确认已经加入对应家谱', '联系谱主说明亲属关系和用途', '权限变更后重新进入成员档案'] }
}
const activeStatus = computed(() => states[statusState.value])
onLoad((query) => { statusState.value = states[query.state] ? query.state : 'privacy' })
</script>
<style scoped lang="scss">
.member-status-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.member-status-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; }
.member-status-page__header, .status-tabs, .status-card, .status-guidance { position: relative; z-index: 2; }
.status-tabs { display: flex; gap: 10rpx; padding: 22rpx 24rpx 0; }
.status-tab { position: relative; width: calc(33.333% - 7rpx); height: 62rpx; }
.status-tab image { position: absolute; inset: 0; width: 100%; height: 100%; }
.status-tab text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: $ink; font-size: 21rpx; font-weight: 700; }
.status-tab .status-tab__text--active { color: #fff9ed; }
.status-card { width: calc(100% - 48rpx); height: calc((100vw - 24px) * .34286); min-height: 224rpx; margin: 24rpx auto 0; }
.status-card > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.status-card__copy { position: absolute; inset: 18% 10%; z-index: 1; text-align: center; }
.status-card__copy text { display: block; }
.status-card__copy text:first-child { color: $brand-red; font-size: 20rpx; letter-spacing: 3rpx; }
.status-card__copy text:nth-child(2) { margin-top: 7rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 29rpx; font-weight: 700; }
.status-card__copy text:last-child { margin-top: 10rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.status-guidance { width: calc(100% - 48rpx); height: min(400px, calc((100vw - 24px) * .9)); margin: 18rpx auto 0; }
.status-guidance > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.status-guidance > view { position: absolute; inset: 12% 11%; z-index: 1; }
.status-guidance text { display: block; color: $ink-muted; font-size: 21rpx; line-height: 1.65; }
.status-guidance text:first-child { margin-bottom: 18rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
.status-guidance text:not(:first-child) { margin-top: 12rpx; }
</style>