|
|
|
@ -3,20 +3,29 @@ |
|
|
|
<!-- 顶部导航栏 --> |
|
|
|
<view class="header" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"> |
|
|
|
<view class="header-left"> |
|
|
|
<image src="https://d31zlh4on95l9h.cloudfront.net/images/f91e09b5987802185e7679055dafd272.svg" class="icon"> |
|
|
|
<image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/f91e09b5987802185e7679055dafd272.svg" |
|
|
|
class="icon" |
|
|
|
> |
|
|
|
</image> |
|
|
|
</view> |
|
|
|
<view class="header-center" |
|
|
|
> |
|
|
|
<text class="title" |
|
|
|
:style="{ paddingTop: safeAreaInsets?.top + 'px' }" |
|
|
|
>DeepMate</text> |
|
|
|
<view class="header-center"> |
|
|
|
<text class="title" :style="{ paddingTop: safeAreaInsets?.top + 'px' }" |
|
|
|
>DeepMate</text |
|
|
|
> |
|
|
|
</view> |
|
|
|
<view class="header-right"> |
|
|
|
<image src="https://d31zlh4on95l9h.cloudfront.net/images/d7c4e74201213a25dd9574e908233928.svg" class="icon"> |
|
|
|
<image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/d7c4e74201213a25dd9574e908233928.svg" |
|
|
|
class="icon" |
|
|
|
> |
|
|
|
</image> |
|
|
|
<image style="margin-left: 10px;" src="https://d31zlh4on95l9h.cloudfront.net/images/099903c4aabf5713488b5cb60815e3f7.svg" class="icon" |
|
|
|
@click="openHistoryDrawer"></image> |
|
|
|
<image |
|
|
|
style="margin-left: 10px" |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/099903c4aabf5713488b5cb60815e3f7.svg" |
|
|
|
class="icon" |
|
|
|
@click="openHistoryDrawer" |
|
|
|
></image> |
|
|
|
<!-- 新增新会话按钮 |
|
|
|
<button class="new-chat-button" @click="newChat"> |
|
|
|
<text class="new-chat-text">新会话</text> |
|
|
|
@ -27,16 +36,22 @@ |
|
|
|
<!-- 主要内容区域 --> |
|
|
|
<view class="main-content"> |
|
|
|
<view class="banner-panel" v-if="messages.length === 0"> |
|
|
|
<image src="https://d31zlh4on95l9h.cloudfront.net/images/42e18bd7fe97d4f4f37aa70439a0990b.svg" |
|
|
|
class="pray-banner"></image> |
|
|
|
<image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/42e18bd7fe97d4f4f37aa70439a0990b.svg" |
|
|
|
class="pray-banner" |
|
|
|
></image> |
|
|
|
<view class="contain"> |
|
|
|
<!-- 机器人头像和欢迎语 --> |
|
|
|
<view class="robot-container" v-if="messages.length === 0"> |
|
|
|
<image src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg" |
|
|
|
class="robot-avatar"></image> |
|
|
|
<image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg" |
|
|
|
class="robot-avatar" |
|
|
|
></image> |
|
|
|
<view class="welcome-message"> |
|
|
|
<text class="greeting">Hi, 我是您的股市随身顾问~</text> |
|
|
|
<text class="description">个股诊断、市场情绪解读,都可以找我。</text> |
|
|
|
<text class="description" |
|
|
|
>个股诊断、市场情绪解读,都可以找我。</text |
|
|
|
> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
@ -51,17 +66,22 @@ |
|
|
|
<view class="recommend-card" v-if="messages.length === 0"> |
|
|
|
<view class="arrow" v-if="messages.length === 0"></view> |
|
|
|
<view class="card-content"> |
|
|
|
<image src="../../static/images/tesla-logo.png" class="logo"></image> |
|
|
|
<image |
|
|
|
src="../../static/images/tesla-logo.png" |
|
|
|
class="logo" |
|
|
|
></image> |
|
|
|
<view class="card-text"> |
|
|
|
<text class="main-question">当前特斯拉该如何布局?</text> |
|
|
|
<text class="stock-code">TSLA</text> |
|
|
|
</view> |
|
|
|
<image src="https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg" |
|
|
|
class="arrow-icon" @click="goBlank"></image> |
|
|
|
<image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/40d94054644f6e3f1c366751f07f0010.svg" |
|
|
|
class="arrow-icon" |
|
|
|
@click="goBlank" |
|
|
|
></image> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
</view> |
|
|
|
<!-- 可能感兴趣的话题 --> |
|
|
|
<!-- <view v-if="messages.length === 0" class="interest-section"> |
|
|
|
@ -80,67 +100,86 @@ |
|
|
|
<!-- 顶部粘性欢迎块:始终保留在聊天上方 --> |
|
|
|
<view class="chat-header" v-if="messages.length > 0"> |
|
|
|
<view class="robot-container"> |
|
|
|
<image src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg" |
|
|
|
class="robot-avatar"></image> |
|
|
|
<view class="welcome-message"> <text class="greeting">Hi, 我是您的股市随身顾问~</text> <text |
|
|
|
class="description">个股诊断、市场情绪解读,都可以找我。</text> |
|
|
|
<image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg" |
|
|
|
class="robot-avatar" |
|
|
|
></image> |
|
|
|
<view class="welcome-message"> |
|
|
|
<text class="greeting">Hi, 我是您的股市随身顾问~</text> |
|
|
|
<text class="description" |
|
|
|
>个股诊断、市场情绪解读,都可以找我。</text |
|
|
|
> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<scroll-view class="chat-container" scroll-y="true" :scroll-top="chatScrollTop" @scroll="onChatScroll" |
|
|
|
v-if="messages.length > 0"> |
|
|
|
<scroll-view |
|
|
|
class="chat-container" |
|
|
|
scroll-y="true" |
|
|
|
:scroll-top="chatScrollTop" |
|
|
|
@scroll="onChatScroll" |
|
|
|
v-if="messages.length > 0" |
|
|
|
> |
|
|
|
<view class="message-list" id="messageList"> |
|
|
|
<view v-for="(message, index) in messages" :key="index" :class="message.isUser ? 'message user-message' : 'message bot-message' |
|
|
|
"> |
|
|
|
<view |
|
|
|
v-for="(message, index) in messages" |
|
|
|
:key="index" |
|
|
|
:class=" |
|
|
|
message.isUser ? 'message user-message' : 'message bot-message' |
|
|
|
" |
|
|
|
> |
|
|
|
<!-- 会话图标 --> |
|
|
|
<text :class="message.isUser |
|
|
|
<text |
|
|
|
:class=" |
|
|
|
message.isUser |
|
|
|
? 'fa-solid fa-user message-icon' |
|
|
|
: 'fa-solid fa-robot message-icon' |
|
|
|
"></text> |
|
|
|
" |
|
|
|
></text> |
|
|
|
<!-- 会话内容 --> |
|
|
|
<view class="message-content"> |
|
|
|
<!-- <text class="message-text">{{ message.content }}</text> --> |
|
|
|
<!-- loading --> |
|
|
|
<view |
|
|
|
class="loading-dots" |
|
|
|
v-if="message.isThinking || !message.isUser" |
|
|
|
> |
|
|
|
<view class="thinking-process"> |
|
|
|
<view class="thinking-header"> |
|
|
|
<view class="thinking-icon">∞</view> |
|
|
|
<view class="thinking-title">{{ |
|
|
|
message.isTyping ? "正在思考" : "思考完成" |
|
|
|
}}</view> |
|
|
|
<view class="thinking-count"> |
|
|
|
|
|
|
|
</view> |
|
|
|
<view |
|
|
|
class="thinking-toggle" |
|
|
|
@click="message.isThinking = !message.isThinking" |
|
|
|
> |
|
|
|
<span v-if="message.isThinking">↑</span> |
|
|
|
<span v-else>↓</span> |
|
|
|
</view> |
|
|
|
<!-- loading --> |
|
|
|
<view |
|
|
|
class="loading-dots" |
|
|
|
v-if="message.isThinking || !message.isUser" |
|
|
|
> |
|
|
|
<view class="thinking-process"> |
|
|
|
<view class="thinking-header"> |
|
|
|
<view class="thinking-icon">∞</view> |
|
|
|
<view class="thinking-title">{{ |
|
|
|
message.isTyping ? "正在思考" : "思考完成" |
|
|
|
}}</view> |
|
|
|
<view class="thinking-count"> </view> |
|
|
|
<view |
|
|
|
class="thinking-toggle" |
|
|
|
@click="message.isThinking = !message.isThinking" |
|
|
|
> |
|
|
|
<span v-if="message.isThinking">↑</span> |
|
|
|
<span v-else>↓</span> |
|
|
|
</view> |
|
|
|
<view v-show="message.isThinking" class="thinking-content"> |
|
|
|
<view class="thinking-item"> |
|
|
|
<view class="item-status"> |
|
|
|
<span class="checkmark">✓</span> |
|
|
|
</view> |
|
|
|
<view class="item-text">问题分析完成</view> |
|
|
|
</view> |
|
|
|
<view v-show="message.isThinking" class="thinking-content"> |
|
|
|
<view class="thinking-item"> |
|
|
|
<view class="item-status"> |
|
|
|
<span class="checkmark">✓</span> |
|
|
|
</view> |
|
|
|
<view class="thinking-item"> |
|
|
|
<view class="item-status"> |
|
|
|
<span class="checkmark">✓</span> |
|
|
|
</view> |
|
|
|
<view class="item-text">收集相关信息</view> |
|
|
|
<view class="item-text">问题分析完成</view> |
|
|
|
</view> |
|
|
|
<view class="thinking-item"> |
|
|
|
<view class="item-status"> |
|
|
|
<span class="checkmark">✓</span> |
|
|
|
</view> |
|
|
|
<view class="item-text">收集相关信息</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<!-- 使用 rich-text 渲染 Markdown 内容 --> |
|
|
|
<rich-text v-if="!message.isUser" class="message-text" |
|
|
|
:nodes="renderMarkdown(message.content)"></rich-text> |
|
|
|
<rich-text |
|
|
|
v-if="!message.isUser" |
|
|
|
class="message-text" |
|
|
|
:nodes="renderMarkdown(message.content)" |
|
|
|
></rich-text> |
|
|
|
<text v-else class="message-text">{{ message.content }}</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
@ -151,8 +190,14 @@ |
|
|
|
<!-- 输入框区域 --> |
|
|
|
<view class="input-area"> |
|
|
|
<view class="input-wrapper"> |
|
|
|
<input type="text" placeholder="请输入股票代码/名称,获取AI洞察" placeholder-style="color:#fff;opacity:1" class="input-field" |
|
|
|
v-model="inputMessage" @confirm="sendMessage" /> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
placeholder="请输入股票代码/名称,获取AI洞察" |
|
|
|
placeholder-style="color:#fff;opacity:1" |
|
|
|
class="input-field" |
|
|
|
v-model="inputMessage" |
|
|
|
@confirm="sendMessage" |
|
|
|
/> |
|
|
|
<image class="send-button" @click="sendMessage" :disabled="isSending"> |
|
|
|
<!-- <image |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/95f1ea2262e9157db13c93c0dc1c5d96.svg" |
|
|
|
@ -160,17 +205,34 @@ |
|
|
|
></image> --> |
|
|
|
</image> |
|
|
|
</view> |
|
|
|
<text class="disclaimer">以上数据由AI生成,不作为最终投资建议,决策需独立!</text> |
|
|
|
<text class="disclaimer" |
|
|
|
>以上数据由AI生成,不作为最终投资建议,决策需独立!</text |
|
|
|
> |
|
|
|
</view> |
|
|
|
|
|
|
|
<image class="back-to-top" src="https://d31zlh4on95l9h.cloudfront.net/images/ba357635d2bb480241952bb1cabacd73.svg" |
|
|
|
:style="{ transform: 'translate3d(' + backTopX + 'px,' + backTopY + 'px,0)' }" @touchstart="onBackTopTouchStart" |
|
|
|
@touchmove="onBackTopTouchMove" @touchend="onBackTopTouchEnd" @click="onBackTopClick"></image> |
|
|
|
<image |
|
|
|
class="back-to-top" |
|
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/ba357635d2bb480241952bb1cabacd73.svg" |
|
|
|
:style="{ |
|
|
|
transform: 'translate3d(' + backTopX + 'px,' + backTopY + 'px,0)', |
|
|
|
}" |
|
|
|
@touchstart="onBackTopTouchStart" |
|
|
|
@touchmove="onBackTopTouchMove" |
|
|
|
@touchend="onBackTopTouchEnd" |
|
|
|
@click="onBackTopClick" |
|
|
|
></image> |
|
|
|
|
|
|
|
<!-- 搜索历史侧拉框 --> |
|
|
|
<view class="drawer-overlay" v-show="showHistoryDrawer"></view> |
|
|
|
<view class="drawer-panel" v-show="showHistoryDrawer" @click.stop :style="{ transform: 'translateX(' + drawerOffsetX + 'px)' }"> |
|
|
|
<view class="drawer-back" @click="onDrawerBackClick"><text class="drawer-back-icon">></text></view> |
|
|
|
<view |
|
|
|
class="drawer-panel" |
|
|
|
v-show="showHistoryDrawer" |
|
|
|
@click.stop |
|
|
|
:style="{ transform: 'translateX(' + drawerOffsetX + 'px)' }" |
|
|
|
> |
|
|
|
<view class="drawer-back" @click="onDrawerBackClick" |
|
|
|
><text class="drawer-back-icon">></text></view |
|
|
|
> |
|
|
|
<view class="drawer-header"> |
|
|
|
<text class="drawer-title">搜索历史</text> |
|
|
|
<!-- <text class="drawer-close" @click="closeHistoryDrawer">×</text> --> |
|
|
|
@ -179,7 +241,11 @@ |
|
|
|
<view v-if="searchHistory.length === 0" class="empty-history"> |
|
|
|
<text>暂无搜索历史</text> |
|
|
|
</view> |
|
|
|
<view v-for="(item, idx) in searchHistory" :key="idx" class="history-card"> |
|
|
|
<view |
|
|
|
v-for="(item, idx) in searchHistory" |
|
|
|
:key="idx" |
|
|
|
class="history-card" |
|
|
|
> |
|
|
|
<text class="history-query">{{ item.query }}</text> |
|
|
|
<text class="history-time">{{ formatTime(item.time) }}</text> |
|
|
|
</view> |
|
|
|
@ -194,10 +260,10 @@ |
|
|
|
const { safeAreaInsets } = uni.getSystemInfoSync(); |
|
|
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; |
|
|
|
import footerBar from '../../components/footerBar-cn' |
|
|
|
import footerBar from "../../components/footerBar-cn"; |
|
|
|
import marked from "marked"; // 引入 marked 库 |
|
|
|
import { onPageScroll } from '@dcloudio/uni-app' |
|
|
|
import {postStock,postIntent} from '../../api/deepMate/deepMate' |
|
|
|
import { onPageScroll } from "@dcloudio/uni-app"; |
|
|
|
import { postStock, postIntent } from "../../api/deepMate/deepMate"; |
|
|
|
// 设置 marked 选项 |
|
|
|
marked.setOptions({ |
|
|
|
renderer: new marked.Renderer(), |
|
|
|
@ -253,7 +319,7 @@ const FAKE_MARKDOWN = `# <font color="#1890ff">Tesla Inc.全景作战报告</fon |
|
|
|
|
|
|
|
--- |
|
|
|
<font color="#8c8c8c">*该内容由AI生成,仅供参考,投资有风险,请注意甄别。*</font>`; |
|
|
|
const type = ref('member') |
|
|
|
const type = ref("member"); |
|
|
|
const inputMessage = ref(""); |
|
|
|
const showThinking = ref(true); |
|
|
|
const isSending = ref(false); |
|
|
|
@ -308,7 +374,10 @@ onMounted(() => { |
|
|
|
// nextTick(startTabsMarquee); |
|
|
|
} |
|
|
|
if (messages.value.length > 0) { |
|
|
|
nextTick(() => { measureChatContainer(); scrollToBottom(); }); |
|
|
|
nextTick(() => { |
|
|
|
measureChatContainer(); |
|
|
|
scrollToBottom(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 载入历史 |
|
|
|
@ -338,7 +407,7 @@ const generateUUID = () => { |
|
|
|
// 计算聊天容器可视高度 |
|
|
|
const measureChatContainer = () => { |
|
|
|
const q = uni.createSelectorQuery(); |
|
|
|
q.select('.chat-container').boundingClientRect(); |
|
|
|
q.select(".chat-container").boundingClientRect(); |
|
|
|
q.exec((res) => { |
|
|
|
chatContainerHeight.value = res[0]?.height || 0; |
|
|
|
}); |
|
|
|
@ -359,8 +428,12 @@ const goBlank = () => { |
|
|
|
}; |
|
|
|
|
|
|
|
// 历史抽屉控制 |
|
|
|
const openHistoryDrawer = () => { showHistoryDrawer.value = true; }; |
|
|
|
const closeHistoryDrawer = () => { showHistoryDrawer.value = false; }; |
|
|
|
const openHistoryDrawer = () => { |
|
|
|
showHistoryDrawer.value = true; |
|
|
|
}; |
|
|
|
const closeHistoryDrawer = () => { |
|
|
|
showHistoryDrawer.value = false; |
|
|
|
}; |
|
|
|
const onDrawerBackClick = () => { |
|
|
|
drawerOffsetX.value = 500; |
|
|
|
setTimeout(() => { |
|
|
|
@ -370,7 +443,7 @@ const onDrawerBackClick = () => { |
|
|
|
}; |
|
|
|
|
|
|
|
// 时间格式化:YYYY-MM-DD HH:mm |
|
|
|
const pad2 = (n) => (n < 10 ? '0' + n : '' + n); |
|
|
|
const pad2 = (n) => (n < 10 ? "0" + n : "" + n); |
|
|
|
const formatTime = (t) => { |
|
|
|
const d = new Date(t); |
|
|
|
const y = d.getFullYear(); |
|
|
|
@ -398,18 +471,20 @@ const sendMessage = () => { |
|
|
|
// 记录搜索历史 |
|
|
|
const entry = { query: userMessage.content, time: Date.now() }; |
|
|
|
searchHistory.value.unshift(entry); |
|
|
|
uni.setStorageSync('search_history', searchHistory.value); |
|
|
|
uni.setStorageSync("search_history", searchHistory.value); |
|
|
|
|
|
|
|
// 发送后强制恢复并滚到底部 |
|
|
|
shouldAutoScroll.value = true; |
|
|
|
nextTick(() => { scrollToBottom(); }); |
|
|
|
nextTick(() => { |
|
|
|
scrollToBottom(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 模拟机器人回复 |
|
|
|
simulateBotResponse(userMessage.content); |
|
|
|
}; |
|
|
|
|
|
|
|
// 模拟机器人回复 |
|
|
|
const simulateBotResponse = async(userMessage) => { |
|
|
|
const simulateBotResponse = async (userMessage) => { |
|
|
|
isSending.value = true; |
|
|
|
|
|
|
|
// 添加机器人加载消息 |
|
|
|
@ -422,38 +497,38 @@ const simulateBotResponse = async(userMessage) => { |
|
|
|
|
|
|
|
messages.value.push(botMsg); |
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000)); |
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000)); |
|
|
|
// 首先进行意图识别 |
|
|
|
const res = await postIntent({ |
|
|
|
content:"森那美", |
|
|
|
content: "森那美", |
|
|
|
language: "cn", |
|
|
|
marketlist: "hk,cn,usa,my,sg,vi,in,gb", |
|
|
|
token: "9ior41AF0xTIbIG2pRnnbZi0+fEeMx8pywnilrmTwo5FbqJ91WrSWOxp9MkpKiNtedtUafqvzIwpFKrwuMs", |
|
|
|
token: |
|
|
|
"9ior41AF0xTIbIG2pRnnbZi0+fEeMx8pywnilrmTwo5FbqJ91WrSWOxp9MkpKiNtedtUafqvzIwpFKrwuMs", |
|
|
|
model: "1", |
|
|
|
}); |
|
|
|
|
|
|
|
console.log("res"+res); |
|
|
|
console.log("res" + res); |
|
|
|
|
|
|
|
// 意图识别不通过 |
|
|
|
if (res.code !== 200) { |
|
|
|
return ; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 获取意图识别结果 |
|
|
|
// 获取意图识别结果 |
|
|
|
const recordId = res.data.recordId; |
|
|
|
const parentId = res.data.parentId; |
|
|
|
const stockId = res.data.stockId; |
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000)); |
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000)); |
|
|
|
// 获取股票信息 |
|
|
|
const StockInfo = await postStock({ |
|
|
|
recordId, |
|
|
|
parentId, |
|
|
|
stockId, |
|
|
|
token: "9ior41AF0xTIbIG2pRnnbZi0+fEeMx8pywnilrmTwo5FbqJ91WrSWOxp9MkpKiNtedtUafqvzIwpFKrwuMs", |
|
|
|
language:'cn' |
|
|
|
token: |
|
|
|
"9ior41AF0xTIbIG2pRnnbZi0+fEeMx8pywnilrmTwo5FbqJ91WrSWOxp9MkpKiNtedtUafqvzIwpFKrwuMs", |
|
|
|
language: "cn", |
|
|
|
}); |
|
|
|
console.log("StockInfo", StockInfo); |
|
|
|
|
|
|
|
@ -464,18 +539,13 @@ const simulateBotResponse = async(userMessage) => { |
|
|
|
const markdown = StockInfo.markdown; |
|
|
|
console.log("StockInfo", StockInfo); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 添加请求延迟 |
|
|
|
// 添加请求延迟 |
|
|
|
// const toDataInfo = await getData(); |
|
|
|
// console.log(toDataInfo); |
|
|
|
// dataInfo.value = toDataInfo.data; |
|
|
|
// console.log(dataInfo.value); |
|
|
|
messages.value[messages.value.length - 1].isThinking = false; |
|
|
|
|
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
nextTick(() => { |
|
|
|
scrollToBottom(); |
|
|
|
@ -486,39 +556,45 @@ const simulateBotResponse = async(userMessage) => { |
|
|
|
let index = 0; |
|
|
|
|
|
|
|
const botIndex = messages.value.length - 1; |
|
|
|
const baseDelay = 165; // 普通字符基础延迟(毫秒) |
|
|
|
const slowPunct = /[。!?!?;;]/; // 句号、感叹号、分号等较长停顿 |
|
|
|
const midPunct = /[,、,::]/; // 逗号、顿号、冒号等中等停顿 |
|
|
|
const baseDelay = 5; // 普通字符基础延迟(毫秒) |
|
|
|
const slowPunct = /[。!?!?;;]/; // 句号、感叹号、分号等较长停顿 |
|
|
|
const midPunct = /[,、,::]/; // 逗号、顿号、冒号等中等停顿 |
|
|
|
|
|
|
|
const typeWriter = () => { |
|
|
|
if (index < responseText.length) { |
|
|
|
const ch = responseText.charAt(index); |
|
|
|
const current = messages.value[botIndex]; |
|
|
|
// 通过数组替换触发渲染,避免部分平台对子项属性变更不响应 |
|
|
|
messages.value.splice( |
|
|
|
botIndex, |
|
|
|
1, |
|
|
|
{ ...current, content: current.content + ch, isTyping: true } |
|
|
|
); |
|
|
|
messages.value.splice(botIndex, 1, { |
|
|
|
...current, |
|
|
|
content: current.content + ch, |
|
|
|
isTyping: true, |
|
|
|
}); |
|
|
|
index++; |
|
|
|
scrollToBottom(); |
|
|
|
|
|
|
|
const delay = slowPunct.test(ch) ? 220 : midPunct.test(ch) ? 120 : baseDelay; |
|
|
|
const delay = slowPunct.test(ch) |
|
|
|
? 220 |
|
|
|
: midPunct.test(ch) |
|
|
|
? 120 |
|
|
|
: baseDelay; |
|
|
|
setTimeout(typeWriter, delay); |
|
|
|
} else { |
|
|
|
const current = messages.value[botIndex]; |
|
|
|
messages.value.splice( |
|
|
|
botIndex, |
|
|
|
1, |
|
|
|
{ ...current, isTyping: false } |
|
|
|
); |
|
|
|
messages.value.splice(botIndex, 1, { ...current, isTyping: false }); |
|
|
|
isSending.value = false; |
|
|
|
nextTick(() => { scrollToBottom(); }); |
|
|
|
nextTick(() => { |
|
|
|
scrollToBottom(); |
|
|
|
}); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 启动前稍作停顿,避免过快开始 |
|
|
|
setTimeout(typeWriter, 500); |
|
|
|
|
|
|
|
|
|
|
|
console.log("messages", messages); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
// 当消息出现或变化时,测量容器并滚到底部 |
|
|
|
@ -535,7 +611,7 @@ watch(messages, (arr) => { |
|
|
|
const scrollToBottom = () => { |
|
|
|
if (!shouldAutoScroll.value) return; |
|
|
|
const query = uni.createSelectorQuery(); |
|
|
|
query.select('#messageList').boundingClientRect(); |
|
|
|
query.select("#messageList").boundingClientRect(); |
|
|
|
query.exec((res) => { |
|
|
|
if (res[0]) { |
|
|
|
latestContentHeight.value = res[0].height; |
|
|
|
@ -563,7 +639,8 @@ const onChatScroll = (e) => { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const distanceToBottom = latestContentHeight.value - st - chatContainerHeight.value; |
|
|
|
const distanceToBottom = |
|
|
|
latestContentHeight.value - st - chatContainerHeight.value; |
|
|
|
if (distanceToBottom <= AUTO_SCROLL_REENABLE_THRESHOLD) { |
|
|
|
shouldAutoScroll.value = true; |
|
|
|
} |
|
|
|
@ -579,8 +656,14 @@ const backTopTargetY = ref(0); |
|
|
|
let backTopRAF = 0; |
|
|
|
const backTopSmoothing = 0.15; // 越大越跟手,越小越顺滑 |
|
|
|
const backTopEpsilon = 0.5; // 收敛阈值 |
|
|
|
const raf = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : (fn) => setTimeout(fn, 16); |
|
|
|
const caf = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : (id) => clearTimeout(id); |
|
|
|
const raf = |
|
|
|
typeof requestAnimationFrame === "function" |
|
|
|
? requestAnimationFrame |
|
|
|
: (fn) => setTimeout(fn, 16); |
|
|
|
const caf = |
|
|
|
typeof cancelAnimationFrame === "function" |
|
|
|
? cancelAnimationFrame |
|
|
|
: (id) => clearTimeout(id); |
|
|
|
|
|
|
|
function stepBackTop() { |
|
|
|
const dx = backTopTargetX.value - backTopX.value; |
|
|
|
@ -624,7 +707,10 @@ const onBackTopTouchMove = (e) => { |
|
|
|
const ny = t.pageY - backTopDragOffset.value.y; |
|
|
|
const clampedX = clamp(nx, 0, sys.windowWidth - iconSize); |
|
|
|
const clampedY = clamp(ny, 0, sys.windowHeight - reserveBottom - iconSize); |
|
|
|
if (Math.abs(clampedX - backTopX.value) + Math.abs(clampedY - backTopY.value) > 3) { |
|
|
|
if ( |
|
|
|
Math.abs(clampedX - backTopX.value) + Math.abs(clampedY - backTopY.value) > |
|
|
|
3 |
|
|
|
) { |
|
|
|
backTopDragging.value = true; |
|
|
|
} |
|
|
|
backTopTargetX.value = clampedX; |
|
|
|
@ -640,8 +726,6 @@ const onBackTopClick = () => { |
|
|
|
if (backTopDragging.value) return; // 拖拽时不触发点击回到顶部 |
|
|
|
scrollToTop(); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
@ -916,9 +1000,9 @@ const onBackTopClick = () => { |
|
|
|
margin-bottom: 30rpx; |
|
|
|
} |
|
|
|
|
|
|
|
/* .user-message { |
|
|
|
.user-message { |
|
|
|
flex-direction: row-reverse; |
|
|
|
} */ |
|
|
|
} |
|
|
|
|
|
|
|
.message-icon { |
|
|
|
font-size: 24rpx; |
|
|
|
@ -934,8 +1018,8 @@ const onBackTopClick = () => { |
|
|
|
} |
|
|
|
|
|
|
|
.user-message .message-icon { |
|
|
|
background-color: transparent; |
|
|
|
border-radius: 0; |
|
|
|
background-color: #007aff; |
|
|
|
border-radius: 40rpx; |
|
|
|
/* padding: 0; |
|
|
|
width: auto; |
|
|
|
height: auto; */ |
|
|
|
@ -943,7 +1027,7 @@ const onBackTopClick = () => { |
|
|
|
} |
|
|
|
|
|
|
|
.bot-message .message-icon { |
|
|
|
background: url('https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg'); |
|
|
|
background: url("https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg"); |
|
|
|
|
|
|
|
color: white; |
|
|
|
} |
|
|
|
@ -1006,7 +1090,6 @@ const onBackTopClick = () => { |
|
|
|
} |
|
|
|
|
|
|
|
@keyframes loading { |
|
|
|
|
|
|
|
0%, |
|
|
|
80%, |
|
|
|
100% { |
|
|
|
@ -1119,7 +1202,6 @@ const onBackTopClick = () => { |
|
|
|
/* 在灰底之上、内容之下 */ |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.contain { |
|
|
|
margin: 0 20rpx; |
|
|
|
gap: 5rpx; |
|
|
|
@ -1185,7 +1267,7 @@ const onBackTopClick = () => { |
|
|
|
height: 28px; |
|
|
|
border-radius: 14px; |
|
|
|
background: #fff; |
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.15); |
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); |
|
|
|
border: 1px solid #eee; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
@ -1233,9 +1315,18 @@ const onBackTopClick = () => { |
|
|
|
.history-query { |
|
|
|
font-size: 28rpx; |
|
|
|
color: #333333; |
|
|
|
font-weight: 500; } |
|
|
|
.history-time { margin-top: 8rpx; font-size: 22rpx; color: #888888; } |
|
|
|
.empty-history { padding: 40rpx; color: #999999; text-align: center; } |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
.history-time { |
|
|
|
margin-top: 8rpx; |
|
|
|
font-size: 22rpx; |
|
|
|
color: #888888; |
|
|
|
} |
|
|
|
.empty-history { |
|
|
|
padding: 40rpx; |
|
|
|
color: #999999; |
|
|
|
text-align: center; |
|
|
|
} |
|
|
|
.thinking-process { |
|
|
|
margin: 10rpx 0; |
|
|
|
border: 2rpx solid #e5e5e5; |
|
|
|
|