Files
jiapuapp/pages/family/feed-detail.vue
T
2026-09-13 17:45:52 +08:00

419 lines
12 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="feed-detail-page" :class="`feed-state--${feedState}`">
<ModulePageBackground module="family" />
<view class="feed-detail-header"
><PageHeader title="动态详情" custom-back @back="requestBack"
/></view>
<view class="feed-detail-content">
<AppLoading
v-if="feedState === 'loading'"
text="正在读取动态"
description="请稍候,正在同步家族动态与评论。"
/>
<view v-else-if="feedState === 'ready'" class="feed-detail-body">
<view class="feed-card">
<view class="feed-card__heading">
<text>{{ feedTypeLabel(feed.type) }}</text>
<text>{{ formatMinuteTimestamp(feed.time) || "未标注时间" }}</text>
</view>
<text class="feed-card__content">{{ feed.content }}</text>
<FamilyFeedMedia :files="feed.mediaFiles" />
<view class="feed-card__meta">
<text>发布{{ feed.publisher }}</text>
<text
>{{ feed.likeCount }} 次点赞 ·
{{ feed.commentCount }} 条评论</text
>
</view>
<view class="feed-card__actions">
<AppButton
compact
type="secondary"
:disabled="isTogglingLike || !hasLikeState"
:label="
isTogglingLike
? '正在提交'
: hasLikeState
? feed.likedByMe
? '取消点赞'
: '点赞'
: '点赞不可用'
"
@click="toggleLike"
/>
<AppButton
v-if="feed.canEdit"
compact
type="secondary"
label="编辑动态"
@click="openEditFeed"
/>
<AppButton
v-if="feed.canDelete"
compact
type="secondary"
label="删除动态"
@click="requestDeleteFeed"
/>
</view>
<text v-if="!hasLikeState" class="like-note"
>暂时无法确认是否已点赞请稍后再试</text
>
<text v-if="likeError" class="like-error">{{ likeError }}</text>
<text v-if="deleteError" class="delete-error">{{ deleteError }}</text>
</view>
<FeedCommentSection
:genealogy-id="genealogyId"
:feed-id="feedId"
:refresh-feed-summary="refreshFeedSummary"
/>
</view>
<view v-else class="feed-state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton
type="secondary"
block
:label="stateCopy.action"
@click="handleStateAction"
/>
</view>
</view>
<AppDialog
:visible="deleteConfirmationVisible"
:close-on-mask="false"
eyebrow="删除确认"
title="将这条动态移至回收站?"
message="移入回收站后不再展示,家谱管理员可在保留期内恢复。"
confirm-text="移至回收站"
cancel-text="保留动态"
show-cancel
@confirm="deleteFeed"
@cancel="closeDeleteConfirmation"
/>
</view>
</template>
<script setup>
import { computed, ref } from "vue";
import { onBackPress, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import FeedCommentSection from "@/components/family/FeedCommentSection.vue";
import FamilyFeedMedia from "@/components/family/FeedMedia.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { familyFeedApi } from "@/services/api/family-feed-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { formatMinuteTimestamp } from "@/utils/display-time.js";
import { goBack, handleBackPress, openPage, returnTo } from "@/utils/navigation/gateway.js";
const genealogyId = ref("");
const feedId = ref("");
const feedState = ref("loading");
const feed = ref(null);
const isTogglingLike = ref(false);
const likeError = ref("");
const deleteConfirmationVisible = ref(false);
const deleting = ref(false);
const deleteError = ref("");
const feedReadController = createRequestController();
const feedLikeRequestController = createRequestController();
const feedDeletionRequestController = createRequestController();
let pageActive = true;
let skipInitialShowRefresh = true;
const feedTypeLabel = (type) =>
String(type || "")
.trim()
.toLowerCase() === "text"
? "文字动态"
: "家族动态";
const hasValidContext = computed(
() => /^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(feedId.value),
);
const hasLikeState = computed(() => typeof feed.value?.likedByMe === "boolean");
const stateCopy = computed(() => {
if (!hasValidContext.value) {
return {
title: "暂时无法打开动态",
copy: "未找到家谱或动态信息,请返回后重新进入。",
action: "返回上一页",
};
}
if (feedState.value === "missing") {
return {
title: "该动态已不存在",
copy: "它可能已被发布者删除,或你暂时无法查看。",
action: "返回家族动态",
};
}
return {
title: "动态暂时无法读取",
copy: "请检查网络后重新读取。",
action: "重新读取",
};
});
const loadFeed = async ({ preserveContent = false } = {}) => {
if (!hasValidContext.value) {
feedState.value = "invalid";
return false;
}
if (!preserveContent) feedState.value = "loading";
try {
const current = await familyFeedApi.getFeedDetail(
genealogyId.value,
feedId.value,
{ requestController: feedReadController },
);
if (!pageActive) return;
feed.value = current;
feedState.value = "ready";
return true;
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return null;
if (!preserveContent) {
feedState.value =
error?.code === "HTTP_ERROR" && error?.httpStatus === 404
? "missing"
: "error";
}
return false;
}
};
const refresh = () => loadFeed();
const refreshFeedSummary = () => loadFeed({ preserveContent: true });
const toggleLike = async () => {
if (
isTogglingLike.value ||
!hasValidContext.value ||
!feed.value ||
!hasLikeState.value
)
return;
const nextLiked = !feed.value.likedByMe;
isTogglingLike.value = true;
likeError.value = "";
try {
await familyFeedApi.setFeedLike(genealogyId.value, feedId.value, nextLiked, {
requestController: feedLikeRequestController,
});
if (!pageActive) return;
const feedRefreshed = await refreshFeedSummary();
if (feedRefreshed === false) {
likeError.value = "点赞已提交,最新状态暂时无法读取。";
}
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
likeError.value = getRequestErrorMessage(error, "点赞操作失败,请稍后重试");
} finally {
if (pageActive) isTogglingLike.value = false;
}
};
const requestDeleteFeed = () => {
if (!feed.value?.canDelete || deleting.value) return;
deleteError.value = "";
deleteConfirmationVisible.value = true;
};
const closeDeleteConfirmation = () => {
if (!deleting.value) deleteConfirmationVisible.value = false;
};
const openEditFeed = () => {
if (!feed.value?.canEdit || !hasValidContext.value) return;
openPage(
"F02",
{ genealogyId: genealogyId.value, mode: "edit", feedId: feedId.value },
"F03",
);
};
const deleteFeed = async () => {
if (!feed.value?.canDelete || deleting.value) return;
deleting.value = true;
deleteError.value = "";
let deletionCommitted = false;
try {
await familyFeedApi.deleteFeed(genealogyId.value, feedId.value, {
requestController: feedDeletionRequestController,
});
deletionCommitted = true;
if (!pageActive) return;
deleteConfirmationVisible.value = false;
feed.value = null;
feedState.value = "missing";
await backToFamily();
} catch (error) {
if (!pageActive) return;
if (deletionCommitted) {
deleteConfirmationVisible.value = false;
feed.value = null;
feedState.value = "missing";
return;
}
if (isRequestCancelled(error)) return;
deleteError.value = getRequestErrorMessage(error, "动态删除失败,请稍后重试。");
deleteConfirmationVisible.value = false;
} finally {
if (pageActive) deleting.value = false;
}
};
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
feedId.value = String(query?.feedId || "");
void refresh();
});
onShow(() => {
if (skipInitialShowRefresh) {
skipInitialShowRefresh = false;
return;
}
if (hasValidContext.value) void refresh();
});
onUnload(() => {
pageActive = false;
feedReadController.abort();
feedLikeRequestController.abort();
feedDeletionRequestController.abort();
});
const backToFamily = () =>
hasValidContext.value
? returnTo("F12", { genealogyId: genealogyId.value })
: goBack();
const requestBack = () => backToFamily();
const handleStateAction = () =>
feedState.value === "error" ? refresh() : backToFamily();
onBackPress((event) => handleBackPress(event, requestBack));
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.feed-detail-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.feed-detail-header,
.feed-detail-content {
z-index: 1;
}
.feed-detail-content {
flex: 1;
padding: 18rpx 24rpx calc(72rpx + env(safe-area-inset-bottom));
}
.feed-detail-body {
margin-top: 18rpx;
}
.feed-card,
.feed-state-card {
@include adaptive-family-content;
box-sizing: border-box;
background-color: rgba($paper, 0.82);
}
.feed-card {
padding: 30rpx;
}
.feed-card__heading,
.feed-card__meta {
display: flex;
justify-content: space-between;
gap: 18rpx;
}
.feed-card__heading text:first-child {
color: $brand-red;
font-size: clamp(14px, 23rpx, 17px);
font-weight: 700;
}
.feed-card__heading text:last-child {
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
text-align: right;
}
.feed-card__content {
display: block;
margin-top: 20rpx;
color: $ink;
font-size: clamp(16px, 29rpx, 20px);
line-height: 1.7;
overflow-wrap: anywhere;
white-space: pre-wrap;
}
.feed-card__meta {
margin-top: 22rpx;
color: $ink-muted;
font-size: clamp(13px, 21rpx, 16px);
}
.feed-card .app-button {
margin-top: 18rpx;
}
.feed-card__actions {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.feed-card__actions .app-button {
width: auto;
min-width: 180rpx;
flex: 1 1 180rpx;
margin-top: 18rpx;
}
.like-note,
.like-error {
display: block;
margin-top: 10rpx;
font-size: clamp(14px, 22rpx, 17px);
}
.like-note {
color: $ink-muted;
}
.like-error {
color: $brand-red;
}
.delete-error {
display: block;
margin-top: 10rpx;
color: $brand-red;
font-size: clamp(14px, 22rpx, 17px);
text-align: right;
}
.feed-state-card {
min-height: 340rpx;
margin-top: 36rpx;
padding: 78rpx 52rpx 50rpx;
text-align: center;
}
.feed-state-card > text {
display: block;
}
.feed-state-card > text:first-child {
color: $ink;
font-family: STKaiti, KaiTi, serif;
font-size: clamp(19px, 35rpx, 24px);
font-weight: 700;
}
.feed-state-card > text:nth-child(2) {
margin-top: 18rpx;
color: $ink-muted;
font-size: clamp(15px, 24rpx, 18px);
line-height: 1.65;
}
.feed-state-card .app-button {
margin-top: 30rpx;
}
</style>