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
+310
View File
@@ -0,0 +1,310 @@
<template>
<view
class="rank-page"
:class="{
'rank-state--loading': rankState === 'loading',
'rank-state--form': rankState === 'form',
'rank-state--error': rankState === 'error',
}"
>
<ModulePageBackground module="tree" />
<view class="rank-page__header"
><PageHeader title="调整排行" custom-back @back="requestBack"
/></view>
<view class="rank-panel">
<AppLoading
v-if="rankState === 'loading'"
text="正在读取成员资料"
description="请稍候,正在确认待调整人物。"
/>
<view v-else-if="rankState === 'form' && member" class="rank-form">
<text class="form-eyebrow">同辈排行</text>
<text class="form-title">调整{{ member.name }}的排行</text>
<text class="form-copy"
>这里只调整这位成员的排行同辈成员的整体排序暂不能在这里修改</text
>
<view class="member-context">
<text>当前成员</text
><text> {{ member.generation }} · {{ member.branch }}</text>
</view>
<view class="rank-field">
<text>排序值</text>
<input
v-model="sortOrder"
type="number"
:disabled="savingRank"
placeholder="请输入整数,值越小越靠前"
placeholder-class="rank-placeholder"
@input="rankError = ''"
/>
</view>
<text v-if="rankError" class="rank-error">{{ rankError }}</text>
<view
class="form-action"
:class="{ 'form-action--disabled': savingRank }"
@click="saveRank"
>
<image
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="scaleToFill"
/>
<text>{{ savingRank ? "正在保存…" : "保存排行" }}</text>
</view>
</view>
<view v-else class="rank-result">
<text class="form-eyebrow">暂时无法打开成员页面</text>
<text class="form-title">暂时无法读取成员资料</text>
<text class="form-copy">{{ errorMessage }}</text>
<view class="form-action" @click="goBack">
<image
src="/static/assets/foundation/transparent/scroll-primary.png"
mode="scaleToFill"
/>
<text>返回世系树</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { lineageApi } from "@/services/api/lineage-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { goBack, handleBackPress, returnTo } from "@/utils/navigation/gateway.js";
const rankState = ref("loading");
const genealogyId = ref("");
const personId = ref("");
const member = ref(null);
const errorMessage = ref("");
const sortOrder = ref("");
const savingRank = ref(false);
const rankError = ref("");
const committedSortOrder = ref("");
const memberRankReadRequestController = createRequestController();
const memberRankSaveRequestController = createRequestController();
let pageActive = true;
let loadSequence = 0;
const loadMember = async () => {
const activeLoad = ++loadSequence;
rankState.value = "loading";
try {
const personDetail = await lineageApi.getPerson(genealogyId.value, personId.value, {
requestController: memberRankReadRequestController,
});
if (!pageActive || activeLoad !== loadSequence) return;
member.value = personDetail;
sortOrder.value = personDetail.sortOrder === null || personDetail.sortOrder === undefined
? ""
: String(personDetail.sortOrder);
committedSortOrder.value = "";
errorMessage.value = "";
rankState.value = "form";
} catch (error) {
if (!pageActive || activeLoad !== loadSequence || isRequestCancelled(error)) return;
member.value = null;
errorMessage.value =
getRequestErrorMessage(error, "当前成员资料暂不可用,请返回世系树后重试。");
rankState.value = "error";
}
};
const saveRank = async () => {
if (savingRank.value || !member.value) return;
const normalizedSortOrder = String(sortOrder.value).trim();
if (!/^-?\d+$/.test(normalizedSortOrder)) {
rankError.value = "请填写整数,例如 1、2、3。";
return;
}
const numericSortOrder = Number(normalizedSortOrder);
if (!Number.isSafeInteger(numericSortOrder)) {
rankError.value = "排序值超出可保存范围";
return;
}
savingRank.value = true;
rankError.value = "";
try {
if (committedSortOrder.value !== normalizedSortOrder) {
await lineageApi.updatePersonSortOrder(
genealogyId.value,
personId.value,
numericSortOrder,
{ requestController: memberRankSaveRequestController },
);
if (!pageActive) return;
committedSortOrder.value = normalizedSortOrder;
}
await returnTo("T01", {
genealogyId: genealogyId.value,
selectedId: personId.value,
});
} catch (error) {
if (pageActive && !isRequestCancelled(error)) {
rankError.value = committedSortOrder.value === normalizedSortOrder
? "排行已经保存,但页面返回失败。请再次点击保存重试返回。"
: getRequestErrorMessage(error, "排行尚未保存,请稍后重试。");
}
} finally {
if (pageActive) savingRank.value = false;
}
};
const requestBack = () => {
if (savingRank.value) return;
return goBack();
};
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
personId.value = String(query.personId || "");
if (!genealogyId.value || !personId.value || query.mode !== "rank") {
rankState.value = "error";
errorMessage.value = "请从成员资料页重新进入排行调整。";
return;
}
void loadMember();
});
onUnload(() => {
pageActive = false;
loadSequence += 1;
memberRankReadRequestController.abort();
memberRankSaveRequestController.abort();
});
onBackPress((event) => handleBackPress(event, requestBack));
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.rank-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.rank-page__header {
z-index: 3;
}
.rank-panel {
@include adaptive-tree-panel;
z-index: 2;
width: calc(100% - 32rpx);
margin: 18rpx auto 28rpx;
padding: 7.5% 8%;
}
.form-eyebrow {
display: block;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
letter-spacing: 3rpx;
}
.form-title {
display: block;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(19px, 34rpx, 24px);
font-weight: 700;
line-height: 1.35;
}
.form-copy {
display: block;
margin-top: 10rpx;
color: $ink-muted;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.5;
}
.member-context,
.rank-field {
@include adaptive-tree-field;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
min-height: 78rpx;
align-items: center;
gap: 20rpx;
margin-top: 16rpx;
padding: 12rpx 22rpx;
}
.member-context text:first-child,
.rank-field text {
color: $ink;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
}
.member-context text:last-child {
min-width: 0;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
line-height: 1.45;
text-align: right;
}
.rank-field input {
min-width: 0;
min-height: 56rpx;
padding: 0 14rpx;
border: 1rpx solid rgba(143, 108, 63, 0.34);
border-radius: 8rpx;
background: rgba(255, 253, 247, 0.8);
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
text-align: right;
}
.rank-placeholder { color: $ink-muted; }
.rank-error { display: block; margin-top: 12rpx; color: $brand-red; font-size: clamp(14px, 22rpx, 17px); }
.form-action {
display: grid;
width: 100%;
min-height: 76rpx;
margin-top: 22rpx;
}
.form-action--disabled { opacity: 0.58; pointer-events: none; }
.form-action image,
.form-action text {
grid-area: 1 / 1;
width: 100%;
height: 100%;
}
.form-action text {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
color: #fff9ed;
font-size: clamp(15px, 24rpx, 18px);
font-weight: 700;
}
.rank-result {
margin-top: 30%;
text-align: center;
}
.rank-result .form-eyebrow,
.rank-result .form-copy {
text-align: center;
}
.rank-result .form-action {
width: 420rpx;
max-width: 100%;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 400px) {
.rank-panel {
width: calc(100% - 48rpx);
}
}
</style>