feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
+258
View File
@@ -0,0 +1,258 @@
<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>