feat: migrate app routes and business modules
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user