Remove the unused padding.scss stylesheet and its import from the main SCSS entrypoint. Update all legacy padding utility class names across every Vue component to use inline pixel-based utility classes (such as replacing `pl-12` with `pl-[12px]`). Also clean up extraneous whitespace and line breaks in component templates for improved readability.
223 lines
7.3 KiB
Vue
223 lines
7.3 KiB
Vue
<template>
|
||
<view v-if="shouldRenderCard" class="w-full bg-white border-box border-ff overflow-hidden rounded-20 flex flex-col">
|
||
<!-- 占位撑开 -->
|
||
<view class="w-vw"></view>
|
||
<view class="flex flex-col px-[16px] pt-[16px] pb-[12px] border-box" @click="lookDetailAction">
|
||
<view v-if="tag" class="long-answer-tag" :style="longAnswerTagStyle">{{ tag }}</view>
|
||
<view v-if="title" class="flex flex-row flex-items-start flex-justify-start mb-4">
|
||
<text class="font-size-16 font-600 text-color-900"> {{ title }}</text>
|
||
</view>
|
||
<!-- 文字内容,根据数据模式限制预览行数 -->
|
||
<view v-if="processedContent" class="answer-content font-size-12 font-color-600" :style="answerContentStyle">
|
||
<ChatMarkdown :text="processedContent" :fontColor="'#94A3B8'" />
|
||
</view>
|
||
<!-- 超过2行时显示...提示 -->
|
||
<view v-if="!finish" class="flex flex-row flex-items-center mt-8">
|
||
<text class="font-size-12 font-500 font-color-600">正在生成</text>
|
||
<ChatLoading />
|
||
</view>
|
||
<view v-if="isOverflow" class="flex flex-row flex-items-center flex-justify-between mt-12">
|
||
<text class="font-size-12 font-800 color-CBD5E1 mr-4">查看完整{{ tag }}</text>
|
||
<uni-icons type="right" size="16" color="#CBD5E1"></uni-icons>
|
||
</view>
|
||
|
||
</view>
|
||
</view>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import { defineProps, computed, watch, onBeforeUnmount, ref } from "vue";
|
||
|
||
import ChatMarkdown from "../../ChatMain/ChatMarkdown/index.vue";
|
||
import ChatLoading from "../../ChatMain/ChatLoading/index.vue";
|
||
import StreamManager from '@/utils/StreamManager.js';
|
||
import {
|
||
LONG_TEXT_KEYS,
|
||
getLongTextPreviewText,
|
||
getLongTextValue,
|
||
hasLongTextExtraSections,
|
||
} from "@/utils/longTextCard";
|
||
import { buildTagToneStyle, pickRandomTagToneColor } from "@/utils/tagTone";
|
||
|
||
// 直接根据文字长度判断,超过约66个字符认为会溢出(约2行)
|
||
const props = defineProps({
|
||
content: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
longTextData: {
|
||
type: Object,
|
||
default: null,
|
||
},
|
||
finish: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
});
|
||
|
||
const tag = computed(() => getLongTextValue(props.longTextData, LONG_TEXT_KEYS.tag));
|
||
const title = computed(() => getLongTextValue(props.longTextData, LONG_TEXT_KEYS.title));
|
||
const longAnswerTagColor = ref(pickRandomTagToneColor());
|
||
const longAnswerTagStyle = computed(() => buildTagToneStyle(longAnswerTagColor.value));
|
||
const hasStructuredLongTextData = computed(() => {
|
||
const values = props.longTextData?.values;
|
||
return !!(values && Object.keys(values).length > 0);
|
||
});
|
||
const detailLongTextData = computed(() => {
|
||
return hasStructuredLongTextData.value ? props.longTextData : null;
|
||
});
|
||
const previewContent = computed(() => {
|
||
const structuredPreview = getLongTextPreviewText(props.longTextData, [LONG_TEXT_KEYS.contentSummary]);
|
||
if (structuredPreview) return structuredPreview;
|
||
return hasStructuredLongTextData.value ? "" : props.content || "";
|
||
});
|
||
const shouldRenderCard = computed(() => {
|
||
return hasStructuredLongTextData.value || !!previewContent.value || !props.finish;
|
||
});
|
||
|
||
// 处理文本内容:按行截断以保证预览最多显示指定行数(更贴近视觉行数)
|
||
// 点击“查看详情”会跳转到完整页面(不受预览截断影响)。
|
||
const STRUCTURED_PREVIEW_LINES = 2;
|
||
const STRUCTURED_PREVIEW_CHAR_LIMIT = 40;
|
||
const STRUCTURED_PREVIEW_MAX_HEIGHT = "62px";
|
||
const PLAIN_PREVIEW_LINES = 4;
|
||
const PLAIN_PREVIEW_CHAR_LIMIT = 100;
|
||
const PLAIN_PREVIEW_MAX_HEIGHT = "100px";
|
||
const previewLines = computed(() => {
|
||
return hasStructuredLongTextData.value ? STRUCTURED_PREVIEW_LINES : PLAIN_PREVIEW_LINES;
|
||
});
|
||
const previewCharLimit = computed(() => {
|
||
return hasStructuredLongTextData.value ? STRUCTURED_PREVIEW_CHAR_LIMIT : PLAIN_PREVIEW_CHAR_LIMIT;
|
||
});
|
||
const answerContentStyle = computed(() => {
|
||
const maxHeight = hasStructuredLongTextData.value
|
||
? STRUCTURED_PREVIEW_MAX_HEIGHT
|
||
: PLAIN_PREVIEW_MAX_HEIGHT;
|
||
return `-webkit-line-clamp: ${previewLines.value}; max-height: ${maxHeight};`;
|
||
});
|
||
const processedContent = computed(() => {
|
||
const content = previewContent.value ? String(previewContent.value) : "";
|
||
if (!content) return "";
|
||
|
||
// 按行分割(保留空行)
|
||
const lines = content.split(/\r?\n/);
|
||
|
||
// 如果行数超过限制,截取前指定行数并添加省略号
|
||
if (lines.length > previewLines.value) {
|
||
return lines.slice(0, previewLines.value).join("\n") + "...";
|
||
}
|
||
|
||
// 若虽然行数不超过,但总长度仍然很长,做字符级截断作为兜底
|
||
if (content.length > previewCharLimit.value) {
|
||
return content.slice(0, previewCharLimit.value) + "...";
|
||
}
|
||
|
||
return content;
|
||
});
|
||
|
||
const isOverflow = computed(() => {
|
||
const contentStr = previewContent.value ? String(previewContent.value) : "";
|
||
const lines = contentStr.split(/\r?\n/);
|
||
return (
|
||
(hasStructuredLongTextData.value && hasLongTextExtraSections(props.longTextData)) ||
|
||
lines.length > previewLines.value ||
|
||
contentStr.length > previewCharLimit.value
|
||
);
|
||
});
|
||
|
||
let stopForwardWatcher = null;
|
||
let stopFinishWatcher = null;
|
||
let stopLongTextWatcher = null;
|
||
|
||
const cleanupStreamWatchers = () => {
|
||
if (stopForwardWatcher) {
|
||
stopForwardWatcher();
|
||
stopForwardWatcher = null;
|
||
}
|
||
if (stopFinishWatcher) {
|
||
stopFinishWatcher();
|
||
stopFinishWatcher = null;
|
||
}
|
||
if (stopLongTextWatcher) {
|
||
stopLongTextWatcher();
|
||
stopLongTextWatcher = null;
|
||
}
|
||
};
|
||
|
||
onBeforeUnmount(cleanupStreamWatchers);
|
||
|
||
const lookDetailAction = () => {
|
||
const message = previewContent.value ? String(previewContent.value) : "";
|
||
// 使用 StreamManager 以 streamId 转发当前及后续流式更新,详情页通过 streamId 订阅
|
||
const streamId = `stream_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||
const updateStream = () => {
|
||
StreamManager.updateStream(
|
||
streamId,
|
||
previewContent.value ? String(previewContent.value) : "",
|
||
!!props.finish,
|
||
detailLongTextData.value,
|
||
);
|
||
};
|
||
|
||
StreamManager.openStream(streamId, message, !!props.finish, detailLongTextData.value);
|
||
|
||
cleanupStreamWatchers();
|
||
|
||
if (!props.finish) {
|
||
// 将当前组件后续 props.content/props.finish 的更新转发到 StreamManager
|
||
stopForwardWatcher = watch(
|
||
() => props.content,
|
||
updateStream
|
||
);
|
||
stopLongTextWatcher = watch(
|
||
() => props.longTextData,
|
||
updateStream,
|
||
{ deep: true }
|
||
);
|
||
stopFinishWatcher = watch(
|
||
() => props.finish,
|
||
(f) => {
|
||
StreamManager.updateStream(
|
||
streamId,
|
||
previewContent.value ? String(previewContent.value) : "",
|
||
!!f,
|
||
detailLongTextData.value,
|
||
);
|
||
if (f) {
|
||
cleanupStreamWatchers();
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// 传递 finished 参数,完成状态下不自动滚到底部
|
||
uni.navigateTo({
|
||
url: `/pages/ChatMain/ChatLongAnswer/index?streamId=${encodeURIComponent(streamId)}&finished=${props.finish ? '1' : '0'}&tagToneColor=${encodeURIComponent(longAnswerTagColor.value)}`,
|
||
});
|
||
}
|
||
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.answer-content {
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
line-height: 16px;
|
||
}
|
||
|
||
.long-answer-tag {
|
||
display: inline-flex;
|
||
width: fit-content;
|
||
margin-bottom: 8px;
|
||
padding: 3px 8px;
|
||
border-radius: 12px;
|
||
border: 1px solid transparent;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 18px;
|
||
}
|
||
</style>
|