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

568 lines
14 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--${moduleKey}`,
`module-page--${page.template}`,
`module-state--${moduleState}`,
]"
>
<ModulePageBackground :module="moduleKey" />
<view class="module-page__header"><PageHeader :title="page.title" /></view>
<view v-if="moduleState === 'loading'" class="module-page__loading">
<AppLoading
:text="`正在整理${page.title}`"
description="请稍候,内容正在归卷。"
/>
</view>
<view v-else-if="moduleState === 'ready'" class="module-page__content">
<view class="module-lead">
<image
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
<text>{{ page.subtitle }}</text>
</view>
<view v-if="page.template === 'form'" class="module-panel module-form">
<text class="section-eyebrow">填写信息</text>
<view v-for="field in page.fields" :key="field" class="form-row">
<image :src="fieldAsset" 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 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="contentAsset" 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-detail"
>
<text class="section-eyebrow">档案详情</text>
<view v-for="item in page.sections" :key="item[0]" class="detail-card">
<image :src="contentAsset" mode="scaleToFill" />
<view
><text>{{ item[0] }}</text
><text>{{ item[1] }}</text></view
>
</view>
<AppButton block :label="page.action" @click="completeAction" />
</view>
<view
v-else-if="page.template === 'timeline'"
class="content-stack timeline-stack"
>
<view
v-for="(item, index) in page.sections"
:key="item[0]"
class="timeline-row"
>
<image :src="contentAsset" 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 module-settings"
>
<text class="section-eyebrow">服务与设置</text>
<view
v-for="item in page.sections"
:key="item[0]"
class="settings-row"
@click="openPreview(item)"
>
<image :src="fieldAsset" mode="scaleToFill" />
<view
><text>{{ item[0] }}</text
><text>{{ normalizedSectionCopy(item[1]) }}</text></view
>
<text>查看</text>
</view>
<AppButton block :label="page.action" @click="completeAction" />
</view>
<view v-else class="module-panel status-card">
<image
class="status-card__skin"
:src="contentAsset"
mode="scaleToFill"
/>
<view class="status-card__body">
<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 status-card">
<image
class="status-card__skin"
:src="contentAsset"
mode="scaleToFill"
/>
<view class="status-card__body">
<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="handleStateAction"
/>
</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 AppLoading from "@/components/AppLoading.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 assetModule = computed(
() =>
({
family: "family",
records: "records",
notification: "notification",
profile: "profile",
})[moduleKey.value],
);
const contentAsset = computed(
() =>
`/static/assets/modules/${assetModule.value}/transparent/module-content-frame.png`,
);
const fieldAsset = computed(
() =>
`/static/assets/modules/${assetModule.value}/transparent/module-field-frame.png`,
);
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(
["loading", "empty", "success", "error"].includes(query.state)
? query.state
: "ready",
);
const toastVisible = ref(false);
const toastMessage = ref("");
let toastTimer = null;
const normalizedNote = computed(() =>
(page.value.note || "").replace(
/待接入|服务待接入|功能待接入/g,
"将在后续功能阶段开放",
),
);
const normalizedSectionCopy = (copy) =>
String(copy || "").replace(/待接入|功能待接入/g, "后续功能阶段开放");
const stateCopy = computed(() => {
const pageState = page.value.states?.[moduleState.value];
if (pageState) return pageState;
return (
{
empty: {
eyebrow: "暂无内容",
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 handleStateAction = () => {
if (stateCopy.value.actionMode === "back") {
uni.navigateBack();
return;
}
moduleState.value = "ready";
};
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-x: hidden;
background: $paper;
}
.module-page__header,
.module-page__content,
.module-page__loading {
position: relative;
z-index: 2;
}
.module-page__loading {
min-height: calc(100vh - 100rpx);
}
.module-page__content {
padding: 16rpx 26rpx 56rpx;
}
.module-lead {
position: relative;
height: 56rpx;
margin: 0 12rpx 18rpx;
color: $ink-muted;
font-size: 23rpx;
text-align: center;
}
.module-lead image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.module-lead text {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.module-panel {
position: relative;
width: 100%;
}
.section-eyebrow {
display: block;
margin: 2rpx 8rpx 14rpx;
color: $brand-red;
font-family: STKaiti, KaiTi, serif;
font-size: 29rpx;
font-weight: 700;
letter-spacing: 3rpx;
}
.form-row {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 84rpx;
align-items: center;
gap: 22rpx;
margin-top: 14rpx;
padding: 14rpx 26rpx;
box-sizing: border-box;
}
.form-row image,
.detail-card image,
.settings-row image,
.list-card > image,
.timeline-row > image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.form-row > text {
position: relative;
z-index: 1;
color: $ink;
font-size: 24rpx;
font-weight: 700;
}
.form-row input {
position: relative;
z-index: 1;
width: 100%;
min-width: 0;
height: 56rpx;
color: $ink;
font-size: 23rpx;
text-align: right;
}
.form-placeholder {
color: #9e8e79;
}
.form-note {
display: block;
margin: 18rpx 18rpx 0;
color: $ink-muted;
font-size: 22rpx;
line-height: 1.5;
text-align: center;
}
.module-form > .app-button,
.module-detail > .app-button,
.module-settings > .app-button,
.content-stack > .app-button {
margin-top: 22rpx;
}
.content-stack {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.list-card,
.timeline-row {
position: relative;
width: 100%;
min-height: 188rpx;
}
.list-card__copy {
position: relative;
z-index: 1;
padding: 34rpx 46rpx 28rpx;
}
.list-card__copy text {
display: block;
}
.list-card__copy text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 30rpx;
font-weight: 700;
}
.list-card__copy text:nth-child(2) {
margin-top: 10rpx;
color: $ink-muted;
font-size: 23rpx;
line-height: 1.45;
}
.list-card__copy text:last-child {
margin-top: 11rpx;
color: $brand-red;
font-size: 21rpx;
font-weight: 600;
}
.detail-card {
position: relative;
min-height: 170rpx;
margin-top: 16rpx;
}
.detail-card > view {
position: relative;
z-index: 1;
padding: 34rpx 44rpx;
}
.detail-card text,
.settings-row text {
display: block;
}
.detail-card text:first-child {
color: $brand-red;
font-size: 23rpx;
font-weight: 700;
}
.detail-card text:last-child {
margin-top: 10rpx;
color: $ink;
font-size: 24rpx;
line-height: 1.55;
}
.timeline-row > view {
position: relative;
z-index: 1;
padding: 29rpx 44rpx;
}
.timeline-row text {
display: block;
}
.timeline-row text:first-child {
color: $brand-red;
font-size: 21rpx;
letter-spacing: 2rpx;
}
.timeline-row text:nth-child(2) {
margin-top: 6rpx;
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: 30rpx;
font-weight: 700;
}
.timeline-row text:last-child {
margin-top: 9rpx;
color: $ink-muted;
font-size: 23rpx;
}
.settings-row {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
min-height: 94rpx;
align-items: center;
gap: 20rpx;
margin-top: 14rpx;
padding: 20rpx 28rpx 18rpx 26rpx;
box-sizing: border-box;
}
.settings-row > view {
position: relative;
z-index: 1;
}
.settings-row > view text:first-child {
color: $ink;
font-size: 24rpx;
font-weight: 700;
}
.settings-row > view text:last-child {
margin-top: 6rpx;
color: $ink-muted;
font-size: 21rpx;
}
.settings-row > text {
position: relative;
z-index: 1;
color: $brand-red;
font-size: 21rpx;
font-weight: 600;
}
.status-card {
min-height: 330rpx;
text-align: center;
}
.status-card__skin {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.status-card__body {
position: relative;
z-index: 1;
padding: 70rpx 60rpx 48rpx;
}
.status-card__eyebrow {
display: block;
color: $brand-red;
font-size: 23rpx;
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: 24rpx;
line-height: 1.6;
}
.status-card .app-button {
margin: 28rpx auto 0;
}
.module-page__content--state {
padding-top: 72rpx;
}
.module-page--family .section-eyebrow {
color: #9f2c23;
}
.module-page--records .section-eyebrow {
color: #8c261f;
}
.module-page--notification .section-eyebrow {
color: #9b2b21;
}
.module-page--profile .section-eyebrow {
color: #a22d24;
}
@media (min-width: 400px) {
.module-page__content {
padding-right: 34rpx;
padding-left: 34rpx;
}
}
@media (max-width: 340px) {
.module-page__content {
padding-right: 20rpx;
padding-left: 20rpx;
}
.form-row {
gap: 14rpx;
}
.status-card__body {
padding-right: 44rpx;
padding-left: 44rpx;
}
}
</style>