feat: 实现骨架屏的渲染

This commit is contained in:
2026-06-05 16:31:03 +08:00
parent c59342c22c
commit c5d3bf5dbb
4 changed files with 262 additions and 3 deletions

View File

@@ -19,8 +19,11 @@
</view>
<template v-for="section in contentSections" :key="section.contentKey">
<ParsedValueView
v-if="shouldUseParsedValueView(section)"
<LongAnswerSectionSkeleton
v-if="shouldRenderSectionSkeleton(section)"
/>
<ParsedValueView v-else-if="shouldUseParsedValueView(section)"
:field-key="section.contentKey"
:value="section.parsedValue !== null ? section.parsedValue : section.contentValue"
/>
@@ -41,6 +44,7 @@
<script setup>
import TopNavBar from "@/components/TopNavBar/index.vue";
import ChatMarkdown from "../ChatMarkdown/index.vue";
import LongAnswerSectionSkeleton from "./LongAnswerSectionSkeleton.vue";
import ParsedValueView from "./ParsedValueView.vue";
import { defineProps, ref, nextTick, computed } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
@@ -62,6 +66,7 @@ const props = defineProps({
const answerText = ref(props.answerText || "");
const title = ref("");
const longTextData = ref(null);
const streamFinished = ref(false);
const longAnswerTagColor = ref(pickRandomTagToneColor());
const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.value));
@@ -110,6 +115,47 @@ const contentSections = computed(() => {
);
});
const getSectionIndex = (section) => {
return contentSections.value.findIndex((item) => item.contentKey === section?.contentKey);
};
const hasSectionAfter = (section) => {
const sectionIndex = getSectionIndex(section);
return sectionIndex >= 0 && sectionIndex < contentSections.value.length - 1;
};
const isNonStringSectionValue = (value) => {
return value !== undefined && value !== null && typeof value !== "string";
};
const isPendingStructuredText = (value) => {
const text = typeof value === "string" ? value.trim() : "";
return text.startsWith("{") || text.startsWith("[");
};
const isStructuredSection = (section) => {
return (
section?.fromLongTextData &&
(
(section.parsedValue !== null && isNonStringSectionValue(section.parsedValue)) ||
isPendingStructuredText(section.contentValue)
)
);
};
const isSectionReady = (section) => {
if (!isStructuredSection(section)) return true;
return (
streamFinished.value ||
(section.parsedValue !== null && isNonStringSectionValue(section.parsedValue)) ||
hasSectionAfter(section)
);
};
const shouldRenderSectionSkeleton = (section) => {
return isStructuredSection(section) && !isSectionReady(section);
};
/** ✅ scroll-into-view 控制 */
const scrollIntoViewId = ref("");
@@ -208,6 +254,7 @@ const scrollToBottom = () => {
onLoad(({ message = "", streamId = "", finished = "0", tagToneColor = "" }) => {
// 记录初始完成状态
isFinishedOnInit = finished === "1";
streamFinished.value = isFinishedOnInit;
longAnswerTagColor.value = tagToneColor ? decodeURIComponent(tagToneColor) : longAnswerTagColor.value;
console.log("LongAnswer onLoad with params:", { message, streamId, finished, tagToneColor });
@@ -222,6 +269,7 @@ onLoad(({ message = "", streamId = "", finished = "0", tagToneColor = "" }) => {
unsubscribe = StreamManager.subscribe(
streamId,
(text = "", finished = false, payload = null) => {
streamFinished.value = !!finished;
answerText.value = text || "";
longTextData.value = payload || null;
title.value = computeTitle(
@@ -247,6 +295,7 @@ onLoad(({ message = "", streamId = "", finished = "0", tagToneColor = "" }) => {
// ✅ 非流式
answerText.value = decodeURIComponent(message || "");
longTextData.value = null;
streamFinished.value = true;
title.value = computeTitle(answerText.value);
nextTick(() => {