修改完成55%
This commit is contained in:
@@ -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>主支 · 第 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="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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user