259 lines
13 KiB
Vue
259 lines
13 KiB
Vue
<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>
|