|
@@ -8,7 +8,7 @@
|
|
|
<!-- AI消息 -->
|
|
<!-- AI消息 -->
|
|
|
<template v-if="message.sender === 'ai'">
|
|
<template v-if="message.sender === 'ai'">
|
|
|
<view class="avatar-container">
|
|
<view class="avatar-container">
|
|
|
- <image class="avatar" src="/static/icons/ai.png" mode="aspectFill"></image>
|
|
|
|
|
|
|
+ <image class="avatar" src="/static/icons/ai-1.png" mode="aspectFill"></image>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="message-content">
|
|
<view class="message-content">
|
|
|
<view class="message-bubble ai-bubble"
|
|
<view class="message-bubble ai-bubble"
|
|
@@ -25,6 +25,11 @@
|
|
|
:class="{'highlight': containsKeywords(message.content)}">
|
|
:class="{'highlight': containsKeywords(message.content)}">
|
|
|
{{ message.content }}
|
|
{{ message.content }}
|
|
|
</text>
|
|
</text>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI免责声明(非欢迎消息且非输入中显示) -->
|
|
|
|
|
+ <text class="ai-disclaimer">
|
|
|
|
|
+ 内容均由AI生成,仅供参考
|
|
|
|
|
+ </text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 操作按钮区域(仅最后一条AI消息显示) -->
|
|
<!-- 操作按钮区域(仅最后一条AI消息显示) -->
|
|
@@ -55,7 +60,14 @@
|
|
|
</view>
|
|
</view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
|
<!-- 底部输入区 -->
|
|
<!-- 底部输入区 -->
|
|
|
- <view class="input-container" :style="{ paddingBottom: `${isIOS ? safeAreaBottom : 20}rpx` }">
|
|
|
|
|
|
|
+ <view class="input-container" :style="{
|
|
|
|
|
+ // #ifdef H5
|
|
|
|
|
+ paddingBottom: `${safeAreaBottom + 100}rpx`
|
|
|
|
|
+ // #endif
|
|
|
|
|
+ // #ifdef APP-HARMONY
|
|
|
|
|
+ paddingBottom: `${safeAreaBottom + 20}rpx`
|
|
|
|
|
+ // #endif
|
|
|
|
|
+ }">
|
|
|
<!-- 问题建议区 -->
|
|
<!-- 问题建议区 -->
|
|
|
<scroll-view v-if="chatMessages.length <= 3 && !inputMessage && !isProcessing" class="suggested-questions" scroll-x>
|
|
<scroll-view v-if="chatMessages.length <= 3 && !inputMessage && !isProcessing" class="suggested-questions" scroll-x>
|
|
|
<view v-for="(question, index) in suggestedQuestions" :key="index" class="question-chip"
|
|
<view v-for="(question, index) in suggestedQuestions" :key="index" class="question-chip"
|
|
@@ -85,12 +97,14 @@
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- renderjs 模块容器(用于 H5/App 端 SSE 流式连接) -->
|
|
<!-- renderjs 模块容器(用于 H5/App 端 SSE 流式连接) -->
|
|
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
<view
|
|
<view
|
|
|
:change:prop="renderModule.onDataChange"
|
|
:change:prop="renderModule.onDataChange"
|
|
|
:prop="renderjsData"
|
|
:prop="renderjsData"
|
|
|
:onStreamData="onStreamData"
|
|
:onStreamData="onStreamData"
|
|
|
class="renderjs-container"
|
|
class="renderjs-container"
|
|
|
></view>
|
|
></view>
|
|
|
|
|
+ <!-- #endif -->
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
|
<script setup>
|
|
<script setup>
|
|
@@ -131,7 +145,7 @@
|
|
|
'有机肥和化肥怎么搭配使用?'
|
|
'有机肥和化肥怎么搭配使用?'
|
|
|
])
|
|
])
|
|
|
const statusBarHeight = ref(20)
|
|
const statusBarHeight = ref(20)
|
|
|
- const safeAreaBottom = ref(34)
|
|
|
|
|
|
|
+ const safeAreaBottom = ref(0)
|
|
|
const isIOS = ref(false)
|
|
const isIOS = ref(false)
|
|
|
|
|
|
|
|
// renderjs 通信数据
|
|
// renderjs 通信数据
|
|
@@ -165,9 +179,8 @@
|
|
|
|
|
|
|
|
// renderjs 回调:接收流式数据
|
|
// renderjs 回调:接收流式数据
|
|
|
const onStreamData = (data) => {
|
|
const onStreamData = (data) => {
|
|
|
- console.log('=== Vue收到流式数据 ===', data);
|
|
|
|
|
|
|
+ console.log('=== Vue收到流式数据 ===');
|
|
|
console.log('数据类型:', data.type);
|
|
console.log('数据类型:', data.type);
|
|
|
- console.log('当前输入消息索引:', currentTypingMessage.value);
|
|
|
|
|
|
|
|
|
|
if (data.type === 'thinking') {
|
|
if (data.type === 'thinking') {
|
|
|
console.log('进入 Thinking 模式');
|
|
console.log('进入 Thinking 模式');
|
|
@@ -176,7 +189,9 @@
|
|
|
thinkingBuffer.value = data.content;
|
|
thinkingBuffer.value = data.content;
|
|
|
processThinkingContent();
|
|
processThinkingContent();
|
|
|
} else if (data.type === 'message') {
|
|
} else if (data.type === 'message') {
|
|
|
- console.log('收到消息内容:', data.content);
|
|
|
|
|
|
|
+ const contentLength = data.content ? data.content.length : 0;
|
|
|
|
|
+ console.log('收到消息内容,长度:', contentLength);
|
|
|
|
|
+
|
|
|
// 判断是否在 Thinking 模式中
|
|
// 判断是否在 Thinking 模式中
|
|
|
if (isInThinkingMode.value) {
|
|
if (isInThinkingMode.value) {
|
|
|
console.log('仍在 Thinking 模式,累积内容');
|
|
console.log('仍在 Thinking 模式,累积内容');
|
|
@@ -190,8 +205,7 @@
|
|
|
}
|
|
}
|
|
|
} else if (data.type === 'end') {
|
|
} else if (data.type === 'end') {
|
|
|
// 流式结束
|
|
// 流式结束
|
|
|
- console.log("流式结束,消息id:",data.id);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ console.log("流式结束,消息id:", data.id);
|
|
|
finishStreaming(data.id);
|
|
finishStreaming(data.id);
|
|
|
} else if (data.type === 'error') {
|
|
} else if (data.type === 'error') {
|
|
|
// 错误处理
|
|
// 错误处理
|
|
@@ -247,33 +261,58 @@
|
|
|
console.log('startTypingEffect 被调用');
|
|
console.log('startTypingEffect 被调用');
|
|
|
console.log('isTypingEffect:', isTypingEffect.value);
|
|
console.log('isTypingEffect:', isTypingEffect.value);
|
|
|
console.log('currentTypingMessage:', currentTypingMessage.value);
|
|
console.log('currentTypingMessage:', currentTypingMessage.value);
|
|
|
|
|
+ console.log('messageQueue 长度:', messageQueue.value.length);
|
|
|
|
|
+
|
|
|
|
|
+ if (isTypingEffect.value) {
|
|
|
|
|
+ console.log('打字机效果已在运行,退出');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (isTypingEffect.value || currentTypingMessage.value === null) {
|
|
|
|
|
- console.log('打字机效果已在运行或没有当前消息,退出');
|
|
|
|
|
|
|
+ if (currentTypingMessage.value === null) {
|
|
|
|
|
+ console.log('没有当前消息,退出');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (messageQueue.value.length === 0) {
|
|
|
|
|
+ console.log('消息队列为空,退出');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
isTypingEffect.value = true;
|
|
isTypingEffect.value = true;
|
|
|
- chatMessages.value[currentTypingMessage.value].isTyping = false;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭 typing 指示器
|
|
|
|
|
+ if (chatMessages.value[currentTypingMessage.value]) {
|
|
|
|
|
+ chatMessages.value[currentTypingMessage.value].isTyping = false;
|
|
|
|
|
+ console.log('已关闭 typing 指示器');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
console.log('开始打字机效果,消息索引:', currentTypingMessage.value);
|
|
console.log('开始打字机效果,消息索引:', currentTypingMessage.value);
|
|
|
|
|
|
|
|
const processQueue = () => {
|
|
const processQueue = () => {
|
|
|
if (messageQueue.value.length > 0 && currentTypingMessage.value !== null) {
|
|
if (messageQueue.value.length > 0 && currentTypingMessage.value !== null) {
|
|
|
// 每次取出一个字符
|
|
// 每次取出一个字符
|
|
|
const char = messageQueue.value.shift();
|
|
const char = messageQueue.value.shift();
|
|
|
- const currentContent = chatMessages.value[currentTypingMessage.value].content || '';
|
|
|
|
|
- chatMessages.value[currentTypingMessage.value].content = currentContent + char;
|
|
|
|
|
|
|
+ const currentMsg = chatMessages.value[currentTypingMessage.value];
|
|
|
|
|
|
|
|
- // 每20个字符滚动一次,优化性能
|
|
|
|
|
- if (currentContent.length % 20 === 0) {
|
|
|
|
|
- scrollToBottom();
|
|
|
|
|
|
|
+ if (currentMsg) {
|
|
|
|
|
+ const currentContent = currentMsg.content || '';
|
|
|
|
|
+ currentMsg.content = currentContent + char;
|
|
|
|
|
+
|
|
|
|
|
+ // 每20个字符滚动一次,优化性能
|
|
|
|
|
+ if (currentContent.length % 20 === 0) {
|
|
|
|
|
+ scrollToBottom();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 继续处理队列
|
|
// 继续处理队列
|
|
|
typingTimer.value = setTimeout(processQueue, 10);
|
|
typingTimer.value = setTimeout(processQueue, 10);
|
|
|
- } else if (messageQueue.value.length === 0) {
|
|
|
|
|
- // 队列为空,等待新数据
|
|
|
|
|
|
|
+ } else if (messageQueue.value.length === 0 && isProcessing.value) {
|
|
|
|
|
+ // 队列为空但还在处理中,等待新数据
|
|
|
typingTimer.value = setTimeout(processQueue, 50);
|
|
typingTimer.value = setTimeout(processQueue, 50);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 队列为空且不在处理中,停止打字机效果
|
|
|
|
|
+ console.log('队列处理完成,停止打字机效果');
|
|
|
|
|
+ isTypingEffect.value = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -398,6 +437,7 @@
|
|
|
user: 'user_' + sessionId.value
|
|
user: 'user_' + sessionId.value
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // #ifdef H5
|
|
|
// 更新 renderjs 数据,触发 SSE 连接
|
|
// 更新 renderjs 数据,触发 SSE 连接
|
|
|
renderjsData.value = {
|
|
renderjsData.value = {
|
|
|
action: 'start',
|
|
action: 'start',
|
|
@@ -406,17 +446,217 @@
|
|
|
token: storage.getAccessToken(),
|
|
token: storage.getAccessToken(),
|
|
|
timestamp: Date.now()
|
|
timestamp: Date.now()
|
|
|
};
|
|
};
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef APP-HARMONY || APP-PLUS
|
|
|
|
|
+ // 鸿蒙等不支持 renderjs 的平台,使用原生方式
|
|
|
|
|
+ startSSERequestNative(url, requestData);
|
|
|
|
|
+ // #endif
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 鸿蒙端 SSE 请求任务引用
|
|
|
|
|
+ let sseRequestTask = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 原生方式处理 SSE 请求(用于鸿蒙等平台)
|
|
|
|
|
+ const startSSERequestNative = (url, data) => {
|
|
|
|
|
+ console.log('[鸿蒙端] 开始 SSE 请求:', url);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建请求任务
|
|
|
|
|
+ sseRequestTask = uni.request({
|
|
|
|
|
+ url: url,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'Accept': 'text/event-stream',
|
|
|
|
|
+ 'Authorization': `Bearer ${storage.getAccessToken()}`
|
|
|
|
|
+ },
|
|
|
|
|
+ data: data,
|
|
|
|
|
+ enableChunked: true, // 启用分块传输
|
|
|
|
|
+ responseType: 'text', // 接收文本数据
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ console.log('[鸿蒙端] 请求成功,状态码:', res.statusCode);
|
|
|
|
|
+ console.log('[鸿蒙端] 响应数据类型:', typeof res.data);
|
|
|
|
|
+
|
|
|
|
|
+ if (res.statusCode === 200 && res.data) {
|
|
|
|
|
+ console.log('[鸿蒙端] 响应数据长度:', res.data.length);
|
|
|
|
|
+ // 处理 SSE 格式的响应数据
|
|
|
|
|
+ parseSSEResponse(res.data);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('[鸿蒙端] 请求失败或无数据');
|
|
|
|
|
+ handleStreamError('请求失败,状态码: ' + res.statusCode);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('[鸿蒙端] 请求失败:', err);
|
|
|
|
|
+ handleStreamError(err.errMsg || '网络异常');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听数据接收(如果支持分块传输)
|
|
|
|
|
+ if (sseRequestTask && typeof sseRequestTask.onChunkReceived === 'function') {
|
|
|
|
|
+ console.log('[鸿蒙端] 支持分块接收,启用监听');
|
|
|
|
|
+ sseRequestTask.onChunkReceived((chunkRes) => {
|
|
|
|
|
+ console.log('[鸿蒙端] 收到数据块');
|
|
|
|
|
+ if (chunkRes && chunkRes.data) {
|
|
|
|
|
+ parseSSEResponseChunk(chunkRes.data);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('[鸿蒙端] 不支持分块接收,将在 success 回调中处理完整响应');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 SSE 格式的响应数据(处理分块数据)
|
|
|
|
|
+ let sseBuffer = ''; // 用于累积不完整的行
|
|
|
|
|
+
|
|
|
|
|
+ const parseSSEResponseChunk = (chunkText) => {
|
|
|
|
|
+ if (!chunkText || typeof chunkText !== 'string') {
|
|
|
|
|
+ console.log('[鸿蒙端] 无效的数据块');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[鸿蒙端] 处理数据块,长度:', chunkText.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 累积到缓冲区
|
|
|
|
|
+ sseBuffer += chunkText;
|
|
|
|
|
+
|
|
|
|
|
+ // 按行分割
|
|
|
|
|
+ const lines = sseBuffer.split('\n');
|
|
|
|
|
+
|
|
|
|
|
+ // 保留最后一行(可能不完整)
|
|
|
|
|
+ sseBuffer = lines.pop() || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 处理完整的行
|
|
|
|
|
+ processSSELines(lines);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 SSE 格式的响应数据(处理完整响应)
|
|
|
|
|
+ const parseSSEResponse = (responseText) => {
|
|
|
|
|
+ if (!responseText || typeof responseText !== 'string') {
|
|
|
|
|
+ console.log('[鸿蒙端] 无效的响应数据');
|
|
|
|
|
+ finishStreaming();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[鸿蒙端] 开始解析完整 SSE 数据,长度:', responseText.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 按行分割
|
|
|
|
|
+ const lines = responseText.split('\n');
|
|
|
|
|
+
|
|
|
|
|
+ // 处理所有行
|
|
|
|
|
+ processSSELines(lines);
|
|
|
|
|
+
|
|
|
|
|
+ // 清空缓冲区
|
|
|
|
|
+ sseBuffer = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理 SSE 行数据
|
|
|
|
|
+ const processSSELines = (lines) => {
|
|
|
|
|
+ let messageId = null;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
|
|
+ const line = lines[i].trim();
|
|
|
|
|
+
|
|
|
|
|
+ if (!line) continue; // 跳过空行
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`[鸿蒙端] 处理第 ${i} 行:`, line.substring(0, 100));
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 event: 行
|
|
|
|
|
+ if (line.startsWith('event:')) {
|
|
|
|
|
+ // event: message
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析 data: 行
|
|
|
|
|
+ if (line.startsWith('data:') || line !== '') {
|
|
|
|
|
+ let data = line;
|
|
|
|
|
+ if (line.startsWith('data:')) {
|
|
|
|
|
+ data = data.substring(5).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!data || data.includes("ping")) continue;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[鸿蒙端] 解析后的数据,长度:', data.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤掉 MESSAGE_END 等元数据事件(JSON 格式)
|
|
|
|
|
+ if (data.startsWith('{') && data.includes('"eventType"')) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const jsonData = JSON.parse(data);
|
|
|
|
|
+ console.log('[鸿蒙端] JSON 事件:', jsonData.eventType || jsonData.event);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是 MESSAGE_END 事件,通知结束
|
|
|
|
|
+ if (jsonData.eventType === 'MESSAGE_END' || jsonData.event === 'message_end') {
|
|
|
|
|
+ console.log('[鸿蒙端] 收到 MESSAGE_END 事件,流式结束');
|
|
|
|
|
+ messageId = jsonData.id;
|
|
|
|
|
+ // 不要在这里 continue,继续处理后续行
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 其他元数据事件也忽略
|
|
|
|
|
+ if (jsonData.eventType || jsonData.event) {
|
|
|
|
|
+ console.log('[鸿蒙端] 跳过元数据事件');
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.log('[鸿蒙端] JSON 解析失败,作为普通文本处理');
|
|
|
|
|
+ // 不是 JSON,继续处理
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否是 Thinking 内容
|
|
|
|
|
+ if (data.includes('<details') && data.includes('<summary>')) {
|
|
|
|
|
+ console.log('[鸿蒙端] 发送 thinking 类型数据');
|
|
|
|
|
+ onStreamData({ type: 'thinking', content: data });
|
|
|
|
|
+ } else if (data) {
|
|
|
|
|
+ // 普通消息内容 - 逐行发送,不累积
|
|
|
|
|
+ console.log('[鸿蒙端] 发送 message 类型数据,长度:', data.length);
|
|
|
|
|
+ onStreamData({ type: 'message', content: data });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果找到了 messageId,结束流式输出
|
|
|
|
|
+ if (messageId) {
|
|
|
|
|
+ console.log('[鸿蒙端] 数据处理完成,结束流式输出');
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ onStreamData({ type: 'end', id: messageId });
|
|
|
|
|
+ }, 300);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果没有 messageId,延迟结束(兼容处理)
|
|
|
|
|
+ console.log('[鸿蒙端] 无 messageId,延迟结束');
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (isProcessing.value) {
|
|
|
|
|
+ onStreamData({ type: 'end', id: null });
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 停止鸿蒙端的 SSE 请求
|
|
|
|
|
+ const stopSSERequestNative = () => {
|
|
|
|
|
+ if (sseRequestTask) {
|
|
|
|
|
+ console.log('[鸿蒙端] 中止请求');
|
|
|
|
|
+ sseRequestTask.abort();
|
|
|
|
|
+ sseRequestTask = null;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 停止流式输出
|
|
// 停止流式输出
|
|
|
const stopStreaming = () => {
|
|
const stopStreaming = () => {
|
|
|
console.log('用户中止流式输出');
|
|
console.log('用户中止流式输出');
|
|
|
|
|
|
|
|
|
|
+ // #ifdef H5
|
|
|
// 通知 renderjs 停止
|
|
// 通知 renderjs 停止
|
|
|
renderjsData.value = {
|
|
renderjsData.value = {
|
|
|
action: 'stop',
|
|
action: 'stop',
|
|
|
timestamp: Date.now()
|
|
timestamp: Date.now()
|
|
|
};
|
|
};
|
|
|
|
|
+ // #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef APP-HARMONY
|
|
|
|
|
+ // 停止鸿蒙端的请求
|
|
|
|
|
+ stopSSERequestNative();
|
|
|
|
|
+ // #endif
|
|
|
|
|
|
|
|
// 立即停止打字机效果并完成
|
|
// 立即停止打字机效果并完成
|
|
|
finishStreaming();
|
|
finishStreaming();
|
|
@@ -633,7 +873,11 @@
|
|
|
const systemInfo = uni.getSystemInfoSync();
|
|
const systemInfo = uni.getSystemInfoSync();
|
|
|
statusBarHeight.value = systemInfo.statusBarHeight || 20;
|
|
statusBarHeight.value = systemInfo.statusBarHeight || 20;
|
|
|
isIOS.value = systemInfo.platform === 'ios';
|
|
isIOS.value = systemInfo.platform === 'ios';
|
|
|
- safeAreaBottom.value = systemInfo.safeAreaInsets ? (systemInfo.safeAreaInsets.bottom || 0) : 0;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 计算底部安全区域(包含 tabbar 高度)
|
|
|
|
|
+ const safeAreaInsetsBottom = systemInfo.safeAreaInsets ? (systemInfo.safeAreaInsets.bottom || 0) : 0;
|
|
|
|
|
+ // 转换为 rpx,并确保至少有基础间距
|
|
|
|
|
+ safeAreaBottom.value = Math.max(safeAreaInsetsBottom * 2, 0);
|
|
|
|
|
|
|
|
// 初始化消息
|
|
// 初始化消息
|
|
|
initMessages();
|
|
initMessages();
|
|
@@ -643,11 +887,15 @@
|
|
|
scrollToBottom();
|
|
scrollToBottom();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 监听来自 renderjs 的自定义事件
|
|
|
|
|
- window.addEventListener('renderjs-stream-data', (event) => {
|
|
|
|
|
- console.log('通过事件接收到数据:', event.detail);
|
|
|
|
|
- onStreamData(event.detail);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 监听来自 renderjs 的自定义事件(仅在支持的平台)
|
|
|
|
|
+ // #ifdef H5 || APP-PLUS
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.addEventListener('renderjs-stream-data', (event) => {
|
|
|
|
|
+ console.log('通过事件接收到数据:', event.detail);
|
|
|
|
|
+ onStreamData(event.detail);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ // #endif
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 组件销毁时清理资源
|
|
// 组件销毁时清理资源
|
|
@@ -666,11 +914,17 @@
|
|
|
// 清理消息队列
|
|
// 清理消息队列
|
|
|
messageQueue.value = [];
|
|
messageQueue.value = [];
|
|
|
|
|
|
|
|
- // 移除事件监听器
|
|
|
|
|
- window.removeEventListener('renderjs-stream-data', onStreamData);
|
|
|
|
|
|
|
+ // 移除事件监听器(仅在支持的平台)
|
|
|
|
|
+ // #ifdef H5 || APP-PLUS
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.removeEventListener('renderjs-stream-data', onStreamData);
|
|
|
|
|
+ }
|
|
|
|
|
+ // #endif
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+<!-- #ifdef H5 || APP-PLUS -->
|
|
|
<script module="renderModule" lang="renderjs">
|
|
<script module="renderModule" lang="renderjs">
|
|
|
// renderjs 模块状态
|
|
// renderjs 模块状态
|
|
|
let eventSource = null;
|
|
let eventSource = null;
|
|
@@ -718,9 +972,11 @@
|
|
|
|
|
|
|
|
if (done) {
|
|
if (done) {
|
|
|
console.log('SSE 流结束');
|
|
console.log('SSE 流结束');
|
|
|
- window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
- detail: { type: 'end', id: messageIdS }
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
+ detail: { type: 'end', id: messageIdS }
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
break;
|
|
break;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -738,12 +994,14 @@
|
|
|
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('SSE 连接错误:', error);
|
|
console.error('SSE 连接错误:', error);
|
|
|
- window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
- detail: {
|
|
|
|
|
- type: 'error',
|
|
|
|
|
- error: error.message || '连接失败'
|
|
|
|
|
- }
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ type: 'error',
|
|
|
|
|
+ error: error.message || '连接失败'
|
|
|
|
|
+ }
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
} finally {
|
|
} finally {
|
|
|
stopSSE();
|
|
stopSSE();
|
|
|
}
|
|
}
|
|
@@ -780,9 +1038,11 @@
|
|
|
if (jsonData.eventType === 'MESSAGE_END' || jsonData.event === 'message_end') {
|
|
if (jsonData.eventType === 'MESSAGE_END' || jsonData.event === 'message_end') {
|
|
|
console.log('[renderjs] 收到 MESSAGE_END 事件,流式结束');
|
|
console.log('[renderjs] 收到 MESSAGE_END 事件,流式结束');
|
|
|
messageIdS = jsonData.id;
|
|
messageIdS = jsonData.id;
|
|
|
- window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
- detail: { type: 'end', id: messageIdS }
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
+ detail: { type: 'end', id: messageIdS }
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
// 其他元数据事件也忽略
|
|
// 其他元数据事件也忽略
|
|
@@ -797,21 +1057,25 @@
|
|
|
if (data.includes('<details') && data.includes('<summary>')) {
|
|
if (data.includes('<details') && data.includes('<summary>')) {
|
|
|
console.log('[renderjs] 发送 thinking 类型数据');
|
|
console.log('[renderjs] 发送 thinking 类型数据');
|
|
|
// 使用自定义事件发送数据
|
|
// 使用自定义事件发送数据
|
|
|
- window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
- detail: {
|
|
|
|
|
- type: 'thinking',
|
|
|
|
|
- content: data
|
|
|
|
|
- }
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ type: 'thinking',
|
|
|
|
|
+ content: data
|
|
|
|
|
+ }
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
console.log('[renderjs] 发送 message 类型数据,长度:', data.length);
|
|
console.log('[renderjs] 发送 message 类型数据,长度:', data.length);
|
|
|
// 普通消息内容
|
|
// 普通消息内容
|
|
|
- window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
- detail: {
|
|
|
|
|
- type: 'message',
|
|
|
|
|
- content: data
|
|
|
|
|
- }
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
|
|
|
|
|
+ detail: {
|
|
|
|
|
+ type: 'message',
|
|
|
|
|
+ content: data
|
|
|
|
|
+ }
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -851,6 +1115,7 @@
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
</script>
|
|
</script>
|
|
|
|
|
+<!-- #endif -->
|
|
|
|
|
|
|
|
<style>
|
|
<style>
|
|
|
/* 容器样式 */
|
|
/* 容器样式 */
|
|
@@ -991,7 +1256,7 @@
|
|
|
/* 输入区域 */
|
|
/* 输入区域 */
|
|
|
.input-container {
|
|
.input-container {
|
|
|
position: fixed;
|
|
position: fixed;
|
|
|
- bottom: 90rpx;
|
|
|
|
|
|
|
+ bottom: 0;
|
|
|
left: 0;
|
|
left: 0;
|
|
|
right: 0;
|
|
right: 0;
|
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
@@ -999,7 +1264,8 @@
|
|
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
|
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- z-index: 10;
|
|
|
|
|
|
|
+ /* z-index: 999; */
|
|
|
|
|
+ /* 确保在 tabbar 之上 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.input-wrapper {
|
|
.input-wrapper {
|
|
@@ -1167,6 +1433,17 @@
|
|
|
color: #2E7D32;
|
|
color: #2E7D32;
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /* AI免责声明 */
|
|
|
|
|
+ .ai-disclaimer {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 20rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+ padding-top: 8rpx;
|
|
|
|
|
+ border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
/* 欢迎消息特殊样式 */
|
|
/* 欢迎消息特殊样式 */
|
|
|
.welcome {
|
|
.welcome {
|