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.

715 lines
20 KiB

2 days ago
2 days ago
2 days ago
  1. <template>
  2. <div ref="bottomRadarRef" id="bottomRadarChart"></div>
  3. </template>
  4. <script setup>
  5. import { ref, onBeforeUnmount } from 'vue'
  6. import { createChartResizeHandler, applyResponsiveStyles } from '@/utils/chartResize.js'
  7. import * as echarts from 'echarts'
  8. const bottomRadarRef = ref(null)
  9. let bottomRadarChart = null
  10. let resizeHandler = null
  11. function initEmotionalBottomRadar(KlineData, barAndLineData) {
  12. // 如果已存在图表实例,先销毁
  13. if (bottomRadarChart) {
  14. bottomRadarChart.dispose()
  15. bottomRadarChart = null
  16. }
  17. let bottomRadarChartDom = document.getElementById('bottomRadarChart')
  18. bottomRadarChart = echarts.init(bottomRadarChartDom)
  19. // 日期-作为x轴
  20. let dateArray = barAndLineData.map(subArray => subArray[0])
  21. // k线数据格式:['2025/04/24', 250.5, 259.51, 249.2, 259.54, 274.899, 0.685, 258.354]
  22. // 原始数据:索引1=开盘价, 索引2=收盘价, 索引3=最低价, 索引4=最高价
  23. // ECharts candlestick需要:[开盘, 收盘, 最低, 最高]
  24. let kLineDataArray = KlineData.map(subArray => [
  25. subArray[1], // 开盘价
  26. subArray[2], // 收盘价
  27. subArray[3], // 最低价
  28. subArray[4] // 最高价
  29. ])
  30. // 计算K线数据的最小值,用于设置y轴起始值
  31. let allKlineValues = []
  32. kLineDataArray.forEach(item => {
  33. if (Array.isArray(item) && item.length >= 4) {
  34. // K线数据格式:[开盘价, 收盘价, 最低价, 最高价]
  35. allKlineValues.push(item[0], item[1], item[2], item[3])
  36. }
  37. })
  38. // 找到最小值和最大值
  39. let validValues = allKlineValues.filter(val => typeof val === 'number' && !isNaN(val))
  40. let minValue = Math.min(...validValues)
  41. let maxValue = Math.max(...validValues)
  42. // 最小值向下取整,最大值向上取整
  43. let yAxisMin = Math.floor(minValue)
  44. let yAxisMax = Math.ceil(maxValue)
  45. // 红线,取第二个值
  46. let redLineDataArray = barAndLineData.map(subArray => subArray[1])
  47. // 色块数据格式化
  48. let barTotalDataArray = barAndLineData.map(subArray => subArray.slice(2, 6))
  49. // 删掉
  50. // barTotalDataArray[0] = [0, 0, 0, 0]
  51. // barTotalDataArray[8] = [1, 1, 1, 1]
  52. // barTotalDataArray[9] = [0, 0, 1, 1]
  53. // barTotalDataArray[13] = [1, 0, 1, 0]
  54. // barTotalDataArray[16] = [0, 0, 1, 0]
  55. // 黄色块、黄色加仓资金、紫色柱子、蓝色柱子
  56. let yellowBlockDataArray = []
  57. let yellowLineDataArray = []
  58. let purpleLineDataArray = []
  59. let blueLineDataArray = []
  60. let transparentFillingDataArray = []
  61. // 黄色块:为1 0-4显示柱体
  62. // 黄色加仓资金文字:为1 在4的位置展示文字
  63. // 紫色柱子:为1 1-80显示柱体
  64. // 蓝色柱子:为1 0-40显示柱体
  65. // 因数据要互相叠加展示,所以需要数据处理。base适用于 ECharts 4.x 及以上版本
  66. barTotalDataArray.forEach((item) => {
  67. if (item[0]) {
  68. yellowBlockDataArray.push(4)
  69. if (item[3]) {
  70. // 40-4
  71. blueLineDataArray.push(36)
  72. if (item[2]) {
  73. // 80-40
  74. purpleLineDataArray.push(40)
  75. transparentFillingDataArray.push(0)
  76. } else {
  77. purpleLineDataArray.push(0)
  78. transparentFillingDataArray.push(0)
  79. }
  80. } else {
  81. blueLineDataArray.push(0)
  82. if (item[2]) {
  83. // 80-4
  84. purpleLineDataArray.push(76)
  85. transparentFillingDataArray.push(0)
  86. } else {
  87. purpleLineDataArray.push(0)
  88. transparentFillingDataArray.push(0)
  89. }
  90. }
  91. } else if (!item[0]) {
  92. yellowBlockDataArray.push(0)
  93. if (item[3]) {
  94. blueLineDataArray.push(40)
  95. if (item[2]) {
  96. // 80-40
  97. purpleLineDataArray.push(40)
  98. transparentFillingDataArray.push(0)
  99. } else {
  100. purpleLineDataArray.push(0)
  101. transparentFillingDataArray.push(0)
  102. }
  103. } else {
  104. blueLineDataArray.push(0)
  105. if (item[2]) {
  106. // 80-1,base为1
  107. purpleLineDataArray.push(79)
  108. transparentFillingDataArray.push(1)
  109. } else {
  110. purpleLineDataArray.push(0)
  111. transparentFillingDataArray.push(0)
  112. }
  113. }
  114. }
  115. // 加仓资金
  116. if (item[1]) {
  117. yellowLineDataArray.push(1)
  118. } else if (!item[1]) {
  119. yellowLineDataArray.push(0)
  120. }
  121. })
  122. // 配置图表选项,很多操作和展示已限制,如果需要需放开
  123. let option = {
  124. // backgroundColor: '#000046', // 设置整个图表的背景色
  125. tooltip: {
  126. show: true, // 启用tooltip显示
  127. trigger: 'axis',
  128. triggerOn: 'mousemove',
  129. confine: true,
  130. axisPointer: {
  131. type: 'cross',
  132. crossStyle: {
  133. color: '#fff',
  134. width: 1,
  135. type: 'solid'
  136. },
  137. lineStyle: {
  138. color: '#fff',
  139. width: 1,
  140. type: 'solid'
  141. },
  142. label: {
  143. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  144. color: '#fff',
  145. borderColor: '#fff',
  146. borderWidth: 1
  147. }
  148. },
  149. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  150. borderColor: '#fff',
  151. borderWidth: 1,
  152. padding: 10,
  153. textStyle: {
  154. color: '#fff',
  155. fontSize: 12
  156. },
  157. formatter: function (params) {
  158. if (!params || params.length === 0) return ''
  159. // 检查是否有第二个或第三个网格的数据,如果有则不显示tooltip
  160. let hasSecondOrThirdGrid = params.some(param => {
  161. return (param.seriesName === '红线' && param.axisIndex === 1) ||
  162. (param.axisIndex === 2) ||
  163. (param.seriesName !== 'K线' && param.seriesName !== '基础base')
  164. })
  165. // 如果鼠标悬浮在第二个或第三个网格上,不显示tooltip
  166. if (hasSecondOrThirdGrid && !params.some(param => param.seriesType === 'candlestick')) {
  167. return ''
  168. }
  169. let result = `<div style="font-weight: bold; color: #fff; margin-bottom: 8px;">${params[0].name}</div>`
  170. params.forEach(param => {
  171. let value = param.value
  172. let color = param.color
  173. if (param.seriesType === 'candlestick') {
  174. // ECharts candlestick的value格式:[开盘, 收盘, 最低, 最高]
  175. let candlestickData = param.value
  176. // 确保数据有效性
  177. if (!Array.isArray(candlestickData) || candlestickData.length < 4) {
  178. return ''
  179. }
  180. let openPrice = candlestickData[1] // 开盘价
  181. let closePrice = candlestickData[2] // 收盘价
  182. let lowPrice = candlestickData[3] // 最低价
  183. let highPrice = candlestickData[4] // 最高价
  184. // 确保所有价格都是有效数字
  185. if (typeof openPrice !== 'number' || typeof closePrice !== 'number' ||
  186. typeof lowPrice !== 'number' || typeof highPrice !== 'number') {
  187. return ''
  188. }
  189. let priceChange = closePrice - openPrice
  190. let changePercent = ((priceChange / openPrice) * 100).toFixed(2)
  191. let changeColor = priceChange >= 0 ? '#14b143' : '#ef232a' // 互换颜色:上涨红色,下跌绿色
  192. result += `<div style="margin-bottom: 6px;">`
  193. result += `<div style="color: #fff; font-weight: bold;">${param.seriesName}</div>`
  194. result += `<div style="color: #fff;">开盘: ${openPrice.toFixed(1)}</div>`
  195. result += `<div style="color: #fff;">收盘: ${closePrice.toFixed(1)}</div>`
  196. result += `<div style="color: #fff;">最低: ${lowPrice.toFixed(1)}</div>`
  197. result += `<div style="color: #fff;">最高: ${highPrice.toFixed(1)}</div>`
  198. result += `<div style="color: ${changeColor};">涨跌: ${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</div>`
  199. result += `</div>`
  200. } else if (param.seriesName === '红线') {
  201. result += `<div style="color: #ef232a; margin-bottom: 4px;">${param.seriesName}: ${value}</div>`
  202. } else if (param.seriesName !== '基础base' && value > 0) {
  203. result += `<div style="color: ${color}; margin-bottom: 4px;">${param.seriesName}: ${value}</div>`
  204. }
  205. })
  206. return result
  207. }
  208. },
  209. legend: {
  210. // data: ['K线', '红线', '色块'], 不要展示图例
  211. type: 'scroll',
  212. pageButtonItemGap: 2,
  213. pageButtonPosition: 'end',
  214. textStyle: {
  215. color: '#666'
  216. }
  217. },
  218. grid: [
  219. {
  220. left: '10%',
  221. right: '3%',
  222. top: '20px',
  223. bottom: '50%',
  224. height: '300px',
  225. width: '85%'
  226. // containLabel: true
  227. },
  228. {
  229. left: '10%',
  230. right: '3%',
  231. top: '320px',
  232. bottom: '25%',
  233. height: '300px',
  234. width: '85%'
  235. // containLabel: true
  236. },
  237. {
  238. left: '10%',
  239. right: '3%',
  240. top: '620px',
  241. bottom: '50px',
  242. height: '300px',
  243. width: '85%'
  244. // containLabel: true
  245. }
  246. ],
  247. xAxis: [
  248. {
  249. type: 'category',
  250. data: dateArray,
  251. gridIndex: 0,
  252. boundaryGap: true, // 保持间距,不要离y轴太近,不然重叠了
  253. axisLine: {
  254. // show: false,
  255. lineStyle: {
  256. color: 'white', // x轴线颜色
  257. }
  258. },
  259. axisTick: {
  260. show: false
  261. },
  262. axisLabel: {
  263. show: false
  264. },
  265. splitLine: {
  266. show: false // 不要x轴的分割线
  267. },
  268. axisPointer: {
  269. link: {
  270. xAxisIndex: 'all'
  271. },
  272. }
  273. },
  274. {
  275. type: 'category',
  276. data: dateArray,
  277. gridIndex: 1,
  278. boundaryGap: true,
  279. axisLine: {
  280. // show: false,
  281. lineStyle: {
  282. // color: '#008000'
  283. color: 'white'
  284. }
  285. },
  286. axisTick: {
  287. show: false
  288. },
  289. axisLabel: {
  290. show: false
  291. },
  292. splitLine: {
  293. show: false
  294. },
  295. axisPointer: {
  296. link: {
  297. xAxisIndex: 'all'
  298. }
  299. }
  300. },
  301. {
  302. type: 'category',
  303. data: dateArray,
  304. gridIndex: 2,
  305. axisLine: {
  306. lineStyle: {
  307. color: 'white'
  308. }
  309. },
  310. axisTick: {
  311. show: true, // 显示刻度线
  312. alignWithLabel: true,
  313. lineStyle: {
  314. color: '#999', // 颜色
  315. width: 1, // 宽度
  316. type: 'solid' // 线样式(solid/dashed/dotted)
  317. }
  318. },
  319. axisLabel: {
  320. color: 'white',
  321. interval: 'auto', // 自动计算显示间隔,只显示部分日期但覆盖所有范围
  322. rotate: 0 // 取消倾斜角度
  323. },
  324. splitLine: {
  325. show: false
  326. },
  327. axisPointer: {
  328. link: {
  329. xAxisIndex: 'all'
  330. }
  331. }
  332. }
  333. ],
  334. yAxis: [
  335. {
  336. type: 'value',
  337. gridIndex: 0,
  338. splitNumber: 4,
  339. min: yAxisMin, // 设置y轴最小值为数据最小值向下取整
  340. max: yAxisMax, // 设置y轴最大值为数据最大值向上取整
  341. axisLine: {
  342. lineStyle: {
  343. color: 'white' // y轴坐标轴颜色
  344. }
  345. },
  346. axisTick: {
  347. show: true
  348. },
  349. axisLabel: {
  350. width: 50, // 宽度限制
  351. color: 'white',
  352. formatter: function (value, index) {
  353. return value.toFixed(2)
  354. }
  355. },
  356. splitLine: {
  357. show: false,
  358. lineStyle: {
  359. color: '#837b7b',
  360. type: 'dotted' // 设置网格线类型 dotted:虚线 solid:实线
  361. }
  362. },
  363. scale: true, // 不强制包含0,不然k线图底部空余太多
  364. },
  365. {
  366. type: 'value',
  367. gridIndex: 1,
  368. splitNumber: 3,
  369. axisLine: {
  370. lineStyle: {
  371. color: 'white'
  372. }
  373. },
  374. axisTick: {
  375. show: true
  376. },
  377. splitNumber: 5, // 刻度数量
  378. axisLabel: {
  379. width: 50, // 宽度限制
  380. color: 'white',
  381. formatter: function (value, index) {
  382. // 如果没有刻度数量,其他方法获取不到y轴刻度总长
  383. if (index === 0) {
  384. return '0'
  385. } else if (index === 5) {
  386. return ''
  387. }
  388. return value
  389. }
  390. },
  391. splitLine: {
  392. show: false,
  393. lineStyle: {
  394. color: '#837b7b',
  395. type: 'dotted'
  396. }
  397. },
  398. },
  399. {
  400. type: 'value',
  401. gridIndex: 2,
  402. splitNumber: 2,
  403. axisLine: {
  404. lineStyle: {
  405. color: 'white'
  406. }
  407. },
  408. axisTick: {
  409. show: true
  410. },
  411. splitNumber: 5, // 刻度数量
  412. axisLabel: {
  413. width: 50, // 宽度限制
  414. color: 'white',
  415. formatter: function (value, index) {
  416. if (index === 5) {
  417. return ''
  418. }
  419. return value
  420. }
  421. },
  422. splitLine: {
  423. show: false,
  424. lineStyle: {
  425. color: '#837b7b',
  426. type: 'dotted' // 设置网格线类型 dotted:虚线 solid:实线
  427. }
  428. },
  429. splitNumber: 5,
  430. min: function (value) {
  431. return 0 // 最小值
  432. },
  433. max: function (value) {
  434. return value.max + 10 // 比最大值高10, 避免最高点和上一个图表x轴重合
  435. }
  436. }
  437. ],
  438. dataZoom: [
  439. {
  440. type: 'slider',
  441. xAxisIndex: [0, 1, 2],
  442. start: 0,
  443. end: 100,
  444. show: true,
  445. bottom: window.innerWidth > 768 ? 80 : 50,
  446. height: 20,
  447. borderColor: '#CFD6E3',
  448. fillerColor: 'rgba(135, 175, 274, 0.2)',
  449. handleStyle: {
  450. color: '#CFD6E3'
  451. },
  452. textStyle: {
  453. color: '#fff'
  454. },
  455. dataBackground: {
  456. lineStyle: {
  457. color: '#CFD6E3'
  458. },
  459. areaStyle: {
  460. color: 'rgba(241,243,247,0.5)'
  461. }
  462. }
  463. },
  464. {
  465. type: 'inside',
  466. xAxisIndex: [0, 1, 2],
  467. start: 0,
  468. end: 100,
  469. zoomOnMouseWheel: true,
  470. moveOnMouseMove: true,
  471. moveOnMouseWheel: false
  472. }
  473. ],
  474. series: [
  475. {
  476. name: 'K线',
  477. type: 'candlestick',
  478. data: kLineDataArray,
  479. xAxisIndex: 0,
  480. yAxisIndex: 0,
  481. itemStyle: {
  482. color: '#14b143', // 开盘价 > 收盘价时为绿色
  483. color0: '#ef232a', // 开盘价 < 收盘价时为红色
  484. borderColor: '#14b143',
  485. borderColor0: '#ef232a',
  486. normal: {
  487. color: '#14b143', // 开盘价 > 收盘价时为绿色
  488. color0: '#ef232a', // 开盘价 < 收盘价时为红色
  489. borderColor: '#14b143',
  490. borderColor0: '#ef232a',
  491. opacity: function (params) {
  492. // K线数据格式:[开,收,低,高] 收盘价 > 开盘价时为阳线,设置边框不透明、填充透明
  493. return params.data[1] > params.data[0] ? 0 : 1
  494. }
  495. }
  496. }
  497. },
  498. {
  499. name: '红线',
  500. type: 'line',
  501. data: redLineDataArray,
  502. xAxisIndex: 1,
  503. yAxisIndex: 1,
  504. symbol: 'none',
  505. sampling: 'average',
  506. itemStyle: {
  507. normal: {
  508. color: '#ef232a'
  509. }
  510. },
  511. areaStyle: {
  512. color: {
  513. type: 'linear',
  514. x: 0,
  515. y: 0,
  516. x2: 0,
  517. y2: 1,
  518. colorStops: [
  519. { offset: 0, color: 'rgba(33, 150, 243, 0.4)' },
  520. { offset: 1, color: 'rgba(33, 150, 243, 0)' }
  521. ]
  522. }
  523. }
  524. },
  525. {
  526. name: '基础base',
  527. type: 'bar',
  528. stack: 'total',
  529. // barGap: '-100%', // 重叠
  530. xAxisIndex: 2,
  531. yAxisIndex: 2,
  532. barCategoryGap: '0%',
  533. itemStyle: {
  534. normal: {
  535. color: '#ffffff',
  536. borderWidth: 0,
  537. }
  538. },
  539. data: transparentFillingDataArray,
  540. },
  541. {
  542. name: '黄色',
  543. type: 'bar',
  544. stack: 'total',
  545. // barGap: '-100%', // 重叠
  546. xAxisIndex: 2,
  547. yAxisIndex: 2,
  548. barCategoryGap: '0%', // 类目间柱条间距为0
  549. itemStyle: {
  550. normal: {
  551. color: 'rgba(255, 255, 0, 1)',
  552. borderWidth: 0,
  553. // 加仓资金的文字显示
  554. label: {
  555. show: (params) => {
  556. return yellowLineDataArray[params.dataIndex] > 0
  557. },
  558. position: 'top',
  559. textStyle: {
  560. color: 'rgba(255, 255, 0, 1)'
  561. },
  562. formatter: (params) => {
  563. return yellowLineDataArray[params.dataIndex] > 0 ? '加仓资金' : ''
  564. }
  565. }
  566. }
  567. },
  568. data: yellowBlockDataArray,
  569. },
  570. {
  571. name: '蓝色',
  572. type: 'bar',
  573. stack: 'total',
  574. xAxisIndex: 2,
  575. yAxisIndex: 2,
  576. barCategoryGap: '0%', // 类目间柱条间距为0
  577. label: {
  578. show: true,
  579. position: 'inside'
  580. },
  581. itemStyle: {
  582. normal: {
  583. color: 'rgba(34, 196, 190, 1)',
  584. borderWidth: 0
  585. }
  586. },
  587. data: blueLineDataArray
  588. },
  589. {
  590. name: '紫色',
  591. type: 'bar',
  592. stack: 'total',
  593. xAxisIndex: 2,
  594. yAxisIndex: 2,
  595. barCategoryGap: '0%', // 类目间柱条间距为0
  596. label: {
  597. show: true,
  598. position: 'inside'
  599. },
  600. itemStyle: {
  601. normal: {
  602. color: 'rgba(191, 87, 222, 1)',
  603. borderWidth: 0
  604. }
  605. },
  606. data: purpleLineDataArray
  607. },
  608. ]
  609. }
  610. // 使用配置项显示图表
  611. bottomRadarChart.setOption(option)
  612. // 应用响应式样式
  613. if (bottomRadarRef.value) {
  614. applyResponsiveStyles(bottomRadarRef.value);
  615. }
  616. // 创建响应式处理器
  617. if (resizeHandler) {
  618. resizeHandler.cleanup();
  619. }
  620. resizeHandler = createChartResizeHandler({
  621. chart: bottomRadarChart,
  622. container: bottomRadarRef.value,
  623. option: option,
  624. beforeResize: adjustGridHeight,
  625. name: '情绪探底雷达图表'
  626. });
  627. // 立即触发一次resize以确保初始布局正确
  628. setTimeout(() => {
  629. if (resizeHandler) {
  630. resizeHandler.triggerResize();
  631. }
  632. }, 100);
  633. function adjustGridHeight() {
  634. if (window.innerWidth <= 768) {
  635. option.grid[0].height = '150px'
  636. option.grid[1].height = '150px'
  637. option.grid[2].height = '150px'
  638. option.grid[0].left = '15%'
  639. option.grid[1].left = '15%'
  640. option.grid[2].left = '15%'
  641. option.grid[1].top = '170px'
  642. option.grid[2].top = '320px'
  643. option.grid[0].width = '80%'
  644. option.grid[1].width = '80%'
  645. option.grid[2].width = '80%'
  646. }
  647. bottomRadarChart.setOption(option)
  648. }
  649. // 初始化时调整高度
  650. adjustGridHeight()
  651. }
  652. // 暴露给父级
  653. defineExpose({
  654. initEmotionalBottomRadar
  655. })
  656. onBeforeUnmount(() => {
  657. // 清理响应式处理器
  658. if (resizeHandler) {
  659. resizeHandler.cleanup();
  660. resizeHandler = null;
  661. }
  662. // 组件卸载时销毁图表
  663. if (bottomRadarChart) {
  664. bottomRadarChart.dispose();
  665. bottomRadarChart = null;
  666. }
  667. })
  668. </script>
  669. <style>
  670. #bottomRadarChart {
  671. width: 100%;
  672. height: 1040px;
  673. box-sizing: border-box;
  674. overflow: hidden;
  675. margin: 0px auto !important;
  676. padding: 0;
  677. }
  678. /* 手机端适配样式 */
  679. @media only screen and (max-width: 768px) {
  680. #bottomRadarChart {
  681. width: 90% !important;
  682. height: 560px;
  683. padding: 0;
  684. }
  685. }
  686. </style>