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.

973 lines
30 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 * as echarts from "echarts";
  7. defineExpose({ initQXNLZHEcharts });
  8. let qxnlzhqEchartsRef = ref(null);
  9. let qxnlzhqEchartsInstance = null;
  10. let regions = reactive([]);
  11. const dataMax = ref(null)
  12. // 设置区域名称 位置
  13. function getNameTop(min, max, regionMiidle) {
  14. // 获取整个图表的高度
  15. const chartHeight = qxnlzhqEchartsInstance.getHeight();
  16. // 60: 为x轴占的高度
  17. return (max - Number(regionMiidle)) / (max - min) * (chartHeight - 60)
  18. }
  19. // 设置区域最大值 位置
  20. function getNumberTop(min, max, regionMax) {
  21. // 获取整个图表的高度
  22. const chartHeight = qxnlzhqEchartsInstance.getHeight();
  23. // 60: 为x轴占的高度
  24. return (max - Number(regionMax)) / (max - min) * (chartHeight - 60)
  25. }
  26. // 生成图形标注(核心逻辑)
  27. const generateGraphics = (min, max) => {
  28. let hasPartialVisible = false; // 标记是否已经遇到第一个部分可见的区域
  29. return regions.flatMap((region) => {
  30. if (!region.min || !region.max) return [];
  31. const middleY = (Number(region.min) + Number(region.max)) / 2;
  32. const safeY = Math.max(min, Math.min(middleY, max * 0.99));
  33. // 检查区域是否完全可见
  34. const isFullyVisible = region.min >= min && region.max <= max;
  35. // 检查区域是否部分可见
  36. const isPartiallyVisible = (region.min < max && region.max > min) && !isFullyVisible;
  37. // 如果已经有一个部分可见的区域名称显示了,就不再显示其他部分可见的区域名称
  38. if (isPartiallyVisible && hasPartialVisible) {
  39. return [];
  40. }
  41. // 如果是第一个部分可见的区域,设置标记
  42. if (isPartiallyVisible) {
  43. hasPartialVisible = true;
  44. }
  45. const graphics = [];
  46. // 区域名称(中间位置)
  47. if (isFullyVisible || isPartiallyVisible) {
  48. graphics.push({
  49. type: "text",
  50. left: '13%',
  51. top: getNameTop(min, max, safeY),
  52. style: {
  53. text: region.name,
  54. fill: region.fontColor,
  55. fontSize: 14,
  56. fontWeight: "bold",
  57. },
  58. z: 3,
  59. });
  60. }
  61. // y轴数值(顶部位置)
  62. // if (isFullyVisible) {
  63. // graphics.push({
  64. // type: "text",
  65. // left: '13%', // 向右调整位置
  66. // top: getNumberTop(min, max, region.max),
  67. // // top: 100,
  68. // style: {
  69. // text: region.max.toString(),
  70. // fill: region.NumberColor,
  71. // fontSize: 12,
  72. // fontWeight: "bold",
  73. // },
  74. // z: 3,
  75. // });
  76. // }
  77. return graphics;
  78. });
  79. };
  80. function initQXNLZHEcharts(kline, qxnlzhqData) {
  81. // 测试数据 !!! 删掉
  82. // qxnlzhqData.topxh = ["2025/04/04", "2025/04/15"]
  83. // qxnlzhqData.lowxh = ["2025/04/08", "2025/04/18"]
  84. // qxnlzhqData.qixh = ["2025/04/10", "2025/04/21"]
  85. if (qxnlzhqEchartsInstance) {
  86. qxnlzhqEchartsInstance.dispose();
  87. }
  88. // 数据
  89. let mixData = [];
  90. kline.forEach((element) => {
  91. let date = element[0];
  92. let value = [element[1], element[2], element[3], element[4]];
  93. mixData.push({
  94. date,
  95. value,
  96. });
  97. });
  98. // 动态区域配置
  99. // dd到zc 低吸区------情绪冰点区 ; zc到ht 关注区------认知潜伏区; ht到qs 回调区------多空消化区 ; qs到tp 拉升区------共识加速区;
  100. // tp到js 突破区------情绪临界区 ; js到yl 警示区-------杠杆失衡区 ; yl到gg 风险区-------情绪熔断区;
  101. regions = [
  102. {
  103. min: qxnlzhqData.dd,
  104. max: qxnlzhqData.zc,
  105. name: "【情绪冰点区】",
  106. color: "#F5D6FF",
  107. fontColor: '#A7691C',
  108. NumberColor: 'white',
  109. },
  110. {
  111. min: qxnlzhqData.zc,
  112. max: qxnlzhqData.ht,
  113. name: "【认知潜伏区】",
  114. color: "#FFF6C4",
  115. fontColor: '#A7691C',
  116. NumberColor: 'white',
  117. },
  118. {
  119. min: qxnlzhqData.ht,
  120. max: qxnlzhqData.qs,
  121. name: "【多空消化区】",
  122. color: {
  123. type: 'linear',
  124. x: 0,
  125. y: 0,
  126. x2: 1,
  127. y2: 0,
  128. colorStops: [
  129. { offset: 0, color: '#D7FF9B' },
  130. { offset: 1, color: '#CEFF85' }
  131. ]
  132. },
  133. fontColor: '#A7691C',
  134. NumberColor: 'white',
  135. },
  136. {
  137. min: qxnlzhqData.qs,
  138. max: qxnlzhqData.tp,
  139. name: "【共识加速区】",
  140. color: "#FFDC8F",
  141. fontColor: '#A7691C',
  142. NumberColor: 'white',
  143. },
  144. {
  145. min: qxnlzhqData.tp,
  146. max: qxnlzhqData.js,
  147. name: "【情绪临界区】",
  148. color: "#FFC0AA",
  149. fontColor: '#2D2D89',
  150. NumberColor: 'white',
  151. },
  152. ];
  153. // gg yl为-1 不绘制部分图表
  154. if (Number(qxnlzhqData.yl) != -1) {
  155. regions.push(
  156. {
  157. min: qxnlzhqData.js,
  158. max: qxnlzhqData.yl,
  159. name: "【杠杆失衡区】",
  160. color: "#51C3F9",
  161. fontColor: '#2D2D89',
  162. NumberColor: 'white',
  163. },
  164. )
  165. }
  166. if (Number(qxnlzhqData.gg) != -1) {
  167. regions.push(
  168. {
  169. min: qxnlzhqData.yl,
  170. max: qxnlzhqData.gg,
  171. name: "【情绪熔断区】",
  172. color: "#D0A7FF",
  173. fontColor: '#2D2D89',
  174. NumberColor: 'white',
  175. },
  176. )
  177. }
  178. // 计算动态的y轴范围
  179. const priceValues = kline.flatMap(item => [item[1], item[2], item[3], item[4]]);
  180. const dataMin = Math.min(...priceValues);
  181. const dataMax = Math.max(...priceValues);
  182. // 找到最高价的最大值及其对应的索引
  183. let maxHighPrice = -Infinity;
  184. let maxHighPriceIndex = -1;
  185. kline.forEach((item, index) => {
  186. const highPrice = item[4]; // 最高价在数组的第5个位置(索引4)
  187. if (highPrice > maxHighPrice) {
  188. maxHighPrice = highPrice;
  189. maxHighPriceIndex = index;
  190. }
  191. });
  192. // 计算止盈止损价格
  193. const stopProfitPrice = Number(qxnlzhqData.cc) * 1.05; // 止盈价
  194. const stopLossPrice = Number(qxnlzhqData.cc) * 0.97; // 止损价
  195. // 计算最后一根K线的收盘价
  196. const lastClosePrice = mixData[mixData.length - 1].value[2];
  197. // 动态调整标记位置以避免与止盈止损线重叠
  198. const priceBuffer = (dataMax - dataMin) * 0.02; // 2%的缓冲区间
  199. // 最高价标记位置调整
  200. let maxPricePosition = 'top';
  201. let maxPriceOffset = [0, -5];
  202. if (Math.abs(maxHighPrice - stopProfitPrice) < priceBuffer) {
  203. maxPriceOffset = [0, -25]; // 增加偏移避免重叠
  204. }
  205. // 收盘价标记位置调整
  206. let closePricePosition = 'bottom';
  207. let closePriceOffset = [0, 15];
  208. if (Math.abs(lastClosePrice - stopLossPrice) < priceBuffer) {
  209. closePriceOffset = [0, 25]; // 增加偏移避免重叠
  210. }
  211. if (Math.abs(lastClosePrice - stopProfitPrice) < priceBuffer) {
  212. closePriceOffset = [0, 25]; // 增加偏移避免重叠
  213. }
  214. // 确定起始和结束位置
  215. const startIndex = Math.max(0, mixData.length - 17);
  216. // 创建完整数据数组
  217. const takeProfitData = new Array(mixData.length).fill(null);
  218. const stopLossData = new Array(mixData.length).fill(null);
  219. // 填充显示区域的数据
  220. for (var i = startIndex; i < mixData.length; i++) {
  221. takeProfitData[i] = stopProfitPrice;
  222. stopLossData[i] = stopLossPrice;
  223. }
  224. // topxh、lowxh、qixh 对应k线染色
  225. // 创建中间区域数据
  226. const middleRangeData = [];
  227. const middleRangeData1 = [];
  228. const markPointData = [];
  229. mixData.forEach((item, index) => {
  230. const [open, close, low, high] = item.value;
  231. const rangeHeight = high - low;
  232. // const middleThirdStart = low + rangeHeight * (1/3);
  233. // const middleThirdEnd = low + rangeHeight * (2/3);
  234. let color = null;
  235. if (qxnlzhqData.topxh.includes(item.date)) {
  236. color = '#000000'; // 黑色
  237. } else if (qxnlzhqData.lowxh.includes(item.date)) {
  238. color = '#1E90FF'; // 蓝色
  239. }
  240. // 添加中间区域数据
  241. if (color) {
  242. middleRangeData.push({
  243. value: [index, close > open ? (close - open) : (open - close)], // 修正数据格式
  244. itemStyle: {
  245. normal: {
  246. color: color
  247. }
  248. },
  249. });
  250. middleRangeData1.push({
  251. value: [index, close > open ? open : close], // 修正数据格式
  252. itemStyle: {
  253. normal: {
  254. color: 'transparent'
  255. }
  256. },
  257. });
  258. } else {
  259. middleRangeData.push(null);
  260. middleRangeData1.push(null);
  261. }
  262. // 添加文字标记数据
  263. if (qxnlzhqData.qixh.includes(item.date)) {
  264. markPointData.push({
  265. name: '起',
  266. coord: [index, (open + close) / 2],
  267. itemStyle: {
  268. normal: {
  269. color: 'rgba(0,0,0,0)' // 标记点透明
  270. }
  271. },
  272. label: {
  273. normal: {
  274. show: true,
  275. position: 'inside',
  276. formatter: '起',
  277. textStyle: {
  278. color: '#FF0000',
  279. fontSize: 10,
  280. // fontWeight: 'bold'
  281. }
  282. }
  283. }
  284. });
  285. }
  286. });
  287. // 检查DOM元素是否存在
  288. if (!qxnlzhqEchartsRef.value) {
  289. console.error('emoEnergyConverter: DOM元素未找到,无法初始化图表');
  290. return;
  291. }
  292. // 初始化图表
  293. qxnlzhqEchartsInstance = echarts.init(qxnlzhqEchartsRef.value);
  294. let option;
  295. // 设置图表配置
  296. option = {
  297. tooltip: {
  298. show: true,
  299. trigger: 'axis',
  300. confine: true, // 限制tooltip在图表区域内
  301. position: function (point, params, dom, rect, size) {
  302. // 获取图表容器大小
  303. const chartWidth = size.viewSize[0];
  304. const chartHeight = size.viewSize[1];
  305. const tooltipWidth = size.contentSize[0];
  306. const tooltipHeight = size.contentSize[1];
  307. // 检测是否为移动设备
  308. const isMobile = window.innerWidth <= 768;
  309. if (isMobile) {
  310. // 移动端:固定在顶部中央
  311. return {
  312. top: 10,
  313. left: Math.max(10, (chartWidth - tooltipWidth) / 2)
  314. };
  315. } else {
  316. // 桌面端:智能定位
  317. let x = point[0];
  318. let y = point[1];
  319. // 防止tooltip超出右边界
  320. if (x + tooltipWidth > chartWidth) {
  321. x = chartWidth - tooltipWidth - 10;
  322. }
  323. // 防止tooltip超出下边界
  324. if (y + tooltipHeight > chartHeight) {
  325. y = chartHeight - tooltipHeight - 10;
  326. }
  327. return [Math.max(10, x), Math.max(10, y)];
  328. }
  329. },
  330. axisPointer: {
  331. type: 'cross',
  332. lineStyle: {
  333. color: 'grey',
  334. width: 1,
  335. type: 'dashed'
  336. },
  337. label: {
  338. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  339. color: '#fff',
  340. borderColor: '#fff',
  341. borderWidth: 1
  342. }
  343. },
  344. backgroundColor: '#E8E8F2',
  345. borderColor: '#fff',
  346. borderWidth: 1,
  347. padding: [8, 12],
  348. textStyle: {
  349. color: '#000',
  350. fontSize: window.innerWidth <= 768 ? 10 : 12 // 移动端使用更小字体
  351. },
  352. extraCssText: window.innerWidth <= 768 ?
  353. 'max-width: 280px; word-wrap: break-word; white-space: normal; box-shadow: 0 2px 8px rgba(0,0,0,0.3);' :
  354. 'max-width: 350px; word-wrap: break-word; white-space: normal; box-shadow: 0 2px 8px rgba(0,0,0,0.3);',
  355. formatter: function (params) {
  356. if (!params || params.length === 0) return ''
  357. const isMobile = window.innerWidth <= 768;
  358. const fontSize = isMobile ? '10px' : '12px';
  359. const lineHeight = isMobile ? '1.3' : '1.5';
  360. const marginBottom = isMobile ? '4px' : '6px';
  361. let result = `<div style="font-weight: bold; color: #000; margin-bottom: ${marginBottom}; font-size: ${fontSize}; line-height: ${lineHeight};">${params[0].name}</div>`
  362. params.forEach(param => {
  363. let value = param.value
  364. let color = param.color
  365. if (param.seriesType === 'candlestick') {
  366. let openPrice = value[1] // 开盘价
  367. let closePrice = value[2] // 收盘价
  368. let lowPrice = value[3] // 最低价
  369. let highPrice = value[4] // 最高价
  370. // 检查数据有效性
  371. if (typeof openPrice !== 'number' || typeof closePrice !== 'number' ||
  372. typeof lowPrice !== 'number' || typeof highPrice !== 'number') {
  373. return ''
  374. }
  375. // 获取前一日收盘价用于计算涨跌幅
  376. let previousClosePrice = null;
  377. const currentIndex = param.dataIndex;
  378. if (currentIndex > 0 && mixData[currentIndex - 1]) {
  379. previousClosePrice = mixData[currentIndex - 1].value[2]; // 前一日收盘价
  380. }
  381. let priceChange, changePercent;
  382. if (previousClosePrice !== null && typeof previousClosePrice === 'number') {
  383. // 使用前一日收盘价计算涨跌幅
  384. priceChange = closePrice - previousClosePrice;
  385. changePercent = ((priceChange / previousClosePrice) * 100).toFixed(2);
  386. } else {
  387. // 如果没有前一日数据,使用开盘价计算(兜底方案)
  388. priceChange = closePrice - openPrice;
  389. changePercent = ((priceChange / openPrice) * 100).toFixed(2);
  390. }
  391. let changeColor = priceChange >= 0 ? '#32B520' : '#D8001B'
  392. if (isMobile) {
  393. // 移动端简化显示
  394. result += `<div style="margin-bottom: ${marginBottom}; font-size: ${fontSize}; line-height: ${lineHeight};">`
  395. result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>开盘价:</span><span>${openPrice.toFixed(2)}</span></div>`
  396. result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>收盘价:</span><span>${closePrice.toFixed(2)}</span></div>`
  397. result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>最低价:</span><span>${lowPrice.toFixed(2)}</span></div>`
  398. result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>最高价:</span><span>${highPrice.toFixed(2)}</span></div>`
  399. result += `<div style="color: ${changeColor}; display: flex; justify-content: space-between;"><span>涨跌:</span><span>${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</span></div>`
  400. result += `</div>`
  401. } else {
  402. // 桌面端完整显示
  403. result += `<div style="margin-bottom: ${marginBottom}; font-size: ${fontSize}; line-height: ${lineHeight};">`
  404. result += `<div style="color: #000;">开盘价: ${openPrice.toFixed(2)}</div>`
  405. result += `<div style="color: #000;">收盘价: ${closePrice.toFixed(2)}</div>`
  406. result += `<div style="color: #000;">最低价: ${lowPrice.toFixed(2)}</div>`
  407. result += `<div style="color: #000;">最高价: ${highPrice.toFixed(2)}</div>`
  408. result += `<div style="color: ${changeColor};">涨跌: ${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</div>`
  409. result += `</div>`
  410. }
  411. } else if (param.seriesName === '止盈线' && value !== null && value !== undefined && typeof value === 'number') {
  412. result += `<div style="color: #FC0000; margin-bottom: 2px; font-size: ${fontSize}; line-height: ${lineHeight};">${isMobile ? '止盈' : param.seriesName}: ${value.toFixed(2)}</div>`
  413. } else if (param.seriesName === '止损线' && value !== null && value !== undefined && typeof value === 'number') {
  414. result += `<div style="color: #002DFF; margin-bottom: 2px; font-size: ${fontSize}; line-height: ${lineHeight};">${isMobile ? '止损' : param.seriesName}: ${value.toFixed(2)}</div>`
  415. }
  416. })
  417. return result
  418. }
  419. },
  420. dataZoom: [
  421. {
  422. type: 'slider',
  423. xAxisIndex: 0,
  424. start: 50,
  425. end: 100,
  426. show: true,
  427. bottom: 10,
  428. height: 20,
  429. borderColor: '#fff',
  430. fillerColor: 'rgba(255, 255, 255, 0.2)',
  431. handleStyle: {
  432. color: '#fff',
  433. borderColor: '#fff'
  434. },
  435. textStyle: {
  436. color: '#fff'
  437. }
  438. },
  439. {
  440. type: 'inside',
  441. xAxisIndex: 0,
  442. start: 50,
  443. end: 100,
  444. zoomOnMouseWheel: true,
  445. moveOnMouseMove: true,
  446. moveOnMouseWheel: false
  447. }
  448. ],
  449. xAxis: {
  450. type: "category",
  451. data: [...mixData.map((item) => item.date), '', '', ''], // 在末尾添加三个空占位符,留出三根K线的距离
  452. axisLabel: {
  453. rotate: 0, // 取消倾斜角度
  454. color: "white",
  455. interval: 'auto' // 自动计算显示间隔,只显示部分日期但覆盖所有范围
  456. },
  457. axisLine: {
  458. // show: false,
  459. lineStyle: {
  460. color: 'white', // x轴线颜色
  461. }
  462. },
  463. axisTick: {
  464. show: true,
  465. alignWithLabel: true, // 刻度线与标签对齐
  466. lineStyle: {
  467. color: "white", // 与十字线颜色保持一致
  468. width: 1,
  469. type: "dashed" // 与十字线样式保持一致
  470. }
  471. },
  472. },
  473. yAxis: {
  474. scale: true,
  475. axisLine: {
  476. // show: false,
  477. lineStyle: {
  478. color: 'white', // y轴线颜色
  479. width: 3
  480. }
  481. },
  482. splitLine: {
  483. show: false,
  484. },
  485. axisLabel: { // 刻度标签
  486. show: true,
  487. color: 'white',
  488. },
  489. axisTick: { // 刻度线
  490. show: true,
  491. color: 'white',
  492. },
  493. min:
  494. qxnlzhqData.dd < stopLossPrice * 0.98
  495. ? Math.floor(qxnlzhqData.dd)
  496. : Math.floor(stopLossPrice * 0.98),
  497. max: Math.round(Math.max(Math.ceil(dataMax * 1.02), (qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02)), stopProfitPrice * 1.02)),
  498. },
  499. // 自定义区域名称和区域范围值 位置
  500. graphic: generateGraphics(qxnlzhqData.dd < stopLossPrice * 0.98
  501. ? Math.floor(qxnlzhqData.dd)
  502. : Math.floor(stopLossPrice * 0.98), Math.max(Math.ceil(dataMax * 1.02), (qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02)), stopProfitPrice * 1.02),),
  503. series: [
  504. {
  505. type: "candlestick",
  506. data: mixData.map((item) => item.value),
  507. z: 1,
  508. clip: true,
  509. markPoint: {
  510. symbol: 'circle',
  511. symbolSize: 10,
  512. data: [
  513. ...markPointData,
  514. // 添加最高价标记
  515. {
  516. name: '最高价',
  517. coord: [maxHighPriceIndex, maxHighPrice],
  518. itemStyle: {
  519. normal: {
  520. color: 'transparent', // 透明标记点
  521. borderColor: 'transparent',
  522. borderWidth: 0
  523. }
  524. },
  525. label: {
  526. normal: {
  527. show: true,
  528. position: maxPricePosition,
  529. formatter: `${maxHighPrice.toFixed(2)}`,
  530. textStyle: {
  531. color: 'rgb(0,170,255)',
  532. fontSize: 12,
  533. fontWeight: 'bold',
  534. // textBorderColor: '#000000',
  535. textBorderWidth: 1
  536. },
  537. offset: maxPriceOffset
  538. }
  539. }
  540. },
  541. // 添加最后一根K线收盘价标记
  542. {
  543. name: '收盘价',
  544. coord: [mixData.length - 1, mixData[mixData.length - 1].value[2]],
  545. itemStyle: {
  546. normal: {
  547. color: 'transparent', // 透明标记点
  548. borderColor: 'transparent',
  549. borderWidth: 0
  550. }
  551. },
  552. label: {
  553. normal: {
  554. show: true,
  555. position: closePricePosition,
  556. formatter: `${lastClosePrice.toFixed(2)}`,
  557. textStyle: {
  558. color: 'rgb(59,143,8)',
  559. fontSize: 12,
  560. fontWeight: 'bold',
  561. // textBorderColor: '#000000',
  562. textBorderWidth: 1
  563. },
  564. offset: closePriceOffset
  565. }
  566. }
  567. }
  568. ],
  569. z: 5 // 确保标记显示在最上层
  570. },
  571. itemStyle: {
  572. normal: {
  573. // 阳线样式(收盘 > 开盘)
  574. // color: '#14b143', // 开盘价 < 收盘价时为绿色
  575. color: '#00AAFF',
  576. color0: '#FF007F', // 开盘价 > 收盘价时为红色
  577. borderColor: '#00AAFF', // 阳线边框色(绿)
  578. borderColor0: '#FF007F', // 阴线边框色(红)
  579. borderWidth: 1.5
  580. }
  581. },
  582. // 实现 分区域背景色
  583. markArea: {
  584. silent: true,
  585. data: regions.map((region) => [
  586. {
  587. yAxis: region.min,
  588. itemStyle: { normal: { color: region.color } },
  589. },
  590. { yAxis: region.max },
  591. ]),
  592. },
  593. },
  594. {
  595. name: '中间区域',
  596. type: 'bar',
  597. stack: 'total',
  598. data: middleRangeData1,
  599. barWidth: '20%',
  600. barCategoryGap: '40%',
  601. itemStyle: {
  602. normal: {
  603. color: 'rgba(0,0,0,0)' // 默认透明
  604. }
  605. },
  606. z: 2
  607. },
  608. // 中间区域染色
  609. {
  610. name: '中间区域',
  611. type: 'bar',
  612. stack: 'total',
  613. data: middleRangeData,
  614. barWidth: '20%',
  615. barCategoryGap: '40%',
  616. itemStyle: {
  617. normal: {
  618. color: 'rgba(0,0,0,0)' // 默认透明
  619. }
  620. },
  621. z: 2
  622. },
  623. {
  624. name: '止盈线描边',
  625. type: 'line',
  626. data: takeProfitData,
  627. symbol: 'none',
  628. lineStyle: {
  629. normal: {
  630. color: '#ffffff', // 白色描边
  631. width: 6,
  632. type: 'solid'
  633. }
  634. },
  635. z: 10,
  636. silent: true,
  637. showInLegend: false
  638. },
  639. {
  640. name: '止盈线',
  641. type: 'line',
  642. data: takeProfitData,
  643. symbol: 'none',
  644. lineStyle: {
  645. normal: {
  646. color: '#FF0000', // 红色
  647. width: 2,
  648. type: 'solid'
  649. }
  650. },
  651. z: 10,
  652. markPoint: {
  653. symbol: 'circle',
  654. symbolSize: 1,
  655. data: [
  656. {
  657. coord: [startIndex, stopProfitPrice],
  658. itemStyle: {
  659. color: 'transparent'
  660. },
  661. label: {
  662. normal: {
  663. show: true,
  664. position: 'left',
  665. formatter: `止盈${stopProfitPrice.toFixed(2)}`,
  666. textStyle: {
  667. color: '#FF0000',
  668. fontSize: 12,
  669. fontWeight: 'bold',
  670. textBorderColor: '#ffffff',
  671. textBorderWidth: 2
  672. },
  673. offset: [-10, 0]
  674. }
  675. }
  676. }
  677. ]
  678. }
  679. },
  680. {
  681. name: '止损线描边',
  682. type: 'line',
  683. data: stopLossData,
  684. symbol: 'none',
  685. lineStyle: {
  686. normal: {
  687. color: '#ffffff', // 白色描边
  688. width: 6,
  689. type: 'solid'
  690. }
  691. },
  692. z: 10,
  693. silent: true,
  694. showInLegend: false
  695. },
  696. {
  697. name: '止损线',
  698. type: 'line',
  699. data: stopLossData,
  700. symbol: 'none',
  701. lineStyle: {
  702. normal: {
  703. color: '#001EFF',
  704. width: 2,
  705. type: 'solid'
  706. }
  707. },
  708. z: 10,
  709. markPoint: {
  710. symbol: 'circle',
  711. symbolSize: 1,
  712. data: [
  713. {
  714. coord: [startIndex, stopLossPrice],
  715. itemStyle: {
  716. color: 'transparent'
  717. },
  718. label: {
  719. normal: {
  720. show: true,
  721. position: 'left',
  722. formatter: `止损${stopLossPrice.toFixed(2)}`,
  723. textStyle: {
  724. color: '#001EFF',
  725. fontSize: 12,
  726. fontWeight: 'bold',
  727. textBorderColor: '#ffffff',
  728. textBorderWidth: 2
  729. },
  730. offset: [-10, 0]
  731. }
  732. }
  733. }
  734. ]
  735. }
  736. },
  737. // {
  738. // name: '最低价',
  739. // type: 'line',
  740. // symbol: 'none',
  741. // lineStyle: {
  742. // normal: {
  743. // color: 'transparent',
  744. // width: 0
  745. // }
  746. // },
  747. // markPoint: {
  748. // symbol: 'circle',
  749. // symbolSize: 1,
  750. // data: [
  751. // {
  752. // coord: [mixData.length - 1, mixData[mixData.length - 1].value[2]],
  753. // itemStyle: {
  754. // color: 'transparent'
  755. // },
  756. // label: {
  757. // normal: {
  758. // show: true,
  759. // position: 'bottom',
  760. // formatter: `{text| ${mixData[mixData.length - 1].value[2].toFixed(2)}}`,
  761. // rich: {
  762. // text: {
  763. // color: '#001EFF',
  764. // fontSize: 12,
  765. // fontWeight: 'bold',
  766. // textBorderColor: '#ffffff',
  767. // textBorderWidth: 2,
  768. // }
  769. // },
  770. // offset: [-25, -40]
  771. // }
  772. // }
  773. // }
  774. // ]
  775. // }
  776. // },
  777. // {
  778. // name: '最高价',
  779. // type: 'line',
  780. // symbol: 'none',
  781. // lineStyle: {
  782. // normal: {
  783. // color: 'transparent',
  784. // width: 0
  785. // }
  786. // },
  787. // markPoint: {
  788. // symbol: 'circle',
  789. // symbolSize: 1,
  790. // data: [
  791. // {
  792. // coord: [mixData.length - 1, mixData[mixData.length - 1].value[3]],
  793. // itemStyle: {
  794. // color: 'transparent'
  795. // },
  796. // label: {
  797. // normal: {
  798. // show: true,
  799. // position: 'top',
  800. // formatter: `{text| ${mixData[mixData.length - 1].value[3].toFixed(2)}}`,
  801. // rich: {
  802. // text: {
  803. // color: '#FF0000',
  804. // fontSize: 12,
  805. // fontWeight: 'bold',
  806. // textBorderColor: '#ffffff',
  807. // textBorderWidth: 2,
  808. // }
  809. // },
  810. // offset: [-25, 40]
  811. // }
  812. // }
  813. // }
  814. // ]
  815. // }
  816. // }
  817. ],
  818. grid: {
  819. left: window.innerWidth >= 768 ? "13%" : "18%",
  820. right: "10",
  821. top: '10',
  822. bottom: "60",
  823. containLabel: false,
  824. width: window.innerWidth >= 768 ? '80%': '70%',
  825. height: 'auto',
  826. overflow: 'hidden'
  827. },
  828. };
  829. // 应用配置
  830. qxnlzhqEchartsInstance.setOption(option);
  831. // 防抖函数,避免频繁触发resize
  832. const debounce = (func, wait) => {
  833. let timeout;
  834. return function executedFunction(...args) {
  835. const later = () => {
  836. clearTimeout(timeout);
  837. func(...args);
  838. };
  839. clearTimeout(timeout);
  840. timeout = setTimeout(later, wait);
  841. };
  842. };
  843. // 监听窗口大小变化,调整图表尺寸
  844. const resizeHandler = debounce(() => {
  845. if (qxnlzhqEchartsInstance && !qxnlzhqEchartsInstance.isDisposed()) {
  846. try {
  847. qxnlzhqEchartsInstance.resize();
  848. console.log('情绪能量转化器图表已重新调整大小');
  849. } catch (error) {
  850. console.error('情绪能量转化器图表resize失败:', error);
  851. }
  852. }
  853. }, 100); // 100ms防抖延迟
  854. // 移除之前的监听器(如果存在)
  855. if (window.emoEnergyConverterResizeHandler) {
  856. window.removeEventListener('resize', window.emoEnergyConverterResizeHandler);
  857. }
  858. // 添加新的监听器
  859. window.addEventListener('resize', resizeHandler);
  860. // 存储resize处理器以便后续清理
  861. window.emoEnergyConverterResizeHandler = resizeHandler;
  862. // 添加容器大小监听器
  863. if (qxnlzhqEchartsRef.value && window.ResizeObserver) {
  864. const containerObserver = new ResizeObserver(debounce(() => {
  865. if (qxnlzhqEchartsInstance && !qxnlzhqEchartsInstance.isDisposed()) {
  866. try {
  867. qxnlzhqEchartsInstance.resize();
  868. console.log('情绪能量转化器容器大小变化,图表已调整');
  869. } catch (error) {
  870. console.error('情绪能量转化器容器resize失败:', error);
  871. }
  872. }
  873. }, 100));
  874. containerObserver.observe(qxnlzhqEchartsRef.value);
  875. window.emoEnergyConverterContainerObserver = containerObserver;
  876. }
  877. }
  878. onBeforeUnmount(() => {
  879. // 组件卸载时销毁图表
  880. if (qxnlzhqEchartsInstance) {
  881. qxnlzhqEchartsInstance.dispose();
  882. qxnlzhqEchartsInstance = null;
  883. }
  884. // 移除窗口resize监听器
  885. if (window.emoEnergyConverterResizeHandler) {
  886. window.removeEventListener('resize', window.emoEnergyConverterResizeHandler);
  887. window.emoEnergyConverterResizeHandler = null;
  888. }
  889. // 清理容器观察器
  890. if (window.emoEnergyConverterContainerObserver) {
  891. window.emoEnergyConverterContainerObserver.disconnect();
  892. window.emoEnergyConverterContainerObserver = null;
  893. }
  894. });
  895. </script>
  896. <style scoped>
  897. .qxnlzhqEcharts {
  898. width: 100%;
  899. height: 542px;
  900. margin: 0;
  901. box-sizing: border-box;
  902. overflow: hidden;
  903. }
  904. /* 手机端适配样式 */
  905. @media only screen and (max-width: 768px) {
  906. .qxnlzhqEcharts {
  907. width: 100%;
  908. height: 300px;
  909. /* margin: 0; */
  910. }
  911. /* 移动端tooltip优化 */
  912. :deep(.echarts-tooltip) {
  913. max-width: 280px !important;
  914. font-size: 10px !important;
  915. line-height: 1.3 !important;
  916. padding: 8px 10px !important;
  917. word-wrap: break-word !important;
  918. white-space: normal !important;
  919. box-sizing: border-box !important;
  920. }
  921. /* 确保tooltip不会超出屏幕 */
  922. :deep(.echarts-tooltip-content) {
  923. max-width: 100% !important;
  924. overflow: hidden !important;
  925. }
  926. }
  927. </style>