|
|
@ -77,12 +77,10 @@ |
|
|
</view> --> |
|
|
</view> --> |
|
|
|
|
|
|
|
|
<!-- 聊天区域 --> |
|
|
<!-- 聊天区域 --> |
|
|
<view class="robot-container" v-if="messages.length > 0"> |
|
|
|
|
|
<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 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> |
|
|
|
|
|
</view> </view> |
|
|
</view> |
|
|
</view> |
|
|
<view class="chat-container" v-if="messages.length > 0"> |
|
|
<view class="chat-container" v-if="messages.length > 0"> |
|
|
<view class="message-list" id="messageList"> |
|
|
<view class="message-list" id="messageList"> |
|
|
@ -145,7 +143,11 @@ |
|
|
<image |
|
|
<image |
|
|
class="back-to-top" |
|
|
class="back-to-top" |
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/ba357635d2bb480241952bb1cabacd73.svg" |
|
|
src="https://d31zlh4on95l9h.cloudfront.net/images/ba357635d2bb480241952bb1cabacd73.svg" |
|
|
@click="scrollToTop" |
|
|
|
|
|
|
|
|
:style="{ transform: 'translate3d(' + backTopX + 'px,' + backTopY + 'px,0)' }" |
|
|
|
|
|
@touchstart="onBackTopTouchStart" |
|
|
|
|
|
@touchmove="onBackTopTouchMove" |
|
|
|
|
|
@touchend="onBackTopTouchEnd" |
|
|
|
|
|
@click="onBackTopClick" |
|
|
></image> |
|
|
></image> |
|
|
<footerBar class="static-footer" :type="type"></footerBar> |
|
|
<footerBar class="static-footer" :type="type"></footerBar> |
|
|
</view> |
|
|
</view> |
|
|
@ -156,6 +158,7 @@ const { safeAreaInsets } = uni.getSystemInfoSync(); |
|
|
|
|
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; |
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; |
|
|
import footerBar from '../../components/footerBar-cn' |
|
|
import footerBar from '../../components/footerBar-cn' |
|
|
|
|
|
import { onPageScroll } from '@dcloudio/uni-app' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const type = ref('member') |
|
|
const type = ref('member') |
|
|
@ -185,60 +188,23 @@ const hotTopics = ref([ |
|
|
icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg", |
|
|
icon: "https://d31zlh4on95l9h.cloudfront.net/images/7ed58be0f4b81aeb398d9ba2534a624b.svg", |
|
|
}, |
|
|
}, |
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
// 新增:tabs 列表用于渲染与轮换 |
|
|
|
|
|
const tabsList = ref(['个股诊断', '市场情绪温度计', '买卖时机提示', '个股']); |
|
|
|
|
|
|
|
|
|
|
|
// 用于无缝滚动的引用与状态(兼容小程序/App) |
|
|
|
|
|
const tabsTrack = ref(null); |
|
|
|
|
|
const marqueeOffset = ref(0); |
|
|
|
|
|
let tabsTickId = 0; |
|
|
|
|
|
const marqueeSpeed = 0.6; // 像素/帧 |
|
|
|
|
|
let currentThreshold = 0; // 当前第一个标签的宽度(含右外边距) |
|
|
|
|
|
|
|
|
|
|
|
// 统一 raf/caf(小程序端可能没有 rAF) |
|
|
// 统一 raf/caf(小程序端可能没有 rAF) |
|
|
const hasRAF = typeof requestAnimationFrame === 'function'; |
|
|
|
|
|
const startFrame = (fn) => hasRAF ? requestAnimationFrame(fn) : setTimeout(fn, 16); |
|
|
|
|
|
const stopFrame = (id) => hasRAF ? cancelAnimationFrame(id) : clearTimeout(id); |
|
|
|
|
|
|
|
|
|
|
|
const measureFirstThreshold = (cb) => { |
|
|
|
|
|
if (!tabsList.value.length) { currentThreshold = 0; if (cb) cb(0); return; } |
|
|
|
|
|
const q = uni.createSelectorQuery(); |
|
|
|
|
|
q.selectAll('.function-tabs .tab-item').boundingClientRect(); |
|
|
|
|
|
q.exec(res => { |
|
|
|
|
|
if (res && res[0] && res[0].length) { |
|
|
|
|
|
const firstRect = res[0][0]; |
|
|
|
|
|
const marginRightPx = uni.upx2px(20); |
|
|
|
|
|
currentThreshold = (firstRect?.width || 0) + marginRightPx; |
|
|
|
|
|
if (typeof cb === 'function') cb(currentThreshold); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 若暂时无法获取,稍后重试一次 |
|
|
|
|
|
setTimeout(() => measureFirstThreshold(cb), 16); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const startTabsMarquee = () => { |
|
|
|
|
|
stopFrame(tabsTickId); |
|
|
|
|
|
if (tabsList.value.length <= 1) return; // 单个标签不滚动 |
|
|
|
|
|
marqueeOffset.value = 0; |
|
|
|
|
|
measureFirstThreshold(() => { |
|
|
|
|
|
const step = () => { |
|
|
|
|
|
marqueeOffset.value += marqueeSpeed; |
|
|
|
|
|
if (currentThreshold > 0 && marqueeOffset.value >= currentThreshold) { |
|
|
|
|
|
const first = tabsList.value.shift(); |
|
|
|
|
|
if (first !== undefined) tabsList.value.push(first); |
|
|
|
|
|
marqueeOffset.value -= currentThreshold; |
|
|
|
|
|
measureFirstThreshold(); |
|
|
|
|
|
} |
|
|
|
|
|
tabsTickId = startFrame(step); |
|
|
|
|
|
}; |
|
|
|
|
|
nextTick(step); |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// const hasRAF = typeof requestAnimationFrame === 'function'; |
|
|
|
|
|
// const startFrame = (fn) => hasRAF ? requestAnimationFrame(fn) : setTimeout(fn, 16); |
|
|
|
|
|
// const stopFrame = (id) => hasRAF ? cancelAnimationFrame(id) : clearTimeout(id); |
|
|
|
|
|
|
|
|
// 初始化 |
|
|
// 初始化 |
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
|
|
|
const sys = uni.getSystemInfoSync(); |
|
|
|
|
|
const iconSize = uni.upx2px(100); // 和样式保持一致 |
|
|
|
|
|
const reserveBottom = uni.upx2px(260); // 预留底部输入区域空间 |
|
|
|
|
|
const initX = Math.max(0, sys.windowWidth - iconSize - uni.upx2px(30)); |
|
|
|
|
|
const initY = Math.max(0, Math.floor(sys.windowHeight * 0.65) - iconSize); |
|
|
|
|
|
backTopTargetX.value = initX; |
|
|
|
|
|
backTopTargetY.value = initY; |
|
|
|
|
|
backTopX.value = initX; |
|
|
|
|
|
backTopY.value = initY; |
|
|
|
|
|
|
|
|
initUUID(); |
|
|
initUUID(); |
|
|
if (messages.value.length === 0) { |
|
|
if (messages.value.length === 0) { |
|
|
nextTick(startTabsMarquee); |
|
|
nextTick(startTabsMarquee); |
|
|
@ -248,17 +214,6 @@ onMounted(() => { |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// 当消息数量变化,控制滚动是否运行 |
|
|
|
|
|
watch(messages, (val) => { |
|
|
|
|
|
if (val.length === 0) { |
|
|
|
|
|
nextTick(startTabsMarquee); |
|
|
|
|
|
} else { |
|
|
|
|
|
stopFrame(tabsTickId); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => { stopFrame(tabsTickId); }); |
|
|
|
|
|
|
|
|
|
|
|
// 初始化 UUID |
|
|
// 初始化 UUID |
|
|
const initUUID = () => { |
|
|
const initUUID = () => { |
|
|
let storedUUID = uni.getStorageSync("user_uuid"); |
|
|
let storedUUID = uni.getStorageSync("user_uuid"); |
|
|
@ -306,33 +261,9 @@ const sendMessage = () => { |
|
|
messages.value.push(userMessage); |
|
|
messages.value.push(userMessage); |
|
|
inputMessage.value = ""; |
|
|
inputMessage.value = ""; |
|
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 模拟机器人回复 |
|
|
|
|
|
simulateBotResponse(userMessage.content); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 发送消息 |
|
|
|
|
|
const sendMessageList = (listMessage) => { |
|
|
|
|
|
console.log(listMessage); |
|
|
|
|
|
|
|
|
|
|
|
const userMessage = { |
|
|
|
|
|
content: listMessage, |
|
|
|
|
|
isUser: true, |
|
|
|
|
|
isThinking: false, |
|
|
|
|
|
isTyping: false, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
messages.value.push(userMessage); |
|
|
|
|
|
inputMessage.value = ""; |
|
|
|
|
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
|
|
nextTick(() => { |
|
|
|
|
|
scrollToBottom(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
// 发送后强制恢复并滚到底部 |
|
|
|
|
|
shouldAutoScroll.value = true; |
|
|
|
|
|
nextTick(() => { forceScrollToBottom(); }); |
|
|
|
|
|
|
|
|
// 模拟机器人回复 |
|
|
// 模拟机器人回复 |
|
|
simulateBotResponse(userMessage.content); |
|
|
simulateBotResponse(userMessage.content); |
|
|
@ -358,45 +289,156 @@ const simulateBotResponse = (userMessage) => { |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// 模拟流式响应 |
|
|
// 模拟流式响应 |
|
|
let responseText = `我已经收到您的消息: "${userMessage}"。作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?`; |
|
|
|
|
|
|
|
|
let responseText = `我已经收到您的消息: "${userMessage}"。作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?`; |
|
|
let index = 0; |
|
|
let index = 0; |
|
|
|
|
|
|
|
|
|
|
|
const botIndex = messages.value.length - 1; |
|
|
|
|
|
const baseDelay = 165; // 普通字符基础延迟(毫秒) |
|
|
|
|
|
const slowPunct = /[。!?!?;;]/; // 句号、感叹号、分号等较长停顿 |
|
|
|
|
|
const midPunct = /[,、,::]/; // 逗号、顿号、冒号等中等停顿 |
|
|
|
|
|
|
|
|
const typeWriter = () => { |
|
|
const typeWriter = () => { |
|
|
if (index < responseText.length) { |
|
|
if (index < responseText.length) { |
|
|
botMsg.content += responseText.charAt(index); |
|
|
|
|
|
|
|
|
const ch = responseText.charAt(index); |
|
|
|
|
|
const current = messages.value[botIndex]; |
|
|
|
|
|
// 通过数组替换触发渲染,避免部分平台对子项属性变更不响应 |
|
|
|
|
|
messages.value.splice( |
|
|
|
|
|
botIndex, |
|
|
|
|
|
1, |
|
|
|
|
|
{ ...current, content: current.content + ch, isTyping: true } |
|
|
|
|
|
); |
|
|
index++; |
|
|
index++; |
|
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
|
|
scrollToBottom(); |
|
|
scrollToBottom(); |
|
|
|
|
|
|
|
|
setTimeout(typeWriter, 30); |
|
|
|
|
|
|
|
|
const delay = slowPunct.test(ch) ? 220 : midPunct.test(ch) ? 120 : baseDelay; |
|
|
|
|
|
setTimeout(typeWriter, delay); |
|
|
} else { |
|
|
} else { |
|
|
botMsg.isTyping = false; |
|
|
|
|
|
|
|
|
const current = messages.value[botIndex]; |
|
|
|
|
|
messages.value.splice( |
|
|
|
|
|
botIndex, |
|
|
|
|
|
1, |
|
|
|
|
|
{ ...current, isTyping: false } |
|
|
|
|
|
); |
|
|
isSending.value = false; |
|
|
isSending.value = false; |
|
|
|
|
|
nextTick(() => { scrollToBottom(); }); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 启动前稍作停顿,避免过快开始 |
|
|
setTimeout(typeWriter, 500); |
|
|
setTimeout(typeWriter, 500); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
// 滚动到底部 |
|
|
const scrollToBottom = () => { |
|
|
const scrollToBottom = () => { |
|
|
|
|
|
if (!shouldAutoScroll.value) return; // 暂停自动滚动 |
|
|
const query = uni.createSelectorQuery(); |
|
|
const query = uni.createSelectorQuery(); |
|
|
query.select("#messageList").boundingClientRect(); |
|
|
query.select("#messageList").boundingClientRect(); |
|
|
query.selectViewport().scrollOffset(); |
|
|
query.selectViewport().scrollOffset(); |
|
|
|
|
|
|
|
|
query.exec((res) => { |
|
|
query.exec((res) => { |
|
|
if (res[0] && res[1]) { |
|
|
if (res[0] && res[1]) { |
|
|
uni.pageScrollTo({ |
|
|
|
|
|
scrollTop: res[0].height, |
|
|
|
|
|
duration: 100, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
latestContentHeight.value = res[0].height; |
|
|
|
|
|
uni.pageScrollTo({ scrollTop: res[0].height, duration: 100 }); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
const scrollToTop = () => { |
|
|
const scrollToTop = () => { |
|
|
uni.pageScrollTo({ scrollTop: 0, duration: 200 }); |
|
|
uni.pageScrollTo({ scrollTop: 0, duration: 200 }); |
|
|
}; |
|
|
}; |
|
|
|
|
|
// 自动滚动控制:用户向上滚动时暂停自动滚到底部 |
|
|
|
|
|
const shouldAutoScroll = ref(true); |
|
|
|
|
|
const latestContentHeight = ref(0); |
|
|
|
|
|
const lastScrollTop = ref(0); |
|
|
|
|
|
const windowHeight = uni.getSystemInfoSync().windowHeight; |
|
|
|
|
|
const AUTO_SCROLL_REENABLE_THRESHOLD = 1; // px,接近底部时恢复自动滚动 |
|
|
|
|
|
|
|
|
|
|
|
onPageScroll((e) => { |
|
|
|
|
|
const st = e.scrollTop; |
|
|
|
|
|
const delta = st - lastScrollTop.value; |
|
|
|
|
|
lastScrollTop.value = st; |
|
|
|
|
|
|
|
|
|
|
|
if (delta < 0) { |
|
|
|
|
|
shouldAutoScroll.value = false; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const distanceToBottom = latestContentHeight.value - st - windowHeight; |
|
|
|
|
|
if (distanceToBottom <= AUTO_SCROLL_REENABLE_THRESHOLD) { |
|
|
|
|
|
shouldAutoScroll.value = true; |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 回到顶部图标拖拽状态 |
|
|
|
|
|
const backTopX = ref(0); |
|
|
|
|
|
const backTopY = ref(0); |
|
|
|
|
|
const backTopDragging = ref(false); |
|
|
|
|
|
const backTopDragOffset = ref({ x: 0, y: 0 }); |
|
|
|
|
|
const backTopTargetX = ref(0); |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
function stepBackTop() { |
|
|
|
|
|
const dx = backTopTargetX.value - backTopX.value; |
|
|
|
|
|
const dy = backTopTargetY.value - backTopY.value; |
|
|
|
|
|
// 插值缓动,避免每帧重排 |
|
|
|
|
|
backTopX.value += dx * backTopSmoothing; |
|
|
|
|
|
backTopY.value += dy * backTopSmoothing; |
|
|
|
|
|
if (Math.abs(dx) > backTopEpsilon || Math.abs(dy) > backTopEpsilon) { |
|
|
|
|
|
backTopRAF = raf(stepBackTop); |
|
|
|
|
|
} else { |
|
|
|
|
|
backTopX.value = backTopTargetX.value; |
|
|
|
|
|
backTopY.value = backTopTargetY.value; |
|
|
|
|
|
backTopRAF = 0; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
function ensureBackTopRAF() { |
|
|
|
|
|
if (!backTopRAF) { |
|
|
|
|
|
backTopRAF = raf(stepBackTop); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const clamp = (val, min, max) => Math.max(min, Math.min(val, max)); |
|
|
|
|
|
|
|
|
|
|
|
const onBackTopTouchStart = (e) => { |
|
|
|
|
|
const t = e.touches && e.touches[0]; |
|
|
|
|
|
if (!t) return; |
|
|
|
|
|
backTopDragging.value = false; |
|
|
|
|
|
backTopDragOffset.value = { |
|
|
|
|
|
x: t.pageX - backTopX.value, |
|
|
|
|
|
y: t.pageY - backTopY.value, |
|
|
|
|
|
}; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onBackTopTouchMove = (e) => { |
|
|
|
|
|
const t = e.touches && e.touches[0]; |
|
|
|
|
|
if (!t) return; |
|
|
|
|
|
const sys = uni.getSystemInfoSync(); |
|
|
|
|
|
const iconSize = uni.upx2px(100); |
|
|
|
|
|
const reserveBottom = uni.upx2px(260); |
|
|
|
|
|
const nx = t.pageX - backTopDragOffset.value.x; |
|
|
|
|
|
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) { |
|
|
|
|
|
backTopDragging.value = true; |
|
|
|
|
|
} |
|
|
|
|
|
backTopTargetX.value = clampedX; |
|
|
|
|
|
backTopTargetY.value = clampedY; |
|
|
|
|
|
ensureBackTopRAF(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onBackTopTouchEnd = () => { |
|
|
|
|
|
// 结束拖拽即可 |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onBackTopClick = () => { |
|
|
|
|
|
if (backTopDragging.value) return; // 拖拽时不触发点击回到顶部 |
|
|
|
|
|
scrollToTop(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
<style scoped> |
|
|
<style scoped> |
|
|
@ -450,13 +492,23 @@ const scrollToTop = () => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.main-content { |
|
|
.main-content { |
|
|
|
|
|
background-color: #fff; |
|
|
flex: 1; |
|
|
flex: 1; |
|
|
padding: 20rpx; |
|
|
padding: 20rpx; |
|
|
overflow-y: auto; |
|
|
|
|
|
|
|
|
/* overflow-y: auto; 取消独立滚动,使用页面滚动以便自动到达底部 */ |
|
|
margin-top: 20rpx; |
|
|
margin-top: 20rpx; |
|
|
margin-bottom: 120rpx; |
|
|
margin-bottom: 120rpx; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* 聊天顶部粘性欢迎块样式 */ |
|
|
|
|
|
.chat-header { |
|
|
|
|
|
position: sticky; |
|
|
|
|
|
top: 0; /* 在页面滚动时始终贴顶 */ |
|
|
|
|
|
z-index: 50; |
|
|
|
|
|
background-color: #ffffff; |
|
|
|
|
|
padding: 3rpx 20rpx; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
.robot-container { |
|
|
.robot-container { |
|
|
display: flex; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
@ -633,6 +685,8 @@ const scrollToTop = () => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.message-list { |
|
|
.message-list { |
|
|
|
|
|
background-color: #fff; |
|
|
|
|
|
margin-bottom: 400rpx; |
|
|
/* padding: 20rpx; */ |
|
|
/* padding: 20rpx; */ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -642,9 +696,9 @@ const scrollToTop = () => { |
|
|
margin-bottom: 30rpx; |
|
|
margin-bottom: 30rpx; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.user-message { |
|
|
|
|
|
|
|
|
/* .user-message { |
|
|
flex-direction: row-reverse; |
|
|
flex-direction: row-reverse; |
|
|
} |
|
|
|
|
|
|
|
|
} */ |
|
|
|
|
|
|
|
|
.message-icon { |
|
|
.message-icon { |
|
|
font-size: 24rpx; |
|
|
font-size: 24rpx; |
|
|
@ -660,12 +714,17 @@ const scrollToTop = () => { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.user-message .message-icon { |
|
|
.user-message .message-icon { |
|
|
background-color: #007aff; |
|
|
|
|
|
color: white; |
|
|
|
|
|
|
|
|
background-color: transparent; |
|
|
|
|
|
border-radius: 0; |
|
|
|
|
|
/* padding: 0; |
|
|
|
|
|
width: auto; |
|
|
|
|
|
height: auto; */ |
|
|
|
|
|
color: #333; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.bot-message .message-icon { |
|
|
.bot-message .message-icon { |
|
|
background-color: #34c759; |
|
|
|
|
|
|
|
|
background: url('https://d31zlh4on95l9h.cloudfront.net/images/61fa384381c88ad80be28f41827fe0e5.svg'); |
|
|
|
|
|
|
|
|
color: white; |
|
|
color: white; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -739,6 +798,7 @@ const scrollToTop = () => { |
|
|
|
|
|
|
|
|
.input-area { |
|
|
.input-area { |
|
|
position: fixed; |
|
|
position: fixed; |
|
|
|
|
|
margin-top: 20rpx; |
|
|
bottom: 70rpx; |
|
|
bottom: 70rpx; |
|
|
left: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
right: 0; |
|
|
@ -841,8 +901,8 @@ const scrollToTop = () => { |
|
|
} |
|
|
} |
|
|
.back-to-top { |
|
|
.back-to-top { |
|
|
position: fixed; |
|
|
position: fixed; |
|
|
right: 30rpx; |
|
|
|
|
|
bottom: 35%; |
|
|
|
|
|
|
|
|
left: 0; |
|
|
|
|
|
top: 0; |
|
|
width: 100rpx; |
|
|
width: 100rpx; |
|
|
height: 100rpx; |
|
|
height: 100rpx; |
|
|
z-index: 1000; |
|
|
z-index: 1000; |
|
|
|