Files
jiapuapp/pages/genealogy/my-applications.vue
T

266 lines
12 KiB
Vue

<template>
<view class="applications-page">
<GenealogyPageBackground />
<view class="page-header">
<PageHeader title="我的申请" custom-back @back="returnToGenealogies" />
</view>
<view class="page-content">
<view v-if="applicationListState === 'loading'" class="state-card">
<AppLoading text="正在读取我的申请" />
</view>
<view v-else-if="applicationListState === 'error'" class="state-card">
<text>暂时无法读取我的申请</text>
<text>{{ applicationListError || "请检查网络后重试。" }}</text>
<AppButton block type="secondary" label="重新加载" @click="loadApplications" />
</view>
<view v-else-if="applicationListState === 'empty'" class="state-card">
<text>暂无加入申请</text>
<text>从公开家谱提交的申请会显示在这里</text>
<AppButton block label="查找公开家谱" @click="toSearch" />
</view>
<view v-else class="application-list">
<text class="application-list__count"> {{ visibleApplications.length }} 条申请</text>
<text v-if="feedbackMessage" class="page-feedback" role="status">{{ feedbackMessage }}</text>
<view
v-for="(item, index) in visibleApplications"
:key="applicationKey(item, index)"
class="application-card"
>
<view class="application-card__heading">
<text>{{ applicationGenealogyName(item) }}</text>
<text>{{ applicationTime(item) }}</text>
</view>
<text class="application-card__relation">{{ applicationRelation(item) }}</text>
<text
class="application-card__status"
:class="`application-card__status--${applicationStatusTone(applicationStatus(item))}`"
>{{ statusLabel(applicationStatus(item)) }}</text>
<text v-if="applicationRemark(item)" class="application-card__remark">{{
applicationRemark(item)
}}</text>
<view class="application-card__actions">
<AppButton
v-if="applicationStatus(item) === JOIN_APPLICATION_STATUS.PENDING"
compact
type="secondary"
label="撤回申请"
:disabled="!applicationId(item) || operationPending"
@click="openWithdraw(item)"
/>
<AppButton
v-else-if="applicationStatus(item) === JOIN_APPLICATION_STATUS.APPROVED"
compact
label="查看家谱"
:disabled="!applicationGenealogyId(item)"
@click="openGenealogy(item)"
/>
<AppButton
v-else-if="applicationStatus(item) === JOIN_APPLICATION_STATUS.REJECTED"
compact
type="secondary"
label="重新申请"
:disabled="!applicationGenealogyId(item)"
@click="reapply(item)"
/>
</view>
</view>
</view>
</view>
<AppDialog
:visible="Boolean(withdrawTarget)"
eyebrow="申请管理"
title="撤回这条申请?"
:message="withdrawTarget ? `将撤回“${applicationGenealogyName(withdrawTarget)}”的加入申请。` : ''"
:confirm-text="operationPending ? '正在撤回…' : '确认撤回'"
cancel-text="暂不撤回"
show-cancel
:close-on-mask="!operationPending"
@confirm="withdrawApplication"
@cancel="closeWithdraw"
>
<text v-if="operationError" class="dialog-error" role="alert">{{ operationError }}</text>
</AppDialog>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onBackPress, 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 GenealogyPageBackground from "@/components/genealogy/PageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
JOIN_APPLICATION_STATUS,
JOIN_APPLICATION_STATUS_LABELS
} from "@/services/api/genealogy-membership-contract.js";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { genealogyMembershipApi } from "@/services/api/genealogy-membership-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { handleBackPress, openPage, returnTo, runBackGuard } from "@/utils/navigation/gateway.js";
const applications = ref([]);
const applicationListState = ref("loading");
const applicationListError = ref("");
const requestedStatus = ref("");
const withdrawTarget = ref(null);
const operationPending = ref(false);
const operationError = ref("");
const feedbackMessage = ref("");
const applicationListController = createRequestController();
const applicationWithdrawalController = createRequestController();
let isPageActive = true;
const applicationId = (item) => {
const applicationIdValue = item?.applyId ?? item?.applicationId ?? item?.id;
const text = String(applicationIdValue ?? "").trim();
return /^[1-9]\d*$/.test(text) ? text : "";
};
const applicationKey = (item, index) => applicationId(item) || `application-${index}`;
const applicationGenealogyId = (item) => {
const genealogyIdValue = item?.genealogyId ?? item?.familyId;
const text = String(genealogyIdValue ?? "").trim();
return /^[1-9]\d*$/.test(text) ? text : "";
};
const applicationGenealogyName = (item) =>
String(item?.genealogyName ?? item?.familyName ?? item?.genealogyTitle ?? "家谱加入申请");
const applicationTime = (item) =>
String(item?.appliedAt ?? item?.applyTime ?? item?.createTime ?? item?.createdAt ?? "");
const applicationRelation = (item) =>
String(item?.relationDesc ?? item?.relation ?? "未填写关系说明");
const applicationRemark = (item) =>
String(item?.auditRemark ?? item?.rejectionReason ?? item?.reason ?? "");
const applicationStatus = (item) =>
String(item?.status ?? JOIN_APPLICATION_STATUS.PENDING).trim();
const statusLabel = (status) => JOIN_APPLICATION_STATUS_LABELS[status] || "状态待确认";
const applicationStatusTones = Object.freeze({
[JOIN_APPLICATION_STATUS.PENDING]: "pending",
[JOIN_APPLICATION_STATUS.APPROVED]: "approved",
[JOIN_APPLICATION_STATUS.REJECTED]: "rejected",
[JOIN_APPLICATION_STATUS.CANCELLED]: "cancelled",
});
const applicationStatusTone = (status) => applicationStatusTones[status] || "unknown";
const visibleApplications = computed(() =>
requestedStatus.value
? applications.value.filter((item) => applicationStatus(item) === requestedStatus.value)
: applications.value,
);
const loadApplications = async () => {
applicationListController.abort();
applicationListState.value = "loading";
applicationListError.value = "";
feedbackMessage.value = "";
try {
const loadedApplications = await genealogyMembershipApi.getMyJoinApplications({
requestController: applicationListController,
});
if (!isPageActive) return;
applications.value = loadedApplications;
applicationListState.value = visibleApplications.value.length ? "ready" : "empty";
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
applicationListError.value = getRequestErrorMessage(error, "请稍后重试。");
applicationListState.value = "error";
}
};
const returnToGenealogies = () => returnTo("G01");
const toSearch = () => openPage("G06", {}, "G09");
const openGenealogy = (item) =>
openPage("G05", { genealogyId: applicationGenealogyId(item) }, "G09");
const reapply = (item) =>
openPage("G08", { genealogyId: applicationGenealogyId(item), source: "search" }, "G09");
const openWithdraw = (item) => {
operationError.value = "";
withdrawTarget.value = item;
};
const closeWithdraw = () => {
if (operationPending.value) return;
withdrawTarget.value = null;
operationError.value = "";
};
const withdrawApplication = async () => {
const application = withdrawTarget.value;
const id = applicationId(application);
if (!application || !id || operationPending.value) return;
operationPending.value = true;
operationError.value = "";
try {
await genealogyMembershipApi.withdrawJoinApplication(id, {
requestController: applicationWithdrawalController,
});
if (!isPageActive) return;
applications.value = applications.value.map((item) =>
applicationId(item) === id
? { ...item, status: JOIN_APPLICATION_STATUS.CANCELLED }
: item,
);
applicationListState.value = visibleApplications.value.length ? "ready" : "empty";
feedbackMessage.value = "申请已撤回。";
withdrawTarget.value = null;
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
operationError.value = getRequestErrorMessage(error, "撤回申请失败,请稍后重试。");
} finally {
if (isPageActive) operationPending.value = false;
}
};
const requestBack = () =>
runBackGuard({
transientOpen: Boolean(withdrawTarget.value),
submitting: operationPending.value,
"close-transient": closeWithdraw,
"block-submitting": () => true,
});
onLoad((query) => {
const status = String(query?.status || "").trim();
requestedStatus.value = Object.values(JOIN_APPLICATION_STATUS).includes(status)
? status
: "";
loadApplications();
});
onShow(() => {
if (applicationListState.value !== "loading" && !operationPending.value) loadApplications();
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
isPageActive = false;
applicationListController.abort();
applicationWithdrawalController.abort();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.applications-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, .application-card { box-sizing: border-box; @include adaptive.adaptive-genealogy-state-panel; }
.state-card { min-height: 340rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
.state-card text, .application-card__relation, .application-card__remark, .page-feedback { display: block; }
.state-card text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: clamp(19px, 35rpx, 24px); font-weight: 700; }
.state-card text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: clamp(15px, 24rpx, 18px); line-height: 1.65; }
.state-card .app-button { margin-top: 28rpx; }
.application-list { display: flex; flex-direction: column; gap: 16rpx; }
.application-list__count { color: $ink-muted; font-size: clamp(13px, 21rpx, 16px); }
.page-feedback { padding: 18rpx 22rpx; border: 1rpx solid rgba(66, 107, 88, .32); border-radius: 12rpx; background: rgba(66, 107, 88, .08); color: #426b58; font-size: clamp(14px, 23rpx, 17px); }
.application-card { padding: 28rpx 30rpx; }
.application-card__heading { display: flex; align-items: start; justify-content: space-between; gap: 20rpx; }
.application-card__heading text:first-child { min-width: 0; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: clamp(18px, 32rpx, 23px); font-weight: 700; overflow-wrap: anywhere; }
.application-card__heading text:last-child { flex: 0 0 auto; color: $ink-muted; font-size: clamp(13px, 21rpx, 16px); text-align: right; }
.application-card__relation { margin-top: 14rpx; color: $ink-muted; font-size: clamp(15px, 24rpx, 18px); line-height: 1.55; overflow-wrap: anywhere; }
.application-card__status { display: block; margin-top: 16rpx; color: $brand-red; font-size: clamp(15px, 24rpx, 18px); font-weight: 700; }
.application-card__status--approved { color: #426b58; }
.application-card__status--rejected { color: #886037; }
.application-card__status--cancelled, .application-card__status--unknown { color: $ink-muted; }
.application-card__remark { margin-top: 8rpx; color: $ink-muted; font-size: clamp(14px, 22rpx, 17px); line-height: 1.5; overflow-wrap: anywhere; }
.application-card__actions { display: flex; justify-content: flex-end; margin-top: 22rpx; }
.application-card__actions .app-button { min-width: 164rpx; }
.dialog-error { display: block; width: 100%; margin-top: 18rpx; color: $brand-red; font-size: clamp(14px, 23rpx, 17px); line-height: 1.5; text-align: left; }
</style>