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.

1462 lines
36 KiB

  1. <template>
  2. <div ref="qxnlzhqEchartsRef" class="qxnlzhqEcharts"></div>
  3. </template>
  4. <script setup>
  5. import { ref, onMounted, onBeforeUnmount, toRef, reactive } from 'vue'
  6. import { useLanguage } from '@/utils/languageService'
  7. import { ElMessage } from 'element-plus'
  8. import * as echarts from 'echarts'
  9. // import { text } from 'stream/consumers'
  10. // import { start } from 'repl'
  11. const { translate, t } = useLanguage()
  12. defineExpose({ initQXNLZHEcharts })
  13. let qxnlzhqEchartsRef = ref(null)
  14. let qxnlzhqEchartsInstance = null
  15. let regions = reactive([])
  16. let markLineRegions = reactive([])
  17. const dataMax = ref(null)
  18. // 设置区域名称 位置
  19. function getNameTop(min, max, regionMin, regionMax, regionMiidle) {
  20. max = Math.min(max, regionMax)
  21. min = Math.max(min, regionMin)
  22. // console.log(
  23. // 'min',
  24. // min,
  25. // 'max',
  26. // max,
  27. // 'regionMin',
  28. // regionMin,
  29. // 'regionMax',
  30. // regionMax,
  31. // 'regionMiidle',
  32. // regionMiidle
  33. // )
  34. // 获取整个图表的高度
  35. const chartHeight = qxnlzhqEchartsInstance.getHeight()
  36. const topHeight = 40
  37. const bottomHeight = 60
  38. const dataZoomHeight = 20
  39. const noHeight = topHeight + bottomHeight + dataZoomHeight
  40. // console.log('%', ((max - Number(regionMiidle)) / (max - min)) * (chartHeight - noHeight))
  41. // console.log(chartHeight)
  42. // 60: 为x轴占的高度,20: 向上偏移量让文字向上移动
  43. return ((max - Number(regionMiidle)) / (max - min)) * (chartHeight - noHeight)
  44. // return 2.84
  45. }
  46. // 设置区域最大值 位置
  47. function getNumberTop(min, max, regionMax) {
  48. // 获取整个图表的高度
  49. const chartHeight = qxnlzhqEchartsInstance.getHeight()
  50. // 60: 为x轴占的高度
  51. return ((max - Number(regionMax)) / (max - min)) * (chartHeight - 60)
  52. // return 10
  53. }
  54. // 生成图形标注(核心逻辑)
  55. const generateGraphics = (min, max) => {
  56. // console.log('regions', regions)
  57. let regionMin
  58. let regionMax
  59. for (let i = 0; i < regions.length; i++) {
  60. if (i == 0) {
  61. regionMin = Number(regions[i].min)
  62. regionMax = Number(regions[i].max)
  63. } else {
  64. regionMin = Math.min(regionMin, Number(regions[i].min))
  65. regionMax = Math.max(regionMax, Number(regions[i].max))
  66. }
  67. // console.log('regionMin', regionMin, 'regionMax', regionMax)
  68. }
  69. return regions.flatMap((region) => {
  70. // console.log(region)
  71. if (!region.min || !region.max) return []
  72. const middleY = (Number(region.min) + Number(region.max)) / 2
  73. // const safeY = Math.max(min, Math.min(middleY, max * 0.99))
  74. const safeY = (Math.max(min, region.min) + Math.min(max, region.max)) / 2
  75. // 检查区域是否完全可见
  76. const isFullyVisible = region.min >= min && region.max <= max
  77. // 检查区域是否部分可见
  78. const isPartiallyVisible = region.min < max && region.max > min && !isFullyVisible
  79. // 计算区域在图表中的实际像素高度
  80. const chartHeight = qxnlzhqEchartsInstance ? qxnlzhqEchartsInstance.getHeight() : 400
  81. const visibleRegionMin = Math.max(region.min, min)
  82. const visibleRegionMax = Math.min(region.max, max)
  83. const regionPixelHeight =
  84. ((visibleRegionMax - visibleRegionMin) / (max - min)) * (chartHeight - 60)
  85. // 设置最小高度阈值,区域太小时不显示名称
  86. const minHeightThreshold = 5 // 像素
  87. const shouldShowName = regionPixelHeight >= minHeightThreshold
  88. const graphics = []
  89. // 区域名称(中间位置)- 只有在区域足够大时才显示
  90. if ((isFullyVisible || isPartiallyVisible) && shouldShowName) {
  91. graphics.push({
  92. type: 'text',
  93. left: region.left,
  94. right: region.right,
  95. top:
  96. window.innerWidth > 769
  97. ? 40 - 6 + getNameTop(min, max, regionMin, regionMax, safeY)
  98. : 40 - 3 + getNameTop(min, max, regionMin, regionMax, safeY),
  99. // top: 40,
  100. style: {
  101. text: region.name,
  102. fill: region.fontColor,
  103. fontSize: window.innerWidth > 769 ? 12 : 9,
  104. fontWeight: 'bold'
  105. },
  106. z: 2
  107. })
  108. }
  109. // y轴数值(顶部位置)
  110. // if (isFullyVisible) {
  111. // // 检测是否为手机端
  112. // const isMobile = window.matchMedia('(max-width: 768px)').matches
  113. // graphics.push({
  114. // type: 'text',
  115. // left: '5%', // 向右调整位置
  116. // top: getNumberTop(min, max, region.max),
  117. // // left: isMobile ? '15%' : '5%', // 手机端调整位置,其他设备保持原位置
  118. // // top: getNumberTop(min, max, region.max) + (isMobile ? 0 : 5), // 手机端向下偏移5像素
  119. // // top: 100,
  120. // style: {
  121. // text: region.max.toString(),
  122. // // fill: isMobile ? '#8f8f98' : region.NumberColor, // 手机端使用灰色,其他设备保持原色
  123. // fontSize: 12,
  124. // fill: region.NumberColor
  125. // // fontWeight: isMobile ? 'bold' : 'normal', // 手机端加粗
  126. // // textBorderColor: isMobile ? 'rgba(0,0,0,0.5)' : 'transparent',
  127. // // textBorderWidth: isMobile ? 1 : 0
  128. // },
  129. // z: 3
  130. // })
  131. // }
  132. return graphics
  133. })
  134. }
  135. const klineData = ref()
  136. const qxnlzhqData = ref()
  137. // const fetchData = async () => {
  138. // const qxnlzhqStore = localStorage.getItem('qxnlzhq')
  139. // const klineStore = localStorage.getItem('kline20Data')
  140. // // 检查是否有缓存数据
  141. // if (qxnlzhqStore && klineStore) {
  142. // const qxnlzhqParsed = JSON.parse(qxnlzhqStore)
  143. // const klineParsed = JSON.parse(klineStore)
  144. // // 深拷贝数据
  145. // qxnlzhqData.value = JSON.parse(JSON.stringify(qxnlzhqParsed))
  146. // klineData.value = JSON.parse(JSON.stringify(klineParsed))
  147. // initQXNLZHEcharts(klineData.value, qxnlzhqData.value)
  148. // } else {
  149. // ElMessage.error(response.data.msg || '请求失败')
  150. // }
  151. // }
  152. function initQXNLZHEcharts(kline, qxnlzhqData) {
  153. // console.log('kline', kline)
  154. const textEcahrts = t.value // 创建多语言实例
  155. // 测试数据 !!! 删掉
  156. // qxnlzhqData.topxh = ['2025/07/22', '2025/07/22']
  157. // qxnlzhqData.lowxh = ['2025/07/02', '2025/07/02']
  158. // qxnlzhqData.qixh = ['2025/07/08', '2025/07/08']
  159. if (!qxnlzhqEchartsRef.value) {
  160. console.log('DOM 元素未准备好,无法初始化 ECharts')
  161. return
  162. }
  163. if (qxnlzhqEchartsInstance) {
  164. qxnlzhqEchartsInstance.dispose()
  165. }
  166. // 数据
  167. let mixData = []
  168. if (!Array.isArray(kline)) {
  169. console.log('不是')
  170. }
  171. kline.forEach((element) => {
  172. let date = element[0]
  173. let value = [element[1], element[2], element[3], element[4]]
  174. mixData.push({
  175. date,
  176. value
  177. })
  178. })
  179. // 动态区域配置
  180. // dd到zc 低吸区------情绪冰点区 ; zc到ht 关注区------认知潜伏区; ht到qs 回调区------多空消化区 ; qs到tp 拉升区------共识加速区;
  181. // tp到js 突破区------情绪临界区 ; js到yl 警示区-------杠杆失衡区 ; yl到gg 风险区-------情绪熔断区;
  182. regions = [
  183. {
  184. min: qxnlzhqData.dd,
  185. max: qxnlzhqData.zc,
  186. name: '情绪冰点区',
  187. color: '#F5D6FF',
  188. fontColor: '#A7691C',
  189. fontSize: window.innerWidth > 769 ? 12 : 9,
  190. NumberColor: 'white',
  191. left: null,
  192. right: '7%'
  193. },
  194. {
  195. min: qxnlzhqData.zc,
  196. max: qxnlzhqData.ht,
  197. name: '认知潜伏区',
  198. color: '#FFF6C4',
  199. fontColor: '#A7691C',
  200. NumberColor: 'white',
  201. left: null,
  202. right: '7%'
  203. },
  204. {
  205. min: qxnlzhqData.ht,
  206. max: qxnlzhqData.qs,
  207. name: '多空消化区',
  208. color: {
  209. type: 'linear',
  210. x: 0,
  211. y: 0,
  212. x2: 1,
  213. y2: 0,
  214. colorStops: [
  215. { offset: 0, color: '#D7FF9B' },
  216. { offset: 1, color: '#CEFF85' }
  217. ]
  218. },
  219. fontColor: '#A7691C',
  220. NumberColor: 'white',
  221. left: null,
  222. right: '7%'
  223. },
  224. {
  225. min: qxnlzhqData.qs,
  226. max: qxnlzhqData.tp,
  227. name: '共识加速区',
  228. color: '#FFDC8F',
  229. fontColor: '#A7691C',
  230. NumberColor: 'white',
  231. left: null,
  232. right: '7%'
  233. },
  234. {
  235. min: qxnlzhqData.tp,
  236. max: qxnlzhqData.js,
  237. name: '情绪临界区',
  238. color: '#FFC0AA',
  239. fontColor: '#A7691C',
  240. NumberColor: 'white',
  241. left: '32%',
  242. right: null
  243. }
  244. ]
  245. // gg yl为-1 不绘制部分图表
  246. if (Number(qxnlzhqData.yl) != -1) {
  247. regions.push({
  248. min: qxnlzhqData.js,
  249. max: qxnlzhqData.yl,
  250. name: '杠杆失衡区',
  251. color: {
  252. type: 'linear',
  253. x: 0,
  254. y: 0,
  255. x2: 1,
  256. y2: 0,
  257. colorStops: [
  258. { offset: 0, color: '#FEA474' },
  259. { offset: 1, color: '#FFAAF6' }
  260. ]
  261. },
  262. fontColor: '#A7691C',
  263. NumberColor: 'white',
  264. left: '32%',
  265. right: null
  266. })
  267. }
  268. if (Number(qxnlzhqData.gg) != -1) {
  269. regions.push({
  270. min: qxnlzhqData.yl,
  271. max: qxnlzhqData.gg,
  272. name: '情绪熔断区',
  273. color: {
  274. type: 'linear',
  275. x: 0,
  276. y: 0,
  277. x2: 1,
  278. y2: 0,
  279. colorStops: [
  280. { offset: 0, color: '#F66475' },
  281. { offset: 1, color: '#FFB98E' }
  282. ]
  283. },
  284. fontColor: '#A7691C',
  285. NumberColor: 'white',
  286. left: '32%',
  287. right: null
  288. })
  289. }
  290. // 计算动态的y轴范围
  291. const priceValues = kline.flatMap((item) => [item[1], item[2], item[3], item[4]])
  292. const dataMin = Math.min(...priceValues)
  293. const dataMax = Math.max(...priceValues)
  294. // 计算止盈止损价格
  295. const stopProfitPrice = Number(qxnlzhqData.cc) * 1.05 // 止盈价
  296. const stopLossPrice = Number(qxnlzhqData.cc) * 0.97 // 止损价
  297. // 确定起始和结束位置
  298. const startIndex = Math.max(0, mixData.length - 17)
  299. // 创建完整数据数组
  300. const takeProfitData = new Array(mixData.length).fill(null)
  301. const stopLossData = new Array(mixData.length).fill(null)
  302. // 填充显示区域的数据
  303. for (var i = startIndex; i < mixData.length; i++) {
  304. takeProfitData[i] = stopProfitPrice
  305. stopLossData[i] = stopLossPrice
  306. }
  307. // topxh、lowxh、qixh 对应k线染色
  308. // 创建中间区域数据
  309. const topMiddleRangeData = []
  310. const topMiddleRangeData1 = []
  311. const lowMiddleRangeData = []
  312. const lowMiddleRangeData1 = []
  313. const markPointData = []
  314. const qixhData = ref([])
  315. const topData = ref([])
  316. const lowData = ref([])
  317. const maxKlineData = {
  318. data: {
  319. value: [0, 0, 0, 0]
  320. }
  321. }
  322. const lastKlineData = {
  323. data: {
  324. value: [0, 0, 0, 0]
  325. }
  326. }
  327. let markLineMax = Math.max(
  328. Math.ceil(dataMax * 1.02),
  329. qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02),
  330. stopProfitPrice * 1.02
  331. )
  332. markLineRegions = regions.filter((region) => {
  333. return region.max < markLineMax
  334. })
  335. console.log('markLineMax', markLineMax, 'markLineRegions', markLineRegions)
  336. mixData.forEach((item, index) => {
  337. const [open, close, low, high] = item.value
  338. const rangeHeight = high - low
  339. const noneItem = { date: item.date, value: [null, null, null, null] }
  340. // const middleThirdStart = low + rangeHeight * (1/3);
  341. // const middleThirdEnd = low + rangeHeight * (2/3);
  342. let color = null
  343. // 获取最高价的K线数据和最后一根K线数据
  344. if (maxKlineData == null || maxKlineData.data.value[3] < high) {
  345. maxKlineData.data = item
  346. maxKlineData.index = index
  347. }
  348. lastKlineData.data = item
  349. lastKlineData.index = index
  350. let isTopxh = false
  351. let isLowxh = false
  352. if (qxnlzhqData.topxh.includes(item.date)) {
  353. topData.value.push({
  354. date: item.date,
  355. value: [item.value[0], item.value[1], item.value[0], item.value[1]]
  356. })
  357. color = '#000000' // 黑色
  358. isTopxh = true
  359. } else {
  360. topData.value.push(noneItem)
  361. }
  362. if (qxnlzhqData.lowxh.includes(item.date)) {
  363. lowData.value.push({
  364. date: item.date,
  365. value: [item.value[0], item.value[1], item.value[0], item.value[1]]
  366. })
  367. color = '#001EFF' // 蓝色
  368. isLowxh = true
  369. } else {
  370. lowData.value.push(noneItem)
  371. }
  372. if (qxnlzhqData.qixh.includes(item.date)) {
  373. console.log('item', item)
  374. qixhData.value.push({
  375. date: item.date,
  376. value: [item.value[0], item.value[1], item.value[0], item.value[1]]
  377. })
  378. } else {
  379. qixhData.value.push(noneItem)
  380. }
  381. // 添加TOP中间区域数据
  382. if (isTopxh) {
  383. topMiddleRangeData.push({
  384. value: [index, close > open ? close - open : open - close],
  385. itemStyle: {
  386. normal: {
  387. color: color
  388. }
  389. }
  390. })
  391. topMiddleRangeData1.push({
  392. value: [index, close > open ? open : close],
  393. itemStyle: {
  394. normal: {
  395. color: 'transparent'
  396. }
  397. }
  398. })
  399. lowMiddleRangeData.push(null)
  400. lowMiddleRangeData1.push(null)
  401. } else if (isLowxh) {
  402. lowMiddleRangeData.push({
  403. value: [index, close > open ? close - open : open - close],
  404. itemStyle: {
  405. normal: {
  406. color: '#001EFF'
  407. }
  408. }
  409. })
  410. lowMiddleRangeData1.push({
  411. value: [index, close > open ? open : close],
  412. itemStyle: {
  413. normal: {
  414. color: 'transparent'
  415. }
  416. }
  417. })
  418. console.log('lowMiddleRangeData', lowMiddleRangeData)
  419. console.log('lowMiddleRangeData111111', lowMiddleRangeData1)
  420. topMiddleRangeData.push(null)
  421. topMiddleRangeData1.push(null)
  422. } else {
  423. topMiddleRangeData.push(null)
  424. topMiddleRangeData1.push(null)
  425. lowMiddleRangeData.push(null)
  426. lowMiddleRangeData1.push(null)
  427. }
  428. // 添加文字标记数据
  429. // if (qxnlzhqData.qixh.includes(item.date)) {
  430. // markPointData.push({
  431. // name: '起',
  432. // coord: [index, high],
  433. // itemStyle: {
  434. // normal: {
  435. // color: 'rgba(0,0,0,0)' // 标记点透明
  436. // }
  437. // },
  438. // label: {
  439. // normal: {
  440. // show: true,
  441. // position: 'top',
  442. // formatter: '起',
  443. // textStyle: {
  444. // color: '#249409',
  445. // fontSize: window.innerWidth > 769 ? 12 : 9,
  446. // textBorderColor: '#FFFFFF',
  447. // textBorderWidth: 2,
  448. // fontWeight: 'bold'
  449. // }
  450. // }
  451. // }
  452. // })
  453. // } else if (qxnlzhqData.lowxh.includes(item.date)) {
  454. // markPointData.push({
  455. // name: 'LOW',
  456. // coord: [index, low],
  457. // itemStyle: {
  458. // normal: {
  459. // color: 'rgba(0,0,0,0)' // 标记点透明
  460. // }
  461. // },
  462. // label: {
  463. // normal: {
  464. // show: true,
  465. // position: 'bottom',
  466. // formatter: 'LOW',
  467. // textStyle: {
  468. // color: '#001EFF',
  469. // fontSize: window.innerWidth > 769 ? 12 : 9,
  470. // textBorderColor: '#FFFFFF',
  471. // textBorderWidth: 2,
  472. // fontWeight: 'bold'
  473. // }
  474. // }
  475. // }
  476. // })
  477. // } else if (qxnlzhqData.topxh.includes(item.date)) {
  478. // markPointData.push({
  479. // name: 'TOP',
  480. // coord: [index, high],
  481. // itemStyle: {
  482. // normal: {
  483. // color: 'rgba(0,0,0,0)' // 标记点透明
  484. // }
  485. // },
  486. // label: {
  487. // normal: {
  488. // show: true,
  489. // position: 'top',
  490. // formatter: 'TOP',
  491. // textStyle: {
  492. // color: '#000',
  493. // fontSize: window.innerWidth > 769 ? 12 : 9,
  494. // textBorderColor: '#FFFFFF',
  495. // textBorderWidth: 2,
  496. // fontWeight: 'bold'
  497. // }
  498. // }
  499. // }
  500. // })
  501. // }
  502. })
  503. // console.log('maxKlineData', maxKlineData)
  504. // console.log('lastKlineData', lastKlineData)
  505. markPointData.push({
  506. name: `${Number(maxKlineData.data.value[3]).toFixed(2)}`,
  507. coord: [maxKlineData.index, maxKlineData.data.value[3]],
  508. itemStyle: {
  509. normal: {
  510. color: 'rgba(0,0,0,0)' // 标记点透明
  511. }
  512. },
  513. label: {
  514. normal: {
  515. show: true,
  516. position: 'top',
  517. formatter: `${Number(maxKlineData.data.value[3]).toFixed(2)}`,
  518. textStyle: {
  519. color: '#2171DD',
  520. fontSize: window.innerWidth > 769 ? 12 : 9,
  521. textBorderColor: '#FFFFFF',
  522. textBorderWidth: 2,
  523. fontWeight: 'bold'
  524. }
  525. }
  526. }
  527. })
  528. markPointData.push({
  529. name: `${Number(lastKlineData.data.value[3]).toFixed(2)}`,
  530. coord: [lastKlineData.index, lastKlineData.data.value[2]],
  531. itemStyle: {
  532. normal: {
  533. color: 'rgba(0,0,0,0)' // 标记点透明
  534. }
  535. },
  536. label: {
  537. normal: {
  538. show: true,
  539. position: 'bottom',
  540. formatter: `${Number(lastKlineData.data.value[1]).toFixed(2)}`,
  541. textStyle: {
  542. color: '#3B8F08',
  543. fontSize: window.innerWidth > 769 ? 12 : 9,
  544. textBorderColor: '#FFFFFF',
  545. textBorderWidth: 2,
  546. fontWeight: 'bold'
  547. }
  548. }
  549. }
  550. })
  551. // 创建带有 backgroundSize 的图案
  552. function createPatternWithSize(gradient, size) {
  553. const canvas = document.createElement('canvas')
  554. const ctx = canvas.getContext('2d')
  555. // 解析 backgroundSize (例如: '60px 100%')
  556. const [width, height] = size.split(' ')
  557. canvas.width = parseInt(width) || 60
  558. canvas.height = parseInt(height) || 60 // 固定高度或根据需要调整
  559. // 创建渐变
  560. const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
  561. grad.addColorStop(0.24, 'rgba(0,0,0,0)')
  562. grad.addColorStop(0.25, 'rgba(255,255,255,0.7)')
  563. grad.addColorStop(0.26, 'rgba(0,0,0,0)')
  564. grad.addColorStop(0.74, 'rgba(0,0,0,0)')
  565. grad.addColorStop(0.75, 'rgba(255,255,255,0.7)')
  566. grad.addColorStop(0.76, 'rgba(0,0,0,0)')
  567. ctx.fillStyle = grad
  568. ctx.fillRect(0, 0, canvas.width, canvas.height)
  569. return canvas
  570. }
  571. // 定义额外的区域
  572. const addWhiteRegions = []
  573. regions.forEach((item) => {
  574. if (item.name == '情绪熔断区' || item.name == '情绪临界区' || item.name == '情绪冰点区') {
  575. addWhiteRegions.push({
  576. name: item.name,
  577. min: item.min,
  578. max: item.max,
  579. backgroundSize: '10px 10px '
  580. })
  581. }
  582. })
  583. // 动态生成图例数据
  584. const legendData = []
  585. const legendSelected = {}
  586. // 检查是否存在TOP数据
  587. const hasTopData = qxnlzhqData.topxh && qxnlzhqData.topxh.length > 0
  588. if (hasTopData) {
  589. legendData.push({
  590. name: 'TOP',
  591. textStyle: {
  592. color: '#000000',
  593. fontSize: window.innerWidth > 769 ? 12 : 9,
  594. textBorderColor: '#FFFFFF',
  595. textBorderWidth: 2,
  596. fontWeight: 'bold'
  597. }
  598. })
  599. legendSelected.TOP = true
  600. }
  601. // 检查是否存在LOW数据
  602. const hasLowData = qxnlzhqData.lowxh && qxnlzhqData.lowxh.length > 0
  603. if (hasLowData) {
  604. legendData.push({
  605. name: 'LOW',
  606. textStyle: {
  607. color: '#001EFF',
  608. fontSize: window.innerWidth > 769 ? 12 : 9,
  609. textBorderColor: '#FFFFFF',
  610. textBorderWidth: 2,
  611. fontWeight: 'bold'
  612. }
  613. })
  614. legendSelected.LOW = true
  615. }
  616. // 检查是否存在起数据
  617. const hasQixhData = qxnlzhqData.qixh && qxnlzhqData.qixh.length > 0
  618. if (hasQixhData) {
  619. legendData.push({
  620. name: '起',
  621. textStyle: {
  622. color: '#249409',
  623. fontSize: window.innerWidth > 769 ? 12 : 9,
  624. textBorderColor: '#FFFFFF',
  625. textBorderWidth: 2,
  626. fontWeight: 'bold'
  627. }
  628. })
  629. legendSelected. = true
  630. }
  631. // 初始化图表
  632. qxnlzhqEchartsInstance = echarts.init(qxnlzhqEchartsRef.value)
  633. let option
  634. // 设置图表配置
  635. option = {
  636. legend: {
  637. data: legendData,
  638. selected: legendSelected,
  639. top: window.innerWidth > 768 ? '0%' : '1%',
  640. textStyle: {
  641. fontSize: window.matchMedia('(max-width: 767px)').matches ? 9 : 12
  642. }
  643. },
  644. tooltip: {
  645. show: true,
  646. trigger: 'axis',
  647. axisPointer: {
  648. type: 'cross',
  649. lineStyle: {
  650. color: 'grey',
  651. width: 1,
  652. type: 'dashed'
  653. },
  654. label: {
  655. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  656. color: '#fff',
  657. borderColor: '#fff',
  658. borderWidth: 1
  659. }
  660. },
  661. backgroundColor: 'rgba(232, 232, 242, 0.87)',
  662. borderColor: '#fff',
  663. borderWidth: 1,
  664. borderRadius: 8,
  665. padding: 10,
  666. textStyle: {
  667. color: '#fff',
  668. fontSize: 12
  669. },
  670. position: function (point, params, dom, rect, size) {
  671. // 检测是否为手机端
  672. const isMobile = window.matchMedia('(max-width: 768px)').matches
  673. const tooltipWidth = size.contentSize[0]
  674. const tooltipHeight = size.contentSize[1]
  675. const chartWidth = size.viewSize[0]
  676. const chartHeight = size.viewSize[1]
  677. let x = point[0]
  678. let y = point[1]
  679. if (isMobile) {
  680. // 手机端:确保tooltip完全在图表内显示
  681. // 水平位置调整
  682. if (x + tooltipWidth > chartWidth - 10) {
  683. x = chartWidth - tooltipWidth - 10
  684. }
  685. if (x < 10) {
  686. x = 10
  687. }
  688. // 垂直位置调整:确保不被遮挡,优先显示在下方
  689. if (y - tooltipHeight - 20 < 0) {
  690. // 如果上方空间不足,显示在下方
  691. y = y + 20
  692. if (y + tooltipHeight > chartHeight - 60) {
  693. // 如果下方也不够,显示在中间偏上位置
  694. y = Math.max(20, chartHeight - tooltipHeight - 60)
  695. }
  696. } else {
  697. // 上方有足够空间,显示在上方
  698. y = y - tooltipHeight - 20
  699. }
  700. } else {
  701. // 桌面端:保持原有逻辑
  702. if (x + tooltipWidth > chartWidth - 20) {
  703. x = x - tooltipWidth - 20
  704. } else {
  705. x = x + 20
  706. }
  707. if (y - tooltipHeight < 20) {
  708. y = y + 20
  709. } else {
  710. y = y - tooltipHeight - 20
  711. }
  712. }
  713. return [x, y]
  714. },
  715. formatter: function (params) {
  716. if (!params || params.length === 0) return ''
  717. let result = `<div style="font-weight: bold; color: black; margin-bottom: 8px;">${params[0].name}</div>`
  718. params.forEach((param, index) => {
  719. console.log('参数索引:', index, '系列:', param)
  720. let value = param.value
  721. let color = param.color
  722. if (param.seriesType === 'candlestick') {
  723. const openN = textEcahrts.kai
  724. const closeN = textEcahrts.shou
  725. const lowN = textEcahrts.di
  726. const highN = textEcahrts.gao
  727. // const zhangdie = textEcahrts.zhangdie
  728. let openPrice = value[1] // 开盘价
  729. let closePrice = value[2] // 收盘价
  730. let lowPrice = value[3] // 最低价
  731. let highPrice = value[4] // 最高价
  732. // 检查数据有效性
  733. if (
  734. typeof openPrice !== 'number' ||
  735. typeof closePrice !== 'number' ||
  736. typeof lowPrice !== 'number' ||
  737. typeof highPrice !== 'number'
  738. ) {
  739. return ''
  740. }
  741. let priceChange
  742. let changePercent
  743. if (param.data[0] != 0) {
  744. // console.log(
  745. // 'preDayDate',
  746. // kline[param.data[0] - 1][0],
  747. // 'preDayClosePrice',
  748. // kline[param.data[0] - 1][2]
  749. // )
  750. // console.log('paramDate', param.name, 'paramClosePrice', closePrice)
  751. let preClosePrice = kline[param.data[0] - 1][2] //昨日收盘价;
  752. priceChange = closePrice - preClosePrice
  753. changePercent = ((priceChange / preClosePrice) * 100).toFixed(2)
  754. }
  755. let changeColor = priceChange >= 0 ? '#32B520' : '#D8001B'
  756. result += `<div style="margin-bottom: 6px;">`
  757. // result += `<div style="color: #fff; font-weight: bold;">${param.seriesName}</div>`
  758. result += `<div style="color: black;">${openN}: ${openPrice.toFixed(2)}</div>`
  759. result += `<div style="color: black;">${closeN}: ${closePrice.toFixed(2)}</div>`
  760. result += `<div style="color: black;">${lowN}: ${lowPrice.toFixed(2)}</div>`
  761. result += `<div style="color: black;">${highN}: ${highPrice.toFixed(2)}</div>`
  762. if (param.data[0] != 0) {
  763. result += `<div style="color: ${changeColor};">涨跌: ${
  764. priceChange >= 0 ? '+' : ''
  765. }${priceChange.toFixed(2)} (${changePercent}%)</div>`
  766. result += `</div>`
  767. }
  768. } else if (
  769. param.seriesName === '止盈线' &&
  770. value !== null &&
  771. value !== undefined &&
  772. typeof value === 'number'
  773. ) {
  774. result += `<div style="color: #FF0000; margin-bottom: 4px;">${
  775. param.seriesName
  776. }: ${value.toFixed(2)}</div>`
  777. } else if (
  778. param.seriesName === '止损线' &&
  779. value !== null &&
  780. value !== undefined &&
  781. typeof value === 'number'
  782. ) {
  783. result += `<div style="color: #001EFF; margin-bottom: 4px;">${
  784. param.seriesName
  785. }: ${value.toFixed(2)}</div>`
  786. }
  787. })
  788. return result
  789. }
  790. },
  791. dataZoom: [
  792. {
  793. top: window.innerWidth <= 768 ? '86%' : '',
  794. type: 'slider',
  795. xAxisIndex: 0,
  796. start: 0,
  797. end: 100,
  798. show: true,
  799. bottom: 40,
  800. height: 20,
  801. borderColor: '#fff',
  802. fillerColor: 'rgba(255, 255, 255, 0.2)',
  803. handleStyle: {
  804. color: '#fff',
  805. borderColor: 'white'
  806. },
  807. textStyle: {
  808. color: 'white'
  809. }
  810. },
  811. {
  812. type: 'inside',
  813. xAxisIndex: 0,
  814. start: 0,
  815. end: 100,
  816. zoomOnMouseWheel: true,
  817. moveOnMouseMove: true,
  818. moveOnMouseWheel: false
  819. }
  820. ],
  821. xAxis: {
  822. type: 'category',
  823. data: mixData.map((item) => item.date),
  824. axisLabel: {
  825. rotate: 0, // 取消倾斜角度
  826. color: 'white',
  827. interval: 'auto', // 自动计算显示间隔,只显示部分日期但覆盖所有范围
  828. fontSize: window.innerWidth > 769 ? 12 : 9,
  829. showMaxLabel: true
  830. },
  831. axisLine: {
  832. // show: false,
  833. onZero: false,
  834. lineStyle: {
  835. color: 'white' // x轴线颜色
  836. }
  837. },
  838. axisTick: {
  839. alignWithLabel: true
  840. }
  841. },
  842. yAxis: {
  843. scale: true,
  844. axisLine: {
  845. // show: false,
  846. lineStyle: {
  847. color: 'white' // x轴线颜色
  848. // width: 1
  849. }
  850. },
  851. splitLine: {
  852. show: false
  853. },
  854. axisLabel: {
  855. // 刻度标签
  856. show: true,
  857. color: 'white',
  858. fontSize: window.innerWidth > 769 ? 12 : 9
  859. },
  860. axisTick: {
  861. // 刻度线
  862. show: true,
  863. color: 'white'
  864. },
  865. min:
  866. qxnlzhqData.dd < stopLossPrice * 0.98
  867. ? Math.floor(qxnlzhqData.dd)
  868. : Math.floor(stopLossPrice * 0.98),
  869. max: Math.round(
  870. Math.max(
  871. Math.ceil(dataMax * 1.02),
  872. qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02),
  873. stopProfitPrice * 1.02
  874. )
  875. )
  876. },
  877. // 自定义区域名称和区域范围值 位置
  878. graphic: generateGraphics(
  879. qxnlzhqData.dd < stopLossPrice * 0.98
  880. ? Math.floor(qxnlzhqData.dd)
  881. : Math.floor(stopLossPrice * 0.98),
  882. Math.max(
  883. Math.ceil(dataMax * 1.02),
  884. qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02),
  885. stopProfitPrice * 1.02
  886. )
  887. ),
  888. series: [
  889. {
  890. type: 'candlestick',
  891. data: mixData.map((item) => item.value),
  892. z: 3,
  893. clip: true,
  894. markPoint: {
  895. symbol: 'circle',
  896. symbolSize: 10,
  897. data: markPointData,
  898. z: 5 // 确保标记显示在最上层
  899. },
  900. itemStyle: {
  901. normal: {
  902. // 阳线样式(收盘 > 开盘)
  903. // color: '#14b143', // 开盘价 < 收盘价时为绿色
  904. color: '#00AAFF',
  905. color0: '#FF007F', // 开盘价 > 收盘价时为红色
  906. borderColor: '#00AAFF', // 阳线边框色(绿)
  907. borderColor0: '#FF007F' // 阴线边框色(红)
  908. // borderWidth: 1.5
  909. }
  910. },
  911. // 实现 分区域背景色
  912. markArea: {
  913. silent: true,
  914. data: [
  915. ...regions.map((region) => [
  916. {
  917. x: '30%',
  918. yAxis: region.min,
  919. itemStyle: {
  920. normal: {
  921. color: region.color
  922. // color:'#000'
  923. }
  924. }
  925. },
  926. { x: '95%', yAxis: region.max }
  927. ]),
  928. ...addWhiteRegions.map((region) => [
  929. {
  930. x: '30%',
  931. yAxis: region.min,
  932. itemStyle: {
  933. normal: {
  934. color: {
  935. image: createPatternWithSize(region.color, region.backgroundSize),
  936. repeat: 'repeat'
  937. }
  938. }
  939. }
  940. },
  941. { x: '95%', yAxis: region.max }
  942. ])
  943. ],
  944. markPoint: {
  945. symbol: 'circle',
  946. symbolSize: 10,
  947. data: markPointData,
  948. z: 5 // 确保标记显示在最上层
  949. }
  950. },
  951. // 添加markLine实现顶部边框
  952. markLine: {
  953. silent: true,
  954. symbol: 'none',
  955. data: [
  956. ...markLineRegions.map((region) => [
  957. {
  958. name: `${Number(region.max).toFixed(2)}`,
  959. x: '30%',
  960. yAxis: region.max, // 只在区域顶部画线
  961. label: {
  962. normal: {
  963. // show: true,
  964. position: 'start',
  965. color:
  966. region.name == '情绪冰点区' ||
  967. region.name == '多空消化区' ||
  968. region.name == '认知潜伏区'
  969. ? 'white'
  970. : 'white',
  971. fontSize: window.innerWidth > 769 ? 12 : 9,
  972. }
  973. },
  974. lineStyle: {
  975. normal: {
  976. color: '#FFFFFF',
  977. width: 2,
  978. type: 'dashed'
  979. }
  980. }
  981. },
  982. {
  983. x: '95%',
  984. yAxis: region.max
  985. }
  986. ]),
  987. [
  988. {
  989. name: `止盈${stopProfitPrice.toFixed(2)}`,
  990. x: '60%',
  991. yAxis: stopProfitPrice,
  992. label: {
  993. normal: {
  994. position: 'start',
  995. fontSize: window.innerWidth > 769 ? 13 : 9,
  996. fontWeight: 'bold',
  997. textBorderColor: '#FFFFFF',
  998. textBorderWidth: 2
  999. }
  1000. },
  1001. lineStyle: {
  1002. normal: {
  1003. color: '#FF0000',
  1004. width: 2,
  1005. type: 'solid'
  1006. }
  1007. }
  1008. },
  1009. {
  1010. x: '95%',
  1011. yAxis: stopProfitPrice
  1012. }
  1013. ],
  1014. [
  1015. {
  1016. name: `止损${stopLossPrice.toFixed(2)}`,
  1017. x: '60%',
  1018. yAxis: stopLossPrice,
  1019. label: {
  1020. normal: {
  1021. position: 'start',
  1022. fontSize: window.innerWidth > 769 ? 13 : 9,
  1023. fontWeight: 'bold',
  1024. textBorderColor: '#FFFFFF',
  1025. textBorderWidth: 2
  1026. }
  1027. },
  1028. lineStyle: {
  1029. normal: {
  1030. color: '#001EFF',
  1031. width: 2,
  1032. type: 'solid'
  1033. }
  1034. }
  1035. },
  1036. {
  1037. x: '95%',
  1038. yAxis: stopLossPrice
  1039. }
  1040. ]
  1041. ]
  1042. }
  1043. },
  1044. {
  1045. name: '起',
  1046. type: 'candlestick',
  1047. data: qixhData.value.map((item) => item.value),
  1048. itemStyle: {
  1049. normal: {
  1050. color: '#87FF6B',
  1051. color0: '#87FF6B',
  1052. borderColor: '#87FF6B',
  1053. borderColor0: '#87FF6B'
  1054. }
  1055. },
  1056. gridIndex: 0,
  1057. z: 4,
  1058. tooltip: {
  1059. show: false
  1060. }
  1061. },
  1062. {
  1063. name: 'TOP',
  1064. type: 'candlestick',
  1065. data: topData.value.map((item) => item.value),
  1066. itemStyle: {
  1067. normal: {
  1068. color: '#000',
  1069. color0: '#000',
  1070. borderColor: '#000',
  1071. borderColor0: '#000'
  1072. }
  1073. },
  1074. gridIndex: 0,
  1075. z: 4,
  1076. tooltip: {
  1077. show: false
  1078. }
  1079. },
  1080. {
  1081. name: 'LOW',
  1082. type: 'candlestick',
  1083. data: lowData.value.map((item) => item.value),
  1084. itemStyle: {
  1085. normal: {
  1086. color: '#001EFF',
  1087. color0: '#001EFF',
  1088. borderColor: '#001EFF',
  1089. borderColor0: '#001EFF'
  1090. }
  1091. },
  1092. gridIndex: 0,
  1093. z: 4,
  1094. tooltip: {
  1095. show: false
  1096. }
  1097. }
  1098. // {
  1099. // name: '止盈线',
  1100. // type: 'line',
  1101. // data: takeProfitData,
  1102. // symbol: 'none',
  1103. // lineStyle: {
  1104. // normal: {
  1105. // color: '#FF0000', // 蓝色
  1106. // width: 2,
  1107. // type: 'solid'
  1108. // }
  1109. // },
  1110. // markPoint: {
  1111. // symbol: 'circle',
  1112. // symbolSize: 1,
  1113. // data: [
  1114. // {
  1115. // coord: [mixData.map((item) => item.value).length, stopProfitPrice],
  1116. // itemStyle: {
  1117. // color: '#ff80ff',
  1118. // textBorderColor: '#ffffff'
  1119. // },
  1120. // label: {
  1121. // normal: {
  1122. // show: true,
  1123. // position: 'start',
  1124. // formatter: `{text|止盈}`,
  1125. // rich: {
  1126. // text: {
  1127. // color: '#FF0000',
  1128. // fontSize: 14,
  1129. // fontWeight: 'bold',
  1130. // textBorderColor: '#ffffff'
  1131. // }
  1132. // }
  1133. // // offset: [-20, 0]
  1134. // }
  1135. // }
  1136. // }
  1137. // ]
  1138. // }
  1139. // },
  1140. // {
  1141. // name: '止损线',
  1142. // type: 'line',
  1143. // data: stopLossData,
  1144. // symbol: 'none',
  1145. // lineStyle: {
  1146. // normal: {
  1147. // color: '#001EFF',
  1148. // width: 2,
  1149. // type: 'solid'
  1150. // }
  1151. // },
  1152. // markPoint: {
  1153. // symbol: 'circle',
  1154. // symbolSize: 1,
  1155. // data: [
  1156. // {
  1157. // coord: [mixData.map((item) => item.value).length - 1, stopLossPrice],
  1158. // itemStyle: {
  1159. // color: '#080bfd'
  1160. // },
  1161. // label: {
  1162. // normal: {
  1163. // show: true,
  1164. // position: 'start',
  1165. // formatter: `{text|止损}`,
  1166. // rich: {
  1167. // text: {
  1168. // color: '#001EFF',
  1169. // fontSize: 14,
  1170. // fontWeight: 'bold',
  1171. // textBorderColor: '#ffffff'
  1172. // }
  1173. // }
  1174. // // offset: [-20, 0]
  1175. // }
  1176. // }
  1177. // }
  1178. // ]
  1179. // }
  1180. // },
  1181. // {
  1182. // name: '最低价',
  1183. // type: 'line',
  1184. // symbol: 'none',
  1185. // lineStyle: {
  1186. // normal: {
  1187. // color: 'transparent',
  1188. // width: 0
  1189. // }
  1190. // },
  1191. // markPoint: {
  1192. // symbol: 'circle',
  1193. // symbolSize: 1,
  1194. // data: [
  1195. // {
  1196. // coord: [mixData.length - 1, mixData[mixData.length - 1].value[2]],
  1197. // itemStyle: {
  1198. // color: 'transparent'
  1199. // },
  1200. // label: {
  1201. // normal: {
  1202. // show: true,
  1203. // position: 'top',
  1204. // formatter: `{text|${mixData[mixData.length - 1].value[2].toFixed(2)}}`,
  1205. // rich: {
  1206. // text: {
  1207. // color: '#001EFF',
  1208. // fontSize: 12,
  1209. // fontWeight: 'bold',
  1210. // textBorderColor: '#ffffff',
  1211. // textBorderWidth: 2
  1212. // }
  1213. // },
  1214. // offset: [0, 30]
  1215. // }
  1216. // }
  1217. // }
  1218. // ]
  1219. // }
  1220. // },
  1221. // {
  1222. // name: '最高价',
  1223. // type: 'line',
  1224. // symbol: 'none',
  1225. // lineStyle: {
  1226. // normal: {
  1227. // color: 'transparent',
  1228. // width: 0
  1229. // }
  1230. // },
  1231. // markPoint: {
  1232. // symbol: 'circle',
  1233. // symbolSize: 1,
  1234. // data: [
  1235. // {
  1236. // coord: [mixData.length - 1, mixData[mixData.length - 1].value[3]],
  1237. // itemStyle: {
  1238. // color: 'transparent'
  1239. // },
  1240. // label: {
  1241. // normal: {
  1242. // show: true,
  1243. // position: 'bottom',
  1244. // formatter: `{text|${mixData[mixData.length - 1].value[3].toFixed(2)}}`,
  1245. // rich: {
  1246. // text: {
  1247. // color: '#FF0000',
  1248. // fontSize: 12,
  1249. // fontWeight: 'bold',
  1250. // textBorderColor: '#ffffff',
  1251. // textBorderWidth: 2
  1252. // }
  1253. // },
  1254. // offset: [0, -20]
  1255. // }
  1256. // }
  1257. // }
  1258. // ]
  1259. // }
  1260. // }
  1261. ],
  1262. // 修改grid配置,添加响应式设置
  1263. grid: {
  1264. // 根据屏幕宽度动态调整
  1265. left: window.innerWidth <= 768 ? '2%' : '5%', // 手机端增加左边距
  1266. right: window.innerWidth <= 768 ? '8%' : '8%', // 手机端增加右边距
  1267. // top: window.innerWidth <= 768 ? '2' : '10',
  1268. top: window.innerWidth <= 768 ? '40' : '40',
  1269. // top: '10',
  1270. bottom: '60',
  1271. containLabel: true, // 改为true确保标签完全显示
  1272. width: 'auto', // 改为auto让系统自动计算
  1273. height: 'auto',
  1274. overflow: 'hidden'
  1275. }
  1276. }
  1277. // 应用配置
  1278. qxnlzhqEchartsInstance.setOption(option)
  1279. // 防抖函数,避免频繁触发resize
  1280. const debounce = (func, wait) => {
  1281. let timeout
  1282. return function executedFunction(...args) {
  1283. const later = () => {
  1284. clearTimeout(timeout)
  1285. func(...args)
  1286. }
  1287. clearTimeout(timeout)
  1288. timeout = setTimeout(later, wait)
  1289. }
  1290. }
  1291. // 监听窗口大小变化,调整图表尺寸
  1292. const resizeHandler = debounce(() => {
  1293. if (qxnlzhqEchartsInstance && !qxnlzhqEchartsInstance.isDisposed()) {
  1294. try {
  1295. qxnlzhqEchartsInstance.resize()
  1296. console.log('情绪能量转化器图表已重新调整大小')
  1297. } catch (error) {
  1298. console.error('情绪能量转化器图表resize失败:', error)
  1299. }
  1300. }
  1301. }, 100) // 100ms防抖延迟
  1302. // 移除之前的监听器(如果存在)
  1303. if (window.emoEnergyConverterResizeHandler) {
  1304. window.removeEventListener('resize', window.emoEnergyConverterResizeHandler)
  1305. }
  1306. // 添加新的监听器
  1307. window.addEventListener('resize', resizeHandler)
  1308. // 存储resize处理器以便后续清理
  1309. window.emoEnergyConverterResizeHandler = resizeHandler
  1310. // 添加容器大小监听器
  1311. if (qxnlzhqEchartsRef.value && window.ResizeObserver) {
  1312. const containerObserver = new ResizeObserver(
  1313. debounce(() => {
  1314. if (qxnlzhqEchartsInstance && !qxnlzhqEchartsInstance.isDisposed()) {
  1315. try {
  1316. qxnlzhqEchartsInstance.resize()
  1317. console.log('情绪能量转化器容器大小变化,图表已调整')
  1318. } catch (error) {
  1319. console.error('情绪能量转化器容器resize失败:', error)
  1320. }
  1321. }
  1322. }, 100)
  1323. )
  1324. containerObserver.observe(qxnlzhqEchartsRef.value)
  1325. window.emoEnergyConverterContainerObserver = containerObserver
  1326. }
  1327. }
  1328. // 监听数据变化或语言变化
  1329. // watch(
  1330. // () => t.value,
  1331. // (newLang) => {
  1332. // fetchData()
  1333. // },
  1334. // { immediate: true, deep: true }
  1335. // )
  1336. // onMounted(fetchData)
  1337. onBeforeUnmount(() => {
  1338. // 组件卸载时销毁图表
  1339. if (qxnlzhqEchartsInstance) {
  1340. qxnlzhqEchartsInstance.dispose()
  1341. qxnlzhqEchartsInstance = null
  1342. }
  1343. // 移除窗口resize监听器
  1344. if (window.emoEnergyConverterResizeHandler) {
  1345. window.removeEventListener('resize', window.emoEnergyConverterResizeHandler)
  1346. window.emoEnergyConverterResizeHandler = null
  1347. }
  1348. // 清理容器观察器
  1349. if (window.emoEnergyConverterContainerObserver) {
  1350. window.emoEnergyConverterContainerObserver.disconnect()
  1351. window.emoEnergyConverterContainerObserver = null
  1352. }
  1353. })
  1354. </script>
  1355. <style scoped>
  1356. .qxnlzhqEcharts {
  1357. width: 100%;
  1358. height: 542px;
  1359. margin: 0;
  1360. box-sizing: border-box;
  1361. overflow: hidden;
  1362. }
  1363. /* 手机端适配样式 */
  1364. @media only screen and (max-width: 768px) {
  1365. .qxnlzhqEcharts {
  1366. width: 100%;
  1367. height: 300px;
  1368. /* margin: 0; */
  1369. }
  1370. /* 移动端tooltip优化 */
  1371. :deep(.echarts-tooltip) {
  1372. max-width: 280px !important;
  1373. font-size: 10px !important;
  1374. line-height: 1.3 !important;
  1375. padding: 8px 10px !important;
  1376. word-wrap: break-word !important;
  1377. white-space: normal !important;
  1378. box-sizing: border-box !important;
  1379. }
  1380. /* 确保tooltip不会超出屏幕 */
  1381. :deep(.echarts-tooltip-content) {
  1382. max-width: 100% !important;
  1383. overflow: hidden !important;
  1384. }
  1385. }
  1386. </style>