Files
jiapuapp/pages/genealogy/g01-my-genealogies.vue
T
2026-07-27 06:50:23 +08:00

1408 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 页面编号G-01用途我的家谱列表与首次分流本轮仍待用户重新审核 -->
<template>
<view
class="page-shell genealogy-index"
:class="{ 'genealogy-index--split': isListLayout }"
>
<GenealogyPageBackground />
<PageHeader
root
notice
title="我的家谱"
:unread-count="unreadCount"
@notice="toNotifications"
/>
<view class="genealogy-content">
<view v-if="isLoading" class="state-panel state-panel--loading">
<AppLoading
text="正在整理家谱"
description="请稍候,家族记忆正在归卷。"
/>
</view>
<view v-else-if="hasError" class="state-panel state-panel--error">
<view class="error-panel__content">
<image
class="error-panel__seal"
src="/static/assets/foundation/transparent/brand-seal.png"
mode="aspectFit"
/>
<text class="state-title">暂时无法读取家谱</text>
<text class="state-copy">网络或服务暂不可用请稍后重新查看</text>
<image
class="error-panel__divider"
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
<view
class="state-retry"
hover-class="action-hover"
@click="retryLoad"
>
<text class="state-retry__copy">重新加载</text>
</view>
</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">
<view class="current-summary">
<view class="current-seal">
<text class="current-seal-title">家谱</text>
</view>
<text class="current-name">{{ currentGenealogy.name }}</text>
<text class="current-switch-copy">切换</text>
</view>
<view class="current-info-divider"></view>
<view class="current-meta">
<view class="current-meta-item">
<image
class="current-meta-icon"
src="/static/assets/foundation/transparent/meta-location.png"
mode="aspectFit"
/>
<text class="current-meta-item-text">{{
currentGenealogy.location
}}</text>
</view>
<view class="current-meta-item">
<image
class="current-meta-icon"
src="/static/assets/foundation/transparent/meta-member.png"
mode="aspectFit"
/>
<text class="current-meta-item-text"
>{{ currentGenealogy.memberCount }} 位成员</text
>
</view>
<view class="current-meta-item">
<image
class="current-meta-icon"
src="/static/assets/foundation/transparent/meta-admin.png"
mode="aspectFit"
/>
<text class="current-meta-item-text">{{ currentRoleLabel }}</text>
</view>
</view>
</view>
<view class="shortcut-grid">
<view
v-for="item in visibleShortcuts"
:key="item.key"
class="shortcut-item"
@click="openShortcut(item.key)"
>
<image class="shortcut-icon" :src="item.icon" mode="aspectFit" />
<text class="shortcut-label">{{ item.label }}</text>
</view>
</view>
<image
class="section-divider"
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
</view>
<scroll-view
class="genealogy-list-scroll"
scroll-y
:scroll-top="listScrollCommand"
@scroll="handleListScroll"
>
<view class="genealogy-lower">
<view v-if="createdGenealogies.length" class="list-section">
<view class="section-heading"><text>我管理的</text></view>
<GenealogyCard
v-for="item in createdGenealogies"
:key="item.id"
:genealogy="item"
role="管理员"
:selected="item.id === currentGenealogy.id"
@select="openGenealogy"
/>
</view>
<view v-if="joinedGenealogies.length" class="list-section">
<view class="section-heading"><text>我加入的</text></view>
<GenealogyCard
v-for="item in joinedGenealogies"
:key="item.id"
:genealogy="item"
role="成员"
:selected="item.id === currentGenealogy.id"
@select="openGenealogy"
/>
</view>
<view
v-if="applicationRecords.length"
class="list-section application-section"
>
<view class="section-heading"><text>加入申请</text></view>
<view
v-for="item in applicationRecords"
:key="item.id"
class="application-record"
hover-class="action-hover"
@click="openApplication(item)"
>
<view class="application-record__main">
<view class="application-record__title-row">
<text class="application-record__name">{{
item.name
}}</text>
<text
class="application-record__status"
:class="`application-record__status--${item.tone}`"
>{{ item.statusLabel }}</text
>
</view>
<text class="application-record__copy">{{
item.description
}}</text>
</view>
<image
class="application-record__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</view>
</view>
<view class="create-action" @click="openAddDialog">
<image
class="create-cloud"
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
mode="aspectFit"
/>
<image
class="create-icon"
src="/static/assets/modules/genealogy/transparent/add.png"
mode="aspectFit"
/>
<text>添加家谱</text>
<image
class="create-cloud create-cloud--right"
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
mode="aspectFit"
/>
</view>
</view>
</scroll-view>
</template>
<view v-else class="genealogy-empty-state">
<view class="empty-panel">
<view class="empty-panel__content">
<view class="empty-seal">
<text class="empty-seal__text">家谱</text>
</view>
<text class="empty-title">还没有加入任何家谱</text>
<text class="empty-copy"
>先搜索家族是否已有家谱再选择加入或创建</text
>
<image
class="empty-divider"
src="/static/assets/modules/genealogy/transparent/section-divider.png"
mode="scaleToFill"
/>
<view
class="empty-search-action"
hover-class="action-hover"
@click="applyToJoin"
>
<text
class="empty-search-action__copy empty-search-action__copy--primary"
>搜索家谱</text
>
</view>
<view
class="empty-invite-action"
hover-class="action-hover"
@click="joinByInvite"
>
<text class="empty-invite-action__copy">邀请码加入</text>
</view>
<view
class="empty-create-action"
hover-class="action-hover"
@click="createGenealogy"
>
<text class="empty-create-action__copy">创建家谱</text>
</view>
<text class="empty-create-note"
>确认没有现有家谱后再创建避免重复建谱</text
>
</view>
</view>
</view>
</view>
<view
v-if="addDialogVisible"
class="add-dialog-layer"
@click="closeAddDialog"
>
<view class="add-dialog" @click.stop>
<view class="add-dialog__content">
<view class="add-dialog__body">
<view class="add-dialog__heading">
<text class="dialog-title">添加家谱</text>
<text class="dialog-copy">建议先搜索已有家谱避免重复创建</text>
<text v-if="quota" class="dialog-copy">{{ quota.createRemaining === -1 ? '当前可继续创建家谱' : `还可创建 ${quota.createRemaining} 部家谱` }}</text>
<view
class="add-dialog__close"
role="button"
aria-label="关闭"
hover-class="action-hover"
@click="closeAddDialog"
>
<image
class="add-dialog__close-icon"
src="/static/assets/modules/genealogy/transparent/g01-dialog-close.png"
mode="aspectFit"
/>
</view>
</view>
<view class="add-dialog__actions">
<AppButton block label="搜索家谱" @click="applyToJoin" />
<AppButton
block
type="secondary"
label="邀请码加入"
@click="joinByInvite"
/>
<AppButton
block
type="secondary"
label="继续创建家谱"
:disabled="quota?.canCreate === false"
@click="createGenealogy"
/>
</view>
</view>
</view>
</view>
</view>
<view
v-if="switcherVisible"
class="genealogy-switcher-layer"
@click="closeSwitcher"
>
<view
class="genealogy-switcher"
role="dialog"
aria-modal="true"
aria-label="切换当前家谱"
@click.stop
>
<view class="genealogy-switcher__content">
<text class="dialog-title">切换当前家谱</text>
<view
class="genealogy-switcher__close"
role="button"
aria-label="关闭"
hover-class="action-hover"
@click="closeSwitcher"
>
<image
class="genealogy-switcher__close-icon"
src="/static/assets/modules/genealogy/transparent/g01-dialog-close.png"
mode="aspectFit"
/>
</view>
<scroll-view class="genealogy-switcher__list" scroll-y>
<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>
<text class="switcher-item__name">{{ item.name }}</text>
<text class="switcher-item__meta"
>{{ item.location }} · {{ item.memberCount }} 位成员</text
>
</view>
<text class="switcher-item__state">{{
item.id === selectedGenealogyId ? "当前" : "选择"
}}</text>
</button>
</scroll-view>
</view>
</view>
</view>
<AppTabbar active="genealogy" />
</view>
</template>
<script setup>
import { computed, nextTick, ref } from "vue";
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppLoading from "@/components/AppLoading.vue";
import AppTabbar from "@/components/AppTabbar.vue";
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 {
appApi,
createRequestController,
isRequestCancelled,
} from "@/utils/api.js";
import { genealogyContext } from "@/utils/genealogy-context.js";
import { consumeNavigationResult, handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";
const isLoading = ref(false);
const hasError = ref(false);
const list = ref([]);
const forceEmptyState = ref(false);
const contextInvalidated = ref(false);
const contextReconcileFailed = ref(false);
const requestedGenealogyId = ref("");
const addDialogVisible = ref(false);
const switcherVisible = ref(false);
const selectedGenealogyId = ref(null);
const listScrollCommand = ref(0);
const currentListScrollTop = ref(0);
const unreadCount = ref(0);
const quota = ref(null);
const listRequestController = createRequestController();
const unreadRequestController = createRequestController();
const quotaRequestController = createRequestController();
let loadGeneration = 0;
let unreadLoadGeneration = 0;
let quotaLoadGeneration = 0;
let pageActive = true;
let presentationState = "default";
let skipNextShowRefresh = true;
const syncEmptyStateFromRoute = (query = {}) => {
presentationState = ["empty", "loading", "error"].includes(
query?.state,
)
? query.state
: "default";
forceEmptyState.value = presentationState === "empty";
isLoading.value = presentationState === "loading";
hasError.value = presentationState === "error";
};
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;
}
};
const loadGenealogies = async () => {
const generation = ++loadGeneration;
listRequestController.abort();
isLoading.value = true;
hasError.value = false;
try {
const result = await appApi.getMyGenealogies({
requestController: listRequestController,
});
if (!pageActive || generation !== loadGeneration) return;
list.value = result;
reconcilePageGenealogyContext();
} catch (error) {
if (
!pageActive ||
generation !== loadGeneration ||
isRequestCancelled(error)
) {
return;
}
hasError.value = true;
} finally {
if (pageActive && generation === loadGeneration) {
isLoading.value = false;
}
}
};
const loadUnreadCount = async () => {
const generation = ++unreadLoadGeneration;
unreadRequestController.abort();
try {
const count = await appApi.getUnreadNotificationCount({
requestController: unreadRequestController,
});
if (pageActive && generation === unreadLoadGeneration) unreadCount.value = count;
} catch (error) {
if (!isRequestCancelled(error)) unreadCount.value = 0;
}
};
const loadQuota = async () => {
const generation = ++quotaLoadGeneration;
quotaRequestController.abort();
try {
const result = await appApi.getGenealogyQuota({
requestController: quotaRequestController,
});
if (pageActive && generation === quotaLoadGeneration) quota.value = result;
} catch (error) {
if (!isRequestCancelled(error) && pageActive && generation === quotaLoadGeneration) quota.value = null;
}
};
onLoad((query) => {
syncEmptyStateFromRoute(query);
requestedGenealogyId.value = String(query?.genealogyId || "");
if (presentationState === "default") {
loadGenealogies();
loadUnreadCount();
loadQuota();
} else {
reconcilePageGenealogyContext();
}
});
onShow(() => {
const result = consumeNavigationResult("G01");
if (
result?.operation === "genealogy-created" &&
result.refresh &&
result.entityId
) {
requestedGenealogyId.value = result.entityId;
loadGenealogies();
return;
}
if (skipNextShowRefresh) {
skipNextShowRefresh = false;
return;
}
if (presentationState === "default") loadGenealogies();
if (presentationState === "default") loadUnreadCount();
if (presentationState === "default") loadQuota();
});
onUnload(() => {
pageActive = false;
loadGeneration += 1;
unreadLoadGeneration += 1;
quotaLoadGeneration += 1;
listRequestController.abort();
unreadRequestController.abort();
quotaRequestController.abort();
});
const hasGenealogies = computed(
() => !forceEmptyState.value && list.value.length > 0,
);
const isListLayout = computed(
() =>
!isLoading.value &&
!hasError.value &&
!contextInvalidated.value &&
hasGenealogies.value,
);
const createdGenealogies = computed(() =>
list.value.filter((item) => item.accessRole === "owner"),
);
const joinedGenealogies = computed(() =>
list.value.filter((item) => item.accessRole === "member"),
);
const availableGenealogies = computed(() => list.value);
const currentGenealogy = computed(
() =>
availableGenealogies.value.find(
(item) => item.id === selectedGenealogyId.value,
) || null,
);
const isCurrentGenealogyOwner = computed(
() => currentGenealogy.value?.accessRole === "owner",
);
const currentRoleLabel = computed(() =>
isCurrentGenealogyOwner.value ? "管理员" : "成员",
);
// 普通加入申请批次接通前不展示本地伪记录。
const applicationRecords = ref([]);
const shortcuts = [
{
key: "tree",
label: "世系图",
icon: "/static/assets/modules/genealogy/transparent/shortcut-tree.png",
},
{
key: "members",
label: "成员",
icon: "/static/assets/modules/genealogy/transparent/shortcut-members.png",
},
{
key: "poem",
label: "字辈诗",
icon: "/static/assets/modules/genealogy/transparent/shortcut-generation-poem.png",
},
{
key: "applications",
label: "申请审核",
icon: "/static/assets/modules/genealogy/transparent/shortcut-application.png",
},
];
const visibleShortcuts = computed(() =>
isCurrentGenealogyOwner.value
? shortcuts
: shortcuts.filter((item) => item.key !== "applications"),
);
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 openAddDialog = () => {
addDialogVisible.value = true;
};
const closeAddDialog = () => {
addDialogVisible.value = 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);
};
const resetListScroll = async () => {
listScrollCommand.value = currentListScrollTop.value;
await nextTick();
listScrollCommand.value = 0;
currentListScrollTop.value = 0;
};
const selectGenealogy = async (genealogy) => {
selectedGenealogyId.value = String(genealogy.id);
genealogyContext.setCurrentGenealogyId(String(genealogy.id));
contextInvalidated.value = false;
closeSwitcher();
await resetListScroll();
};
const openApplication = (record) => {
if (record.id === "pending")
return openPage("G09", { status: "pending" }, "G01");
return openPage(
"G08",
{
genealogyId: String(record.genealogyId),
source: "search",
},
"G01",
);
};
const retryLoad = () => {
loadGenealogies();
};
const openShortcut = (key) => {
if (!currentGenealogy.value) return;
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"),
};
return actions[key]?.();
};
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.genealogy-index {
display: flex;
min-height: 100vh;
flex-direction: column;
overflow: hidden;
background: #f9f6ef;
}
.genealogy-content {
z-index: 1;
padding: 24rpx 32rpx 176rpx;
}
.genealogy-index--split {
display: flex;
height: 100vh;
min-height: 0;
flex-direction: column;
padding-bottom: calc(112rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.genealogy-index--split .genealogy-content {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
padding-bottom: 0;
}
.genealogy-fixed-zone {
flex: 0 0 auto;
}
.genealogy-list-scroll {
width: 100%;
height: 0;
min-height: 0;
flex: 1;
}
.current-slip {
@include adaptive.adaptive-genealogy-current-slip;
display: block;
min-height: 268rpx;
padding: 30rpx 34rpx 28rpx;
}
.current-seal {
display: flex;
width: 80rpx;
height: 128rpx;
flex: 0 0 auto;
align-items: center;
justify-content: center;
margin-right: 24rpx;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat;
color: #fff7e7;
}
.current-seal-title {
color: #fff7e7;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
letter-spacing: 2rpx;
writing-mode: vertical-rl;
}
.current-summary {
display: flex;
min-width: 0;
align-items: center;
}
.current-name {
min-width: 0;
flex: 1;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 38rpx;
font-weight: 700;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.current-switch-copy {
flex: 0 0 auto;
margin-left: auto;
color: $brand-red;
font-size: 28rpx;
}
.current-info-divider {
width: 100%;
height: 1rpx;
margin: 14rpx 0 16rpx;
background: rgba(181, 138, 75, 0.48);
}
.current-meta {
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
margin-top: 0;
color: #62584c;
font-size: 27rpx;
font-weight: 500;
line-height: 1.4;
}
.current-meta-item {
display: flex;
align-items: center;
margin: 0;
flex-wrap: wrap;
.current-meta-item-text {
overflow-wrap: anywhere;
}
}
.current-meta-icon {
width: 36rpx;
height: 36rpx;
margin-right: 8rpx;
opacity: 1;
}
.shortcut-grid {
display: flex;
min-height: 194rpx;
align-items: center;
margin-top: 24rpx;
padding: 8rpx 0;
}
.shortcut-item {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: center;
}
.shortcut-icon {
width: 82rpx;
height: 82rpx;
}
.shortcut-label {
width: 100%;
margin-top: 12rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
font-weight: 700;
text-align: center;
overflow-wrap: anywhere;
}
.section-divider {
display: block;
width: 100%;
height: 30rpx;
margin: 4rpx 0 0;
}
.genealogy-lower {
padding: 12rpx 0 36rpx;
}
.list-section {
margin-top: 16rpx;
}
.section-heading {
display: flex;
min-height: 56rpx;
align-items: center;
margin-bottom: 8rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
}
.section-heading::before {
width: 8rpx;
height: 34rpx;
margin-right: 14rpx;
border-radius: 8rpx;
background: $brand-red;
content: "";
}
.list-section + .list-section {
margin-top: 18rpx;
}
.application-record {
@include adaptive.adaptive-genealogy-list-card;
display: flex;
min-height: 138rpx;
align-items: center;
padding: 22rpx 26rpx;
}
.application-record + .application-record {
margin-top: 12rpx;
}
.application-record__main {
min-width: 0;
flex: 1;
}
.application-record__title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.application-record__name {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
overflow-wrap: anywhere;
}
.application-record__status {
flex: 0 0 auto;
margin-left: 16rpx;
color: #7f4f16;
font-size: 27rpx;
font-weight: 600;
}
.application-record__status--rejected {
color: #a7160c;
}
.application-record__status--muted {
color: #62584c;
}
.application-record__copy {
display: block;
margin-top: 10rpx;
color: #62584c;
font-size: 28rpx;
font-weight: 500;
line-height: 1.4;
}
.application-record__chevron {
width: 32rpx;
height: 32rpx;
margin-left: 14rpx;
}
.create-action {
display: flex;
min-height: 96rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 30rpx;
}
.create-action {
width: 350rpx;
margin: 32rpx auto 0;
border: 2rpx solid $brand-red;
border-radius: 48rpx;
background: rgba(255, 249, 238, 0.66);
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
letter-spacing: 3rpx;
}
.create-icon {
width: 38rpx;
height: 38rpx;
margin-right: 12rpx;
}
.create-action .create-icon {
width: 40rpx;
height: 40rpx;
margin: 0 8rpx;
}
.create-cloud {
width: 54rpx;
height: 28rpx;
flex: 0 0 auto;
}
.create-action > text {
flex: 0 0 auto;
text-align: center;
}
.create-cloud--right {
transform: scaleX(-1);
}
.state-panel {
display: flex;
min-height: 540rpx;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 42rpx;
box-sizing: border-box;
text-align: center;
}
.state-title {
margin-top: 24rpx;
color: $ink;
font-family: serif;
font-size: 38rpx;
font-weight: 700;
}
.state-copy {
margin-top: 14rpx;
color: $ink-muted;
font-size: 25rpx;
line-height: 1.7;
}
.state-panel--error {
@include adaptive.adaptive-genealogy-state-panel;
min-height: 1120rpx;
padding: 0;
}
.state-panel--context > .app-button {
margin-top: 30rpx;
}
.error-panel__content {
display: flex;
width: 100%;
min-height: 1120rpx;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 50rpx 28rpx 46rpx;
}
.error-panel__seal {
width: 132rpx;
height: 136rpx;
}
.state-panel--error .state-title {
display: block;
margin-top: 30rpx;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 42rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.state-panel--error .state-copy {
display: block;
min-height: 72rpx;
margin-top: 16rpx;
color: #786654;
font-size: 27rpx;
line-height: 1.65;
}
.error-panel__divider {
display: block;
width: 100%;
height: 30rpx;
margin-top: 28rpx;
}
.state-panel--error .state-retry {
width: 560rpx;
min-height: 124rpx;
margin-top: 22rpx;
}
.genealogy-empty-state {
min-height: 1120rpx;
}
.empty-panel {
@include adaptive.adaptive-genealogy-state-panel;
min-height: 1120rpx;
}
.empty-panel__content {
display: flex;
min-height: 1120rpx;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 50rpx 28rpx 46rpx;
text-align: center;
}
.empty-seal {
display: flex;
width: 120rpx;
height: 184rpx;
align-items: center;
justify-content: center;
background: url("/static/assets/modules/genealogy/transparent/current-seal-frame.png") center / contain no-repeat;
}
.empty-seal__text {
color: #fff7e7;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
letter-spacing: 2rpx;
writing-mode: vertical-rl;
}
.empty-title {
display: block;
margin-top: 30rpx;
color: #392719;
font-family: "STKaiti", "KaiTi", serif;
font-size: 48rpx;
font-weight: 700;
letter-spacing: 2rpx;
}
.empty-copy {
display: block;
min-height: 84rpx;
margin-top: 18rpx;
color: #786654;
font-size: 28rpx;
line-height: 1.65;
}
.empty-divider {
display: block;
width: 100%;
height: 30rpx;
margin-top: 26rpx;
}
.empty-search-action,
.empty-invite-action {
display: flex;
width: 560rpx;
min-height: 124rpx;
align-items: center;
justify-content: center;
}
.empty-search-action {
margin-top: 26rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat;
}
.empty-invite-action {
margin-top: 22rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-secondary-v3.png") center / contain no-repeat;
}
.empty-search-action__copy,
.empty-invite-action__copy {
color: #7b4e24;
font-family: "STKaiti", "KaiTi", serif;
font-size: 34rpx;
font-weight: 700;
letter-spacing: 3rpx;
}
.empty-search-action__copy--primary {
color: #fff9ec;
}
.empty-create-action {
display: flex;
align-items: center;
justify-content: center;
min-height: 76rpx;
margin-top: 14rpx;
color: $brand-red;
}
.empty-create-action__copy {
font-family: "STKaiti", "KaiTi", serif;
font-size: 31rpx;
}
.empty-create-note {
display: block;
color: $ink-muted;
font-size: 26rpx;
line-height: 38rpx;
}
.state-retry {
display: flex;
width: 360rpx;
min-height: 96rpx;
align-items: center;
justify-content: center;
margin-top: 30rpx;
background: url("/static/assets/foundation/transparent/a01-scroll-primary-v3.png") center / contain no-repeat;
}
.state-retry__copy {
color: #fff9ec;
font-family: "STKaiti", "KaiTi", serif;
font-size: 29rpx;
}
.add-dialog-layer {
position: fixed;
z-index: 40;
inset: 0;
display: flex;
align-items: flex-end;
justify-content: center;
box-sizing: border-box;
background: rgba(34, 20, 12, 0.68);
}
.add-dialog {
@include adaptive.adaptive-g01-add-sheet;
width: 100%;
min-height: 780rpx;
max-height: calc(100vh - 80rpx);
}
.add-dialog__content {
display: flex;
min-height: 780rpx;
max-height: calc(100vh - 80rpx);
flex-direction: column;
align-items: stretch;
box-sizing: border-box;
padding: 96rpx 52rpx calc(96rpx + env(safe-area-inset-bottom));
overflow-y: auto;
}
.add-dialog__body {
margin: auto 0;
}
.add-dialog__heading {
display: grid;
grid-template-columns: minmax(0, 1fr) 96rpx;
}
.add-dialog .dialog-title,
.add-dialog .dialog-copy {
display: block;
grid-column: 1;
text-align: left;
}
.add-dialog__close {
display: flex;
grid-column: 2;
grid-row: 1 / span 2;
align-self: start;
justify-self: end;
width: 80rpx;
height: 80rpx;
align-items: center;
justify-content: center;
margin-right: -22rpx;
}
.add-dialog__close-icon {
width: 80rpx;
height: 80rpx;
}
.add-dialog__actions {
display: flex;
flex-direction: column;
align-items: center;
margin: 62rpx -32rpx 0;
}
.add-dialog__actions > .app-button {
width: 595rpx;
max-width: 100%;
min-height: 96rpx;
}
.add-dialog__actions > .app-button + .app-button {
margin-top: 24rpx;
}
.genealogy-switcher-layer {
position: fixed;
z-index: 40;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 40rpx;
background: rgba(34, 20, 12, 0.58);
}
.genealogy-switcher {
@include adaptive.adaptive-g01-switcher;
width: 670rpx;
max-width: 100%;
min-height: 600rpx;
max-height: calc(100vh - 120rpx);
}
.genealogy-switcher__content {
display: grid;
min-height: 600rpx;
max-height: calc(100vh - 120rpx);
grid-template-rows: auto minmax(0, 1fr);
align-items: center;
box-sizing: border-box;
padding: 120rpx 58rpx 140rpx;
}
.genealogy-switcher__content > .dialog-title {
grid-area: 1 / 1;
}
.genealogy-switcher__close {
display: flex;
grid-area: 1 / 1;
align-self: start;
justify-self: end;
width: 80rpx;
height: 80rpx;
align-items: center;
justify-content: center;
margin-top: -42rpx;
margin-right: -24rpx;
}
.genealogy-switcher__close-icon {
width: 80rpx;
height: 80rpx;
}
.genealogy-switcher__list {
grid-area: 2 / 1;
width: 100%;
min-height: 0;
max-height: calc(100vh - 456rpx);
margin-top: 24rpx;
overflow-y: auto;
}
.dialog-title {
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-size: 42rpx;
font-weight: 700;
letter-spacing: 4rpx;
}
.dialog-copy {
margin-top: 12rpx;
color: $ink-muted;
font-size: 25rpx;
line-height: 1.5;
}
.switcher-item {
display: flex;
width: 100%;
min-height: 112rpx;
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 {
display: block;
}
.switcher-item__name {
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: 32rpx;
font-weight: 700;
}
.switcher-item__meta {
margin-top: 6rpx;
color: #62584c;
font-size: 24rpx;
}
.switcher-item__state {
color: $brand-red;
font-size: 25rpx;
font-weight: 600;
}
.switcher-item--active {
border-color: rgba(159, 23, 15, 0.22);
background: rgba(159, 23, 15, 0.055);
}
.switcher-item--active .switcher-item__name {
color: $brand-red;
}
.action-hover {
opacity: 0.82;
}
@media screen and (max-width: 340px) {
.genealogy-content {
padding-right: 20rpx;
padding-left: 20rpx;
}
.current-name {
font-size: 40rpx;
}
.shortcut-label {
font-size: 23rpx;
}
.current-meta-icon {
width: 32rpx;
height: 32rpx;
}
.create-action {
width: 320rpx;
}
.create-cloud {
width: 46rpx;
}
}
</style>