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.

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