Files
jiapuapp/pages/genealogy/my-genealogies.vue
T

1137 lines
29 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.
<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="retryGenealogyLoad"
>
<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 current-meta-item--location">
<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 current-meta-item--members">
<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 current-meta-item--role">
<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="shortcut in visibleShortcuts"
:key="shortcut.key"
class="shortcut-item"
@click="openShortcut(shortcut.key)"
>
<image class="shortcut-icon" :src="shortcut.icon" mode="aspectFit" />
<text class="shortcut-label">{{ shortcut.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="managedGenealogies.length" class="list-section">
<view class="section-heading"><text>我管理的</text></view>
<GenealogyCard
v-for="genealogy in managedGenealogies"
:key="genealogy.id"
:genealogy="genealogy"
role="管理员"
:selected="genealogy.id === currentGenealogy.id"
@select="openGenealogy"
/>
</view>
<view v-if="memberGenealogies.length" class="list-section">
<view class="section-heading"><text>我加入的</text></view>
<GenealogyCard
v-for="genealogy in memberGenealogies"
:key="genealogy.id"
:genealogy="genealogy"
role="成员"
:selected="genealogy.id === currentGenealogy.id"
@select="openGenealogy"
/>
</view>
<view
v-if="genealogies.length > 1"
class="genealogy-order-trigger"
role="button"
aria-label="调整我的家谱排序"
hover-class="action-hover"
@click="openOrderDialog"
>
<view>
<text class="genealogy-order-trigger__title">调整家谱排序</text>
<text class="genealogy-order-trigger__copy"
>按上移下移调整显示顺序</text
>
</view>
<image
class="genealogy-order-trigger__chevron"
src="/static/assets/foundation/transparent/chevron-right.png"
mode="aspectFit"
/>
</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>
<AppPromotionStrip placement="home_bottom" title="家谱服务推荐" />
</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-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>
<GenealogyAddDialog
:visible="addDialogVisible"
:creation-quota="creationQuota"
@close="closeAddDialog"
@search="applyToJoin"
@create="createGenealogy"
/>
<GenealogySwitcherDialog
:visible="switcherVisible"
:genealogies="genealogies"
:selected-genealogy-id="selectedGenealogyId"
@close="closeSwitcher"
@select="selectGenealogy"
/>
<GenealogyOrderDialog
ref="genealogyOrderDialog"
:visible="orderDialogVisible"
:genealogies="genealogies"
@close="closeOrderDialog"
@saved="applySavedGenealogyOrder"
/>
<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 AppPromotionStrip from "@/components/AppPromotionStrip.vue";
import GenealogyAddDialog from "@/components/genealogy/AddDialog.vue";
import GenealogyCard from "@/components/genealogy/Card.vue";
import GenealogyOrderDialog from "@/components/genealogy/OrderDialog.vue";
import GenealogyPageBackground from "@/components/genealogy/PageBackground.vue";
import GenealogySwitcherDialog from "@/components/genealogy/SwitcherDialog.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { genealogyApi } from "@/services/api/genealogy-service.js";
import { notificationApi } from "@/services/api/notification-service.js";
import { genealogyContext } from "@/utils/genealogy/context.js";
import {
consumeNavigationResult,
handleBackPress,
openPage,
runBackGuard,
} from "@/utils/navigation/gateway.js";
const isLoading = ref(false);
const hasError = ref(false);
const genealogies = ref([]);
const contextInvalidated = ref(false);
const contextReconcileFailed = ref(false);
const requestedGenealogyId = ref("");
const addDialogVisible = ref(false);
const switcherVisible = ref(false);
const orderDialogVisible = ref(false);
const genealogyOrderDialog = ref(null);
const selectedGenealogyId = ref(null);
const listScrollCommand = ref(0);
const currentListScrollTop = ref(0);
const unreadCount = ref(0);
const creationQuota = ref(null);
const genealogyListRequestController = createRequestController();
const unreadRequestController = createRequestController();
const quotaRequestController = createRequestController();
// uni-app 的 abort 与成功回调可能在同一事件循环竞争。控制器负责取消任务,
// generation 再阻止已经迟到的旧响应覆盖新页面状态,两层保护不能互相替代。
let genealogyLoadGeneration = 0;
let unreadLoadGeneration = 0;
let quotaLoadGeneration = 0;
let pageActive = true;
let skipNextShowRefresh = true;
const reconcilePageGenealogyContext = () => {
try {
const availableIds = genealogies.value.map((genealogy) =>
String(genealogy.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 = ++genealogyLoadGeneration;
genealogyListRequestController.abort();
isLoading.value = true;
hasError.value = false;
try {
const loadedGenealogies = await genealogyApi.getMyGenealogies({
requestController: genealogyListRequestController,
});
if (!pageActive || generation !== genealogyLoadGeneration) return;
genealogies.value = loadedGenealogies;
reconcilePageGenealogyContext();
} catch (error) {
if (
!pageActive ||
generation !== genealogyLoadGeneration ||
isRequestCancelled(error)
) {
return;
}
hasError.value = true;
} finally {
if (pageActive && generation === genealogyLoadGeneration) {
isLoading.value = false;
}
}
};
const loadUnreadCount = async () => {
const generation = ++unreadLoadGeneration;
unreadRequestController.abort();
try {
const count = await notificationApi.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 genealogyQuota = await genealogyApi.getGenealogyQuota({
requestController: quotaRequestController,
});
if (pageActive && generation === quotaLoadGeneration) {
creationQuota.value = genealogyQuota;
}
} catch (error) {
if (
!isRequestCancelled(error) &&
pageActive &&
generation === quotaLoadGeneration
)
creationQuota.value = null;
}
};
onLoad((query) => {
requestedGenealogyId.value = String(query?.genealogyId || "");
loadGenealogies();
loadUnreadCount();
loadQuota();
});
onShow(() => {
const navigationResult = consumeNavigationResult("G01");
if (
navigationResult?.operation === "genealogy-created" &&
navigationResult.refresh &&
navigationResult.entityId
) {
requestedGenealogyId.value = navigationResult.entityId;
loadGenealogies();
return;
}
if (skipNextShowRefresh) {
skipNextShowRefresh = false;
return;
}
loadGenealogies();
loadUnreadCount();
loadQuota();
});
onUnload(() => {
pageActive = false;
genealogyLoadGeneration += 1;
unreadLoadGeneration += 1;
quotaLoadGeneration += 1;
genealogyListRequestController.abort();
unreadRequestController.abort();
quotaRequestController.abort();
});
const hasGenealogies = computed(() => genealogies.value.length > 0);
const isListLayout = computed(
() =>
!isLoading.value &&
!hasError.value &&
!contextInvalidated.value &&
hasGenealogies.value,
);
const managedGenealogies = computed(() =>
genealogies.value.filter((genealogy) => genealogy.canManage),
);
const memberGenealogies = computed(() =>
genealogies.value.filter((genealogy) => !genealogy.canManage),
);
const currentGenealogy = computed(
() =>
genealogies.value.find(
(genealogy) => genealogy.id === selectedGenealogyId.value,
) || null,
);
const canManageCurrentGenealogy = computed(
() => currentGenealogy.value?.canManage === true,
);
const currentRoleLabel = computed(() =>
canManageCurrentGenealogy.value ? "管理员" : "成员",
);
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(() =>
canManageCurrentGenealogy.value
? shortcuts
: shortcuts.filter((shortcut) => shortcut.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 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 openOrderDialog = () => {
if (genealogies.value.length < 2) return;
orderDialogVisible.value = true;
};
const closeOrderDialog = () => {
orderDialogVisible.value = false;
};
const applySavedGenealogyOrder = async (confirmedGenealogies) => {
if (!pageActive) return;
genealogies.value = confirmedGenealogies;
reconcilePageGenealogyContext();
orderDialogVisible.value = false;
await resetListScroll();
};
const closeActiveOverlay = () => {
if (switcherVisible.value) closeSwitcher();
else if (orderDialogVisible.value) genealogyOrderDialog.value?.requestClose();
else closeAddDialog();
};
const requestBack = () =>
runBackGuard({
transientOpen:
switcherVisible.value || addDialogVisible.value || orderDialogVisible.value,
"close-transient": closeActiveOverlay,
});
onBackPress((event) => {
if (
!switcherVisible.value &&
!addDialogVisible.value &&
!orderDialogVisible.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 retryGenealogyLoad = () => {
loadGenealogies();
};
const openShortcut = (shortcutKey) => {
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[shortcutKey]?.();
};
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.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-genealogy-current-slip;
display: block;
min-height: 300rpx;
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: clamp(17px, 32rpx, 22px);
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: clamp(18px, 34rpx, 24px);
font-weight: 700;
line-height: 1.25;
overflow: hidden;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.current-switch-copy {
flex: 0 0 auto;
margin-left: 16rpx;
padding: 4rpx 12rpx;
border: 1rpx solid rgba(159, 23, 15, 0.34);
border-radius: 999rpx;
color: $brand-red;
font-size: clamp(15px, 25rpx, 18px);
font-weight: 600;
line-height: 1.3;
white-space: nowrap;
}
.current-info-divider {
width: 100%;
height: 1rpx;
margin: 18rpx 0 16rpx;
background: rgba(181, 138, 75, 0.48);
}
.current-meta {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: 28rpx;
row-gap: 10rpx;
margin-top: 0;
color: #62584c;
font-size: clamp(15px, 25rpx, 18px);
font-weight: 500;
line-height: 1.4;
}
.current-meta-item {
display: flex;
min-width: 0;
align-items: center;
margin: 0;
.current-meta-item-text {
min-width: 0;
overflow-wrap: anywhere;
}
}
.current-meta-item--location {
grid-column: 1 / -1;
}
.current-meta-item--role {
justify-self: end;
}
.current-meta-icon {
width: 34rpx;
height: 34rpx;
flex: 0 0 auto;
margin-right: 8rpx;
opacity: 1;
}
.shortcut-grid {
display: flex;
min-height: 168rpx;
align-items: center;
margin-top: 18rpx;
padding: 8rpx 0;
}
.shortcut-item {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: center;
}
.shortcut-icon {
width: 76rpx;
height: 76rpx;
}
.shortcut-label {
width: 100%;
margin-top: 10rpx;
color: $ink;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(16px, 27rpx, 19px);
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: clamp(19px, 34rpx, 24px);
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;
}
.genealogy-order-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
min-height: 112rpx;
margin: 20rpx 0 24rpx;
padding: 24rpx 28rpx;
border: 1rpx solid rgba(149, 103, 49, 0.24);
border-radius: 14rpx;
background: rgba(255, 250, 240, 0.72);
box-sizing: border-box;
}
.genealogy-order-trigger__title,
.genealogy-order-trigger__copy {
display: block;
}
.genealogy-order-trigger__title {
color: #5c4330;
font-size: clamp(16px, 28rpx, 20px);
font-weight: 700;
}
.genealogy-order-trigger__copy {
margin-top: 8rpx;
color: #8a7564;
font-size: clamp(14px, 23rpx, 17px);
}
.genealogy-order-trigger__chevron {
width: 26rpx;
height: 26rpx;
flex: 0 0 auto;
}
.create-action {
display: flex;
min-height: 96rpx;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: clamp(17px, 30rpx, 22px);
}
.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: clamp(17px, 32rpx, 22px);
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: clamp(20px, 38rpx, 26px);
font-weight: 700;
}
.state-copy {
margin-top: 14rpx;
color: $ink-muted;
font-size: clamp(15px, 25rpx, 18px);
line-height: 1.7;
}
.state-panel--error {
@include 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: clamp(22px, 42rpx, 28px);
font-weight: 700;
letter-spacing: 2rpx;
}
.state-panel--error .state-copy {
display: block;
min-height: 72rpx;
margin-top: 16rpx;
color: #786654;
font-size: clamp(16px, 27rpx, 20px);
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-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: clamp(17px, 32rpx, 22px);
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: clamp(24px, 48rpx, 30px);
font-weight: 700;
letter-spacing: 2rpx;
}
.empty-copy {
display: block;
min-height: 84rpx;
margin-top: 18rpx;
color: #786654;
font-size: clamp(16px, 28rpx, 20px);
line-height: 1.65;
}
.empty-divider {
display: block;
width: 100%;
height: 30rpx;
margin-top: 26rpx;
}
.empty-search-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/scroll-primary.png")
center / contain no-repeat;
}
.empty-search-action__copy {
color: #7b4e24;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(19px, 34rpx, 24px);
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: clamp(17px, 31rpx, 22px);
}
.empty-create-note {
display: block;
color: $ink-muted;
font-size: clamp(16px, 26rpx, 20px);
line-height: max(1.35em, clamp(20px, 38rpx, 26px));
}
.state-retry {
display: flex;
width: 360rpx;
min-height: 96rpx;
align-items: center;
justify-content: center;
margin-top: 30rpx;
background: url("/static/assets/foundation/transparent/scroll-primary.png")
center / contain no-repeat;
}
.state-retry__copy {
color: #fff9ec;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(16px, 29rpx, 20px);
}
.action-hover {
opacity: 0.82;
}
@media screen and (max-width: 340px) {
.genealogy-content {
padding-right: 20rpx;
padding-left: 20rpx;
}
.current-name {
font-size: clamp(20px, 40rpx, 26px);
}
.shortcut-label {
font-size: clamp(14px, 23rpx, 17px);
}
.current-meta-icon {
width: 32rpx;
height: 32rpx;
}
.create-action {
width: 320rpx;
}
.create-cloud {
width: 46rpx;
}
}
</style>