You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2056 lines
57 KiB
2056 lines
57 KiB
<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="(item, index) in 5" :key="index">
|
|
<image class="swiper-image" src="https://d31zlh4on95l9h.cloudfront.net/images/e4272cc034fa2a3d1ca588ef84e51ab0.png" 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">查看更多</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">添加自选股</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">
|
|
<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">
|
|
<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' }
|
|
],
|
|
|
|
// 我的自选股票数据
|
|
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股票数据存储
|
|
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)
|
|
}
|
|
})
|
|
})
|
|
|
|
// 初始化TCP连接监听器
|
|
this.initTcpListeners()
|
|
|
|
// 检查用户登录状态,只有已登录并且不是游客才加载自选股数据
|
|
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.$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()
|
|
},
|
|
|
|
methods: {
|
|
// 防抖函数
|
|
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连接相关方法
|
|
|
|
// 初始化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": ["SH.000001","SH.000002","SH.000003"]}
|
|
|
|
// 发送消息
|
|
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": ["SH.000001","SH.000002","SH.000003"]}
|
|
|
|
// 发送消息到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: 150px;
|
|
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>
|