完成50%
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<GenealogyPageBackground />
|
||||
<PageHeader
|
||||
root
|
||||
notice
|
||||
title="我的家谱"
|
||||
:unread-count="unreadCount"
|
||||
@notice="toNotifications"
|
||||
@@ -44,6 +45,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="contextInvalidated" class="state-panel state-panel--context">
|
||||
<text class="state-title">当前家谱已不可用</text>
|
||||
<text class="state-copy">权限或家谱列表可能已经变化,请明确选择仍可访问的家谱。</text>
|
||||
<AppButton block label="选择可用家谱" @click="openSwitcher" />
|
||||
</view>
|
||||
|
||||
<template v-else-if="hasGenealogies">
|
||||
<view class="genealogy-fixed-zone">
|
||||
<view class="current-slip" @click="openSwitcher">
|
||||
@@ -132,6 +139,7 @@
|
||||
:key="item.id"
|
||||
:genealogy="item"
|
||||
role="成员"
|
||||
:selected="item.id === currentGenealogy.id"
|
||||
@select="openGenealogy"
|
||||
/>
|
||||
</view>
|
||||
@@ -310,13 +318,15 @@
|
||||
/>
|
||||
</view>
|
||||
<scroll-view class="genealogy-switcher__list" scroll-y>
|
||||
<view
|
||||
<button
|
||||
v-for="item in availableGenealogies"
|
||||
:key="item.id"
|
||||
class="switcher-item"
|
||||
:class="{
|
||||
'switcher-item--active': item.id === selectedGenealogyId,
|
||||
}"
|
||||
:aria-pressed="item.id === selectedGenealogyId"
|
||||
:aria-label="`${item.name},${item.location},${item.memberCount} 位成员`"
|
||||
@click="selectGenealogy(item)"
|
||||
>
|
||||
<view>
|
||||
@@ -328,7 +338,7 @@
|
||||
<text class="switcher-item__state">{{
|
||||
item.id === selectedGenealogyId ? "当前" : "选择"
|
||||
}}</text>
|
||||
</view>
|
||||
</button>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -347,21 +357,25 @@ import AppButton from "@/components/AppButton.vue";
|
||||
import GenealogyCard from "@/components/GenealogyCard.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogies, notifications } from "@/data/mock.js";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
genealogies,
|
||||
getGenealogyFixtureAccess,
|
||||
listNotificationFixtures,
|
||||
} from "@/data/mock.js";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
import { handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasError = ref(false);
|
||||
const list = ref(genealogies);
|
||||
const forceEmptyState = ref(false);
|
||||
const contextInvalidated = ref(false);
|
||||
const contextReconcileFailed = ref(false);
|
||||
const requestedGenealogyId = ref("");
|
||||
const addDialogVisible = ref(false);
|
||||
const switcherVisible = ref(false);
|
||||
const storedGenealogyId = Number(genealogyContext.getCurrentGenealogyId());
|
||||
const selectedGenealogyId = ref(
|
||||
genealogies.some((item) => item.id === storedGenealogyId)
|
||||
? storedGenealogyId
|
||||
: genealogies[0]?.id || null,
|
||||
);
|
||||
const selectedGenealogyId = ref(null);
|
||||
const listScrollCommand = ref(0);
|
||||
const currentListScrollTop = ref(0);
|
||||
|
||||
@@ -376,41 +390,75 @@ const syncEmptyStateFromRoute = (query = {}) => {
|
||||
hasError.value = presentationState === "error";
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
const requestedId = Number(query?.genealogyId);
|
||||
if (genealogies.some((item) => item.id === requestedId)) {
|
||||
selectedGenealogyId.value = requestedId;
|
||||
genealogyContext.setCurrentGenealogyId(requestedId);
|
||||
} else if (selectedGenealogyId.value) {
|
||||
genealogyContext.setCurrentGenealogyId(selectedGenealogyId.value);
|
||||
const reconcilePageGenealogyContext = () => {
|
||||
try {
|
||||
const availableIds = list.value.map((item) => String(item.id));
|
||||
const previousId = genealogyContext.getCurrentGenealogyId();
|
||||
selectedGenealogyId.value =
|
||||
genealogyContext.reconcileCurrentGenealogyId(
|
||||
availableIds,
|
||||
requestedGenealogyId.value,
|
||||
) ||
|
||||
null;
|
||||
contextReconcileFailed.value = false;
|
||||
contextInvalidated.value = Boolean(
|
||||
availableIds.length &&
|
||||
!selectedGenealogyId.value &&
|
||||
(requestedGenealogyId.value ||
|
||||
previousId ||
|
||||
genealogyContext.isCurrentGenealogyInvalidated()),
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
genealogyContext.invalidateCurrentGenealogyId();
|
||||
selectedGenealogyId.value = null;
|
||||
contextReconcileFailed.value = true;
|
||||
contextInvalidated.value = false;
|
||||
hasError.value = true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
syncEmptyStateFromRoute(query);
|
||||
requestedGenealogyId.value = String(query?.genealogyId || "");
|
||||
reconcilePageGenealogyContext();
|
||||
});
|
||||
|
||||
const unreadCount = computed(
|
||||
() => notifications.filter((item) => item.unread).length,
|
||||
() => listNotificationFixtures().filter((item) => item.unread).length,
|
||||
);
|
||||
const hasGenealogies = computed(
|
||||
() => !forceEmptyState.value && list.value.length > 0,
|
||||
);
|
||||
const isListLayout = computed(
|
||||
() => !isLoading.value && !hasError.value && hasGenealogies.value,
|
||||
() =>
|
||||
!isLoading.value &&
|
||||
!hasError.value &&
|
||||
!contextInvalidated.value &&
|
||||
hasGenealogies.value,
|
||||
);
|
||||
const createdGenealogies = computed(() =>
|
||||
list.value.filter((item) => item.membership === "created"),
|
||||
list.value.filter(
|
||||
(item) => getGenealogyFixtureAccess(item.id).accessRole === "owner",
|
||||
),
|
||||
);
|
||||
const joinedGenealogies = computed(() =>
|
||||
list.value.filter((item) => item.membership === "joined"),
|
||||
list.value.filter(
|
||||
(item) => getGenealogyFixtureAccess(item.id).accessRole === "member",
|
||||
),
|
||||
);
|
||||
const availableGenealogies = computed(() => list.value);
|
||||
const currentGenealogy = computed(
|
||||
() =>
|
||||
availableGenealogies.value.find(
|
||||
(item) => item.id === selectedGenealogyId.value,
|
||||
) || availableGenealogies.value[0],
|
||||
) || null,
|
||||
);
|
||||
const isCurrentGenealogyOwner = computed(
|
||||
() => currentGenealogy.value?.membership === "created",
|
||||
() =>
|
||||
getGenealogyFixtureAccess(currentGenealogy.value?.id).accessRole ===
|
||||
"owner",
|
||||
);
|
||||
const currentRoleLabel = computed(() =>
|
||||
isCurrentGenealogyOwner.value ? "管理员" : "成员",
|
||||
@@ -420,26 +468,29 @@ const currentRoleLabel = computed(() =>
|
||||
const applicationRecords = [
|
||||
{
|
||||
id: "pending",
|
||||
name: "汤氏南阳宗谱",
|
||||
genealogyId: "2003",
|
||||
statusLabel: "审核中",
|
||||
tone: "pending",
|
||||
description: "申请已提交,等待管理员审核",
|
||||
},
|
||||
{
|
||||
id: "rejected",
|
||||
name: "汤氏清河家谱",
|
||||
genealogyId: "2004",
|
||||
statusLabel: "被拒绝",
|
||||
tone: "rejected",
|
||||
description: "可修改关系说明后重新申请",
|
||||
},
|
||||
{
|
||||
id: "removed",
|
||||
name: "汤氏汝南支谱",
|
||||
genealogyId: "2005",
|
||||
statusLabel: "已退出",
|
||||
tone: "muted",
|
||||
description: "如需恢复成员身份,可重新申请加入",
|
||||
},
|
||||
];
|
||||
].map((record) => ({
|
||||
...record,
|
||||
name: findGenealogyFixture(record.genealogyId)?.name || "未知家谱",
|
||||
}));
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
@@ -469,22 +520,34 @@ const visibleShortcuts = computed(() =>
|
||||
: shortcuts.filter((item) => item.key !== "applications"),
|
||||
);
|
||||
|
||||
const openGenealogy = (genealogy) => {
|
||||
genealogyContext.setCurrentGenealogyId(genealogy.id);
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${genealogy.id}`,
|
||||
});
|
||||
const openGenealogy = (genealogy) =>
|
||||
openPage("G05", { genealogyId: String(genealogy.id) }, "G01").then(
|
||||
(opened) => {
|
||||
if (opened) {
|
||||
selectedGenealogyId.value = String(genealogy.id);
|
||||
genealogyContext.setCurrentGenealogyId(genealogy.id);
|
||||
}
|
||||
return opened;
|
||||
},
|
||||
);
|
||||
const createGenealogy = () => {
|
||||
closeAddDialog();
|
||||
return openPage("G03", {}, "G01");
|
||||
};
|
||||
const applyToJoin = () => {
|
||||
closeAddDialog();
|
||||
return openPage("G06", {}, "G01");
|
||||
};
|
||||
const joinByInvite = () => {
|
||||
closeAddDialog();
|
||||
return openPage("G06", { mode: "invite" }, "G01");
|
||||
};
|
||||
const toNotifications = () => {
|
||||
const notificationParams = currentGenealogy.value
|
||||
? { genealogyId: String(currentGenealogy.value.id) }
|
||||
: {};
|
||||
return openPage("N01", notificationParams, "G01");
|
||||
};
|
||||
const createGenealogy = () =>
|
||||
uni.navigateTo({ url: "/pages/genealogy/g03-create-genealogy" });
|
||||
const applyToJoin = () =>
|
||||
uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
|
||||
const joinByInvite = () =>
|
||||
uni.navigateTo({
|
||||
url: "/pages/genealogy/g06-search-genealogies?mode=invite",
|
||||
});
|
||||
const toNotifications = () =>
|
||||
uni.navigateTo({ url: "/pages/notification/n01-message-center" });
|
||||
|
||||
const openAddDialog = () => {
|
||||
addDialogVisible.value = true;
|
||||
@@ -492,23 +555,25 @@ const openAddDialog = () => {
|
||||
const closeAddDialog = () => {
|
||||
addDialogVisible.value = false;
|
||||
};
|
||||
onBackPress(() => {
|
||||
if (switcherVisible.value) {
|
||||
closeSwitcher();
|
||||
return true;
|
||||
}
|
||||
if (addDialogVisible.value) {
|
||||
closeAddDialog();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const openSwitcher = () => {
|
||||
switcherVisible.value = true;
|
||||
};
|
||||
const closeSwitcher = () => {
|
||||
switcherVisible.value = false;
|
||||
};
|
||||
const closeActiveOverlay = () => {
|
||||
if (switcherVisible.value) closeSwitcher();
|
||||
else closeAddDialog();
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: switcherVisible.value || addDialogVisible.value,
|
||||
"close-transient": closeActiveOverlay,
|
||||
});
|
||||
onBackPress((event) => {
|
||||
if (!switcherVisible.value && !addDialogVisible.value) return false;
|
||||
return handleBackPress(event, requestBack);
|
||||
});
|
||||
const handleListScroll = (event) => {
|
||||
currentListScrollTop.value = Number(event?.detail?.scrollTop || 0);
|
||||
};
|
||||
@@ -519,33 +584,44 @@ const resetListScroll = async () => {
|
||||
currentListScrollTop.value = 0;
|
||||
};
|
||||
const selectGenealogy = async (genealogy) => {
|
||||
selectedGenealogyId.value = genealogy.id;
|
||||
genealogyContext.setCurrentGenealogyId(genealogy.id);
|
||||
selectedGenealogyId.value = String(genealogy.id);
|
||||
genealogyContext.setCurrentGenealogyId(String(genealogy.id));
|
||||
contextInvalidated.value = false;
|
||||
closeSwitcher();
|
||||
await resetListScroll();
|
||||
};
|
||||
const openApplication = (record) => {
|
||||
const statusQuery = record.id === "pending" ? "pending" : record.id;
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g09-my-applications?status=${statusQuery}`,
|
||||
});
|
||||
if (record.id === "pending")
|
||||
return openPage("G09", { status: "pending" }, "G01");
|
||||
return openPage(
|
||||
"G08",
|
||||
{
|
||||
genealogyId: String(record.genealogyId),
|
||||
source: "search",
|
||||
},
|
||||
"G01",
|
||||
);
|
||||
};
|
||||
const retryLoad = () => {
|
||||
if (contextReconcileFailed.value) {
|
||||
hasError.value = false;
|
||||
reconcilePageGenealogyContext();
|
||||
return;
|
||||
}
|
||||
hasError.value = false;
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
const openShortcut = (key) => {
|
||||
if (!currentGenealogy.value) return;
|
||||
const genealogyId = currentGenealogy.value.id;
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId);
|
||||
const paths = {
|
||||
tree: `/pages/tree/t01-tree-overview?genealogyId=${currentGenealogy.value.id}`,
|
||||
members: `/pages/genealogy/g05-genealogy-overview?genealogyId=${currentGenealogy.value.id}`,
|
||||
poem: `/pages/genealogy/g12-generation-poems?genealogyId=${currentGenealogy.value.id}`,
|
||||
applications: `/pages/genealogy/g10-application-review?genealogyId=${currentGenealogy.value.id}`,
|
||||
const genealogyId = String(currentGenealogy.value.id);
|
||||
const actions = {
|
||||
tree: () => openPage("T01", { genealogyId }, "G01"),
|
||||
members: () => openPage("G05", { genealogyId }, "G01"),
|
||||
poem: () => openPage("G12", { genealogyId }, "G01"),
|
||||
applications: () => openPage("G10", { genealogyId }, "G01"),
|
||||
};
|
||||
uni.navigateTo({ url: paths[key] });
|
||||
return actions[key]?.();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -869,6 +945,10 @@ const openShortcut = (key) => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.state-panel--context > .app-button {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.error-panel__content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -1189,9 +1269,16 @@ const openShortcut = (key) => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 18rpx 16rpx;
|
||||
border: 1rpx solid transparent;
|
||||
border-bottom-color: rgba(181, 138, 75, 0.42);
|
||||
background: transparent;
|
||||
line-height: normal;
|
||||
text-align: left;
|
||||
}
|
||||
.switcher-item::after {
|
||||
border: 0;
|
||||
}
|
||||
.switcher-item__name,
|
||||
.switcher-item__meta {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<PageHeader
|
||||
:title="isAncestorStep ? '录入首代人物' : '创建家谱'"
|
||||
custom-back
|
||||
@back="goBack"
|
||||
@back="requestBack"
|
||||
/>
|
||||
|
||||
<view class="flow-content">
|
||||
@@ -73,22 +73,15 @@
|
||||
<text class="flow-rule__label">访问规则</text>
|
||||
<view class="flow-rule__options">
|
||||
<view
|
||||
v-for="option in GENEALOGY_ACCESS_PRESET_OPTIONS"
|
||||
:key="option.value"
|
||||
class="flow-rule__option"
|
||||
:class="{
|
||||
'flow-rule__option--active':
|
||||
createForm.visibility === 'MEMBER_ONLY',
|
||||
createForm.accessPreset === option.value,
|
||||
}"
|
||||
@click="createForm.visibility = 'MEMBER_ONLY'"
|
||||
>仅成员可见</view
|
||||
>
|
||||
<view
|
||||
class="flow-rule__option"
|
||||
:class="{
|
||||
'flow-rule__option--active':
|
||||
createForm.visibility === 'SEARCHABLE',
|
||||
}"
|
||||
@click="createForm.visibility = 'SEARCHABLE'"
|
||||
>可搜索申请</view
|
||||
@click="createForm.accessPreset = option.value"
|
||||
>{{ option.label }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
@@ -223,7 +216,7 @@
|
||||
<view class="flow-success-dialog__content">
|
||||
<text class="flow-success-dialog__title">家谱创建完成</text>
|
||||
<text class="flow-success-dialog__copy"
|
||||
>首代人物已保存,接下来进入家谱总览继续完善资料。</text
|
||||
>当前为本地流程预览,资料尚未提交服务器,可进入总览继续检查页面。</text
|
||||
>
|
||||
<view class="flow-success-dialog__action" @click="enterOverview">
|
||||
<text>进入家谱总览</text>
|
||||
@@ -231,14 +224,40 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃创建?"
|
||||
message="当前填写内容尚未保存,确认返回后将清空。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onUnmounted, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onBackPress, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
createLocalGenealogyPreview,
|
||||
removeLocalGenealogyPreview,
|
||||
updateLocalGenealogyPreview,
|
||||
updateLocalGenealogyPreviewAncestor,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
GENEALOGY_ACCESS_PRESET,
|
||||
GENEALOGY_ACCESS_PRESET_OPTIONS,
|
||||
} from "@/utils/genealogy-contracts.js";
|
||||
import { handleBackPress, openPage, returnTo, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const currentStep = ref("create");
|
||||
const genealogyId = ref("");
|
||||
@@ -246,6 +265,7 @@ const isSubmitting = ref(false);
|
||||
const createState = ref("form");
|
||||
const ancestorState = ref("form");
|
||||
const duplicateReminderVisible = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
const fieldErrors = reactive({
|
||||
surname: "",
|
||||
name: "",
|
||||
@@ -258,7 +278,7 @@ const createForm = reactive({
|
||||
name: "",
|
||||
hall: "",
|
||||
location: "",
|
||||
visibility: "MEMBER_ONLY",
|
||||
accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
|
||||
});
|
||||
const ancestorForm = reactive({
|
||||
personName: "",
|
||||
@@ -286,31 +306,69 @@ const changeAncestorBirthDate = (event) => {
|
||||
};
|
||||
|
||||
const isAncestorStep = computed(() => currentStep.value === "ancestor");
|
||||
const isDirty = computed(() =>
|
||||
isAncestorStep.value
|
||||
? Boolean(
|
||||
ancestorForm.personName ||
|
||||
ancestorForm.birthDate ||
|
||||
ancestorForm.introduction ||
|
||||
ancestorForm.sex !== "0",
|
||||
)
|
||||
: Boolean(
|
||||
createForm.surname ||
|
||||
createForm.name ||
|
||||
createForm.hall ||
|
||||
createForm.location ||
|
||||
createForm.accessPreset !== GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
|
||||
),
|
||||
);
|
||||
|
||||
const syncFlowFromRoute = (query = {}) => {
|
||||
const isAncestorRoute = query?.step === "ancestor";
|
||||
const routeGenealogyId = query?.genealogyId || "";
|
||||
currentStep.value = isAncestorRoute ? "ancestor" : "create";
|
||||
genealogyId.value =
|
||||
routeGenealogyId || (isAncestorStep.value ? "local-created-genealogy" : "");
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
syncFlowFromRoute(query);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (submitTimer) clearTimeout(submitTimer);
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
if (isAncestorStep.value) {
|
||||
uni.redirectTo({ url: "/pages/genealogy/g03-create-genealogy" });
|
||||
return;
|
||||
const requestRawDiscardConfirmation = discardConfirmation.request;
|
||||
const requestDiscardConfirmation = async () => {
|
||||
const confirmed = await requestRawDiscardConfirmation();
|
||||
if (confirmed && currentStep.value === "create" && genealogyId.value) {
|
||||
removeLocalGenealogyPreview(genealogyId.value);
|
||||
genealogyId.value = "";
|
||||
}
|
||||
|
||||
uni.navigateBack();
|
||||
return confirmed;
|
||||
};
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const closeActiveTransient = () => {
|
||||
if (duplicateReminderVisible.value) closeDuplicateReminder();
|
||||
else cancelDiscard();
|
||||
};
|
||||
const popInternalTrail = () => {
|
||||
currentStep.value = "create";
|
||||
ancestorState.value = "form";
|
||||
return true;
|
||||
};
|
||||
const requestBack = () => {
|
||||
if (ancestorState.value === "success") return enterOverview();
|
||||
return runBackGuard({
|
||||
transientOpen: duplicateReminderVisible.value || discardVisible.value,
|
||||
internalTrail: isAncestorStep.value && !isSubmitting.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": closeActiveTransient,
|
||||
"pop-internal-trail": popInternalTrail,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
};
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const clearFieldError = (field) => {
|
||||
fieldErrors[field] = "";
|
||||
@@ -333,23 +391,31 @@ const submitCreate = () => {
|
||||
const closeDuplicateReminder = () => {
|
||||
duplicateReminderVisible.value = false;
|
||||
};
|
||||
const searchExistingGenealogy = () =>
|
||||
uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
|
||||
const confirmCreate = () => {
|
||||
const searchExistingGenealogy = () => {
|
||||
closeDuplicateReminder();
|
||||
return openPage("G06", {}, "G03");
|
||||
};
|
||||
const confirmCreate = () => {
|
||||
if (isSubmitting.value) return;
|
||||
closeDuplicateReminder();
|
||||
const createSnapshot = Object.freeze({ ...createForm });
|
||||
isSubmitting.value = true;
|
||||
createState.value = "submitting";
|
||||
submitTimer = setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
isSubmitting.value = false;
|
||||
if (createForm.name.trim() === "失败") {
|
||||
if (createSnapshot.name.trim() === "失败") {
|
||||
createState.value = "error";
|
||||
return;
|
||||
}
|
||||
const createdId = "local-created-genealogy";
|
||||
uni.redirectTo({
|
||||
url: `/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=${createdId}`,
|
||||
});
|
||||
genealogyId.value =
|
||||
updateLocalGenealogyPreview(genealogyId.value, createSnapshot) ||
|
||||
createLocalGenealogyPreview(createSnapshot);
|
||||
currentStep.value = "ancestor";
|
||||
createState.value = "form";
|
||||
}, 320);
|
||||
submitTimer = timer;
|
||||
};
|
||||
|
||||
const submitAncestor = () => {
|
||||
@@ -363,19 +429,29 @@ const submitAncestor = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const ancestorSnapshot = Object.freeze({ ...ancestorForm });
|
||||
isSubmitting.value = true;
|
||||
ancestorState.value = "submitting";
|
||||
submitTimer = setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
isSubmitting.value = false;
|
||||
ancestorState.value =
|
||||
ancestorForm.personName.trim() === "失败" ? "error" : "success";
|
||||
if (ancestorSnapshot.personName.trim() === "失败") {
|
||||
ancestorState.value = "error";
|
||||
return;
|
||||
}
|
||||
ancestorState.value = updateLocalGenealogyPreviewAncestor(
|
||||
genealogyId.value,
|
||||
ancestorSnapshot,
|
||||
)
|
||||
? "success"
|
||||
: "error";
|
||||
}, 320);
|
||||
submitTimer = timer;
|
||||
};
|
||||
|
||||
const enterOverview = () =>
|
||||
uni.redirectTo({
|
||||
url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
returnTo("G05", { genealogyId: genealogyId.value });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<GenealogyPageBackground />
|
||||
<view class="overview-page__header">
|
||||
<PageHeader
|
||||
:title="viewMode === 'public' ? '家谱公开预览' : '家谱总览'"
|
||||
:action="
|
||||
overviewState === 'ready' &&
|
||||
viewMode === 'member' &&
|
||||
accessRole === 'owner'
|
||||
? '管理'
|
||||
: ''
|
||||
:title="
|
||||
viewMode === 'public'
|
||||
? '家谱公开预览'
|
||||
: viewMode === 'preview'
|
||||
? '创建流程预览'
|
||||
: '家谱总览'
|
||||
"
|
||||
@action="toSettings"
|
||||
custom-back
|
||||
@back="requestBack"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<view class="overview-hero__stats">
|
||||
<text>共 {{ genealogy.memberCount || 0 }} 人</text>
|
||||
<text>已激活 {{ genealogy.activeCount || 0 }} 人</text>
|
||||
<text>{{ genealogy.visibility || "仅成员可见" }}</text>
|
||||
<text>{{ getGenealogyAccessPresetLabel(genealogy.accessPreset) }}</text>
|
||||
</view>
|
||||
<view class="overview-hero__stats">
|
||||
<text>始祖 {{ genealogy.ancestorName }}</text>
|
||||
@@ -52,14 +52,6 @@
|
||||
<text class="overview-action__title">世系树</text
|
||||
><text>查看家脉关系</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="accessRole === 'owner'"
|
||||
class="overview-action overview-action--ancestor"
|
||||
@click="toFirstPerson"
|
||||
>
|
||||
<text class="overview-action__title">录入族人</text
|
||||
><text>从首代开始完善</text>
|
||||
</view>
|
||||
<view
|
||||
class="overview-action overview-action--poem"
|
||||
@click="toGenerationPoems"
|
||||
@@ -75,6 +67,24 @@
|
||||
<text class="overview-action__title">入谱审核</text
|
||||
><text>处理加入申请</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="accessRole === 'owner'"
|
||||
class="overview-action overview-action--settings"
|
||||
@click="toSettings"
|
||||
>
|
||||
<text class="overview-action__title">家谱设置</text
|
||||
><text>维护公开范围与基础资料</text>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="overview-summary">
|
||||
<text class="overview-action__title">成员身份</text
|
||||
><text>已加入 · 普通成员</text>
|
||||
</view>
|
||||
<view class="overview-summary">
|
||||
<text class="overview-action__title">访问范围</text
|
||||
><text>{{ getGenealogyAccessPresetLabel(genealogy.accessPreset) }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="overview-family" @click="toFamily">
|
||||
@@ -82,8 +92,8 @@
|
||||
><text>查看</text>
|
||||
</view>
|
||||
<view class="overview-note">
|
||||
<text class="overview-note__title">家谱资料,仅向家人开放</text>
|
||||
<text>公开范围、访问说明与管理权由家谱管理员在设置中维护。</text>
|
||||
<text class="overview-note__title">成员资料按家谱访问规则保护</text>
|
||||
<text>名称、公开范围与家谱简介由谱主在设置中维护。</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -93,34 +103,49 @@
|
||||
class="overview-public"
|
||||
>
|
||||
<view class="overview-public__hero">
|
||||
<text class="overview-public__eyebrow">公开家谱</text>
|
||||
<text class="overview-public__eyebrow">{{
|
||||
viewMode === "preview" ? "本地流程预览" : "公开家谱"
|
||||
}}</text>
|
||||
<text class="overview-public__title">{{ genealogy.name }}</text>
|
||||
<text class="overview-public__source">{{ genealogy.source }}</text>
|
||||
<text class="overview-public__source">{{ genealogy.source || "来源信息待同步" }}</text>
|
||||
</view>
|
||||
<view class="overview-public__details">
|
||||
<view
|
||||
><text>姓氏</text><text>{{ genealogy.surname }}氏</text></view
|
||||
>
|
||||
<view
|
||||
><text>地区</text><text>{{ genealogy.location }}</text></view
|
||||
><text>地区</text><text>{{ genealogy.location || "待补充" }}</text></view
|
||||
>
|
||||
<view
|
||||
><text>堂号</text><text>{{ genealogy.hall }}</text></view
|
||||
><text>堂号</text><text>{{ genealogy.hall || "待补充" }}</text></view
|
||||
>
|
||||
<view
|
||||
><text>当前支系</text><text>{{ genealogy.branchName }}</text></view
|
||||
><text>当前支系</text><text>{{ genealogy.branchName || "待补充" }}</text></view
|
||||
>
|
||||
<view
|
||||
><text>所属上级谱</text
|
||||
><text>{{ genealogy.parentName }}</text></view
|
||||
><text>{{ viewMode === "preview" ? "首代人物" : "所属上级谱" }}</text
|
||||
><text>{{ (viewMode === "preview" ? genealogy.ancestorName : genealogy.parentName) || "待补充" }}</text></view
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
v-if="viewMode === 'public' && (genealogy.manager || genealogy.certification)"
|
||||
class="overview-public__trust"
|
||||
>
|
||||
<text>{{ genealogy.manager || "管理者待确认" }}</text>
|
||||
<text>{{ genealogy.certification || "认证信息待确认" }}</text>
|
||||
<text>{{ genealogy.memberCount || 0 }} 位成员</text>
|
||||
<text>更新于 {{ genealogy.updatedAt || "待同步" }}</text>
|
||||
</view>
|
||||
<view class="overview-public__notice">
|
||||
<text>公开说明</text>
|
||||
<text>{{ genealogy.publicDescription }}</text>
|
||||
<text>{{ viewMode === "preview" ? "预览说明" : "公开说明" }}</text>
|
||||
<text>{{ genealogy.publicDescription || "公开说明待补充" }}</text>
|
||||
</view>
|
||||
<view class="overview-public__action" @click="applyToJoin">
|
||||
<text>申请加入这部家谱</text>
|
||||
<view
|
||||
v-if="viewMode === 'public' && publicActionLabel"
|
||||
class="overview-public__action"
|
||||
@click="applyToJoin"
|
||||
>
|
||||
<text>{{ publicActionLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -169,55 +194,39 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad } from "@dcloudio/uni-app";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { findGenealogyFixture, getGenealogyFixtureAccess } from "@/data/mock.js";
|
||||
import { getGenealogyAccessPresetLabel } from "@/utils/genealogy-contracts.js";
|
||||
import {
|
||||
goBack,
|
||||
goRoot,
|
||||
handleBackPress,
|
||||
openPage,
|
||||
returnTo,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const genealogy = ref(null);
|
||||
const genealogyId = ref("");
|
||||
const overviewState = ref("loading");
|
||||
const loadError = ref("");
|
||||
const viewMode = ref("member");
|
||||
const accessRole = ref("owner");
|
||||
const overviewFixture = {
|
||||
id: "2001",
|
||||
surname: "汤",
|
||||
name: "汤氏南阳宗谱",
|
||||
hall: "敦睦堂",
|
||||
location: "河南·南阳",
|
||||
parentName: "汤氏中华总谱",
|
||||
branchName: "南阳主支",
|
||||
source: "由南阳汤氏族人整理并维护",
|
||||
publicDescription:
|
||||
"公开展示家谱身份、地区、堂号与支系信息;成员资料和世系详情仅向已加入成员开放。",
|
||||
motto: "敦亲睦族,敬祖传家。",
|
||||
memberCount: 428,
|
||||
activeCount: 316,
|
||||
visibility: "仅成员可见",
|
||||
ancestorName: "汤文远",
|
||||
updatedAt: "2026-07-12",
|
||||
};
|
||||
const overviewFixtures = {
|
||||
1001: {
|
||||
...overviewFixture,
|
||||
id: "1001",
|
||||
name: "汤氏家谱",
|
||||
location: "河南·洛阳",
|
||||
memberCount: 158,
|
||||
activeCount: 108,
|
||||
},
|
||||
1002: {
|
||||
...overviewFixture,
|
||||
id: "1002",
|
||||
name: "汤氏宗谱",
|
||||
hall: "承志堂",
|
||||
location: "山东·济宁",
|
||||
memberCount: 286,
|
||||
activeCount: 215,
|
||||
},
|
||||
2001: overviewFixture,
|
||||
};
|
||||
const accessRole = ref("guest");
|
||||
const publicRelation = ref("unknown");
|
||||
const publicCanApply = ref(false);
|
||||
const publicActionLabel = computed(() => {
|
||||
if (publicRelation.value === "pending") return "查看申请进度";
|
||||
if (!publicCanApply.value) return "";
|
||||
return (
|
||||
({
|
||||
available: "申请加入这部家谱",
|
||||
rejected: "修改后重新申请",
|
||||
removed: "重新申请加入",
|
||||
})[publicRelation.value] || ""
|
||||
);
|
||||
});
|
||||
|
||||
const stateTitle = computed(
|
||||
() =>
|
||||
@@ -244,9 +253,11 @@ const loadGenealogy = (query = {}) => {
|
||||
overviewState.value = "loading";
|
||||
loadError.value = "";
|
||||
genealogy.value = null;
|
||||
genealogyId.value = query.genealogyId || genealogyId.value || "";
|
||||
viewMode.value = query.mode === "public" ? "public" : "member";
|
||||
accessRole.value = query.role === "member" ? "member" : "owner";
|
||||
genealogyId.value = String(query.genealogyId || genealogyId.value || "");
|
||||
viewMode.value = "member";
|
||||
accessRole.value = "guest";
|
||||
publicRelation.value = "unknown";
|
||||
publicCanApply.value = false;
|
||||
|
||||
if (query.state === "empty" || !genealogyId.value) {
|
||||
overviewState.value = "empty";
|
||||
@@ -262,45 +273,49 @@ const loadGenealogy = (query = {}) => {
|
||||
}
|
||||
if (query.state === "loading") return;
|
||||
|
||||
genealogy.value = {
|
||||
...(overviewFixtures[genealogyId.value] || overviewFixture),
|
||||
id: genealogyId.value,
|
||||
};
|
||||
if (query.genealogyName) {
|
||||
genealogy.value.name = decodeURIComponent(query.genealogyName);
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
if (!fixture) {
|
||||
overviewState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
|
||||
const access = getGenealogyFixtureAccess(genealogyId.value);
|
||||
if (!access.canView) {
|
||||
overviewState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
genealogy.value = { ...fixture };
|
||||
viewMode.value = access.viewMode;
|
||||
accessRole.value = access.accessRole;
|
||||
publicRelation.value = access.relation;
|
||||
publicCanApply.value = access.canApply;
|
||||
overviewState.value = "ready";
|
||||
};
|
||||
|
||||
const requestBack = () => goBack();
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onLoad(loadGenealogy);
|
||||
const reloadOverview = () => loadGenealogy({ genealogyId: genealogyId.value });
|
||||
const toGenealogies = () =>
|
||||
uni.reLaunch({ url: "/pages/genealogy/g01-my-genealogies" });
|
||||
const toGenealogies = () => returnTo("G01", {});
|
||||
const toTree = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/tree/t01-tree-overview?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
const toFirstPerson = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
const toFamily = () => uni.reLaunch({ url: "/pages/family/f01-family-feed" });
|
||||
openPage("T01", { genealogyId: genealogyId.value }, "G05");
|
||||
const toFamily = () => goRoot("F01", { genealogyId: genealogyId.value });
|
||||
const toApplications = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g10-application-review?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
openPage("G10", { genealogyId: genealogyId.value }, "G05");
|
||||
const toSettings = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g11-genealogy-settings?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
openPage("G11", { genealogyId: genealogyId.value }, "G05");
|
||||
const toGenerationPoems = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g12-generation-poems?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
const applyToJoin = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
openPage("G12", { genealogyId: genealogyId.value }, "G05");
|
||||
const applyToJoin = () => {
|
||||
if (publicRelation.value === "pending")
|
||||
return openPage("G09", { status: "pending" }, "G05");
|
||||
if (!publicCanApply.value) return false;
|
||||
return openPage(
|
||||
"G08",
|
||||
{ genealogyId: genealogyId.value, source: "search" },
|
||||
"G05",
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -380,7 +395,7 @@ const applyToJoin = () =>
|
||||
min-height: 386rpx;
|
||||
}
|
||||
.overview-action,
|
||||
.overview-action-lock {
|
||||
.overview-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -390,9 +405,6 @@ const applyToJoin = () =>
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.overview-action-lock {
|
||||
opacity: 0.58;
|
||||
}
|
||||
.overview-action__title {
|
||||
margin-bottom: 9rpx;
|
||||
color: $ink;
|
||||
@@ -562,6 +574,15 @@ const applyToJoin = () =>
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
.overview-public__trust {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 10rpx 24rpx;
|
||||
margin: 28rpx 9% 0;
|
||||
color: $ink-muted;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.overview-public__notice {
|
||||
display: flex;
|
||||
margin: 54rpx 9% 0;
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<text
|
||||
class="result-card__relation"
|
||||
:class="`result-card__relation--${item.relation}`"
|
||||
>{{ item.actionLabel }}</text
|
||||
>{{ resultRelationLabel(item) }}</text
|
||||
>
|
||||
</view>
|
||||
<view class="result-card__facts">
|
||||
@@ -113,9 +113,10 @@
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
v-if="resultActionLabel(item)"
|
||||
class="result-card__action"
|
||||
@click.stop="handleResultAction(item)"
|
||||
>{{ item.actionLabel }}</view
|
||||
>{{ resultActionLabel(item) }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
@@ -159,6 +160,7 @@
|
||||
maxlength="12"
|
||||
placeholder="请输入邀请码"
|
||||
placeholder-class="search-input__placeholder"
|
||||
@input="resetInvite"
|
||||
/>
|
||||
</view>
|
||||
<view class="search-action" @click="verifyInvite">
|
||||
@@ -179,7 +181,7 @@
|
||||
>
|
||||
<text class="search-status__lead">通过邀请码直接定位家谱</text>
|
||||
<text class="search-status__copy"
|
||||
>验证有效后会显示目标家谱,确认关系后可直接加入。</text
|
||||
>验证有效后仅显示目标家谱,仍需填写身份关系;本地验证不会变更成员身份。</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
@@ -209,11 +211,11 @@
|
||||
>
|
||||
</view>
|
||||
<view class="result-card__action" @click="confirmInvite"
|
||||
>确认关系并加入</view
|
||||
>填写关系信息</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<text class="invite-result__note">提交后直接加入,无需等待审核</text>
|
||||
<text class="invite-result__note">当前为样式验证,不会变更成员身份</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
@@ -226,6 +228,12 @@ import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
getGenealogyFixtureAccess,
|
||||
isGenealogySearchVisible,
|
||||
publicGenealogies,
|
||||
} from "@/data/mock.js";
|
||||
import { goRoot, openPage } from "@/utils/navigation.js";
|
||||
|
||||
const mode = ref("search");
|
||||
const keyword = ref("");
|
||||
@@ -235,102 +243,14 @@ const inviteCode = ref("");
|
||||
const inviteState = ref("initial");
|
||||
const results = ref([]);
|
||||
let searchTimer = null;
|
||||
const invalidateSearch = () => {
|
||||
const timer = searchTimer;
|
||||
searchTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
|
||||
const areas = ["全部", "河南", "山东"];
|
||||
const resultFixtures = [
|
||||
{
|
||||
id: 2001,
|
||||
surname: "汤",
|
||||
name: "汤氏南阳宗谱",
|
||||
hall: "敦睦堂",
|
||||
location: "河南·南阳",
|
||||
parentName: "汤氏中华总谱",
|
||||
branchName: "南阳主支",
|
||||
manager: "管理员 汤文礼",
|
||||
certification: "资料已认证",
|
||||
memberCount: 428,
|
||||
updatedAt: "2026-07-12",
|
||||
relation: "available",
|
||||
actionLabel: "申请加入",
|
||||
},
|
||||
{
|
||||
id: 2002,
|
||||
surname: "汤",
|
||||
name: "汤氏洛阳家谱",
|
||||
hall: "承志堂",
|
||||
location: "河南·洛阳",
|
||||
parentName: "汤氏中华总谱",
|
||||
branchName: "洛阳二支",
|
||||
manager: "管理员 汤文远",
|
||||
certification: "资料已认证",
|
||||
memberCount: 158,
|
||||
updatedAt: "2026-07-10",
|
||||
relation: "joined",
|
||||
actionLabel: "已加入",
|
||||
},
|
||||
{
|
||||
id: 2003,
|
||||
surname: "汤",
|
||||
name: "汤氏济宁宗谱",
|
||||
hall: "敬宗堂",
|
||||
location: "山东·济宁",
|
||||
parentName: "汤氏鲁西总谱",
|
||||
branchName: "济宁主支",
|
||||
manager: "管理员 汤正明",
|
||||
certification: "管理员已实名",
|
||||
memberCount: 286,
|
||||
updatedAt: "2026-07-08",
|
||||
relation: "pending",
|
||||
actionLabel: "审核中",
|
||||
},
|
||||
{
|
||||
id: 2004,
|
||||
surname: "汤",
|
||||
name: "汤氏清河家谱",
|
||||
hall: "思源堂",
|
||||
location: "山东·临清",
|
||||
parentName: "汤氏鲁西总谱",
|
||||
branchName: "清河支系",
|
||||
manager: "管理员 汤志成",
|
||||
certification: "资料已认证",
|
||||
memberCount: 96,
|
||||
updatedAt: "2026-07-05",
|
||||
relation: "rejected",
|
||||
actionLabel: "修改后重新申请",
|
||||
},
|
||||
{
|
||||
id: 2005,
|
||||
surname: "汤",
|
||||
name: "汤氏汝南支谱",
|
||||
hall: "崇本堂",
|
||||
location: "河南·驻马店",
|
||||
parentName: "汤氏中原总谱",
|
||||
branchName: "汝南三支",
|
||||
manager: "管理员 汤国安",
|
||||
certification: "管理员已实名",
|
||||
memberCount: 72,
|
||||
updatedAt: "2026-07-02",
|
||||
relation: "removed",
|
||||
actionLabel: "重新申请",
|
||||
},
|
||||
{
|
||||
id: 2006,
|
||||
surname: "汤",
|
||||
name: "汤氏新安家谱",
|
||||
hall: "继善堂",
|
||||
location: "河南·三门峡",
|
||||
parentName: "无上级谱",
|
||||
branchName: "新安主支",
|
||||
manager: "创建者 当前用户",
|
||||
certification: "资料待完善",
|
||||
memberCount: 34,
|
||||
updatedAt: "2026-06-28",
|
||||
relation: "owned",
|
||||
actionLabel: "我创建的",
|
||||
},
|
||||
];
|
||||
|
||||
const inviteTarget = computed(() => resultFixtures[0]);
|
||||
const inviteTarget = computed(() => publicGenealogies[0]);
|
||||
|
||||
const syncModeFromRoute = (query = {}) => {
|
||||
mode.value = query?.mode === "invite" ? "invite" : "search";
|
||||
@@ -339,81 +259,121 @@ const syncModeFromRoute = (query = {}) => {
|
||||
};
|
||||
|
||||
onLoad((query) => syncModeFromRoute(query));
|
||||
onUnload(() => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
});
|
||||
onUnload(invalidateSearch);
|
||||
|
||||
const switchMode = (nextMode) => {
|
||||
invalidateSearch();
|
||||
mode.value = nextMode;
|
||||
results.value = [];
|
||||
searchState.value = "initial";
|
||||
inviteState.value = "initial";
|
||||
};
|
||||
|
||||
const search = () => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
invalidateSearch();
|
||||
searchState.value = "loading";
|
||||
searchTimer = setTimeout(() => {
|
||||
const value = keyword.value.trim();
|
||||
const value = keyword.value.trim();
|
||||
const area = selectedArea.value;
|
||||
const timer = setTimeout(() => {
|
||||
if (searchTimer !== timer || mode.value !== "search") return;
|
||||
searchTimer = null;
|
||||
if (value === "失败") {
|
||||
searchState.value = "error";
|
||||
return;
|
||||
}
|
||||
results.value = resultFixtures.filter((item) => {
|
||||
results.value = publicGenealogies.filter((item) => {
|
||||
const matchesKeyword =
|
||||
!value ||
|
||||
`${item.name}${item.surname}${item.location}${item.hall}`.includes(
|
||||
value,
|
||||
);
|
||||
const matchesArea =
|
||||
selectedArea.value === "全部" ||
|
||||
item.location.includes(selectedArea.value);
|
||||
return matchesKeyword && matchesArea;
|
||||
area === "全部" || item.location.includes(area);
|
||||
return isGenealogySearchVisible(String(item.id)) && matchesKeyword && matchesArea;
|
||||
});
|
||||
searchState.value = results.value.length ? "results" : "empty";
|
||||
searchTimer = null;
|
||||
}, 260);
|
||||
searchTimer = timer;
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
invalidateSearch();
|
||||
keyword.value = "";
|
||||
results.value = [];
|
||||
searchState.value = "initial";
|
||||
};
|
||||
|
||||
const openPreview = (item) => {
|
||||
if (item.relation === "available") {
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g05-genealogy-overview?mode=public&genealogyId=${item.id}&genealogyName=${encodeURIComponent(item.name)}`,
|
||||
});
|
||||
if (
|
||||
item.relation === "available" &&
|
||||
getGenealogyFixtureAccess(String(item.id)).canApply
|
||||
) {
|
||||
return openPage(
|
||||
"G05",
|
||||
{ genealogyId: String(item.id) },
|
||||
"G06",
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const resultActionLabel = (item) => {
|
||||
if (item.relation === "pending") return "查看申请进度";
|
||||
if (item.relation === "joined") return "切换到该家谱";
|
||||
if (item.relation === "owned") return "进入我的家谱";
|
||||
if (!getGenealogyFixtureAccess(String(item.id)).canApply) return "";
|
||||
return {
|
||||
available: "申请加入",
|
||||
rejected: "修改后重新申请",
|
||||
removed: "重新申请",
|
||||
}[item.relation] || "";
|
||||
};
|
||||
const resultRelationLabel = (item) =>
|
||||
({
|
||||
available: "可申请",
|
||||
joined: "已加入",
|
||||
pending: "审核中",
|
||||
rejected: "已拒绝",
|
||||
removed: "已退出",
|
||||
owned: "我创建的",
|
||||
})[item.relation] || "关系待确认";
|
||||
|
||||
const handleResultAction = (item) => {
|
||||
if (item.relation === "available")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&genealogyId=${item.id}&genealogyName=${encodeURIComponent(item.name)}`,
|
||||
});
|
||||
if (item.relation === "pending")
|
||||
return uni.navigateTo({
|
||||
url: "/pages/genealogy/g09-my-applications?status=pending",
|
||||
});
|
||||
if (item.relation === "rejected" || item.relation === "removed")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=${item.relation}&genealogyId=${item.id}&genealogyName=${encodeURIComponent(item.name)}`,
|
||||
});
|
||||
return uni.reLaunch({
|
||||
url: `/pages/genealogy/g01-my-genealogies?genealogyId=${item.id}`,
|
||||
});
|
||||
return openPage("G09", { status: "pending" }, "G06");
|
||||
if (
|
||||
["available", "rejected", "removed"].includes(item.relation) &&
|
||||
getGenealogyFixtureAccess(String(item.id)).canApply
|
||||
)
|
||||
return openPage(
|
||||
"G08",
|
||||
{ genealogyId: String(item.id), source: "search" },
|
||||
"G06",
|
||||
);
|
||||
if (item.relation === "joined" || item.relation === "owned")
|
||||
return goRoot("G01", { genealogyId: String(item.id) });
|
||||
return false;
|
||||
};
|
||||
|
||||
const verifyInvite = () => {
|
||||
inviteState.value =
|
||||
inviteCode.value.toUpperCase() === "JP2026" ? "valid" : "invalid";
|
||||
};
|
||||
const confirmInvite = () =>
|
||||
uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=invite&genealogyId=${inviteTarget.value.id}&genealogyName=${encodeURIComponent(inviteTarget.value.name)}`,
|
||||
});
|
||||
const resetInvite = () => {
|
||||
inviteState.value = "initial";
|
||||
};
|
||||
const confirmInvite = () => {
|
||||
if (inviteState.value !== "valid") return false;
|
||||
if (inviteCode.value.trim().toUpperCase() !== "JP2026") return false;
|
||||
return openPage(
|
||||
"G08",
|
||||
{
|
||||
genealogyId: String(inviteTarget.value.id),
|
||||
source: "invite",
|
||||
},
|
||||
"G06",
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<template>
|
||||
<view class="join-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="join-page__header"
|
||||
><PageHeader :title="sourceContract.headerTitle"
|
||||
/></view>
|
||||
<view class="join-page__header">
|
||||
<PageHeader :title="sourceContract.headerTitle" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="join-panel"
|
||||
@@ -12,6 +12,7 @@
|
||||
'join-state--form': joinState === 'form',
|
||||
'join-state--success': joinState === 'success',
|
||||
'join-state--error': joinState === 'error',
|
||||
'join-state--ineligible': joinState === 'ineligible',
|
||||
}"
|
||||
>
|
||||
<view v-if="joinState === 'form'" class="join-form">
|
||||
@@ -20,10 +21,6 @@
|
||||
>{{ sourceContract.formTitle }} {{ genealogyName }}</text
|
||||
>
|
||||
<text class="join-form__copy">{{ sourceContract.formCopy }}</text>
|
||||
<text v-if="previousNotice" class="join-form__previous">{{
|
||||
previousNotice
|
||||
}}</text>
|
||||
|
||||
<view class="join-field">
|
||||
<text>真实姓名</text
|
||||
><input
|
||||
@@ -76,74 +73,113 @@
|
||||
<text class="join-result__eyebrow">{{
|
||||
joinState === "success"
|
||||
? sourceContract.successEyebrow
|
||||
: joinState === "ineligible"
|
||||
? "当前不可申请"
|
||||
: sourceContract.errorEyebrow
|
||||
}}</text>
|
||||
<text class="join-result__title">{{
|
||||
joinState === "success"
|
||||
? sourceContract.successTitle
|
||||
: joinState === "ineligible"
|
||||
? ineligibleTitle
|
||||
: sourceContract.errorTitle
|
||||
}}</text>
|
||||
<text class="join-result__copy">{{ resultCopy }}</text>
|
||||
<view
|
||||
class="join-action"
|
||||
@click="joinState === 'success' ? completeFlow() : retryForm()"
|
||||
@click="
|
||||
joinState === 'success'
|
||||
? completeFlow()
|
||||
: joinState === 'ineligible'
|
||||
? handleIneligibleAction()
|
||||
: retryForm()
|
||||
"
|
||||
>
|
||||
<text>{{
|
||||
joinState === "success" ? sourceContract.nextLabel : "重新填写"
|
||||
joinState === "success"
|
||||
? sourceContract.nextLabel
|
||||
: joinState === "ineligible"
|
||||
? ineligibleActionLabel
|
||||
: "重新填写"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃填写?"
|
||||
message="当前身份关系尚未保存,确认返回后将清空。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续填写"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
getGenealogyFixtureAccess,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
goBack,
|
||||
goRoot,
|
||||
handleBackPress,
|
||||
openPage,
|
||||
returnTo,
|
||||
runBackGuard,
|
||||
} from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const source = ref("search");
|
||||
const previousRelation = ref("");
|
||||
const genealogyName = ref("这部家谱");
|
||||
const genealogyPreview = {
|
||||
1001: "汤氏家谱",
|
||||
1002: "汝南汤氏家谱",
|
||||
};
|
||||
const joinState = ref("form");
|
||||
const isSubmitting = ref(false);
|
||||
const errorMessage = ref("");
|
||||
const ineligibleRelation = ref("unknown");
|
||||
const discardVisible = ref(false);
|
||||
const form = reactive({ realName: "", relation: "", message: "" });
|
||||
const fieldErrors = reactive({ realName: "", relation: "" });
|
||||
const previousNotice = computed(() =>
|
||||
previousRelation.value === "rejected"
|
||||
? "上次申请未通过,请补充更准确的长辈姓名、祖居地或支系信息。"
|
||||
: previousRelation.value === "removed"
|
||||
? "你曾退出或被移出这部家谱,请重新确认身份关系后申请。"
|
||||
: previousRelation.value === "withdrawn"
|
||||
? "上次申请已撤回;如仍希望加入,请重新确认关系并提交。"
|
||||
: "",
|
||||
let submitTimer = null;
|
||||
const isDirty = computed(() =>
|
||||
Boolean(form.realName || form.relation || form.message),
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
const sourceContract = computed(() =>
|
||||
source.value === "invite"
|
||||
? {
|
||||
headerTitle: "确认关系并加入",
|
||||
eyebrow: "邀请码直接加入",
|
||||
formTitle: "确认加入",
|
||||
headerTitle: "确认身份关系",
|
||||
eyebrow: "邀请码定位预览",
|
||||
formTitle: "核对家谱",
|
||||
formCopy:
|
||||
"请填写真实身份和亲属关系;提交后直接加入,无需等待管理员审核。",
|
||||
"请填写真实身份和亲属关系;当前仅验证页面流程,不会变更成员身份。",
|
||||
thirdFieldLabel: "补充信息",
|
||||
thirdFieldPlaceholder: "选填:补充祖居地、长辈姓名等信息",
|
||||
note: "邀请码来源提交后直接加入,无需等待审核。",
|
||||
submitLabel: "确认加入",
|
||||
submittingLabel: "正在加入…",
|
||||
successEyebrow: "已加入家谱",
|
||||
successTitle: "关系信息已提交",
|
||||
errorEyebrow: "加入未完成",
|
||||
errorTitle: "暂时无法加入家谱",
|
||||
note: "后端尚未提供邀请码验证与直接入谱接口。",
|
||||
submitLabel: "完成本地校验",
|
||||
submittingLabel: "正在校验…",
|
||||
successEyebrow: "本地校验完成",
|
||||
successTitle: "信息尚未提交服务器",
|
||||
errorEyebrow: "校验未完成",
|
||||
errorTitle: "暂时无法完成校验",
|
||||
nextLabel: "返回我的家谱",
|
||||
successCopy: `已直接加入“${genealogyName.value}”,无需等待审核;返回后将刷新并选中这部家谱。`,
|
||||
successCopy: `本地流程预览已完成“${genealogyName.value}”的身份填写;返回后不会选中或加入这部家谱。`,
|
||||
}
|
||||
: {
|
||||
headerTitle: "申请加入家谱",
|
||||
@@ -152,42 +188,84 @@ const sourceContract = computed(() =>
|
||||
formCopy: "请填写真实身份和亲属关系,管理员审核后会通过消息告知结果。",
|
||||
thirdFieldLabel: "申请说明",
|
||||
thirdFieldPlaceholder: "补充祖居地、长辈姓名等核验信息",
|
||||
note: "提交后可在“我的申请”中查看审核进度。",
|
||||
submitLabel: "提交申请",
|
||||
submittingLabel: "正在提交…",
|
||||
successEyebrow: "申请已送达",
|
||||
successTitle: "等待管理员核实亲属关系",
|
||||
note: "当前仅验证页面流程,后端申请接口接入后才能正式提交。",
|
||||
submitLabel: "完成本地校验",
|
||||
submittingLabel: "正在校验…",
|
||||
successEyebrow: "本地校验完成",
|
||||
successTitle: "申请尚未提交服务器",
|
||||
errorEyebrow: "申请未提交",
|
||||
errorTitle: "暂时无法提交申请",
|
||||
errorTitle: "暂时无法完成校验",
|
||||
nextLabel: "查看我的申请",
|
||||
successCopy: `“${genealogyName.value}”的管理员会在核实后给出结果,请留意消息中心。`,
|
||||
successCopy: `本地流程预览已完成“${genealogyName.value}”的申请填写,当前不会新增审核记录。`,
|
||||
},
|
||||
);
|
||||
const resultCopy = computed(() =>
|
||||
joinState.value === "success"
|
||||
? sourceContract.value.successCopy
|
||||
: joinState.value === "ineligible"
|
||||
? ({
|
||||
owned: "这是你创建的家谱,无需重复提交加入申请。",
|
||||
joined: "你已经是这部家谱的成员,无需重复申请。",
|
||||
pending: "这部家谱已有待审核申请,请先查看申请进度。",
|
||||
})[ineligibleRelation.value] ||
|
||||
"当前家谱不存在、未公开或不可申请,请返回后重新选择。"
|
||||
: errorMessage.value ||
|
||||
"请检查网络后重新填写;未成功提交的内容不会进入审核列表。",
|
||||
);
|
||||
const ineligibleTitle = computed(
|
||||
() =>
|
||||
({
|
||||
owned: "你已拥有这部家谱",
|
||||
joined: "你已加入这部家谱",
|
||||
pending: "申请正在审核中",
|
||||
})[ineligibleRelation.value] || "无法打开申请表",
|
||||
);
|
||||
const ineligibleActionLabel = computed(
|
||||
() =>
|
||||
ineligibleRelation.value === "pending"
|
||||
? "查看申请进度"
|
||||
: ["owned", "joined"].includes(ineligibleRelation.value)
|
||||
? "返回我的家谱"
|
||||
: "返回上一页",
|
||||
);
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
source.value = query.source === "invite" ? "invite" : "search";
|
||||
previousRelation.value = ["rejected", "removed", "withdrawn"].includes(query.previous)
|
||||
? query.previous
|
||||
: "";
|
||||
if (query.state === "success") {
|
||||
joinState.value = "success";
|
||||
return;
|
||||
}
|
||||
if (!genealogyId.value) {
|
||||
errorMessage.value = "没有找到要申请加入的家谱,请先返回公开家谱检索。";
|
||||
joinState.value = "error";
|
||||
ineligibleRelation.value = "unknown";
|
||||
joinState.value = "ineligible";
|
||||
return;
|
||||
}
|
||||
genealogyName.value = query.genealogyName
|
||||
? decodeURIComponent(query.genealogyName)
|
||||
: genealogyPreview[genealogyId.value] || "这部家谱";
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
genealogyName.value = fixture?.name || "这部家谱";
|
||||
const access = getGenealogyFixtureAccess(genealogyId.value);
|
||||
if (!fixture || !access.canApply) {
|
||||
ineligibleRelation.value = access.relation;
|
||||
joinState.value = "ineligible";
|
||||
return;
|
||||
}
|
||||
if (query.state === "success") joinState.value = "success";
|
||||
});
|
||||
|
||||
const requestBack = () => {
|
||||
if (joinState.value === "success") return completeFlow();
|
||||
return runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
submitting: isSubmitting.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"block-submitting": () => true,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
};
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
onUnload(() => {
|
||||
const timer = submitTimer;
|
||||
submitTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const submitJoin = () => {
|
||||
@@ -195,9 +273,13 @@ const submitJoin = () => {
|
||||
fieldErrors.realName = form.realName.trim() ? "" : "请填写真实姓名";
|
||||
fieldErrors.relation = form.relation.trim() ? "" : "请填写与家谱的关系";
|
||||
if (fieldErrors.realName || fieldErrors.relation) return;
|
||||
const submitSnapshot = Object.freeze({ ...form });
|
||||
isSubmitting.value = true;
|
||||
setTimeout(() => {
|
||||
joinState.value = form.realName.trim() === "失败" ? "error" : "success";
|
||||
const timer = setTimeout(() => {
|
||||
if (submitTimer !== timer) return;
|
||||
submitTimer = null;
|
||||
joinState.value =
|
||||
submitSnapshot.realName.trim() === "失败" ? "error" : "success";
|
||||
if (joinState.value === "error")
|
||||
errorMessage.value =
|
||||
source.value === "invite"
|
||||
@@ -205,6 +287,7 @@ const submitJoin = () => {
|
||||
: "申请暂未提交,请稍后重试。";
|
||||
isSubmitting.value = false;
|
||||
}, 280);
|
||||
submitTimer = timer;
|
||||
};
|
||||
const clearFieldError = (field) => {
|
||||
fieldErrors[field] = "";
|
||||
@@ -213,14 +296,15 @@ const retryForm = () => {
|
||||
joinState.value = "form";
|
||||
errorMessage.value = "";
|
||||
};
|
||||
const toMyApplications = () =>
|
||||
uni.redirectTo({ url: "/pages/genealogy/g09-my-applications" });
|
||||
const toMyGenealogies = () =>
|
||||
uni.reLaunch({
|
||||
url: `/pages/genealogy/g01-my-genealogies?genealogyId=${genealogyId.value}`,
|
||||
});
|
||||
const handleIneligibleAction = () => {
|
||||
if (ineligibleRelation.value === "pending")
|
||||
return openPage("G09", { status: "pending" }, "G08");
|
||||
if (["owned", "joined"].includes(ineligibleRelation.value))
|
||||
return goRoot("G01", { genealogyId: genealogyId.value });
|
||||
return goBack();
|
||||
};
|
||||
const completeFlow = () =>
|
||||
source.value === "invite" ? toMyGenealogies() : toMyApplications();
|
||||
source.value === "invite" ? returnTo("G01", {}) : returnTo("G09", {});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<template>
|
||||
<view class="application-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="application-page__header"
|
||||
><PageHeader title="我的申请"
|
||||
/></view>
|
||||
<view class="application-page__header">
|
||||
<PageHeader title="我的申请" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="application-content"
|
||||
@@ -71,55 +71,59 @@
|
||||
|
||||
<AppDialog
|
||||
:visible="!!withdrawTarget"
|
||||
title="撤回加入申请"
|
||||
title="预览撤回效果"
|
||||
:message="
|
||||
withdrawTarget
|
||||
? `确认撤回对“${withdrawTarget.genealogyName}”的申请?撤回后如需加入,可重新提交。`
|
||||
? `当前只更新本页对“${withdrawTarget.genealogyName}”的撤回预览,不会向服务器提交;真实申请仍可能处于审核中。`
|
||||
: ''
|
||||
"
|
||||
confirm-text="确认撤回"
|
||||
confirm-text="查看本地效果"
|
||||
cancel-text="暂不撤回"
|
||||
show-cancel
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmWithdraw"
|
||||
@cancel="cancelWithdraw"
|
||||
@close="cancelWithdraw"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { findGenealogyFixture } from "@/data/mock.js";
|
||||
import { handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const applications = ref([]);
|
||||
const applicationState = ref("loading");
|
||||
const errorMessage = ref("");
|
||||
const withdrawTarget = ref(null);
|
||||
const genealogyNameFor = (genealogyId) =>
|
||||
findGenealogyFixture(genealogyId)?.name || "未知家谱";
|
||||
const applicationSamples = [
|
||||
{
|
||||
id: 1,
|
||||
genealogyId: 1001,
|
||||
genealogyName: "汤氏家谱",
|
||||
id: "pending-2003",
|
||||
genealogyId: "2003",
|
||||
genealogyName: genealogyNameFor("2003"),
|
||||
relation: "自述为汤正华堂侄",
|
||||
appliedAt: "今天 10:24",
|
||||
status: "PENDING",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
genealogyId: 1002,
|
||||
genealogyName: "汝南汤氏家谱",
|
||||
id: "approved-1002",
|
||||
genealogyId: "1002",
|
||||
genealogyName: genealogyNameFor("1002"),
|
||||
relation: "祖居河南汝南",
|
||||
appliedAt: "昨天 18:02",
|
||||
status: "APPROVED",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
genealogyId: 1003,
|
||||
genealogyName: "清河汤氏家谱",
|
||||
id: "rejected-2004",
|
||||
genealogyId: "2004",
|
||||
genealogyName: genealogyNameFor("2004"),
|
||||
relation: "补充材料不足",
|
||||
appliedAt: "7月12日 09:18",
|
||||
status: "REJECTED",
|
||||
@@ -131,7 +135,7 @@ const statusLabel = (status) =>
|
||||
PENDING: "审核中",
|
||||
APPROVED: "已通过",
|
||||
REJECTED: "未通过",
|
||||
WITHDRAWN: "已撤回",
|
||||
LOCAL_WITHDRAWN: "本地撤回预览",
|
||||
})[status] || "状态未知";
|
||||
|
||||
const statusHint = (status) =>
|
||||
@@ -139,11 +143,11 @@ const statusHint = (status) =>
|
||||
PENDING: "管理员尚未处理,可在审核前撤回",
|
||||
APPROVED: "申请已通过,可进入这部家谱",
|
||||
REJECTED: "请修改关系说明后重新提交",
|
||||
WITHDRAWN: "申请已撤回,不再进入管理员审核",
|
||||
LOCAL_WITHDRAWN: "尚未提交服务器,真实申请仍可能处于审核中",
|
||||
})[status] || "";
|
||||
|
||||
const actionFor = (item) =>
|
||||
({ PENDING: "撤回申请", APPROVED: "进入家谱", REJECTED: "修改后重新提交", WITHDRAWN: "重新申请" })[
|
||||
({ PENDING: "预览撤回效果", APPROVED: "进入家谱", REJECTED: "修改后重新提交" })[
|
||||
item.status
|
||||
] || "";
|
||||
const stateTitle = computed(() =>
|
||||
@@ -155,7 +159,7 @@ const stateTitle = computed(() =>
|
||||
);
|
||||
const stateCopy = computed(() =>
|
||||
applicationState.value === "empty"
|
||||
? "从家谱搜索提交的申请会显示在这里;邀请码直接加入不进入本页。"
|
||||
? "从家谱搜索提交的申请会显示在这里;邀请码本地校验不会生成申请记录。"
|
||||
: applicationState.value === "loading"
|
||||
? "请稍候,正在同步审核状态。"
|
||||
: errorMessage.value || "请检查网络后重新查看。",
|
||||
@@ -188,19 +192,27 @@ const loadApplications = (query = {}) => {
|
||||
};
|
||||
|
||||
onLoad(loadApplications);
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: Boolean(withdrawTarget.value),
|
||||
"close-transient": cancelWithdraw,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const handleApplicationAction = (item) => {
|
||||
if (item.status === "APPROVED")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g05-genealogy-overview?genealogyId=${item.genealogyId}`,
|
||||
});
|
||||
return openPage(
|
||||
"G05",
|
||||
{ genealogyId: String(item.genealogyId) },
|
||||
"G09",
|
||||
);
|
||||
if (item.status === "REJECTED")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=rejected&genealogyId=${item.genealogyId}&genealogyName=${encodeURIComponent(item.genealogyName)}`,
|
||||
});
|
||||
if (item.status === "WITHDRAWN")
|
||||
return uni.navigateTo({
|
||||
url: `/pages/genealogy/g08-join-application?source=search&previous=withdrawn&genealogyId=${item.genealogyId}&genealogyName=${encodeURIComponent(item.genealogyName)}`,
|
||||
});
|
||||
return openPage(
|
||||
"G08",
|
||||
{ genealogyId: String(item.genealogyId), source: "search" },
|
||||
"G09",
|
||||
);
|
||||
if (item.status === "PENDING") withdrawTarget.value = item;
|
||||
};
|
||||
const cancelWithdraw = () => {
|
||||
@@ -210,11 +222,10 @@ const confirmWithdraw = () => {
|
||||
const target = applications.value.find(
|
||||
(item) => item.id === withdrawTarget.value?.id,
|
||||
);
|
||||
if (target) target.status = "WITHDRAWN";
|
||||
if (target) target.status = "LOCAL_WITHDRAWN";
|
||||
cancelWithdraw();
|
||||
};
|
||||
const toSearch = () =>
|
||||
uni.navigateTo({ url: "/pages/genealogy/g06-search-genealogies" });
|
||||
const toSearch = () => openPage("G06", {}, "G09");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
<template>
|
||||
<view class="review-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="review-page__header"
|
||||
><PageHeader title="入谱审核" action="说明" @action="showHelp"
|
||||
/></view>
|
||||
<view class="review-page__header">
|
||||
<PageHeader
|
||||
title="入谱审核"
|
||||
action="说明"
|
||||
custom-back
|
||||
@action="showHelp"
|
||||
@back="requestBack"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="review-content"
|
||||
@@ -17,7 +23,7 @@
|
||||
>
|
||||
<view v-if="reviewState !== 'loading'" class="review-intro">
|
||||
<text>核实亲属关系后再决定</text>
|
||||
<text>审核结果会通过消息告知申请人</text>
|
||||
<text>当前只预览审核交互,不会提交服务器</text>
|
||||
</view>
|
||||
|
||||
<template v-if="reviewState === 'list'">
|
||||
@@ -35,12 +41,12 @@
|
||||
<AppButton
|
||||
compact
|
||||
type="secondary"
|
||||
label="拒绝申请"
|
||||
label="预览拒绝"
|
||||
@click="confirmAudit(item, false)"
|
||||
/>
|
||||
<AppButton
|
||||
compact
|
||||
label="通过申请"
|
||||
label="预览通过"
|
||||
@click="confirmAudit(item, true)"
|
||||
/>
|
||||
</view>
|
||||
@@ -86,28 +92,28 @@
|
||||
helpVisible
|
||||
? '审核说明'
|
||||
: confirmation?.approved
|
||||
? '确认通过申请?'
|
||||
: '确认拒绝申请?'
|
||||
? '预览通过效果?'
|
||||
: '预览拒绝效果?'
|
||||
"
|
||||
:message="
|
||||
helpVisible
|
||||
? '请核对申请人的姓名、亲属关系和补充说明,仅确认与本家谱存在真实关系的申请。'
|
||||
: confirmation?.approved
|
||||
? '通过后,申请人将成为本家谱成员。'
|
||||
: '拒绝后,申请人会收到审核结果,并可修改后重新申请。'
|
||||
? '当前只更新本页本地预览,不会让申请人成为成员,也不会提交服务器。'
|
||||
: '当前只更新本页本地预览,不会通知申请人,也不会提交服务器。'
|
||||
"
|
||||
:confirm-text="
|
||||
helpVisible
|
||||
? '我知道了'
|
||||
: confirmation?.approved
|
||||
? '确认通过'
|
||||
: '确认拒绝'
|
||||
? '查看通过效果'
|
||||
: '查看拒绝效果'
|
||||
"
|
||||
:show-cancel="!!confirmation"
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="helpVisible ? closeDialog() : applyAudit()"
|
||||
@cancel="closeDialog"
|
||||
@close="closeDialog"
|
||||
>
|
||||
<view v-if="confirmation && !confirmation.approved" class="rejection-field">
|
||||
<text>拒绝原因</text>
|
||||
@@ -140,18 +146,19 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppButton from "@/components/AppButton.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import { genealogyContext } from "@/utils/genealogy-context.js";
|
||||
import { getGenealogyFixtureAccess } from "@/data/mock.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const applications = ref([]);
|
||||
const reviewSamples = [
|
||||
{
|
||||
id: 1,
|
||||
id: "review-1",
|
||||
name: "汤志成",
|
||||
phone: "139****6421",
|
||||
relation: "自述为汤正华堂侄 · 祖居洛阳",
|
||||
@@ -159,7 +166,7 @@ const reviewSamples = [
|
||||
status: "PENDING",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: "review-2",
|
||||
name: "汤雨薇",
|
||||
phone: "136****2798",
|
||||
relation: "自述为汤正国之女 · 已补充长辈姓名",
|
||||
@@ -179,7 +186,9 @@ const feedbackVisible = ref(false);
|
||||
const feedbackMessage = ref("");
|
||||
let feedbackTimer = null;
|
||||
onUnload(() => {
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = feedbackTimer;
|
||||
feedbackTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
const stateTitle = computed(() =>
|
||||
reviewState.value === "empty"
|
||||
@@ -199,11 +208,18 @@ const stateCopy = computed(() =>
|
||||
const loadApplications = (query = {}) => {
|
||||
reviewState.value = "loading";
|
||||
errorMessage.value = "";
|
||||
genealogyId.value =
|
||||
query.genealogyId?.value ||
|
||||
query.genealogyId ||
|
||||
genealogyId.value ||
|
||||
genealogyContext.getCurrentGenealogyId();
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
if (!genealogyId.value) {
|
||||
errorMessage.value = "没有找到当前家谱,请从家谱总览进入。";
|
||||
reviewState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (
|
||||
getGenealogyFixtureAccess(genealogyId.value).accessRole !== "owner"
|
||||
) {
|
||||
reviewState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
if (query.state === "loading") {
|
||||
reviewState.value = "loading";
|
||||
return;
|
||||
@@ -220,12 +236,6 @@ const loadApplications = (query = {}) => {
|
||||
reviewState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
if (!genealogyId.value) {
|
||||
errorMessage.value = "没有找到当前家谱,请从家谱总览进入。";
|
||||
reviewState.value = "error";
|
||||
return;
|
||||
}
|
||||
genealogyContext.setCurrentGenealogyId(genealogyId.value);
|
||||
applications.value = reviewSamples.map((item) => ({ ...item }));
|
||||
reviewState.value = "list";
|
||||
};
|
||||
@@ -244,14 +254,24 @@ const closeDialog = () => {
|
||||
rejectionFocused.value = false;
|
||||
helpVisible.value = false;
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: Boolean(confirmation.value) || helpVisible.value,
|
||||
"close-transient": closeDialog,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const showFeedback = (message) => {
|
||||
feedbackMessage.value = message;
|
||||
feedbackVisible.value = true;
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
feedbackTimer = setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (feedbackTimer !== timer) return;
|
||||
feedbackVisible.value = false;
|
||||
feedbackTimer = null;
|
||||
}, 1800);
|
||||
feedbackTimer = timer;
|
||||
};
|
||||
const clearRejectionError = () => {
|
||||
rejectionError.value = "";
|
||||
@@ -269,7 +289,9 @@ const applyAudit = async () => {
|
||||
current.item.status = current.approved ? "APPROVED" : "REJECTED";
|
||||
if (!current.approved) current.item.rejectionReason = rejectionReason.value.trim();
|
||||
closeDialog();
|
||||
showFeedback(current.approved ? "已通过申请" : "已拒绝申请");
|
||||
showFeedback(
|
||||
`本地审核预览已更新,尚未提交服务器 · ${current.approved ? "已通过" : "已拒绝"}`,
|
||||
);
|
||||
};
|
||||
const showHelp = () => {
|
||||
helpVisible.value = true;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!-- 页面编号:G-11;用途:家谱设置、公开范围与访问说明(页面设计阶段使用本地模拟交互)。 -->
|
||||
<!-- 页面编号:G-11;用途:家谱设置、访问预设与家谱简介(页面设计阶段使用本地模拟交互)。 -->
|
||||
<template>
|
||||
<view class="settings-page">
|
||||
<GenealogyPageBackground />
|
||||
<view class="settings-page__header"><PageHeader title="家谱设置" /></view>
|
||||
<view class="settings-page__header">
|
||||
<PageHeader title="家谱设置" custom-back @back="requestBack" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="settings-panel"
|
||||
@@ -22,7 +24,7 @@
|
||||
<text class="settings-form__eyebrow">谱主可见 · 基础设置</text>
|
||||
<text class="settings-form__title">完善家谱访问规则</text>
|
||||
<text class="settings-form__copy"
|
||||
>这里仅安排接口支持的名称、公开范围与访问说明;管理权转让将在成员选择场景中单独处理。</text
|
||||
>这里维护名称、访问规则与家谱简介;访问规则会同时决定公开范围和加入方式,管理权转让在成员场景中单独处理。</text
|
||||
>
|
||||
|
||||
<view class="settings-field">
|
||||
@@ -40,17 +42,17 @@
|
||||
}}</text>
|
||||
|
||||
<view class="visibility-block">
|
||||
<text class="visibility-block__label">公开范围</text>
|
||||
<text class="visibility-block__label">访问规则</text>
|
||||
<view class="visibility-options">
|
||||
<view
|
||||
v-for="option in visibilityOptions"
|
||||
v-for="option in accessPresetOptions"
|
||||
:key="option.value"
|
||||
class="visibility-option"
|
||||
@click="genealogyDraft.visibility = option.value"
|
||||
@click="genealogyDraft.accessPreset = option.value"
|
||||
>
|
||||
<image
|
||||
:src="
|
||||
genealogyDraft.visibility === option.value
|
||||
genealogyDraft.accessPreset === option.value
|
||||
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
|
||||
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
|
||||
"
|
||||
@@ -59,7 +61,7 @@
|
||||
<text
|
||||
:class="{
|
||||
'visibility-option__text--active':
|
||||
genealogyDraft.visibility === option.value,
|
||||
genealogyDraft.accessPreset === option.value,
|
||||
}"
|
||||
>{{ option.label }}</text
|
||||
>
|
||||
@@ -69,12 +71,12 @@
|
||||
</view>
|
||||
|
||||
<view class="settings-field settings-field--note">
|
||||
<text>访问说明</text>
|
||||
<text>家谱简介</text>
|
||||
<textarea
|
||||
v-model="genealogyDraft.accessNote"
|
||||
v-model="genealogyDraft.intro"
|
||||
auto-height
|
||||
maxlength="80"
|
||||
placeholder="向访问者说明家谱用途"
|
||||
placeholder="简要介绍家谱来源与支系"
|
||||
placeholder-class="settings-placeholder"
|
||||
/>
|
||||
</view>
|
||||
@@ -94,85 +96,123 @@
|
||||
}}</text>
|
||||
<text class="settings-result__title">{{
|
||||
settingsState === "success"
|
||||
? "新的访问规则已经生效"
|
||||
? "本页设置草稿已更新"
|
||||
: settingsState === "no-permission"
|
||||
? "当前账号不能修改家谱"
|
||||
: "暂时无法打开家谱设置"
|
||||
}}</text>
|
||||
<text class="settings-result__copy">{{
|
||||
settingsState === "success"
|
||||
? `${genealogyDraft.name} · ${visibilityLabel}`
|
||||
? `本地预览已更新,尚未提交服务器;当前只保留在本页,返回总览不会改变原资料 · ${genealogyDraft.name} · ${accessPresetLabel}`
|
||||
: settingsState === "no-permission"
|
||||
? "只有家谱所有者可以修改名称、公开范围和访问说明。"
|
||||
? "只有家谱所有者可以修改名称、访问规则和家谱简介。"
|
||||
: "请从家谱总览重新进入,当前修改不会被保留。"
|
||||
}}</text>
|
||||
<view class="settings-action" @click="settingsState = 'form'">
|
||||
<view class="settings-action" @click="handleResultAction">
|
||||
<text>{{
|
||||
settingsState === "success" ? "继续调整" : "重新查看"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃设置修改?"
|
||||
message="当前修改尚未保存,确认返回后将清空。"
|
||||
confirm-text="放弃并返回"
|
||||
cancel-text="继续修改"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
<view v-if="feedbackVisible" class="settings-feedback">
|
||||
<text>设置已保存</text>
|
||||
<text>本页草稿已更新</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
getGenealogyFixtureAccess,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
GENEALOGY_ACCESS_PRESET,
|
||||
GENEALOGY_ACCESS_PRESET_OPTIONS,
|
||||
isGenealogyAccessPreset,
|
||||
} from "@/utils/genealogy-contracts.js";
|
||||
import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const genealogyId = ref("");
|
||||
const settingsState = ref("loading");
|
||||
const nameError = ref("");
|
||||
const feedbackVisible = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
let feedbackTimer = null;
|
||||
const settingsFixtures = {
|
||||
1001: {
|
||||
name: "汤氏家谱",
|
||||
visibility: "MEMBER_ONLY",
|
||||
accessNote: "家族资料,请妥善保存",
|
||||
},
|
||||
1002: {
|
||||
name: "汤氏宗谱",
|
||||
visibility: "PUBLIC_APPLY",
|
||||
accessNote: "公开家谱身份,成员资料需审核后查看",
|
||||
},
|
||||
};
|
||||
const genealogyDraft = reactive({
|
||||
name: "汤氏家谱",
|
||||
visibility: "MEMBER_ONLY",
|
||||
accessNote: "家族资料,请妥善保存",
|
||||
name: "",
|
||||
accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY,
|
||||
intro: "",
|
||||
});
|
||||
const originalDraft = ref("");
|
||||
const isDirty = computed(
|
||||
() => JSON.stringify(genealogyDraft) !== originalDraft.value,
|
||||
);
|
||||
const visibilityOptions = [
|
||||
{ value: "MEMBER_ONLY", label: "仅成员可见" },
|
||||
{ value: "PUBLIC_APPLY", label: "公开可申请" },
|
||||
];
|
||||
const visibilityLabel = computed(
|
||||
() =>
|
||||
visibilityOptions.find((item) => item.value === genealogyDraft.visibility)
|
||||
settingsState.value === "form" &&
|
||||
JSON.stringify(genealogyDraft) !== originalDraft.value,
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
const accessPresetOptions = GENEALOGY_ACCESS_PRESET_OPTIONS;
|
||||
const accessPresetLabel = computed(
|
||||
() =>
|
||||
accessPresetOptions.find((item) => item.value === genealogyDraft.accessPreset)
|
||||
?.label || "",
|
||||
);
|
||||
const visibilityHint = computed(() =>
|
||||
genealogyDraft.visibility === "MEMBER_ONLY"
|
||||
genealogyDraft.accessPreset === GENEALOGY_ACCESS_PRESET.MEMBER_ONLY
|
||||
? "只有已加入本家谱的成员可以查看谱系和家族资料。"
|
||||
: "访客可检索到家谱并提交入谱申请,资料仍需审核后查看。",
|
||||
);
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
Object.assign(
|
||||
genealogyDraft,
|
||||
settingsFixtures[genealogyId.value] || settingsFixtures[1001],
|
||||
);
|
||||
const loadSettings = (query = {}) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
if (!genealogyId.value) {
|
||||
settingsState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (
|
||||
getGenealogyFixtureAccess(genealogyId.value).accessRole !== "owner"
|
||||
) {
|
||||
settingsState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
if (!fixture) {
|
||||
settingsState.value = "error";
|
||||
return;
|
||||
}
|
||||
if (!isGenealogyAccessPreset(fixture.accessPreset)) {
|
||||
settingsState.value = "error";
|
||||
return;
|
||||
}
|
||||
Object.assign(genealogyDraft, {
|
||||
name: fixture.name,
|
||||
accessPreset: fixture.accessPreset,
|
||||
intro: fixture.publicDescription || "家族资料,请妥善保存",
|
||||
});
|
||||
originalDraft.value = JSON.stringify(genealogyDraft);
|
||||
settingsState.value =
|
||||
query.state === "loading"
|
||||
@@ -184,27 +224,59 @@ onLoad((query) => {
|
||||
: query.state === "error" || !genealogyId.value
|
||||
? "error"
|
||||
: "form";
|
||||
});
|
||||
};
|
||||
|
||||
onLoad(loadSettings);
|
||||
onUnload(() => {
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = feedbackTimer;
|
||||
feedbackTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
dirty: isDirty.value,
|
||||
"close-transient": cancelDiscard,
|
||||
"confirm-discard": requestDiscardConfirmation,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const continueEditing = () => {
|
||||
if (
|
||||
getGenealogyFixtureAccess(genealogyId.value).accessRole === "owner"
|
||||
) {
|
||||
settingsState.value = "form";
|
||||
}
|
||||
};
|
||||
const handleResultAction = () => {
|
||||
if (settingsState.value === "success") return continueEditing();
|
||||
if (settingsState.value === "error")
|
||||
return loadSettings({ genealogyId: genealogyId.value });
|
||||
return goBack();
|
||||
};
|
||||
|
||||
const saveSettings = () => {
|
||||
if (!genealogyDraft.name.trim()) {
|
||||
nameError.value = "请填写家谱名称";
|
||||
return;
|
||||
}
|
||||
if (genealogyDraft.name.trim().length > 30) {
|
||||
nameError.value = "家谱名称不能超过 30 个字";
|
||||
if (genealogyDraft.name.trim().length > 20) {
|
||||
nameError.value = "家谱名称不能超过 20 个字";
|
||||
return;
|
||||
}
|
||||
originalDraft.value = JSON.stringify(genealogyDraft);
|
||||
settingsState.value = "success";
|
||||
feedbackVisible.value = true;
|
||||
feedbackTimer = setTimeout(() => {
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = setTimeout(() => {
|
||||
if (feedbackTimer !== timer) return;
|
||||
feedbackVisible.value = false;
|
||||
feedbackTimer = null;
|
||||
}, 1800);
|
||||
feedbackTimer = timer;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
<GenealogyPageBackground />
|
||||
<view class="poem-page__header"
|
||||
><PageHeader
|
||||
title="字辈诗"
|
||||
:action="poemState === 'list' ? '维护' : ''"
|
||||
:title="pageTitle"
|
||||
:action="canManage && poemState === 'list' ? '维护' : ''"
|
||||
custom-back
|
||||
@action="openEditor"
|
||||
@back="requestBack"
|
||||
/></view>
|
||||
|
||||
<view
|
||||
@@ -25,26 +27,38 @@
|
||||
description="请稍候,正在读取家谱字序。"
|
||||
/>
|
||||
<view v-else-if="poemState === 'list'" class="poem-list">
|
||||
<text class="poem-list__eyebrow">汤氏家谱 · 传承字序</text>
|
||||
<text class="poem-list__title">启宗敦本,继世传芳</text>
|
||||
<text class="poem-list__copy"
|
||||
>按世代查看字辈,当前家谱使用到“敦”字辈。</text
|
||||
>
|
||||
<text class="poem-list__eyebrow">{{ genealogyName }} · 传承字序</text>
|
||||
<text class="poem-list__title">{{ generationRangeTitle }}</text>
|
||||
<text class="poem-list__copy">{{ currentGenerationCopy }}</text>
|
||||
<view class="poem-rows">
|
||||
<view
|
||||
v-for="item in poemRows"
|
||||
v-for="item in visiblePoemRows"
|
||||
:key="item.generationNo"
|
||||
class="poem-row"
|
||||
:class="{ 'poem-row--current': item.current }"
|
||||
:class="{
|
||||
'poem-row--current': item.current,
|
||||
'poem-row--disabled': item.status === GENERATION_POEM_STATUS.DISABLED,
|
||||
}"
|
||||
>
|
||||
<text class="poem-row__number">第 {{ item.generationNo }} 世</text>
|
||||
<text class="poem-row__character">{{ item.character }}</text>
|
||||
<text class="poem-row__character">{{ item.generationText }}</text>
|
||||
<text class="poem-row__status">{{
|
||||
item.current ? "当前字辈" : "传承字序"
|
||||
item.current
|
||||
? "当前字辈"
|
||||
: item.status === GENERATION_POEM_STATUS.DISABLED
|
||||
? "已停用·记录保留"
|
||||
: "传承字序"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="poem-action" @click="openEditor">
|
||||
<view
|
||||
v-if="remainingPoemCount > 0"
|
||||
class="poem-load-more"
|
||||
@click="loadMorePoems"
|
||||
>
|
||||
<text>继续加载后续字辈(剩余 {{ remainingPoemCount }} 代)</text>
|
||||
</view>
|
||||
<view v-if="canManage" class="poem-action" @click="openEditor">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
@@ -54,31 +68,33 @@
|
||||
|
||||
<view v-else-if="poemState === 'edit'" class="poem-editor">
|
||||
<text class="poem-list__eyebrow">批量维护</text>
|
||||
<text class="poem-list__title">录入连续字辈</text>
|
||||
<text class="poem-list__title">录入完整字辈序列</text>
|
||||
<text class="poem-list__copy"
|
||||
>按照接口支持的连续文本录入,每个汉字对应一代;保存前可预览新字序。</text
|
||||
>无分隔符时每个字符对应一代;也可用空格、逗号、分号、顿号、斜杠或竖线分隔多字字辈。一次最多
|
||||
{{ MAX_GENERATION_COUNT }} 代,每代最多
|
||||
{{ MAX_GENERATION_TEXT_LENGTH }} 个字符。本页只更新本地预览,尚未提交服务器。</text
|
||||
>
|
||||
<view class="poem-field">
|
||||
<text>字辈内容</text>
|
||||
<textarea
|
||||
v-model="poemDraft"
|
||||
auto-height
|
||||
maxlength="50"
|
||||
placeholder="例如:启宗敦本继世传芳"
|
||||
:maxlength="MAX_GENERATION_POEM_INPUT_LENGTH * 2"
|
||||
placeholder="例如:启宗敦本,或 克勤 克俭 承先 启后"
|
||||
placeholder-class="poem-placeholder"
|
||||
@input="poemError = ''"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="poemError" class="poem-field-error">{{ poemError }}</text>
|
||||
<view class="poem-policy">
|
||||
<text>缺失旧世代时</text>
|
||||
<text>未被新文本覆盖的后续世代</text>
|
||||
<view
|
||||
class="poem-policy__option"
|
||||
@click="stopMissingOldGeneration = !stopMissingOldGeneration"
|
||||
@click="disableMissing = !disableMissing"
|
||||
>
|
||||
<image
|
||||
:src="
|
||||
stopMissingOldGeneration
|
||||
disableMissing
|
||||
? '/static/assets/foundation/transparent/a01-scroll-primary-v3.png'
|
||||
: '/static/assets/foundation/transparent/a01-scroll-secondary-v3.png'
|
||||
"
|
||||
@@ -86,17 +102,17 @@
|
||||
/>
|
||||
<text
|
||||
:class="{
|
||||
'poem-policy__option-text--active': stopMissingOldGeneration,
|
||||
'poem-policy__option-text--active': disableMissing,
|
||||
}"
|
||||
>{{ stopMissingOldGeneration ? "停止并提醒" : "继续补录" }}</text
|
||||
>{{ disableMissing ? "停用并保留记录" : "保持原状态" }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<text class="poem-preview"
|
||||
>预览:{{ previewCharacters || "尚未录入字辈" }}</text
|
||||
>预览:{{ previewSummary }}</text
|
||||
>
|
||||
<view class="poem-editor__actions">
|
||||
<view class="poem-action" @click="poemState = 'list'">
|
||||
<view class="poem-action" @click="requestLeaveEditor">
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-secondary-v3.png"
|
||||
mode="aspectFit"
|
||||
@@ -134,56 +150,171 @@
|
||||
: "请从家谱总览重新进入,或稍后再试。"
|
||||
}}</text>
|
||||
<view
|
||||
v-if="poemState !== 'empty' || canManage"
|
||||
class="poem-action"
|
||||
@click="poemState === 'empty' ? openEditor() : (poemState = 'list')"
|
||||
@click="handleStateAction"
|
||||
>
|
||||
<image
|
||||
src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"
|
||||
mode="aspectFit"
|
||||
/><text>{{ poemState === "empty" ? "开始录入" : "重新查看" }}</text>
|
||||
/><text>{{
|
||||
poemState === "empty"
|
||||
? "开始录入"
|
||||
: poemState === "no-permission"
|
||||
? "返回家谱总览"
|
||||
: "重新查看"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<AppDialog
|
||||
:visible="discardVisible"
|
||||
title="放弃字辈修改?"
|
||||
message="当前字辈草稿尚未保存,确认后将恢复进入编辑器前的内容。"
|
||||
confirm-text="放弃修改"
|
||||
cancel-text="继续编辑"
|
||||
show-cancel
|
||||
compact-actions
|
||||
:close-on-mask="false"
|
||||
@confirm="confirmDiscard"
|
||||
@cancel="cancelDiscard"
|
||||
/>
|
||||
<view v-if="feedbackVisible" class="poem-feedback">
|
||||
<text>字辈预览已更新</text>
|
||||
<text>本地字辈预览已更新</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import { onBackPress, onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AppDialog from "@/components/AppDialog.vue";
|
||||
import AppLoading from "@/components/AppLoading.vue";
|
||||
import GenealogyPageBackground from "@/components/GenealogyPageBackground.vue";
|
||||
import PageHeader from "@/components/PageHeader.vue";
|
||||
import {
|
||||
findGenealogyFixture,
|
||||
getGenealogyFixtureAccess,
|
||||
} from "@/data/mock.js";
|
||||
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
|
||||
import {
|
||||
MAX_GENERATION_COUNT,
|
||||
MAX_GENERATION_POEM_INPUT_LENGTH,
|
||||
MAX_GENERATION_TEXT_LENGTH,
|
||||
GENERATION_POEM_STATUS,
|
||||
findFirstGenerationGap,
|
||||
mergeGenerationPoemRows,
|
||||
validateGenerationPoemText,
|
||||
} from "@/utils/generation-poem.js";
|
||||
import { handleBackPress, runBackGuard } from "@/utils/navigation.js";
|
||||
|
||||
const INITIAL_POEM_TEXT = "启宗敦本";
|
||||
const POEM_RENDER_BATCH_SIZE = 50;
|
||||
const PREVIEW_GENERATION_LIMIT = 12;
|
||||
const LOCAL_PREVIEW_START_GENERATION = 1;
|
||||
const LOCAL_CURRENT_GENERATION = 3;
|
||||
const genealogyId = ref("");
|
||||
const genealogyName = ref("家谱");
|
||||
const poemState = ref("loading");
|
||||
const poemError = ref("");
|
||||
const feedbackVisible = ref(false);
|
||||
const discardVisible = ref(false);
|
||||
const canManage = ref(false);
|
||||
let feedbackTimer = null;
|
||||
const poemDraft = ref("启宗敦本继世传芳");
|
||||
const stopMissingOldGeneration = ref(true);
|
||||
const startGeneration = ref(12);
|
||||
const currentGeneration = ref(14);
|
||||
const poemRows = ref([
|
||||
{ generationNo: 12, character: "启", current: false },
|
||||
{ generationNo: 13, character: "宗", current: false },
|
||||
{ generationNo: 14, character: "敦", current: true },
|
||||
{ generationNo: 15, character: "本", current: false },
|
||||
]);
|
||||
const previewCharacters = computed(() =>
|
||||
poemDraft.value.trim().split("").join(" · "),
|
||||
const poemDraft = ref(INITIAL_POEM_TEXT);
|
||||
// OpenAPI 的 disableMissing 示例为 false;本地同样采用安全默认 false。停用后续世代是高影响动作,
|
||||
// 必须由谱主主动选择,不能把缩短一次本地草稿解释为默认停用历史记录。
|
||||
const disableMissing = ref(false);
|
||||
const editorOrigin = ref("list");
|
||||
const editorSnapshot = ref(null);
|
||||
const poemRows = ref([]);
|
||||
const visiblePoemCount = ref(POEM_RENDER_BATCH_SIZE);
|
||||
const visiblePoemRows = computed(() =>
|
||||
poemRows.value.slice(0, visiblePoemCount.value),
|
||||
);
|
||||
const remainingPoemCount = computed(() =>
|
||||
poemRows.value.length > visiblePoemCount.value
|
||||
? poemRows.value.length - visiblePoemCount.value
|
||||
: 0,
|
||||
);
|
||||
const pageTitle = computed(() =>
|
||||
poemState.value === "edit" ? "维护字辈诗" : "字辈诗",
|
||||
);
|
||||
const generationRangeTitle = computed(() => {
|
||||
if (!poemRows.value.length) return "尚未建立字辈";
|
||||
const first = poemRows.value[0].generationNo;
|
||||
const last = poemRows.value[poemRows.value.length - 1].generationNo;
|
||||
return first === last ? `第 ${first} 世字辈` : `第 ${first}—${last} 世字辈`;
|
||||
});
|
||||
const currentGenerationCopy = computed(() => {
|
||||
const current = poemRows.value.find((item) => item.current);
|
||||
return current
|
||||
? `当前为第 ${current.generationNo} 世“${current.generationText}”字辈。`
|
||||
: `当前第 ${LOCAL_CURRENT_GENERATION} 世尚未被有效字辈覆盖。`;
|
||||
});
|
||||
const previewSummary = computed(() => {
|
||||
const validation = validateGenerationPoemText(poemDraft.value);
|
||||
if (!validation.valid) {
|
||||
return poemDraft.value.trim() ? validation.message : "尚未录入字辈";
|
||||
}
|
||||
const visible = validation.generations
|
||||
.slice(0, PREVIEW_GENERATION_LIMIT)
|
||||
.join(" · ");
|
||||
const remaining = validation.generations.length - PREVIEW_GENERATION_LIMIT;
|
||||
return remaining > 0 ? `${visible} · …另 ${remaining} 代` : visible;
|
||||
});
|
||||
const isDirty = computed(() =>
|
||||
Boolean(
|
||||
poemState.value === "edit" &&
|
||||
editorSnapshot.value &&
|
||||
(poemDraft.value !== editorSnapshot.value.poemDraft ||
|
||||
disableMissing.value !== editorSnapshot.value.disableMissing),
|
||||
),
|
||||
);
|
||||
const discardConfirmation = createDiscardConfirmation((visible) => {
|
||||
discardVisible.value = visible;
|
||||
});
|
||||
const requestDiscardConfirmation = discardConfirmation.request;
|
||||
const confirmDiscard = discardConfirmation.confirm;
|
||||
const cancelDiscard = discardConfirmation.cancel;
|
||||
|
||||
onLoad((query) => {
|
||||
genealogyId.value = query.genealogyId || "";
|
||||
startGeneration.value = Math.max(1, Number(query.startGeneration) || 12);
|
||||
currentGeneration.value = Math.max(
|
||||
startGeneration.value,
|
||||
Number(query.currentGeneration) || 14,
|
||||
const resetPoemRows = () => {
|
||||
const seed = validateGenerationPoemText(INITIAL_POEM_TEXT).generations;
|
||||
poemDraft.value = INITIAL_POEM_TEXT;
|
||||
poemRows.value = mergeGenerationPoemRows({
|
||||
existingRows: [],
|
||||
generationTexts: seed,
|
||||
startGeneration: LOCAL_PREVIEW_START_GENERATION,
|
||||
currentGeneration: LOCAL_CURRENT_GENERATION,
|
||||
disableMissing: false,
|
||||
});
|
||||
visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
|
||||
};
|
||||
|
||||
const loadMorePoems = () => {
|
||||
visiblePoemCount.value = Math.min(
|
||||
poemRows.value.length,
|
||||
visiblePoemCount.value + POEM_RENDER_BATCH_SIZE,
|
||||
);
|
||||
poemState.value =
|
||||
};
|
||||
|
||||
const loadPoems = (query = {}) => {
|
||||
genealogyId.value = String(query.genealogyId || "");
|
||||
const fixture = findGenealogyFixture(genealogyId.value);
|
||||
if (!fixture) {
|
||||
canManage.value = false;
|
||||
poemState.value = "error";
|
||||
return;
|
||||
}
|
||||
genealogyName.value = fixture.name || "家谱";
|
||||
const accessRole = getGenealogyFixtureAccess(genealogyId.value).accessRole;
|
||||
canManage.value = accessRole === "owner";
|
||||
if (accessRole === "guest") {
|
||||
poemState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
resetPoemRows();
|
||||
const requestedState =
|
||||
query.state === "loading"
|
||||
? "loading"
|
||||
: query.state === "empty"
|
||||
@@ -192,47 +323,123 @@ onLoad((query) => {
|
||||
? "edit"
|
||||
: query.state === "no-permission"
|
||||
? "no-permission"
|
||||
: query.state === "error" || !genealogyId.value
|
||||
: query.state === "error"
|
||||
? "error"
|
||||
: "list";
|
||||
});
|
||||
if (requestedState === "empty") {
|
||||
poemDraft.value = "";
|
||||
poemRows.value = [];
|
||||
}
|
||||
if (requestedState === "edit") {
|
||||
if (!canManage.value) {
|
||||
poemState.value = "no-permission";
|
||||
return;
|
||||
}
|
||||
poemState.value = "list";
|
||||
openEditor("list");
|
||||
return;
|
||||
}
|
||||
poemState.value = requestedState;
|
||||
};
|
||||
|
||||
onLoad(loadPoems);
|
||||
onUnload(() => {
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = feedbackTimer;
|
||||
feedbackTimer = null;
|
||||
if (timer) clearTimeout(timer);
|
||||
discardConfirmation.dispose();
|
||||
});
|
||||
|
||||
const openEditor = () => {
|
||||
const openEditor = (origin = poemState.value) => {
|
||||
if (!canManage.value) return false;
|
||||
editorOrigin.value = origin === "empty" ? "empty" : "list";
|
||||
editorSnapshot.value = Object.freeze({
|
||||
poemDraft: poemDraft.value,
|
||||
disableMissing: disableMissing.value,
|
||||
});
|
||||
poemState.value = "edit";
|
||||
return true;
|
||||
};
|
||||
const restoreEditorSnapshot = () => {
|
||||
if (!editorSnapshot.value) return;
|
||||
poemDraft.value = editorSnapshot.value.poemDraft;
|
||||
disableMissing.value = editorSnapshot.value.disableMissing;
|
||||
};
|
||||
const requestLeaveEditor = async () => {
|
||||
if (isDirty.value) {
|
||||
const confirmed = await requestDiscardConfirmation();
|
||||
if (!confirmed) return false;
|
||||
}
|
||||
restoreEditorSnapshot();
|
||||
poemState.value = editorOrigin.value;
|
||||
editorSnapshot.value = null;
|
||||
poemError.value = "";
|
||||
return true;
|
||||
};
|
||||
const requestBack = () =>
|
||||
runBackGuard({
|
||||
transientOpen: discardVisible.value,
|
||||
internalTrail: poemState.value === "edit",
|
||||
"close-transient": cancelDiscard,
|
||||
"pop-internal-trail": requestLeaveEditor,
|
||||
});
|
||||
|
||||
onBackPress((event) => handleBackPress(event, requestBack));
|
||||
|
||||
const handleStateAction = () => {
|
||||
if (poemState.value === "empty") return openEditor("empty");
|
||||
if (poemState.value === "error")
|
||||
return loadPoems({ genealogyId: genealogyId.value });
|
||||
return requestBack();
|
||||
};
|
||||
const savePoems = () => {
|
||||
const characters = poemDraft.value.trim().split("").filter(Boolean);
|
||||
if (!characters.length) {
|
||||
poemError.value = "请录入字辈内容";
|
||||
const validation = validateGenerationPoemText(poemDraft.value);
|
||||
if (!validation.valid) {
|
||||
poemError.value = validation.message;
|
||||
return;
|
||||
}
|
||||
if (poemDraft.value.trim() === "失败") {
|
||||
poemError.value = "字辈保存失败,请稍后重试";
|
||||
const lastGeneration =
|
||||
LOCAL_PREVIEW_START_GENERATION + validation.generations.length - 1;
|
||||
if (LOCAL_CURRENT_GENERATION > lastGeneration) {
|
||||
poemError.value = `请至少录入到当前第 ${LOCAL_CURRENT_GENERATION} 世`;
|
||||
return;
|
||||
}
|
||||
const preservedRows = stopMissingOldGeneration.value
|
||||
? []
|
||||
: poemRows.value.filter(
|
||||
(item) => item.generationNo < startGeneration.value,
|
||||
);
|
||||
const nextRows = characters.map((character, index) => {
|
||||
const generationNo = startGeneration.value + index;
|
||||
return {
|
||||
generationNo,
|
||||
character,
|
||||
current: generationNo === currentGeneration.value,
|
||||
};
|
||||
const nextRows = mergeGenerationPoemRows({
|
||||
existingRows: poemRows.value,
|
||||
generationTexts: validation.generations,
|
||||
startGeneration: LOCAL_PREVIEW_START_GENERATION,
|
||||
currentGeneration: LOCAL_CURRENT_GENERATION,
|
||||
disableMissing: disableMissing.value,
|
||||
});
|
||||
poemRows.value = [...preservedRows, ...nextRows];
|
||||
const activeRows = nextRows.filter(
|
||||
(item) => item.status === GENERATION_POEM_STATUS.ACTIVE,
|
||||
);
|
||||
const lastActiveGeneration = activeRows.length
|
||||
? activeRows[activeRows.length - 1].generationNo
|
||||
: null;
|
||||
const firstGap = lastActiveGeneration === null
|
||||
? null
|
||||
: findFirstGenerationGap(
|
||||
nextRows,
|
||||
lastActiveGeneration + 1,
|
||||
LOCAL_PREVIEW_START_GENERATION,
|
||||
);
|
||||
if (firstGap !== null) {
|
||||
poemError.value = `第 ${firstGap} 世字辈缺失;请补齐完整序列,或选择停用未覆盖的后续记录`;
|
||||
return;
|
||||
}
|
||||
poemRows.value = nextRows;
|
||||
visiblePoemCount.value = POEM_RENDER_BATCH_SIZE;
|
||||
editorSnapshot.value = null;
|
||||
poemState.value = "list";
|
||||
feedbackVisible.value = true;
|
||||
feedbackTimer = setTimeout(() => {
|
||||
if (feedbackTimer) clearTimeout(feedbackTimer);
|
||||
const timer = setTimeout(() => {
|
||||
if (feedbackTimer !== timer) return;
|
||||
feedbackVisible.value = false;
|
||||
feedbackTimer = null;
|
||||
}, 1800);
|
||||
feedbackTimer = timer;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -287,12 +494,26 @@ const savePoems = () => {
|
||||
.poem-rows {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.poem-load-more {
|
||||
display: flex;
|
||||
min-height: 64rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 14rpx;
|
||||
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
.poem-load-more text {
|
||||
color: $ink;
|
||||
font-size: 23rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.poem-row {
|
||||
@include adaptive.adaptive-genealogy-form-field;
|
||||
display: grid;
|
||||
min-height: 72rpx;
|
||||
margin-top: 10rpx;
|
||||
grid-template-columns: 48% auto 1fr auto;
|
||||
grid-template-columns: minmax(116rpx, 38%) minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
.poem-row__number {
|
||||
@@ -305,14 +526,19 @@ const savePoems = () => {
|
||||
.poem-row__character {
|
||||
z-index: 1;
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
padding: 12rpx 16rpx 12rpx 0;
|
||||
color: $ink;
|
||||
font-family: "STKaiti", "KaiTi", serif;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
.poem-row__status {
|
||||
z-index: 1;
|
||||
grid-column: 4;
|
||||
grid-column: 3;
|
||||
margin-right: 22rpx;
|
||||
color: $ink-muted;
|
||||
font-size: 22rpx;
|
||||
@@ -321,6 +547,11 @@ const savePoems = () => {
|
||||
.poem-row--current .poem-row__status {
|
||||
color: $brand-red;
|
||||
}
|
||||
.poem-row--disabled .poem-row__character,
|
||||
.poem-row--disabled .poem-row__status {
|
||||
color: $ink-muted;
|
||||
opacity: 0.62;
|
||||
}
|
||||
.poem-action {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user