Files
jiapuapp/pages/family/f03-feed-detail.vue
T
2026-07-24 07:56:33 +08:00

234 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用途动态详情与一级评论动态正文 DTO 未声明时不展示伪字段 -->
<template>
<view class="feed-detail-page" :class="{ 'feed-state--expired': feedState === 'expired', 'feed-state--error': feedState === 'error', 'feed-state--ready': feedState === 'ready' }">
<ModulePageBackground module="family" />
<view class="feed-detail-header"><PageHeader title="动态详情" custom-back @back="requestBack" /></view>
<view v-if="feedState === 'loading'" class="feed-detail-loading">
<AppLoading text="正在读取家人评论" description="动态正文的展示字段尚未由后端合同声明。" />
</view>
<view v-else class="feed-detail-content">
<template v-if="feedState === 'ready'">
<view class="feed-source-note"><text>动态详情接口尚未声明标题正文发布人和发布时间字段本页不猜测这些字段只展示该动态的真实一级评论</text></view>
<view class="feed-article-card">
<text class="feed-article-card__title">动态正文待后端字段合同</text>
<text class="feed-article-card__body">已根据当前动态 ID 读取评论正文内容会在详情响应提供可消费 DTO 后再接入</text>
</view>
<view class="feed-comments-panel">
<view class="feed-comments-heading">
<text>家人评论</text><text>{{ feedComments.length }} </text>
</view>
<view v-for="comment in feedComments" :key="comment.id" class="feed-comment-card">
<view><text>{{ comment.author }}</text><text>{{ comment.time }}</text></view>
<text>{{ comment.content }}</text>
<text v-if="comment.replyCount > 0" class="feed-comment-card__replies">直属回复 {{ comment.replyCount }} </text>
</view>
<view v-if="!feedComments.length" class="feed-comments-empty">
<text>还没有一级评论</text><text>服务端返回评论后会显示在这里</text>
</view>
</view>
<view class="feed-comment-form" :class="{ 'comment-state--submitting': commentState === 'submitting', 'comment-state--error': commentState === 'error' }">
<text>提交一级评论</text>
<textarea v-model="commentDraft" auto-height maxlength="1000" placeholder="对家人说点什么" />
<text v-if="commentError" class="feed-comment-error">{{ commentError }}</text>
<text v-else-if="commentNotice" class="feed-comment-notice">{{ commentNotice }}</text>
<AppButton block :disabled="commentState === 'submitting'" :label="commentState === 'submitting' ? '正在提交' : '提交评论'" @click="submitComment" />
</view>
</template>
<view v-else class="feed-state-card">
<text>{{ stateCopy.title }}</text>
<text>{{ stateCopy.copy }}</text>
<AppButton :type="feedState === 'error' ? 'secondary' : 'primary'" block :label="stateCopy.action" @click="handleStateAction" />
</view>
</view>
<AppDialog
:visible="discardVisible"
title="放弃评论草稿?"
message="当前评论尚未提交服务端,确认返回后不会保留。"
confirm-text="放弃并返回"
cancel-text="继续填写"
show-cancel
:close-on-mask="false"
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</view>
</template>
<script setup>
import { computed, onUnmounted, ref } from "vue";
import { onBackPress, onLoad } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppLoading from "@/components/AppLoading.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { appApi, createRequestController, isRequestCancelled } from "@/utils/api.js";
import {
goBack,
handleBackPress,
returnTo,
runBackGuard,
} from "@/utils/navigation.js";
const genealogyId = ref("");
const feedId = ref("");
const feedState = ref("loading");
const commentState = ref("idle");
const commentDraft = ref("");
const commentError = ref("");
const commentNotice = ref("");
const discardVisible = ref(false);
const feedComments = ref([]);
const feedRequestController = createRequestController();
let loadSequence = 0;
const isDirty = computed(() => Boolean(commentDraft.value.trim()));
const stateCopy = computed(() => {
if (feedState.value === "error") {
return {
title: "评论暂不可用",
copy: "评论读取没有得到可消费的服务端响应;页面不会用本地数据替代。",
action: "重新读取",
};
}
return {
title: "动态入口无效",
copy: "没有取得当前家谱和动态标识,页面不会回退到其他动态。",
action: genealogyId.value ? "返回家族动态" : "返回上一页",
};
});
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const loadComments = async () => {
if (!genealogyId.value || !feedId.value) {
feedState.value = "expired";
return false;
}
const sequence = ++loadSequence;
feedState.value = "loading";
try {
const comments = await appApi.getFeedComments(genealogyId.value, feedId.value, {
requestController: feedRequestController,
});
if (sequence !== loadSequence) return false;
feedComments.value = comments;
feedState.value = "ready";
return true;
} catch (error) {
if (isRequestCancelled(error) || sequence !== loadSequence) return false;
feedState.value = "error";
return false;
}
};
onLoad((query) => {
genealogyId.value = String(query.genealogyId || "");
feedId.value = String(query.feedId || "");
loadComments();
});
const submitComment = async () => {
if (commentState.value === "submitting" || feedState.value !== "ready") return;
const content = commentDraft.value.trim();
if (!content) {
commentError.value = "请先写下评论内容";
commentNotice.value = "";
return;
}
commentError.value = "";
commentNotice.value = "";
commentState.value = "submitting";
try {
await appApi.createFeedComment(genealogyId.value, feedId.value, { commentContent: content }, {
requestController: feedRequestController,
});
commentDraft.value = "";
const refreshed = await loadComments();
if (!refreshed) {
commentState.value = "error";
commentError.value = "评论已提交,但列表刷新失败;请稍后重新查看。";
return;
}
commentState.value = "idle";
commentNotice.value = "评论已提交,并已从服务端刷新。";
} catch (error) {
if (isRequestCancelled(error)) return;
commentState.value = "error";
commentError.value = error?.message || "评论提交失败,请稍后重试。";
}
};
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: commentState.value === "submitting",
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
const backToFamily = async () => {
if (!genealogyId.value) return goBack();
const confirmed = isDirty.value ? await requestDiscardConfirmation() : true;
if (!confirmed) return false;
return returnTo("F01", { genealogyId: genealogyId.value });
};
const handleStateAction = () => {
if (feedState.value === "error") return loadComments();
return backToFamily();
};
onBackPress((event) => handleBackPress(event, requestBack));
onUnmounted(() => {
++loadSequence;
feedRequestController.abort();
discardConfirmation.dispose();
});
</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-loading, .feed-detail-content { z-index: 1; }
.feed-detail-loading { min-height: calc(100vh - 100rpx); }
.feed-detail-content { padding: 18rpx 24rpx 72rpx; }
.feed-source-note { margin-top: 12rpx; padding: 14rpx 18rpx; border: 1rpx solid rgba(128, 106, 81, .22); border-radius: 10rpx; background: rgba(255,255,255,.56); }
.feed-source-note text { display: block; color: $ink-muted; font-size: 20rpx; line-height: 1.5; }
.feed-article-card, .feed-comments-panel, .feed-comment-form, .feed-state-card { @include adaptive.adaptive-family-content; width: 100%; }
.feed-article-card { padding: 42rpx 48rpx; }
.feed-article-card text, .feed-state-card > text { display: block; }
.feed-article-card__title { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; }
.feed-article-card__body { margin-top: 18rpx; color: $ink-muted; font-size: 24rpx; line-height: 1.7; }
.feed-comments-panel { margin-top: 18rpx; padding: 38rpx 38rpx 34rpx; }
.feed-comments-heading { display: flex; align-items: center; justify-content: space-between; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.feed-comments-heading text:last-child { color: $ink-muted; font-size: 21rpx; font-weight: 400; }
.feed-comment-card { @include adaptive.adaptive-family-field; margin-top: 16rpx; padding: 20rpx 22rpx; }
.feed-comment-card > view { display: flex; justify-content: space-between; gap: 18rpx; color: $ink-muted; font-size: 20rpx; }
.feed-comment-card > text { display: block; margin-top: 9rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
.feed-comment-card > .feed-comment-card__replies { color: $ink-muted; font-size: 20rpx; }
.feed-comments-empty { padding: 34rpx 10rpx 20rpx; text-align: center; }
.feed-comments-empty text { display: block; color: $ink-muted; font-size: 23rpx; line-height: 1.6; }
.feed-comments-empty text:first-child { color: $ink; font-size: 28rpx; font-weight: 700; }
.feed-comment-form { margin-top: 18rpx; padding: 38rpx 40rpx 42rpx; }
.feed-comment-form > text:first-child { color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 30rpx; font-weight: 700; }
.feed-comment-form textarea { @include adaptive.adaptive-family-field; width: auto; min-width: 0; min-height: 126rpx; margin-top: 16rpx; padding: 20rpx 22rpx; color: $ink; font-size: 24rpx; line-height: 1.55; }
.feed-comment-error, .feed-comment-notice { display: block; margin-top: 10rpx; font-size: 22rpx; }
.feed-comment-error { color: $brand-red; }
.feed-comment-notice { color: $ink-muted; }
.feed-comment-form .app-button { margin-top: 22rpx; }
.feed-state-card { min-height: 340rpx; margin-top: 36rpx; padding: 78rpx 52rpx 50rpx; text-align: center; }
.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>