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
+295
View File
@@ -0,0 +1,295 @@
<template>
<view class="join-page">
<GenealogyPageBackground />
<view class="page-header"
><PageHeader title="申请加入家谱" custom-back @back="requestBack"
/></view>
<view class="page-content">
<view v-if="!hasValidContext" class="state-card">
<text>暂时无法提交申请</text>
<text>未找到可申请的家谱请返回后重新进入</text>
<AppButton block label="返回搜索家谱" @click="backToSearch" />
</view>
<view v-else-if="joinApplicationState === 'success'" class="state-card">
<text>申请已提交</text>
<text>申请已提交可在我的申请中查看审核进度</text>
<AppButton block label="查看我的申请" @click="openMyApplications" />
</view>
<view v-else class="join-form">
<view class="form-heading">
<text>申请加入</text>
<text>{{ genealogyName || "当前家谱" }}</text>
</view>
<view v-for="field in fields" :key="field.key" class="form-field">
<text>{{ field.label }}</text>
<textarea
v-if="field.multiline"
v-model.trim="form[field.key]"
auto-height
:maxlength="field.maxlength"
:placeholder="field.placeholder"
:disabled="joinApplicationState === 'submitting'"
@input="error = ''"
/>
<input
v-else
v-model.trim="form[field.key]"
:type="field.inputType || 'text'"
:maxlength="field.maxlength"
:placeholder="field.placeholder"
:disabled="joinApplicationState === 'submitting'"
@input="error = ''"
/>
<text v-if="field.key === 'relationDesc'" class="form-field__hint"
>请填写您与家谱成员的关系例如我是某某某的堂侄</text
>
</view>
<text v-if="error" class="form-error">{{ error }}</text>
<AppButton
block
:disabled="joinApplicationState === 'submitting'"
:label="joinApplicationState === 'submitting' ? '正在提交' : '提交申请'"
@click="submitJoinApplication"
/>
</view>
</view>
<AppDialog
:visible="discardVisible"
:close-on-mask="false"
eyebrow="放弃确认"
title="放弃当前申请?"
message="当前填写内容尚未提交,离开后会清除。"
confirm-text="确认放弃"
cancel-text="继续填写"
show-cancel
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import GenealogyPageBackground from "@/components/genealogy/PageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
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 { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
import { handleBackPress, openPage, returnTo, runBackGuard } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const genealogyName = ref("");
const joinApplicationState = ref("form");
const error = ref("");
const discardVisible = ref(false);
const form = reactive({
applicantName: "",
phone: "",
relationDesc: "",
applyReason: "",
});
const fields = [
{
key: "applicantName",
label: "申请人姓名",
maxlength: 50,
placeholder: "填写您的姓名",
},
{
key: "phone",
label: "手机号",
inputType: "number",
maxlength: 11,
placeholder: "填写联系电话",
},
{
key: "relationDesc",
label: "关系说明",
maxlength: 100,
placeholder: "例如:本族成员",
},
{
key: "applyReason",
label: "申请理由",
multiline: true,
maxlength: 500,
placeholder: "说明申请加入的原因",
},
];
const joinApplicationSubmitController = createRequestController();
const joinApplicationGuard = createNonIdempotentWriteGuard();
let pageActive = true;
const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value));
const dirty = computed(() => Object.values(form).some((value) => value.trim()));
const confirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const confirmDiscard = confirmation.confirm;
const cancelDiscard = confirmation.cancel;
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
genealogyName.value = String(query?.genealogyName || "");
});
const backToSearch = () => returnTo("G06");
const openMyApplications = () => openPage("G09", {}, "G08");
const submitJoinApplication = async () => {
if (joinApplicationState.value === "submitting" || !hasValidContext.value) return;
const payload = { ...form };
const submitAttempt = joinApplicationGuard.begin(payload);
if (submitAttempt === null) {
error.value =
"上次提交结果暂时无法确认,请先查看“我的申请”,避免重复提交。";
return;
}
joinApplicationState.value = "submitting";
error.value = "";
try {
await genealogyMembershipApi.applyToJoin(
genealogyId.value,
payload,
{ requestController: joinApplicationSubmitController },
);
if (!pageActive) return;
joinApplicationState.value = "success";
} catch (cause) {
if (!pageActive) return;
joinApplicationState.value = "form";
if (joinApplicationGuard.recordFailure(submitAttempt, cause)) {
error.value =
"申请结果暂时无法确认,请先查看“我的申请”,避免重复提交。";
return;
}
if (!isRequestCancelled(cause))
error.value = getRequestErrorMessage(cause, "申请提交失败,请稍后重试。");
}
};
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: dirty.value && joinApplicationState.value === "form",
submitting: joinApplicationState.value === "submitting",
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": confirmation.request,
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
pageActive = false;
joinApplicationSubmitController.abort();
confirmation.dispose();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.join-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,
.join-form {
@include adaptive-genealogy-state-panel;
}
.state-card {
min-height: 340rpx;
padding: 78rpx 52rpx 50rpx;
text-align: center;
}
.state-card text {
display: block;
}
.state-card text:first-child,
.form-heading 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,
.join-form .app-button {
margin-top: 28rpx;
}
.join-form {
padding: 30rpx;
}
.form-heading {
display: flex;
flex-direction: column;
gap: 8rpx;
padding-bottom: 22rpx;
border-bottom: 1rpx solid rgba(128, 89, 49, 0.24);
}
.form-heading text:last-child {
color: $brand-red;
font-size: clamp(15px, 24rpx, 18px);
}
.form-field {
margin-top: 22rpx;
}
.form-field > text {
display: block;
color: $ink;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
}
.form-field input,
.form-field textarea {
@include adaptive-genealogy-form-field;
box-sizing: border-box;
display: block;
width: 100%;
margin-top: 10rpx;
padding: 16rpx 22rpx;
color: $ink;
font-size: clamp(15px, 24rpx, 18px);
}
.form-field > text.form-field__hint {
margin-top: 8rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
font-weight: 400;
line-height: 1.5;
}
.form-field input {
min-height: 76rpx;
padding-top: 0;
padding-bottom: 0;
}
.form-field textarea {
min-height: 120rpx;
line-height: 1.55;
}
.form-error {
display: block;
margin-top: 16rpx;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.45;
}
</style>