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.

1427 lines
34 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. <template>
  2. <view class="main">
  3. <!-- 顶部状态栏占位 -->
  4. <view class="top" :style="{height:iSMT+'px'}"></view>
  5. <!-- 头部导航 -->
  6. <view class="header">
  7. <view class="headphone-icon">
  8. <image src="https://d31zlh4on95l9h.cloudfront.net/images/bef2edba6cc0c85671fde07cfab5270d.png" class="header-icon-image"></image>
  9. </view>
  10. <view class="title">DeepChart</view>
  11. <view class="notification-icon">
  12. <image src="https://d31zlh4on95l9h.cloudfront.net/images/2554c84b91712d2a6cb6b00380e63bac.png" class="header-icon-image"></image>
  13. </view>
  14. </view>
  15. <!-- 内容区域 - 使用滚动视图 -->
  16. <scroll-view scroll-y class="content-container">
  17. <!-- 1. 今日市场概览 -->
  18. <market-overview :stock-info-list="currentStockInfoList"></market-overview>
  19. <!-- 间隔 -->
  20. <view class="section-gap"></view>
  21. <!-- 新增欢迎部分 -->
  22. <view class="section welcome-section">
  23. <!-- 轮播图 -->
  24. <swiper class="welcome-swiper" circular autoplay interval="3000" duration="500" indicator-dots indicator-active-color="#4080ff">
  25. <swiper-item v-for="(item, index) in 5" :key="index">
  26. <image class="swiper-image" src="https://d31zlh4on95l9h.cloudfront.net/images/e4272cc034fa2a3d1ca588ef84e51ab0.png" mode="aspectFill"></image>
  27. </swiper-item>
  28. </swiper>
  29. </view>
  30. <!-- 2. DeepMate -->
  31. <view class="section deepmate-section">
  32. <DeepMate />
  33. </view>
  34. <!-- 3. 深度探索 -->
  35. <view class="section deep-exploration">
  36. <!-- 上部分标题和查看更多按钮 -->
  37. <view class="section-header-container">
  38. <view class="section-header">
  39. <view class="header-left">
  40. <text class="section-title">深度探索</text>
  41. </view>
  42. <view class="header-right">
  43. <text class="more-btn" @click="goToDeepExploration">查看更多</text>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 下部分四个图标 -->
  48. <view class="exploration-container">
  49. <view class="exploration-content">
  50. <view class="exploration-item">
  51. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/199472b0ee90a1c897f7c87b85accd84.png" mode="aspectFit" lazy-load="true"></image>
  52. <text class="exploration-text">主力追踪</text>
  53. </view>
  54. <view class="exploration-item">
  55. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/c25ca5e176efc961dabfa5d0d1b486b0.png" mode="aspectFit" lazy-load="true"></image>
  56. <text class="exploration-text">主力资达</text>
  57. </view>
  58. <view class="exploration-item">
  59. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/c064d7066dc8129a7df7b052762f82cf.png" mode="aspectFit" lazy-load="true"></image>
  60. <text class="exploration-text">主力解码</text>
  61. </view>
  62. <view class="exploration-item">
  63. <image class="exploration-icon" src="https://d31zlh4on95l9h.cloudfront.net/images/9d69cceee9c515911477078af6f68d88.png" mode="aspectFit" lazy-load="true"></image>
  64. <text class="exploration-text">主力资金流</text>
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- 4. 我的自选 -->
  70. <view class="section my-selection">
  71. <!-- 上部分标题和查看更多按钮 -->
  72. <view class="section-header-container">
  73. <view class="section-header">
  74. <text class="section-title">我的自选</text>
  75. <text class="more-btn" @click="goToMarketSituation">添加自选股</text>
  76. </view>
  77. </view>
  78. <!-- 下部分股票列表 -->
  79. <view class="stock-container">
  80. <view class="stock-list">
  81. <view class="stock-item" v-for="(item, index) in myStocks" :key="item.code">
  82. <view class="stock-info">
  83. <view class="name-container">
  84. <text class="stock-name">{{item.name}}</text>
  85. <text class="stock-code-label">{{item.code}}</text>
  86. </view>
  87. <view class="price-container">
  88. <text class="stock-price">{{item.price}}</text>
  89. <text class="stock-change" :class="{'stock-up': item.change > 0, 'stock-down': item.change < 0}">{{item.change > 0 ? '+' : ''}}{{item.change}}%</text>
  90. </view>
  91. </view>
  92. <view class="stock-chart">
  93. <image :src="item.chartImg" mode="aspectFit" class="chart-image"></image>
  94. </view>
  95. </view>
  96. </view>
  97. <!-- 机构动向简报数据 -->
  98. <view class="institutional-reports">
  99. <view class="section-title-container">
  100. <text class="section-title-text">机构动向简报</text>
  101. </view>
  102. <view class="text-gap"></view>
  103. <view class="report-item" v-for="(report, index) in institutionalReports" :key="index">
  104. <view class="report-stock">{{report.stock}}</view>
  105. <view class="report-status">{{report.status}}</view>
  106. </view>
  107. <view class="view-more">
  108. <view><text>查看更多 >></text></view>
  109. <view><text class="disclaimer-text">免责声明以上数据由AI生成不作为最终投资建议决策需独立</text></view>
  110. </view>
  111. </view>
  112. </view>
  113. </view>
  114. <!-- 5. TCP连接测试 -->
  115. <!-- <view class="section tcp-test-section">
  116. <view class="section-header">
  117. <text class="section-title">TCP连接测试</text>
  118. <view class="tcp-status" :class="{'connected': tcpConnected, 'disconnected': !tcpConnected}">
  119. <text class="status-text">{{tcpConnected ? '已连接' : '未连接'}}</text>
  120. </view>
  121. </view> -->
  122. <!-- TCP控制按钮 -->
  123. <!-- <view class="tcp-controls">
  124. <button class="tcp-btn connect-btn" @click="connectTcp" :disabled="tcpConnected">连接</button>
  125. <button class="tcp-btn disconnect-btn" @click="disconnectTcp" :disabled="!tcpConnected">断开</button>
  126. <button class="tcp-btn send-btn" @click="sendTcpMessage" :disabled="!tcpConnected">发送测试消息</button>
  127. <button class="tcp-btn status-btn" @click="getTcpStatus">查看状态</button>
  128. </view> -->
  129. <!-- 消息记录 -->
  130. <!-- <view class="tcp-messages" v-if="tcpMessages.length > 0">
  131. <view class="messages-header">
  132. <text class="messages-title">消息记录 ({{tcpMessages.length}})</text>
  133. <button class="clear-btn" @click="clearTcpMessages">清空</button>
  134. </view>
  135. <scroll-view scroll-y class="messages-list" scroll-top="{{tcpMessages.length * 50}}">
  136. <view class="message-item" v-for="(msg, index) in tcpMessages" :key="index"
  137. :class="{'sent': msg.direction === 'sent', 'received': msg.direction === 'received'}">
  138. <view class="message-info">
  139. <text class="message-direction">{{msg.direction === 'sent' ? '发送' : '接收'}}</text>
  140. <text class="message-time">{{msg.timestamp}}</text>
  141. </view>
  142. <text class="message-content">{{msg.content}}</text>
  143. </view>
  144. </scroll-view>
  145. </view>
  146. </view> -->
  147. <!-- 6. 今日市场看点 -->
  148. <view class="section-header highlights-title-container">
  149. <text class="section-title">今日市场核心看点</text>
  150. </view>
  151. <view class="highlights-image-container">
  152. <image src="https://d31zlh4on95l9h.cloudfront.net/images/8d5365af968402a18cedb120c09460b0.png" mode="aspectFit" class="highlights-image"></image>
  153. </view>
  154. <!-- 底部空间 - 为底部导航腾出空间 -->
  155. <view class="bottom-space"></view>
  156. </scroll-view>
  157. <!-- 底部导航 -->
  158. <footerBar class="static-footer" :type="type"></footerBar>
  159. </view>
  160. </template>
  161. <script>
  162. import footerBar from '../../components/footerBar.vue'
  163. import MarketOverview from '../../components/MarketOverview.vue'
  164. import DeepMate from '../../components/DeepMate.vue'
  165. import tcpConnection from '../../api/tcpConnection.js'
  166. import th from '../../static/language/th'
  167. export default {
  168. components: {
  169. footerBar,
  170. MarketOverview,
  171. DeepMate
  172. },
  173. data() {
  174. return {
  175. type: 'home',
  176. iSMT: 0,
  177. // 深度探索数据
  178. explorationItems: [
  179. { title: '主力追踪', icon: '/static/c1.png' },
  180. { title: '主力资金', icon: '/static/c2.png' },
  181. { title: '主力解码', icon: '/static/c3.png' },
  182. { title: '主力资金流', icon: '/static/c4.png' }
  183. ],
  184. // 我的自选股票数据
  185. myStocks: [
  186. { name: '特斯拉', code: 'TSLA', price: '482.00', change: 2.80, chartImg: '/static/c5.png' },
  187. { name: '英伟达', code: 'NVDA', price: '189.800', change: -2.92, chartImg: '/static/c6.png' },
  188. { name: '苹果', code: 'AAPL', price: '256.430', change: 2.60, chartImg: '/static/c7.png' }
  189. ],
  190. // 机构动向简报数据
  191. institutionalReports: [
  192. { stock: '特斯拉', status: '当前市场多头资金占比,且多头资金持续流入。' },
  193. { stock: '英伟达', status: '当前市场多头资金占比,且多头资金持续流入。' },
  194. { stock: '苹果', status: '当前市场多头资金占比,且多头资金持续流入。' }
  195. ],
  196. // 防抖定时器
  197. debounceTimer: null,
  198. // TCP连接相关状态
  199. tcpConnected: false,
  200. tcpMessages: [],
  201. lastSentMessage: '',
  202. // TCP监听器引用,用于移除监听器
  203. connectionListener: null,
  204. messageListener: null,
  205. // TCP股票数据存储
  206. tcpStockData: {
  207. count: 0,
  208. data: {},
  209. stock_count: 0,
  210. timestamp: '',
  211. type: ''
  212. },
  213. // TCP数据缓存机制,用于处理分片数据
  214. tcpDataCache: {
  215. isCollecting: false, // 是否正在收集数据片段
  216. expectedCount: 0, // 期望的数据总数
  217. collectedData: {}, // 已收集的数据对象(用于对象级拼接)
  218. timestamp: '', // 数据时间戳
  219. type: '', // 数据类型
  220. // 新增:字符串片段缓存
  221. firstPartData: '', // 前半部分数据字符串
  222. isWaitingSecondPart: false // 是否正在等待后半部分数据
  223. },
  224. // 当前显示的3个股票数据(用于MarketOverview组件)
  225. currentStockInfoList: [
  226. {
  227. stock_name: '美元/日元',
  228. current_price: '151.13',
  229. change: '+1.62%',
  230. change_value: 0,
  231. change_percent: 0
  232. },
  233. {
  234. stock_name: '美元/韩元',
  235. current_price: '1424.900',
  236. change: '-2.92%',
  237. change_value: 0,
  238. change_percent: 0
  239. },
  240. {
  241. stock_name: '美元/英镑',
  242. current_price: '0.730',
  243. change: '+2.92%',
  244. change_value: 0,
  245. change_percent: 0
  246. }
  247. ]
  248. }
  249. },
  250. // Vue 2生命周期方法
  251. mounted() {
  252. // 状态栏高度
  253. this.iSMT = uni.getSystemInfoSync().statusBarHeight;
  254. // 预加载图片资源
  255. this.myStocks.forEach(stock => {
  256. // 使用uni.getImageInfo替代Image对象
  257. uni.getImageInfo({
  258. src: stock.chartImg,
  259. success: function(res) {
  260. // 图片加载成功
  261. console.log('图片预加载成功:', stock.name)
  262. },
  263. fail: function(err) {
  264. console.log('图片预加载失败:', err)
  265. }
  266. })
  267. })
  268. // 初始化TCP连接监听器
  269. this.initTcpListeners()
  270. // 页面渲染完成后自动连接TCP
  271. this.$nextTick(() => {
  272. console.log('页面渲染完成,开始自动连接TCP服务器...')
  273. this.connectTcp()
  274. })
  275. },
  276. // 页面销毁前的清理工作
  277. onUnload() {
  278. // 自动关闭TCP连接
  279. if (this.tcpConnected) {
  280. console.log('页面销毁,自动关闭TCP连接')
  281. tcpConnection.disconnect()
  282. this.tcpConnected = false
  283. }
  284. // 清理防抖定时器
  285. if (this.debounceTimer) {
  286. clearTimeout(this.debounceTimer)
  287. this.debounceTimer = null
  288. }
  289. // 移除TCP监听器,防止内存泄漏
  290. this.removeTcpListeners()
  291. },
  292. methods: {
  293. goToDeepExploration() {
  294. // 跳转到深度探索页面
  295. uni.navigateTo({
  296. url: '/pages/home/deepExploration'
  297. });
  298. },
  299. goToMarketSituation() {
  300. // 跳转到行情页面
  301. uni.navigateTo({
  302. url: '/pages/home/marketSituation'
  303. });
  304. },
  305. // 防抖函数
  306. debounce(fn, delay = 300) {
  307. if (this.debounceTimer) clearTimeout(this.debounceTimer)
  308. this.debounceTimer = setTimeout(() => {
  309. fn()
  310. this.debounceTimer = null
  311. }, delay)
  312. },
  313. // TCP连接相关方法
  314. // 初始化TCP监听器
  315. initTcpListeners() {
  316. // 创建连接状态监听器并保存引用
  317. this.connectionListener = (status, result) => {
  318. this.tcpConnected = (status === 'connected')
  319. console.log('TCP连接状态变化:', status, this.tcpConnected)
  320. // 显示连接状态提示
  321. uni.showToast({
  322. title: status === 'connected' ? '连接服务器成功' : '服务器连接断开',
  323. icon: status === 'connected' ? 'success' : 'none',
  324. duration: 1000
  325. })
  326. // 连接成功后自动发送股票数据请求命令
  327. if (status === 'connected') {
  328. console.log('TCP连接成功,自动发送股票数据请求...')
  329. // 延迟一小段时间确保连接稳定后再发送命令
  330. setTimeout(() => {
  331. this.sendTcpMessage()
  332. }, 500)
  333. }
  334. }
  335. // 创建消息监听器并保存引用
  336. this.messageListener = (type, message, parsedArray) => {
  337. const messageObj = {
  338. type: type,
  339. content: message,
  340. parsedArray: parsedArray,
  341. timestamp: new Date().toLocaleTimeString(),
  342. direction: 'received'
  343. }
  344. console.log("0000")
  345. this.tcpMessages.push(messageObj)
  346. // console.log('收到TCP消息:', messageObj)
  347. console.log('home开始调用parseStockData',messageObj)
  348. // 解析股票数据
  349. this.parseStockData(message)
  350. }
  351. // 注册监听器
  352. tcpConnection.onConnectionChange(this.connectionListener)
  353. tcpConnection.onMessage(this.messageListener)
  354. },
  355. // 连接TCP服务器
  356. connectTcp() {
  357. console.log('开始连接TCP服务器...')
  358. // 先断开现有连接(如果存在)
  359. if (this.tcpConnected) {
  360. console.log('检测到现有连接,先断开...')
  361. this.disconnectTcp()
  362. // 等待断开完成后再连接
  363. setTimeout(() => {
  364. this.performTcpConnect()
  365. }, 500)
  366. } else {
  367. // 直接连接
  368. this.performTcpConnect()
  369. }
  370. },
  371. // 执行TCP连接
  372. performTcpConnect() {
  373. console.log('执行TCP连接...')
  374. tcpConnection.connect(
  375. {
  376. ip: '192.168.1.9',
  377. port: '8080',
  378. channel: '1', // 可选 1~20
  379. charsetname: 'UTF-8' // 默认UTF-8,可选GBK
  380. }
  381. )
  382. },
  383. // 断开TCP连接
  384. disconnectTcp() {
  385. console.log('断开TCP连接...')
  386. tcpConnection.disconnect(
  387. {
  388. ip: '192.168.1.9',
  389. port: '8080',
  390. channel: '1', // 可选 1~20
  391. charsetname: 'UTF-8' // 默认UTF-8,可选GBK
  392. }
  393. )
  394. this.tcpConnected = false
  395. },
  396. // 发送TCP消息
  397. sendTcpMessage() {
  398. // 构造要发送的消息对象
  399. const messageData =
  400. // {
  401. // command: "real_time",
  402. // stock_code: "SH.000001"
  403. // }
  404. // {"command": "stock_list"}
  405. // {"command": "batch_real_time", "stock_codes": ["SH.000001"]}
  406. {"command": "batch_real_time", "stock_codes": ["SH.000001","SH.000002","SH.000003"]}
  407. // 发送消息
  408. const success = tcpConnection.send(messageData)
  409. if (success) {
  410. console.log('home发送TCP消息:', messageData)
  411. uni.showToast({
  412. title: '服务器连接成功',
  413. icon: 'success',
  414. duration: 1500
  415. })
  416. }
  417. },
  418. // 清空消息记录
  419. clearTcpMessages() {
  420. this.tcpMessages = []
  421. uni.showToast({
  422. title: '消息记录已清空',
  423. icon: 'success',
  424. duration: 1500
  425. })
  426. },
  427. // 获取TCP连接状态
  428. getTcpStatus() {
  429. const status = tcpConnection.getConnectionStatus()
  430. uni.showModal({
  431. title: 'TCP连接状态',
  432. content: `当前状态: ${status ? '已连接' : '未连接'}\n消息数量: ${this.tcpMessages.length}`,
  433. showCancel: false
  434. })
  435. },
  436. // 验证数据完整性(检查count值与data对象个数是否相等)
  437. validateDataIntegrity(parsedMessage) {
  438. if (!parsedMessage.count || !parsedMessage.data) {
  439. return false
  440. }
  441. const dataObjectCount = Object.keys(parsedMessage.data).length
  442. const expectedCount = parsedMessage.count
  443. console.log(`数据完整性验证: 期望${expectedCount}个对象,实际${dataObjectCount}个对象`)
  444. return dataObjectCount === expectedCount
  445. },
  446. // 检查数据状态(完整数据、前半部分数据、后半部分数据)
  447. getDataStatus(message) {
  448. if (typeof message !== 'string') {
  449. return 'invalid'
  450. }
  451. const trimmedMessage = message.trim()
  452. const startsWithBrace = trimmedMessage.startsWith('{')
  453. const endsWithBrace = trimmedMessage.endsWith('}')
  454. if (startsWithBrace && endsWithBrace) {
  455. // 以{开头,以}结尾 - 完整数据
  456. console.log('检测到完整数据格式')
  457. return 'complete'
  458. } else if (startsWithBrace && !endsWithBrace) {
  459. // 以{开头,不以}结尾 - 前半部分数据
  460. console.log('检测到前半部分数据')
  461. return 'first_part'
  462. } else if (!startsWithBrace && endsWithBrace) {
  463. // 不以{开头,以}结尾 - 后半部分数据
  464. console.log('检测到后半部分数据')
  465. return 'second_part'
  466. } else {
  467. // 其他情况 - 无效数据
  468. console.log('检测到无效数据格式')
  469. return 'invalid'
  470. }
  471. },
  472. // 缓存前半部分数据
  473. cacheFirstPartData(message) {
  474. this.tcpDataCache.firstPartData = message.trim()
  475. this.tcpDataCache.isWaitingSecondPart = true
  476. console.log('已缓存前半部分数据,长度:', this.tcpDataCache.firstPartData.length)
  477. },
  478. // 拼接前后两部分数据
  479. concatenateDataParts(secondPartMessage) {
  480. const completeMessage = this.tcpDataCache.firstPartData + secondPartMessage.trim()
  481. console.log('数据拼接完成,完整数据长度:', completeMessage.length)
  482. // 清空缓存
  483. this.clearStringFragmentCache()
  484. return completeMessage
  485. },
  486. // 清空字符串片段缓存
  487. clearStringFragmentCache() {
  488. this.tcpDataCache.firstPartData = ''
  489. this.tcpDataCache.isWaitingSecondPart = false
  490. console.log('字符串片段缓存已清空')
  491. },
  492. // 合并数据到缓存中
  493. mergeDataToCache(parsedMessage) {
  494. // 如果是第一次收集数据,初始化缓存
  495. if (!this.tcpDataCache.isCollecting) {
  496. this.tcpDataCache.isCollecting = true
  497. this.tcpDataCache.expectedCount = parsedMessage.count
  498. this.tcpDataCache.collectedData = {}
  499. this.tcpDataCache.timestamp = parsedMessage.timestamp
  500. this.tcpDataCache.type = parsedMessage.type
  501. console.log('开始收集数据片段,期望总数:', parsedMessage.count)
  502. }
  503. // 合并新数据到缓存中
  504. if (parsedMessage.data) {
  505. Object.assign(this.tcpDataCache.collectedData, parsedMessage.data)
  506. console.log('数据片段已合并,当前已收集:', Object.keys(this.tcpDataCache.collectedData).length, '个对象')
  507. }
  508. },
  509. // 检查缓存数据是否完整
  510. isCacheDataComplete() {
  511. const collectedCount = Object.keys(this.tcpDataCache.collectedData).length
  512. const expectedCount = this.tcpDataCache.expectedCount
  513. console.log(`缓存数据检查: 已收集${collectedCount}个,期望${expectedCount}`)
  514. return collectedCount === expectedCount && collectedCount > 0
  515. },
  516. // 获取完整的缓存数据并清空缓存
  517. getCompleteDataFromCache() {
  518. const completeData = {
  519. count: this.tcpDataCache.expectedCount,
  520. data: { ...this.tcpDataCache.collectedData },
  521. stock_count: this.tcpDataCache.expectedCount,
  522. timestamp: this.tcpDataCache.timestamp,
  523. type: this.tcpDataCache.type
  524. }
  525. // 清空缓存
  526. this.tcpDataCache.isCollecting = false
  527. this.tcpDataCache.expectedCount = 0
  528. this.tcpDataCache.collectedData = {}
  529. this.tcpDataCache.timestamp = ''
  530. this.tcpDataCache.type = ''
  531. console.log('获取完整数据并清空缓存,数据对象数:', Object.keys(completeData.data).length)
  532. return completeData
  533. },
  534. // 解析TCP股票数据
  535. parseStockData(message) {
  536. try {
  537. console.log('进入parseStockData, message类型:', typeof message, '长度:', message.length)
  538. // 第一步:检查数据状态(完整、前半部分、后半部分)
  539. const dataStatus = this.getDataStatus(message)
  540. let completeMessage = ''
  541. switch (dataStatus) {
  542. case 'complete':
  543. // 完整数据,直接处理
  544. console.log('检测到完整数据,直接处理')
  545. completeMessage = message
  546. break
  547. case 'first_part':
  548. // 前半部分数据,缓存并等待后半部分
  549. console.log('检测到前半部分数据,开始缓存')
  550. this.cacheFirstPartData(message)
  551. return // 等待后半部分数据
  552. case 'second_part':
  553. // 后半部分数据,检查是否有缓存的前半部分
  554. if (this.tcpDataCache.isWaitingSecondPart) {
  555. console.log('检测到后半部分数据,开始拼接')
  556. completeMessage = this.concatenateDataParts(message)
  557. } else {
  558. console.log('收到后半部分数据,但没有缓存的前半部分,跳过处理')
  559. return
  560. }
  561. break
  562. case 'invalid':
  563. default:
  564. console.log('数据格式无效,跳过处理')
  565. return
  566. }
  567. // 第二步:解析完整的JSON数据
  568. let parsedMessage
  569. try {
  570. console.log('开始解析完整JSON数据,长度:', completeMessage.length)
  571. parsedMessage = JSON.parse(completeMessage)
  572. console.log('JSON解析成功,解析后类型:', typeof parsedMessage, parsedMessage)
  573. } catch (parseError) {
  574. console.error('JSON解析失败:', parseError.message)
  575. // 清空字符串片段缓存
  576. this.clearStringFragmentCache()
  577. return
  578. }
  579. // 第三步:检查是否是股票数据类型
  580. if (!((parsedMessage.type === 'batch_data_chunk' || parsedMessage.type === 'batch_realtime_data') && parsedMessage.data)) {
  581. console.log('不是batch_data_chunk或batch_realtime_data类型的消息,跳过处理')
  582. return
  583. }
  584. console.log('开始处理股票数据')
  585. // 第四步:验证数据完整性(对象级别的完整性检查)
  586. // 注意:batch_data_chunk类型的数据不需要验证完整性,直接处理
  587. if (parsedMessage.type === 'batch_data_chunk') {
  588. console.log('batch_data_chunk类型数据,跳过完整性验证,直接处理')
  589. this.processCompleteStockData(parsedMessage)
  590. } else {
  591. const isDataComplete = this.validateDataIntegrity(parsedMessage)
  592. if (isDataComplete) {
  593. // 数据完整,直接处理
  594. console.log('对象级数据完整,直接处理')
  595. this.processCompleteStockData(parsedMessage)
  596. } else {
  597. // 数据不完整,需要拼接(对象级别的拼接)
  598. console.log('对象级数据不完整,开始拼接处理')
  599. // 将当前数据合并到缓存中
  600. this.mergeDataToCache(parsedMessage)
  601. // 检查缓存中的数据是否已经完整
  602. if (this.isCacheDataComplete()) {
  603. console.log('缓存数据已完整,开始处理')
  604. const completeData = this.getCompleteDataFromCache()
  605. this.processCompleteStockData(completeData)
  606. } else {
  607. console.log('缓存数据仍不完整,等待更多数据片段')
  608. }
  609. }
  610. }
  611. } catch (error) {
  612. console.error('解析TCP股票数据失败:', error.message)
  613. console.error('错误详情:', error)
  614. // 发生错误时清空所有缓存
  615. this.clearStringFragmentCache()
  616. if (this.tcpDataCache.isCollecting) {
  617. console.log('发生错误,清空对象级数据缓存')
  618. this.tcpDataCache.isCollecting = false
  619. this.tcpDataCache.expectedCount = 0
  620. this.tcpDataCache.collectedData = {}
  621. this.tcpDataCache.timestamp = ''
  622. this.tcpDataCache.type = ''
  623. }
  624. }
  625. },
  626. // 处理完整的股票数据
  627. processCompleteStockData(completeData) {
  628. console.log("开始更新TCP股票数据存储")
  629. // 更新TCP股票数据存储
  630. this.tcpStockData = {
  631. count: completeData.count || 0,
  632. data: completeData.data || {},
  633. stock_count: completeData.stock_count || 0,
  634. timestamp: completeData.timestamp || '',
  635. type: completeData.type || ''
  636. }
  637. // 获取所有股票的数据用于显示(最多3个)
  638. const stockCodes = Object.keys(completeData.data)
  639. const newStockInfoList = []
  640. // 处理最多3个股票数据
  641. for (let i = 0; i < Math.min(stockCodes.length, 3); i++) {
  642. const stockCode = stockCodes[i]
  643. // 检查数据结构
  644. if (completeData.data[stockCode] && Array.isArray(completeData.data[stockCode]) && completeData.data[stockCode].length > 0) {
  645. const stockData = completeData.data[stockCode][0] // 取第一条数据
  646. if (stockData && stockData.current_price !== undefined && stockData.pre_close !== undefined) {
  647. // 计算涨跌幅
  648. const changeValue = stockData.current_price - stockData.pre_close
  649. const changePercent = ((changeValue / stockData.pre_close) * 100).toFixed(2)
  650. const changeSign = changeValue >= 0 ? '+' : ''
  651. // 添加到股票信息列表
  652. newStockInfoList.push({
  653. stock_name: stockData.stock_name || '未知股票',
  654. current_price: stockData.current_price ? stockData.current_price.toFixed(2) : '0.00',
  655. change: `${changeSign}${changePercent}%`,
  656. change_value: changeValue,
  657. change_percent: parseFloat(changePercent)
  658. })
  659. }
  660. }
  661. }
  662. // 更新当前显示的股票信息列表
  663. if (newStockInfoList.length > 0) {
  664. this.currentStockInfoList = newStockInfoList
  665. console.log('股票数据更新成功,共', newStockInfoList.length, '个股票:', this.currentStockInfoList)
  666. }
  667. },
  668. // 移除TCP监听器
  669. removeTcpListeners() {
  670. if (this.connectionListener) {
  671. tcpConnection.removeConnectionListener(this.connectionListener)
  672. this.connectionListener = null
  673. console.log('已移除TCP连接状态监听器')
  674. }
  675. if (this.messageListener) {
  676. tcpConnection.removeMessageListener(this.messageListener)
  677. this.messageListener = null
  678. console.log('已移除TCP消息监听器')
  679. }
  680. }
  681. }
  682. }
  683. </script>
  684. <style scoped>
  685. .main {
  686. display: flex;
  687. flex-direction: column;
  688. height: 100vh;
  689. background-color: #ffffff;
  690. }
  691. .header {
  692. display: flex;
  693. justify-content: space-between;
  694. align-items: center;
  695. padding: 10px 15px;
  696. background-color: #ffffff;
  697. }
  698. .title {
  699. font-size: 22px;
  700. font-weight: bold;
  701. text-align: center;
  702. flex: 1;
  703. }
  704. .headphone-icon, .notification-icon {
  705. width: 40px;
  706. display: flex;
  707. align-items: center;
  708. justify-content: center;
  709. }
  710. .header-icon-image {
  711. width: 24px;
  712. height: 24px;
  713. object-fit: contain;
  714. }
  715. .content-container {
  716. flex: 1;
  717. padding: 10px;
  718. width: 100%;
  719. box-sizing: border-box;
  720. overflow-x: hidden;
  721. }
  722. .section {
  723. margin-bottom: 15px;
  724. /* background-color: #f5f5f5; */
  725. background-color: #ffffff;
  726. border-radius: 8px;
  727. padding: 15px;
  728. }
  729. .section-header {
  730. display: flex;
  731. justify-content: space-between;
  732. align-items: center;
  733. margin-bottom: 15px;
  734. padding-bottom: 10px;
  735. }
  736. .section-title {
  737. font-size: 18px;
  738. font-weight: bold;
  739. color: #000000;
  740. }
  741. .section-title-text{
  742. font-size: 14px;
  743. }
  744. .text-gap{
  745. height: 10px;
  746. }
  747. .more-btn {
  748. font-size: 12px;
  749. font-weight: bold;
  750. color: #ffffff;
  751. background-color: #000000;
  752. padding: 4px 8px;
  753. border-radius: 4px;
  754. }
  755. /* 市场概览样式 */
  756. .market-header {
  757. display: flex;
  758. justify-content: space-between;
  759. align-items: center;
  760. padding: 10px 15px;
  761. background-color: #ffffff;
  762. margin-bottom: 10px;
  763. }
  764. .market-content {
  765. background-color: #f5f5f5;
  766. border-radius: 8px;
  767. padding: 15px;
  768. margin: 0 15px;
  769. }
  770. .market-image {
  771. margin-bottom: 15px;
  772. display: flex;
  773. justify-content: center;
  774. }
  775. .overview-image {
  776. width: 100%;
  777. border-radius: 8px;
  778. }
  779. .market-data {
  780. display: flex;
  781. flex-direction: column;
  782. gap: 10px;
  783. }
  784. /* 间隔样式 */
  785. .section-gap {
  786. height: 20px;
  787. }
  788. .market-item {
  789. display: flex;
  790. justify-content: space-between;
  791. padding: 8px 0;
  792. border-bottom: 1px solid #f0f0f0;
  793. }
  794. .down {
  795. color: #ff4d4f;
  796. }
  797. .up {
  798. color: #52c41a;
  799. }
  800. /* DeepMate样式 */
  801. .deepmate-container {
  802. background-color: #ffe6e6;
  803. border-radius: 10px;
  804. padding: 15px;
  805. margin: 0 15px;
  806. }
  807. .deepmate-header {
  808. margin-bottom: 10px;
  809. }
  810. .title-container {
  811. display: flex;
  812. align-items: center;
  813. justify-content: space-between;
  814. width: 100%;
  815. }
  816. .title-left {
  817. width: 50%;
  818. }
  819. .title-right {
  820. width: 50%;
  821. display: flex;
  822. justify-content: flex-end;
  823. }
  824. .deepmate-title {
  825. font-size: 18px;
  826. font-weight: bold;
  827. color: #ff4d4f;
  828. }
  829. .deepmate-icon {
  830. width: 60px;
  831. height: 60px;
  832. margin-left: 0;
  833. }
  834. .deepmate-subtitle {
  835. font-size: 12px;
  836. color: #666;
  837. margin-left: 5px;
  838. }
  839. .deepmate-hotspots {
  840. margin: 10px 0;
  841. }
  842. .hotspot-item {
  843. background-color: #f5f5f5;
  844. padding: 8px 12px;
  845. border-radius: 6px;
  846. margin-bottom: 8px;
  847. }
  848. .hotspot-item text {
  849. font-size: 14px;
  850. color: #333;
  851. }
  852. .deepmate-action {
  853. display: flex;
  854. justify-content: center;
  855. align-items: center;
  856. background-color: #ffffff;
  857. border-radius: 20px;
  858. padding: 10px;
  859. margin-top: 10px;
  860. }
  861. /* 欢迎部分样式 */
  862. .welcome-section {
  863. margin-bottom: 15px;
  864. padding: 0;
  865. }
  866. .welcome-swiper {
  867. width: 100%;
  868. height: 150px;
  869. border-radius: 0;
  870. overflow: hidden;
  871. }
  872. .deepmate-section {
  873. padding: 0;
  874. }
  875. .swiper-image {
  876. width: 100%;
  877. height: 100%;
  878. border-radius: 8px;
  879. object-fit: contain;
  880. }
  881. /* 深度探索样式 */
  882. .deep-exploration {
  883. margin-top: 15px;
  884. padding: 0; /* 移除内边距,让子容器自己控制 */
  885. }
  886. .section-header-container {
  887. margin-bottom: 10px;
  888. }
  889. .section-header {
  890. display: flex;
  891. justify-content: space-between;
  892. align-items: center;
  893. padding: 10px 15px;
  894. background-color: #ffffff;
  895. }
  896. .header-left {
  897. display: flex;
  898. align-items: center;
  899. }
  900. .header-right {
  901. display: flex;
  902. align-items: center;
  903. }
  904. .section-title {
  905. font-size: 16px;
  906. font-weight: bold;
  907. color: #333;
  908. }
  909. .more-btn {
  910. font-size: 12px;
  911. color: #ffffff;
  912. }
  913. .exploration-container {
  914. border-radius: 8px;
  915. overflow: hidden;
  916. }
  917. .exploration-content {
  918. display: flex;
  919. justify-content: space-between;
  920. padding: 15px;
  921. background-color: #f5f5f5;
  922. border-radius: 8px;
  923. }
  924. .exploration-item {
  925. display: flex;
  926. flex-direction: column;
  927. align-items: center;
  928. width: 22%;
  929. background-color: #ffffff;
  930. border-radius: 8px;
  931. padding: 10px 0;
  932. }
  933. .exploration-icon {
  934. width: 50px;
  935. height: 50px;
  936. margin-bottom: 8px;
  937. }
  938. .exploration-text {
  939. font-size: 12px;
  940. color: #333;
  941. }
  942. .icon-text {
  943. font-size: 12px;
  944. }
  945. /* 我的自选样式 */
  946. .my-selection {
  947. padding: 0; /* 移除内边距,让子容器自己控制 */
  948. }
  949. .stock-container {
  950. border-radius: 8px;
  951. overflow: hidden;
  952. background-color: #f5f5f5;
  953. padding: 15px;
  954. box-sizing: border-box;
  955. }
  956. .stock-list {
  957. display: flex;
  958. flex-direction: row;
  959. justify-content: center;
  960. background-color: #f8f8f8;
  961. border-radius: 8px;
  962. padding: 15px;
  963. gap: 10px; /* 添加卡片之间的间距 */
  964. }
  965. .stock-item {
  966. display: flex;
  967. flex-direction: column;
  968. justify-content: space-between;
  969. padding: 3px;
  970. background-color: #ffffff;
  971. border-radius: 8px;
  972. width: 30%;
  973. box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  974. will-change: transform;
  975. transform: translateZ(0);
  976. }
  977. .stock-info {
  978. display: flex;
  979. flex-direction: column;
  980. align-items: center;
  981. margin-bottom: 5px;
  982. }
  983. .stock-chart {
  984. display: flex;
  985. align-items: center;
  986. justify-content: center;
  987. }
  988. .name-container {
  989. display: flex;
  990. align-items: center;
  991. gap: 5px;
  992. }
  993. .stock-name {
  994. font-size: 12px;
  995. font-weight: bold;
  996. color: #333;
  997. }
  998. .stock-code-label {
  999. font-size: 12px;
  1000. color: #666;
  1001. background-color: #f5f5f5;
  1002. padding: 0 4px;
  1003. border-radius: 3px;
  1004. }
  1005. .stock-price {
  1006. font-size: 12px;
  1007. color: #666;
  1008. }
  1009. .price-container {
  1010. display: flex;
  1011. align-items: center;
  1012. gap: 5px;
  1013. }
  1014. .stock-change {
  1015. font-size: 12px;
  1016. }
  1017. .stock-up {
  1018. color: #4cd964;
  1019. }
  1020. .stock-down {
  1021. color: #ff3b30;
  1022. }
  1023. .chart-image {
  1024. width: 100px;
  1025. height: 40px;
  1026. }
  1027. /* 市场看点样式 */
  1028. .highlights-title-container {
  1029. background-color: #ffffff;
  1030. margin-bottom: 10px;
  1031. }
  1032. .highlights-image-container {
  1033. background-color: #f5f5f5;
  1034. border-radius: 8px;
  1035. padding: 10px;
  1036. margin-bottom: 15px;
  1037. }
  1038. .highlights-image {
  1039. width: 100%;
  1040. height: 150px;
  1041. border-radius: 4px;
  1042. }
  1043. /* 机构动向简报样式 */
  1044. .institutional-reports {
  1045. margin-top: 15px;
  1046. background-color: #f8f8f8;
  1047. border-radius: 8px;
  1048. padding: 10px;
  1049. }
  1050. .section-title-container {
  1051. position: relative;
  1052. padding-left: 10px;
  1053. margin-bottom: 5px;
  1054. }
  1055. .section-title-container:before {
  1056. content: "";
  1057. position: absolute;
  1058. left: 0;
  1059. top: 0;
  1060. bottom: 0;
  1061. width: 4px;
  1062. background-color: #ff4d4f;
  1063. border-radius: 2px;
  1064. }
  1065. .report-item {
  1066. background-color: #ffffff;
  1067. border-radius: 8px;
  1068. padding: 10px;
  1069. margin-bottom: 8px;
  1070. }
  1071. .report-stock {
  1072. font-size: 14px;
  1073. font-weight: bold;
  1074. color: #e74c3c;
  1075. margin-bottom: 5px;
  1076. }
  1077. .report-status {
  1078. font-size: 12px;
  1079. color: #333;
  1080. }
  1081. .view-more {
  1082. text-align: center;
  1083. color: #1890ff;
  1084. font-size: 12px;
  1085. padding: 5px;
  1086. }
  1087. /* 底部空间 */
  1088. .bottom-space {
  1089. height: 60px;
  1090. }
  1091. /* 免责声明样式 */
  1092. .disclaimer-text {
  1093. font-size: 8px;
  1094. color: #999999;
  1095. text-align: center;
  1096. }
  1097. /* 底部导航 */
  1098. .static-footer {
  1099. position: fixed;
  1100. bottom: 0;
  1101. width: 100%;
  1102. }
  1103. /* TCP测试区域样式 */
  1104. .tcp-test-section {
  1105. border: 1px solid #e0e0e0;
  1106. background-color: #f9f9f9;
  1107. }
  1108. .tcp-status {
  1109. padding: 4px 12px;
  1110. border-radius: 12px;
  1111. font-size: 12px;
  1112. }
  1113. .tcp-status.connected {
  1114. background-color: #e8f5e8;
  1115. color: #4caf50;
  1116. border: 1px solid #4caf50;
  1117. }
  1118. .tcp-status.disconnected {
  1119. background-color: #ffeaea;
  1120. color: #f44336;
  1121. border: 1px solid #f44336;
  1122. }
  1123. .status-text {
  1124. font-size: 12px;
  1125. font-weight: bold;
  1126. }
  1127. .tcp-controls {
  1128. display: flex;
  1129. flex-wrap: wrap;
  1130. gap: 8px;
  1131. margin-bottom: 15px;
  1132. }
  1133. .tcp-btn {
  1134. flex: 1;
  1135. min-width: 80px;
  1136. height: 36px;
  1137. border-radius: 6px;
  1138. font-size: 12px;
  1139. border: none;
  1140. color: white;
  1141. font-weight: bold;
  1142. }
  1143. .connect-btn {
  1144. background-color: #4caf50;
  1145. }
  1146. .connect-btn:disabled {
  1147. background-color: #cccccc;
  1148. }
  1149. .disconnect-btn {
  1150. background-color: #f44336;
  1151. }
  1152. .disconnect-btn:disabled {
  1153. background-color: #cccccc;
  1154. }
  1155. .send-btn {
  1156. background-color: #2196f3;
  1157. }
  1158. .send-btn:disabled {
  1159. background-color: #cccccc;
  1160. }
  1161. .status-btn {
  1162. background-color: #ff9800;
  1163. }
  1164. .tcp-messages {
  1165. margin-top: 15px;
  1166. border-top: 1px solid #e0e0e0;
  1167. padding-top: 15px;
  1168. }
  1169. .messages-header {
  1170. display: flex;
  1171. justify-content: space-between;
  1172. align-items: center;
  1173. margin-bottom: 10px;
  1174. }
  1175. .messages-title {
  1176. font-size: 14px;
  1177. font-weight: bold;
  1178. color: #333;
  1179. }
  1180. .clear-btn {
  1181. padding: 4px 8px;
  1182. background-color: #f44336;
  1183. color: white;
  1184. border: none;
  1185. border-radius: 4px;
  1186. font-size: 10px;
  1187. }
  1188. .messages-list {
  1189. max-height: 200px;
  1190. border: 1px solid #e0e0e0;
  1191. border-radius: 6px;
  1192. padding: 8px;
  1193. background-color: white;
  1194. }
  1195. .message-item {
  1196. margin-bottom: 8px;
  1197. padding: 8px;
  1198. border-radius: 6px;
  1199. border-left: 3px solid #ccc;
  1200. }
  1201. .message-item.sent {
  1202. background-color: #e3f2fd;
  1203. border-left-color: #2196f3;
  1204. }
  1205. .message-item.received {
  1206. background-color: #f1f8e9;
  1207. border-left-color: #4caf50;
  1208. }
  1209. .message-info {
  1210. display: flex;
  1211. justify-content: space-between;
  1212. margin-bottom: 4px;
  1213. }
  1214. .message-direction {
  1215. font-size: 10px;
  1216. font-weight: bold;
  1217. color: #666;
  1218. }
  1219. .message-time {
  1220. font-size: 10px;
  1221. color: #999;
  1222. }
  1223. .message-content {
  1224. font-size: 12px;
  1225. color: #333;
  1226. word-break: break-all;
  1227. }
  1228. </style>