Files
jiapuapp/pages/family/f03-feed-detail.vue
T
2026-07-27 06:50:23 +08:00

261 lines
11 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.
<!-- 页面编号F-03用途家族动态详情与一级评论 -->
<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>{{ feed.time || '未标注时间' }}</text>
</view>
<text class="feed-card__content">{{ feed.content }}</text>
<view class="feed-card__meta">
<text>发布{{ feed.publisher }}</text>
<text>{{ feed.likeCount }} 次点赞 · {{ feed.commentCount }} 条评论</text>
</view>
<AppButton
compact
type="secondary"
:disabled="isTogglingLike || !hasLikeState"
:label="isTogglingLike ? '正在提交' : (hasLikeState ? (feed.likedByMe ? '取消点赞' : '点赞') : '点赞状态不可用')"
@click="toggleLike"
/>
<text v-if="!hasLikeState" class="like-note">点赞状态暂不可用页面不会猜测下一次操作</text>
<text v-if="likeError" class="like-error">{{ likeError }}</text>
</view>
<view class="comment-section">
<text class="section-title">评论</text>
<AppLoading v-if="commentState === 'loading'" text="正在读取评论" />
<view v-else-if="commentState === 'list'" class="comment-list">
<view v-for="item in comments" :key="item.id" class="comment-card">
<view class="comment-card__heading">
<text>{{ item.author }}</text>
<text>{{ item.time || '刚刚' }}</text>
</view>
<text class="comment-card__content">{{ item.content }}</text>
</view>
</view>
<text v-else class="comment-state-copy">{{ commentStateCopy }}</text>
<view class="comment-editor">
<textarea
v-model="commentDraft"
auto-height
maxlength="1000"
placeholder="写下你的评论"
placeholder-class="comment-editor__placeholder"
@input="commentError = ''"
/>
<text v-if="commentError" class="comment-error">{{ commentError }}</text>
<AppButton
block
:disabled="isSubmittingComment"
:label="isSubmittingComment ? '正在提交' : '发表评论'"
@click="submitComment"
/>
</view>
</view>
</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>
</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 AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import { goBack, handleBackPress, returnTo } from "@/utils/navigation.js";
const genealogyId = ref("");
const feedId = ref("");
const feedState = ref("loading");
const commentState = ref("loading");
const feed = ref(null);
const comments = ref([]);
const commentDraft = ref("");
const commentError = ref("");
const isSubmittingComment = ref(false);
const isTogglingLike = ref(false);
const likeError = ref("");
const controller = createRequestController();
let pageActive = 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 commentStateCopy = computed(() => {
if (commentState.value === "empty") return "还没有评论,欢迎留下第一句话。";
return "评论暂时无法读取,稍后可重新进入本页查看。";
});
const loadFeed = async () => {
if (!hasValidContext.value) {
feedState.value = "invalid";
return;
}
feedState.value = "loading";
try {
const current = await appApi.getFeedDetail(genealogyId.value, feedId.value, { requestController: controller });
if (!pageActive) return;
feed.value = current;
feedState.value = "ready";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
feedState.value = error?.code === "HTTP_ERROR" && error?.httpStatus === 404 ? "missing" : "error";
}
};
const loadComments = async () => {
if (!hasValidContext.value || feedState.value !== "ready") return;
commentState.value = "loading";
try {
const rows = await appApi.getFeedComments(genealogyId.value, feedId.value, { requestController: controller });
if (!pageActive) return;
comments.value = rows;
commentState.value = rows.length ? "list" : "empty";
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
commentState.value = "error";
}
};
const refresh = async () => {
await loadFeed();
if (feedState.value === "ready") await loadComments();
};
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 appApi.setFeedLike(genealogyId.value, feedId.value, nextLiked, { requestController: controller });
if (!pageActive) return;
await loadFeed();
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
likeError.value = error?.message || "点赞操作失败,请稍后重试";
} finally {
if (pageActive) isTogglingLike.value = false;
}
};
const submitComment = async () => {
if (isSubmittingComment.value || !hasValidContext.value) return;
const commentContent = commentDraft.value.trim();
if (!commentContent) {
commentError.value = "请填写评论内容";
return;
}
isSubmittingComment.value = true;
commentError.value = "";
try {
await appApi.createFeedComment(genealogyId.value, feedId.value, { commentContent }, { requestController: controller });
if (!pageActive) return;
commentDraft.value = "";
await refresh();
} catch (error) {
if (!pageActive || isRequestCancelled(error)) return;
commentError.value = error?.message || "评论提交失败,请稍后重试";
} finally {
if (pageActive) isSubmittingComment.value = false;
}
};
onLoad((query) => {
genealogyId.value = String(query?.genealogyId || "");
feedId.value = String(query?.feedId || "");
void refresh();
});
onShow(() => {
if (hasValidContext.value) void refresh();
});
onUnload(() => {
pageActive = false;
controller.abort();
});
const backToFamily = () => hasValidContext.value
? returnTo("F01", { genealogyId: genealogyId.value })
: goBack();
const requestBack = () => backToFamily();
const handleStateAction = () => feedState.value === "error" ? refresh() : backToFamily();
onBackPress((event) => handleBackPress(event, requestBack));
</script>
<style scoped lang="scss">
@use "../../styles/adaptive-frame-profiles.scss" as adaptive;
.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 72rpx; }
.feed-detail-body { margin-top: 18rpx; }
.feed-card, .comment-section, .feed-state-card { @include adaptive.adaptive-family-content; box-sizing: border-box; }
.feed-card { padding: 30rpx; }
.feed-card__heading, .feed-card__meta, .comment-card__heading { display: flex; justify-content: space-between; gap: 18rpx; }
.feed-card__heading text:first-child, .comment-card__heading text:first-child { color: $brand-red; font-size: 23rpx; font-weight: 700; }
.feed-card__heading text:last-child, .comment-card__heading text:last-child { color: $ink-muted; font-size: 21rpx; text-align: right; }
.feed-card__content { display: block; margin-top: 20rpx; color: $ink; font-size: 29rpx; line-height: 1.7; white-space: pre-wrap; }
.feed-card__meta { margin-top: 22rpx; color: $ink-muted; font-size: 21rpx; }
.feed-card .app-button { margin-top: 18rpx; }
.like-note, .like-error { display: block; margin-top: 10rpx; font-size: 22rpx; }
.like-note { color: $ink-muted; }
.like-error { color: $brand-red; }
.comment-section { margin-top: 18rpx; padding: 28rpx; }
.section-title { display: block; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; }
.comment-list { margin-top: 18rpx; }
.comment-card { padding: 18rpx 0; border-bottom: 1rpx solid rgba(128, 89, 49, .16); }
.comment-card__content { display: block; margin-top: 10rpx; color: $ink; font-size: 25rpx; line-height: 1.55; white-space: pre-wrap; }
.comment-state-copy { display: block; margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.5; }
.comment-editor { margin-top: 24rpx; padding-top: 22rpx; border-top: 1rpx solid rgba(128, 89, 49, .18); }
.comment-editor textarea { display: block; box-sizing: border-box; width: 100%; min-height: 130rpx; padding: 18rpx; border: 1rpx solid rgba(128, 89, 49, .3); border-radius: 12rpx; color: $ink; font-size: 25rpx; line-height: 1.55; }
.comment-editor__placeholder { color: #ab9a86; }
.comment-editor .app-button { margin-top: 18rpx; }
.comment-error { display: block; margin-top: 10rpx; color: $brand-red; font-size: 22rpx; }
.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: 35rpx; font-weight: 700; }
.feed-state-card > text:nth-child(2) { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.65; }
.feed-state-card .app-button { margin-top: 30rpx; }
</style>