Files
YGChatCS/src/pages/ChatModule/AnswerComponent/index.vue
duanshuwen 4af4255610 refactor: migrate to tailwind arbitrary width classes
remove the unused src/static/scss/width.scss file and its import from index.scss, replace all existing custom width utility class usages with the equivalent Tailwind arbitrary width syntax
2026-07-24 22:28:16 +08:00

223 lines
7.4 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 v-if="shouldRenderCard" class="w-full bg-white border-ff overflow-hidden rounded-[20px] flex flex-col">
<!-- 占位撑开 -->
<view class="w-screen"></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-[4px]">
<text class="text-[16px] font-[600] text-[#0B5C2D]"> {{ title }}</text>
</view>
<!-- 文字内容根据数据模式限制预览行数 -->
<view v-if="processedContent" class="answer-content text-[12px] font-color-600" :style="answerContentStyle">
<ChatMarkdown :text="processedContent" :fontColor="'#94A3B8'" />
</view>
<!-- 超过2行时显示...提示 -->
<view v-if="!finish" class="flex flex-row flex-items-center mt-[8px]">
<text class="text-[12px] font-[500] font-color-600">正在生成</text>
<ChatLoading />
</view>
<view v-if="isOverflow" class="flex flex-row flex-items-center flex-justify-between mt-[12px]">
<text class="text-[12px] font-bold text-[#cbd5e1] mr-[4px]">查看完整{{ 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>