修改完成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
+26 -61
View File
@@ -1,69 +1,34 @@
<!-- 页面编号F-01用途家族动态首页 -->
<!-- 页面编号F-01用途家族动态首页列表空态与失败状态 -->
<template>
<view class="page-shell family-page">
<view class="family-top"><view><text class="top-kicker">家族共建</text><text class="page-name">家族记事</text></view><text class="publish" @click="publishFeed">发布动态</text></view>
<view v-if="loadError" class="error-copy">{{ loadError }}</view>
<view class="content-rail"><view v-for="item in sections" :key="item.type" class="rail-item" @click="openSection(item.type)"><text>{{ item.name }}</text><text>{{ item.detail }}</text></view></view>
<view class="feed-heading"><text class="section-title">家族近况</text><text class="feed-filter">全部动态</text></view>
<view class="feed-list"><view v-for="feed in feeds" :key="feed.id" class="feed-card paper-card"><view class="feed-meta"><text>{{ feed.type || feed.feedType || '动态' }}</text><text>{{ feed.date || feed.createTime || '' }}</text></view><text class="feed-title">{{ feed.title || '家族动态' }}</text><text class="feed-content">{{ feed.content || feed.feedContent }}</text><text class="feed-author">发布人:{{ feed.author || feed.creatorName || '族人' }}</text></view><view v-if="!feeds.length && !loadError" class="empty-copy">暂未发布家族动态</view></view>
<view class="family-page" :class="{ 'feed-state--list': feedState === 'list', 'feed-state--empty': feedState === 'empty', 'feed-state--error': feedState === 'error' }">
<image class="family-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="family-page__header"><PageHeader title="家族动态" action="发布" @action="toPublish" /></view>
<view class="feed-content">
<view class="feed-heading"><text>汤氏家族圈</text><text>家宴通知与共同记忆</text></view>
<scroll-view class="feed-shortcuts" scroll-x :show-scrollbar="false">
<view v-for="item in shortcuts" :key="item.key" class="feed-shortcut" @click="openSection(item.key)"><image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" /><text>{{ item.label }}</text></view>
</scroll-view>
<template v-if="feedState === 'list'">
<view v-for="item in feeds" :key="item.id" class="feed-card" @click="openDetail(item)">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text>{{ item.tag }} · {{ item.time }}</text><text>{{ item.title }}</text><text>{{ item.content }}</text><text>发布人{{ item.author }}</text></view>
</view>
</template>
<view v-else class="feed-state-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text>{{ feedState === 'empty' ? '还没有家族动态' : '家族动态暂不可用' }}</text><text>{{ feedState === 'empty' ? '发布第一条通知、家宴记录或家族故事。' : '请稍后重新进入,已有内容不会受到影响。' }}</text></view>
</view>
<view class="feed-action" @click="feedState === 'error' ? feedState = 'list' : toPublish()"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ feedState === 'error' ? '重新查看' : '发布家族动态' }}</text></view>
</view>
<AppTabbar active="family" />
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import AppTabbar from '@/components/AppTabbar.vue'
import { appApi } from '@/utils/api.js'
import { genealogyContext } from '@/utils/genealogy-context.js'
const feeds = ref([])
const loadError = ref('')
const genealogyId = ref('')
const sections = [
{ type: 'article', name: '谱文', detail: '族史与家训' },
{ type: 'album', name: '相册', detail: '珍存旧影' },
{ type: 'ceremony', name: '祭祀', detail: '慎终追远' },
{ type: 'record', name: '族务', detail: '共同记事' }
]
onMounted(async () => {
genealogyId.value = genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) { loadError.value = '请先从“我的家谱”选择一个家谱。'; return }
try { feeds.value = await appApi.getFeeds(genealogyId.value) } catch (error) { loadError.value = error.message || '家族动态加载失败。' }
})
const openSection = (type) => {
const routes = {
article: '/pages/family/f04-article-list',
album: '/pages/family/f07-album-list',
ceremony: '/pages/records/r05-ritual-list',
record: '/pages/records/r10-memo-list'
}
uni.navigateTo({ url: `${routes[type]}?genealogyId=${genealogyId.value}` })
}
const publishFeed = () => {
if (!genealogyId.value) { uni.showToast({ title: '请先选择家谱', icon: 'none' }); return }
uni.navigateTo({ url: `/pages/family/f02-publish-feed?type=feed&genealogyId=${genealogyId.value}` })
}
import { ref } from 'vue';import { onLoad } from '@dcloudio/uni-app';import PageHeader from '@/components/PageHeader.vue';import AppTabbar from '@/components/AppTabbar.vue';import{genealogyContext}from'@/utils/genealogy-context.js'
const genealogyId=ref('');const feedState=ref('loading');const feeds=[{id:1,tag:'团圆记忆',time:'今天 10:24',title:'端午家宴',content:'今年端午全家相聚,留下了许多温暖照片。',author:'汤正国'},{id:2,tag:'家族通知',time:'昨天 18:02',title:'修谱资料征集',content:'请家人补充老照片中的人物姓名与拍摄时间。',author:'谱主'}];const shortcuts=[{key:'articles',label:'谱文'},{key:'albums',label:'相册'},{key:'rituals',label:'礼仪'},{key:'memos',label:'备忘'}]
onLoad(query=>{genealogyId.value=query.genealogyId||genealogyContext.getCurrentGenealogyId()||'';feedState.value=query.state==='empty'?'empty':query.state==='error'?'error':'list'})
const toPublish=()=>uni.navigateTo({url:`/pages/family/f02-publish-feed?genealogyId=${genealogyId.value}`});const openDetail=item=>uni.navigateTo({url:`/pages/family/f03-feed-detail?feedId=${item.id}`});const openSection=key=>{const routes={articles:'/pages/family/f04-article-list',albums:'/pages/family/f07-album-list',rituals:'/pages/records/r05-ritual-list',memos:'/pages/records/r10-memo-list'};uni.navigateTo({url:routes[key]})}
</script>
<style scoped lang="scss">
.family-page { padding: 42rpx 28rpx 178rpx; }
.family-top { display: flex; align-items: flex-start; justify-content: space-between; }
.top-kicker { display: block; color: $brand-red; font-size: 22rpx; letter-spacing: 5rpx; }
.page-name { display: block; margin-top: 10rpx; color: $ink; font-family: serif; font-size: 52rpx; font-weight: 700; letter-spacing: 5rpx; }
.publish { margin-top: 22rpx; color: $brand-red; font-size: 26rpx; }
.error-copy { margin-top: 24rpx; color: $brand-red; font-size: 24rpx; }
.content-rail { display: flex; gap: 12rpx; margin-top: 34rpx; overflow-x: auto; }
.rail-item { min-width: 150rpx; padding: 18rpx; border: 1rpx solid $gold-soft; background: #fffaf0; }
.rail-item text { display: block; color: $ink; font-size: 28rpx; font-weight: 700; }
.rail-item text + text { margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; font-weight: 400; }
.feed-heading { display: flex; justify-content: space-between; align-items: center; margin-top: 38rpx; }
.feed-filter { color: $brand-red; font-size: 24rpx; }
.feed-list { display: flex; flex-direction: column; gap: 18rpx; margin-top: 20rpx; }
.feed-card { padding: 28rpx; }
.feed-meta { display: flex; justify-content: space-between; color: $gold; font-size: 21rpx; }
.feed-title { display: block; margin-top: 16rpx; color: $ink; font-size: 32rpx; font-weight: 700; }
.feed-content { display: block; margin-top: 12rpx; color: $ink-muted; font-size: 25rpx; line-height: 1.65; }
.feed-author { display: block; margin-top: 16rpx; color: #9a8c78; font-size: 21rpx; }
.empty-copy { padding: 80rpx 0; color: $ink-muted; font-size: 26rpx; text-align: center; }
.family-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.family-page__paper{position:absolute;inset:0;z-index:0;width:100%;height:100%;pointer-events:none}.family-page__header,.feed-content{position:relative;z-index:2}.feed-content{padding:24rpx 24rpx 190rpx}.feed-heading text{display:block}.feed-heading text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:31rpx;font-weight:700}.feed-heading text:last-child{margin-top:6rpx;color:$ink-muted;font-size:20rpx}.feed-shortcuts{width:100%;margin-top:15rpx;white-space:nowrap}.feed-shortcut{position:relative;display:inline-block;width:150rpx;height:58rpx;margin-right:10rpx}.feed-shortcut image{position:absolute;inset:0;width:100%;height:100%}.feed-shortcut text{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;height:100%;color:$ink;font-size:20rpx;font-weight:700}.feed-card,.feed-state-card{position:relative;width:100%;height:calc((100vw - 24px)*.34286);min-height:220rpx;margin-top:17rpx}.feed-card>image,.feed-state-card>image,.feed-action image{position:absolute;inset:0;width:100%;height:100%}.feed-card>view{position:absolute;inset:16% 9%;z-index:1}.feed-card text{display:block}.feed-card text:first-child{color:$brand-red;font-size:18rpx;letter-spacing:2rpx}.feed-card text:nth-child(2){margin-top:5rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:28rpx;font-weight:700}.feed-card text:nth-child(3){margin-top:6rpx;color:$ink-muted;font-size:19rpx}.feed-card text:last-child{margin-top:6rpx;color:#917b62;font-size:17rpx}.feed-state-card{margin-top:70rpx}.feed-state-card>view{position:absolute;inset:25% 12%;z-index:1;text-align:center}.feed-state-card text{display:block}.feed-state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:30rpx;font-weight:700}.feed-state-card text:last-child{margin-top:13rpx;color:$ink-muted;font-size:20rpx}.feed-action{position:relative;width:100%;height:76rpx;margin-top:19rpx}.feed-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}
</style>
+4 -36
View File
@@ -1,36 +1,4 @@
<!-- 页面编号F-02用途发布家族动态 -->
<template>
<view class="page-shell editor-page">
<PageHeader title="发布家族动态" />
<view class="form-paper"><view class="section-title">记录此刻</view><text class="form-note">发布后当前家谱的成员可以在家族圈查看</text><textarea v-model="feedContent" maxlength="300" placeholder="写下通知、活动、家族故事或照片说明" placeholder-class="placeholder" /></view>
<view class="bottom-action"><button class="primary-button" :loading="isSubmitting" :disabled="isSubmitting" @click="submit">发布动态</button></view>
</view>
</template>
<script setup>
import { 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 genealogyId = ref('')
const feedContent = ref('')
const isSubmitting = ref(false)
onLoad((query) => { genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId() })
const submit = async () => {
if (!genealogyId.value) { uni.showToast({ title: '未找到当前家谱', icon: 'none' }); return }
if (!feedContent.value.trim()) { uni.showToast({ title: '请填写动态内容', icon: 'none' }); return }
isSubmitting.value = true
try { await appApi.createFeed(genealogyId.value, { feedType: 'text', feedContent: feedContent.value.trim() }); uni.navigateBack() } catch (error) { uni.showToast({ title: error.message || '发布失败,请稍后重试', icon: 'none' }) } finally { isSubmitting.value = false }
}
</script>
<style scoped lang="scss">
.editor-page { padding-bottom: 136rpx; }
.form-paper { margin: 28rpx; padding: 34rpx 30rpx; border: 1rpx solid $gold-soft; border-radius: 22rpx; background: $paper-white; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.65; }
textarea { width: 100%; height: 360rpx; margin-top: 30rpx; color: $ink; font-size: 28rpx; line-height: 1.7; }
.placeholder { color: #b7aa97; }
.bottom-action { position: fixed; right: 0; bottom: 0; left: 0; padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom)); background: #fffaf0; border-top: 1rpx solid $gold-soft; }
</style>
<!-- 页面编号F-02用途发布家族动态与提交结果 -->
<template><view class="publish-page" :class="{'publish-state--form':publishState==='form','publish-state--success':publishState==='success','publish-state--error':publishState==='error'}"><image class="publish-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill"/><view class="publish-page__header"><PageHeader title="发布动态"/></view><view class="publish-panel"><image class="publish-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill"/><view v-if="publishState==='form'" class="publish-form"><text>记录此刻</text><text>分享通知、活动、家族故事或一段共同记忆。</text><view class="publish-field"><image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill"/><textarea v-model="content" maxlength="300" placeholder="写下想对家人说的话" placeholder-class="publish-placeholder"/></view><view class="publish-action" @click="submit"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill"/><text>发布动态</text></view></view><view v-else class="publish-result"><text>{{publishState==='success'?'动态已发布':'动态未发布'}}</text><text>{{publishState==='success'?'家人现在可以在家族圈看到这条记录。':'请检查内容后重新发布,当前文字仍保留在页面中。'}}</text><view class="publish-action" @click="publishState='form'"><image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill"/><text>{{publishState==='success'?'继续发布':'重新填写'}}</text></view></view></view></view></template>
<script setup>import{ref}from'vue';import{onLoad}from'@dcloudio/uni-app';import PageHeader from'@/components/PageHeader.vue';const content=ref('');const publishState=ref('form');onLoad(query=>{publishState.value=query.state==='success'?'success':query.state==='error'?'error':'form'});const submit=()=>{if(!content.value.trim()){uni.showToast({title:'请先写下动态内容',icon:'none'});return}publishState.value='success'}</script>
<style scoped lang="scss">.publish-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.publish-page__paper{position:absolute;inset:0;z-index:0;width:100%;height:100%;pointer-events:none}.publish-page__header,.publish-panel{position:relative;z-index:2}.publish-panel{width:calc(100% - 32rpx);height:min(640px,calc((100vw - 16px)*1.48));margin:18rpx auto 0}.publish-panel__skin{position:absolute;inset:0;width:100%;height:100%}.publish-form,.publish-result{position:absolute;inset:9% 9%}.publish-form>text,.publish-result>text{display:block}.publish-form>text:first-child,.publish-result>text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:36rpx;font-weight:700}.publish-form>text:nth-child(2),.publish-result>text:nth-child(2){margin-top:12rpx;color:$ink-muted;font-size:21rpx;line-height:1.6}.publish-field{position:relative;height:250rpx;margin-top:24rpx}.publish-field image,.publish-action image{position:absolute;inset:0;width:100%;height:100%}.publish-field textarea{position:absolute;inset:25rpx;z-index:1;width:auto;height:200rpx;color:$ink;font-size:22rpx;line-height:1.7}.publish-placeholder{color:#a79884}.publish-action{position:relative;width:100%;height:78rpx;margin-top:24rpx}.publish-action text{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;height:100%;color:#fff9ed;font-size:25rpx;font-weight:700}.publish-result{top:30%;text-align:center}.publish-result .publish-action{width:420rpx;max-width:100%;margin:30rpx auto 0}</style>
+2 -2
View File
@@ -278,7 +278,7 @@ const shortcuts = [
];
const openGenealogy = (genealogy) =>
uni.navigateTo({ url: `/pages/genealogy/g05-genealogy-overview?id=${genealogy.id}` });
uni.navigateTo({ url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${genealogy.id}` });
const createGenealogy = () =>
uni.navigateTo({ url: "/pages/genealogy/g03-create-genealogy" });
const applyToJoin = () => uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
@@ -288,7 +288,7 @@ const toNotifications = () =>
const openShortcut = (key) => {
const paths = {
tree: "/pages/tree/t01-tree-overview",
members: `/pages/genealogy/g05-genealogy-overview?id=${currentGenealogy.value.id}`,
members: `/pages/genealogy/g05-genealogy-overview?genealogyId=${currentGenealogy.value.id}`,
poem: "/pages/genealogy/g12-generation-poems",
applications: "/pages/genealogy/g10-application-review",
};
+123 -50
View File
@@ -1,33 +1,71 @@
<!-- 页面编号G-05用途家谱总览 -->
<!-- 页面编号G-05用途家谱总览与加载空数据失败状态 -->
<template>
<view class="page-shell detail-page">
<PageHeader title="家谱总览" action="管理" @action="toSettings" />
<view v-if="genealogy" class="genealogy-hero">
<text class="hero-kicker">{{ genealogy.hall || '堂号待补' }} · {{ genealogy.location || '所在地待补' }}</text>
<text class="hero-title">{{ genealogy.name }}</text>
<text class="hero-motto">{{ genealogy.motto || '敦亲睦族,敬祖传家。' }}</text>
<view class="hero-stats"><text> {{ genealogy.memberCount || 0 }} </text><text>已激活 {{ genealogy.activeCount || 0 }} </text><text>{{ genealogy.visibility || '仅成员可见' }}</text></view>
<view class="overview-page">
<image class="overview-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="overview-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="overview-page__header">
<PageHeader title="家谱总览" :action="overviewState === 'ready' ? '管理' : ''" @action="toSettings" />
</view>
<view v-else class="state-copy">{{ loadError || '正在载入家谱…' }}</view>
<template v-if="genealogy">
<view class="core-actions paper-card">
<view class="section-title">家谱核心</view>
<view class="action-grid">
<view class="action-cell primary-cell" @click="toTree"><text>世系树</text><text>查看家脉关系</text></view>
<view class="action-cell" @click="toFirstPerson"><text>录入族人</text><text>从首代开始完善</text></view>
<view class="action-cell" @click="toGenerationPoems"><text>字辈谱</text><text>传承行辈次序</text></view>
<view class="action-cell" @click="toApplications"><text>入谱审核</text><text>处理加入申请</text></view>
<view class="overview-surface">
<image class="overview-surface__skin" src="/static/assets/modules/genealogy/opaque/g05-overview-surface.png" mode="scaleToFill" />
<template v-if="overviewState === 'ready' && genealogy">
<view class="overview-ready">
<view class="overview-hero">
<text class="overview-hero__kicker">{{ genealogy.hall || '堂号待补' }} · {{ genealogy.location || '所在地待补' }}</text>
<text class="overview-hero__title">{{ genealogy.name }}</text>
<text class="overview-hero__motto">{{ genealogy.motto || '敦亲睦族,敬祖传家。' }}</text>
<view class="overview-hero__stats">
<text> {{ genealogy.memberCount || 0 }} </text>
<text>已激活 {{ genealogy.activeCount || 0 }} </text>
<text>{{ genealogy.visibility || '仅成员可见' }}</text>
</view>
</view>
<view class="overview-actions">
<view class="overview-action overview-action--tree" @click="toTree">
<text class="overview-action__title">世系树</text><text>查看家脉关系</text>
</view>
<view class="overview-action overview-action--ancestor" @click="toFirstPerson">
<text class="overview-action__title">录入族人</text><text>从首代开始完善</text>
</view>
<view class="overview-action overview-action--poem" @click="toGenerationPoems">
<text class="overview-action__title">字辈谱</text><text>传承行辈次序</text>
</view>
<view class="overview-action overview-action--review" @click="toApplications">
<text class="overview-action__title">入谱审核</text><text>处理加入申请</text>
</view>
</view>
<view class="overview-family" @click="toFamily">
<text>家族近况</text><text>进入家族圈查看动态</text><text>查看</text>
</view>
<view class="overview-note">
<text class="overview-note__title">家谱资料仅向家人开放</text>
<text>公开范围访问说明与管理权由家谱管理员在设置中维护</text>
</view>
</view>
</template>
<view v-else class="overview-state-panel" :class="{
'overview-state--loading': overviewState === 'loading',
'overview-state--empty': overviewState === 'empty',
'overview-state--error': overviewState === 'error'
}">
<text class="overview-state-panel__title">{{ stateTitle }}</text>
<text class="overview-state-panel__copy">{{ stateCopy }}</text>
<view v-if="overviewState !== 'loading'" class="overview-state-panel__action" @click="overviewState === 'error' ? reloadOverview() : toGenealogies()">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>{{ overviewState === 'error' ? '重新查看' : '返回我的家谱' }}</text>
</view>
</view>
<view class="family-summary"><text class="section-title">家族近况</text><view class="summary-row" @click="toFamily"><text>进入家族圈查看动态</text><text>查看</text></view></view>
<view class="invite-block"><text class="invite-title">邀亲人共建家谱</text><text class="invite-copy">族人申请加入后由创建者审核确认</text><button class="primary-button" @click="toJoinApplication">邀请亲人</button></view>
</template>
</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'
@@ -35,52 +73,87 @@ import { genealogyContext } from '@/utils/genealogy-context.js'
const genealogy = ref(null)
const genealogyId = ref('')
const overviewState = ref('loading')
const loadError = ref('')
const loadGenealogy = async (id) => {
genealogyId.value = id || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) {
loadError.value = '未找到当前家谱,请从“我的家谱”重新进入。'
const stateTitle = computed(() => ({
loading: '正在展开家谱…',
empty: '还没有可查看的家谱',
error: '家谱暂时无法打开'
}[overviewState.value] || ''))
const stateCopy = computed(() => loadError.value || ({
loading: '请稍候,正在读取家谱概览。',
empty: '请从“我的家谱”选择一部家谱后再进入。',
error: '可能是网络波动、家谱不存在或当前账号无访问权限。'
}[overviewState.value] || ''))
const loadGenealogy = async (query = {}) => {
overviewState.value = 'loading'
loadError.value = ''
genealogy.value = null
genealogyId.value = query.genealogyId || genealogyId.value || genealogyContext.getCurrentGenealogyId()
if (query.state === 'empty' || !genealogyId.value) {
overviewState.value = 'empty'
return
}
if (query.state === 'error') {
overviewState.value = 'error'
return
}
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
genealogy.value = await appApi.getOverview(genealogyId.value)
if (!genealogy.value) loadError.value = '家谱不存在或无权访问。'
overviewState.value = genealogy.value ? 'ready' : 'error'
if (!genealogy.value) loadError.value = '家谱不存在或当前账号无权访问。'
} catch (error) {
loadError.value = error.message || '家谱加载失败。'
loadError.value = error.message || '家谱加载失败,请稍后重试。'
overviewState.value = 'error'
}
}
onLoad((query) => loadGenealogy(query.id))
onLoad(loadGenealogy)
const reloadOverview = () => loadGenealogy({ genealogyId: genealogyId.value })
const toGenealogies = () => uni.reLaunch({ url: '/pages/genealogy/g01-my-genealogies' })
const toTree = () => uni.navigateTo({ url: `/pages/tree/t01-tree-overview?genealogyId=${genealogyId.value}` })
const toFirstPerson = () => uni.navigateTo({ url: `/pages/genealogy/g04-first-ancestor?genealogyId=${genealogyId.value}` })
const toFirstPerson = () => uni.navigateTo({ url: `/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=${genealogyId.value}` })
const toFamily = () => uni.reLaunch({ url: '/pages/family/f01-family-feed' })
const toApplications = () => uni.navigateTo({ url: `/pages/genealogy/g10-application-review?genealogyId=${genealogyId.value}` })
const toSettings = () => uni.navigateTo({ url: `/pages/genealogy/g11-genealogy-settings?genealogyId=${genealogyId.value}` })
const toGenerationPoems = () => uni.navigateTo({ url: `/pages/genealogy/g12-generation-poems?genealogyId=${genealogyId.value}` })
const toJoinApplication = () => uni.navigateTo({ url: `/pages/genealogy/g08-join-application?genealogyId=${genealogyId.value}` })
</script>
<style scoped lang="scss">
.detail-page { padding-bottom: 48rpx; }
.genealogy-hero { padding: 48rpx 34rpx 38rpx; background: $navy; color: #fff8ec; }
.hero-kicker { display: block; color: #e6c57f; font-size: 22rpx; letter-spacing: 4rpx; }
.hero-title { display: block; margin-top: 20rpx; font-family: serif; font-size: 52rpx; font-weight: 700; letter-spacing: 4rpx; }
.hero-motto { display: block; margin-top: 16rpx; color: #eadcbf; font-size: 25rpx; }
.hero-stats { display: flex; flex-wrap: wrap; gap: 12rpx 20rpx; margin-top: 28rpx; color: #dac797; font-size: 22rpx; }
.state-copy { padding: 80rpx 40rpx; color: $ink-muted; font-size: 27rpx; text-align: center; }
.core-actions { margin: -18rpx 28rpx 0; padding: 30rpx; }
.action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16rpx; margin-top: 26rpx; }
.action-cell { min-height: 122rpx; padding: 20rpx; box-sizing: border-box; border: 1rpx solid #e7d9be; background: #fbf5e9; }
.action-cell text { display: block; color: $ink; font-size: 28rpx; font-weight: 700; }
.action-cell text + text { margin-top: 10rpx; color: $ink-muted; font-size: 21rpx; font-weight: 400; }
.primary-cell { border-color: $brand-red; background: #f8ede3; }
.primary-cell text:first-child { color: $brand-red; }
.family-summary { margin: 38rpx 28rpx 0; }
.summary-row { display: flex; justify-content: space-between; margin-top: 14rpx; padding: 24rpx 6rpx; border-bottom: 1rpx solid #dfd1b6; color: $ink-muted; font-size: 25rpx; }
.summary-row text:last-child { color: $brand-red; }
.invite-block { margin: 42rpx 28rpx 0; padding: 32rpx; border: 1rpx solid $gold; background: #efe0c0; }
.invite-title { display: block; color: $brand-red; font-family: serif; font-size: 37rpx; font-weight: 700; }
.invite-copy { display: block; margin: 14rpx 0 24rpx; color: $ink-muted; font-size: 23rpx; }
.overview-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.overview-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
.overview-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .13; pointer-events: none; }
.overview-page__header { position: relative; z-index: 3; }
.overview-surface { position: relative; z-index: 2; width: 100%; height: min(483px, calc(100vw * 1.3422)); }
.overview-surface__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.overview-ready { position: absolute; inset: 0; }
.overview-hero { position: absolute; top: 6.5%; right: 8%; left: 8%; color: #fff8ec; }
.overview-hero__kicker { display: block; color: #dec483; font-size: 22rpx; letter-spacing: 3rpx; }
.overview-hero__title { display: block; margin-top: 12rpx; font-family: 'STKaiti', 'KaiTi', serif; font-size: 46rpx; font-weight: 700; letter-spacing: 4rpx; }
.overview-hero__motto { display: block; margin-top: 6rpx; color: #e9dcc1; font-size: 23rpx; }
.overview-hero__stats { display: flex; gap: 18rpx; margin-top: 12rpx; color: #dbc990; font-size: 22rpx; }
.overview-actions { position: absolute; top: 29.2%; right: 7.2%; left: 7.2%; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 3.4% 3%; height: 36.2%; }
.overview-action { display: flex; flex-direction: column; justify-content: center; min-width: 0; padding: 0 22rpx; box-sizing: border-box; color: $ink-muted; font-size: 22rpx; }
.overview-action__title { margin-bottom: 9rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 30rpx; font-weight: 700; }
.overview-action--tree .overview-action__title { color: $brand-red; }
.overview-family { position: absolute; top: 70.2%; right: 7.5%; left: 7.5%; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 16rpx; color: $ink-muted; font-size: 22rpx; }
.overview-family text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 28rpx; font-weight: 700; }
.overview-family text:last-child { color: $brand-red; }
.overview-note { position: absolute; top: 80.5%; right: 8.5%; left: 8.5%; display: flex; flex-direction: column; justify-content: center; height: 15%; color: $ink-muted; font-size: 22rpx; line-height: 1.6; }
.overview-note__title { margin-bottom: 8rpx; color: $brand-red; font-family: 'STKaiti', 'KaiTi', serif; font-size: 28rpx; font-weight: 700; }
.overview-state-panel { position: absolute; top: 29%; right: 8%; left: 8%; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 48%; padding: 0 40rpx; box-sizing: border-box; text-align: center; }
.overview-state-panel__title { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 34rpx; font-weight: 700; }
.overview-state-panel__copy { max-width: 480rpx; margin-top: 18rpx; color: $ink-muted; font-size: 23rpx; line-height: 1.7; }
.overview-state-panel__action { position: relative; width: 300rpx; height: 76rpx; margin-top: 30rpx; }
.overview-state-panel__action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.overview-state-panel__action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 25rpx; font-weight: 700; }
@media (min-width: 400px) {
.overview-hero { top: 7%; }
.overview-action { padding: 0 28rpx; }
}
</style>
+131 -3
View File
@@ -1,3 +1,131 @@
<!-- 页面编号G-08用途申请加入家谱与提交成功状态 -->
<template><ModulePage page-id="g08" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号G-08用途申请加入家谱与提交成功/失败状态 -->
<template>
<view class="join-page">
<image class="join-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="join-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="join-page__header"><PageHeader title="申请加入家谱" /></view>
<view class="join-panel" :class="{
'join-state--form': joinState === 'form',
'join-state--success': joinState === 'success',
'join-state--error': joinState === 'error'
}">
<image class="join-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view v-if="joinState === 'form'" class="join-form">
<text class="join-form__eyebrow">公开家谱入谱申请</text>
<text class="join-form__title">申请加入 {{ genealogyName }}</text>
<text class="join-form__copy">请填写真实身份和亲属关系管理员审核后会通过消息告知结果</text>
<view class="join-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>真实姓名</text><input v-model="form.realName" placeholder="请输入真实姓名" placeholder-class="join-placeholder" />
</view>
<view class="join-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>与家谱关系</text><input v-model="form.relation" placeholder="例如:汤正华堂侄" placeholder-class="join-placeholder" />
</view>
<view class="join-field join-field--message">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>申请说明</text><textarea v-model="form.message" maxlength="80" placeholder="补充祖居地、长辈姓名等核验信息" placeholder-class="join-placeholder" />
</view>
<text class="join-form__note">提交后可在我的申请中查看审核进度</text>
<view class="join-action" @click="submitJoin">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>{{ isSubmitting ? '正在提交…' : '提交申请' }}</text>
</view>
</view>
<view v-else class="join-result">
<text class="join-result__eyebrow">{{ joinState === 'success' ? '申请已送达' : '申请未提交' }}</text>
<text class="join-result__title">{{ joinState === 'success' ? '等待管理员核实亲属关系' : '暂时无法提交申请' }}</text>
<text class="join-result__copy">{{ resultCopy }}</text>
<view class="join-action" @click="joinState === 'success' ? toMyApplications() : retryForm()">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>{{ joinState === 'success' ? '查看我的申请' : '重新填写' }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
const genealogyId = ref('')
const genealogyName = ref('这部家谱')
const genealogyPreview = {
'1001': '汤氏家谱',
'1002': '汝南汤氏家谱'
}
const joinState = ref('form')
const isSubmitting = ref(false)
const errorMessage = ref('')
const form = reactive({ realName: '', relation: '', message: '' })
const resultCopy = computed(() => joinState.value === 'success'
? `${genealogyName.value}”的管理员会在核实后给出结果,请留意消息中心。`
: errorMessage.value || '请检查网络后重新填写;未成功提交的内容不会进入审核列表。')
onLoad((query) => {
genealogyId.value = query.genealogyId || ''
if (query.state === 'success') {
joinState.value = 'success'
return
}
if (!genealogyId.value) {
errorMessage.value = '没有找到要申请加入的家谱,请先返回公开家谱检索。'
joinState.value = 'error'
return
}
genealogyName.value = genealogyPreview[genealogyId.value] || '这部家谱'
})
const submitJoin = () => {
if (isSubmitting.value) return
if (!form.realName.trim() || !form.relation.trim()) {
uni.showToast({ title: '请填写真实姓名和亲属关系', icon: 'none' })
return
}
isSubmitting.value = true
setTimeout(() => {
joinState.value = 'success'
isSubmitting.value = false
}, 280)
}
const retryForm = () => { joinState.value = 'form'; errorMessage.value = '' }
const toMyApplications = () => uni.redirectTo({ url: '/pages/genealogy/g09-my-applications' })
</script>
<style scoped lang="scss">
.join-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.join-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
.join-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .13; pointer-events: none; }
.join-page__header { position: relative; z-index: 3; }
.join-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(590px, calc((100vw - 16px) * 1.337)); margin: 22rpx auto 0; }
.join-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.join-form { position: absolute; inset: 8.5% 8%; }
.join-form__eyebrow, .join-result__eyebrow { display: block; color: $brand-red; font-size: 23rpx; letter-spacing: 3rpx; }
.join-form__title, .join-result__title { display: block; margin-top: 12rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 36rpx; font-weight: 700; }
.join-form__copy, .join-result__copy { display: block; margin-top: 12rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.6; }
.join-field { position: relative; height: 92rpx; margin-top: 18rpx; }
.join-field > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.join-field > text { position: absolute; top: 31rpx; left: 24rpx; z-index: 1; color: $ink; font-size: 23rpx; font-weight: 700; }
.join-field input, .join-field textarea { position: absolute; top: 0; right: 20rpx; bottom: 0; left: 170rpx; z-index: 1; height: 92rpx; color: $ink; font-size: 23rpx; line-height: 92rpx; }
.join-field textarea { box-sizing: border-box; padding-top: 26rpx; line-height: 1.5; }
.join-placeholder { color: #a79884; }
.join-form__note { display: block; margin-top: 18rpx; color: $ink-muted; font-size: 21rpx; text-align: center; }
.join-action { position: relative; width: 100%; height: 82rpx; margin-top: 20rpx; }
.join-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.join-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 27rpx; font-weight: 700; letter-spacing: 3rpx; }
.join-result { position: absolute; top: 28%; right: 12%; left: 12%; text-align: center; }
.join-result__eyebrow { text-align: center; }
.join-result__copy { margin-top: 22rpx; }
.join-result .join-action { width: 420rpx; max-width: 100%; margin: 34rpx auto 0; }
@media (min-width: 400px) {
.join-panel { width: calc(100% - 48rpx); }
.join-form { right: 9%; left: 9%; }
}
</style>
+108 -3
View File
@@ -1,3 +1,108 @@
<!-- 页面编号G-09用途我的家谱申请记录 -->
<template><ModulePage page-id="g09" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号G-09用途我的家谱申请列表与空/失败状态 -->
<template>
<view class="application-page">
<image class="application-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="application-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="application-page__header"><PageHeader title="我的申请" /></view>
<view class="application-content" :class="{
'application-state--list': applicationState === 'list',
'application-state--empty': applicationState === 'empty',
'application-state--error': applicationState === 'error'
}">
<template v-if="applicationState === 'list'">
<view class="application-intro">
<text>入谱申请进度</text><text>审核结果会同步到消息中心</text>
</view>
<view v-for="item in applications" :key="item.id" class="application-card" @click="openApprovedGenealogy(item)">
<image class="application-card__skin" src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="application-card__body">
<text class="application-card__name">{{ item.genealogyName }}</text>
<text class="application-card__time">{{ item.appliedAt }}</text>
<text class="application-card__relation">{{ item.relation }}</text>
<text class="application-card__status" :class="`application-card__status--${item.status.toLowerCase()}`">{{ statusLabel(item.status) }}</text>
<text class="application-card__hint">{{ item.status === 'APPROVED' ? '点按进入家谱' : item.status === 'PENDING' ? '请耐心等待管理员核实' : '可重新检索并提交申请' }}</text>
</view>
</view>
</template>
<view v-else class="application-state-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="application-state-card__copy">
<text>{{ applicationState === 'empty' ? '还没有入谱申请' : '申请记录暂时无法读取' }}</text>
<text>{{ applicationState === 'empty' ? '从公开家谱检索开始,找到家谱后可提交加入申请。' : errorMessage || '请检查网络后重新查看。' }}</text>
</view>
</view>
<view v-if="applicationState !== 'list'" class="application-page__action" @click="applicationState === 'error' ? loadApplications({}) : toSearch()">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>{{ applicationState === 'error' ? '重新查看' : '查找公开家谱' }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
const applications = ref([])
const applicationState = ref('loading')
const errorMessage = ref('')
const applicationSamples = [
{ id: 1, genealogyId: 1001, genealogyName: '汤氏家谱', relation: '自述为汤正华堂侄', appliedAt: '今天 10:24', status: 'PENDING' },
{ id: 2, genealogyId: 1002, genealogyName: '汝南汤氏家谱', relation: '祖居河南汝南', appliedAt: '昨天 18:02', status: 'APPROVED' },
{ id: 3, genealogyId: 1003, genealogyName: '清河汤氏家谱', relation: '补充材料不足', appliedAt: '7月12日 09:18', status: 'REJECTED' }
]
const statusLabel = (status) => ({
'PENDING': '审核中',
'APPROVED': '已通过',
'REJECTED': '未通过'
}[status] || '状态未知')
const loadApplications = (query = {}) => {
applicationState.value = 'loading'
errorMessage.value = ''
if (query.state === 'empty') { applicationState.value = 'empty'; return }
if (query.state === 'error') { applicationState.value = 'error'; return }
applications.value = applicationSamples.map((item) => ({ ...item }))
applicationState.value = 'list'
}
onLoad(loadApplications)
const openApprovedGenealogy = (item) => {
if (item.status !== 'APPROVED') return
uni.navigateTo({ url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${item.genealogyId}` })
}
const toSearch = () => uni.navigateTo({ url: '/pages/genealogy/g06-search-genealogies' })
</script>
<style scoped lang="scss">
.application-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.application-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
.application-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .13; pointer-events: none; }
.application-page__header { position: relative; z-index: 3; }
.application-content { position: relative; z-index: 2; padding: 28rpx 24rpx 60rpx; }
.application-intro { display: flex; align-items: baseline; justify-content: space-between; margin: 0 8rpx 20rpx; }
.application-intro text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
.application-intro text:last-child { color: $ink-muted; font-size: 21rpx; }
.application-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 228rpx; max-height: 282rpx; margin-bottom: 18rpx; }
.application-card__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-card__body { position: absolute; inset: 18% 7% 14% 8.5%; }
.application-card__name { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 30rpx; font-weight: 700; }
.application-card__time { position: absolute; top: 3rpx; right: 0; color: #998873; font-size: 20rpx; }
.application-card__relation { display: block; max-width: 70%; margin-top: 14rpx; color: $ink-muted; font-size: 22rpx; }
.application-card__status { position: absolute; right: 3%; bottom: 11%; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.application-card__status--approved { color: #537368; }
.application-card__status--rejected { color: #7e6f62; }
.application-card__hint { position: absolute; bottom: 5%; left: 0; color: #8d7c67; font-size: 20rpx; }
.application-state-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 228rpx; margin-top: 80rpx; }
.application-state-card > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-state-card__copy { position: absolute; inset: 22% 12%; display: flex; flex-direction: column; justify-content: center; text-align: center; }
.application-state-card__copy text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
.application-state-card__copy text:last-child { margin-top: 16rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.6; }
.application-page__action { position: relative; width: 420rpx; height: 82rpx; margin: 34rpx auto 0; }
.application-page__action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-page__action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 26rpx; font-weight: 700; }
</style>
+112 -47
View File
@@ -1,16 +1,52 @@
<!-- 页面编号G-10用途管理员入谱申请审核 -->
<!-- 页面编号G-10用途管理员入谱申请审核与空/失败状态 -->
<template>
<view class="page-shell applications-page">
<PageHeader title="入谱审核" action="审核说明" @action="showHelp" />
<view class="audit-tip"><text>{{ loadError || '通过前请核实申请人的亲属关系;审核结果会通过通知告知对方。' }}</text></view>
<view class="application-list">
<view v-for="item in applications" :key="item.id" class="application-card paper-card">
<view class="application-top"><view><text class="applicant-name">{{ item.name }}</text><text class="applicant-phone">{{ item.phone }}</text></view><text class="applicant-time">{{ item.appliedAt }}</text></view>
<text class="relation-copy">{{ item.relation }}</text>
<view v-if="item.status === 'PENDING'" class="audit-actions"><button class="reject-button" @click="audit(item, false)">拒绝</button><button class="approve-button" @click="audit(item, true)">通过</button></view>
<text v-else class="audit-status">{{ item.status === 'APPROVED' ? '已通过' : '已拒绝' }}</text>
<view class="review-page">
<image class="review-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="review-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="review-page__header"><PageHeader title="入谱审核" action="说明" @action="showHelp" /></view>
<view class="review-content" :class="{
'review-state--list': reviewState === 'list',
'review-state--empty': reviewState === 'empty',
'review-state--error': reviewState === 'error'
}">
<view class="review-intro">
<text>核实亲属关系后再决定</text>
<text>审核结果会通过消息告知申请人</text>
<image src="/static/assets/modules/genealogy/transparent/section-divider.png" mode="scaleToFill" />
</view>
<template v-if="reviewState === 'list'">
<view v-for="item in applications" :key="item.id" class="application-card">
<image class="application-card__skin" src="/static/assets/modules/genealogy/opaque/application-record-card.png" mode="scaleToFill" />
<view class="application-card__body">
<text class="application-card__name">{{ item.name }}</text>
<text class="application-card__phone">{{ item.phone }}</text>
<text class="application-card__time">{{ item.appliedAt }}</text>
<text class="application-card__relation">{{ item.relation }}</text>
<view v-if="item.status === 'PENDING'" class="review-actions">
<view class="review-action" @click="confirmAudit(item, false)">
<image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" /><text>拒绝</text>
</view>
<view class="review-action" @click="confirmAudit(item, true)">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>通过</text>
</view>
</view>
<text v-else class="application-card__status">{{ item.status === 'APPROVED' ? '已通过' : '已拒绝' }}</text>
</view>
</view>
</template>
<view v-else class="review-state-card">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="review-state-card__copy">
<text>{{ reviewState === 'empty' ? '暂无待审核申请' : '审核列表暂时无法读取' }}</text>
<text>{{ reviewState === 'empty' ? '新的入谱申请会在这里出现,并同步发送消息提醒。' : errorMessage || '请检查网络或家谱权限后重试。' }}</text>
</view>
</view>
<view v-if="reviewState === 'error'" class="review-page__retry" @click="loadApplications({ genealogyId })">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>重新查看</text>
</view>
<view v-if="!applications.length && !loadError" class="empty-copy">暂无待审核申请</view>
</view>
</view>
</template>
@@ -19,50 +55,79 @@
import { 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 applications = ref([])
const reviewSamples = [
{ id: 1, name: '汤志成', phone: '139****6421', relation: '自述为汤正华堂侄 · 祖居洛阳', appliedAt: '今天 10:24', status: 'PENDING' },
{ id: 2, name: '汤雨薇', phone: '136****2798', relation: '自述为汤正国之女 · 已补充长辈姓名', appliedAt: '昨天 18:02', status: 'PENDING' }
]
const genealogyId = ref('')
const loadError = ref('')
const reviewState = ref('loading')
const errorMessage = ref('')
onLoad(async (query) => {
genealogyId.value = query.genealogyId || genealogyContext.getCurrentGenealogyId()
if (!genealogyId.value) { loadError.value = '未找到当前家谱,请从家谱总览进入。'; return }
const loadApplications = (query = {}) => {
reviewState.value = 'loading'
errorMessage.value = ''
genealogyId.value = query.genealogyId?.value || query.genealogyId || genealogyId.value || genealogyContext.getCurrentGenealogyId()
if (query.state === 'empty') { reviewState.value = 'empty'; return }
if (query.state === 'error') { reviewState.value = 'error'; return }
if (!genealogyId.value) {
errorMessage.value = '没有找到当前家谱,请从家谱总览进入。'
reviewState.value = 'error'
return
}
genealogyContext.setCurrentGenealogyId(genealogyId.value)
try {
applications.value = await appApi.getPendingApplications(genealogyId.value)
} catch (error) {
loadError.value = error.message || '审核列表加载失败。'
}
})
const audit = async (item, approved) => {
try {
await appApi.auditApplication(genealogyId.value, item.id, approved)
item.status = approved ? 'APPROVED' : 'REJECTED'
uni.showToast({ title: approved ? '已通过申请' : '已拒绝申请', icon: 'none' })
} catch (error) {
uni.showToast({ title: error.message || '审核失败,请稍后重试', icon: 'none' })
}
applications.value = reviewSamples.map((item) => ({ ...item }))
reviewState.value = 'list'
}
const showHelp = () => uni.showModal({ title: '审核说明', content: '仅确认与本家谱存在真实亲属关系的申请,避免错误入谱。', showCancel: false })
onLoad(loadApplications)
const confirmAudit = (item, approved) => {
uni.showModal({
title: approved ? '确认通过申请?' : '确认拒绝申请?',
content: approved ? '通过后,申请人将成为本家谱成员。' : '拒绝后,申请人会收到审核结果。',
confirmText: approved ? '确认通过' : '确认拒绝',
success: ({ confirm }) => {
if (!confirm) return
item.status = approved ? 'APPROVED' : 'REJECTED'
uni.showToast({ title: approved ? '已通过申请' : '已拒绝申请', icon: 'none' })
}
})
}
const showHelp = () => uni.showModal({ title: '审核说明', content: '请核对申请人的姓名、亲属关系和补充说明,仅确认与本家谱存在真实关系的申请。', showCancel: false })
</script>
<style scoped lang="scss">
.applications-page { padding-bottom: 36rpx; }
.audit-tip { margin: 26rpx 28rpx 0; padding: 24rpx; border-left: 5rpx solid $brand-red; background: #efe3cd; color: $ink-muted; font-size: 24rpx; line-height: 1.6; }
.application-list { display: flex; flex-direction: column; gap: 20rpx; margin: 24rpx 28rpx; }
.application-card { padding: 28rpx; }
.application-top { display: flex; justify-content: space-between; }
.applicant-name { color: $ink; font-size: 32rpx; font-weight: 700; }
.applicant-phone { margin-left: 16rpx; color: $ink-muted; font-size: 22rpx; }
.applicant-time { color: #a39583; font-size: 21rpx; }
.relation-copy { display: block; margin-top: 16rpx; color: $ink-muted; font-size: 25rpx; }
.audit-actions { display: flex; justify-content: flex-end; gap: 16rpx; margin-top: 24rpx; }
.audit-actions button { min-width: 128rpx; min-height: 64rpx; margin: 0; border-radius: 8rpx; font-size: 24rpx; }
.reject-button { border: 1rpx solid $gold-soft; background: #fffaf0; color: $ink-muted; }
.approve-button { border: 1rpx solid $brand-red; background: $brand-red; color: #fff9ed; }
.audit-status { display: block; margin-top: 24rpx; color: $brand-red; font-size: 24rpx; text-align: right; }
.empty-copy { padding: 80rpx 0; color: $ink-muted; font-size: 26rpx; text-align: center; }
.review-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.review-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
.review-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .13; pointer-events: none; }
.review-page__header { position: relative; z-index: 3; }
.review-content { position: relative; z-index: 2; padding: 24rpx 24rpx 60rpx; }
.review-intro { position: relative; margin: 0 8rpx 24rpx; padding-bottom: 22rpx; }
.review-intro text { display: block; }
.review-intro text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 29rpx; font-weight: 700; }
.review-intro text:nth-child(2) { margin-top: 8rpx; color: $ink-muted; font-size: 21rpx; }
.review-intro image { position: absolute; right: 0; bottom: 0; left: 0; width: 100%; height: 14rpx; }
.application-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 228rpx; max-height: 282rpx; margin-bottom: 18rpx; }
.application-card__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.application-card__body { position: absolute; inset: 18% 7% 12% 8.5%; }
.application-card__name { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 30rpx; font-weight: 700; }
.application-card__phone { margin-left: 14rpx; color: $ink-muted; font-size: 20rpx; }
.application-card__time { position: absolute; top: 3rpx; right: 0; color: #998873; font-size: 20rpx; }
.application-card__relation { display: block; max-width: 58%; margin-top: 15rpx; color: $ink-muted; font-size: 22rpx; }
.review-actions { position: absolute; right: 0; bottom: 0; display: flex; gap: 12rpx; }
.review-action { position: relative; width: 128rpx; height: 60rpx; }
.review-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.review-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: $ink; font-size: 22rpx; font-weight: 700; }
.review-action:last-child text { color: #fff9ed; }
.application-card__status { position: absolute; right: 3%; bottom: 10%; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.review-state-card { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 228rpx; margin-top: 70rpx; }
.review-state-card > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.review-state-card__copy { position: absolute; inset: 22% 12%; display: flex; flex-direction: column; justify-content: center; text-align: center; }
.review-state-card__copy text:first-child { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 31rpx; font-weight: 700; }
.review-state-card__copy text:last-child { margin-top: 15rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.6; }
.review-page__retry { position: relative; width: 420rpx; height: 82rpx; margin: 32rpx auto 0; }
.review-page__retry image { position: absolute; inset: 0; width: 100%; height: 100%; }
.review-page__retry text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 26rpx; font-weight: 700; }
</style>
+131 -3
View File
@@ -1,3 +1,131 @@
<!-- 页面编号G-11用途家谱设置公开范围与访问说明 -->
<template><ModulePage page-id="g11" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号G-11用途家谱设置公开范围与访问说明页面设计阶段使用本地模拟交互 -->
<template>
<view class="settings-page">
<image class="settings-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="settings-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="settings-page__header"><PageHeader title="家谱设置" /></view>
<view class="settings-panel" :class="{
'settings-state--form': settingsState === 'form',
'settings-state--success': settingsState === 'success',
'settings-state--error': settingsState === 'error'
}">
<image class="settings-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view v-if="settingsState === 'form'" class="settings-form">
<text class="settings-form__eyebrow">谱主可见 · 基础设置</text>
<text class="settings-form__title">完善家谱访问规则</text>
<text class="settings-form__copy">这里仅安排接口支持的名称公开范围与访问说明管理权转让将在成员选择场景中单独处理</text>
<view class="settings-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>家谱名称</text>
<input v-model="genealogyDraft.name" maxlength="20" placeholder="请输入家谱名称" placeholder-class="settings-placeholder" />
</view>
<view class="visibility-block">
<text class="visibility-block__label">公开范围</text>
<view class="visibility-options">
<view v-for="option in visibilityOptions" :key="option.value" class="visibility-option" @click="genealogyDraft.visibility = option.value">
<image :src="genealogyDraft.visibility === option.value ? '/static/assets/foundation/opaque/a01-primary-button.png' : '/static/assets/foundation/opaque/a01-secondary-button.png'" mode="scaleToFill" />
<text :class="{ 'visibility-option__text--active': genealogyDraft.visibility === option.value }">{{ option.label }}</text>
</view>
</view>
<text class="visibility-block__hint">{{ visibilityHint }}</text>
</view>
<view class="settings-field settings-field--note">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>访问说明</text>
<textarea v-model="genealogyDraft.accessNote" maxlength="80" placeholder="向访问者说明家谱用途" placeholder-class="settings-placeholder" />
</view>
<view class="settings-action" @click="saveSettings">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>保存设置</text>
</view>
</view>
<view v-else class="settings-result">
<text class="settings-result__eyebrow">{{ settingsState === 'success' ? '设置已保存' : '设置未保存' }}</text>
<text class="settings-result__title">{{ settingsState === 'success' ? '新的访问规则已经生效' : '暂时无法打开家谱设置' }}</text>
<text class="settings-result__copy">{{ settingsState === 'success' ? `${genealogyDraft.name} · ${visibilityLabel}` : '请从家谱总览重新进入,当前修改不会被保留。' }}</text>
<view class="settings-action" @click="settingsState = 'form'">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" />
<text>{{ settingsState === 'success' ? '继续调整' : '重新查看' }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
const genealogyId = ref('')
const settingsState = ref('loading')
const genealogyDraft = reactive({
name: '汤氏家谱',
visibility: 'MEMBER_ONLY',
accessNote: '家族资料,请妥善保存'
})
const visibilityOptions = [
{ value: 'MEMBER_ONLY', label: '仅成员可见' },
{ value: 'PUBLIC_APPLY', label: '公开可申请' }
]
const visibilityLabel = computed(() => visibilityOptions.find((item) => item.value === genealogyDraft.visibility)?.label || '')
const visibilityHint = computed(() => genealogyDraft.visibility === 'MEMBER_ONLY'
? '只有已加入本家谱的成员可以查看谱系和家族资料。'
: '访客可检索到家谱并提交入谱申请,资料仍需审核后查看。')
onLoad((query) => {
genealogyId.value = query.genealogyId || ''
settingsState.value = query.state === 'success' ? 'success' : query.state === 'error' || !genealogyId.value ? 'error' : 'form'
})
const saveSettings = () => {
if (!genealogyDraft.name.trim()) {
uni.showToast({ title: '请填写家谱名称', icon: 'none' })
return
}
settingsState.value = 'success'
}
</script>
<style scoped lang="scss">
.settings-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.settings-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
.settings-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .13; pointer-events: none; }
.settings-page__header { position: relative; z-index: 3; }
.settings-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(620px, calc((100vw - 16px) * 1.43)); margin: 18rpx auto 0; }
.settings-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.settings-form { position: absolute; inset: 7.5% 8%; }
.settings-form__eyebrow, .settings-result__eyebrow { display: block; color: $brand-red; font-size: 23rpx; letter-spacing: 3rpx; }
.settings-form__title, .settings-result__title { display: block; margin-top: 10rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 35rpx; font-weight: 700; }
.settings-form__copy, .settings-result__copy { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.55; }
.settings-field { position: relative; height: 92rpx; margin-top: 17rpx; }
.settings-field > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.settings-field > text { position: absolute; top: 31rpx; left: 24rpx; z-index: 1; color: $ink; font-size: 22rpx; font-weight: 700; }
.settings-field input, .settings-field textarea { position: absolute; top: 0; right: 20rpx; bottom: 0; left: 164rpx; z-index: 1; height: 92rpx; color: $ink; font-size: 22rpx; line-height: 92rpx; }
.settings-field textarea { box-sizing: border-box; padding-top: 26rpx; line-height: 1.5; }
.settings-placeholder { color: #a79884; }
.visibility-block { margin-top: 17rpx; }
.visibility-block__label { display: block; color: $ink; font-size: 22rpx; font-weight: 700; }
.visibility-options { display: flex; gap: 14rpx; margin-top: 10rpx; }
.visibility-option { position: relative; width: calc(50% - 7rpx); height: 64rpx; }
.visibility-option image { position: absolute; inset: 0; width: 100%; height: 100%; }
.visibility-option text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: $ink; font-size: 22rpx; font-weight: 700; }
.visibility-option .visibility-option__text--active { color: #fff9ed; }
.visibility-block__hint { display: block; margin-top: 9rpx; color: $ink-muted; font-size: 19rpx; line-height: 1.45; }
.settings-field--note { margin-top: 14rpx; }
.settings-action { position: relative; width: 100%; height: 78rpx; margin-top: 19rpx; }
.settings-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.settings-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 26rpx; font-weight: 700; letter-spacing: 3rpx; }
.settings-result { position: absolute; top: 29%; right: 12%; left: 12%; text-align: center; }
.settings-result__eyebrow { text-align: center; }
.settings-result__copy { margin-top: 22rpx; }
.settings-result .settings-action { width: 420rpx; max-width: 100%; margin: 34rpx auto 0; }
@media (min-width: 400px) { .settings-panel { width: calc(100% - 48rpx); } .settings-form { right: 9%; left: 9%; } }
</style>
+148 -3
View File
@@ -1,3 +1,148 @@
<!-- 页面编号G-12用途字辈诗列表详情与维护 -->
<template><ModulePage page-id="g12" /></template>
<script setup>import ModulePage from '@/components/ModulePage.vue'</script>
<!-- 页面编号G-12用途字辈诗列表空状态与批量维护页面设计阶段使用本地模拟交互 -->
<template>
<view class="poem-page">
<image class="poem-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<image class="poem-page__footer" src="/static/assets/foundation/transparent/footer-mountain-bamboo.png" mode="widthFix" />
<view class="poem-page__header"><PageHeader title="字辈诗" :action="poemState === 'list' ? '维护' : ''" @action="openEditor" /></view>
<view class="poem-panel" :class="{
'poem-state--list': poemState === 'list',
'poem-state--empty': poemState === 'empty',
'poem-state--edit': poemState === 'edit',
'poem-state--error': poemState === 'error'
}">
<image class="poem-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view v-if="poemState === 'list'" class="poem-list">
<text class="poem-list__eyebrow">汤氏家谱 · 传承字序</text>
<text class="poem-list__title">启宗敦本继世传芳</text>
<text class="poem-list__copy">按世代查看字辈当前家谱使用到字辈</text>
<view class="poem-rows">
<view v-for="item in poemRows" :key="item.generationNo" class="poem-row" :class="{ 'poem-row--current': item.current }">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text class="poem-row__number"> {{ item.generationNo }} </text>
<text class="poem-row__character">{{ item.character }}</text>
<text class="poem-row__status">{{ item.current ? '当前字辈' : '传承字序' }}</text>
</view>
</view>
<view class="poem-action" @click="openEditor">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>维护字辈诗</text>
</view>
</view>
<view v-else-if="poemState === 'edit'" class="poem-editor">
<text class="poem-list__eyebrow">批量维护</text>
<text class="poem-list__title">录入连续字辈</text>
<text class="poem-list__copy">按照接口支持的连续文本录入每个汉字对应一代保存前可预览新字序</text>
<view class="poem-field">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<text>字辈内容</text>
<textarea v-model="poemDraft" maxlength="24" placeholder="例如:启宗敦本继世传芳" placeholder-class="poem-placeholder" />
</view>
<view class="poem-policy">
<text>缺失旧世代时</text>
<view class="poem-policy__option" @click="stopMissingOldGeneration = !stopMissingOldGeneration">
<image :src="stopMissingOldGeneration ? '/static/assets/foundation/opaque/a01-primary-button.png' : '/static/assets/foundation/opaque/a01-secondary-button.png'" mode="scaleToFill" />
<text :class="{ 'poem-policy__option-text--active': stopMissingOldGeneration }">{{ stopMissingOldGeneration ? '停止并提醒' : '继续补录' }}</text>
</view>
</view>
<text class="poem-preview">预览{{ previewCharacters || '尚未录入字辈' }}</text>
<view class="poem-editor__actions">
<view class="poem-action" @click="poemState = 'list'">
<image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill" /><text class="poem-action__secondary">取消</text>
</view>
<view class="poem-action" @click="savePoems">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>保存字辈</text>
</view>
</view>
</view>
<view v-else class="poem-state-card">
<text class="poem-list__eyebrow">{{ poemState === 'empty' ? '尚未建立字辈' : '字辈暂不可用' }}</text>
<text class="poem-list__title">{{ poemState === 'empty' ? '从第一段传承字序开始' : '暂时无法读取字辈诗' }}</text>
<text class="poem-list__copy">{{ poemState === 'empty' ? '管理员可以一次录入连续字辈,系统会按世代拆分展示。' : '请从家谱总览重新进入,或稍后再试。' }}</text>
<view class="poem-action" @click="poemState === 'empty' ? openEditor() : poemState = 'list'">
<image src="/static/assets/foundation/opaque/a01-primary-button.png" mode="scaleToFill" /><text>{{ poemState === '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 poemState = ref('loading')
const poemDraft = ref('启宗敦本继世传芳')
const stopMissingOldGeneration = ref(true)
const poemRows = ref([
{ generationNo: 12, character: '启', current: false },
{ generationNo: 13, character: '宗', current: false },
{ generationNo: 14, character: '敦', current: true },
{ generationNo: 15, character: '本', current: false }
])
const previewCharacters = computed(() => poemDraft.value.trim().split('').join(' · '))
onLoad((query) => {
genealogyId.value = query.genealogyId || ''
poemState.value = query.state === 'empty' ? 'empty' : query.state === 'edit' ? 'edit' : query.state === 'error' || !genealogyId.value ? 'error' : 'list'
})
const openEditor = () => { poemState.value = 'edit' }
const savePoems = () => {
const characters = poemDraft.value.trim().split('').filter(Boolean)
if (!characters.length) {
uni.showToast({ title: '请录入字辈内容', icon: 'none' })
return
}
poemRows.value = characters.slice(0, 4).map((character, index) => ({ generationNo: 12 + index, character, current: index === 2 }))
poemState.value = 'list'
uni.showToast({ title: '字辈预览已更新', icon: 'none' })
}
</script>
<style scoped lang="scss">
.poem-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.poem-page__paper { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; opacity: .98; pointer-events: none; }
.poem-page__footer { position: absolute; right: 0; bottom: 0; left: 0; z-index: 1; width: 100%; opacity: .13; pointer-events: none; }
.poem-page__header { position: relative; z-index: 3; }
.poem-panel { position: relative; z-index: 2; width: calc(100% - 32rpx); height: min(650px, calc((100vw - 16px) * 1.5)); margin: 18rpx auto 0; }
.poem-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.poem-list, .poem-editor, .poem-state-card { position: absolute; inset: 7.5% 8%; }
.poem-list__eyebrow { display: block; color: $brand-red; font-size: 23rpx; letter-spacing: 3rpx; }
.poem-list__title { display: block; margin-top: 11rpx; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 35rpx; font-weight: 700; }
.poem-list__copy { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.55; }
.poem-rows { margin-top: 18rpx; }
.poem-row { position: relative; height: 72rpx; margin-top: 10rpx; }
.poem-row image { position: absolute; inset: 0; width: 100%; height: 100%; }
.poem-row__number { position: absolute; top: 24rpx; left: 24rpx; z-index: 1; color: $ink-muted; font-size: 21rpx; }
.poem-row__character { position: absolute; top: 14rpx; left: 48%; z-index: 1; color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 32rpx; font-weight: 700; }
.poem-row__status { position: absolute; top: 25rpx; right: 22rpx; z-index: 1; color: $ink-muted; font-size: 19rpx; }
.poem-row--current .poem-row__character, .poem-row--current .poem-row__status { color: $brand-red; }
.poem-action { position: relative; width: 100%; height: 76rpx; margin-top: 20rpx; }
.poem-action image { position: absolute; inset: 0; width: 100%; height: 100%; }
.poem-action text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: #fff9ed; font-size: 25rpx; font-weight: 700; letter-spacing: 2rpx; }
.poem-field { position: relative; height: 118rpx; margin-top: 22rpx; }
.poem-field image { position: absolute; inset: 0; width: 100%; height: 100%; }
.poem-field > text { position: absolute; top: 47rpx; left: 24rpx; z-index: 1; color: $ink; font-size: 22rpx; font-weight: 700; }
.poem-field textarea { position: absolute; top: 0; right: 20rpx; bottom: 0; left: 164rpx; z-index: 1; box-sizing: border-box; height: 118rpx; padding-top: 34rpx; color: $ink; font-size: 22rpx; line-height: 1.5; }
.poem-placeholder { color: #a79884; }
.poem-policy { display: flex; align-items: center; justify-content: space-between; margin-top: 22rpx; }
.poem-policy > text { color: $ink; font-size: 22rpx; font-weight: 700; }
.poem-policy__option { position: relative; width: 248rpx; height: 62rpx; }
.poem-policy__option image { position: absolute; inset: 0; width: 100%; height: 100%; }
.poem-policy__option text { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; height: 100%; color: $ink; font-size: 21rpx; font-weight: 700; }
.poem-policy__option .poem-policy__option-text--active { color: #fff9ed; }
.poem-preview { display: block; margin-top: 22rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.6; }
.poem-editor__actions { display: flex; gap: 14rpx; margin-top: 18rpx; }
.poem-editor__actions .poem-action { width: calc(50% - 7rpx); margin-top: 0; }
.poem-action .poem-action__secondary { color: $ink; }
.poem-state-card { top: 29%; text-align: center; }
.poem-state-card .poem-list__eyebrow { text-align: center; }
.poem-state-card .poem-list__copy { margin-top: 22rpx; }
.poem-state-card .poem-action { width: 420rpx; max-width: 100%; margin: 34rpx auto 0; }
@media (min-width: 400px) { .poem-panel { width: calc(100% - 48rpx); } .poem-list, .poem-editor, .poem-state-card { right: 9%; left: 9%; } }
</style>
+4 -59
View File
@@ -1,59 +1,4 @@
<!-- 页面编号N-01用途消息中心 -->
<template>
<view class="page-shell notification-page">
<PageHeader title="消息通知" action="全部已读" @action="readAll" />
<view class="notice-intro"><text>{{ loadError || '与家谱相关的提醒都会留在这里。' }}</text></view>
<view class="notice-list"><view v-for="item in list" :key="item.id" class="notice-row" :class="{ unread: item.unread }" @click="read(item)"><view class="notice-dot"></view><view class="notice-copy"><text class="notice-title">{{ item.title }}</text><text class="notice-content">{{ item.content }}</text></view><text class="notice-time">{{ item.time }}</text></view></view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
import { appApi } from '@/utils/api.js'
const list = ref([])
const loadError = ref('')
onMounted(async () => {
try {
list.value = await appApi.getNotifications()
} catch (error) {
loadError.value = error.message || '通知加载失败。'
}
})
const read = async (item) => {
if (!item.unread) {
uni.navigateTo({ url: `/pages/notification/n02-message-detail?id=${item.id}` })
return
}
try {
await appApi.markNotificationRead(item.id)
item.unread = false
uni.navigateTo({ url: `/pages/notification/n02-message-detail?id=${item.id}` })
} catch (error) {
uni.showToast({ title: error.message || '标记已读失败', icon: 'none' })
}
}
const readAll = async () => {
try {
await appApi.markAllNotificationsRead()
list.value.forEach((item) => { item.unread = false })
uni.showToast({ title: '已全部标记为已读', icon: 'none' })
} catch (error) {
uni.showToast({ title: error.message || '操作失败,请稍后重试', icon: 'none' })
}
}
</script>
<style scoped lang="scss">
.notification-page { padding-bottom: 36rpx; }
.notice-intro { padding: 28rpx 30rpx; background: #efe3cd; color: $ink-muted; font-size: 24rpx; }
.notice-list { margin: 0 28rpx; }
.notice-row { display: flex; align-items: flex-start; gap: 16rpx; padding: 28rpx 0; border-bottom: 1rpx solid #e4d8c0; }
.notice-dot { width: 14rpx; height: 14rpx; flex: 0 0 auto; margin-top: 13rpx; border-radius: 50%; background: transparent; }
.notice-row.unread .notice-dot { background: $brand-red; }
.notice-copy { min-width: 0; flex: 1; }
.notice-title { display: block; color: $ink; font-size: 30rpx; font-weight: 700; }
.notice-content { display: block; margin-top: 10rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.55; }
.notice-time { flex: 0 0 auto; color: #a19482; font-size: 20rpx; }
</style>
<!-- 页面编号N-01用途消息中心已读操作空态与失败状态 -->
<template><view class="notice-page" :class="{'notice-state--list':noticeState==='list','notice-state--empty':noticeState==='empty','notice-state--error':noticeState==='error'}"><image class="notice-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill"/><view class="notice-page__header"><PageHeader title="消息中心" action="全部已读" @action="markAllRead"/></view><view class="notice-content"><template v-if="noticeState==='list'"><view v-for="item in notices" :key="item.id" class="notice-card" @click="readNotice(item)"><image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill"/><view><text>{{item.unread?'未读提醒':'已读'}} · {{item.time}}</text><text>{{item.title}}</text><text>{{item.content}}</text></view></view></template><view v-else class="notice-state-card"><image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill"/><view><text>{{noticeState==='empty'?'暂时没有新消息':'消息中心暂不可用'}}</text><text>{{noticeState==='empty'?'家谱申请、审核结果和家族提醒会留在这里。':'请稍后重新进入,已读状态不会受到影响。'}}</text></view></view><view class="notice-action" @click="noticeState==='error'?noticeState='list':toReview()"><image src="/static/assets/foundation/opaque/a01-secondary-button.png" mode="scaleToFill"/><text>{{noticeState==='error'?'重新查看':'前往入谱审核'}}</text></view></view></view></template>
<script setup>import{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 noticeState=ref('loading');const notices=ref([{id:1,title:'申请待审核',content:'汤志成申请加入汤氏家谱,请核实亲属关系。',time:'今天 10:28',unread:true},{id:2,title:'入谱申请已通过',content:'你申请加入汝南汤氏家谱的请求已通过。',time:'昨天 18:10',unread:false}]);onLoad(query=>{genealogyId.value=query.genealogyId||genealogyContext.getCurrentGenealogyId()||'';noticeState.value=query.state==='empty'?'empty':query.state==='error'?'error':'list'});const readNotice=item=>{item.unread=false};const markAllRead=()=>{notices.value.forEach(x=>x.unread=false);uni.showToast({title:'已全部标记为已读',icon:'none'})};const toReview=()=>uni.navigateTo({url:`/pages/genealogy/g10-application-review?genealogyId=${genealogyId.value}`})</script>
<style scoped lang="scss">.notice-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.notice-page__paper{position:absolute;inset:0;z-index:0;width:100%;height:100%;pointer-events:none}.notice-page__header,.notice-content{position:relative;z-index:2}.notice-content{padding:24rpx}.notice-card,.notice-state-card{position:relative;width:100%;height:calc((100vw - 24px)*.34286);min-height:220rpx;margin-bottom:16rpx}.notice-card>image,.notice-state-card>image,.notice-action image{position:absolute;inset:0;width:100%;height:100%}.notice-card>view{position:absolute;inset:17% 9%;z-index:1}.notice-card text{display:block}.notice-card text:first-child{color:$brand-red;font-size:18rpx;letter-spacing:2rpx}.notice-card text:nth-child(2){margin-top:6rpx;color:$ink;font-family:STKaiti,KaiTi,serif;font-size:28rpx;font-weight:700}.notice-card text:last-child{margin-top:7rpx;color:$ink-muted;font-size:19rpx}.notice-state-card{margin-top:70rpx}.notice-state-card>view{position:absolute;inset:25% 12%;z-index:1;text-align:center}.notice-state-card text{display:block}.notice-state-card text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:30rpx;font-weight:700}.notice-state-card text:last-child{margin-top:13rpx;color:$ink-muted;font-size:20rpx}.notice-action{position:relative;width:420rpx;height:76rpx;margin:20rpx auto 0}.notice-action text{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;height:100%;color:$ink;font-size:23rpx;font-weight:700}</style>
+4 -58
View File
@@ -1,58 +1,4 @@
<!-- 页面编号M-01用途我的首页 -->
<template>
<view class="page-shell profile-page">
<view class="profile-hero"><view class="profile-seal">{{ profile.name ? profile.name.slice(0, 1) : '谱' }}</view><view class="profile-copy"><text class="profile-name">{{ profile.name || '家谱用户' }}</text><text class="profile-meta">{{ profile.phone || '资料加载中' }} · {{ profile.role || '家谱成员' }}</text></view><text class="profile-edit" @click="toProfile">资料</text></view>
<text v-if="loadError" class="error-copy">{{ loadError }}</text>
<view class="todo-card paper-card" @click="toNotifications"><view><text class="todo-title">待你处理</text><text class="todo-copy">家谱相关提醒与审核通知</text></view><text class="todo-count">{{ unreadCount }}</text></view>
<view class="menu-group paper-card"><view v-for="item in accountItems" :key="item.label" class="menu-row" @click="openItem(item)"><text>{{ item.label }}</text><text>查看</text></view></view>
<view class="menu-group paper-card"><view v-for="item in serviceItems" :key="item.label" class="menu-row" @click="openItem(item)"><text>{{ item.label }}</text><text>查看</text></view></view>
<AppTabbar active="profile" />
</view>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import AppTabbar from '@/components/AppTabbar.vue'
import { appApi } from '@/utils/api.js'
import { notifications } from '@/data/mock.js'
const profile = ref({})
const loadError = ref('')
const unreadCount = computed(() => notifications.filter((item) => item.unread).length)
const accountItems = [
{ label: '我的资料', path: '/pages/profile/m02-edit-profile' },
{ label: '我的加入申请', path: '/pages/genealogy/g09-my-applications' },
{ label: '账号与安全', path: '/pages/profile/m03-security-settings' }
]
const serviceItems = [
{ label: '帮助中心', path: '/pages/profile/m06-help-center' },
{ label: '意见反馈', path: '/pages/profile/m07-feedback' },
{ label: 'VIP 服务', path: '/pages/profile/m09-vip-orders' }
]
onMounted(async () => {
try { profile.value = await appApi.getProfile() } catch (error) { loadError.value = error.message || '个人资料加载失败。' }
})
const toNotifications = () => uni.navigateTo({ url: '/pages/notification/n01-message-center' })
const toProfile = () => uni.navigateTo({ url: '/pages/profile/m02-edit-profile' })
const openItem = (item) => uni.navigateTo({ url: item.path })
</script>
<style scoped lang="scss">
.profile-page { padding-bottom: 178rpx; }
.profile-hero { display: flex; align-items: center; gap: 22rpx; padding: 48rpx 30rpx; background: $navy; }
.profile-seal { display: flex; width: 88rpx; height: 88rpx; align-items: center; justify-content: center; border: 3rpx solid #e0bd77; border-radius: 50%; color: #f4daa1; font-family: serif; font-size: 43rpx; }
.profile-copy { min-width: 0; flex: 1; }
.profile-name { display: block; color: #fff8ec; font-family: serif; font-size: 43rpx; font-weight: 700; }
.profile-meta { display: block; margin-top: 10rpx; color: #dfc590; font-size: 21rpx; }
.profile-edit { color: #f4dca8; font-size: 24rpx; }
.error-copy { display: block; margin: 18rpx 28rpx 0; color: $brand-red; font-size: 23rpx; }
.todo-card { display: flex; align-items: center; justify-content: space-between; margin: -14rpx 28rpx 0; padding: 28rpx; background: #fff8ea; }
.todo-title { display: block; color: $ink; font-size: 30rpx; font-weight: 700; }
.todo-copy { display: block; margin-top: 8rpx; color: $ink-muted; font-size: 22rpx; }
.todo-count { display: flex; width: 52rpx; height: 52rpx; align-items: center; justify-content: center; border-radius: 50%; background: $brand-red; color: #fff8ec; font-size: 26rpx; }
.menu-group { margin: 28rpx 28rpx 0; padding: 0 28rpx; }
.menu-row { display: flex; align-items: center; justify-content: space-between; min-height: 102rpx; border-bottom: 1rpx solid #eadfc9; color: $ink; font-size: 29rpx; }
.menu-row:last-child { border-bottom: 0; }
.menu-row text:last-child { color: $brand-red; font-size: 23rpx; }
</style>
<!-- 页面编号M-01用途个人中心首页提醒与服务导航 -->
<template><view class="profile-page" :class="{'profile-state--ready':profileState==='ready','profile-state--error':profileState==='error'}"><image class="profile-page__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill"/><view class="profile-page__header"><PageHeader title="我的" action="资料" @action="toProfile"/></view><view class="profile-content"><view v-if="profileState==='ready'" class="profile-panel"><image class="profile-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill"/><view class="profile-panel__body"><view class="profile-heading"><image src="/static/assets/modules/genealogy/transparent/current-seal-frame.png" mode="scaleToFill"/><view><text>汤文清</text><text>139****6421 · 家谱成员</text></view></view><view class="profile-todo" @click="toNotifications"><image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill"/><view><text>待你处理</text><text>2 条家谱提醒与审核通知</text></view></view><view v-for="item in menuItems" :key="item.label" class="profile-menu" @click="openItem(item)"><image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill"/><view><text>{{item.label}}</text><text>{{item.note}}</text></view><text>查看</text></view></view></view><view v-else class="profile-error"><image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill"/><view><text>个人资料暂不可用</text><text>请稍后重新进入,账号和家谱资料不会受到影响。</text></view></view></view><AppTabbar active="profile"/></view></template>
<script setup>import{ref}from'vue';import{onLoad}from'@dcloudio/uni-app';import PageHeader from'@/components/PageHeader.vue';import AppTabbar from'@/components/AppTabbar.vue';const profileState=ref('ready');const menuItems=[{label:'账号与安全',note:'密码、手机与设备',url:'/pages/profile/m03-security-settings'},{label:'帮助与反馈',note:'使用说明与问题反馈',url:'/pages/profile/m06-help-center'},{label:'关于家谱',note:'协议、隐私与版本',url:'/pages/profile/m10-about-settings'}];onLoad(query=>{profileState.value=query.state==='error'?'error':'ready'});const toProfile=()=>uni.navigateTo({url:'/pages/profile/m02-edit-profile'});const toNotifications=()=>uni.navigateTo({url:'/pages/notification/n01-message-center'});const openItem=item=>uni.navigateTo({url:item.url})</script>
<style scoped lang="scss">.profile-page{position:relative;min-height:100vh;overflow:hidden;background:$paper}.profile-page__paper{position:absolute;inset:0;z-index:0;width:100%;height:100%;pointer-events:none}.profile-page__header,.profile-content{position:relative;z-index:2}.profile-content{padding:18rpx 24rpx 190rpx}.profile-panel{position:relative;width:100%;height:min(650px,calc((100vw - 24px)*1.5))}.profile-panel__skin{position:absolute;inset:0;width:100%;height:100%}.profile-panel__body{position:absolute;inset:7% 8%}.profile-heading{display:flex;align-items:center;gap:18rpx}.profile-heading>image{width:76rpx;height:98rpx}.profile-heading text{display:block}.profile-heading text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:35rpx;font-weight:700}.profile-heading text:last-child{margin-top:7rpx;color:$ink-muted;font-size:19rpx}.profile-todo{position:relative;height:124rpx;margin-top:18rpx}.profile-todo>image,.profile-menu>image,.profile-error>image{position:absolute;inset:0;width:100%;height:100%}.profile-todo>view{position:absolute;inset:22% 9%;z-index:1}.profile-todo text{display:block}.profile-todo text:first-child{color:$brand-red;font-size:22rpx;font-weight:700}.profile-todo text:last-child{margin-top:7rpx;color:$ink-muted;font-size:19rpx}.profile-menu{position:relative;height:76rpx;margin-top:11rpx}.profile-menu>view{position:absolute;top:14rpx;left:22rpx;z-index:1}.profile-menu>view text{display:block}.profile-menu>view text:first-child{color:$ink;font-size:21rpx;font-weight:700}.profile-menu>view text:last-child{margin-top:3rpx;color:$ink-muted;font-size:17rpx}.profile-menu>text{position:absolute;top:29rpx;right:22rpx;z-index:1;color:$brand-red;font-size:18rpx}.profile-error{position:relative;height:230rpx;margin-top:70rpx}.profile-error>view{position:absolute;inset:27% 12%;z-index:1;text-align:center}.profile-error text{display:block}.profile-error text:first-child{color:$ink;font-family:STKaiti,KaiTi,serif;font-size:30rpx;font-weight:700}.profile-error text:last-child{margin-top:13rpx;color:$ink-muted;font-size:20rpx}</style>
+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>