|
|
<template> <view class="main"> <!-- 顶部状态栏占位 --> <view class="top" :style="{height:iSMT+'px'}"></view> <!-- 登录的提示 --> <LoginPrompt ref="loginPrompt"></LoginPrompt> <!-- 头部导航 --> <view class="header"> <view class="headphone-icon"> <image src="https://d31zlh4on95l9h.cloudfront.net/images/bef2edba6cc0c85671fde07cfab5270d.png" class="header-icon-image"></image> </view> <view class="title">DeepChart</view> <view class="notification-icon"> <image src="https://d31zlh4on95l9h.cloudfront.net/images/2554c84b91712d2a6cb6b00380e63bac.png" class="header-icon-image"></image> </view> </view> <!-- 内容区域 - 使用滚动视图 --> <scroll-view scroll-y class="content-container"> <!-- 1. 今日市场概览 --> <market-overview :stock-info-list="currentStockInfoList"></market-overview> <!-- 间隔 --> <view class="section-gap"></view> <!-- 新增欢迎部分 --> <view class="section welcome-section"> <!-- 轮播图 --> <swiper class="welcome-swiper" circular autoplay interval="3000" duration="500" indicator-dots indicator-active-color="#4080ff"> <swiper-item v-for="(imageUrl, index) in swiperImages" :key="index"> <image class="swiper-image" :src="imageUrl" mode="aspectFill"></image> </swiper-item> </swiper> </view> <!-- 2. DeepMate --> <view class="section deepmate-section"> <DeepMate /> </view> <!-- 3. 深度探索 --> <view class="section deep-exploration"> <!-- 上部分:标题和查看更多按钮 --> <view class="section-header-container"> <view class="section-header"> <view class="header-left"> <text class="section-title">深度探索</text> </view> <view class="header-right"> <text class="more-btn" @click="goToDeepExploration">查看更多</text> </view> </view> </view> <!-- 下部分:四个图标 --> <view class="exploration-container"> <view class="exploration-content"> <view class="exploration-item"> <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/199472b0ee90a1c897f7c87b85accd84.png" mode="aspectFit" lazy-load="true"></image> <text class="exploration-text">主力追踪</text> </view> <view class="exploration-item"> <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/c25ca5e176efc961dabfa5d0d1b486b0.png" mode="aspectFit" lazy-load="true"></image> <text class="exploration-text">主力资达</text> </view> <view class="exploration-item"> <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/c064d7066dc8129a7df7b052762f82cf.png" mode="aspectFit" lazy-load="true"></image> <text class="exploration-text">主力解码</text> </view> <view class="exploration-item"> <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/9d69cceee9c515911477078af6f68d88.png" mode="aspectFit" lazy-load="true"></image> <text class="exploration-text">主力资金流</text> </view> </view> </view> </view> <!-- 4. 我的自选 --> <view class="section my-selection"> <!-- 上部分:标题和查看更多按钮 --> <view class="section-header-container"> <view class="section-header"> <text class="section-title">我的自选</text> <text class="more-btn" @click="goToMarketSituation">添加自选股</text> </view> <!-- 我的自选TCP连接状态和控制 --> <!-- <view class="my-stocks-tcp-control"> <view class="tcp-status" :class="{'connected': myStocksTcpConnected, 'disconnected': !myStocksTcpConnected}"> <text class="status-text">{{myStocksTcpConnected ? '自选已连接' : '自选未连接'}}</text> </view> <view class="tcp-buttons"> <button class="tcp-btn connect-btn" @click="connectMyStocksTcp" :disabled="myStocksTcpConnected">连接</button> <button class="tcp-btn disconnect-btn" @click="disconnectMyStocksTcp" :disabled="!myStocksTcpConnected">断开</button> </view> </view> --> </view> <!-- 下部分:股票列表 --> <view class="stock-container"> <view class="stock-list"> <!-- 如果有TCP数据,显示实时数据;否则显示静态数据 --> <view class="stock-item" v-for="(item, index) in (myStocksInfoList.length > 0 ? myStocksInfoList : myStocks)" :key="item.code || item.stock_code"> <view class="stock-info"> <view class="name-container"> <text class="stock-name">{{item.name || item.stock_name}}</text> <text class="stock-code-label">{{item.code || item.stock_code}}</text> </view> <view class="price-container"> <text class="stock-price">{{item.price || item.current_price}}</text> <text class="stock-change" :class="{'stock-up': (item.change_percent || item.change || item.change_amount || 0) > 0, 'stock-down': (item.change_percent || item.change || item.change_amount || 0) < 0}"> {{(item.change_percent || item.change || item.change_amount || 0) > 0 ? '+' : ''}}{{(parseFloat(item.change_percent || item.change || item.change_amount || 0)).toFixed(2)}}% </text> </view> </view> <view class="stock-chart"> <image :src="item.chartImg || '/static/c5.png'" mode="aspectFit" class="chart-image"></image> </view> </view> </view> <!-- 机构动向简报数据 --> <view class="institutional-reports"> <view class="section-title-container"> <text class="section-title-text">机构动向简报</text> </view> <view class="text-gap"></view> <view class="report-item" v-for="(report, index) in institutionalReports" :key="index"> <view class="report-stock">{{report.stock}}</view> <view class="report-status">{{report.status}}</view> </view> <view class="view-more" @click="goToCustomStockList"> <text>查看更多 >></text> </view> </view> </view> </view> <!-- 5. TCP连接测试 --> <!-- <view class="section tcp-test-section"> <view class="section-header"> <text class="section-title">TCP连接测试</text> <view class="tcp-status" :class="{'connected': tcpConnected, 'disconnected': !tcpConnected}"> <text class="status-text">{{tcpConnected ? '已连接' : '未连接'}}</text> </view> </view> --> <!-- TCP控制按钮 --> <!-- <view class="tcp-controls"> <button class="tcp-btn connect-btn" @click="connectTcp" :disabled="tcpConnected">连接</button> <button class="tcp-btn disconnect-btn" @click="disconnectTcp" :disabled="!tcpConnected">断开</button> <button class="tcp-btn send-btn" @click="sendTcpMessage" :disabled="!tcpConnected">发送测试消息</button> <button class="tcp-btn status-btn" @click="getTcpStatus">查看状态</button> </view> --> <!-- 消息记录 --> <!-- <view class="tcp-messages" v-if="tcpMessages.length > 0"> <view class="messages-header"> <text class="messages-title">消息记录 ({{tcpMessages.length}})</text> <button class="clear-btn" @click="clearTcpMessages">清空</button> </view> <scroll-view scroll-y class="messages-list" scroll-top="{{tcpMessages.length * 50}}"> <view class="message-item" v-for="(msg, index) in tcpMessages" :key="index" :class="{'sent': msg.direction === 'sent', 'received': msg.direction === 'received'}"> <view class="message-info"> <text class="message-direction">{{msg.direction === 'sent' ? '发送' : '接收'}}</text> <text class="message-time">{{msg.timestamp}}</text> </view> <text class="message-content">{{msg.content}}</text> </view> </scroll-view> </view> </view> --> <!-- 6. 今日市场看点 --> <view class="section-header highlights-title-container"> <text class="section-title">今日市场核心看点</text> </view> <view class="highlights-image-container" @click="goToAnalysisInstitutionalTrends"> <image src="https://d31zlh4on95l9h.cloudfront.net/images/8d5365af968402a18cedb120c09460b0.png" mode="aspectFit" class="highlights-image"></image> </view> <!-- 底部空间 - 为底部导航腾出空间 --> <view class="bottom-space"></view> </scroll-view> <!-- 底部导航 --> <footerBar class="static-footer" :type="type"></footerBar> </view></template>
<script>import footerBar from '../../components/footerBar.vue'import MarketOverview from '../../components/MarketOverview.vue'import DeepMate from '../../components/DeepMate.vue'import tcpConnection from '../../api/tcpConnection.js'import th from '../../static/language/th'import MySelectionsAPI from '../../api/home/mySelections.js'import { useUserStore } from '../../stores/modules/userInfo'export default { components: { footerBar, MarketOverview, DeepMate }, data() { return { type: 'home', iSMT: 0, // 用户信息 store
userStore: null, // 深度探索数据
explorationItems: [ { title: '主力追踪', icon: '/static/c1.png' }, { title: '主力资金', icon: '/static/c2.png' }, { title: '主力解码', icon: '/static/c3.png' }, { title: '主力资金流', icon: '/static/c4.png' } ], // 轮播图片数据
swiperImages: [ 'https://d31zlh4on95l9h.cloudfront.net/images/44ec37501cf3ecf5438d401c3d66fcd3.png', 'https://d31zlh4on95l9h.cloudfront.net/images/80274b6626bb403d717469b3672333c9.png', 'https://d31zlh4on95l9h.cloudfront.net/images/14481c76b09509977d3879ba3141ebc7.png', 'https://d31zlh4on95l9h.cloudfront.net/images/19e4c5542e5f974e1c6938897aa20ef9.png', 'https://d31zlh4on95l9h.cloudfront.net/images/d0bc1c1bb71917132865b82dab99831b.png' ], // 我的自选股票数据
myStocks: [ { name: '特斯拉', code: 'TSLA', price: '482.00', change: 2.80, chartImg: '/static/c5.png' }, { name: '英伟达', code: 'NVDA', price: '189.800', change: -2.92, chartImg: '/static/c6.png' }, { name: '苹果', code: 'AAPL', price: '256.430', change: 2.60, chartImg: '/static/c7.png' } ], // 机构动向简报数据
institutionalReports: [ { stock: '特斯拉', status: '当前市场多头资金占比,且多头资金持续流入。' }, { stock: '英伟达', status: '当前市场多头资金占比,且多头资金持续流入。' }, { stock: '苹果', status: '当前市场多头资金占比,且多头资金持续流入。' } ], // 防抖定时器
debounceTimer: null, // TCP连接相关状态
tcpConnected: false, tcpMessages: [], lastSentMessage: '', // 我的自选TCP连接状态
myStocksTcpConnected: false, // TCP监听器引用,用于移除监听器
connectionListener: null, messageListener: null, // TCP股票数据存储
tcpStockData: { count: 0, data: {}, stock_count: 0, timestamp: '', type: '' }, // 当前TCP使用的股票代码
currentStockCodes: ['SH.000001', 'SH.000002', 'SH.000003'], // 我的自选TCP股票数据存储
myStocksTcpData: { count: 0, data: {}, stock_count: 0, timestamp: '', type: '' }, // TCP数据缓存机制,用于处理分片数据
tcpDataCache: { isCollecting: false, // 是否正在收集数据片段
expectedCount: 0, // 期望的数据总数
collectedData: {}, // 已收集的数据对象(用于对象级拼接)
timestamp: '', // 数据时间戳
type: '', // 数据类型
// 新增:字符串片段缓存
firstPartData: '', // 前半部分数据字符串
isWaitingSecondPart: false // 是否正在等待后半部分数据
}, // 我的自选TCP数据缓存机制,用于处理分片数据
myStocksTcpDataCache: { isCollecting: false, // 是否正在收集数据片段
expectedCount: 0, // 期望的数据总数
collectedData: {}, // 已收集的数据对象(用于对象级拼接)
timestamp: '', // 数据时间戳
type: '', // 数据类型
// 字符串片段缓存
firstPartData: '', // 前半部分数据字符串
isWaitingSecondPart: false // 是否正在等待后半部分数据
}, // 当前显示的3个股票数据(用于MarketOverview组件)
currentStockInfoList: [ { stock_name: '美元/日元', current_price: '151.13', change: '+1.62%', change_value: 0, change_percent: 0 }, { stock_name: '美元/韩元', current_price: '1424.900', change: '-2.92%', change_value: 0, change_percent: 0 }, { stock_name: '美元/英镑', current_price: '0.730', change: '+2.92%', change_value: 0, change_percent: 0 } ], // 我的自选处理后的股票信息列表
myStocksInfoList: [] } },
// Vue 2生命周期方法
mounted() { // 状态栏高度
this.iSMT = uni.getSystemInfoSync().statusBarHeight; // 初始化用户信息 store
this.userStore = useUserStore(); // 预加载图片资源
this.myStocks.forEach(stock => { // 使用uni.getImageInfo替代Image对象
uni.getImageInfo({ src: stock.chartImg, success: function(res) { // 图片加载成功
console.log('图片预加载成功:', stock.name) }, fail: function(err) { console.log('图片预加载失败:', err) } }) })
// 检查用户登录状态,只有已登录并且不是游客才加载自选股数据
if (this.userStore.userInfo && !this.userStore.userInfo.isVisitor) { console.log('是用户登录,加载自选股数据', this.userStore.userInfo) this.loadMySelectionsData() } else if (this.userStore.userInfo && this.userStore.userInfo.isVisitor){ console.log('是游客登录,加载默认自选股',this.userStore.userInfo) this.loadGuestDefaultStocks() }else { console.log('用户未登录',this.userStore.userInfo) }
// 初始化TCP连接监听器
this.initTcpListeners() // 监听游客登录成功事件
uni.$on('visitorLoginSuccess', this.handleVisitorLoginSuccess) // 页面渲染完成后自动连接两个TCP连接
// this.$nextTick(() => {
// console.log('页面渲染完成,开始自动连接TCP服务器...')
// // 延迟一小段时间确保页面完全加载后再连接
// setTimeout(() => {
// // 连接今日市场概览TCP(channel 1)
// console.log('连接今日市场概览TCP(channel 1)...')
// this.connectTcp()
// // 稍微延迟后连接我的自选TCP(channel 2)
// setTimeout(() => {
// console.log('连接我的自选TCP(channel 2)...')
// this.connectMyStocksTcp()
// }, 500)
// }, 1000)
// })
},
// 页面销毁前的清理工作
onUnload() { // 自动关闭主TCP连接
if (this.tcpConnected) { console.log('页面销毁,自动关闭主TCP连接') tcpConnection.disconnect({ ip: '192.168.1.9', port: '8080', channel: '1', charsetname: 'UTF-8' }) this.tcpConnected = false } // 自动关闭我的自选TCP连接
if (this.myStocksTcpConnected) { console.log('页面销毁,自动关闭我的自选TCP连接') tcpConnection.disconnect({ ip: '192.168.1.9', port: '8080', channel: '2', charsetname: 'UTF-8' }) this.myStocksTcpConnected = false } // 清理防抖定时器
if (this.debounceTimer) { clearTimeout(this.debounceTimer) this.debounceTimer = null } // 移除TCP监听器,防止内存泄漏
this.removeTcpListeners() // 移除游客登录成功事件监听器
uni.$off('visitorLoginSuccess', this.handleVisitorLoginSuccess) }, methods: { // 跳转到自定义股票列表页面
goToCustomStockList() { uni.navigateTo({ url: '/pages/customStockList/customStockList' }) }, // 跳转到行情页面
goToMarketSituation() { uni.navigateTo({ url: '/pages/marketSituation/marketSituation' }) }, // 跳转到机构动向解析页面
goToAnalysisInstitutionalTrends() { uni.navigateTo({ url: '/pages/analysisInstitutionalTrends/analysisInstitutionalTrends' }) }, // 跳转到深度探索页面
goToDeepExploration() { uni.navigateTo({ url: '/pages/deepExploration/deepExploration' }) }, // 处理游客登录成功事件
handleVisitorLoginSuccess(data) { console.log('收到游客登录成功事件:', data) // 重新加载页面数据
this.reloadPageData() }, // 重新加载页面数据
reloadPageData() { console.log('重新加载页面数据...') // 更新用户信息store
this.userStore = useUserStore() // 根据新的用户状态加载相应的数据
if (this.userStore.userInfo && this.userStore.userInfo.isVisitor) { console.log('游客登录成功,加载默认自选股') this.loadGuestDefaultStocks() } else if (this.userStore.userInfo && !this.userStore.userInfo.isVisitor) { console.log('用户登录,加载自选股数据') this.loadMySelectionsData() } }, // 防抖函数
debounce(fn, delay = 300) { if (this.debounceTimer) clearTimeout(this.debounceTimer) this.debounceTimer = setTimeout(() => { fn() this.debounceTimer = null }, delay) }, // 自选股数据加载相关方法
// 加载自选股数据的主方法
async loadMySelectionsData() { try { console.log('开始加载自选股数据...') // 调用checkExist判断用户是否存在自选股
const checkResult = await MySelectionsAPI.checkExist() console.log('检查用户自选股存在性结果:', checkResult) if (checkResult.code === 200) { const isHave = checkResult.data.is_have if (isHave === 1) { // 用户有自选股,查询所有分组
console.log('用户存在自选股,查询所有分组...') await this.loadUserStockGroups() } else { // 用户没有自选股,查询默认自选股
console.log('用户不存在自选股,查询默认自选股...') await this.loadDefaultStocks() } } else { console.error('检查用户自选股存在性失败:', checkResult.message) // 失败时也尝试加载默认自选股
// await this.loadDefaultStocks()
} } catch (error) { console.error('加载自选股数据失败:', error) // 出错时尝试加载默认自选股
await this.loadDefaultStocks() } }, // 加载用户自选股分组
async loadUserStockGroups() { try { const groupResult = await MySelectionsAPI.getUserStockGroupList() console.log('查询用户自选股分组结果:', groupResult) if (groupResult.code === 200 && groupResult.data && groupResult.data.length > 0) { // 找到id最小的分组
const minIdGroup = groupResult.data.reduce((min, current) => { return current.id < min.id ? current : min }) console.log('找到最小id分组:', minIdGroup) // 查询该分组下的股票
await this.loadGroupStocks(minIdGroup.id) } else { console.log('没有找到用户自选股分组,加载默认自选股') await this.loadDefaultStocks() } } catch (error) { console.error('加载用户自选股分组失败:', error) await this.loadDefaultStocks() } }, // 加载指定分组下的股票
async loadGroupStocks(groupId) { try { const stockResult = await MySelectionsAPI.getUserStockList(null, null, { groupId: groupId, pageNum: 1, pageSize: 20 }) console.log('查询分组自选股结果:', stockResult) if (stockResult.code === 200 && stockResult.data) { this.processStockData(stockResult.data) } else { console.log('分组下没有股票数据,加载默认自选股') await this.loadDefaultStocks() } } catch (error) { console.error('加载分组股票失败:', error) await this.loadDefaultStocks() } }, // 加载默认自选股
async loadDefaultStocks() { try { const defaultResult = await MySelectionsAPI.getUserOrDefault() console.log('查询默认自选股结果:', defaultResult) if (defaultResult.code === 200 && defaultResult.data) { this.processStockData(defaultResult.data) } else { console.log('没有默认自选股数据') } } catch (error) { console.error('加载默认自选股失败:', error) } }, // 游客加载默认自选股
async loadGuestDefaultStocks() { try { console.log('游客开始加载默认自选股...') const guestResult = await MySelectionsAPI.getDefaultStocks() console.log('游客查询默认自选股结果:', guestResult) if (guestResult.code === 200 && guestResult.data) { this.processStockData(guestResult.data) } else { console.log('游客没有默认自选股数据') } } catch (error) { console.error('游客加载默认自选股失败:', error) } }, // 处理股票数据
processStockData(stockData) { console.log('处理股票数据:', stockData) // 根据返回的数据结构更新myStocks数组
// 这里需要根据实际API返回的数据结构进行调整
if (Array.isArray(stockData)) { // 如果是数组格式
this.myStocks = stockData.map(stock => ({ name: stock.name || stock.stock_name || '', code: stock.code || stock.stock_code || '', // price: stock.price || stock.current_price || '0.00',
// change: stock.change || stock.change_percent || 0,
chartImg: stock.chartImg || '/static/c5.png' // 默认图片
})) //重新赋值机构动向简报
this.institutionalReports = stockData.map(stock => ({ stock: stock.name || stock.stock_name || '', status: stock.code || stock.stock_code || '',
})) } else if (stockData.list && Array.isArray(stockData.list)) { // 如果是分页格式
this.myStocks = stockData.list.map(stock => ({ name: stock.name || stock.stock_name || '', code: stock.code || stock.stock_code || '', // price: stock.price || stock.current_price || '0.00',
// change: stock.change || stock.change_percent || 0,
chartImg: stock.chartImg || '/static/c5.png' // 默认图片
})) //重新赋值机构动向简报
//重新赋值机构动向简报
this.institutionalReports = stockData.map(stock => ({ stock: stock.name || stock.stock_name || '', status: stock.code || stock.stock_code || '', })) } console.log('更新后的自选股数据:', this.myStocks) // 更新TCP消息中的股票代码
this.updateTcpStockCodes() }, // 更新TCP消息中的股票代码
updateTcpStockCodes() { // 从myStocks中提取前3个股票代码
const stockCodes = this.myStocks.slice(0, 3).map(stock => stock.code).filter(code => code) console.log('更新TCP消息股票代码:', stockCodes) // 如果有股票代码,更新TCP消息
if (stockCodes.length > 0) { // 补充到3个代码(如果不足3个,用默认代码补充)
while (stockCodes.length < 3) { stockCodes.push('SH.000001') // 默认代码
} // 保存到组件数据中,供TCP方法使用
this.currentStockCodes = stockCodes console.log('当前TCP使用的股票代码:', this.currentStockCodes) } }, // TCP连接相关方法
// 初始化TCP监听器
initTcpListeners() { // 创建连接状态监听器并保存引用
this.connectionListener = (status, result, channel) => { console.log('TCP连接状态变化:', status, 'channel:', channel) // 根据channel更新对应的连接状态
if (channel === '1') { // 主TCP连接
this.tcpConnected = (status === 'connected') console.log('主TCP连接状态:', this.tcpConnected) // 显示连接状态提示
uni.showToast({ title: status === 'connected' ? '主连接服务器成功' : '主服务器连接断开', icon: status === 'connected' ? 'success' : 'none', duration: 1000 }) // 连接成功后自动发送股票数据请求命令
if (status === 'connected') { console.log('主TCP连接成功,自动发送股票数据请求...') // 延迟一小段时间确保连接稳定后再发送命令
setTimeout(() => { this.sendTcpMessage() }, 500) } } else if (channel === '2') { // 我的自选TCP连接
this.myStocksTcpConnected = (status === 'connected') console.log('我的自选TCP连接状态:', this.myStocksTcpConnected) // 显示连接状态提示
uni.showToast({ title: status === 'connected' ? '我的自选连接成功' : '我的自选连接断开', icon: status === 'connected' ? 'success' : 'none', duration: 1000 }) // 连接成功后自动发送我的自选股票数据请求
if (status === 'connected') { console.log('我的自选TCP连接成功,自动发送自选股票数据请求...') // 延迟一小段时间确保连接稳定后再发送命令
setTimeout(() => { this.sendMyStocksTcpMessage() }, 500) } } } // 创建消息监听器并保存引用
this.messageListener = (type, message, parsedArray, channel) => { const messageObj = { type: type, content: message, parsedArray: parsedArray, channel: channel, timestamp: new Date().toLocaleTimeString(), direction: 'received' } this.tcpMessages.push(messageObj) console.log('收到TCP消息:', messageObj) console.log('消息来源channel:', channel) // 根据channel调用相应的数据处理方法
if (channel === '1') { // 主TCP连接 - 今日市场概览数据
// console.log('处理今日市场概览数据')
this.parseStockData(message) } else if (channel === '2') { // 我的自选TCP连接 - 我的自选数据
console.log('处理我的自选数据') this.parseMyStocksData(message) } else { console.warn('未知的channel:', channel) } } // 注册监听器
tcpConnection.onConnectionChange(this.connectionListener) tcpConnection.onMessage(this.messageListener) }, // 连接TCP服务器
connectTcp() { // console.log('开始连接TCP服务器...')
// 先断开现有连接(如果存在)
if (this.tcpConnected) { // console.log('检测到现有连接,先断开...')
this.disconnectTcp() // 等待断开完成后再连接
setTimeout(() => { this.performTcpConnect() }, 500) } else { // 直接连接
this.performTcpConnect() } }, // 执行TCP连接
performTcpConnect() { // console.log('执行TCP连接...')
tcpConnection.connect( { ip: '192.168.1.9', port: '8080', channel: '1', // 可选 1~20
charsetname: 'UTF-8' // 默认UTF-8,可选GBK
} ) }, // 断开TCP连接
disconnectTcp() { // console.log('断开TCP连接...')
tcpConnection.disconnect( { ip: '192.168.1.9', port: '8080', channel: '1', // 可选 1~20
charsetname: 'UTF-8' // 默认UTF-8,可选GBK
} ) this.tcpConnected = false }, // 连接我的自选TCP服务器
connectMyStocksTcp() { console.log('开始连接我的自选TCP服务器...') // 先断开现有连接(如果存在)
if (this.myStocksTcpConnected) { console.log('检测到我的自选现有连接,先断开...') this.disconnectMyStocksTcp() // 等待断开完成后再连接
setTimeout(() => { this.performMyStocksTcpConnect() }, 500) } else { // 直接连接
this.performMyStocksTcpConnect() } }, // 执行我的自选TCP连接
performMyStocksTcpConnect() { console.log('执行我的自选TCP连接...') tcpConnection.connect( { ip: '192.168.1.9', port: '8080', channel: '2', // 我的自选使用channel 2
charsetname: 'UTF-8' // 默认UTF-8,可选GBK
} ) }, // 断开我的自选TCP连接
disconnectMyStocksTcp() { console.log('断开我的自选TCP连接...') tcpConnection.disconnect( { ip: '192.168.1.9', port: '8080', channel: '2', // 我的自选使用channel 2
charsetname: 'UTF-8' // 默认UTF-8,可选GBK
} ) this.myStocksTcpConnected = false }, // 解析我的自选TCP股票数据
parseMyStocksData(message) { try { console.log('进入parseMyStocksData, message类型:', typeof message, '长度:', message.length) // 第一步:检查数据状态(完整、前半部分、后半部分)
const dataStatus = this.getMyStocksDataStatus(message) let completeMessage = '' switch (dataStatus) { case 'complete': // 完整数据,直接处理
console.log('我的自选:检测到完整数据,直接处理') completeMessage = message break case 'first_part': // 前半部分数据,缓存并等待后半部分
console.log('我的自选:检测到前半部分数据,开始缓存') this.cacheMyStocksFirstPartData(message) return // 等待后半部分数据
case 'second_part': // 后半部分数据,检查是否有缓存的前半部分
if (this.myStocksTcpDataCache.isWaitingSecondPart) { console.log('我的自选:检测到后半部分数据,开始拼接') completeMessage = this.concatenateMyStocksDataParts(message) } else { console.log('我的自选:收到后半部分数据,但没有缓存的前半部分,跳过处理') return } break case 'invalid': default: console.log('我的自选:数据格式无效,跳过处理') return } // 第二步:解析完整的JSON数据
let parsedMessage try { console.log('我的自选:开始解析完整JSON数据,长度:', completeMessage.length) parsedMessage = JSON.parse(completeMessage) console.log('我的自选:JSON解析成功,解析后类型:', typeof parsedMessage, parsedMessage) } catch (parseError) { console.error('我的自选:JSON解析失败:', parseError.message) // 清空字符串片段缓存
this.clearMyStocksStringFragmentCache() return } // 第三步:检查是否是股票数据类型
if (!((parsedMessage.type === 'batch_data_chunk' || parsedMessage.type === 'batch_realtime_data') && parsedMessage.data)) { console.log('我的自选:不是batch_data_chunk或batch_realtime_data类型的消息,跳过处理') return } console.log('我的自选:开始处理股票数据') // 第四步:验证数据完整性(对象级别的完整性检查)
// 注意:batch_data_chunk类型的数据不需要验证完整性,直接处理
if (parsedMessage.type === 'batch_data_chunk') { console.log('我的自选:batch_data_chunk类型数据,跳过完整性验证,直接处理') this.processCompleteMyStocksData(parsedMessage) } else { const isDataComplete = this.validateMyStocksDataIntegrity(parsedMessage) if (isDataComplete) { // 数据完整,直接处理
console.log('我的自选:对象级数据完整,直接处理') this.processCompleteMyStocksData(parsedMessage) } else { // 数据不完整,需要拼接(对象级别的拼接)
console.log('我的自选:对象级数据不完整,开始拼接处理') // 将当前数据合并到缓存中
this.mergeMyStocksDataToCache(parsedMessage) // 检查缓存中的数据是否已经完整
if (this.isMyStocksCacheDataComplete()) { console.log('我的自选:缓存数据已完整,开始处理') const completeData = this.getCompleteMyStocksDataFromCache() this.processCompleteMyStocksData(completeData) } else { console.log('我的自选:缓存数据仍不完整,等待更多数据片段') } } } } catch (error) { console.error('我的自选:解析TCP股票数据失败:', error.message) console.error('我的自选:错误详情:', error) // 发生错误时清空所有缓存
this.clearMyStocksStringFragmentCache() if (this.myStocksTcpDataCache.isCollecting) { console.log('我的自选:发生错误,清空对象级数据缓存') this.myStocksTcpDataCache.isCollecting = false this.myStocksTcpDataCache.expectedCount = 0 this.myStocksTcpDataCache.collectedData = {} this.myStocksTcpDataCache.timestamp = '' this.myStocksTcpDataCache.type = '' } } }, // 验证我的自选数据完整性
validateMyStocksDataIntegrity(parsedMessage) { if (!parsedMessage.count || !parsedMessage.data) { return false } const dataObjectCount = Object.keys(parsedMessage.data).length const expectedCount = parsedMessage.count console.log(`我的自选数据完整性验证: 期望${expectedCount}个对象,实际${dataObjectCount}个对象`) return dataObjectCount === expectedCount }, // 检查我的自选数据状态(完整数据、前半部分数据、后半部分数据)
getMyStocksDataStatus(message) { if (typeof message !== 'string') { return 'invalid' } const trimmedMessage = message.trim() const startsWithBrace = trimmedMessage.startsWith('{') const endsWithBrace = trimmedMessage.endsWith('}') if (startsWithBrace && endsWithBrace) { // 以{开头,以}结尾 - 完整数据
console.log('我的自选:检测到完整数据格式') return 'complete' } else if (startsWithBrace && !endsWithBrace) { // 以{开头,不以}结尾 - 前半部分数据
console.log('我的自选:检测到前半部分数据') return 'first_part' } else if (!startsWithBrace && endsWithBrace) { // 不以{开头,以}结尾 - 后半部分数据
console.log('我的自选:检测到后半部分数据') return 'second_part' } else { // 其他情况 - 无效数据
console.log('我的自选:检测到无效数据格式') return 'invalid' } }, // 缓存我的自选前半部分数据
cacheMyStocksFirstPartData(message) { this.myStocksTcpDataCache.firstPartData = message.trim() this.myStocksTcpDataCache.isWaitingSecondPart = true console.log('我的自选:已缓存前半部分数据,长度:', this.myStocksTcpDataCache.firstPartData.length) }, // 拼接我的自选前后两部分数据
concatenateMyStocksDataParts(secondPartMessage) { const completeMessage = this.myStocksTcpDataCache.firstPartData + secondPartMessage.trim() console.log('我的自选:数据拼接完成,完整数据长度:', completeMessage.length) // 清空缓存
this.clearMyStocksStringFragmentCache() return completeMessage }, // 清空我的自选字符串片段缓存
clearMyStocksStringFragmentCache() { this.myStocksTcpDataCache.firstPartData = '' this.myStocksTcpDataCache.isWaitingSecondPart = false console.log('我的自选:字符串片段缓存已清空') }, // 合并我的自选数据到缓存中
mergeMyStocksDataToCache(parsedMessage) { // 如果是第一次收集数据,初始化缓存
if (!this.myStocksTcpDataCache.isCollecting) { this.myStocksTcpDataCache.isCollecting = true this.myStocksTcpDataCache.expectedCount = parsedMessage.count this.myStocksTcpDataCache.collectedData = {} this.myStocksTcpDataCache.timestamp = parsedMessage.timestamp this.myStocksTcpDataCache.type = parsedMessage.type console.log('我的自选:开始收集数据片段,期望总数:', parsedMessage.count) } // 合并新数据到缓存中
if (parsedMessage.data) { Object.assign(this.myStocksTcpDataCache.collectedData, parsedMessage.data) console.log('我的自选:数据片段已合并,当前已收集:', Object.keys(this.myStocksTcpDataCache.collectedData).length, '个对象') } }, // 检查我的自选缓存数据是否完整
isMyStocksCacheDataComplete() { const collectedCount = Object.keys(this.myStocksTcpDataCache.collectedData).length const expectedCount = this.myStocksTcpDataCache.expectedCount console.log(`我的自选缓存数据检查: 已收集${collectedCount}个,期望${expectedCount}个`) return collectedCount === expectedCount && collectedCount > 0 }, // 获取我的自选完整的缓存数据并清空缓存
getCompleteMyStocksDataFromCache() { const completeData = { count: this.myStocksTcpDataCache.expectedCount, data: { ...this.myStocksTcpDataCache.collectedData }, stock_count: this.myStocksTcpDataCache.expectedCount, timestamp: this.myStocksTcpDataCache.timestamp, type: this.myStocksTcpDataCache.type } // 清空缓存
this.myStocksTcpDataCache.isCollecting = false this.myStocksTcpDataCache.expectedCount = 0 this.myStocksTcpDataCache.collectedData = {} this.myStocksTcpDataCache.timestamp = '' this.myStocksTcpDataCache.type = '' console.log('我的自选:获取完整数据并清空缓存,数据对象数:', Object.keys(completeData.data).length) return completeData }, // 处理我的自选完整的股票数据
processCompleteMyStocksData(completeData) { console.log("我的自选:开始更新我的自选TCP股票数据存储") // 更新我的自选TCP股票数据存储
this.myStocksTcpData = { count: completeData.count || 0, data: completeData.data || {}, stock_count: completeData.stock_count || 0, timestamp: completeData.timestamp || '', type: completeData.type || '' } // 获取所有股票的数据用于显示
const stockCodes = Object.keys(completeData.data) const newMyStocksInfoList = [] // 只处理前3条股票数据
const maxStocks = Math.min(3, stockCodes.length) for (let i = 0; i < maxStocks; i++) { const stockCode = stockCodes[i] // 检查数据结构
if (completeData.data[stockCode] && Array.isArray(completeData.data[stockCode]) && completeData.data[stockCode].length > 0) { const stockData = completeData.data[stockCode][0] // 取第一条数据
console.log('遍历的数据:', stockCode, stockData) if (stockData && stockData.current_price !== undefined) { // 直接使用返回的数据,不重新计算
newMyStocksInfoList.push({ stock_code: stockCode, stock_name: stockData.stock_name || '未知股票', current_price: parseFloat(stockData.current_price).toFixed(2), change: stockData.change || '0.00%', change_value: stockData.change_value || 0, change_percent: parseFloat(stockData.change) || 0 }) } } } // 更新我的自选股票信息列表
if (newMyStocksInfoList.length > 0) { this.myStocksInfoList = newMyStocksInfoList console.log('我的自选:股票数据更新成功,共', newMyStocksInfoList.length, '个股票:', this.myStocksInfoList) } }, // 发送TCP消息
sendTcpMessage() { // 构造要发送的消息对象
const messageData = // {
// command: "real_time",
// stock_code: "SH.000001"
// }
// {"command": "stock_list"}
// {"command": "batch_real_time", "stock_codes": ["SH.000001"]}
{"command": "batch_real_time", "stock_codes": this.currentStockCodes} // 发送消息
const success = tcpConnection.send(messageData) if (success) { // console.log('home发送TCP消息:', messageData)
uni.showToast({ title: '服务器连接成功', icon: 'success', duration: 1500 }) } }, // 发送我的自选TCP消息
sendMyStocksTcpMessage() { // 构造要发送的消息对象 - 我的自选股票数据请求
const messageData = {"command": "batch_real_time", "stock_codes": this.currentStockCodes} // 发送消息到channel 2(我的自选TCP连接)
const success = tcpConnection.send(messageData, { channel: '2' }) if (success) { console.log('我的自选:发送TCP消息成功:', messageData) uni.showToast({ title: '我的自选数据请求已发送', icon: 'success', duration: 1500 }) } else { console.error('我的自选:发送TCP消息失败') uni.showToast({ title: '我的自选连接失败', icon: 'error', duration: 1500 }) } }, // 清空消息记录
clearTcpMessages() { this.tcpMessages = [] uni.showToast({ title: '消息记录已清空', icon: 'success', duration: 1500 }) }, // 获取TCP连接状态
getTcpStatus() { const status = tcpConnection.getConnectionStatus() uni.showModal({ title: 'TCP连接状态', content: `当前状态: ${status ? '已连接' : '未连接'}\n消息数量: ${this.tcpMessages.length}`, showCancel: false }) }, // 验证数据完整性(检查count值与data对象个数是否相等)
validateDataIntegrity(parsedMessage) { if (!parsedMessage.count || !parsedMessage.data) { return false } const dataObjectCount = Object.keys(parsedMessage.data).length const expectedCount = parsedMessage.count // console.log(`数据完整性验证: 期望${expectedCount}个对象,实际${dataObjectCount}个对象`)
return dataObjectCount === expectedCount }, // 检查数据状态(完整数据、前半部分数据、后半部分数据)
getDataStatus(message) { if (typeof message !== 'string') { return 'invalid' } const trimmedMessage = message.trim() const startsWithBrace = trimmedMessage.startsWith('{') const endsWithBrace = trimmedMessage.endsWith('}') if (startsWithBrace && endsWithBrace) { // 以{开头,以}结尾 - 完整数据
// console.log('检测到完整数据格式')
return 'complete' } else if (startsWithBrace && !endsWithBrace) { // 以{开头,不以}结尾 - 前半部分数据
// console.log('检测到前半部分数据')
return 'first_part' } else if (!startsWithBrace && endsWithBrace) { // 不以{开头,以}结尾 - 后半部分数据
// console.log('检测到后半部分数据')
return 'second_part' } else { // 其他情况 - 无效数据
// console.log('检测到无效数据格式')
return 'invalid' } }, // 缓存前半部分数据
cacheFirstPartData(message) { this.tcpDataCache.firstPartData = message.trim() this.tcpDataCache.isWaitingSecondPart = true // console.log('已缓存前半部分数据,长度:', this.tcpDataCache.firstPartData.length)
}, // 拼接前后两部分数据
concatenateDataParts(secondPartMessage) { const completeMessage = this.tcpDataCache.firstPartData + secondPartMessage.trim() // console.log('数据拼接完成,完整数据长度:', completeMessage.length)
// 清空缓存
this.clearStringFragmentCache() return completeMessage }, // 清空字符串片段缓存
clearStringFragmentCache() { this.tcpDataCache.firstPartData = '' this.tcpDataCache.isWaitingSecondPart = false // console.log('字符串片段缓存已清空')
}, // 合并数据到缓存中
mergeDataToCache(parsedMessage) { // 如果是第一次收集数据,初始化缓存
if (!this.tcpDataCache.isCollecting) { this.tcpDataCache.isCollecting = true this.tcpDataCache.expectedCount = parsedMessage.count this.tcpDataCache.collectedData = {} this.tcpDataCache.timestamp = parsedMessage.timestamp this.tcpDataCache.type = parsedMessage.type // console.log('开始收集数据片段,期望总数:', parsedMessage.count)
} // 合并新数据到缓存中
if (parsedMessage.data) { Object.assign(this.tcpDataCache.collectedData, parsedMessage.data) // console.log('数据片段已合并,当前已收集:', Object.keys(this.tcpDataCache.collectedData).length, '个对象')
} }, // 检查缓存数据是否完整
isCacheDataComplete() { const collectedCount = Object.keys(this.tcpDataCache.collectedData).length const expectedCount = this.tcpDataCache.expectedCount // console.log(`缓存数据检查: 已收集${collectedCount}个,期望${expectedCount}个`)
return collectedCount === expectedCount && collectedCount > 0 }, // 获取完整的缓存数据并清空缓存
getCompleteDataFromCache() { const completeData = { count: this.tcpDataCache.expectedCount, data: { ...this.tcpDataCache.collectedData }, stock_count: this.tcpDataCache.expectedCount, timestamp: this.tcpDataCache.timestamp, type: this.tcpDataCache.type } // 清空缓存
this.tcpDataCache.isCollecting = false this.tcpDataCache.expectedCount = 0 this.tcpDataCache.collectedData = {} this.tcpDataCache.timestamp = '' this.tcpDataCache.type = '' // console.log('获取完整数据并清空缓存,数据对象数:', Object.keys(completeData.data).length)
return completeData }, // 解析TCP股票数据
parseStockData(message) { try { // console.log('进入parseStockData, message类型:', typeof message, '长度:', message.length)
// 第一步:检查数据状态(完整、前半部分、后半部分)
const dataStatus = this.getDataStatus(message) let completeMessage = '' switch (dataStatus) { case 'complete': // 完整数据,直接处理
// console.log('检测到完整数据,直接处理')
completeMessage = message break case 'first_part': // 前半部分数据,缓存并等待后半部分
// console.log('检测到前半部分数据,开始缓存')
this.cacheFirstPartData(message) return // 等待后半部分数据
case 'second_part': // 后半部分数据,检查是否有缓存的前半部分
if (this.tcpDataCache.isWaitingSecondPart) { // console.log('检测到后半部分数据,开始拼接')
completeMessage = this.concatenateDataParts(message) } else { // console.log('收到后半部分数据,但没有缓存的前半部分,跳过处理')
return } break case 'invalid': default: // console.log('数据格式无效,跳过处理')
return } // 第二步:解析完整的JSON数据
let parsedMessage try { // console.log('开始解析完整JSON数据,长度:', completeMessage.length)
parsedMessage = JSON.parse(completeMessage) // console.log('JSON解析成功,解析后类型:', typeof parsedMessage, parsedMessage)
} catch (parseError) { // console.error('JSON解析失败:', parseError.message)
// 清空字符串片段缓存
this.clearStringFragmentCache() return } // 第三步:检查是否是股票数据类型
if (!((parsedMessage.type === 'batch_data_chunk' || parsedMessage.type === 'batch_realtime_data') && parsedMessage.data)) { // console.log('不是batch_data_chunk或batch_realtime_data类型的消息,跳过处理')
return } // console.log('开始处理股票数据')
// 第四步:验证数据完整性(对象级别的完整性检查)
// 注意:batch_data_chunk类型的数据不需要验证完整性,直接处理
if (parsedMessage.type === 'batch_data_chunk') { // console.log('batch_data_chunk类型数据,跳过完整性验证,直接处理')
this.processCompleteStockData(parsedMessage) } else { const isDataComplete = this.validateDataIntegrity(parsedMessage) if (isDataComplete) { // 数据完整,直接处理
// console.log('对象级数据完整,直接处理')
this.processCompleteStockData(parsedMessage) } else { // 数据不完整,需要拼接(对象级别的拼接)
// console.log('对象级数据不完整,开始拼接处理')
// 将当前数据合并到缓存中
this.mergeDataToCache(parsedMessage) // 检查缓存中的数据是否已经完整
if (this.isCacheDataComplete()) { // console.log('缓存数据已完整,开始处理')
const completeData = this.getCompleteDataFromCache() this.processCompleteStockData(completeData) } else { // console.log('缓存数据仍不完整,等待更多数据片段')
} } } } catch (error) { // console.error('解析TCP股票数据失败:', error.message)
// console.error('错误详情:', error)
// 发生错误时清空所有缓存
this.clearStringFragmentCache() if (this.tcpDataCache.isCollecting) { // console.log('发生错误,清空对象级数据缓存')
this.tcpDataCache.isCollecting = false this.tcpDataCache.expectedCount = 0 this.tcpDataCache.collectedData = {} this.tcpDataCache.timestamp = '' this.tcpDataCache.type = '' } } }, // 处理完整的股票数据
processCompleteStockData(completeData) { // console.log("开始更新TCP股票数据存储")
// 更新TCP股票数据存储
this.tcpStockData = { count: completeData.count || 0, data: completeData.data || {}, stock_count: completeData.stock_count || 0, timestamp: completeData.timestamp || '', type: completeData.type || '' } // 获取所有股票的数据用于显示(最多3个)
const stockCodes = Object.keys(completeData.data) const newStockInfoList = [] // 处理最多3个股票数据
for (let i = 0; i < Math.min(stockCodes.length, 3); i++) { const stockCode = stockCodes[i] // 检查数据结构
if (completeData.data[stockCode] && Array.isArray(completeData.data[stockCode]) && completeData.data[stockCode].length > 0) { const stockData = completeData.data[stockCode][0] // 取第一条数据
if (stockData && stockData.current_price !== undefined && stockData.pre_close !== undefined) { // 计算涨跌幅
const changeValue = stockData.current_price - stockData.pre_close const changePercent = ((changeValue / stockData.pre_close) * 100).toFixed(2) const changeSign = changeValue >= 0 ? '+' : '' // 添加到股票信息列表
newStockInfoList.push({ stock_name: stockData.stock_name || '未知股票', current_price: stockData.current_price ? stockData.current_price.toFixed(2) : '0.00', change: `${changeSign}${changePercent}%`, change_value: changeValue, change_percent: parseFloat(changePercent) }) } } } // 更新当前显示的股票信息列表
if (newStockInfoList.length > 0) { this.currentStockInfoList = newStockInfoList // console.log('股票数据更新成功,共', newStockInfoList.length, '个股票:', this.currentStockInfoList)
} }, // 移除TCP监听器
removeTcpListeners() { if (this.connectionListener) { tcpConnection.removeConnectionListener(this.connectionListener) this.connectionListener = null // console.log('已移除TCP连接状态监听器')
} if (this.messageListener) { tcpConnection.removeMessageListener(this.messageListener) this.messageListener = null // console.log('已移除TCP消息监听器')
} } }}</script>
<style scoped>.main { display: flex; flex-direction: column; height: 100vh; background-color: #ffffff;}
.header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background-color: #ffffff;}
.title { font-size: 22px; font-weight: bold; text-align: center; flex: 1;}
.headphone-icon, .notification-icon { width: 40px; display: flex; align-items: center; justify-content: center;}
.header-icon-image { width: 24px; height: 24px; object-fit: contain;}
.content-container { flex: 1; padding: 10px; width: 100%; box-sizing: border-box; overflow-x: hidden;}
.section { margin-bottom: 15px; /* background-color: #f5f5f5; */ background-color: #ffffff; border-radius: 8px; padding: 15px;}
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px;}
.section-title { font-size: 18px; font-weight: bold; color: #000000;}.section-title-text{ font-size: 14px;
}
.text-gap{ height: 10px;}
.more-btn { font-size: 12px; font-weight: bold; color: #ffffff; background-color: #000000; padding: 4px 8px; border-radius: 4px;}
/* 市场概览样式 */.market-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background-color: #ffffff; margin-bottom: 10px;}
.market-content { background-color: #f5f5f5; border-radius: 8px; padding: 15px; margin: 0 15px;}
.market-image { margin-bottom: 15px; display: flex; justify-content: center;}
.overview-image { width: 100%; border-radius: 8px;}
.market-data { display: flex; flex-direction: column; gap: 10px;}
/* 间隔样式 */.section-gap { height: 20px;}
.market-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0;}
.down { color: #ff4d4f;}
.up { color: #52c41a;}
/* DeepMate样式 */.deepmate-container { background-color: #ffe6e6; border-radius: 10px; padding: 15px; margin: 0 15px;}
.deepmate-header { margin-bottom: 10px;}
.title-container { display: flex; align-items: center; justify-content: space-between; width: 100%;}
.title-left { width: 50%;}
.title-right { width: 50%; display: flex; justify-content: flex-end;}
.deepmate-title { font-size: 18px; font-weight: bold; color: #ff4d4f;}
.deepmate-icon { width: 60px; height: 60px; margin-left: 0;}
.deepmate-subtitle { font-size: 12px; color: #666; margin-left: 5px;}
.deepmate-hotspots { margin: 10px 0;}
.hotspot-item { background-color: #f5f5f5; padding: 8px 12px; border-radius: 6px; margin-bottom: 8px;}
.hotspot-item text { font-size: 14px; color: #333;}
.deepmate-action { display: flex; justify-content: center; align-items: center; background-color: #ffffff; border-radius: 20px; padding: 10px; margin-top: 10px;}
/* 欢迎部分样式 */.welcome-section { margin-bottom: 15px; padding: 0;}
.welcome-swiper { width: 100%; height: 110px; border-radius: 0; overflow: hidden;}
.deepmate-section { padding: 0;}
.swiper-image { width: 100%; height: 100%; border-radius: 8px; object-fit: contain;}
/* 深度探索样式 */.deep-exploration { margin-top: 15px; padding: 0; /* 移除内边距,让子容器自己控制 */}
.section-header-container { margin-bottom: 10px;}
.section-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background-color: #ffffff;}
.header-left { display: flex; align-items: center;}
.header-right { display: flex; align-items: center;}
.section-title { font-size: 16px; font-weight: bold; color: #333;}
.more-btn { font-size: 12px; color: #ffffff;}
.exploration-container { border-radius: 8px; overflow: hidden;}
.exploration-content { display: flex; justify-content: space-between; padding: 15px; background-color: #f5f5f5; border-radius: 8px;}
.exploration-item { display: flex; flex-direction: column; align-items: center; width: 22%; background-color: #ffffff; border-radius: 8px; padding: 10px 0;}
.exploration-icon { width: 50px; height: 50px; margin-bottom: 8px;}
.exploration-text { font-size: 12px; color: #333;}
.icon-text { font-size: 12px;}
/* 我的自选样式 */.my-selection { padding: 0; /* 移除内边距,让子容器自己控制 */}
.stock-container { border-radius: 8px; overflow: hidden; background-color: #f5f5f5; padding: 15px; box-sizing: border-box;}
.stock-list { display: flex; flex-direction: row; justify-content: center; background-color: #f8f8f8; border-radius: 8px; padding: 15px; gap: 10px; /* 添加卡片之间的间距 */}
.stock-item { display: flex; flex-direction: column; justify-content: space-between; padding: 3px; background-color: #ffffff; border-radius: 8px; width: 30%; box-shadow: 0 2px 5px rgba(0,0,0,0.1); will-change: transform; transform: translateZ(0);}
.stock-info { display: flex; flex-direction: column; align-items: center; margin-bottom: 5px;}
.stock-chart { display: flex; align-items: center; justify-content: center;}
.name-container { display: flex; align-items: center; gap: 5px;}
.stock-name { font-size: 12px; font-weight: bold; color: #333;}
.stock-code-label { font-size: 12px; color: #666; background-color: #f5f5f5; padding: 0 4px; border-radius: 3px;}
.stock-price { font-size: 12px; color: #666;}
.price-container { display: flex; align-items: center; gap: 5px;}
.stock-change { font-size: 12px;}
.stock-up { color: #4cd964;}
.stock-down { color: #ff3b30;}
.chart-image { width: 100px; height: 40px;}
/* 市场看点样式 */.highlights-title-container { background-color: #ffffff; margin-bottom: 10px;}
.highlights-image-container { background-color: #f5f5f5; border-radius: 8px; padding: 10px; margin-bottom: 15px;}
.highlights-image { width: 100%; height: 150px; border-radius: 4px;}
/* 机构动向简报样式 */.institutional-reports { margin-top: 15px; background-color: #f8f8f8; border-radius: 8px; padding: 10px;}
.section-title-container { position: relative; padding-left: 10px; margin-bottom: 5px;}
.section-title-container:before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background-color: #ff4d4f; border-radius: 2px;}
.report-item { background-color: #ffffff; border-radius: 8px; padding: 10px; margin-bottom: 8px;}
.report-stock { font-size: 14px; font-weight: bold; color: #e74c3c; margin-bottom: 5px;}
.report-status { font-size: 12px; color: #333;}
.view-more { text-align: center; color: #1890ff; font-size: 12px; padding: 5px;}
/* 底部空间 */.bottom-space { height: 60px;}
/* 底部导航 */.static-footer { position: fixed; bottom: 0; width: 100%;}
/* TCP测试区域样式 */.tcp-test-section { border: 1px solid #e0e0e0; background-color: #f9f9f9;}
.tcp-status { padding: 4px 12px; border-radius: 12px; font-size: 12px;}
.tcp-status.connected { background-color: #e8f5e8; color: #4caf50; border: 1px solid #4caf50;}
.tcp-status.disconnected { background-color: #ffeaea; color: #f44336; border: 1px solid #f44336;}
.status-text { font-size: 12px; font-weight: bold;}
.tcp-controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 15px;}
.tcp-btn { flex: 1; min-width: 80px; height: 36px; border-radius: 6px; font-size: 12px; border: none; color: white; font-weight: bold;}
.connect-btn { background-color: #4caf50;}
.connect-btn:disabled { background-color: #cccccc;}
.disconnect-btn { background-color: #f44336;}
.disconnect-btn:disabled { background-color: #cccccc;}
.send-btn { background-color: #2196f3;}
.send-btn:disabled { background-color: #cccccc;}
.status-btn { background-color: #ff9800;}
.tcp-messages { margin-top: 15px; border-top: 1px solid #e0e0e0; padding-top: 15px;}
.messages-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;}
.messages-title { font-size: 14px; font-weight: bold; color: #333;}
.clear-btn { padding: 4px 8px; background-color: #f44336; color: white; border: none; border-radius: 4px; font-size: 10px;}
.messages-list { max-height: 200px; border: 1px solid #e0e0e0; border-radius: 6px; padding: 8px; background-color: white;}
.message-item { margin-bottom: 8px; padding: 8px; border-radius: 6px; border-left: 3px solid #ccc;}
.message-item.sent { background-color: #e3f2fd; border-left-color: #2196f3;}
.message-item.received { background-color: #f1f8e9; border-left-color: #4caf50;}
.message-info { display: flex; justify-content: space-between; margin-bottom: 4px;}
.message-direction { font-size: 10px; font-weight: bold; color: #666;}
.message-time { font-size: 10px; color: #999;}
.message-content { font-size: 12px; color: #333; word-break: break-all;}
/* 我的自选TCP控制样式 */.my-stocks-tcp-control { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; padding: 8px 12px; background-color: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;}
.tcp-buttons { display: flex; gap: 6px;}
.my-stocks-tcp-control .tcp-btn { flex: none; min-width: 50px; height: 28px; border-radius: 4px; font-size: 11px; border: none; color: white; font-weight: bold;}</style>
|