Files
jiapuapp/pages/records/relative-records.vue
T

334 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.
<template>
<view class="gift-page">
<ModulePageBackground module="records" />
<view class="page-header"
><PageHeader
title="贺礼簿"
:action="valid ? '新建' : ''"
custom-back
@back="returnToFamily"
@action="createRelative"
/></view>
<view class="page-content">
<view v-if="!valid" class="state-card"
><text>暂时无法打开贺礼簿</text
><AppButton block label="返回上一页" @click="returnToFamily"
/></view>
<view v-else-if="relativeRecordListState === 'loading'" class="state-card"
><AppLoading text="正在读取亲友往来"
/></view>
<view v-else-if="relativeRecordListState === 'error'" class="state-card"
><text>暂时无法读取亲友往来</text
><AppButton
block
type="secondary"
label="重新加载"
@click="loadRecords"
/></view>
<view v-else-if="relativeRecordListState === 'empty'" class="state-card"
><text>还没有亲友往来记录</text
><AppButton block label="新建往来记录" @click="createRelative"
/></view>
<view v-else class="record-list">
<text v-if="deleteError" class="delete-error">{{ deleteError }}</text>
<view v-for="item in records" :key="item.id" class="record-card" role="button" :aria-label="`查看${item.name}的往来详情`" @click="openRecordDetail(item)">
<view
><text>{{ item.name }}</text
><text
>{{ item.relation
}}{{ item.event ? ` · ${item.event}` : "" }}</text
><text v-if="item.time">{{ item.time }}</text
><text v-if="item.content">{{ item.content }}</text></view
>
<view class="record-card__amount">
<text v-if="item.amount">¥{{ item.amount }}</text>
<AppButton
v-if="item.canEdit"
compact
type="secondary"
label="编辑"
@click.stop="openEditRelative(item)"
/>
<AppButton
v-if="item.canDelete"
compact
type="secondary"
label="删除"
@click.stop="requestDeleteRecord(item)"
/>
</view>
</view>
</view>
</view>
<AppDialog
:visible="Boolean(detailTarget)"
eyebrow="往来详情"
:title="detailTarget?.name || '亲友往来'"
confirm-text="关闭"
:close-on-mask="detailState !== 'loading'"
@confirm="closeRecordDetail"
@cancel="closeRecordDetail"
>
<view class="detail-content">
<AppLoading v-if="detailState === 'loading'" text="正在读取完整记录" />
<text v-else-if="detailState === 'error'" class="detail-error">{{ detailError }}</text>
<template v-else>
<text>关系{{ detailTarget?.relation || "未填写" }}</text>
<text>事项{{ detailTarget?.event || "未填写" }}</text>
<text>时间{{ detailTarget?.time || "未填写" }}</text>
<text>金额{{ detailTarget?.amount ? ${detailTarget.amount}` : "未填写" }}</text>
<text class="detail-content__body">{{ detailTarget?.content || "未填写记录内容" }}</text>
<view v-if="detailTarget?.mediaFiles?.length" class="detail-media">
<image v-for="file in detailTarget.mediaFiles" :key="file.fileId" :src="file.accessUrl" mode="aspectFill" role="button" aria-label="查看往来记录图片" @click="previewDetailMedia(file)" />
</view>
</template>
</view>
</AppDialog>
<AppDialog
:visible="deleteConfirmationVisible"
title="删除这条亲友往来?"
message="删除后无法恢复,请确认当前内容不再需要。"
confirm-text="确认删除"
cancel-text="保留记录"
show-cancel
:close-on-mask="false"
@confirm="deleteRecord"
@cancel="closeDeleteConfirmation"
/>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad, onShow, 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 {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { lifeRecordApi } from "@/services/api/life-record-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { goBack, openPage, returnTo } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const records = ref([]);
const relativeRecordListState = ref("loading");
const relativeRecordListController = createRequestController();
const relativeRecordDeleteController = createRequestController();
const deleteConfirmationVisible = ref(false);
const deleteTarget = ref(null);
const deleting = ref(false);
const deleteError = ref("");
const detailTarget = ref(null);
const detailState = ref("idle");
const detailError = ref("");
const relativeRecordDetailController = createRequestController();
let isPageActive = true;
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const loadRecords = async () => {
if (!valid.value) return;
relativeRecordListController.abort();
relativeRecordListState.value = "loading";
try {
const rows = await lifeRecordApi.getRelativeRecords(genealogyId.value, {
requestController: relativeRecordListController,
});
if (!isPageActive) return;
records.value = rows;
relativeRecordListState.value = records.value.length ? "ready" : "empty";
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
relativeRecordListState.value = "error";
}
};
const returnToFamily = () =>
valid.value ? returnTo("F01", { genealogyId: genealogyId.value }) : goBack();
const createRelative = () =>
valid.value
? openPage("R04", { genealogyId: genealogyId.value, mode: "create" }, "R03")
: Promise.resolve(false);
const openEditRelative = (record) =>
record?.canEdit && valid.value
? openPage(
"R04",
{ genealogyId: genealogyId.value, mode: "edit", relativeId: record.id },
"R03",
)
: Promise.resolve(false);
const openRecordDetail = async (record) => {
if (!record?.id || detailState.value === "loading") return;
detailTarget.value = record;
detailState.value = "loading";
detailError.value = "";
relativeRecordDetailController.abort();
try {
const loadedRecord = await lifeRecordApi.getRelativeRecordDetail(genealogyId.value, record.id, {
requestController: relativeRecordDetailController,
});
if (!isPageActive) return;
detailTarget.value = loadedRecord;
detailState.value = "ready";
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
detailState.value = "error";
detailError.value = getRequestErrorMessage(error, "完整记录读取失败,请稍后重试。");
}
};
const closeRecordDetail = () => {
if (detailState.value === "loading") return;
detailTarget.value = null;
detailState.value = "idle";
detailError.value = "";
};
const previewDetailMedia = (file) => {
const urls = detailTarget.value?.mediaFiles?.map((item) => item.accessUrl).filter(Boolean) || [];
if (!file?.accessUrl || !urls.length || typeof uni?.previewImage !== "function") return;
uni.previewImage({ current: file.accessUrl, urls });
};
const requestDeleteRecord = (record) => {
if (!record?.canDelete || deleting.value) return;
deleteError.value = "";
deleteTarget.value = record;
deleteConfirmationVisible.value = true;
};
const closeDeleteConfirmation = () => {
if (deleting.value) return;
deleteConfirmationVisible.value = false;
deleteTarget.value = null;
};
const deleteRecord = async () => {
const record = deleteTarget.value;
if (!record?.canDelete || deleting.value) return;
deleting.value = true;
deleteError.value = "";
try {
await lifeRecordApi.deleteRelativeRecord(genealogyId.value, record.id, {
requestController: relativeRecordDeleteController,
});
if (!isPageActive) return;
deleteConfirmationVisible.value = false;
deleteTarget.value = null;
await loadRecords();
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
deleteError.value = getRequestErrorMessage(error, "亲友往来删除失败,请稍后重试。");
deleteConfirmationVisible.value = false;
} finally {
if (isPageActive) deleting.value = false;
}
};
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
if (valid.value) loadRecords();
});
onShow(() => {
if (valid.value && relativeRecordListState.value !== "loading") loadRecords();
});
onUnload(() => {
isPageActive = false;
relativeRecordListController.abort();
relativeRecordDeleteController.abort();
relativeRecordDetailController.abort();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.gift-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-header,
.page-content {
z-index: 1;
}
.page-content {
padding: 18rpx 24rpx 72rpx;
}
.state-card,
.record-card {
@include adaptive-records-content;
}
.state-card {
display: flex;
min-height: 320rpx;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 42rpx;
text-align: center;
color: $ink;
font-size: clamp(16px, 28rpx, 20px);
}
.state-card .app-button {
width: 100%;
margin-top: 24rpx;
}
.record-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.record-card {
display: flex;
min-height: 138rpx;
flex-direction: column;
align-items: stretch;
gap: 18rpx;
padding: 28rpx 32rpx;
}
.record-card > view {
min-width: 0;
flex: 1;
}
.record-card text {
display: block;
}
.record-card > view text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(17px, 30rpx, 22px);
font-weight: 700;
overflow-wrap: anywhere;
}
.record-card > view text:not(:first-child) {
margin-top: 7rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
line-height: 1.45;
overflow-wrap: anywhere;
}
.record-card__amount {
display: flex;
width: 100%;
align-items: center;
gap: 10rpx;
}
.record-card__amount > text {
margin-right: auto;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
}
.record-card__amount .app-button {
width: 140rpx;
min-height: 68rpx;
}
.delete-error {
display: block;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
}
.detail-content { width: 100%; margin-top: 18rpx; text-align: left; }
.detail-content > text { display: block; margin-top: 9rpx; color: $ink-muted; font-size: clamp(14px, 23rpx, 17px); line-height: 1.55; overflow-wrap: anywhere; }
.detail-content__body { padding-top: 10rpx; border-top: 1rpx solid rgba(142, 95, 41, .2); color: $ink !important; white-space: pre-wrap; }
.detail-error { color: $brand-red !important; }
.detail-media { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 16rpx; gap: 10rpx; }
.detail-media image { width: 100%; height: 150rpx; border-radius: 8rpx; background: rgba(128, 89, 49, .12); }
</style>