149 lines
4.6 KiB
Vue
149 lines
4.6 KiB
Vue
<template>
|
||
<view 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 p-16 border-box border-left-4">
|
||
<view v-if="title" class="flex flex-row flex-items-start flex-justify-start mb-8">
|
||
<uni-icons class="icon-active" type="fire-filled" size="18" color="opacity" />
|
||
<text class="font-size-16 font-500 text-color-900 ml-6"> {{ title }}</text>
|
||
</view>
|
||
<!-- 文字内容,最多显示3行 -->
|
||
<view class="answer-content font-size-12 font-color-600">
|
||
<ChatMarkdown :key="textKey" :text="processedText" />
|
||
</view>
|
||
<!-- 超过3行时显示...提示 -->
|
||
<view v-if="!finish" class="flex flex-row flex-items-center mt-8">
|
||
<text class="font-size-12 font-400 font-color-600">正在生成</text>
|
||
<DotLoading />
|
||
</view>
|
||
<view v-if="isOverflow" class="flex flex-row flex-items-center mt-8" @click="lookDetailAction">
|
||
<text class="font-size-12 font-400 theme-color-500 mr-4">查看详情</text>
|
||
<uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons>
|
||
</view>
|
||
|
||
</view>
|
||
</view>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import { defineProps, computed, ref, watch, onBeforeUnmount } from "vue";
|
||
|
||
import ChatMarkdown from "../../chat/ChatMarkdown/index.vue";
|
||
import DotLoading from "../../loading/DotLoading.vue";
|
||
import StreamManager from '@/utils/StreamManager.js';
|
||
|
||
const isOverflow = ref(false)
|
||
|
||
// 直接根据文字长度判断,超过约100个字符认为会溢出(约3行)
|
||
const props = defineProps({
|
||
title: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
text: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
finish: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
});
|
||
|
||
// 用于强制重新渲染的key
|
||
const textKey = ref(0);
|
||
|
||
// 处理文本内容:按行截断以保证预览最多显示三行(更贴近视觉行数)
|
||
// 点击“查看详情”会跳转到完整页面(不受预览截断影响)。
|
||
const PREVIEW_LINES = 3;
|
||
const PREVIEW_CHAR_LIMIT = 100; // 作为备用,当没有换行但过长时也会截断
|
||
const processedText = computed(() => {
|
||
const txt = props.text ? String(props.text) : "";
|
||
if (!txt) return "";
|
||
|
||
// 按行分割(保留空行)
|
||
const lines = txt.split(/\r?\n/);
|
||
|
||
// 如果行数超过限制,截取前 PREVIEW_LINES 行并添加省略号
|
||
if (lines.length > PREVIEW_LINES) {
|
||
return lines.slice(0, PREVIEW_LINES).join("\n") + "...";
|
||
}
|
||
|
||
// 若虽然行数不超过,但总长度仍然很长,做字符级截断作为兜底
|
||
if (txt.length > PREVIEW_CHAR_LIMIT) {
|
||
return txt.slice(0, PREVIEW_CHAR_LIMIT) + "...";
|
||
}
|
||
|
||
return txt;
|
||
});
|
||
|
||
// 监听 text 变化:更新 textKey 并同步 isOverflow(合并为单一响应函数,避免冗余)
|
||
watch(
|
||
() => props.text,
|
||
(newText, oldText) => {
|
||
const textStr = newText ? String(newText) : "";
|
||
const lines = textStr.split(/\r?\n/);
|
||
isOverflow.value = lines.length > PREVIEW_LINES || textStr.length > PREVIEW_CHAR_LIMIT;
|
||
if (newText !== oldText) {
|
||
textKey.value++;
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
const lookDetailAction = () => {
|
||
const message = props.text ? String(props.text) : "";
|
||
// 使用 StreamManager 以 streamId 转发当前及后续流式更新,详情页通过 streamId 订阅
|
||
const streamId = `stream_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
|
||
StreamManager.openStream(streamId, message, !!props.finish);
|
||
|
||
// 将当前组件后续 props.text/props.finish 的更新转发到 StreamManager
|
||
const stopForward = watch(
|
||
() => props.text,
|
||
(v) => {
|
||
StreamManager.updateStream(streamId, v ? String(v) : "", !!props.finish);
|
||
}
|
||
);
|
||
const stopFinishWatcher = watch(
|
||
() => props.finish,
|
||
(f) => {
|
||
StreamManager.updateStream(streamId, props.text ? String(props.text) : "", !!f);
|
||
if (f) {
|
||
stopForward();
|
||
stopFinishWatcher();
|
||
}
|
||
}
|
||
);
|
||
|
||
// 清理:组件卸载时停止转发(若仍存在)
|
||
onBeforeUnmount(() => {
|
||
try {
|
||
stopForward && stopForward();
|
||
stopFinishWatcher && stopFinishWatcher();
|
||
} catch (e) {}
|
||
});
|
||
|
||
uni.navigateTo({ url: `/pages/long-answer/index?streamId=${encodeURIComponent(streamId)}` });
|
||
}
|
||
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.icon-active {
|
||
margin-top: 1px;
|
||
color: $theme-color-500;
|
||
}
|
||
.answer-content {
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 3;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
line-height: 16px;
|
||
max-height: 80px;
|
||
}
|
||
.border-left-4 {
|
||
border-left: 4px solid $theme-color-500;
|
||
}
|
||
</style> |