Files
jiapuapp/pages/records/r11-merit-records.vue
T
2026-07-23 08:24:07 +08:00

289 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.
<!-- 页面编号R-11用途当前家谱功德记录与不写库的本地预览 -->
<template>
<view class="merit-page" :class="stateClasses">
<ModulePageBackground module="records" />
<view class="page-header">
<PageHeader
title="功德记录"
:action="hasValidContext ? '填写预览' : ''"
custom-back
@back="requestBack"
@action="startMeritPreview"
/>
</view>
<view v-if="meritState === 'loading'" class="page-loading">
<AppLoading text="正在整理功德记录" description="请稍候,正在核对当前家谱的正式记录。" />
</view>
<view v-else class="page-content">
<view v-if="hasValidContext" class="merit-summary">
<text>正式记录</text>
<text>{{ totalContribution }} </text>
<text>本地预览不计入正式记录数量</text>
</view>
<view v-if="localMeritPreview" class="preview-card">
<text>本地预览 · 尚未提交</text>
<text>{{ localMeritPreview.meritTitle }}</text>
<text>{{ localMeritPreview.donorName }} · {{ localMeritPreview.meritTime || "时间未填写" }}</text>
<text>{{ localMeritPreview.meritType || "类型未填写" }}</text>
<text>金额数值单位待确认):{{ formatMeritAmount(localMeritPreview.amount) }}</text>
<text>{{ localMeritPreview.meritContent || "内容未填写" }}</text>
</view>
<template v-if="meritState === 'ready' && meritRecords.length">
<view v-for="merit in meritRecords" :key="merit.meritId" class="merit-card">
<text>{{ merit.meritTypeLabel || "类型未标注" }}</text>
<text>{{ merit.meritTitle }}</text>
<text>{{ merit.donorName }} · {{ merit.meritTime || "时间未填写" }}</text>
<text>{{ merit.meritContent || "暂无记录内容" }}</text>
</view>
<AppButton block label="填写功德预览" @click="startMeritPreview" />
</template>
<view v-else class="state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton
:type="meritState === 'empty' ? 'primary' : 'secondary'"
block
:label="stateCopy.action"
@click="handleStateAction"
/>
</view>
</view>
<AppDialog
:visible="dialogVisible"
:close-on-mask="false"
eyebrow="功德记录"
title="填写一份功德预览"
confirm-text="生成预览"
cancel-text="取消"
show-cancel
@confirm="createMeritPreview"
@cancel="requestCloseEditor"
>
<view class="dialog-form">
<input v-model="meritForm.meritTitle" placeholder="贡献事项" />
<input v-model="meritForm.donorName" placeholder="贡献人" />
<input v-model="meritForm.meritType" placeholder="贡献类型(选填)" />
<input v-model="meritForm.meritTime" placeholder="时间(选填)" />
<input v-model="meritForm.amount" type="digit" placeholder="金额数值(单位未明确,可不填)" />
<textarea v-model="meritForm.meritContent" auto-height placeholder="说明时间、物资或具体帮助(选填)" />
<text v-if="formError">{{ formError }}</text>
</view>
</AppDialog>
<AppDialog
:visible="discardVisible"
:close-on-mask="false"
eyebrow="放弃确认"
title="放弃当前填写?"
message="尚未提交的预览内容将从当前页面清除。"
confirm-text="确认放弃"
cancel-text="继续填写"
show-cancel
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
<AppToast :visible="toastVisible" message="已生成本地预览,尚未保存" />
</view>
</template>
<script setup>
import { computed, onUnmounted, reactive, ref } from "vue";
import { onBackPress, onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
getGenealogyFixtureAccess,
listMeritRecordFixtures,
} from "@/data/mock.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const meritRecords = ref([]);
const meritState = ref("loading");
const dialogVisible = ref(false);
const discardVisible = ref(false);
const toastVisible = ref(false);
const formError = ref("");
const localMeritPreview = ref(null);
const meritForm = reactive({
meritTitle: "",
donorName: "",
meritType: "",
meritTime: "",
amount: "",
meritContent: "",
});
const editorBaseline = ref("");
let timer = null;
const totalContribution = computed(() => meritRecords.value.length);
const stateClasses = computed(() => ({
"merit-state--loading": meritState.value === "loading",
"merit-state--empty": meritState.value === "empty",
"merit-state--error": meritState.value === "error",
"merit-state--invalid": meritState.value === "invalid",
}));
const hasValidContext = computed(() => ["ready", "empty"].includes(meritState.value));
const formSnapshot = computed(() => JSON.stringify({ ...meritForm }));
const meritDraftDirty = computed(() =>
dialogVisible.value && formSnapshot.value !== editorBaseline.value,
);
const stateCopy = computed(() => ({
error: {
title: "功德记录暂不可用",
copy: "请稍后重新查看,已有记录不会受到影响。",
action: "重新查看",
},
invalid: {
title: "功德记录入口无效",
copy: "没有找到可访问的成员家谱,页面不会展示其他家谱记录。",
action: "返回上一页",
},
empty: {
title: "还没有功德记录",
copy: "可以先生成一份本地预览;正式创建仍需等待线上写接口启用。",
action: "填写功德预览",
},
})[meritState.value] || {
title: "功德记录暂不可用",
copy: "请返回上一页重新进入。",
action: "返回上一页",
});
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
const access = getGenealogyFixtureAccess(genealogyId.value);
if (!["owner", "member"].includes(access.accessRole)) {
meritState.value = "invalid";
return;
}
meritRecords.value = listMeritRecordFixtures(genealogyId.value);
meritState.value = ["loading", "empty", "error"].includes(query.state)
? query.state
: meritRecords.value.length
? "ready"
: "empty";
});
const startMeritPreview = () => {
if (!hasValidContext.value) return false;
Object.assign(meritForm, {
meritTitle: "",
donorName: "",
meritType: "",
meritTime: "",
amount: "",
meritContent: "",
});
formError.value = "";
editorBaseline.value = formSnapshot.value;
dialogVisible.value = true;
return true;
};
const formatMeritAmount = (amount) =>
amount === null || amount === "" ? "未填写" : String(amount);
const createMeritPreview = () => {
const missing = [];
if (!meritForm.meritTitle.trim()) missing.push("贡献事项");
if (!meritForm.donorName.trim()) missing.push("贡献人");
formError.value = missing.length ? `请填写${missing.join("和")}` : "";
const amountInput = meritForm.amount.trim();
if (!formError.value && amountInput && !Number.isFinite(Number(amountInput))) {
formError.value = "金额必须是数字,单位仍待后端确认";
}
if (formError.value) return false;
// 金额的单位、精度和取值规则尚未由后端明确,本地预览只保留原始输入。
localMeritPreview.value = Object.freeze({
meritTitle: meritForm.meritTitle.trim(),
donorName: meritForm.donorName.trim(),
meritType: meritForm.meritType.trim(),
meritTime: meritForm.meritTime.trim(),
amount: amountInput ? Number(amountInput) : null,
meritContent: meritForm.meritContent.trim(),
});
dialogVisible.value = false;
toastVisible.value = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
toastVisible.value = false;
timer = null;
}, 1800);
return true;
};
const closeEditor = () => {
dialogVisible.value = false;
formError.value = "";
};
const discardConfirmation = createDiscardConfirmation(
(visible) => { discardVisible.value = visible; },
);
const confirmDiscard = () => {
discardConfirmation.confirm();
closeEditor();
};
const cancelDiscard = discardConfirmation.cancel;
const requestCloseEditor = async () => {
if (!meritDraftDirty.value) {
closeEditor();
return true;
}
const confirmed = await discardConfirmation.request();
if (confirmed) closeEditor();
return confirmed;
};
const restoreMeritRecords = () => {
meritRecords.value = listMeritRecordFixtures(genealogyId.value);
meritState.value = meritRecords.value.length ? "ready" : "empty";
};
const handleStateAction = () => {
if (meritState.value === "invalid") return goBack();
if (meritState.value === "error") return restoreMeritRecords();
return startMeritPreview();
};
const requestBack = () => {
if (discardVisible.value) {
cancelDiscard();
return Promise.resolve(true);
}
if (dialogVisible.value) return requestCloseEditor();
return runBackGuard({
dirty: Boolean(localMeritPreview.value),
"confirm-discard": discardConfirmation.request,
});
};
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => {
if (timer) clearTimeout(timer);
discardConfirmation.dispose();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.merit-page { display: flex; min-height: 100vh; flex-direction: column; background: $paper; }
.page-header,.page-loading,.page-content { z-index: 1; }
.page-loading { min-height: calc(100vh - 100rpx); }
.page-content { display: flex; flex-direction: column; gap: 16rpx; padding: 18rpx 24rpx 72rpx; }
.merit-summary,.merit-card,.preview-card,.state-card { box-sizing: border-box; padding: 34rpx 46rpx; @include adaptive.adaptive-records-content; }
.merit-summary { text-align: center; }
.merit-summary > text,.merit-card > text,.preview-card > text,.state-card > text { display: block; }
.merit-summary > text:first-child,.merit-card > text:first-child,.preview-card > text:first-child { color: $brand-red; font-size: 21rpx; }
.merit-summary > text:nth-child(2) { margin-top: 5rpx; color: $ink; font-size: 38rpx; font-weight: 700; }
.merit-summary > text:last-child { margin-top: 9rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.5; }
.merit-card > text:nth-child(2),.preview-card > text:nth-child(2) { margin-top: 6rpx; color: $ink; font-size: 31rpx; font-weight: 700; }
.merit-card > text:nth-child(3),.preview-card > text:nth-child(3) { margin-top: 8rpx; color: $ink-muted; font-size: 21rpx; }
.merit-card > text:last-child,.preview-card > text:last-child { margin-top: 9rpx; color: $ink; font-size: 23rpx; line-height: 1.55; }
.state-card { min-height: 340rpx; padding-top: 78rpx; text-align: center; }
.state-card > text:first-child { color: $ink; font-size: 35rpx; font-weight: 700; }
.state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
.state-card .app-button { margin-top: 28rpx; }
.dialog-form { width: 100%; margin: 14rpx 0; }
.dialog-form input,.dialog-form textarea { width: 100%; min-height: 62rpx; margin-top: 7rpx; padding: 11rpx 18rpx; box-sizing: border-box; color: $ink; font-size: 22rpx; @include adaptive.adaptive-records-field; }
.dialog-form text { display: block; margin-top: 7rpx; color: $brand-red; font-size: 20rpx; }
</style>