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.

444 lines
12 KiB

  1. <template>
  2. <div ref="qxnlzhqEchartsRef" id="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. // 设置区域名称 位置
  12. function getNameTop(min, max, regionMiidle) {
  13. // 获取整个图表的高度
  14. const chartHeight = qxnlzhqEchartsInstance.getHeight();
  15. // 60: 为x轴占的高度
  16. return (max - Number(regionMiidle)) / (max - min) * (chartHeight - 60)
  17. }
  18. // 设置区域最大值 位置
  19. function getNumberTop(min, max, regionMax) {
  20. // 获取整个图表的高度
  21. const chartHeight = qxnlzhqEchartsInstance.getHeight();
  22. // 60: 为x轴占的高度
  23. return (max - Number(regionMax)) / (max - min) * (chartHeight - 60)
  24. }
  25. // 生成图形标注(核心逻辑)
  26. const generateGraphics = (min, max) => {
  27. return regions.flatMap((region) => {
  28. const middleY = (Number(region.min) + Number(region.max)) / 2;
  29. return [
  30. // 区域名称(中间位置)
  31. {
  32. type: "text",
  33. left: '17', // 靠近y轴内侧
  34. top: getNameTop(min, max, middleY),
  35. style: {
  36. text: region.name,
  37. fill: region.fontColor,
  38. fontSize: 14,
  39. fontWeight: "bold",
  40. },
  41. z: 3,
  42. },
  43. // y轴数值(顶部位置)
  44. {
  45. type: "text",
  46. left: '25',
  47. top: getNumberTop(min, max, region.max),
  48. // top: 100,
  49. style: {
  50. text: region.max.toString(),
  51. fill: region.NumberColor,
  52. fontSize: 12,
  53. },
  54. z: 3,
  55. },
  56. ];
  57. });
  58. };
  59. function initQXNLZHEcharts(kline, qxnlzhqData) {
  60. // 测试数据 !!! 删掉
  61. // qxnlzhqData.topxh = ["2025/04/04", "2025/04/15"]
  62. // qxnlzhqData.lowxh = ["2025/04/08", "2025/04/18"]
  63. // qxnlzhqData.qixh = ["2025/04/10", "2025/04/21"]
  64. if (qxnlzhqEchartsInstance) {
  65. qxnlzhqEchartsInstance.dispose();
  66. }
  67. // 数据
  68. let mixData = [];
  69. kline.forEach((element) => {
  70. let date = element[0];
  71. let value = [element[1], element[2], element[3], element[4]];
  72. mixData.push({
  73. date,
  74. value,
  75. });
  76. });
  77. // 动态区域配置
  78. // dd到zc 低吸区------情绪冰点区 ; zc到ht 关注区------认知潜伏区; ht到qs 回调区------多空消化区 ; qs到tp 拉升区------共识加速区;
  79. // tp到js 突破区------情绪临界区 ; js到yl 警示区-------杠杆失衡区 ; yl到gg 风险区-------情绪熔断区;
  80. regions = [
  81. {
  82. min: qxnlzhqData.dd,
  83. max: qxnlzhqData.zc,
  84. name: "【情绪冰点区】",
  85. color: "#e7a5d6",
  86. fontColor: '#000',
  87. NumberColor: 'blue',
  88. },
  89. {
  90. min: qxnlzhqData.zc,
  91. max: qxnlzhqData.ht,
  92. name: "【认知潜伏区】",
  93. color: "#f36587",
  94. fontColor: '#000',
  95. NumberColor: 'blue',
  96. },
  97. {
  98. min: qxnlzhqData.ht,
  99. max: qxnlzhqData.qs,
  100. name: "【多空消化区】",
  101. color: "#e99883",
  102. fontColor: '#000',
  103. NumberColor: 'blue',
  104. },
  105. {
  106. min: qxnlzhqData.qs,
  107. max: qxnlzhqData.tp,
  108. name: "【共识加速区】",
  109. color: "#f0db84",
  110. fontColor: '#000',
  111. NumberColor: 'red',
  112. },
  113. {
  114. min: qxnlzhqData.tp,
  115. max: qxnlzhqData.js,
  116. name: "【情绪临界区】",
  117. color: "#dbeee3",
  118. fontColor: 'red',
  119. NumberColor: 'red',
  120. },
  121. ];
  122. // gg yl为-1 不绘制部分图表
  123. if (Number(qxnlzhqData.yl) != -1) {
  124. regions.push(
  125. {
  126. min: qxnlzhqData.js,
  127. max: qxnlzhqData.yl,
  128. name: "【杠杆失衡区】",
  129. color: "#9ac2d8",
  130. fontColor: 'red',
  131. NumberColor: 'red',
  132. },
  133. )
  134. }
  135. if (Number(qxnlzhqData.gg) != -1) {
  136. regions.push(
  137. {
  138. min: qxnlzhqData.yl,
  139. max: qxnlzhqData.gg,
  140. name: "【情绪熔断区】",
  141. color: "#bce283",
  142. fontColor: 'red',
  143. NumberColor: 'red',
  144. },
  145. )
  146. }
  147. // 计算止盈止损价格
  148. const stopProfitPrice = Number(qxnlzhqData.cc) * 1.05; // 止盈价
  149. const stopLossPrice = Number(qxnlzhqData.cc) * 0.97; // 止损价
  150. // 确定起始和结束位置
  151. const startIndex = Math.max(0, mixData.length - 17);
  152. // 创建完整数据数组
  153. const takeProfitData = new Array(mixData.length).fill(null);
  154. const stopLossData = new Array(mixData.length).fill(null);
  155. // 填充显示区域的数据
  156. for (var i = startIndex; i < mixData.length; i++) {
  157. takeProfitData[i] = stopProfitPrice;
  158. stopLossData[i] = stopLossPrice;
  159. }
  160. // topxh、lowxh、qixh 对应k线染色
  161. // 创建中间区域数据
  162. const middleRangeData = [];
  163. const middleRangeData1 = [];
  164. const markPointData = [];
  165. mixData.forEach((item, index) => {
  166. const [open, close, low, high] = item.value;
  167. const rangeHeight = high - low;
  168. // const middleThirdStart = low + rangeHeight * (1/3);
  169. // const middleThirdEnd = low + rangeHeight * (2/3);
  170. let color = null;
  171. if (qxnlzhqData.topxh.includes(item.date)) {
  172. color = '#000000'; // 黑色
  173. } else if (qxnlzhqData.lowxh.includes(item.date)) {
  174. color = '#1E90FF'; // 蓝色
  175. }
  176. // 添加中间区域数据
  177. if (color) {
  178. middleRangeData.push({
  179. value: [index, close > open ? (close - open) : (open - close)], // 修正数据格式
  180. itemStyle: {
  181. normal: {
  182. color: color
  183. }
  184. },
  185. });
  186. middleRangeData1.push({
  187. value: [index, close > open ? open : close], // 修正数据格式
  188. itemStyle: {
  189. normal: {
  190. color: 'transparent'
  191. }
  192. },
  193. });
  194. } else {
  195. middleRangeData.push(null);
  196. middleRangeData1.push(null);
  197. }
  198. // 添加文字标记数据
  199. if (qxnlzhqData.qixh.includes(item.date)) {
  200. markPointData.push({
  201. name: '起',
  202. coord: [index, (open + close) / 2],
  203. itemStyle: {
  204. normal: {
  205. color: 'rgba(0,0,0,0)' // 标记点透明
  206. }
  207. },
  208. label: {
  209. normal:{
  210. show: true,
  211. position: 'inside',
  212. formatter: '起',
  213. textStyle: {
  214. color: '#FF0000',
  215. fontSize: 10,
  216. // fontWeight: 'bold'
  217. }
  218. }
  219. }
  220. });
  221. }
  222. });
  223. // 初始化图表
  224. qxnlzhqEchartsInstance = echarts.init(qxnlzhqEchartsRef.value);
  225. let option;
  226. // 设置图表配置
  227. option = {
  228. xAxis: {
  229. type: "category",
  230. data: mixData.map((item) => item.date),
  231. axisLabel: {
  232. rotate: 45, // 日期旋转45度防止重叠
  233. },
  234. },
  235. yAxis: {
  236. scale: true,
  237. splitLine: {
  238. show: false,
  239. },
  240. axisLabel: { // 刻度标签
  241. show: false // 不显示刻度标签
  242. },
  243. axisTick: { // 刻度线
  244. show: false // 不显示刻度线
  245. },
  246. min:
  247. qxnlzhqData.dd < stopLossPrice * 0.98
  248. ? Math.floor(qxnlzhqData.dd)
  249. : Math.floor(stopLossPrice * 0.98),
  250. max:
  251. qxnlzhqData.yl > stopProfitPrice * 1.02
  252. ? Math.ceil(qxnlzhqData.yl)
  253. : Math.ceil(stopProfitPrice * 1.02),
  254. },
  255. // 自定义区域名称和区域范围值 位置
  256. graphic: generateGraphics(qxnlzhqData.dd < stopLossPrice * 0.98
  257. ? Math.floor(qxnlzhqData.dd)
  258. : Math.floor(stopLossPrice * 0.98),qxnlzhqData.yl > stopProfitPrice * 1.02
  259. ? Math.ceil(qxnlzhqData.yl)
  260. : Math.ceil(stopProfitPrice * 1.02)),
  261. series: [
  262. {
  263. type: "candlestick",
  264. data: mixData.map((item) => item.value),
  265. z: 1,
  266. markPoint: {
  267. symbol: 'circle',
  268. symbolSize: 10,
  269. data: markPointData,
  270. z: 5 // 确保标记显示在最上层
  271. },
  272. itemStyle: {
  273. normal: {
  274. // 阳线样式(收盘 > 开盘)
  275. color: 'transparent', // 阳线色
  276. color0: '#008080', // 阴线色
  277. borderColor: '#ff0783', // 阳线边框色(红)
  278. borderColor0: '#008080', // 阴线边框色(绿)
  279. }
  280. },
  281. // 实现 分区域背景色
  282. markArea: {
  283. silent: true,
  284. data: regions.map((region) => [
  285. {
  286. yAxis: region.min,
  287. itemStyle: { normal: { color: region.color } },
  288. },
  289. { yAxis: region.max },
  290. ]),
  291. },
  292. },
  293. {
  294. name: '中间区域',
  295. type: 'bar',
  296. stack: 'total',
  297. data: middleRangeData1,
  298. barWidth: '20%',
  299. barCategoryGap: '40%',
  300. itemStyle: {
  301. normal: {
  302. color: 'rgba(0,0,0,0)' // 默认透明
  303. }
  304. },
  305. z:2
  306. },
  307. // 中间区域染色
  308. {
  309. name: '中间区域',
  310. type: 'bar',
  311. stack: 'total',
  312. data: middleRangeData,
  313. barWidth: '20%',
  314. barCategoryGap: '40%',
  315. itemStyle: {
  316. normal: {
  317. color: 'rgba(0,0,0,0)' // 默认透明
  318. }
  319. },
  320. z:2
  321. },
  322. {
  323. name: '止盈线',
  324. type: 'line',
  325. data: takeProfitData,
  326. symbol: 'none',
  327. lineStyle: {
  328. normal: {
  329. color: '#ff80ff', // 蓝色
  330. width: 2,
  331. type: 'solid'
  332. }
  333. },
  334. markPoint: {
  335. symbol: 'circle',
  336. symbolSize: 1,
  337. data: [
  338. {
  339. coord: [mixData.map((item) => item.value).length - 1, stopProfitPrice],
  340. itemStyle: {
  341. color: '#ff80ff'
  342. },
  343. label: {
  344. normal: {
  345. show: true,
  346. position: 'bottom',
  347. formatter: `{text|止盈: ${stopProfitPrice}}`,
  348. rich: {
  349. text: {
  350. color: '#820a06',
  351. fontSize: 14,
  352. fontWeight: 'bold'
  353. }
  354. }
  355. }
  356. }
  357. }
  358. ]
  359. }
  360. },
  361. {
  362. name: '止损线',
  363. type: 'line',
  364. data: stopLossData,
  365. symbol: 'none',
  366. lineStyle: {
  367. normal: {
  368. color: '#080bfd', // 红色
  369. width: 2,
  370. type: 'solid'
  371. }
  372. },
  373. markPoint: {
  374. symbol: 'circle',
  375. symbolSize: 1,
  376. data: [
  377. {
  378. coord: [mixData.map((item) => item.value).length - 1, stopLossPrice],
  379. itemStyle: {
  380. color: '#080bfd'
  381. },
  382. label: {
  383. normal: {
  384. show: true,
  385. position: 'bottom',
  386. formatter: `{text|止损: ${stopLossPrice}}`,
  387. rich: {
  388. text: {
  389. color: '#080bfd',
  390. fontSize: 14,
  391. fontWeight: 'bold'
  392. }
  393. }
  394. }
  395. }
  396. }
  397. ]
  398. }
  399. },
  400. ],
  401. grid: {
  402. left: "0",
  403. right: "10",
  404. top: '10',
  405. bottom: "0",
  406. containLabel: true,
  407. },
  408. };
  409. // 应用配置
  410. qxnlzhqEchartsInstance.setOption(option);
  411. // 监听窗口大小变化,调整图表尺寸
  412. window.addEventListener('resize', () => {
  413. qxnlzhqEchartsInstance.resize()
  414. })
  415. }
  416. onBeforeUnmount(() => {
  417. // 组件卸载时销毁图表
  418. if (qxnlzhqEchartsInstance) {
  419. qxnlzhqEchartsInstance.dispose();
  420. }
  421. });
  422. </script>
  423. <style scoped>
  424. #qxnlzhqEcharts {
  425. width: 100%;
  426. height: 700px;
  427. }
  428. </style>