Merge branch 'main' of https://git.nianxx.cn/zoujing/YGChatCS
# Conflicts: # src/pages/index/components/chat/ChatMainList/index.vue
This commit is contained in:
@@ -50,7 +50,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tianmu": {
|
"tianmu": {
|
||||||
"clientId": "4",
|
"clientId": "9",
|
||||||
"appId": "wx0be424e1d22065a9",
|
"appId": "wx0be424e1d22065a9",
|
||||||
"name": "沐沐",
|
"name": "沐沐",
|
||||||
"placeholder": "快告诉沐沐您在想什么~",
|
"placeholder": "快告诉沐沐您在想什么~",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<text class="font-size-12 font-400 font-color-600">正在生成</text>
|
<text class="font-size-12 font-400 font-color-600">正在生成</text>
|
||||||
<DotLoading />
|
<DotLoading />
|
||||||
</view>
|
</view>
|
||||||
<view v-if="isOverflow && finish" class="flex flex-row flex-items-center mt-8" @click="lookDetailAction">
|
<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>
|
<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>
|
<uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
@@ -27,10 +27,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, computed, ref, watch } from "vue";
|
import { defineProps, computed, ref, watch, onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
import ChatMarkdown from "../../chat/ChatMarkdown/index.vue";
|
import ChatMarkdown from "../../chat/ChatMarkdown/index.vue";
|
||||||
import DotLoading from "../../loading/DotLoading.vue";
|
import DotLoading from "../../loading/DotLoading.vue";
|
||||||
|
import StreamManager from '@/utils/StreamManager.js';
|
||||||
|
|
||||||
const isOverflow = ref(false)
|
const isOverflow = ref(false)
|
||||||
|
|
||||||
@@ -93,9 +94,37 @@ watch(
|
|||||||
|
|
||||||
const lookDetailAction = () => {
|
const lookDetailAction = () => {
|
||||||
const message = props.text ? String(props.text) : "";
|
const message = props.text ? String(props.text) : "";
|
||||||
uni.navigateTo({
|
// 使用 StreamManager 以 streamId 转发当前及后续流式更新,详情页通过 streamId 订阅
|
||||||
url: `/pages/long-answer/index?message=${encodeURIComponent(message)}`,
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,32 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="bg-F5F7FA flex flex-col h-screen overflow-hidden">
|
<view class="flex flex-col bg-liner h-screen overflow-hidden">
|
||||||
<TopNavBar :title="title" backgroundColor="transparent" />
|
<!-- ✅ 顶部固定导航 -->
|
||||||
<view class="flex-full overflow-hidden scroll-y p-12 border-box">
|
<view class="flex-shrink-0">
|
||||||
<ChatMarkdown :text="answerText" />
|
<TopNavBar :title="title" background="transparent" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- ✅ 滚动区域 -->
|
||||||
|
<scroll-view class="flex-full overflow-hidden" scroll-y :scroll-into-view="scrollIntoViewId" scroll-with-animation>
|
||||||
|
<view class="pt-12 px-12 pb-24 border-box">
|
||||||
|
<!-- ✅ 答案内容,支持markdown -->
|
||||||
|
<ChatMarkdown :text="answerText" />
|
||||||
|
|
||||||
|
<!-- ✅ 底部锚点(必须存在) -->
|
||||||
|
<view id="bottom-anchor"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||||
import ChatMarkdown from "../index/components/chat/ChatMarkdown/index.vue";
|
import ChatMarkdown from "../index/components/chat/ChatMarkdown/index.vue";
|
||||||
import { defineProps, ref } from "vue";
|
import { defineProps, ref, nextTick } from "vue";
|
||||||
import { onLoad } from "@dcloudio/uni-app";
|
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||||
|
import StreamManager from "@/utils/StreamManager.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
answerText: {
|
answerText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const answerText = ref(props.answerText || "");
|
const answerText = ref(props.answerText || "");
|
||||||
const title = ref("");
|
const title = ref("");
|
||||||
|
|
||||||
onLoad(({ message = "" }) => {
|
let unsubscribe = null;
|
||||||
answerText.value = decodeURIComponent(message);
|
|
||||||
if (answerText.value.length > 6) {
|
/** ✅ scroll-into-view 控制 */
|
||||||
title.value = answerText.value.substring(0, 6) + "...";
|
const scrollIntoViewId = ref("");
|
||||||
|
|
||||||
|
/** ✅ 防抖 */
|
||||||
|
let scrollTimer = null;
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
if (scrollTimer) return;
|
||||||
|
|
||||||
|
scrollTimer = setTimeout(() => {
|
||||||
|
// ❗关键:强制触发滚动(小程序必须这样)
|
||||||
|
scrollIntoViewId.value = "";
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// 再次 nextTick + 延迟,兼容 markdown 渲染延迟
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollIntoViewId.value = "bottom-anchor";
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
scrollTimer = null;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(({ message = "", streamId = "" }) => {
|
||||||
|
if (streamId) {
|
||||||
|
// ✅ 流式数据
|
||||||
|
unsubscribe = StreamManager.subscribe(
|
||||||
|
streamId,
|
||||||
|
(text = "", finished = false) => {
|
||||||
|
answerText.value = text || "";
|
||||||
|
|
||||||
|
if (answerText.value.length > 6) {
|
||||||
|
title.value = answerText.value.substring(0, 6) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// ✅ 非流式
|
||||||
|
answerText.value = decodeURIComponent(message || "");
|
||||||
|
|
||||||
|
if (answerText.value.length > 6) {
|
||||||
|
title.value = answerText.value.substring(0, 6) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
onUnload(() => {
|
||||||
|
try {
|
||||||
|
unsubscribe && unsubscribe();
|
||||||
|
} catch (e) { }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
52
src/utils/StreamManager.js
Normal file
52
src/utils/StreamManager.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// 简单的流式数据管理器:开启流、更新流、订阅流、关闭流
|
||||||
|
const streams = {};
|
||||||
|
|
||||||
|
function openStream(id, initial = '', finished = false) {
|
||||||
|
if (!id) return;
|
||||||
|
streams[id] = streams[id] || { text: '', finished: false, subs: new Set() };
|
||||||
|
streams[id].text = initial || '';
|
||||||
|
streams[id].finished = !!finished;
|
||||||
|
// notify existing subscribers
|
||||||
|
streams[id].subs.forEach((cb) => cb(streams[id].text, streams[id].finished));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStream(id, text, finished = false) {
|
||||||
|
if (!id || !streams[id]) return;
|
||||||
|
streams[id].text = text || '';
|
||||||
|
streams[id].finished = !!finished;
|
||||||
|
streams[id].subs.forEach((cb) => cb(streams[id].text, streams[id].finished));
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(id, cb) {
|
||||||
|
if (!id) return () => {};
|
||||||
|
streams[id] = streams[id] || { text: '', finished: false, subs: new Set() };
|
||||||
|
streams[id].subs.add(cb);
|
||||||
|
// send current snapshot immediately
|
||||||
|
cb(streams[id].text, streams[id].finished);
|
||||||
|
return () => {
|
||||||
|
streams[id] && streams[id].subs.delete(cb);
|
||||||
|
// 移除空流
|
||||||
|
if (streams[id] && streams[id].subs.size === 0 && streams[id].finished) {
|
||||||
|
delete streams[id];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStream(id) {
|
||||||
|
if (!id || !streams[id]) return;
|
||||||
|
streams[id].subs.forEach((cb) => cb(streams[id].text, true));
|
||||||
|
delete streams[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapshot(id) {
|
||||||
|
if (!id || !streams[id]) return { text: '', finished: false };
|
||||||
|
return { text: streams[id].text, finished: streams[id].finished };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
openStream,
|
||||||
|
updateStream,
|
||||||
|
subscribe,
|
||||||
|
closeStream,
|
||||||
|
getSnapshot,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user