Files
jiapuapp/pages/records/ceremony-detail.vue
T

686 lines
22 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.
<template>
<view class="ritual-detail-page">
<ModulePageBackground module="records" />
<view class="page-header">
<PageHeader
title="礼仪详情"
:action="ceremonyDetailState === 'ready' ? (giftFormVisible ? '收起' : '登记献礼') : ''"
custom-back
@back="requestBack"
@action="toggleGiftForm"
/>
</view>
<view class="page-content">
<view v-if="!valid" class="state-card">
<text>暂时无法打开礼仪活动</text>
<text>未找到这项活动请从活动列表重新进入</text>
<AppButton block label="返回上一页" @click="returnToList" />
</view>
<view v-else-if="ceremonyDetailState === 'loading'" class="state-card">
<AppLoading text="正在加载礼仪详情" />
</view>
<view v-else-if="ceremonyDetailState === 'error'" class="state-card">
<text>暂时无法加载礼仪详情</text>
<text>请检查网络后重新加载</text>
<AppButton block type="secondary" label="重新加载" @click="loadDetail" />
</view>
<view v-else>
<view class="detail-card">
<image
v-if="detail.coverFile?.accessUrl"
class="detail-card__cover"
:src="detail.coverFile.accessUrl"
mode="aspectFill"
role="button"
aria-label="查看礼仪活动封面"
@click="previewCover"
/>
<text>{{ detail.title }}</text>
<text>{{ detail.typeLabel }}{{ detail.time ? ` · ${detail.time}` : "" }}</text>
<text v-if="detail.location || detail.locationAddress">地点{{ detail.location || detail.locationAddress }}</text>
<text v-if="detail.description">{{ detail.description }}</text>
<text>已有 {{ detail.giftCount }} 笔献礼</text>
<view v-if="detail.canEdit || detail.canDelete" class="detail-card__actions">
<AppButton v-if="detail.canEdit" compact type="secondary" label="编辑活动" @click="openEditCeremony" />
<AppButton v-if="detail.canDelete" compact type="secondary" label="删除活动" @click="requestDeleteCeremony" />
</view>
<text v-if="deleteError" class="delete-error">{{ deleteError }}</text>
</view>
<view class="gift-list-card">
<view class="gift-list-card__heading">
<view>
<text>献礼记录</text>
<text>仅展示可查看的姓名金额留言和时间</text>
</view>
<AppButton
compact
type="secondary"
:disabled="giftListState === 'loading'"
:label="giftListState === 'loading' ? '加载中' : '查看清单'"
@click="loadGiftRecords"
/>
</view>
<AppLoading v-if="giftListState === 'loading'" text="正在加载献礼记录" />
<view v-else-if="giftListState === 'ready'" class="gift-list-card__rows">
<view v-for="item in giftRows" :key="item.id" class="gift-list-card__row">
<view>
<text>{{ item.giverName || item.giverNickName || '未署名献礼' }}</text>
<text v-if="item.time">{{ item.time }}</text>
<text v-if="item.message">{{ item.message }}</text>
</view>
<view class="gift-list-card__amount">
<text>{{ item.amount }}</text>
<AppButton
v-if="item.canDelete"
compact
type="secondary"
label="删除"
@click="requestDeleteGift(item)"
/>
</view>
</view>
</view>
<view v-else-if="giftListState === 'empty'" class="gift-list-card__empty">
<text>暂时没有献礼记录</text>
</view>
<view v-else-if="giftListState === 'error'" class="gift-list-card__error">
<text>暂时无法加载献礼记录</text>
<AppButton compact type="secondary" label="重新查看" @click="loadGiftRecords" />
</view>
</view>
<CeremonyInvitationManager
ref="ceremonyInvitationManager"
:genealogy-id="genealogyId"
:ceremony-id="ceremonyId"
@transient-change="invitationTransientOpen = $event"
@busy-change="invitationBusy = $event"
/>
<view v-if="giftFormVisible" class="gift-form-card">
<text class="gift-form-card__title">登记献礼</text>
<text class="gift-form-card__note">金额为必填项赠礼人和留言可按实际情况填写</text>
<view class="gift-field">
<text>赠礼人</text>
<input
v-model.trim="giftForm.giverName"
aria-label="赠礼人选填"
:disabled="giftSubmitting"
placeholder="请输入赠礼人姓名(选填)"
placeholder-class="gift-placeholder"
/>
</view>
<view class="gift-field">
<text><text class="required-mark">*</text>金额</text>
<input
v-model.trim="giftForm.giftAmount"
type="digit"
aria-label="献礼金额"
:aria-invalid="Boolean(giftError)"
:disabled="giftSubmitting"
placeholder="请输入献礼金额"
placeholder-class="gift-placeholder"
@input="giftError = ''"
/>
</view>
<view class="gift-message-field">
<text>留言</text>
<textarea
v-model.trim="giftForm.giftMessage"
auto-height
aria-label="献礼留言选填"
:disabled="giftSubmitting"
placeholder="写下对家族的祝福或缅怀(选填)"
placeholder-class="gift-placeholder"
/>
</view>
<text v-if="giftError" class="gift-error">{{ giftError }}</text>
<view class="gift-form-card__actions">
<AppButton type="secondary" label="取消" :disabled="giftSubmitting" @click="closeGiftForm" />
<AppButton :label="giftSubmitting ? '正在登记' : '确认登记献礼'" :disabled="giftSubmitting" @click="requestGiftSubmit" />
</view>
</view>
<text
v-if="giftResult"
class="gift-result"
:class="`gift-result--${giftResultTone}`"
:role="giftResultTone === 'error' ? 'alert' : 'status'"
>{{ giftResult }}</text>
</view>
</view>
<AppDialog
:visible="giftConfirmVisible"
eyebrow="献礼确认"
title="确认登记本次献礼?"
message="提交后会保存到当前礼仪活动。"
:confirm-text="giftSubmitting ? '正在登记' : '确认登记'"
cancel-text="继续填写"
show-cancel
:close-on-mask="false"
@confirm="confirmGiftSubmit"
@cancel="giftConfirmVisible = false"
/>
<AppDialog
:visible="giftDiscardVisible"
eyebrow="放弃确认"
title="放弃本次献礼?"
message="当前填写内容尚未提交,关闭后将被清除。"
confirm-text="确认放弃"
cancel-text="继续填写"
show-cancel
:close-on-mask="false"
@confirm="confirmGiftDiscard"
@cancel="cancelGiftDiscard"
/>
<AppDialog
:visible="ceremonyDeleteConfirmVisible"
:close-on-mask="false"
eyebrow="删除确认"
title="将这项礼仪活动移至回收站?"
message="活动和相关内容将不再展示,家谱管理员可在保留期内恢复。"
confirm-text="移至回收站"
cancel-text="保留活动"
show-cancel
@confirm="deleteCeremony"
@cancel="closeCeremonyDeleteConfirmation"
/>
<AppDialog
:visible="giftDeleteConfirmVisible"
:close-on-mask="false"
eyebrow="删除确认"
title="删除这笔献礼记录?"
message="删除后无法恢复,请确认当前记录不再需要。"
confirm-text="确认删除"
cancel-text="保留记录"
show-cancel
@confirm="deleteGift"
@cancel="closeGiftDeleteConfirmation"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onUnload } 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 CeremonyInvitationManager from "@/components/records/CeremonyInvitationManager.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { ceremonyApi } from "@/services/api/ceremony-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
import {
goBack,
handleBackPress,
openPage,
returnTo,
} from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const ceremonyId = ref("");
const ceremonyDetailState = ref("loading");
const detail = ref({
title: "",
type: "",
time: "",
location: "",
locationAddress: "",
description: "",
giftCount: 0,
});
// 详情与献礼可以同时刷新,独立控制器可避免一个区域取消另一区域的读取。
const ceremonyDetailRequestController = createRequestController();
const giftSubmissionRequestController = createRequestController();
const ceremonyGiftCreateGuard = createNonIdempotentWriteGuard();
const giftListRequestController = createRequestController();
const giftFormVisible = ref(false);
const giftConfirmVisible = ref(false);
const giftDiscardVisible = ref(false);
const giftSubmitting = ref(false);
const giftError = ref("");
const giftResult = ref("");
const giftResultTone = ref("");
const giftForm = reactive({
giverName: "",
giftAmount: "",
giftMessage: "",
});
const giftBaseline = ref("");
const giftRows = ref([]);
const giftListState = ref("idle");
const ceremonyInvitationManager = ref(null);
const invitationTransientOpen = ref(false);
const invitationBusy = ref(false);
const ceremonyDeleteConfirmVisible = ref(false);
const giftDeleteConfirmVisible = ref(false);
const giftDeleteTarget = ref(null);
const deleting = ref(false);
const deleteError = ref("");
const ceremonyContentDeletionRequestController = createRequestController();
let pageActive = true;
const valid = computed(
() =>
/^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(ceremonyId.value),
);
const previewCover = () => {
const url = detail.value?.coverFile?.accessUrl;
if (!url || typeof uni?.previewImage !== "function") return;
uni.previewImage({ current: url, urls: [url] });
};
const giftSnapshot = computed(() => JSON.stringify(giftForm));
const giftDirty = computed(
() => giftFormVisible.value && giftSnapshot.value !== giftBaseline.value,
);
const isPageWriteBusy = computed(
() => giftSubmitting.value || invitationBusy.value || deleting.value,
);
const resetGiftForm = () => {
Object.assign(giftForm, { giverName: "", giftAmount: "", giftMessage: "" });
giftBaseline.value = JSON.stringify(giftForm);
giftError.value = "";
};
const giftDiscard = createDiscardConfirmation((visible) => {
giftDiscardVisible.value = visible;
});
const requestGiftDiscard = giftDiscard.request;
const confirmGiftDiscard = giftDiscard.confirm;
const cancelGiftDiscard = giftDiscard.cancel;
const loadDetail = async ({ preserveCurrent = false } = {}) => {
if (!valid.value) return false;
ceremonyDetailRequestController.abort();
if (!preserveCurrent) ceremonyDetailState.value = "loading";
try {
const ceremonyDetail = await ceremonyApi.getCeremonyDetail(
genealogyId.value,
ceremonyId.value,
{ requestController: ceremonyDetailRequestController },
);
if (!pageActive) return false;
detail.value = ceremonyDetail;
ceremonyDetailState.value = "ready";
return true;
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return false;
if (!preserveCurrent) ceremonyDetailState.value = "error";
return false;
}
};
const loadGiftRecords = async () => {
if (!valid.value || giftListState.value === "loading") return;
giftListRequestController.abort();
giftListState.value = "loading";
try {
const rows = await ceremonyApi.getCeremonyGifts(
genealogyId.value,
ceremonyId.value,
{ requestController: giftListRequestController },
);
if (!pageActive) return;
giftRows.value = rows;
giftListState.value = rows.length ? "ready" : "empty";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
giftListState.value = "error";
}
};
const openGiftForm = () => {
resetGiftForm();
giftResult.value = "";
giftResultTone.value = "";
giftFormVisible.value = true;
};
const closeGiftForm = async () => {
if (giftSubmitting.value) return false;
if (giftDirty.value && !(await requestGiftDiscard())) return false;
giftConfirmVisible.value = false;
giftFormVisible.value = false;
resetGiftForm();
return true;
};
const toggleGiftForm = async () => {
if (giftFormVisible.value) return closeGiftForm();
openGiftForm();
return true;
};
const requestGiftSubmit = () => {
const giftAmount = giftForm.giftAmount.trim();
giftError.value = !giftAmount
? "请填写献礼金额"
: /^(?:0|[1-9]\d{0,9})(?:\.\d{1,2})?$/.test(giftAmount)
? ""
: "献礼金额应为 0 至 9999999999.99,最多保留两位小数";
if (!giftError.value && !giftSubmitting.value) giftConfirmVisible.value = true;
};
const confirmGiftSubmit = async () => {
if (giftSubmitting.value || !giftForm.giftAmount.trim()) return;
const payload = {
giverName: giftForm.giverName,
giftAmount: giftForm.giftAmount,
giftMessage: giftForm.giftMessage,
};
const createAttempt = ceremonyGiftCreateGuard.begin(payload);
if (createAttempt === null) {
giftConfirmVisible.value = false;
giftError.value =
"上次登记结果暂时无法确认,请先查看献礼记录,避免重复登记。";
return;
}
giftSubmitting.value = true;
giftError.value = "";
giftResult.value = "";
try {
await ceremonyApi.createCeremonyGift(
genealogyId.value,
ceremonyId.value,
payload,
{ requestController: giftSubmissionRequestController },
);
if (!pageActive) return;
giftConfirmVisible.value = false;
giftFormVisible.value = false;
resetGiftForm();
giftResultTone.value = "success";
giftResult.value = "献礼已登记。";
const refreshed = await loadDetail({ preserveCurrent: true });
if (giftListState.value !== "idle") await loadGiftRecords();
if (pageActive && !refreshed) {
giftResult.value = "献礼已登记,但礼仪详情暂未刷新。";
}
} catch (error) {
if (!pageActive) return;
if (ceremonyGiftCreateGuard.recordFailure(createAttempt, error)) {
giftError.value =
"登记结果暂时无法确认,请先查看献礼记录,避免重复登记。";
giftConfirmVisible.value = false;
return;
}
if (isRequestCancelled(error)) return;
giftError.value = getRequestErrorMessage(error, "献礼登记失败,请稍后重试。");
giftConfirmVisible.value = false;
} finally {
if (pageActive) giftSubmitting.value = false;
}
};
const requestDeleteCeremony = () => {
if (!detail.value?.canDelete || deleting.value) return;
deleteError.value = "";
ceremonyDeleteConfirmVisible.value = true;
};
const openEditCeremony = async () => {
if (!detail.value?.canEdit || !valid.value) return;
if (!(await closeGiftForm())) return;
return openPage(
"R07",
{ genealogyId: genealogyId.value, ceremonyId: ceremonyId.value, mode: "edit" },
"R06",
);
};
const closeCeremonyDeleteConfirmation = () => {
if (!deleting.value) ceremonyDeleteConfirmVisible.value = false;
};
const deleteCeremony = async () => {
if (!detail.value?.canDelete || deleting.value) return;
deleting.value = true;
deleteError.value = "";
let deletionCommitted = false;
try {
await ceremonyApi.deleteCeremony(genealogyId.value, ceremonyId.value, {
requestController: ceremonyContentDeletionRequestController,
});
deletionCommitted = true;
if (!pageActive) return;
ceremonyDeleteConfirmVisible.value = false;
detail.value = { ...detail.value, canDelete: false, canEdit: false };
giftConfirmVisible.value = false;
giftFormVisible.value = false;
resetGiftForm();
const returned = await returnToList();
if (pageActive && returned === false) {
deleteError.value = "礼仪活动已删除,但暂时无法返回活动列表。";
}
} catch (error) {
if (!pageActive) return;
if (deletionCommitted) {
ceremonyDeleteConfirmVisible.value = false;
detail.value = { ...detail.value, canDelete: false, canEdit: false };
deleteError.value = "礼仪活动已删除,但暂时无法返回活动列表。";
return;
}
if (isRequestCancelled(error)) return;
deleteError.value = getRequestErrorMessage(error, "礼仪活动删除失败,请稍后重试。");
ceremonyDeleteConfirmVisible.value = false;
} finally {
if (pageActive) deleting.value = false;
}
};
const requestDeleteGift = (gift) => {
if (!gift?.canDelete || deleting.value) return;
deleteError.value = "";
giftDeleteTarget.value = gift;
giftDeleteConfirmVisible.value = true;
};
const closeGiftDeleteConfirmation = () => {
if (deleting.value) return;
giftDeleteConfirmVisible.value = false;
giftDeleteTarget.value = null;
};
const deleteGift = async () => {
const gift = giftDeleteTarget.value;
if (!gift?.canDelete || deleting.value) return;
deleting.value = true;
deleteError.value = "";
try {
await ceremonyApi.deleteCeremonyGift(genealogyId.value, ceremonyId.value, gift.id, {
requestController: ceremonyContentDeletionRequestController,
});
if (!pageActive) return;
giftDeleteConfirmVisible.value = false;
giftDeleteTarget.value = null;
await loadDetail({ preserveCurrent: true });
await loadGiftRecords();
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
deleteError.value = getRequestErrorMessage(error, "献礼记录删除失败,请稍后重试。");
giftDeleteConfirmVisible.value = false;
} finally {
if (pageActive) deleting.value = false;
}
};
const closeActiveTransient = () => {
if (giftConfirmVisible.value) {
giftConfirmVisible.value = false;
return true;
}
if (giftDiscardVisible.value) {
cancelGiftDiscard();
return true;
}
if (
invitationTransientOpen.value &&
ceremonyInvitationManager.value?.closeTransient()
) {
return true;
}
if (ceremonyDeleteConfirmVisible.value) {
closeCeremonyDeleteConfirmation();
return true;
}
if (giftDeleteConfirmVisible.value) {
closeGiftDeleteConfirmation();
return true;
}
return false;
};
const returnToList = async () => {
if (!(await closeGiftForm())) return false;
return /^[1-9]\d*$/.test(genealogyId.value)
? returnTo("R05", { genealogyId: genealogyId.value })
: goBack();
};
const requestBack = async () => {
if (isPageWriteBusy.value) return true;
if (closeActiveTransient()) return true;
return returnToList();
};
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
ceremonyId.value = String(query?.ceremonyId || "");
if (valid.value) loadDetail();
else ceremonyDetailState.value = "invalid";
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
pageActive = false;
ceremonyDetailRequestController.abort();
giftSubmissionRequestController.abort();
giftListRequestController.abort();
ceremonyContentDeletionRequestController.abort();
giftDiscard.dispose();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.ritual-detail-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-header,
.page-content {
z-index: 1;
}
.page-content {
flex: 1;
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
}
.state-card,
.detail-card,
.gift-list-card,
.gift-form-card,
.gift-result {
@include adaptive-records-content;
background-color: rgba($paper, 0.82);
}
.gift-form-card {
margin-top: 18rpx;
padding: 34rpx 32rpx;
}
.detail-card__cover {
width: 100%;
height: 320rpx;
margin-bottom: 22rpx;
border-radius: 10rpx;
background: rgba(128, 89, 49, 0.12);
}
.gift-form-card__title,
.gift-form-card__note,
.gift-field > text,
.gift-message-field > text,
.gift-error,
.gift-result {
display: block;
}
.gift-form-card__title {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(18px, 32rpx, 23px);
font-weight: 700;
}
.gift-form-card__note {
margin-top: 10rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.55;
}
.gift-field {
display: grid;
grid-template-columns: 148rpx minmax(0, 1fr);
min-height: 90rpx;
align-items: center;
gap: 14rpx;
margin-top: 18rpx;
border-bottom: 1rpx solid rgba(181, 137, 63, 0.38);
}
.gift-field > text,
.gift-message-field > text {
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
font-weight: 700;
}
.required-mark {
color: $brand-red;
}
.gift-field input,
.gift-message-field textarea {
box-sizing: border-box;
min-width: 0;
color: $ink;
font-size: clamp(14px, 22rpx, 17px);
}
.gift-field input {
width: auto;
min-height: 80rpx;
text-align: right;
}
.gift-message-field {
margin-top: 22rpx;
}
.gift-message-field textarea {
display: block;
width: 100%;
min-height: 154rpx;
margin-top: 12rpx;
padding: 18rpx 20rpx;
border: 1rpx solid rgba(181, 137, 63, 0.42);
border-radius: 12rpx;
line-height: 1.55;
}
.gift-placeholder {
color: $ink-muted;
}
.gift-error {
margin-top: 14rpx;
color: #b42318;
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.5;
}
.gift-form-card__actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
margin-top: 26rpx;
}
.gift-result {
margin-top: 18rpx;
padding: 20rpx 24rpx;
color: #245f39;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.55;
}
.gift-result--success {
border-color: rgba(41, 112, 66, 0.34);
background: rgba(234, 246, 237, 0.88);
}
@media (max-width: 340px) {
.gift-form-card {
padding: 28rpx 24rpx;
}
.gift-field {
grid-template-columns: 126rpx minmax(0, 1fr);
}
}
</style>