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

132 lines
11 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">
<ModulePageBackground module="records" />
<view class="page-header"><PageHeader title="功德记录" :action="valid && view === 'list' ? '新建' : ''" custom-back @back="requestBack" @action="openCreate" /></view>
<view class="page-content">
<view v-if="view === 'form'" class="form-card">
<text>新建功德记录</text>
<text class="form-copy">红色 * 为必填项其余内容可按需补充</text>
<view v-for="field in fields" :key="field.key" class="field"><text><text v-if="field.required" class="required-mark">*</text>{{ field.label }}</text><textarea v-if="field.key === 'content'" v-model="form[field.key]" auto-height :placeholder="`请输入${field.label}`" @input="error = ''" /><input v-else v-model="form[field.key]" :type="field.inputType || 'text'" :placeholder="field.placeholder || `请输入${field.label}`" @input="error = ''" /></view>
<view class="field field--picker"><text>记录日期</text><picker mode="date" :value="form.meritDate" @change="selectMeritDate"><view :class="{ placeholder: !form.meritDate }">{{ form.meritDate || '请选择' }}</view></picker></view>
<view class="field field--picker"><text>记录时间</text><picker mode="time" :value="form.meritClock" @change="selectMeritClock"><view :class="{ placeholder: !form.meritClock }">{{ form.meritClock || '请选择' }}</view></picker></view>
<text v-if="error" class="error">{{ error }}</text>
<view class="form-actions"><AppButton type="secondary" label="取消" @click="cancelCreate" /><AppButton :disabled="submitting" :label="submitting ? '正在提交' : '提交功德记录'" @click="submit" /></view>
</view>
<view v-else-if="!valid" class="state-card"><text>功德记录入口无效</text><AppButton block label="返回上一页" @click="requestBack" /></view>
<view v-else-if="listState === 'loading'" class="state-card"><AppLoading text="正在读取功德记录" /></view>
<view v-else-if="listState === 'error'" class="state-card"><text>暂时无法读取功德记录</text><AppButton block type="secondary" label="重新加载" @click="loadMerits" /></view>
<view v-else-if="listState === 'empty'" class="state-card"><text>还没有功德记录</text><AppButton block label="新建功德记录" @click="openCreate" /></view>
<view v-else class="merit-list">
<text v-if="saveNotice" class="save-notice">功德记录已提交并已从服务端重新读取</text>
<text class="merit-summary"> {{ merits.length }} 金额合计 ¥{{ totalAmount }}</text>
<view v-for="item in merits" :key="item.id" class="merit-card"><view><text>{{ item.title }}</text><text>{{ item.donor }}{{ item.type ? ` · ${item.type}` : '' }}</text><text v-if="item.time || item.content">{{ item.time || item.content }}</text></view><text>¥{{ item.amount }}</text></view>
</view>
</view>
<AppDialog :visible="discardVisible" title="放弃功德记录?" message="尚未提交的内容将被清除。" confirm-text="放弃并返回" cancel-text="继续填写" show-cancel :close-on-mask="false" @confirm="confirmDiscard" @cancel="cancelDiscard" />
</view>
</template>
<script setup>
import { computed, onUnmounted, reactive, ref } from "vue";
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const genealogyId = ref("");
const view = ref("list");
const listState = ref("loading");
const merits = ref([]);
const saveNotice = ref(false);
const submitting = ref(false);
const error = ref("");
const discardVisible = ref(false);
const form = reactive({ donor: "", title: "", type: "", amount: "", meritDate: "", meritClock: "", content: "", sortOrder: "" });
const fields = [
{ key: "donor", label: "捐赠人", required: true },
{ key: "title", label: "功德标题", required: true },
{ key: "type", label: "功德类型" },
{ key: "amount", inputType: "digit", label: "金额" },
{ key: "content", label: "记录内容" },
{ key: "sortOrder", label: "排序值", inputType: "number", placeholder: "数值越小越靠前" },
];
const controller = createRequestController();
let active = true;
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const dirty = computed(() => Object.values(form).some((value) => String(value).trim()));
const meritTime = computed(() => form.meritDate ? `${form.meritDate} ${form.meritClock || "00:00"}:00` : "");
const totalAmount = computed(() => merits.value.reduce((total, item) => total + item.amount, 0).toFixed(2));
const confirmation = createDiscardConfirmation((visible) => { discardVisible.value = visible; });
const confirmDiscard = () => { confirmation.confirm(); resetForm(); view.value = "list"; };
const cancelDiscard = confirmation.cancel;
const resetForm = () => { Object.assign(form, { donor: "", title: "", type: "", amount: "", meritDate: "", meritClock: "", content: "", sortOrder: "" }); error.value = ""; };
const loadMerits = async () => {
if (!valid.value) return;
controller.abort();
listState.value = "loading";
try {
const rows = await appApi.getMeritRecords(genealogyId.value, { requestController: controller });
if (!active) return;
merits.value = rows.map((item) => ({
id: String(item.meritId || ""),
donor: String(item.donorName || "未署名"),
title: String(item.meritTitle || "未命名功德"),
type: String(item.meritType || ""),
amount: Number.isFinite(Number(item.amount)) ? Number(item.amount) : 0,
time: String(item.meritTime || ""),
content: String(item.meritContent || ""),
})).filter((item) => /^[1-9]\d*$/.test(item.id));
listState.value = merits.value.length ? "ready" : "empty";
} catch (cause) {
if (!active || isRequestCancelled(cause)) return;
listState.value = "error";
}
};
const openCreate = () => { if (!valid.value) return; saveNotice.value = false; resetForm(); view.value = "form"; };
const cancelCreate = () => { resetForm(); view.value = "list"; };
const selectMeritDate = (event) => { form.meritDate = event.detail.value || ""; error.value = ""; };
const selectMeritClock = (event) => { form.meritClock = event.detail.value || ""; error.value = ""; };
const submit = async () => {
if (submitting.value || !valid.value) return;
const donorName = form.donor.trim();
const meritTitle = form.title.trim();
const amountText = form.amount.trim();
if (!donorName || !meritTitle) { error.value = !donorName ? "请填写捐赠人" : "请填写功德标题"; return; }
if (amountText && !Number.isFinite(Number(amountText))) { error.value = "金额必须是数字"; return; }
submitting.value = true;
error.value = "";
try {
await appApi.createMeritRecord(genealogyId.value, { donorName, meritTitle, meritType: form.type, meritContent: form.content, meritTime: meritTime.value, sortOrder: form.sortOrder, ...(amountText ? { amount: Number(amountText) } : {}) }, { requestController: controller });
resetForm();
view.value = "list";
saveNotice.value = true;
await loadMerits();
} catch (cause) {
if (!isRequestCancelled(cause)) error.value = cause?.message || "功德记录提交失败,请稍后重试。";
} finally { submitting.value = false; }
};
const requestBack = () => view.value !== "form"
? goBack()
: runBackGuard({ transientOpen: discardVisible.value, dirty: dirty.value, submitting: submitting.value, "close-transient": cancelDiscard, "block-submitting": () => true, "confirm-discard": confirmation.request });
onLoad((query) => { genealogyId.value = String(query?.genealogyId || ""); if (valid.value) loadMerits(); else listState.value = "invalid"; });
onShow(() => { if (valid.value && view.value === "list" && listState.value !== "loading") loadMerits(); });
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => { active = false; controller.abort(); confirmation.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-content { z-index: 1; }.page-content { padding: 18rpx 24rpx 72rpx; }
.form-card, .state-card, .merit-card { @include adaptive.adaptive-records-content; }.form-card { box-sizing: border-box; padding: 46rpx; }.form-card > text:first-child { display: block; color: $ink; font-size: 34rpx; font-weight: 700; }.form-copy { display: block; margin-top: 12rpx; color: $ink-muted; font-size: 22rpx; line-height: 1.55; }
.field { margin-top: 16rpx; }.field > text { display: block; margin: 0 8rpx 8rpx; color: $ink; font-size: 23rpx; font-weight: 700; }.required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; transform: translateY(-3rpx); }.field input, .field textarea, .field--picker picker > view { @include adaptive.adaptive-records-field; box-sizing: border-box; width: 100%; min-height: 76rpx; padding: 16rpx 22rpx; color: $ink; font-size: 23rpx; }.field textarea { min-height: 150rpx; }.field--picker picker { display: block; }.field--picker .placeholder { color: $ink-muted; }.error { display: block; margin-top: 12rpx; color: $brand-red; font-size: 22rpx; }
.form-actions { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.45fr); gap: 16rpx; margin-top: 20rpx; }.form-actions .app-button { width: 100%; min-width: 0; }.state-card { display: flex; min-height: 320rpx; flex-direction: column; align-items: center; justify-content: center; padding: 42rpx; color: $ink; font-size: 28rpx; text-align: center; }.state-card .app-button { width: 100%; margin-top: 24rpx; }.merit-list { display: flex; flex-direction: column; gap: 16rpx; }.save-notice { display: block; color: $brand-red; font-size: 22rpx; }.merit-summary { display: block; color: $ink-muted; font-size: 22rpx; }.merit-card { display: flex; min-height: 138rpx; align-items: center; justify-content: space-between; gap: 18rpx; padding: 28rpx 32rpx; }.merit-card > view { min-width: 0; flex: 1; }.merit-card text { display: block; }.merit-card > view text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; overflow-wrap: anywhere; }.merit-card > view text:not(:first-child) { margin-top: 7rpx; color: $ink-muted; font-size: 21rpx; line-height: 1.45; overflow-wrap: anywhere; }.merit-card > text { flex: 0 0 auto; color: $brand-red; font-size: 22rpx; }
</style>