Browse Source

完善聊天页面的功能

wangyi/feature-20251022162725-启动页登录注册
wangyetao 1 month ago
parent
commit
437597a706
  1. 292
      pages/deepMate/deepMate.vue

292
pages/deepMate/deepMate.vue

@ -77,13 +77,11 @@
</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>
<!-- 顶部粘性欢迎块始终保留在聊天上方 -->
<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 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">
<view <view
@ -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;
} }
@ -681,7 +740,7 @@ const scrollToTop = () => {
} }
.bot-message .message-content { .bot-message .message-content {
background-color: #f0f0f0;
background-color: #f0f0f0;
border-radius: 10rpx; border-radius: 10rpx;
padding: 15rpx; padding: 15rpx;
} }
@ -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;

Loading…
Cancel
Save