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.

2162 lines
60 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. <template>
  2. <view class="main">
  3. <!-- 顶部状态栏占位 -->
  4. <view class="top" :style="{height:iSMT+'px'}"></view>
  5. <!-- 登录的提示 -->
  6. <LoginPrompt ref="loginPrompt"></LoginPrompt>
  7. <!-- 头部导航 -->
  8. <view class="header">
  9. <view class="headphone-icon" @click="goToCustomerService">
  10. <image src="https://d31zlh4on95l9h.cloudfront.net/images/bef2edba6cc0c85671fde07cfab5270d.png" class="header-icon-image"></image>
  11. </view>
  12. <view class="title">DeepChart</view>
  13. <view class="notification-icon" @click="goToNotificationCenter">
  14. <image src="https://d31zlh4on95l9h.cloudfront.net/images/2554c84b91712d2a6cb6b00380e63bac.png" class="header-icon-image"></image>
  15. </view>
  16. </view>
  17. <!-- 内容区域 - 使用滚动视图 -->
  18. <scroll-view scroll-y class="content-container">
  19. <!-- 1. 今日市场概览 -->
  20. <market-overview :stock-info-list="currentStockInfoList"></market-overview>
  21. <!-- 间隔 -->
  22. <view class="section-gap"></view>
  23. <!-- 新增欢迎部分 -->
  24. <view class="section welcome-section">
  25. <!-- 轮播图 -->
  26. <swiper class="welcome-swiper" circular autoplay interval="3000" duration="500" indicator-dots indicator-active-color="#4080ff">
  27. <swiper-item v-for="(imageUrl, index) in swiperImages" :key="index">
  28. <image class="swiper-image" :src="imageUrl" mode="aspectFill"></image>
  29. </swiper-item>
  30. </swiper>
  31. </view>
  32. <!-- 2. DeepMate -->
  33. <view class="section deepmate-section">
  34. <DeepMate />
  35. </view>
  36. <!-- 3. 深度探索 -->
  37. <view class="section deep-exploration">
  38. <!-- 上部分标题和查看更多按钮 -->
  39. <view class="section-header-container">
  40. <view class="section-header">
  41. <view class="header-left">
  42. <text class="section-title">深度探索</text>
  43. </view>
  44. <view class="header-right">
  45. <text class="more-btn" @click="goToDeepExploration">查看更多</text>
  46. </view>
  47. </view>
  48. </view>
  49. <!-- 下部分四个图标 -->
  50. <view class="exploration-container">
  51. <view class="exploration-content">
  52. <view class="exploration-item">
  53. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/199472b0ee90a1c897f7c87b85accd84.png" mode="aspectFit" lazy-load="true"></image>
  54. <text class="exploration-text">主力追踪</text>
  55. </view>
  56. <view class="exploration-item">
  57. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/c25ca5e176efc961dabfa5d0d1b486b0.png" mode="aspectFit" lazy-load="true"></image>
  58. <text class="exploration-text">主力资达</text>
  59. </view>
  60. <view class="exploration-item">
  61. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/c064d7066dc8129a7df7b052762f82cf.png" mode="aspectFit" lazy-load="true"></image>
  62. <text class="exploration-text">主力解码</text>
  63. </view>
  64. <view class="exploration-item">
  65. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/9d69cceee9c515911477078af6f68d88.png" mode="aspectFit" lazy-load="true"></image>
  66. <text class="exploration-text">主力资金流</text>
  67. </view>
  68. </view>
  69. </view>
  70. </view>
  71. <!-- 4. 我的自选 -->
  72. <view class="section my-selection">
  73. <!-- 上部分标题和查看更多按钮 -->
  74. <view class="section-header-container">
  75. <view class="section-header">
  76. <text class="section-title">我的自选</text>
  77. <text class="more-btn" @click="goToMarketSituation">添加自选股</text>
  78. </view>
  79. <!-- 我的自选TCP连接状态和控制 -->
  80. <!-- <view class="my-stocks-tcp-control">
  81. <view class="tcp-status" :class="{'connected': myStocksTcpConnected, 'disconnected': !myStocksTcpConnected}">
  82. <text class="status-text">{{myStocksTcpConnected ? '自选已连接' : '自选未连接'}}</text>
  83. </view>
  84. <view class="tcp-buttons">
  85. <button class="tcp-btn connect-btn" @click="connectMyStocksTcp" :disabled="myStocksTcpConnected">连接</button>
  86. <button class="tcp-btn disconnect-btn" @click="disconnectMyStocksTcp" :disabled="!myStocksTcpConnected">断开</button>
  87. </view>
  88. </view> -->
  89. </view>
  90. <!-- 下部分股票列表 -->
  91. <view class="stock-container">
  92. <view class="stock-list">
  93. <!-- 如果有TCP数据显示实时数据否则显示静态数据 -->
  94. <view class="stock-item" v-for="(item, index) in (myStocksInfoList.length > 0 ? myStocksInfoList : myStocks)" :key="item.code || item.stock_code">
  95. <view class="stock-info">
  96. <view class="name-container">
  97. <text class="stock-name">{{item.name || item.stock_name}}</text>
  98. <text class="stock-code-label">{{item.code || item.stock_code}}</text>
  99. </view>
  100. <view class="price-container">
  101. <text class="stock-price">{{item.price || item.current_price}}</text>
  102. <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}">
  103. {{(item.change_percent || item.change || item.change_amount || 0) > 0 ? '+' : ''}}{{(parseFloat(item.change_percent || item.change || item.change_amount || 0)).toFixed(2)}}%
  104. </text>
  105. </view>
  106. </view>
  107. <view class="stock-chart">
  108. <image :src="item.chartImg || '/static/c5.png'" mode="aspectFit" class="chart-image"></image>
  109. </view>
  110. </view>
  111. </view>
  112. <!-- 机构动向简报数据 -->
  113. <view class="institutional-reports">
  114. <view class="section-title-container">
  115. <text class="section-title-text">机构动向简报</text>
  116. </view>
  117. <view class="text-gap"></view>
  118. <view class="report-item" v-for="(report, index) in institutionalReports" :key="index">
  119. <view class="report-stock">{{report.stock}}</view>
  120. <view class="report-status">{{report.status}}</view>
  121. </view>
  122. <view class="view-more" @click="goToCustomStockList">
  123. <text>查看更多 >></text>
  124. </view>
  125. </view>
  126. </view>
  127. </view>
  128. <!-- 5. TCP连接测试 -->
  129. <!-- <view class="section tcp-test-section">
  130. <view class="section-header">
  131. <text class="section-title">TCP连接测试</text>
  132. <view class="tcp-status" :class="{'connected': tcpConnected, 'disconnected': !tcpConnected}">
  133. <text class="status-text">{{tcpConnected ? '已连接' : '未连接'}}</text>
  134. </view>
  135. </view> -->
  136. <!-- TCP控制按钮 -->
  137. <!-- <view class="tcp-controls">
  138. <button class="tcp-btn connect-btn" @click="connectTcp" :disabled="tcpConnected">连接</button>
  139. <button class="tcp-btn disconnect-btn" @click="disconnectTcp" :disabled="!tcpConnected">断开</button>
  140. <button class="tcp-btn send-btn" @click="sendTcpMessage" :disabled="!tcpConnected">发送测试消息</button>
  141. <button class="tcp-btn status-btn" @click="getTcpStatus">查看状态</button>
  142. </view> -->
  143. <!-- 消息记录 -->
  144. <!-- <view class="tcp-messages" v-if="tcpMessages.length > 0">
  145. <view class="messages-header">
  146. <text class="messages-title">消息记录 ({{tcpMessages.length}})</text>
  147. <button class="clear-btn" @click="clearTcpMessages">清空</button>
  148. </view>
  149. <scroll-view scroll-y class="messages-list" scroll-top="{{tcpMessages.length * 50}}">
  150. <view class="message-item" v-for="(msg, index) in tcpMessages" :key="index"
  151. :class="{'sent': msg.direction === 'sent', 'received': msg.direction === 'received'}">
  152. <view class="message-info">
  153. <text class="message-direction">{{msg.direction === 'sent' ? '发送' : '接收'}}</text>
  154. <text class="message-time">{{msg.timestamp}}</text>
  155. </view>
  156. <text class="message-content">{{msg.content}}</text>
  157. </view>
  158. </scroll-view>
  159. </view>
  160. </view> -->
  161. <!-- 6. 今日市场看点 -->
  162. <view class="section-header highlights-title-container">
  163. <text class="section-title">今日市场核心看点</text>
  164. </view>
  165. <view class="highlights-image-container" @click="goToAnalysisInstitutionalTrends">
  166. <image src="https://d31zlh4on95l9h.cloudfront.net/images/8d5365af968402a18cedb120c09460b0.png" mode="aspectFit" class="highlights-image"></image>
  167. </view>
  168. <!-- 底部空间 - 为底部导航腾出空间 -->
  169. <view class="bottom-space"></view>
  170. </scroll-view>
  171. <!-- 底部导航 -->
  172. <footerBar class="static-footer" :type="type"></footerBar>
  173. </view>
  174. </template>
  175. <script>
  176. import footerBar from '../../components/footerBar.vue'
  177. import MarketOverview from '../../components/MarketOverview.vue'
  178. import DeepMate from '../../components/DeepMate.vue'
  179. import tcpConnection from '../../api/tcpConnection.js'
  180. import th from '../../static/language/th'
  181. import MySelectionsAPI from '../../api/home/mySelections.js'
  182. import { useUserStore } from '../../stores/modules/userInfo'
  183. export default {
  184. components: {
  185. footerBar,
  186. MarketOverview,
  187. DeepMate
  188. },
  189. data() {
  190. return {
  191. type: 'home',
  192. iSMT: 0,
  193. // 用户信息 store
  194. userStore: null,
  195. // 深度探索数据
  196. explorationItems: [
  197. { title: '主力追踪', icon: '/static/c1.png' },
  198. { title: '主力资金', icon: '/static/c2.png' },
  199. { title: '主力解码', icon: '/static/c3.png' },
  200. { title: '主力资金流', icon: '/static/c4.png' }
  201. ],
  202. // 轮播图片数据
  203. swiperImages: [
  204. 'https://d31zlh4on95l9h.cloudfront.net/images/44ec37501cf3ecf5438d401c3d66fcd3.png',
  205. 'https://d31zlh4on95l9h.cloudfront.net/images/80274b6626bb403d717469b3672333c9.png',
  206. 'https://d31zlh4on95l9h.cloudfront.net/images/14481c76b09509977d3879ba3141ebc7.png',
  207. 'https://d31zlh4on95l9h.cloudfront.net/images/19e4c5542e5f974e1c6938897aa20ef9.png',
  208. 'https://d31zlh4on95l9h.cloudfront.net/images/d0bc1c1bb71917132865b82dab99831b.png'
  209. ],
  210. // 我的自选股票数据
  211. myStocks: [
  212. { name: '特斯拉', code: 'TSLA', price: '482.00', change: 2.80, chartImg: '/static/c5.png' },
  213. { name: '英伟达', code: 'NVDA', price: '189.800', change: -2.92, chartImg: '/static/c6.png' },
  214. { name: '苹果', code: 'AAPL', price: '256.430', change: 2.60, chartImg: '/static/c7.png' }
  215. ],
  216. // 机构动向简报数据
  217. institutionalReports: [
  218. { stock: '特斯拉', status: '当前市场多头资金占比,且多头资金持续流入。' },
  219. { stock: '英伟达', status: '当前市场多头资金占比,且多头资金持续流入。' },
  220. { stock: '苹果', status: '当前市场多头资金占比,且多头资金持续流入。' }
  221. ],
  222. // 防抖定时器
  223. debounceTimer: null,
  224. // TCP连接相关状态
  225. tcpConnected: false,
  226. tcpMessages: [],
  227. lastSentMessage: '',
  228. // 我的自选TCP连接状态
  229. myStocksTcpConnected: false,
  230. // TCP监听器引用,用于移除监听器
  231. connectionListener: null,
  232. messageListener: null,
  233. // TCP股票数据存储
  234. tcpStockData: {
  235. count: 0,
  236. data: {},
  237. stock_count: 0,
  238. timestamp: '',
  239. type: ''
  240. },
  241. // 当前TCP使用的股票代码
  242. currentStockCodes: ['SH.000001', 'SH.000002', 'SH.000003'],
  243. // 我的自选TCP股票数据存储
  244. myStocksTcpData: {
  245. count: 0,
  246. data: {},
  247. stock_count: 0,
  248. timestamp: '',
  249. type: ''
  250. },
  251. // TCP数据缓存机制,用于处理分片数据
  252. tcpDataCache: {
  253. isCollecting: false, // 是否正在收集数据片段
  254. expectedCount: 0, // 期望的数据总数
  255. collectedData: {}, // 已收集的数据对象(用于对象级拼接)
  256. timestamp: '', // 数据时间戳
  257. type: '', // 数据类型
  258. // 新增:字符串片段缓存
  259. firstPartData: '', // 前半部分数据字符串
  260. isWaitingSecondPart: false // 是否正在等待后半部分数据
  261. },
  262. // 我的自选TCP数据缓存机制,用于处理分片数据
  263. myStocksTcpDataCache: {
  264. isCollecting: false, // 是否正在收集数据片段
  265. expectedCount: 0, // 期望的数据总数
  266. collectedData: {}, // 已收集的数据对象(用于对象级拼接)
  267. timestamp: '', // 数据时间戳
  268. type: '', // 数据类型
  269. // 字符串片段缓存
  270. firstPartData: '', // 前半部分数据字符串
  271. isWaitingSecondPart: false // 是否正在等待后半部分数据
  272. },
  273. // 当前显示的3个股票数据(用于MarketOverview组件)
  274. currentStockInfoList: [
  275. {
  276. stock_name: '美元/日元',
  277. current_price: '151.13',
  278. change: '+1.62%',
  279. change_value: 0,
  280. change_percent: 0
  281. },
  282. {
  283. stock_name: '美元/韩元',
  284. current_price: '1424.900',
  285. change: '-2.92%',
  286. change_value: 0,
  287. change_percent: 0
  288. },
  289. {
  290. stock_name: '美元/英镑',
  291. current_price: '0.730',
  292. change: '+2.92%',
  293. change_value: 0,
  294. change_percent: 0
  295. }
  296. ],
  297. // 我的自选处理后的股票信息列表
  298. myStocksInfoList: []
  299. }
  300. },
  301. // Vue 2生命周期方法
  302. mounted() {
  303. // 状态栏高度
  304. this.iSMT = uni.getSystemInfoSync().statusBarHeight;
  305. // 初始化用户信息 store
  306. this.userStore = useUserStore();
  307. // 预加载图片资源
  308. this.myStocks.forEach(stock => {
  309. // 使用uni.getImageInfo替代Image对象
  310. uni.getImageInfo({
  311. src: stock.chartImg,
  312. success: function(res) {
  313. // 图片加载成功
  314. console.log('图片预加载成功:', stock.name)
  315. },
  316. fail: function(err) {
  317. console.log('图片预加载失败:', err)
  318. }
  319. })
  320. })
  321. // 检查用户登录状态,只有已登录并且不是游客才加载自选股数据
  322. if (this.userStore.userInfo && !this.userStore.userInfo.isVisitor) {
  323. console.log('是用户登录,加载自选股数据', this.userStore.userInfo)
  324. this.loadMySelectionsData()
  325. } else if (this.userStore.userInfo && this.userStore.userInfo.isVisitor){
  326. console.log('是游客登录,加载默认自选股',this.userStore.userInfo)
  327. this.loadGuestDefaultStocks()
  328. }else {
  329. console.log('用户未登录',this.userStore.userInfo)
  330. }
  331. // 初始化TCP连接监听器
  332. this.initTcpListeners()
  333. // 监听游客登录成功事件
  334. uni.$on('visitorLoginSuccess', this.handleVisitorLoginSuccess)
  335. // 页面渲染完成后自动连接两个TCP连接
  336. // this.$nextTick(() => {
  337. // console.log('页面渲染完成,开始自动连接TCP服务器...')
  338. // // 延迟一小段时间确保页面完全加载后再连接
  339. // setTimeout(() => {
  340. // // 连接今日市场概览TCP(channel 1)
  341. // console.log('连接今日市场概览TCP(channel 1)...')
  342. // this.connectTcp()
  343. // // 稍微延迟后连接我的自选TCP(channel 2)
  344. // setTimeout(() => {
  345. // console.log('连接我的自选TCP(channel 2)...')
  346. // this.connectMyStocksTcp()
  347. // }, 500)
  348. // }, 1000)
  349. // })
  350. },
  351. // 页面销毁前的清理工作
  352. onUnload() {
  353. // 自动关闭主TCP连接
  354. if (this.tcpConnected) {
  355. console.log('页面销毁,自动关闭主TCP连接')
  356. tcpConnection.disconnect({
  357. ip: '39.102.136.61',
  358. port: '8080',
  359. channel: '1',
  360. charsetname: 'UTF-8'
  361. })
  362. this.tcpConnected = false
  363. }
  364. // 自动关闭我的自选TCP连接
  365. if (this.myStocksTcpConnected) {
  366. console.log('页面销毁,自动关闭我的自选TCP连接')
  367. tcpConnection.disconnect({
  368. ip: '39.102.136.61',
  369. port: '8088',
  370. channel: '2',
  371. charsetname: 'UTF-8'
  372. })
  373. this.myStocksTcpConnected = false
  374. }
  375. // 清理防抖定时器
  376. if (this.debounceTimer) {
  377. clearTimeout(this.debounceTimer)
  378. this.debounceTimer = null
  379. }
  380. // 移除TCP监听器,防止内存泄漏
  381. this.removeTcpListeners()
  382. // 移除游客登录成功事件监听器
  383. uni.$off('visitorLoginSuccess', this.handleVisitorLoginSuccess)
  384. },
  385. methods: {
  386. // 跳转到客服中台
  387. goToCustomerService() {
  388. uni.navigateTo({
  389. url: '/pages/customerServicePlatform/csPlatformIndex'
  390. })
  391. },
  392. // 跳转到通知中心
  393. goToNotificationCenter() {
  394. uni.navigateTo({
  395. url: '/pages/blank/notice'
  396. })
  397. },
  398. // 跳转到自定义股票列表页面
  399. goToCustomStockList() {
  400. uni.navigateTo({
  401. url: '/pages/customStockList/customStockList'
  402. })
  403. },
  404. // 跳转到行情页面
  405. goToMarketSituation() {
  406. uni.navigateTo({
  407. url: '/pages/marketSituation/marketSituation'
  408. })
  409. },
  410. // 跳转到机构动向解析页面
  411. goToAnalysisInstitutionalTrends() {
  412. uni.navigateTo({
  413. url: '/pages/analysisInstitutionalTrends/analysisInstitutionalTrends'
  414. })
  415. },
  416. // 跳转到深度探索页面
  417. goToDeepExploration() {
  418. uni.navigateTo({
  419. url: '/pages/deepExploration/deepExploration'
  420. })
  421. },
  422. // 处理游客登录成功事件
  423. handleVisitorLoginSuccess(data) {
  424. console.log('收到游客登录成功事件:', data)
  425. // 重新加载页面数据
  426. this.reloadPageData()
  427. },
  428. // 重新加载页面数据
  429. reloadPageData() {
  430. console.log('重新加载页面数据...')
  431. // 更新用户信息store
  432. this.userStore = useUserStore()
  433. // 根据新的用户状态加载相应的数据
  434. if (this.userStore.userInfo && this.userStore.userInfo.isVisitor) {
  435. console.log('游客登录成功,加载默认自选股')
  436. this.loadGuestDefaultStocks()
  437. } else if (this.userStore.userInfo && !this.userStore.userInfo.isVisitor) {
  438. console.log('用户登录,加载自选股数据')
  439. this.loadMySelectionsData()
  440. }
  441. },
  442. // 防抖函数
  443. debounce(fn, delay = 300) {
  444. if (this.debounceTimer) clearTimeout(this.debounceTimer)
  445. this.debounceTimer = setTimeout(() => {
  446. fn()
  447. this.debounceTimer = null
  448. }, delay)
  449. },
  450. // 自选股数据加载相关方法
  451. // 加载自选股数据的主方法
  452. async loadMySelectionsData() {
  453. try {
  454. console.log('开始加载自选股数据...')
  455. // 调用checkExist判断用户是否存在自选股
  456. const checkResult = await MySelectionsAPI.checkExist()
  457. console.log('检查用户自选股存在性结果:', checkResult)
  458. if (checkResult.code === 200) {
  459. const isHave = checkResult.data.is_have
  460. if (isHave === 1) {
  461. // 用户有自选股,查询所有分组
  462. console.log('用户存在自选股,查询所有分组...')
  463. await this.loadUserStockGroups()
  464. } else {
  465. // 用户没有自选股,查询默认自选股
  466. console.log('用户不存在自选股,查询默认自选股...')
  467. await this.loadDefaultStocks()
  468. }
  469. } else {
  470. console.error('检查用户自选股存在性失败:', checkResult.message)
  471. // 失败时也尝试加载默认自选股
  472. // await this.loadDefaultStocks()
  473. }
  474. } catch (error) {
  475. console.error('加载自选股数据失败:', error)
  476. // 出错时尝试加载默认自选股
  477. await this.loadDefaultStocks()
  478. }
  479. },
  480. // 加载用户自选股分组
  481. async loadUserStockGroups() {
  482. try {
  483. const groupResult = await MySelectionsAPI.getUserStockGroupList()
  484. console.log('查询用户自选股分组结果:', groupResult)
  485. if (groupResult.code === 200 && groupResult.data && groupResult.data.length > 0) {
  486. // 找到id最小的分组
  487. const minIdGroup = groupResult.data.reduce((min, current) => {
  488. return current.id < min.id ? current : min
  489. })
  490. console.log('找到最小id分组:', minIdGroup)
  491. // 查询该分组下的股票
  492. await this.loadGroupStocks(minIdGroup.id)
  493. } else {
  494. console.log('没有找到用户自选股分组,加载默认自选股')
  495. await this.loadDefaultStocks()
  496. }
  497. } catch (error) {
  498. console.error('加载用户自选股分组失败:', error)
  499. await this.loadDefaultStocks()
  500. }
  501. },
  502. // 加载指定分组下的股票
  503. async loadGroupStocks(groupId) {
  504. try {
  505. const stockResult = await MySelectionsAPI.getUserStockList(null, null, {
  506. groupId: groupId,
  507. pageNum: 1,
  508. pageSize: 20
  509. })
  510. console.log('查询分组自选股结果:', stockResult)
  511. if (stockResult.code === 200 && stockResult.data) {
  512. this.processStockData(stockResult.data)
  513. } else {
  514. console.log('分组下没有股票数据,加载默认自选股')
  515. await this.loadDefaultStocks()
  516. }
  517. } catch (error) {
  518. console.error('加载分组股票失败:', error)
  519. await this.loadDefaultStocks()
  520. }
  521. },
  522. // 加载默认自选股
  523. async loadDefaultStocks() {
  524. try {
  525. const defaultResult = await MySelectionsAPI.getUserOrDefault()
  526. console.log('查询默认自选股结果:', defaultResult)
  527. if (defaultResult.code === 200 && defaultResult.data) {
  528. this.processStockData(defaultResult.data)
  529. } else {
  530. console.log('没有默认自选股数据')
  531. }
  532. } catch (error) {
  533. console.error('加载默认自选股失败:', error)
  534. }
  535. },
  536. // 游客加载默认自选股
  537. async loadGuestDefaultStocks() {
  538. try {
  539. console.log('游客开始加载默认自选股...')
  540. const guestResult = await MySelectionsAPI.getDefaultStocks()
  541. console.log('游客查询默认自选股结果:', guestResult)
  542. if (guestResult.code === 200 && guestResult.data) {
  543. this.processStockData(guestResult.data)
  544. } else {
  545. console.log('游客没有默认自选股数据')
  546. }
  547. } catch (error) {
  548. console.error('游客加载默认自选股失败:', error)
  549. }
  550. },
  551. // 处理股票数据
  552. processStockData(stockData) {
  553. console.log('处理股票数据:', stockData)
  554. // 根据返回的数据结构更新myStocks数组
  555. // 这里需要根据实际API返回的数据结构进行调整
  556. if (Array.isArray(stockData)) {
  557. // 如果是数组格式
  558. this.myStocks = stockData.map(stock => ({
  559. name: stock.name || stock.stock_name || '',
  560. code: stock.code || stock.stock_code || '',
  561. // price: stock.price || stock.current_price || '0.00',
  562. // change: stock.change || stock.change_percent || 0,
  563. chartImg: stock.chartImg || '/static/c5.png' // 默认图片
  564. }))
  565. //重新赋值机构动向简报
  566. this.institutionalReports = stockData.map(stock => ({
  567. stock: stock.name || stock.stock_name || '',
  568. status: stock.code || stock.stock_code || '',
  569. }))
  570. } else if (stockData.list && Array.isArray(stockData.list)) {
  571. // 如果是分页格式
  572. this.myStocks = stockData.list.map(stock => ({
  573. name: stock.name || stock.stock_name || '',
  574. code: stock.code || stock.stock_code || '',
  575. // price: stock.price || stock.current_price || '0.00',
  576. // change: stock.change || stock.change_percent || 0,
  577. chartImg: stock.chartImg || '/static/c5.png' // 默认图片
  578. }))
  579. //重新赋值机构动向简报
  580. //重新赋值机构动向简报
  581. this.institutionalReports = stockData.map(stock => ({
  582. stock: stock.name || stock.stock_name || '',
  583. status: stock.code || stock.stock_code || '',
  584. }))
  585. }
  586. console.log('更新后的自选股数据:', this.myStocks)
  587. // 更新TCP消息中的股票代码
  588. this.updateTcpStockCodes()
  589. },
  590. // 更新TCP消息中的股票代码
  591. updateTcpStockCodes() {
  592. // 从myStocks中提取前3个股票代码
  593. const stockCodes = this.myStocks.slice(0, 3).map(stock => stock.code).filter(code => code)
  594. console.log('更新TCP消息股票代码:', stockCodes)
  595. // 如果有股票代码,更新TCP消息
  596. if (stockCodes.length > 0) {
  597. // 补充到3个代码(如果不足3个,用默认代码补充)
  598. while (stockCodes.length < 3) {
  599. stockCodes.push('SH.000001') // 默认代码
  600. }
  601. // 保存到组件数据中,供TCP方法使用
  602. this.currentStockCodes = stockCodes
  603. console.log('当前TCP使用的股票代码:', this.currentStockCodes)
  604. }
  605. },
  606. // TCP连接相关方法
  607. // 初始化TCP监听器
  608. initTcpListeners() {
  609. // 创建连接状态监听器并保存引用
  610. this.connectionListener = (status, result, channel) => {
  611. console.log('TCP连接状态变化:', status, 'channel:', channel)
  612. // 根据channel更新对应的连接状态
  613. if (channel === '1') {
  614. // 主TCP连接
  615. this.tcpConnected = (status === 'connected')
  616. console.log('主TCP连接状态:', this.tcpConnected)
  617. // 显示连接状态提示
  618. uni.showToast({
  619. title: status === 'connected' ? '主连接服务器成功' : '主服务器连接断开',
  620. icon: status === 'connected' ? 'success' : 'none',
  621. duration: 1000
  622. })
  623. // 连接成功后自动发送股票数据请求命令
  624. if (status === 'connected') {
  625. console.log('主TCP连接成功,自动发送股票数据请求...')
  626. // 延迟一小段时间确保连接稳定后再发送命令
  627. setTimeout(() => {
  628. this.sendTcpMessage()
  629. }, 500)
  630. }
  631. } else if (channel === '2') {
  632. // 我的自选TCP连接
  633. this.myStocksTcpConnected = (status === 'connected')
  634. console.log('我的自选TCP连接状态:', this.myStocksTcpConnected)
  635. // 显示连接状态提示
  636. uni.showToast({
  637. title: status === 'connected' ? '我的自选连接成功' : '我的自选连接断开',
  638. icon: status === 'connected' ? 'success' : 'none',
  639. duration: 1000
  640. })
  641. // 连接成功后自动发送我的自选股票数据请求
  642. if (status === 'connected') {
  643. console.log('我的自选TCP连接成功,自动发送自选股票数据请求...')
  644. // 延迟一小段时间确保连接稳定后再发送命令
  645. setTimeout(() => {
  646. this.sendMyStocksTcpMessage()
  647. }, 500)
  648. }
  649. }
  650. }
  651. // 创建消息监听器并保存引用
  652. this.messageListener = (type, message, parsedArray, channel) => {
  653. const messageObj = {
  654. type: type,
  655. content: message,
  656. parsedArray: parsedArray,
  657. channel: channel,
  658. timestamp: new Date().toLocaleTimeString(),
  659. direction: 'received'
  660. }
  661. this.tcpMessages.push(messageObj)
  662. console.log('收到TCP消息:', messageObj)
  663. console.log('消息来源channel:', channel)
  664. // 根据channel调用相应的数据处理方法
  665. if (channel === '1') {
  666. // 主TCP连接 - 今日市场概览数据
  667. // console.log('处理今日市场概览数据')
  668. this.parseStockData(message)
  669. } else if (channel === '2') {
  670. // 我的自选TCP连接 - 我的自选数据
  671. console.log('处理我的自选数据')
  672. this.parseMyStocksData(message)
  673. } else {
  674. console.warn('未知的channel:', channel)
  675. }
  676. }
  677. // 注册监听器
  678. tcpConnection.onConnectionChange(this.connectionListener)
  679. tcpConnection.onMessage(this.messageListener)
  680. },
  681. // 连接TCP服务器
  682. connectTcp() {
  683. // console.log('开始连接TCP服务器...')
  684. // 先断开现有连接(如果存在)
  685. if (this.tcpConnected) {
  686. // console.log('检测到现有连接,先断开...')
  687. this.disconnectTcp()
  688. // 等待断开完成后再连接
  689. setTimeout(() => {
  690. this.performTcpConnect()
  691. }, 500)
  692. } else {
  693. // 直接连接
  694. this.performTcpConnect()
  695. }
  696. },
  697. // 执行TCP连接
  698. performTcpConnect() {
  699. // console.log('执行TCP连接...')
  700. tcpConnection.connect(
  701. {
  702. ip: '39.102.136.61',
  703. port: '8088',
  704. channel: '1', // 可选 1~20
  705. charsetname: 'UTF-8' // 默认UTF-8,可选GBK
  706. }
  707. )
  708. },
  709. // 断开TCP连接
  710. disconnectTcp() {
  711. // console.log('断开TCP连接...')
  712. tcpConnection.disconnect(
  713. {
  714. ip: '39.102.136.61',
  715. port: '8088',
  716. channel: '1', // 可选 1~20
  717. charsetname: 'UTF-8' // 默认UTF-8,可选GBK
  718. }
  719. )
  720. this.tcpConnected = false
  721. },
  722. // 连接我的自选TCP服务器
  723. connectMyStocksTcp() {
  724. console.log('开始连接我的自选TCP服务器...')
  725. // 先断开现有连接(如果存在)
  726. if (this.myStocksTcpConnected) {
  727. console.log('检测到我的自选现有连接,先断开...')
  728. this.disconnectMyStocksTcp()
  729. // 等待断开完成后再连接
  730. setTimeout(() => {
  731. this.performMyStocksTcpConnect()
  732. }, 500)
  733. } else {
  734. // 直接连接
  735. this.performMyStocksTcpConnect()
  736. }
  737. },
  738. // 执行我的自选TCP连接
  739. performMyStocksTcpConnect() {
  740. console.log('执行我的自选TCP连接...')
  741. tcpConnection.connect(
  742. {
  743. ip: '39.102.136.61',
  744. port: '8088',
  745. channel: '2', // 我的自选使用channel 2
  746. charsetname: 'UTF-8' // 默认UTF-8,可选GBK
  747. }
  748. )
  749. },
  750. // 断开我的自选TCP连接
  751. disconnectMyStocksTcp() {
  752. console.log('断开我的自选TCP连接...')
  753. tcpConnection.disconnect(
  754. {
  755. ip: '39.102.136.61',
  756. port: '8088',
  757. channel: '2', // 我的自选使用channel 2
  758. charsetname: 'UTF-8' // 默认UTF-8,可选GBK
  759. }
  760. )
  761. this.myStocksTcpConnected = false
  762. },
  763. // 解析我的自选TCP股票数据
  764. parseMyStocksData(message) {
  765. try {
  766. console.log('进入parseMyStocksData, message类型:', typeof message, '长度:', message.length)
  767. // 第一步:检查数据状态(完整、前半部分、后半部分)
  768. const dataStatus = this.getMyStocksDataStatus(message)
  769. let completeMessage = ''
  770. switch (dataStatus) {
  771. case 'complete':
  772. // 完整数据,直接处理
  773. console.log('我的自选:检测到完整数据,直接处理')
  774. completeMessage = message
  775. break
  776. case 'first_part':
  777. // 前半部分数据,缓存并等待后半部分
  778. console.log('我的自选:检测到前半部分数据,开始缓存')
  779. this.cacheMyStocksFirstPartData(message)
  780. return // 等待后半部分数据
  781. case 'second_part':
  782. // 后半部分数据,检查是否有缓存的前半部分
  783. if (this.myStocksTcpDataCache.isWaitingSecondPart) {
  784. console.log('我的自选:检测到后半部分数据,开始拼接')
  785. completeMessage = this.concatenateMyStocksDataParts(message)
  786. } else {
  787. console.log('我的自选:收到后半部分数据,但没有缓存的前半部分,跳过处理')
  788. return
  789. }
  790. break
  791. case 'invalid':
  792. default:
  793. console.log('我的自选:数据格式无效,跳过处理')
  794. return
  795. }
  796. // 第二步:解析完整的JSON数据
  797. let parsedMessage
  798. try {
  799. console.log('我的自选:开始解析完整JSON数据,长度:', completeMessage.length)
  800. parsedMessage = JSON.parse(completeMessage)
  801. console.log('我的自选:JSON解析成功,解析后类型:', typeof parsedMessage, parsedMessage)
  802. } catch (parseError) {
  803. console.error('我的自选:JSON解析失败:', parseError.message)
  804. // 清空字符串片段缓存
  805. this.clearMyStocksStringFragmentCache()
  806. return
  807. }
  808. // 第三步:检查是否是股票数据类型
  809. if (!((parsedMessage.type === 'batch_data_chunk' || parsedMessage.type === 'batch_realtime_data') && parsedMessage.data)) {
  810. console.log('我的自选:不是batch_data_chunk或batch_realtime_data类型的消息,跳过处理')
  811. return
  812. }
  813. console.log('我的自选:开始处理股票数据')
  814. // 第四步:验证数据完整性(对象级别的完整性检查)
  815. // 注意:batch_data_chunk类型的数据不需要验证完整性,直接处理
  816. if (parsedMessage.type === 'batch_data_chunk') {
  817. console.log('我的自选:batch_data_chunk类型数据,跳过完整性验证,直接处理')
  818. this.processCompleteMyStocksData(parsedMessage)
  819. } else {
  820. const isDataComplete = this.validateMyStocksDataIntegrity(parsedMessage)
  821. if (isDataComplete) {
  822. // 数据完整,直接处理
  823. console.log('我的自选:对象级数据完整,直接处理')
  824. this.processCompleteMyStocksData(parsedMessage)
  825. } else {
  826. // 数据不完整,需要拼接(对象级别的拼接)
  827. console.log('我的自选:对象级数据不完整,开始拼接处理')
  828. // 将当前数据合并到缓存中
  829. this.mergeMyStocksDataToCache(parsedMessage)
  830. // 检查缓存中的数据是否已经完整
  831. if (this.isMyStocksCacheDataComplete()) {
  832. console.log('我的自选:缓存数据已完整,开始处理')
  833. const completeData = this.getCompleteMyStocksDataFromCache()
  834. this.processCompleteMyStocksData(completeData)
  835. } else {
  836. console.log('我的自选:缓存数据仍不完整,等待更多数据片段')
  837. }
  838. }
  839. }
  840. } catch (error) {
  841. console.error('我的自选:解析TCP股票数据失败:', error.message)
  842. console.error('我的自选:错误详情:', error)
  843. // 发生错误时清空所有缓存
  844. this.clearMyStocksStringFragmentCache()
  845. if (this.myStocksTcpDataCache.isCollecting) {
  846. console.log('我的自选:发生错误,清空对象级数据缓存')
  847. this.myStocksTcpDataCache.isCollecting = false
  848. this.myStocksTcpDataCache.expectedCount = 0
  849. this.myStocksTcpDataCache.collectedData = {}
  850. this.myStocksTcpDataCache.timestamp = ''
  851. this.myStocksTcpDataCache.type = ''
  852. }
  853. }
  854. },
  855. // 验证我的自选数据完整性
  856. validateMyStocksDataIntegrity(parsedMessage) {
  857. if (!parsedMessage.count || !parsedMessage.data) {
  858. return false
  859. }
  860. const dataObjectCount = Object.keys(parsedMessage.data).length
  861. const expectedCount = parsedMessage.count
  862. console.log(`我的自选数据完整性验证: 期望${expectedCount}个对象,实际${dataObjectCount}个对象`)
  863. return dataObjectCount === expectedCount
  864. },
  865. // 检查我的自选数据状态(完整数据、前半部分数据、后半部分数据)
  866. getMyStocksDataStatus(message) {
  867. if (typeof message !== 'string') {
  868. return 'invalid'
  869. }
  870. const trimmedMessage = message.trim()
  871. const startsWithBrace = trimmedMessage.startsWith('{')
  872. const endsWithBrace = trimmedMessage.endsWith('}')
  873. if (startsWithBrace && endsWithBrace) {
  874. // 以{开头,以}结尾 - 完整数据
  875. console.log('我的自选:检测到完整数据格式')
  876. return 'complete'
  877. } else if (startsWithBrace && !endsWithBrace) {
  878. // 以{开头,不以}结尾 - 前半部分数据
  879. console.log('我的自选:检测到前半部分数据')
  880. return 'first_part'
  881. } else if (!startsWithBrace && endsWithBrace) {
  882. // 不以{开头,以}结尾 - 后半部分数据
  883. console.log('我的自选:检测到后半部分数据')
  884. return 'second_part'
  885. } else {
  886. // 其他情况 - 无效数据
  887. console.log('我的自选:检测到无效数据格式')
  888. return 'invalid'
  889. }
  890. },
  891. // 缓存我的自选前半部分数据
  892. cacheMyStocksFirstPartData(message) {
  893. this.myStocksTcpDataCache.firstPartData = message.trim()
  894. this.myStocksTcpDataCache.isWaitingSecondPart = true
  895. console.log('我的自选:已缓存前半部分数据,长度:', this.myStocksTcpDataCache.firstPartData.length)
  896. },
  897. // 拼接我的自选前后两部分数据
  898. concatenateMyStocksDataParts(secondPartMessage) {
  899. const completeMessage = this.myStocksTcpDataCache.firstPartData + secondPartMessage.trim()
  900. console.log('我的自选:数据拼接完成,完整数据长度:', completeMessage.length)
  901. // 清空缓存
  902. this.clearMyStocksStringFragmentCache()
  903. return completeMessage
  904. },
  905. // 清空我的自选字符串片段缓存
  906. clearMyStocksStringFragmentCache() {
  907. this.myStocksTcpDataCache.firstPartData = ''
  908. this.myStocksTcpDataCache.isWaitingSecondPart = false
  909. console.log('我的自选:字符串片段缓存已清空')
  910. },
  911. // 合并我的自选数据到缓存中
  912. mergeMyStocksDataToCache(parsedMessage) {
  913. // 如果是第一次收集数据,初始化缓存
  914. if (!this.myStocksTcpDataCache.isCollecting) {
  915. this.myStocksTcpDataCache.isCollecting = true
  916. this.myStocksTcpDataCache.expectedCount = parsedMessage.count
  917. this.myStocksTcpDataCache.collectedData = {}
  918. this.myStocksTcpDataCache.timestamp = parsedMessage.timestamp
  919. this.myStocksTcpDataCache.type = parsedMessage.type
  920. console.log('我的自选:开始收集数据片段,期望总数:', parsedMessage.count)
  921. }
  922. // 合并新数据到缓存中
  923. if (parsedMessage.data) {
  924. Object.assign(this.myStocksTcpDataCache.collectedData, parsedMessage.data)
  925. console.log('我的自选:数据片段已合并,当前已收集:', Object.keys(this.myStocksTcpDataCache.collectedData).length, '个对象')
  926. }
  927. },
  928. // 检查我的自选缓存数据是否完整
  929. isMyStocksCacheDataComplete() {
  930. const collectedCount = Object.keys(this.myStocksTcpDataCache.collectedData).length
  931. const expectedCount = this.myStocksTcpDataCache.expectedCount
  932. console.log(`我的自选缓存数据检查: 已收集${collectedCount}个,期望${expectedCount}`)
  933. return collectedCount === expectedCount && collectedCount > 0
  934. },
  935. // 获取我的自选完整的缓存数据并清空缓存
  936. getCompleteMyStocksDataFromCache() {
  937. const completeData = {
  938. count: this.myStocksTcpDataCache.expectedCount,
  939. data: { ...this.myStocksTcpDataCache.collectedData },
  940. stock_count: this.myStocksTcpDataCache.expectedCount,
  941. timestamp: this.myStocksTcpDataCache.timestamp,
  942. type: this.myStocksTcpDataCache.type
  943. }
  944. // 清空缓存
  945. this.myStocksTcpDataCache.isCollecting = false
  946. this.myStocksTcpDataCache.expectedCount = 0
  947. this.myStocksTcpDataCache.collectedData = {}
  948. this.myStocksTcpDataCache.timestamp = ''
  949. this.myStocksTcpDataCache.type = ''
  950. console.log('我的自选:获取完整数据并清空缓存,数据对象数:', Object.keys(completeData.data).length)
  951. return completeData
  952. },
  953. // 处理我的自选完整的股票数据
  954. processCompleteMyStocksData(completeData) {
  955. console.log("我的自选:开始更新我的自选TCP股票数据存储")
  956. // 更新我的自选TCP股票数据存储
  957. this.myStocksTcpData = {
  958. count: completeData.count || 0,
  959. data: completeData.data || {},
  960. stock_count: completeData.stock_count || 0,
  961. timestamp: completeData.timestamp || '',
  962. type: completeData.type || ''
  963. }
  964. // 获取所有股票的数据用于显示
  965. const stockCodes = Object.keys(completeData.data)
  966. const newMyStocksInfoList = []
  967. // 只处理前3条股票数据
  968. const maxStocks = Math.min(3, stockCodes.length)
  969. for (let i = 0; i < maxStocks; i++) {
  970. const stockCode = stockCodes[i]
  971. // 检查数据结构
  972. if (completeData.data[stockCode] && Array.isArray(completeData.data[stockCode]) && completeData.data[stockCode].length > 0) {
  973. const stockData = completeData.data[stockCode][0] // 取第一条数据
  974. console.log('遍历的数据:', stockCode, stockData)
  975. if (stockData && stockData.current_price !== undefined) {
  976. // 直接使用返回的数据,不重新计算
  977. newMyStocksInfoList.push({
  978. stock_code: stockCode,
  979. stock_name: stockData.stock_name || '未知股票',
  980. current_price: parseFloat(stockData.current_price).toFixed(2),
  981. change: stockData.change || '0.00%',
  982. change_value: stockData.change_value || 0,
  983. change_percent: parseFloat(stockData.change) || 0
  984. })
  985. }
  986. }
  987. }
  988. // 更新我的自选股票信息列表
  989. if (newMyStocksInfoList.length > 0) {
  990. this.myStocksInfoList = newMyStocksInfoList
  991. console.log('我的自选:股票数据更新成功,共', newMyStocksInfoList.length, '个股票:', this.myStocksInfoList)
  992. }
  993. },
  994. // 发送TCP消息
  995. sendTcpMessage() {
  996. // 构造要发送的消息对象
  997. const messageData =
  998. // {
  999. // command: "real_time",
  1000. // stock_code: "SH.000001"
  1001. // }
  1002. // {"command": "stock_list"}
  1003. // {"command": "batch_real_time", "stock_codes": ["SH.000001"]}
  1004. {"command": "batch_real_time", "stock_codes": this.currentStockCodes}
  1005. // 发送消息
  1006. const success = tcpConnection.send(messageData)
  1007. if (success) {
  1008. // console.log('home发送TCP消息:', messageData)
  1009. uni.showToast({
  1010. title: '服务器连接成功',
  1011. icon: 'success',
  1012. duration: 1500
  1013. })
  1014. }
  1015. },
  1016. // 发送我的自选TCP消息
  1017. sendMyStocksTcpMessage() {
  1018. // 构造要发送的消息对象 - 我的自选股票数据请求
  1019. const messageData = {"command": "batch_real_time", "stock_codes": this.currentStockCodes}
  1020. // 发送消息到channel 2(我的自选TCP连接)
  1021. const success = tcpConnection.send(messageData, { channel: '2' })
  1022. if (success) {
  1023. console.log('我的自选:发送TCP消息成功:', messageData)
  1024. uni.showToast({
  1025. title: '我的自选数据请求已发送',
  1026. icon: 'success',
  1027. duration: 1500
  1028. })
  1029. } else {
  1030. console.error('我的自选:发送TCP消息失败')
  1031. uni.showToast({
  1032. title: '我的自选连接失败',
  1033. icon: 'error',
  1034. duration: 1500
  1035. })
  1036. }
  1037. },
  1038. // 清空消息记录
  1039. clearTcpMessages() {
  1040. this.tcpMessages = []
  1041. uni.showToast({
  1042. title: '消息记录已清空',
  1043. icon: 'success',
  1044. duration: 1500
  1045. })
  1046. },
  1047. // 获取TCP连接状态
  1048. getTcpStatus() {
  1049. const status = tcpConnection.getConnectionStatus()
  1050. uni.showModal({
  1051. title: 'TCP连接状态',
  1052. content: `当前状态: ${status ? '已连接' : '未连接'}\n消息数量: ${this.tcpMessages.length}`,
  1053. showCancel: false
  1054. })
  1055. },
  1056. // 验证数据完整性(检查count值与data对象个数是否相等)
  1057. validateDataIntegrity(parsedMessage) {
  1058. if (!parsedMessage.count || !parsedMessage.data) {
  1059. return false
  1060. }
  1061. const dataObjectCount = Object.keys(parsedMessage.data).length
  1062. const expectedCount = parsedMessage.count
  1063. // console.log(`数据完整性验证: 期望${expectedCount}个对象,实际${dataObjectCount}个对象`)
  1064. return dataObjectCount === expectedCount
  1065. },
  1066. // 检查数据状态(完整数据、前半部分数据、后半部分数据)
  1067. getDataStatus(message) {
  1068. if (typeof message !== 'string') {
  1069. return 'invalid'
  1070. }
  1071. const trimmedMessage = message.trim()
  1072. const startsWithBrace = trimmedMessage.startsWith('{')
  1073. const endsWithBrace = trimmedMessage.endsWith('}')
  1074. if (startsWithBrace && endsWithBrace) {
  1075. // 以{开头,以}结尾 - 完整数据
  1076. // console.log('检测到完整数据格式')
  1077. return 'complete'
  1078. } else if (startsWithBrace && !endsWithBrace) {
  1079. // 以{开头,不以}结尾 - 前半部分数据
  1080. // console.log('检测到前半部分数据')
  1081. return 'first_part'
  1082. } else if (!startsWithBrace && endsWithBrace) {
  1083. // 不以{开头,以}结尾 - 后半部分数据
  1084. // console.log('检测到后半部分数据')
  1085. return 'second_part'
  1086. } else {
  1087. // 其他情况 - 无效数据
  1088. // console.log('检测到无效数据格式')
  1089. return 'invalid'
  1090. }
  1091. },
  1092. // 缓存前半部分数据
  1093. cacheFirstPartData(message) {
  1094. this.tcpDataCache.firstPartData = message.trim()
  1095. this.tcpDataCache.isWaitingSecondPart = true
  1096. // console.log('已缓存前半部分数据,长度:', this.tcpDataCache.firstPartData.length)
  1097. },
  1098. // 拼接前后两部分数据
  1099. concatenateDataParts(secondPartMessage) {
  1100. const completeMessage = this.tcpDataCache.firstPartData + secondPartMessage.trim()
  1101. // console.log('数据拼接完成,完整数据长度:', completeMessage.length)
  1102. // 清空缓存
  1103. this.clearStringFragmentCache()
  1104. return completeMessage
  1105. },
  1106. // 清空字符串片段缓存
  1107. clearStringFragmentCache() {
  1108. this.tcpDataCache.firstPartData = ''
  1109. this.tcpDataCache.isWaitingSecondPart = false
  1110. // console.log('字符串片段缓存已清空')
  1111. },
  1112. // 合并数据到缓存中
  1113. mergeDataToCache(parsedMessage) {
  1114. // 如果是第一次收集数据,初始化缓存
  1115. if (!this.tcpDataCache.isCollecting) {
  1116. this.tcpDataCache.isCollecting = true
  1117. this.tcpDataCache.expectedCount = parsedMessage.count
  1118. this.tcpDataCache.collectedData = {}
  1119. this.tcpDataCache.timestamp = parsedMessage.timestamp
  1120. this.tcpDataCache.type = parsedMessage.type
  1121. // console.log('开始收集数据片段,期望总数:', parsedMessage.count)
  1122. }
  1123. // 合并新数据到缓存中
  1124. if (parsedMessage.data) {
  1125. Object.assign(this.tcpDataCache.collectedData, parsedMessage.data)
  1126. // console.log('数据片段已合并,当前已收集:', Object.keys(this.tcpDataCache.collectedData).length, '个对象')
  1127. }
  1128. },
  1129. // 检查缓存数据是否完整
  1130. isCacheDataComplete() {
  1131. const collectedCount = Object.keys(this.tcpDataCache.collectedData).length
  1132. const expectedCount = this.tcpDataCache.expectedCount
  1133. // console.log(`缓存数据检查: 已收集${collectedCount}个,期望${expectedCount}个`)
  1134. return collectedCount === expectedCount && collectedCount > 0
  1135. },
  1136. // 获取完整的缓存数据并清空缓存
  1137. getCompleteDataFromCache() {
  1138. const completeData = {
  1139. count: this.tcpDataCache.expectedCount,
  1140. data: { ...this.tcpDataCache.collectedData },
  1141. stock_count: this.tcpDataCache.expectedCount,
  1142. timestamp: this.tcpDataCache.timestamp,
  1143. type: this.tcpDataCache.type
  1144. }
  1145. // 清空缓存
  1146. this.tcpDataCache.isCollecting = false
  1147. this.tcpDataCache.expectedCount = 0
  1148. this.tcpDataCache.collectedData = {}
  1149. this.tcpDataCache.timestamp = ''
  1150. this.tcpDataCache.type = ''
  1151. // console.log('获取完整数据并清空缓存,数据对象数:', Object.keys(completeData.data).length)
  1152. return completeData
  1153. },
  1154. // 解析TCP股票数据
  1155. parseStockData(message) {
  1156. try {
  1157. // console.log('进入parseStockData, message类型:', typeof message, '长度:', message.length)
  1158. // 第一步:检查数据状态(完整、前半部分、后半部分)
  1159. const dataStatus = this.getDataStatus(message)
  1160. let completeMessage = ''
  1161. switch (dataStatus) {
  1162. case 'complete':
  1163. // 完整数据,直接处理
  1164. // console.log('检测到完整数据,直接处理')
  1165. completeMessage = message
  1166. break
  1167. case 'first_part':
  1168. // 前半部分数据,缓存并等待后半部分
  1169. // console.log('检测到前半部分数据,开始缓存')
  1170. this.cacheFirstPartData(message)
  1171. return // 等待后半部分数据
  1172. case 'second_part':
  1173. // 后半部分数据,检查是否有缓存的前半部分
  1174. if (this.tcpDataCache.isWaitingSecondPart) {
  1175. // console.log('检测到后半部分数据,开始拼接')
  1176. completeMessage = this.concatenateDataParts(message)
  1177. } else {
  1178. // console.log('收到后半部分数据,但没有缓存的前半部分,跳过处理')
  1179. return
  1180. }
  1181. break
  1182. case 'invalid':
  1183. default:
  1184. // console.log('数据格式无效,跳过处理')
  1185. return
  1186. }
  1187. // 第二步:解析完整的JSON数据
  1188. let parsedMessage
  1189. try {
  1190. // console.log('开始解析完整JSON数据,长度:', completeMessage.length)
  1191. parsedMessage = JSON.parse(completeMessage)
  1192. // console.log('JSON解析成功,解析后类型:', typeof parsedMessage, parsedMessage)
  1193. } catch (parseError) {
  1194. // console.error('JSON解析失败:', parseError.message)
  1195. // 清空字符串片段缓存
  1196. this.clearStringFragmentCache()
  1197. return
  1198. }
  1199. // 第三步:检查是否是股票数据类型
  1200. if (!((parsedMessage.type === 'batch_data_chunk' || parsedMessage.type === 'batch_realtime_data') && parsedMessage.data)) {
  1201. // console.log('不是batch_data_chunk或batch_realtime_data类型的消息,跳过处理')
  1202. return
  1203. }
  1204. // console.log('开始处理股票数据')
  1205. // 第四步:验证数据完整性(对象级别的完整性检查)
  1206. // 注意:batch_data_chunk类型的数据不需要验证完整性,直接处理
  1207. if (parsedMessage.type === 'batch_data_chunk') {
  1208. // console.log('batch_data_chunk类型数据,跳过完整性验证,直接处理')
  1209. this.processCompleteStockData(parsedMessage)
  1210. } else {
  1211. const isDataComplete = this.validateDataIntegrity(parsedMessage)
  1212. if (isDataComplete) {
  1213. // 数据完整,直接处理
  1214. // console.log('对象级数据完整,直接处理')
  1215. this.processCompleteStockData(parsedMessage)
  1216. } else {
  1217. // 数据不完整,需要拼接(对象级别的拼接)
  1218. // console.log('对象级数据不完整,开始拼接处理')
  1219. // 将当前数据合并到缓存中
  1220. this.mergeDataToCache(parsedMessage)
  1221. // 检查缓存中的数据是否已经完整
  1222. if (this.isCacheDataComplete()) {
  1223. // console.log('缓存数据已完整,开始处理')
  1224. const completeData = this.getCompleteDataFromCache()
  1225. this.processCompleteStockData(completeData)
  1226. } else {
  1227. // console.log('缓存数据仍不完整,等待更多数据片段')
  1228. }
  1229. }
  1230. }
  1231. } catch (error) {
  1232. // console.error('解析TCP股票数据失败:', error.message)
  1233. // console.error('错误详情:', error)
  1234. // 发生错误时清空所有缓存
  1235. this.clearStringFragmentCache()
  1236. if (this.tcpDataCache.isCollecting) {
  1237. // console.log('发生错误,清空对象级数据缓存')
  1238. this.tcpDataCache.isCollecting = false
  1239. this.tcpDataCache.expectedCount = 0
  1240. this.tcpDataCache.collectedData = {}
  1241. this.tcpDataCache.timestamp = ''
  1242. this.tcpDataCache.type = ''
  1243. }
  1244. }
  1245. },
  1246. // 处理完整的股票数据
  1247. processCompleteStockData(completeData) {
  1248. // console.log("开始更新TCP股票数据存储")
  1249. // 更新TCP股票数据存储
  1250. this.tcpStockData = {
  1251. count: completeData.count || 0,
  1252. data: completeData.data || {},
  1253. stock_count: completeData.stock_count || 0,
  1254. timestamp: completeData.timestamp || '',
  1255. type: completeData.type || ''
  1256. }
  1257. // 获取所有股票的数据用于显示(最多3个)
  1258. const stockCodes = Object.keys(completeData.data)
  1259. const newStockInfoList = []
  1260. // 处理最多3个股票数据
  1261. for (let i = 0; i < Math.min(stockCodes.length, 3); i++) {
  1262. const stockCode = stockCodes[i]
  1263. // 检查数据结构
  1264. if (completeData.data[stockCode] && Array.isArray(completeData.data[stockCode]) && completeData.data[stockCode].length > 0) {
  1265. const stockData = completeData.data[stockCode][0] // 取第一条数据
  1266. if (stockData && stockData.current_price !== undefined && stockData.pre_close !== undefined) {
  1267. // 计算涨跌幅
  1268. const changeValue = stockData.current_price - stockData.pre_close
  1269. const changePercent = ((changeValue / stockData.pre_close) * 100).toFixed(2)
  1270. const changeSign = changeValue >= 0 ? '+' : ''
  1271. // 添加到股票信息列表
  1272. newStockInfoList.push({
  1273. stock_name: stockData.stock_name || '未知股票',
  1274. current_price: stockData.current_price ? stockData.current_price.toFixed(2) : '0.00',
  1275. change: `${changeSign}${changePercent}%`,
  1276. change_value: changeValue,
  1277. change_percent: parseFloat(changePercent)
  1278. })
  1279. }
  1280. }
  1281. }
  1282. // 更新当前显示的股票信息列表
  1283. if (newStockInfoList.length > 0) {
  1284. this.currentStockInfoList = newStockInfoList
  1285. // console.log('股票数据更新成功,共', newStockInfoList.length, '个股票:', this.currentStockInfoList)
  1286. }
  1287. },
  1288. // 移除TCP监听器
  1289. removeTcpListeners() {
  1290. if (this.connectionListener) {
  1291. tcpConnection.removeConnectionListener(this.connectionListener)
  1292. this.connectionListener = null
  1293. // console.log('已移除TCP连接状态监听器')
  1294. }
  1295. if (this.messageListener) {
  1296. tcpConnection.removeMessageListener(this.messageListener)
  1297. this.messageListener = null
  1298. // console.log('已移除TCP消息监听器')
  1299. }
  1300. }
  1301. }
  1302. }
  1303. </script>
  1304. <style scoped>
  1305. .main {
  1306. display: flex;
  1307. flex-direction: column;
  1308. height: 100vh;
  1309. background-color: #ffffff;
  1310. }
  1311. .header {
  1312. display: flex;
  1313. justify-content: space-between;
  1314. align-items: center;
  1315. padding: 10px 15px;
  1316. background-color: #ffffff;
  1317. }
  1318. .title {
  1319. font-size: 22px;
  1320. font-weight: bold;
  1321. text-align: center;
  1322. flex: 1;
  1323. }
  1324. .headphone-icon, .notification-icon {
  1325. width: 40px;
  1326. display: flex;
  1327. align-items: center;
  1328. justify-content: center;
  1329. }
  1330. .header-icon-image {
  1331. width: 24px;
  1332. height: 24px;
  1333. object-fit: contain;
  1334. }
  1335. .content-container {
  1336. flex: 1;
  1337. padding: 10px;
  1338. width: 100%;
  1339. box-sizing: border-box;
  1340. overflow-x: hidden;
  1341. }
  1342. .section {
  1343. margin-bottom: 15px;
  1344. /* background-color: #f5f5f5; */
  1345. background-color: #ffffff;
  1346. border-radius: 8px;
  1347. padding: 15px;
  1348. }
  1349. .section-header {
  1350. display: flex;
  1351. justify-content: space-between;
  1352. align-items: center;
  1353. margin-bottom: 15px;
  1354. padding-bottom: 10px;
  1355. }
  1356. .section-title {
  1357. font-size: 18px;
  1358. font-weight: bold;
  1359. color: #000000;
  1360. }
  1361. .section-title-text{
  1362. font-size: 14px;
  1363. }
  1364. .text-gap{
  1365. height: 10px;
  1366. }
  1367. .more-btn {
  1368. font-size: 12px;
  1369. font-weight: bold;
  1370. color: #ffffff;
  1371. background-color: #000000;
  1372. padding: 4px 8px;
  1373. border-radius: 4px;
  1374. }
  1375. /* 市场概览样式 */
  1376. .market-header {
  1377. display: flex;
  1378. justify-content: space-between;
  1379. align-items: center;
  1380. padding: 10px 15px;
  1381. background-color: #ffffff;
  1382. margin-bottom: 10px;
  1383. }
  1384. .market-content {
  1385. background-color: #f5f5f5;
  1386. border-radius: 8px;
  1387. padding: 15px;
  1388. margin: 0 15px;
  1389. }
  1390. .market-image {
  1391. margin-bottom: 15px;
  1392. display: flex;
  1393. justify-content: center;
  1394. }
  1395. .overview-image {
  1396. width: 100%;
  1397. border-radius: 8px;
  1398. }
  1399. .market-data {
  1400. display: flex;
  1401. flex-direction: column;
  1402. gap: 10px;
  1403. }
  1404. /* 间隔样式 */
  1405. .section-gap {
  1406. height: 20px;
  1407. }
  1408. .market-item {
  1409. display: flex;
  1410. justify-content: space-between;
  1411. padding: 8px 0;
  1412. border-bottom: 1px solid #f0f0f0;
  1413. }
  1414. .down {
  1415. color: #ff4d4f;
  1416. }
  1417. .up {
  1418. color: #52c41a;
  1419. }
  1420. /* DeepMate样式 */
  1421. .deepmate-container {
  1422. background-color: #ffe6e6;
  1423. border-radius: 10px;
  1424. padding: 15px;
  1425. margin: 0 15px;
  1426. }
  1427. .deepmate-header {
  1428. margin-bottom: 10px;
  1429. }
  1430. .title-container {
  1431. display: flex;
  1432. align-items: center;
  1433. justify-content: space-between;
  1434. width: 100%;
  1435. }
  1436. .title-left {
  1437. width: 50%;
  1438. }
  1439. .title-right {
  1440. width: 50%;
  1441. display: flex;
  1442. justify-content: flex-end;
  1443. }
  1444. .deepmate-title {
  1445. font-size: 18px;
  1446. font-weight: bold;
  1447. color: #ff4d4f;
  1448. }
  1449. .deepmate-icon {
  1450. width: 60px;
  1451. height: 60px;
  1452. margin-left: 0;
  1453. }
  1454. .deepmate-subtitle {
  1455. font-size: 12px;
  1456. color: #666;
  1457. margin-left: 5px;
  1458. }
  1459. .deepmate-hotspots {
  1460. margin: 10px 0;
  1461. }
  1462. .hotspot-item {
  1463. background-color: #f5f5f5;
  1464. padding: 8px 12px;
  1465. border-radius: 6px;
  1466. margin-bottom: 8px;
  1467. }
  1468. .hotspot-item text {
  1469. font-size: 14px;
  1470. color: #333;
  1471. }
  1472. .deepmate-action {
  1473. display: flex;
  1474. justify-content: center;
  1475. align-items: center;
  1476. background-color: #ffffff;
  1477. border-radius: 20px;
  1478. padding: 10px;
  1479. margin-top: 10px;
  1480. }
  1481. /* 欢迎部分样式 */
  1482. .welcome-section {
  1483. margin-bottom: 15px;
  1484. padding: 0;
  1485. }
  1486. .welcome-swiper {
  1487. width: 100%;
  1488. height: 110px;
  1489. border-radius: 0;
  1490. overflow: hidden;
  1491. }
  1492. .deepmate-section {
  1493. padding: 0;
  1494. }
  1495. .swiper-image {
  1496. width: 100%;
  1497. height: 100%;
  1498. border-radius: 8px;
  1499. object-fit: contain;
  1500. }
  1501. /* 深度探索样式 */
  1502. .deep-exploration {
  1503. margin-top: 15px;
  1504. padding: 0; /* 移除内边距,让子容器自己控制 */
  1505. }
  1506. .section-header-container {
  1507. margin-bottom: 10px;
  1508. }
  1509. .section-header {
  1510. display: flex;
  1511. justify-content: space-between;
  1512. align-items: center;
  1513. padding: 10px 15px;
  1514. background-color: #ffffff;
  1515. }
  1516. .header-left {
  1517. display: flex;
  1518. align-items: center;
  1519. }
  1520. .header-right {
  1521. display: flex;
  1522. align-items: center;
  1523. }
  1524. .section-title {
  1525. font-size: 16px;
  1526. font-weight: bold;
  1527. color: #333;
  1528. }
  1529. .more-btn {
  1530. font-size: 12px;
  1531. color: #ffffff;
  1532. }
  1533. .exploration-container {
  1534. border-radius: 8px;
  1535. overflow: hidden;
  1536. }
  1537. .exploration-content {
  1538. display: flex;
  1539. justify-content: space-between;
  1540. padding: 15px;
  1541. background-color: #f5f5f5;
  1542. border-radius: 8px;
  1543. }
  1544. .exploration-item {
  1545. display: flex;
  1546. flex-direction: column;
  1547. align-items: center;
  1548. width: 22%;
  1549. background-color: #ffffff;
  1550. border-radius: 8px;
  1551. padding: 10px 0;
  1552. }
  1553. .exploration-icon {
  1554. width: 50px;
  1555. height: 50px;
  1556. margin-bottom: 8px;
  1557. }
  1558. .exploration-text {
  1559. font-size: 12px;
  1560. color: #333;
  1561. }
  1562. .icon-text {
  1563. font-size: 12px;
  1564. }
  1565. /* 我的自选样式 */
  1566. .my-selection {
  1567. padding: 0; /* 移除内边距,让子容器自己控制 */
  1568. }
  1569. .stock-container {
  1570. border-radius: 8px;
  1571. overflow: hidden;
  1572. background-color: #f5f5f5;
  1573. padding: 15px;
  1574. box-sizing: border-box;
  1575. }
  1576. .stock-list {
  1577. display: flex;
  1578. flex-direction: row;
  1579. justify-content: center;
  1580. background-color: #f8f8f8;
  1581. border-radius: 8px;
  1582. padding: 15px;
  1583. gap: 10px; /* 添加卡片之间的间距 */
  1584. }
  1585. .stock-item {
  1586. display: flex;
  1587. flex-direction: column;
  1588. justify-content: space-between;
  1589. padding: 3px;
  1590. background-color: #ffffff;
  1591. border-radius: 8px;
  1592. width: 30%;
  1593. box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  1594. will-change: transform;
  1595. transform: translateZ(0);
  1596. }
  1597. .stock-info {
  1598. display: flex;
  1599. flex-direction: column;
  1600. align-items: center;
  1601. margin-bottom: 5px;
  1602. }
  1603. .stock-chart {
  1604. display: flex;
  1605. align-items: center;
  1606. justify-content: center;
  1607. }
  1608. .name-container {
  1609. display: flex;
  1610. align-items: center;
  1611. gap: 5px;
  1612. }
  1613. .stock-name {
  1614. font-size: 12px;
  1615. font-weight: bold;
  1616. color: #333;
  1617. }
  1618. .stock-code-label {
  1619. font-size: 12px;
  1620. color: #666;
  1621. background-color: #f5f5f5;
  1622. padding: 0 4px;
  1623. border-radius: 3px;
  1624. }
  1625. .stock-price {
  1626. font-size: 12px;
  1627. color: #666;
  1628. }
  1629. .price-container {
  1630. display: flex;
  1631. align-items: center;
  1632. gap: 5px;
  1633. }
  1634. .stock-change {
  1635. font-size: 12px;
  1636. }
  1637. .stock-up {
  1638. color: #4cd964;
  1639. }
  1640. .stock-down {
  1641. color: #ff3b30;
  1642. }
  1643. .chart-image {
  1644. width: 100px;
  1645. height: 40px;
  1646. }
  1647. /* 市场看点样式 */
  1648. .highlights-title-container {
  1649. background-color: #ffffff;
  1650. margin-bottom: 10px;
  1651. }
  1652. .highlights-image-container {
  1653. background-color: #f5f5f5;
  1654. border-radius: 8px;
  1655. padding: 10px;
  1656. margin-bottom: 15px;
  1657. }
  1658. .highlights-image {
  1659. width: 100%;
  1660. height: 150px;
  1661. border-radius: 4px;
  1662. }
  1663. /* 机构动向简报样式 */
  1664. .institutional-reports {
  1665. margin-top: 15px;
  1666. background-color: #f8f8f8;
  1667. border-radius: 8px;
  1668. padding: 10px;
  1669. }
  1670. .section-title-container {
  1671. position: relative;
  1672. padding-left: 10px;
  1673. margin-bottom: 5px;
  1674. }
  1675. .section-title-container:before {
  1676. content: "";
  1677. position: absolute;
  1678. left: 0;
  1679. top: 0;
  1680. bottom: 0;
  1681. width: 4px;
  1682. background-color: #ff4d4f;
  1683. border-radius: 2px;
  1684. }
  1685. .report-item {
  1686. background-color: #ffffff;
  1687. border-radius: 8px;
  1688. padding: 10px;
  1689. margin-bottom: 8px;
  1690. }
  1691. .report-stock {
  1692. font-size: 14px;
  1693. font-weight: bold;
  1694. color: #e74c3c;
  1695. margin-bottom: 5px;
  1696. }
  1697. .report-status {
  1698. font-size: 12px;
  1699. color: #333;
  1700. }
  1701. .view-more {
  1702. text-align: center;
  1703. color: #1890ff;
  1704. font-size: 12px;
  1705. padding: 5px;
  1706. }
  1707. /* 底部空间 */
  1708. .bottom-space {
  1709. height: 60px;
  1710. }
  1711. /* 底部导航 */
  1712. .static-footer {
  1713. position: fixed;
  1714. bottom: 0;
  1715. width: 100%;
  1716. }
  1717. /* TCP测试区域样式 */
  1718. .tcp-test-section {
  1719. border: 1px solid #e0e0e0;
  1720. background-color: #f9f9f9;
  1721. }
  1722. .tcp-status {
  1723. padding: 4px 12px;
  1724. border-radius: 12px;
  1725. font-size: 12px;
  1726. }
  1727. .tcp-status.connected {
  1728. background-color: #e8f5e8;
  1729. color: #4caf50;
  1730. border: 1px solid #4caf50;
  1731. }
  1732. .tcp-status.disconnected {
  1733. background-color: #ffeaea;
  1734. color: #f44336;
  1735. border: 1px solid #f44336;
  1736. }
  1737. .status-text {
  1738. font-size: 12px;
  1739. font-weight: bold;
  1740. }
  1741. .tcp-controls {
  1742. display: flex;
  1743. flex-wrap: wrap;
  1744. gap: 8px;
  1745. margin-bottom: 15px;
  1746. }
  1747. .tcp-btn {
  1748. flex: 1;
  1749. min-width: 80px;
  1750. height: 36px;
  1751. border-radius: 6px;
  1752. font-size: 12px;
  1753. border: none;
  1754. color: white;
  1755. font-weight: bold;
  1756. }
  1757. .connect-btn {
  1758. background-color: #4caf50;
  1759. }
  1760. .connect-btn:disabled {
  1761. background-color: #cccccc;
  1762. }
  1763. .disconnect-btn {
  1764. background-color: #f44336;
  1765. }
  1766. .disconnect-btn:disabled {
  1767. background-color: #cccccc;
  1768. }
  1769. .send-btn {
  1770. background-color: #2196f3;
  1771. }
  1772. .send-btn:disabled {
  1773. background-color: #cccccc;
  1774. }
  1775. .status-btn {
  1776. background-color: #ff9800;
  1777. }
  1778. .tcp-messages {
  1779. margin-top: 15px;
  1780. border-top: 1px solid #e0e0e0;
  1781. padding-top: 15px;
  1782. }
  1783. .messages-header {
  1784. display: flex;
  1785. justify-content: space-between;
  1786. align-items: center;
  1787. margin-bottom: 10px;
  1788. }
  1789. .messages-title {
  1790. font-size: 14px;
  1791. font-weight: bold;
  1792. color: #333;
  1793. }
  1794. .clear-btn {
  1795. padding: 4px 8px;
  1796. background-color: #f44336;
  1797. color: white;
  1798. border: none;
  1799. border-radius: 4px;
  1800. font-size: 10px;
  1801. }
  1802. .messages-list {
  1803. max-height: 200px;
  1804. border: 1px solid #e0e0e0;
  1805. border-radius: 6px;
  1806. padding: 8px;
  1807. background-color: white;
  1808. }
  1809. .message-item {
  1810. margin-bottom: 8px;
  1811. padding: 8px;
  1812. border-radius: 6px;
  1813. border-left: 3px solid #ccc;
  1814. }
  1815. .message-item.sent {
  1816. background-color: #e3f2fd;
  1817. border-left-color: #2196f3;
  1818. }
  1819. .message-item.received {
  1820. background-color: #f1f8e9;
  1821. border-left-color: #4caf50;
  1822. }
  1823. .message-info {
  1824. display: flex;
  1825. justify-content: space-between;
  1826. margin-bottom: 4px;
  1827. }
  1828. .message-direction {
  1829. font-size: 10px;
  1830. font-weight: bold;
  1831. color: #666;
  1832. }
  1833. .message-time {
  1834. font-size: 10px;
  1835. color: #999;
  1836. }
  1837. .message-content {
  1838. font-size: 12px;
  1839. color: #333;
  1840. word-break: break-all;
  1841. }
  1842. /* 我的自选TCP控制样式 */
  1843. .my-stocks-tcp-control {
  1844. display: flex;
  1845. align-items: center;
  1846. justify-content: space-between;
  1847. margin-top: 8px;
  1848. padding: 8px 12px;
  1849. background-color: #f8f9fa;
  1850. border-radius: 8px;
  1851. border: 1px solid #e9ecef;
  1852. }
  1853. .tcp-buttons {
  1854. display: flex;
  1855. gap: 6px;
  1856. }
  1857. .my-stocks-tcp-control .tcp-btn {
  1858. flex: none;
  1859. min-width: 50px;
  1860. height: 28px;
  1861. border-radius: 4px;
  1862. font-size: 11px;
  1863. border: none;
  1864. color: white;
  1865. font-weight: bold;
  1866. }
  1867. </style>