Files
jiapuapp/pages/genealogy/application-review.vue
T

259 lines
13 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="review-page">
<GenealogyPageBackground />
<view class="page-header">
<PageHeader title="申请审核" custom-back @back="returnToGenealogies" />
</view>
<view class="page-content">
<view v-if="!valid" class="state-card">
<text>暂时无法打开审核页面</text>
<text>未找到家谱信息请返回后重新进入</text>
<AppButton block label="返回我的家谱" @click="returnToGenealogies" />
</view>
<view v-else-if="applicationReviewState === 'loading'" class="state-card">
<AppLoading text="正在读取待审核申请" />
</view>
<view v-else-if="applicationReviewState === 'error'" class="state-card">
<text>暂时无法读取待审核申请</text>
<text>{{ applicationReviewError || "请检查网络后重试。" }}</text>
<AppButton block type="secondary" label="重新加载" @click="loadApplications" />
</view>
<view v-else-if="applicationReviewState === 'empty'" class="state-card">
<text>暂无待审核申请</text>
<text>新的加入申请会显示在这里</text>
<AppButton block label="返回家谱总览" @click="returnToOverview" />
</view>
<view v-else class="review-list">
<text class="review-list__count"> {{ applications.length }} 条待审核申请</text>
<text v-if="feedbackMessage" class="page-feedback" role="status">{{ feedbackMessage }}</text>
<view v-for="(item, index) in applications" :key="applicationKey(item, index)" class="review-card">
<view class="review-card__heading">
<text>{{ applicantName(item) }}</text>
<text>{{ applicationTime(item) }}</text>
</view>
<text v-if="applicantPhone(item)" class="review-card__phone">{{ applicantPhone(item) }}</text>
<text class="review-card__relation">{{ applicationRelation(item) }}</text>
<text v-if="applicationReason(item)" class="review-card__reason">{{ applicationReason(item) }}</text>
<view class="review-card__actions">
<AppButton compact type="secondary" label="拒绝" :disabled="!applicationId(item) || operationPending" @click="openAudit(item, JOIN_APPLICATION_STATUS.REJECTED)" />
<AppButton compact label="通过" :disabled="!applicationId(item) || operationPending" @click="openAudit(item, JOIN_APPLICATION_STATUS.APPROVED)" />
</view>
</view>
</view>
</view>
<AppDialog
:visible="Boolean(auditTarget)"
eyebrow="申请审核"
:title="auditTarget?.decision === JOIN_APPLICATION_STATUS.APPROVED ? '通过这条申请?' : '拒绝这条申请?'"
:message="auditDialogMessage"
:confirm-text="operationPending ? '正在提交…' : auditTarget?.decision === JOIN_APPLICATION_STATUS.APPROVED ? '确认通过' : '确认拒绝'"
cancel-text="取消"
show-cancel
:close-on-mask="!operationPending"
@confirm="submitAudit"
@cancel="closeAudit"
>
<view v-if="auditTarget?.decision === JOIN_APPLICATION_STATUS.REJECTED" class="rejection-field">
<text>拒绝原因</text>
<textarea
id="application-rejection-reason"
v-model.trim="rejectionReason"
auto-height
maxlength="120"
placeholder="请说明需要补充或核实的信息"
:disabled="operationPending"
:aria-invalid="!!rejectionError"
aria-describedby="application-rejection-error"
:focus="rejectionFocused"
@input="clearRejectionError"
/>
<text v-if="rejectionError" id="application-rejection-error" class="rejection-field__error" role="alert">{{ rejectionError }}</text>
</view>
<text v-if="operationError" class="dialog-error" role="alert">{{ operationError }}</text>
</AppDialog>
</view>
</template>
<script setup>
import { computed, nextTick, 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
} 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, returnTo, runBackGuard } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const applications = ref([]);
const applicationReviewState = ref("loading");
const applicationReviewError = ref("");
const auditTarget = ref(null);
const rejectionReason = ref("");
const rejectionError = ref("");
const rejectionFocused = ref(false);
const operationError = ref("");
const operationPending = ref(false);
const feedbackMessage = ref("");
const applicationListController = createRequestController();
const applicationAuditController = createRequestController();
let isPageActive = true;
const auditDialogMessage = computed(() => {
const name = applicantName(auditTarget.value?.item);
return auditTarget.value?.decision === JOIN_APPLICATION_STATUS.APPROVED
? `通过后,“${name}”将加入家谱。`
: "拒绝后,对方可在“我的申请”中查看处理结果。";
});
const valid = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
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 applicantName = (item) => String(item?.applicantName ?? item?.name ?? "申请人");
const applicantPhone = (item) => String(item?.phone ?? item?.mobile ?? "");
const applicationTime = (item) => String(item?.appliedAt ?? item?.applyTime ?? item?.createTime ?? item?.createdAt ?? "");
const applicationRelation = (item) => String(item?.relationDesc ?? item?.relation ?? "未填写关系说明");
const applicationReason = (item) => String(item?.applyReason ?? item?.reason ?? "");
const loadApplications = async () => {
if (!valid.value) return;
applicationListController.abort();
applicationReviewState.value = "loading";
applicationReviewError.value = "";
feedbackMessage.value = "";
try {
const loadedApplications = await genealogyMembershipApi.getPendingApplications(genealogyId.value, {
requestController: applicationListController,
});
if (!isPageActive) return;
applications.value = loadedApplications;
applicationReviewState.value = applications.value.length ? "ready" : "empty";
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
applicationReviewError.value = getRequestErrorMessage(error, "请稍后重试。");
applicationReviewState.value = "error";
}
};
const returnToGenealogies = () => returnTo("G01");
const returnToOverview = () => returnTo("G05", { genealogyId: genealogyId.value });
const clearRejectionError = () => {
rejectionError.value = "";
operationError.value = "";
};
const openAudit = (item, decision) => {
rejectionReason.value = "";
rejectionError.value = "";
rejectionFocused.value = false;
operationError.value = "";
auditTarget.value = { item, decision };
};
const resetAuditDialog = () => {
auditTarget.value = null;
rejectionReason.value = "";
rejectionError.value = "";
rejectionFocused.value = false;
operationError.value = "";
};
const closeAudit = () => {
if (operationPending.value) return;
resetAuditDialog();
};
const submitAudit = async () => {
const current = auditTarget.value;
const id = applicationId(current?.item);
if (!current || !id || operationPending.value) return;
if (current.decision === JOIN_APPLICATION_STATUS.REJECTED && !rejectionReason.value.trim()) {
rejectionError.value = "请填写拒绝原因,方便申请人补充资料。";
rejectionFocused.value = false;
await nextTick();
rejectionFocused.value = true;
return;
}
operationPending.value = true;
operationError.value = "";
try {
await genealogyMembershipApi.auditApplication(
genealogyId.value,
id,
{
status: current.decision,
...(current.decision === JOIN_APPLICATION_STATUS.REJECTED ? { auditRemark: rejectionReason.value.trim() } : {}),
},
{ requestController: applicationAuditController },
);
if (!isPageActive) return;
applications.value = applications.value.filter((item) => applicationId(item) !== id);
applicationReviewState.value = applications.value.length ? "ready" : "empty";
feedbackMessage.value = current.decision === JOIN_APPLICATION_STATUS.APPROVED ? "申请已通过。" : "申请已拒绝,并已附上审核说明。";
resetAuditDialog();
} catch (error) {
if (!isPageActive || isRequestCancelled(error)) return;
operationError.value = getRequestErrorMessage(error, "提交审核失败,请稍后重试。");
} finally {
if (isPageActive) operationPending.value = false;
}
};
const requestBack = () =>
runBackGuard({
transientOpen: Boolean(auditTarget.value),
submitting: operationPending.value,
"close-transient": closeAudit,
"block-submitting": () => true,
});
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
if (valid.value) loadApplications();
});
onShow(() => {
if (valid.value && applicationReviewState.value !== "loading" && !operationPending.value) loadApplications();
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
isPageActive = false;
applicationListController.abort();
applicationAuditController.abort();
});
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.review-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, .review-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, .review-card__phone, .review-card__relation, .review-card__reason, .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; }
.review-list { display: flex; flex-direction: column; gap: 16rpx; }
.review-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); }
.review-card { padding: 28rpx 30rpx; }
.review-card__heading { display: flex; align-items: start; justify-content: space-between; gap: 20rpx; }
.review-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; }
.review-card__heading text:last-child { flex: 0 0 auto; color: $ink-muted; font-size: clamp(13px, 21rpx, 16px); text-align: right; }
.review-card__phone, .review-card__relation, .review-card__reason { margin-top: 12rpx; color: $ink-muted; font-size: clamp(15px, 24rpx, 18px); line-height: 1.55; overflow-wrap: anywhere; }
.review-card__reason { color: #725840; }
.review-card__actions { display: flex; justify-content: flex-end; margin-top: 22rpx; gap: 16rpx; }
.review-card__actions .app-button { min-width: 140rpx; }
.rejection-field { width: 100%; margin-top: 22rpx; text-align: left; }
.rejection-field > text:first-child { display: block; color: $ink; font-size: clamp(15px, 24rpx, 18px); font-weight: 700; }
.rejection-field textarea { @include adaptive.adaptive-genealogy-form-field; box-sizing: border-box; display: block; width: 100%; min-height: 118rpx; margin-top: 10rpx; padding: 16rpx 20rpx; color: $ink; font-size: clamp(15px, 24rpx, 18px); line-height: 1.55; }
.rejection-field__error, .dialog-error { display: block; width: 100%; margin-top: 10rpx; color: $brand-red; font-size: clamp(14px, 23rpx, 17px); line-height: 1.5; text-align: left; }
</style>