修改完成待测试

This commit is contained in:
2026-09-13 17:45:47 +08:00
parent 9ad572907b
commit bbc024f7f2
80 changed files with 3844 additions and 1672 deletions
+45 -539
View File
@@ -3,7 +3,7 @@
class="page-shell genealogy-index"
:class="{ 'genealogy-index--split': isListLayout }"
>
<GenealogyPageBackground />
<GenealogyPageBackground tone="light" />
<PageHeader
root
notice
@@ -56,69 +56,6 @@
</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
@@ -126,8 +63,13 @@
@scroll="handleListScroll"
>
<view class="genealogy-lower">
<view class="genealogy-list-toolbar">
<view class="section-heading genealogy-list-heading">
<text>{{ managedGenealogies.length ? "我管理的" : "我加入的" }}</text>
</view>
</view>
<view v-if="managedGenealogies.length" class="list-section">
<view class="section-heading"><text>我管理的</text></view>
<GenealogyCard
v-for="genealogy in managedGenealogies"
:key="genealogy.id"
@@ -139,7 +81,7 @@
</view>
<view v-if="memberGenealogies.length" class="list-section">
<view class="section-heading"><text>我加入的</text></view>
<view v-if="managedGenealogies.length" class="section-heading"><text>我加入的</text></view>
<GenealogyCard
v-for="genealogy in memberGenealogies"
:key="genealogy.id"
@@ -171,85 +113,33 @@
/>
</view>
<view class="featured-media-section">
<view class="featured-media-heading">
<view>
<text class="featured-media-heading__title">宣传视频</text>
<text class="featured-media-heading__copy">家谱文化与使用介绍</text>
</view>
<button class="featured-media-more" @click="openPlatformVideos">
查看更多
</button>
</view>
<view v-if="featuredVideoState === 'loading'" class="featured-media-state">
<AppLoading text="正在读取宣传视频" />
</view>
<view v-else-if="featuredVideoState === 'error'" class="featured-media-state">
<text>宣传视频暂时无法显示</text>
<button class="featured-media-retry" @click="loadFeaturedVideos">重新加载</button>
</view>
<view v-else-if="featuredVideos.length" class="featured-media-grid">
<view
v-for="video in featuredVideos"
:key="video.id"
class="featured-media-card"
>
<view
v-if="video.coverFile?.accessUrl"
class="featured-media-cover-button"
role="button"
:aria-label="`播放${video.title}`"
hover-class="action-hover"
@click="openFeaturedVideo(video)"
>
<image
class="featured-media-cover"
:src="video.coverFile.accessUrl"
mode="aspectFill"
/>
<view class="featured-media-play" aria-hidden="true"></view>
</view>
<video
v-else
class="featured-media-video"
:src="video.videoFile.accessUrl"
controls
object-fit="cover"
/>
<button class="featured-media-title" @click="openFeaturedVideo(video)">
{{ video.title }}
</button>
</view>
</view>
<view v-else class="featured-media-state featured-media-state--empty">
<text>暂时没有推荐视频</text>
<button class="featured-media-more featured-media-more--empty" @click="openPlatformVideos">
查看全部视频
</button>
</view>
</view>
<view class="create-action" @click="openAddDialog">
<view class="create-action" hover-class="action-hover" @click="openAddDialog">
<image
class="create-cloud"
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
mode="aspectFit"
aria-hidden="true"
/>
<image
class="create-icon"
src="/static/assets/modules/genealogy/transparent/add.png"
mode="aspectFit"
aria-hidden="true"
/>
<text>添加家谱</text>
<image
class="create-cloud create-cloud--right"
src="/static/assets/modules/genealogy/transparent/create-cloud.png"
mode="aspectFit"
aria-hidden="true"
/>
</view>
<AppPromotionStrip placement="home_bottom" title="家谱服务推荐" />
</view>
</scroll-view>
<HomeAdvertisementPanel
class="home-content-fixed"
@open="openContentHome"
/>
</template>
<view v-else class="genealogy-empty-state">
@@ -284,12 +174,6 @@
>
<text class="empty-create-action__copy">创建家谱</text>
</view>
<AppButton
block
type="secondary"
label="观看宣传视频"
@click="openPlatformVideos"
/>
<text class="empty-create-note"
>确认没有现有家谱后再创建避免重复建谱</text
>
@@ -322,7 +206,7 @@
@saved="applySavedGenealogyOrder"
/>
<AppTabbar active="genealogy" />
<AppTabbar active="genealogy" tone="light" />
</view>
</template>
@@ -332,9 +216,9 @@ 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 HomeAdvertisementPanel from "@/components/genealogy/HomeAdvertisementPanel.vue";
import GenealogyOrderDialog from "@/components/genealogy/OrderDialog.vue";
import GenealogyPageBackground from "@/components/genealogy/PageBackground.vue";
import GenealogySwitcherDialog from "@/components/genealogy/SwitcherDialog.vue";
@@ -345,12 +229,11 @@ import {
} from "@/services/api/request-controller.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { genealogyApi } from "@/services/api/genealogy-service.js";
import { PLATFORM_VIDEO_PLACEMENT } from "@/services/api/family-media-contract.js";
import { genealogyCapabilityApi } from "@/services/api/genealogy-capability-service.js";
import { notificationApi } from "@/services/api/notification-service.js";
import { genealogyContext } from "@/utils/genealogy/context.js";
import {
consumeNavigationResult,
goRoot,
handleBackPress,
openPage,
runBackGuard,
@@ -372,18 +255,14 @@ const listScrollCommand = ref(0);
const currentListScrollTop = ref(0);
const unreadCount = ref(0);
const creationQuota = ref(null);
const featuredVideos = ref([]);
const featuredVideoState = ref("loading");
const genealogyListRequestController = createRequestController();
const unreadRequestController = createRequestController();
const quotaRequestController = createRequestController();
const featuredVideoRequestController = createRequestController();
// uni-app 的 abort 与成功回调可能在同一事件循环竞争。控制器负责取消任务,
// generation 再阻止已经迟到的旧响应覆盖新页面状态,两层保护不能互相替代。
let genealogyLoadGeneration = 0;
let unreadLoadGeneration = 0;
let quotaLoadGeneration = 0;
let featuredVideoLoadGeneration = 0;
let pageActive = true;
let skipNextShowRefresh = true;
@@ -485,37 +364,11 @@ const loadQuota = async () => {
}
};
const loadFeaturedVideos = async () => {
const generation = ++featuredVideoLoadGeneration;
featuredVideoRequestController.abort();
featuredVideoState.value = "loading";
try {
const rows = await genealogyCapabilityApi.getPlatformVideos(
PLATFORM_VIDEO_PLACEMENT.HOME_FEATURED,
{ requestController: featuredVideoRequestController },
);
if (!pageActive || generation !== featuredVideoLoadGeneration) return;
featuredVideos.value = rows.slice(0, 2);
featuredVideoState.value = "ready";
} catch (error) {
if (
!pageActive ||
generation !== featuredVideoLoadGeneration ||
isRequestCancelled(error)
) {
return;
}
featuredVideos.value = [];
featuredVideoState.value = "error";
}
};
onLoad((query) => {
requestedGenealogyId.value = String(query?.genealogyId || "");
loadGenealogies();
loadUnreadCount();
loadQuota();
loadFeaturedVideos();
});
onShow(() => {
@@ -527,7 +380,6 @@ onShow(() => {
) {
requestedGenealogyId.value = navigationResult.entityId;
loadGenealogies();
loadFeaturedVideos();
return;
}
if (skipNextShowRefresh) {
@@ -537,7 +389,6 @@ onShow(() => {
loadGenealogies();
loadUnreadCount();
loadQuota();
loadFeaturedVideos();
});
onUnload(() => {
@@ -545,11 +396,9 @@ onUnload(() => {
genealogyLoadGeneration += 1;
unreadLoadGeneration += 1;
quotaLoadGeneration += 1;
featuredVideoLoadGeneration += 1;
genealogyListRequestController.abort();
unreadRequestController.abort();
quotaRequestController.abort();
featuredVideoRequestController.abort();
});
const hasGenealogies = computed(() => genealogies.value.length > 0);
@@ -572,40 +421,6 @@ const currentGenealogy = computed(
(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(
@@ -644,21 +459,7 @@ const openSwitcher = () => {
const closeSwitcher = () => {
switcherVisible.value = false;
};
const openPlatformVideos = () =>
openPage(
"F11",
{ placement: PLATFORM_VIDEO_PLACEMENT.VIDEO_CENTER },
"G01",
);
const openFeaturedVideo = (video) =>
openPage(
"F11",
{
placement: PLATFORM_VIDEO_PLACEMENT.HOME_FEATURED,
videoId: String(video.id),
},
"G01",
);
const openContentHome = () => goRoot("F01");
const openOrderDialog = () => {
if (genealogies.value.length < 2) return;
orderDialogVisible.value = true;
@@ -714,17 +515,6 @@ 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("G13", { genealogyId }, "G01"),
poem: () => openPage("G12", { genealogyId }, "G01"),
applications: () => openPage("G10", { genealogyId }, "G01"),
};
return actions[shortcutKey]?.();
};
</script>
<style scoped lang="scss">
@@ -735,12 +525,12 @@ const openShortcut = (shortcutKey) => {
min-height: 100vh;
flex-direction: column;
overflow: hidden;
background: #f9f6ef;
background: #faf8f3;
}
.genealogy-content {
z-index: 1;
padding: 24rpx 32rpx calc(176rpx + env(safe-area-inset-bottom));
padding: 0 32rpx calc(176rpx + env(safe-area-inset-bottom));
}
.genealogy-index--split {
@@ -760,10 +550,6 @@ const openShortcut = (shortcutKey) => {
padding-bottom: 0;
}
.genealogy-fixed-zone {
flex: 0 0 auto;
}
.genealogy-list-scroll {
width: 100%;
height: 0;
@@ -771,151 +557,25 @@ const openShortcut = (shortcutKey) => {
flex: 1;
}
.current-slip {
@include adaptive-genealogy-current-slip;
display: block;
min-height: 300rpx;
padding: 30rpx 34rpx 28rpx;
.genealogy-list-toolbar {
display: flex;
align-items: center;
min-height: 70rpx;
padding: 0 4rpx 2rpx;
box-sizing: border-box;
}
.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;
.genealogy-list-heading {
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;
margin-bottom: 0;
}
.genealogy-lower {
padding: 12rpx 0 36rpx;
padding: 0 0 24rpx;
}
.list-section {
margin-top: 16rpx;
margin-top: 6rpx;
}
.section-heading {
@@ -951,7 +611,7 @@ const openShortcut = (shortcutKey) => {
padding: 24rpx 28rpx;
border: 1rpx solid rgba(149, 103, 49, 0.24);
border-radius: 14rpx;
background: rgba(255, 250, 240, 0.72);
background: rgba(255, 253, 249, 0.84);
box-sizing: border-box;
}
@@ -978,164 +638,17 @@ const openShortcut = (shortcutKey) => {
flex: 0 0 auto;
}
.featured-media-section {
margin: 0 0 24rpx;
padding: 26rpx 24rpx;
border: 1rpx solid rgba(149, 103, 49, 0.24);
border-radius: 14rpx;
background: rgba(255, 250, 240, 0.72);
}
.featured-media-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.featured-media-heading__title,
.featured-media-heading__copy {
display: block;
}
.featured-media-heading__title {
color: #5c4330;
font-size: clamp(16px, 28rpx, 20px);
font-weight: 700;
}
.featured-media-heading__copy {
margin-top: 8rpx;
color: #8a7564;
font-size: clamp(14px, 23rpx, 17px);
}
.featured-media-more,
.featured-media-retry,
.featured-media-title {
min-height: var(--app-touch-min);
margin: 0;
padding: 0;
border: 0;
background: transparent;
line-height: 1.4;
}
.featured-media-more::after,
.featured-media-retry::after,
.featured-media-title::after {
border: 0;
}
.featured-media-more,
.featured-media-retry {
flex: 0 0 auto;
color: $brand-red;
font-size: clamp(13px, 22rpx, 16px);
}
.featured-media-state {
display: flex;
min-height: 176rpx;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
color: $ink-muted;
font-size: clamp(14px, 23rpx, 17px);
text-align: center;
}
.featured-media-state--empty {
min-height: 138rpx;
}
.featured-media-more--empty {
margin-top: 4rpx;
}
.featured-media-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
margin-top: 22rpx;
}
.featured-media-card {
min-width: 0;
}
.featured-media-cover-button,
.featured-media-video {
width: 100%;
height: 176rpx;
border-radius: 10rpx;
background: #1f1b17;
overflow: hidden;
}
.featured-media-cover-button {
position: relative;
}
.featured-media-cover {
width: 100%;
height: 100%;
}
.featured-media-play {
position: absolute;
top: 50%;
left: 50%;
display: flex;
width: 58rpx;
height: 58rpx;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(255, 255, 255, 0.9);
border-radius: 50%;
background: rgba(45, 29, 19, 0.62);
transform: translate(-50%, -50%);
}
.featured-media-play::after {
width: 0;
height: 0;
margin-left: 5rpx;
border-top: 10rpx solid transparent;
border-bottom: 10rpx solid transparent;
border-left: 16rpx solid #fff;
content: "";
}
.featured-media-title {
display: block;
width: 100%;
margin-top: 8rpx;
color: $ink;
font-size: clamp(14px, 24rpx, 17px);
font-weight: 600;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.create-action {
display: flex;
width: 350rpx;
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;
margin: 8rpx auto 24rpx;
border: 2rpx solid $brand-red;
border-radius: 48rpx;
background: rgba(255, 249, 238, 0.66);
background: rgba(255, 253, 249, 0.8);
box-sizing: border-box;
color: $brand-red;
font-family: "STKaiti", "KaiTi", serif;
font-size: clamp(17px, 32rpx, 22px);
@@ -1144,28 +657,31 @@ const openShortcut = (shortcutKey) => {
}
.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);
}
.home-content-fixed {
flex: 0 0 auto;
margin-top: 8rpx;
}
.state-panel {
display: flex;
min-height: 540rpx;
@@ -1382,16 +898,6 @@ const openShortcut = (shortcutKey) => {
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;
}