feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="state-title">暂时无法读取家谱</text>
|
||||
<text class="state-copy">网络或服务暂不可用,请稍后重新查看。</text>
|
||||
<text class="state-copy">{{ genealogyListError }}</text>
|
||||
<image
|
||||
class="error-panel__divider"
|
||||
src="/static/assets/modules/genealogy/transparent/section-divider.png"
|
||||
@@ -171,6 +171,64 @@
|
||||
/>
|
||||
</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">
|
||||
<image
|
||||
class="create-cloud"
|
||||
@@ -226,6 +284,12 @@
|
||||
>
|
||||
<text class="empty-create-action__copy">创建家谱</text>
|
||||
</view>
|
||||
<AppButton
|
||||
block
|
||||
type="secondary"
|
||||
label="观看宣传视频"
|
||||
@click="openPlatformVideos"
|
||||
/>
|
||||
<text class="empty-create-note"
|
||||
>确认没有现有家谱后再创建,避免重复建谱</text
|
||||
>
|
||||
@@ -279,7 +343,10 @@ import {
|
||||
createRequestController,
|
||||
isRequestCancelled
|
||||
} 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 {
|
||||
@@ -291,6 +358,7 @@ import {
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasError = ref(false);
|
||||
const genealogyListError = ref("");
|
||||
const genealogies = ref([]);
|
||||
const contextInvalidated = ref(false);
|
||||
const contextReconcileFailed = ref(false);
|
||||
@@ -304,14 +372,18 @@ 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;
|
||||
|
||||
@@ -340,6 +412,7 @@ const reconcilePageGenealogyContext = () => {
|
||||
selectedGenealogyId.value = null;
|
||||
contextReconcileFailed.value = true;
|
||||
contextInvalidated.value = false;
|
||||
genealogyListError.value = "本机家谱选择状态异常,请重新加载。";
|
||||
hasError.value = true;
|
||||
return false;
|
||||
}
|
||||
@@ -350,6 +423,7 @@ const loadGenealogies = async () => {
|
||||
genealogyListRequestController.abort();
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
genealogyListError.value = "";
|
||||
try {
|
||||
const loadedGenealogies = await genealogyApi.getMyGenealogies({
|
||||
requestController: genealogyListRequestController,
|
||||
@@ -365,6 +439,10 @@ const loadGenealogies = async () => {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
genealogyListError.value =
|
||||
error?.code === "GENEALOGY_RESPONSE_INVALID"
|
||||
? "服务返回的家谱数据不完整,请联系管理员处理。"
|
||||
: getRequestErrorMessage(error, "家谱服务暂时无法读取,请稍后重试。");
|
||||
hasError.value = true;
|
||||
} finally {
|
||||
if (pageActive && generation === genealogyLoadGeneration) {
|
||||
@@ -407,11 +485,37 @@ 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(() => {
|
||||
@@ -423,6 +527,7 @@ onShow(() => {
|
||||
) {
|
||||
requestedGenealogyId.value = navigationResult.entityId;
|
||||
loadGenealogies();
|
||||
loadFeaturedVideos();
|
||||
return;
|
||||
}
|
||||
if (skipNextShowRefresh) {
|
||||
@@ -432,6 +537,7 @@ onShow(() => {
|
||||
loadGenealogies();
|
||||
loadUnreadCount();
|
||||
loadQuota();
|
||||
loadFeaturedVideos();
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
@@ -439,9 +545,11 @@ onUnload(() => {
|
||||
genealogyLoadGeneration += 1;
|
||||
unreadLoadGeneration += 1;
|
||||
quotaLoadGeneration += 1;
|
||||
featuredVideoLoadGeneration += 1;
|
||||
genealogyListRequestController.abort();
|
||||
unreadRequestController.abort();
|
||||
quotaRequestController.abort();
|
||||
featuredVideoRequestController.abort();
|
||||
});
|
||||
|
||||
const hasGenealogies = computed(() => genealogies.value.length > 0);
|
||||
@@ -536,6 +644,21 @@ 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 openOrderDialog = () => {
|
||||
if (genealogies.value.length < 2) return;
|
||||
orderDialogVisible.value = true;
|
||||
@@ -596,7 +719,7 @@ const openShortcut = (shortcutKey) => {
|
||||
const genealogyId = String(currentGenealogy.value.id);
|
||||
const actions = {
|
||||
tree: () => openPage("T01", { genealogyId }, "G01"),
|
||||
members: () => openPage("G05", { genealogyId }, "G01"),
|
||||
members: () => openPage("G13", { genealogyId }, "G01"),
|
||||
poem: () => openPage("G12", { genealogyId }, "G01"),
|
||||
applications: () => openPage("G10", { genealogyId }, "G01"),
|
||||
};
|
||||
@@ -617,7 +740,7 @@ const openShortcut = (shortcutKey) => {
|
||||
|
||||
.genealogy-content {
|
||||
z-index: 1;
|
||||
padding: 24rpx 32rpx 176rpx;
|
||||
padding: 24rpx 32rpx calc(176rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.genealogy-index--split {
|
||||
@@ -855,6 +978,149 @@ 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;
|
||||
min-height: 96rpx;
|
||||
@@ -1076,7 +1342,7 @@ const openShortcut = (shortcutKey) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 76rpx;
|
||||
min-height: var(--app-touch-min);
|
||||
margin-top: 14rpx;
|
||||
color: $brand-red;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user