完成80%

This commit is contained in:
2026-07-27 06:50:07 +08:00
parent 8475bbd19a
commit 1eae3bbef4
63 changed files with 13664 additions and 280 deletions
+49 -3
View File
@@ -259,6 +259,7 @@
<view class="add-dialog__heading">
<text class="dialog-title">添加家谱</text>
<text class="dialog-copy">建议先搜索已有家谱避免重复创建</text>
<text v-if="quota" class="dialog-copy">{{ quota.createRemaining === -1 ? '当前可继续创建家谱' : `还可创建 ${quota.createRemaining} 部家谱` }}</text>
<view
class="add-dialog__close"
role="button"
@@ -285,6 +286,7 @@
block
type="secondary"
label="继续创建家谱"
:disabled="quota?.canCreate === false"
@click="createGenealogy"
/>
</view>
@@ -380,8 +382,14 @@ const switcherVisible = ref(false);
const selectedGenealogyId = ref(null);
const listScrollCommand = ref(0);
const currentListScrollTop = ref(0);
const unreadCount = ref(0);
const quota = ref(null);
const listRequestController = createRequestController();
const unreadRequestController = createRequestController();
const quotaRequestController = createRequestController();
let loadGeneration = 0;
let unreadLoadGeneration = 0;
let quotaLoadGeneration = 0;
let pageActive = true;
let presentationState = "default";
let skipNextShowRefresh = true;
@@ -454,11 +462,39 @@ const loadGenealogies = async () => {
}
};
const loadUnreadCount = async () => {
const generation = ++unreadLoadGeneration;
unreadRequestController.abort();
try {
const count = await appApi.getUnreadNotificationCount({
requestController: unreadRequestController,
});
if (pageActive && generation === unreadLoadGeneration) unreadCount.value = count;
} catch (error) {
if (!isRequestCancelled(error)) unreadCount.value = 0;
}
};
const loadQuota = async () => {
const generation = ++quotaLoadGeneration;
quotaRequestController.abort();
try {
const result = await appApi.getGenealogyQuota({
requestController: quotaRequestController,
});
if (pageActive && generation === quotaLoadGeneration) quota.value = result;
} catch (error) {
if (!isRequestCancelled(error) && pageActive && generation === quotaLoadGeneration) quota.value = null;
}
};
onLoad((query) => {
syncEmptyStateFromRoute(query);
requestedGenealogyId.value = String(query?.genealogyId || "");
if (presentationState === "default") {
loadGenealogies();
loadUnreadCount();
loadQuota();
} else {
reconcilePageGenealogyContext();
}
@@ -480,15 +516,20 @@ onShow(() => {
return;
}
if (presentationState === "default") loadGenealogies();
if (presentationState === "default") loadUnreadCount();
if (presentationState === "default") loadQuota();
});
onUnload(() => {
pageActive = false;
loadGeneration += 1;
unreadLoadGeneration += 1;
quotaLoadGeneration += 1;
listRequestController.abort();
unreadRequestController.abort();
quotaRequestController.abort();
});
const unreadCount = computed(() => 0);
const hasGenealogies = computed(
() => !forceEmptyState.value && list.value.length > 0,
);
@@ -725,11 +766,16 @@ const openShortcut = (key) => {
align-items: center;
}
.current-name {
min-width: 0;
flex: 1;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 50rpx;
font-size: 38rpx;
font-weight: 700;
overflow-wrap: anywhere;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.current-switch-copy {
flex: 0 0 auto;
+45 -7
View File
@@ -2,7 +2,7 @@
<template>
<view class="create-page">
<GenealogyPageBackground />
<PageHeader title="创建家谱" custom-back @back="backToGenealogies" />
<PageHeader title="创建家谱" custom-back @back="requestBack" />
<view class="page-content">
<view class="create-card">
@@ -111,19 +111,34 @@
</view>
</view>
</view>
<AppDialog
:visible="discardDialogVisible"
eyebrow="尚未保存"
title="要放弃本次创建吗"
message="返回后,本次填写的家谱信息不会保留。"
cancel-text="继续填写"
confirm-text="放弃并返回"
show-cancel
:close-on-mask="false"
@cancel="cancelDiscard"
@confirm="confirmDiscard"
/>
</view>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { onUnload } from "@dcloudio/uni-app";
import { computed, onMounted, reactive, ref } from "vue";
import { onBackPress, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { GENEALOGY_ACCESS_PRESET, GENEALOGY_ACCESS_PRESET_OPTIONS, toApiGenealogyAccess } from "@/utils/genealogy-contracts.js";
import { isImagePickCancelled, pickAndUploadImage, toConsumerOssId } from "@/utils/resumable-image-upload.js";
import { finishPage, returnTo } from "@/utils/navigation.js";
import { finishPage, handleBackPress, runBackGuard } from "@/utils/navigation.js";
const form = reactive({
surname: "",
@@ -150,8 +165,22 @@ const regionPickerIndexes = ref([0]);
const regionPickerError = ref("");
const regionLoading = ref(false);
const regionPickerOpen = ref(false);
const discardDialogVisible = ref(false);
const regionPickerIndicatorStyle = "height: 104rpx; border-top: 1px solid rgba(159, 23, 15, .46); border-bottom: 1px solid rgba(159, 23, 15, .46); background: rgba(159, 23, 15, .08);";
let pageActive = true;
const hasDraft = computed(() =>
Object.entries(form).some(([key, value]) =>
key === "accessPreset"
? value !== GENEALOGY_ACCESS_PRESET.MEMBER_ONLY
: String(value).trim(),
) || Boolean(selectedRegion.value?.regionCode) || Boolean(coverOssId.value),
);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardDialogVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const clearFieldError = (field) => {
fieldErrors[field] = "";
@@ -259,9 +288,17 @@ onMounted(() => {
loadRegionRoot();
});
const backToGenealogies = () => {
if (!isSubmitting.value && !isUploading.value) return returnTo("G01");
};
const requestBack = () =>
runBackGuard({
transientOpen: regionPickerOpen.value || discardDialogVisible.value,
dirty: hasDraft.value,
submitting: isSubmitting.value || isUploading.value,
"close-transient": () => regionPickerOpen.value ? closeRegionPicker() : cancelDiscard(),
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
onBackPress((event) => handleBackPress(event, requestBack));
const uploadCover = async () => {
if (isUploading.value || isSubmitting.value) return;
@@ -321,6 +358,7 @@ onUnload(() => {
pageActive = false;
createController.abort();
regionController.abort();
discardConfirmation.dispose();
});
</script>
+11 -4
View File
@@ -381,10 +381,10 @@ const applyToJoin = () => {
}
.overview-ready {
display: flex;
min-height: min(483px, calc(100vw * 1.3422));
min-height: min(540px, calc(100vw * 1.5));
flex-direction: column;
gap: 34rpx;
padding: 58rpx 0 42rpx;
padding: 10rpx 0 42rpx;
box-sizing: border-box;
}
.overview-hero {
@@ -410,14 +410,21 @@ const applyToJoin = () => {
margin-top: 6rpx;
color: #e9dcc1;
font-size: 23rpx;
line-height: 1.55;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.overview-hero__stats {
display: flex;
display: inline-flex;
gap: 18rpx;
margin-top: 12rpx;
color: #dbc990;
font-size: 22rpx;
transform: translateY(-18rpx);
}
.overview-hero__stats + .overview-hero__stats { margin-left: 18rpx; }
.overview-actions {
display: grid;
min-height: 386rpx;
@@ -543,7 +550,7 @@ const applyToJoin = () => {
}
.overview-public {
display: flex;
min-height: min(483px, calc(100vw * 1.3422));
min-height: min(540px, calc(100vw * 1.5));
flex-direction: column;
gap: 0;
padding: 58rpx 0 28rpx;
+4 -4
View File
@@ -7,12 +7,12 @@
</view>
<view class="poem-panel" :class="`poem-state--${poemState}`">
<AppLoading v-if="poemState === 'loading'" text="正在读取字辈诗" description="正在读取后端已声明的世代、字辈和状态字段。" />
<AppLoading v-if="poemState === 'loading'" text="正在读取字辈诗" description="正在整理家谱的传承字序。" />
<view v-else-if="poemState === 'list'" class="poem-list">
<text class="poem-list__eyebrow">{{ genealogyName }} · 传承字序</text>
<text class="poem-list__title">{{ generationRangeTitle }}</text>
<text class="poem-list__copy">接口未声明当前世代字段页面只按后端返回的世代文字和状态展示</text>
<text class="poem-list__copy">按已记录的世代和字辈展示传承字序</text>
<view class="poem-rows">
<view v-for="item in visiblePoemRows" :key="item.poemId" class="poem-row" :class="{ 'poem-row--disabled': item.status === GENERATION_POEM_STATUS.DISABLED }">
<text class="poem-row__number"> {{ item.generationNo }} </text>
@@ -136,8 +136,8 @@ const previewSummary = computed(() => {
return `服务端预览:新增 ${preview.value.createCount} 条,更新 ${preview.value.updateCount} 条,保留 ${preview.value.keepCount} 条,停用 ${preview.value.disableCount} 条。`;
});
const stateCopy = computed(() => poemState.value === "empty"
? "正常字辈读取未返回条目。维护列表由后端单独鉴权,点击后才会判断是否可维护。"
: "未取得可消费的服务端响应,页面不会用本地字辈或本地权限替代。",
? "当前尚未添加可用的字辈,可进入维护页面查看。"
: "暂未读取到可展示的字辈,请稍后重试。",
);
const isDirty = computed(() => Boolean(
poemState.value === "edit" && editorSnapshot.value && (