266 lines
12 KiB
Vue
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 calc(72rpx + env(safe-area-inset-bottom)); }
|
|
.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; flex: 1; 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>
|