568 lines
14 KiB
Vue
568 lines
14 KiB
Vue
<!-- 公共组件: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>
|