Files
jiapuapp/components/ModulePage.vue
T
2026-07-17 08:08:33 +08:00

186 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 公共组件F/R/N/M 剩余页面的高规格多任务母版内容独立视觉表面统一复用完整位图 -->
<template>
<view class="module-page" :class="[
`module-page--${page.template}`,
{
'module-state--ready': moduleState === 'ready',
'module-state--empty': moduleState === 'empty',
'module-state--success': moduleState === 'success',
'module-state--error': moduleState === 'error'
}
]">
<ModulePageBackground :module="moduleKey" />
<view class="module-page__header"><PageHeader :title="page.title" /></view>
<view v-if="moduleState === 'ready'" class="module-page__content">
<view class="page-intro">
<image class="page-intro__seal" src="/static/assets/foundation/transparent/brand-seal.png" mode="aspectFit" />
<view><text class="page-intro__number">{{ page.number }}</text><text class="page-intro__subtitle">{{ page.subtitle }}</text></view>
</view>
<view v-if="page.template === 'form'" class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body module-form">
<text class="module-heading">{{ page.title }}</text>
<text class="module-copy">{{ page.subtitle }}</text>
<view v-for="field in page.fields" :key="field" class="form-row">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><text>{{ field }}</text><input :placeholder="`请输入${field}`" placeholder-class="form-placeholder" />
</view>
<text v-if="page.note" class="form-note">{{ normalizedNote }}</text>
<AppButton block :label="page.action" @click="completeAction" />
</view>
</view>
<view v-else-if="page.template === 'list'" class="content-stack">
<view v-for="item in page.sections" :key="item[0]" class="list-card" @click="openPreview(item)">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view class="list-card__copy"><text>{{ item[0] }}</text><text>{{ item[1] }}</text><text>点按查看详情</text></view>
</view>
<AppButton block :label="page.action" @click="completeAction" />
</view>
<view v-else-if="page.template === 'detail'" class="module-panel module-panel--detail">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body module-detail">
<text class="detail-eyebrow">{{ page.number }} · 家族档案</text>
<text class="module-heading">{{ page.title }}</text>
<text class="module-copy">{{ page.subtitle }}</text>
<view v-for="item in page.sections" :key="item[0]" class="detail-card">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><view><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view>
</view>
<AppButton block :label="page.action" @click="completeAction" />
</view>
</view>
<view v-else-if="page.template === 'timeline'" class="content-stack">
<view v-for="(item, index) in page.sections" :key="item[0]" class="timeline-row">
<image src="/static/assets/modules/genealogy/opaque/application-status-card.png" mode="scaleToFill" />
<view><text> {{ index + 1 }} </text><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view>
</view>
<AppButton block :label="page.action" @click="completeAction" />
</view>
<view v-else-if="page.template === 'settings'" class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body module-settings">
<text class="module-heading">{{ page.title }}</text><text class="module-copy">{{ page.subtitle }}</text>
<view v-for="item in page.sections" :key="item[0]" class="settings-row" @click="openPreview(item)">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" /><view><text>{{ item[0] }}</text><text>{{ item[1] }}</text></view><text>查看</text>
</view>
<AppButton block :label="page.action" @click="completeAction" />
</view>
</view>
<view v-else class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body status-card">
<text class="status-card__eyebrow">{{ page.badge }} · 服务说明</text><text class="status-card__lead">{{ page.lead }}</text><text class="status-card__note">{{ normalizedNote }}</text>
<AppButton block :label="page.action" @click="completeAction" />
</view>
</view>
</view>
<view v-else class="module-page__content module-page__content--state">
<view class="module-panel">
<image class="module-panel__skin" src="/static/assets/modules/genealogy/opaque/g03-create-flow-panel.png" mode="scaleToFill" />
<view class="module-panel__body status-card">
<text class="status-card__eyebrow">{{ stateCopy.eyebrow }}</text><text class="status-card__lead">{{ stateCopy.title }}</text><text class="status-card__note">{{ stateCopy.copy }}</text>
<AppButton block :type="moduleState === 'error' ? 'secondary' : 'primary'" :label="stateCopy.action" @click="moduleState = 'ready'" />
</view>
</view>
</view>
<AppTabbar v-if="page.tab" :active="page.tab" />
<AppToast :visible="toastVisible" :message="toastMessage" />
</view>
</template>
<script setup>
import { computed, onUnmounted, ref } from 'vue'
import AppButton from '@/components/AppButton.vue'
import PageHeader from '@/components/PageHeader.vue'
import AppTabbar from '@/components/AppTabbar.vue'
import AppToast from '@/components/AppToast.vue'
import ModulePageBackground from '@/components/ModulePageBackground.vue'
import { pageCatalog } from '@/data/page-catalog.js'
const props = defineProps({ pageId: { type: String, required: true } })
const page = computed(() => pageCatalog[props.pageId])
const moduleKey = computed(() => ({ f: 'family', r: 'records', n: 'notification', m: 'profile' }[props.pageId[0]] || 'profile'))
const query = (() => {
if (typeof location !== 'undefined') return Object.fromEntries(new URLSearchParams(location.hash.split('?')[1] || ''))
const pages = getCurrentPages(); return pages[pages.length - 1]?.options || {}
})()
const moduleState = ref('ready')
const toastVisible = ref(false)
const toastMessage = ref('')
let toastTimer = null
if (query.state === 'empty' || query.state === 'success' || query.state === 'error') moduleState.value = query.state
const normalizedNote = computed(() => (page.value.note || '').replace(/待接入|服务待接入|功能待接入/g, '将在后续功能阶段开放'))
const stateCopy = computed(() => ({
empty: { eyebrow: `${page.value.number} · 暂无内容`, title: `还没有${page.value.title}记录`, copy: '完成第一条记录后,内容会按时间或类别整理在这里。', action: page.value.action },
success: { eyebrow: '本地预览已保存', title: `${page.value.title}内容已更新`, copy: '当前候选已展示完整操作结果;正式数据将在接口阶段接入。', action: '继续查看' },
error: { eyebrow: '内容暂不可用', title: `暂时无法打开${page.value.title}`, copy: '请稍后重新进入,现有资料不会受到影响。', action: '重新查看' }
}[moduleState.value] || {}))
const completeAction = () => { moduleState.value = 'success' }
const openPreview = (item) => {
toastMessage.value = item[0]
toastVisible.value = true
if (toastTimer) clearTimeout(toastTimer)
toastTimer = setTimeout(() => { toastVisible.value = false; toastTimer = null }, 1800)
}
onUnmounted(() => { if (toastTimer) clearTimeout(toastTimer) })
</script>
<style scoped lang="scss">
.module-page { position: relative; min-height: 100vh; overflow: hidden; background: $paper; }
.module-page__header, .module-page__content { position: relative; z-index: 2; }
.module-page__content { padding: 20rpx 24rpx 50rpx; }
.page-intro { display: flex; align-items: center; min-height: 86rpx; margin: 0 8rpx 14rpx; }
.page-intro__seal { width: 66rpx; height: 66rpx; margin-right: 13rpx; }
.page-intro__number, .page-intro__subtitle { display: block; }
.page-intro__number { color: $brand-red; font-size: 23rpx; font-weight: 700; letter-spacing: 3rpx; }
.page-intro__subtitle { margin-top: 5rpx; color: $ink-muted; font-size: 20rpx; }
.module-panel { position: relative; width: 100%; height: min(640px, calc((100vw - 24px) * 1.48)); }
.module-panel__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.module-panel__body { position: absolute; inset: 7.5% 8%; }
.module-heading { display: block; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 34rpx; font-weight: 700; }
.module-copy { display: block; margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.form-row { position: relative; height: 76rpx; margin-top: 12rpx; }
.form-row image, .detail-card image, .settings-row image, .list-card > image, .timeline-row > image { position: absolute; inset: 0; width: 100%; height: 100%; }
.form-row > text { position: absolute; top: 26rpx; left: 22rpx; z-index: 1; color: $ink; font-size: 21rpx; font-weight: 700; }
.form-row input { position: absolute; top: 0; right: 18rpx; bottom: 0; left: 164rpx; z-index: 1; height: 76rpx; color: $ink; font-size: 21rpx; line-height: 76rpx; }
.form-placeholder { color: #a79884; }
.form-note { display: block; margin-top: 14rpx; color: $ink-muted; font-size: 19rpx; text-align: center; }
.module-form > .app-button, .module-detail > .app-button, .module-settings > .app-button, .content-stack > .app-button { margin-top: 18rpx; }
.content-stack { display: flex; flex-direction: column; gap: 14rpx; }
.list-card, .timeline-row { position: relative; width: 100%; height: calc((100vw - 24px) * .34286); min-height: 220rpx; }
.list-card__copy { position: absolute; inset: 19% 9%; z-index: 1; }
.list-card__copy text { display: block; }
.list-card__copy text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }
.list-card__copy text:nth-child(2) { margin-top: 9rpx; color: $ink-muted; font-size: 20rpx; }
.list-card__copy text:last-child { margin-top: 9rpx; color: $brand-red; font-size: 18rpx; }
.detail-eyebrow { display: block; color: $brand-red; font-size: 21rpx; letter-spacing: 3rpx; }
.module-detail .module-heading { margin-top: 9rpx; }
.detail-card, .settings-row { position: relative; min-height: 88rpx; margin-top: 14rpx; }
.detail-card > view { position: relative; z-index: 1; padding: 18rpx 24rpx; }
.detail-card text, .settings-row text { display: block; }
.detail-card text:first-child { color: $brand-red; font-size: 21rpx; font-weight: 700; }
.detail-card text:last-child { margin-top: 7rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; }
.timeline-row > view { position: absolute; inset: 17% 9%; z-index: 1; }
.timeline-row text { display: block; }
.timeline-row text:first-child { color: $brand-red; font-size: 18rpx; letter-spacing: 2rpx; }
.timeline-row text:nth-child(2) { margin-top: 5rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 29rpx; font-weight: 700; }
.timeline-row text:last-child { margin-top: 8rpx; color: $ink-muted; font-size: 20rpx; }
.settings-row > view { position: absolute; top: 19rpx; left: 22rpx; z-index: 1; }
.settings-row > view text:first-child { color: $ink; font-size: 22rpx; font-weight: 700; }
.settings-row > view text:last-child { margin-top: 5rpx; color: $ink-muted; font-size: 18rpx; }
.settings-row > text { position: absolute; top: 34rpx; right: 22rpx; z-index: 1; color: $brand-red; font-size: 18rpx; }
.status-card { top: 28%; text-align: center; }
.status-card__eyebrow { display: block; color: $brand-red; font-size: 21rpx; letter-spacing: 3rpx; }
.status-card__lead { display: block; margin-top: 14rpx; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 35rpx; font-weight: 700; }
.status-card__note { display: block; margin-top: 17rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.65; }
.status-card .app-button { margin: 30rpx auto 0; }
.module-page__content--state { padding-top: 18rpx; }
@media (min-width: 400px) { .module-page__content { padding-right: 32rpx; padding-left: 32rpx; } }
</style>