diff --git a/pages/deepMate/deepMate.vue b/pages/deepMate/deepMate.vue
index abeef37..aea697c 100644
--- a/pages/deepMate/deepMate.vue
+++ b/pages/deepMate/deepMate.vue
@@ -77,13 +77,11 @@
-->
-
-
-
- Hi, 我是您的股市随身顾问~
- 个股诊断、市场情绪解读,都可以找我。
-
-
+
+
@@ -156,6 +158,7 @@ const { safeAreaInsets } = uni.getSystemInfoSync();
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import footerBar from '../../components/footerBar-cn'
+import { onPageScroll } from '@dcloudio/uni-app'
const type = ref('member')
@@ -185,60 +188,23 @@ const hotTopics = ref([
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)
-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(() => {
+ 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();
if (messages.value.length === 0) {
nextTick(startTabsMarquee);
@@ -248,17 +214,6 @@ onMounted(() => {
}
});
-// 当消息数量变化,控制滚动是否运行
-watch(messages, (val) => {
- if (val.length === 0) {
- nextTick(startTabsMarquee);
- } else {
- stopFrame(tabsTickId);
- }
-});
-
-onUnmounted(() => { stopFrame(tabsTickId); });
-
// 初始化 UUID
const initUUID = () => {
let storedUUID = uni.getStorageSync("user_uuid");
@@ -306,33 +261,9 @@ const sendMessage = () => {
messages.value.push(userMessage);
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);
@@ -358,45 +289,156 @@ const simulateBotResponse = (userMessage) => {
});
// 模拟流式响应
- let responseText = `我已经收到您的消息: "${userMessage}"。作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?`;
+ let responseText = `我已经收到您的消息: "${userMessage}"。作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?作为您的股市顾问,我可以为您提供专业的投资建议。请问您想了解哪方面的信息?`;
let index = 0;
+ const botIndex = messages.value.length - 1;
+ const baseDelay = 165; // 普通字符基础延迟(毫秒)
+ const slowPunct = /[。!?!?;;]/; // 句号、感叹号、分号等较长停顿
+ const midPunct = /[,、,::]/; // 逗号、顿号、冒号等中等停顿
+
const typeWriter = () => {
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++;
-
- // 滚动到底部
scrollToBottom();
- setTimeout(typeWriter, 30);
+ const delay = slowPunct.test(ch) ? 220 : midPunct.test(ch) ? 120 : baseDelay;
+ setTimeout(typeWriter, delay);
} else {
- botMsg.isTyping = false;
+ const current = messages.value[botIndex];
+ messages.value.splice(
+ botIndex,
+ 1,
+ { ...current, isTyping: false }
+ );
isSending.value = false;
+ nextTick(() => { scrollToBottom(); });
}
};
+ // 启动前稍作停顿,避免过快开始
setTimeout(typeWriter, 500);
};
// 滚动到底部
const scrollToBottom = () => {
+ if (!shouldAutoScroll.value) return; // 暂停自动滚动
const query = uni.createSelectorQuery();
query.select("#messageList").boundingClientRect();
query.selectViewport().scrollOffset();
-
query.exec((res) => {
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 = () => {
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();
+};
+