|
|
|
@ -33,11 +33,10 @@ |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 功能标签栏 --> |
|
|
|
<view class="function-tabs" v-if="messages.length === 0" scroll-x="true" show-scrollbar="false"> |
|
|
|
<view class="tab-item ">个股诊断</view> |
|
|
|
<view class="tab-item">市场情绪温度计</view> |
|
|
|
<view class="tab-item">买卖时机提示</view> |
|
|
|
<view class="tab-item">个股</view> |
|
|
|
<view class="function-tabs" v-if="messages.length === 0"> |
|
|
|
<view ref="tabsTrack" class="tabs-track" :style="{ transform: 'translate3d(-' + marqueeOffset + 'px,0,0)' }"> |
|
|
|
<view class="tab-item" v-for="(tab, idx) in marqueeList" :key="idx">{{ tab }}</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 特斯拉推荐卡片 --> |
|
|
|
@ -135,7 +134,7 @@ |
|
|
|
<script setup> |
|
|
|
const { safeAreaInsets } = uni.getSystemInfoSync(); |
|
|
|
|
|
|
|
import { ref, onMounted, nextTick } from "vue"; |
|
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; |
|
|
|
|
|
|
|
const inputMessage = ref(""); |
|
|
|
const isSending = ref(false); |
|
|
|
@ -164,17 +163,96 @@ const hotTopics = ref([ |
|
|
|
} |
|
|
|
]); |
|
|
|
|
|
|
|
// 新增:tabs 列表用于渲染与轮换 |
|
|
|
const tabsList = ref(['个股诊断', '市场情绪温度计', '买卖时机提示', '个股']); |
|
|
|
const marqueeList = computed(() => tabsList.value.concat(tabsList.value)); |
|
|
|
|
|
|
|
// 用于无缝滚动的引用与状态(兼容小程序/App) |
|
|
|
const tabsTrack = ref(null); |
|
|
|
const marqueeOffset = ref(0); |
|
|
|
let tabsTickId = 0; |
|
|
|
const marqueeSpeed = 0.6; // 像素/帧 |
|
|
|
let currentThreshold = 0; // 当前第一个标签的宽度(含右外边距) |
|
|
|
let contentWidthHalf = 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); |
|
|
|
|
|
|
|
// 替换测量:测量双列表轨道总宽度的一半,避免频繁 DOM 重排 |
|
|
|
const measureContentWidth = (cb) => { |
|
|
|
const q = uni.createSelectorQuery(); |
|
|
|
q.select('.function-tabs .tabs-track').boundingClientRect(); |
|
|
|
q.exec(res => { |
|
|
|
const rect = res && res[0]; |
|
|
|
if (rect && rect.width) { |
|
|
|
contentWidthHalf = rect.width / 2; |
|
|
|
if (typeof cb === 'function') cb(contentWidthHalf); |
|
|
|
} else { |
|
|
|
setTimeout(() => measureContentWidth(cb), 16); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 原 measureFirstThreshold 保留,但不再用于轮播 |
|
|
|
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; |
|
|
|
nextTick(() => { |
|
|
|
measureContentWidth(() => { |
|
|
|
const step = () => { |
|
|
|
marqueeOffset.value += marqueeSpeed; |
|
|
|
if (contentWidthHalf > 0 && marqueeOffset.value >= contentWidthHalf) { |
|
|
|
marqueeOffset.value = 0; // 重置为 0,实现无缝循环 |
|
|
|
} |
|
|
|
tabsTickId = startFrame(step); |
|
|
|
}; |
|
|
|
step(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 初始化 |
|
|
|
onMounted(() => { |
|
|
|
initUUID(); |
|
|
|
// 如果有历史消息,滚动到底部 |
|
|
|
if (messages.value.length === 0) { |
|
|
|
nextTick(startTabsMarquee); |
|
|
|
} |
|
|
|
if (messages.value.length > 0) { |
|
|
|
nextTick(() => { |
|
|
|
scrollToBottom(); |
|
|
|
}); |
|
|
|
nextTick(() => { scrollToBottom(); }); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 当消息数量变化,控制滚动是否运行 |
|
|
|
watch(messages, (val) => { |
|
|
|
if (val.length === 0) { |
|
|
|
nextTick(startTabsMarquee); |
|
|
|
} else { |
|
|
|
stopFrame(tabsTickId); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
onUnmounted(() => { stopFrame(tabsTickId); }); |
|
|
|
|
|
|
|
// 初始化 UUID |
|
|
|
const initUUID = () => { |
|
|
|
let storedUUID = uni.getStorageSync('user_uuid'); |
|
|
|
@ -412,10 +490,17 @@ const scrollToTop = () => { |
|
|
|
} |
|
|
|
|
|
|
|
.function-tabs { |
|
|
|
display: flex; |
|
|
|
display: block; |
|
|
|
overflow: hidden; |
|
|
|
margin-bottom: 30rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.tabs-track { |
|
|
|
display: inline-flex; |
|
|
|
white-space: nowrap; |
|
|
|
will-change: transform; |
|
|
|
} |
|
|
|
|
|
|
|
.tab-item { |
|
|
|
padding: 5rpx 20rpx; |
|
|
|
border-radius: 20rpx; |
|
|
|
|