st-comp 0.0.164 → 0.0.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/components.d.ts +3 -0
  2. package/es/ChartLayout.cjs +1 -1
  3. package/es/ChartLayout.js +3 -3
  4. package/es/Dialog.cjs +1 -1
  5. package/es/Dialog.js +16 -14
  6. package/es/FactorWarning.cjs +1 -1
  7. package/es/FactorWarning.js +492 -614
  8. package/es/Kline.cjs +1 -1
  9. package/es/Kline.js +16 -16
  10. package/es/KlineBasic.cjs +1 -1
  11. package/es/KlineBasic.js +477 -766
  12. package/es/KlineConfig.cjs +1 -0
  13. package/es/KlineConfig.js +703 -0
  14. package/es/KlineNew.cjs +1 -1
  15. package/es/KlineNew.js +14 -14
  16. package/es/KlinePlus.cjs +6 -0
  17. package/es/KlinePlus.js +1212 -0
  18. package/es/Pagination.cjs +1 -1
  19. package/es/Pagination.js +21 -20
  20. package/es/PasswordPrompt.cjs +1 -1
  21. package/es/PasswordPrompt.js +3 -3
  22. package/es/Table.cjs +1 -1
  23. package/es/Table.js +100 -279
  24. package/es/User.cjs +1 -1
  25. package/es/User.js +41 -39
  26. package/es/VarSelectDialog.cjs +1 -1
  27. package/es/VarSelectDialog.js +23 -22
  28. package/es/VarietySearch.cjs +1 -1
  29. package/es/VarietySearch.js +34 -32
  30. package/es/VirtualTable.cjs +5 -5
  31. package/es/VirtualTable.js +1018 -1056
  32. package/es/{_initCloneObject-47814efa.cjs → _initCloneObject-9ccbb113.cjs} +1 -1
  33. package/es/{_initCloneObject-c939f3af.js → _initCloneObject-a9305c1e.js} +3 -3
  34. package/es/base-4bcd2756.js +304 -0
  35. package/es/base-bdc10baa.cjs +5 -0
  36. package/es/{castArray-a78ae889.cjs → castArray-37eb9980.cjs} +1 -1
  37. package/es/{castArray-2440268f.js → castArray-681d750b.js} +1 -1
  38. package/es/{config-provider-722dca36.js → config-provider-93846ae3.js} +9 -9
  39. package/es/config-provider-ec6b09df.cjs +1 -0
  40. package/es/dayjs.min-19fb125f.cjs +1 -0
  41. package/es/dayjs.min-54a4e095.js +296 -0
  42. package/es/{debounce-82e23c4b.js → debounce-51046b06.js} +1 -1
  43. package/es/{debounce-cf26af24.cjs → debounce-9e323221.cjs} +1 -1
  44. package/es/{dropdown-6f8379f0.js → dropdown-65e74de8.js} +6 -6
  45. package/es/{dropdown-790d31ea.cjs → dropdown-b2dab5bb.cjs} +1 -1
  46. package/es/{el-autocomplete-75762ef9.js → el-autocomplete-5064d466.js} +9 -9
  47. package/es/{el-autocomplete-84de99ba.cjs → el-autocomplete-814993af.cjs} +1 -1
  48. package/es/el-button-960edd49.cjs +1 -0
  49. package/es/{el-button-2e878325.js → el-button-d39472d3.js} +37 -36
  50. package/es/{el-checkbox-group-45247bbf.cjs → el-checkbox-group-020b49f5.cjs} +1 -1
  51. package/es/{el-checkbox-group-5e4f32f0.js → el-checkbox-group-779f353a.js} +2 -2
  52. package/es/el-dialog-cd484c1f.cjs +1 -0
  53. package/es/el-dialog-f45ec2a1.js +267 -0
  54. package/es/el-divider-40c50ce7.cjs +1 -0
  55. package/es/el-divider-7cf13678.js +45 -0
  56. package/es/{el-empty-2ece1134.cjs → el-empty-627ec09e.cjs} +1 -1
  57. package/es/{el-empty-7f8acbdc.js → el-empty-64b2ea8a.js} +2 -2
  58. package/es/{el-form-item-45ba08ff.js → el-form-item-b31e7a98.js} +8 -8
  59. package/es/{el-form-item-47e255d4.cjs → el-form-item-e1c3104b.cjs} +2 -2
  60. package/es/{el-input-1323afd2.js → el-input-42315ac4.js} +48 -48
  61. package/es/{el-input-cb340042.cjs → el-input-5163bda3.cjs} +1 -1
  62. package/es/{el-input-number-0f194f3f.cjs → el-input-number-eb54cde4.cjs} +1 -1
  63. package/es/{el-input-number-991f8bfc.js → el-input-number-ebd64e22.js} +19 -19
  64. package/es/el-loading-15a5501c.cjs +1 -0
  65. package/es/el-loading-5d29f26d.js +187 -0
  66. package/es/{el-menu-item-4c9eb7ad.cjs → el-menu-item-ac89e804.cjs} +1 -1
  67. package/es/{el-menu-item-dbbcd01a.js → el-menu-item-d62e0a3f.js} +32 -32
  68. package/es/el-message-ad87b9dd.cjs +1 -0
  69. package/es/{el-message-03e6f342.js → el-message-bd1d2c82.js} +17 -17
  70. package/es/el-overlay-690b50aa.js +269 -0
  71. package/es/el-overlay-e5d318c3.cjs +1 -0
  72. package/es/{el-popconfirm-8372cd13.js → el-popconfirm-88bbd066.js} +13 -13
  73. package/es/{el-popconfirm-86503f8a.cjs → el-popconfirm-a497b279.cjs} +1 -1
  74. package/es/{el-popper-7adbf1c6.cjs → el-popper-14a9d493.cjs} +1 -1
  75. package/es/{el-popper-b5315f5b.js → el-popper-85365d5b.js} +63 -63
  76. package/es/{el-scrollbar-ac525239.cjs → el-scrollbar-8bf765f2.cjs} +1 -1
  77. package/es/{el-scrollbar-4dc1493a.js → el-scrollbar-bcf29780.js} +21 -21
  78. package/es/el-segmented-84707138.js +136 -0
  79. package/es/el-segmented-bccd9f2a.cjs +1 -0
  80. package/es/{el-select-31f8ac8a.cjs → el-select-1d2309a1.cjs} +1 -1
  81. package/es/{el-select-a4ad7c8b.js → el-select-d3bce4d1.js} +13 -13
  82. package/es/{el-table-column-2e7132b3.js → el-table-column-9691b26d.js} +19 -18
  83. package/es/el-table-column-be9a76bb.cjs +14 -0
  84. package/es/{el-tag-a79385c6.js → el-tag-6036f51c.js} +39 -39
  85. package/es/{el-tag-9732314f.cjs → el-tag-8d49d4d2.cjs} +1 -1
  86. package/es/{index-375e8de4.cjs → index-02ca0096.cjs} +1 -1
  87. package/es/{index-afc88a4b.cjs → index-19fa1fcf.cjs} +1 -1
  88. package/es/{index-733cd68a.js → index-21fb27eb.js} +17 -17
  89. package/es/{index-e5fe37a4.js → index-3425f2f0.js} +10 -10
  90. package/es/{index-1d85ba4b.cjs → index-5b81f4da.cjs} +1 -1
  91. package/es/index-8d3c5e96.cjs +3 -0
  92. package/es/{index-97d24ab7.js → index-8ee89b40.js} +16 -16
  93. package/es/{index-9e6e8d03.cjs → index-a99a5307.cjs} +1 -1
  94. package/es/{index-58521b9e.cjs → index-b90c746b.cjs} +1 -1
  95. package/es/index-c45ac024.js +14 -0
  96. package/es/{index-f5b9b211.js → index-cb8157f2.js} +2 -2
  97. package/es/{index-e6a36e09.js → index-d3c37134.js} +7 -7
  98. package/es/{index-65b4113e.cjs → index-d94bae37.cjs} +1 -1
  99. package/es/{index-2d9c78d4.cjs → index-e348d5e8.cjs} +1 -1
  100. package/es/{index-57a7323a.js → index-ef7c4392.js} +11 -11
  101. package/es/{index-e3353eeb.js → index-fbe85773.js} +13 -13
  102. package/es/{raf-f46caea0.cjs → raf-07cb6953.cjs} +1 -1
  103. package/es/{raf-c817c3f7.js → raf-36faa519.js} +1 -1
  104. package/es/{scroll-4fe7a76f.js → scroll-0277d145.js} +1 -1
  105. package/es/{scroll-339681d4.cjs → scroll-4769a65d.cjs} +1 -1
  106. package/es/style.css +1 -1
  107. package/es/{use-form-common-props-04fb0752.js → use-form-common-props-54c31983.js} +40 -40
  108. package/es/{use-form-common-props-74a96df2.cjs → use-form-common-props-592797e2.cjs} +1 -1
  109. package/es/use-global-config-584b62f1.cjs +1 -0
  110. package/es/use-global-config-946f61a4.js +73 -0
  111. package/es/{vnode-b15eb11b.cjs → vnode-1b827c99.cjs} +1 -1
  112. package/es/{vnode-0fd3003d.js → vnode-dfc92f20.js} +1 -1
  113. package/es/{zh-cn-cac8283b.js → zh-cn-5066f5e9.js} +2 -2
  114. package/es/{zh-cn-36f34912.cjs → zh-cn-b24a7dc9.cjs} +1 -1
  115. package/lib/bundle.js +1 -1
  116. package/lib/bundle.umd.cjs +217 -212
  117. package/lib/{index-5f63dc2e.js → index-80531492.js} +39042 -37182
  118. package/lib/{python-5918f7d5.js → python-20a1a5fc.js} +1 -1
  119. package/lib/style.css +1 -1
  120. package/package.json +1 -1
  121. package/packages/KlineConfig/config.js +39 -0
  122. package/packages/KlineConfig/index.ts +8 -0
  123. package/packages/KlineConfig/index.vue +315 -0
  124. package/packages/KlinePlus/components/SliderChart.vue +218 -0
  125. package/packages/KlinePlus/components/Tips.vue +36 -0
  126. package/packages/KlinePlus/images/buy.svg +1 -0
  127. package/packages/KlinePlus/images/sell.svg +1 -0
  128. package/packages/KlinePlus/images/t.svg +1 -0
  129. package/packages/KlinePlus/index.ts +8 -0
  130. package/packages/KlinePlus/index.vue +880 -0
  131. package/packages/KlinePlus/utils.js +705 -0
  132. package/packages/index.ts +4 -0
  133. package/src/pages/KlineConfig/index.vue +20 -0
  134. package/src/pages/KlinePlus/index.vue +180 -0
  135. package/src/router/routes.ts +10 -0
  136. package/es/base-0ca7c43a.cjs +0 -5
  137. package/es/base-b56cf478.js +0 -303
  138. package/es/config-provider-cfbb9cd8.cjs +0 -1
  139. package/es/el-button-62966947.cjs +0 -1
  140. package/es/el-message-d5ed7a4f.cjs +0 -1
  141. package/es/el-overlay-41994f67.cjs +0 -1
  142. package/es/el-overlay-47b665a0.js +0 -521
  143. package/es/el-table-column-bdc46568.cjs +0 -14
  144. package/es/index-0baeaec7.js +0 -82
  145. package/es/index-2f958e56.cjs +0 -3
@@ -0,0 +1,880 @@
1
+ <!-- 单品种单周期K线业务组件 -->
2
+ <script setup>
3
+ import dayjs from "dayjs";
4
+ import * as echarts from "echarts";
5
+ import { stMath, debounce, addResizeListener } from "st-func";
6
+ import { ref, watch, computed, onMounted, onUnmounted, inject } from "vue";
7
+ import { loadKlineConfig, getSubOptions, mergeklineData, handleMarkPointTradeLog, handleNetPositionLine, handleMarkPointOffset, normalizeToKlineTimeByMatch } from "./utils.js";
8
+ import Tips from "./components/Tips.vue";
9
+ import SliderChart from "./components/SliderChart.vue";
10
+
11
+ const { round, formatValue } = stMath;
12
+ const { request } = inject("stConfig"); // 组件库全局配置
13
+
14
+ const sliderChartRef = ref();
15
+ const loading = ref(false);
16
+ const isHover = ref(false);
17
+ const props = defineProps({
18
+ varietyCode: { type: String, required: true },
19
+ varietyStock: { type: Number, required: true }, // 品种市场类型 0-期货, 1-股票, 2-期权
20
+ indicatorStore: { type: Object, required: true }, // 指标配置Store
21
+ userKlineConfig: { type: Object, required: true }, // 用户自定义的K线配置
22
+
23
+ // 交互选项
24
+ cycle: { type: String, required: true },
25
+ sellBuy: { type: Number, default: 1 },
26
+ rightType: { type: Number, default: null },
27
+ klineType: { type: Number, default: null },
28
+ initTimeRange: { type: Array, required: true },
29
+ mainIndicator: { type: String, required: true },
30
+
31
+ // 绘制数据: 成交
32
+ tradeLog: { type: Array, default: () => [] }, // 成交数据
33
+ // 绘制数据: 净值
34
+ netPositionData: { type: Array, default: () => [] }, // 净值数据
35
+
36
+ // 功能: 强制定位高亮(非必填)
37
+ positionTime: { type: [String, null], default: null },
38
+ // 功能: 同步刷选范围(非必填)
39
+ syncBrushRange: { type: Object, default: () => ({ cycleList: [], startTime: null, endTime: null }) },
40
+ });
41
+
42
+ // 加载标记
43
+ let isLoadNew = false;
44
+ let isloadAllNew = false;
45
+ let isLoadHistory = false;
46
+ let isloadAllHistory = false;
47
+
48
+ let resizeRo = null;
49
+ let subChartIns = null;
50
+ let mainChartIns = null;
51
+ const subChartRef = ref(null);
52
+ const mainChartRef = ref(null);
53
+
54
+ // K线数据
55
+ const klineData = ref({
56
+ data: [],
57
+ mainIndicator: [],
58
+ subIndicator: [],
59
+ time: [],
60
+ });
61
+ const activeIndex = ref(0);
62
+ const screenTimeRange = ref([]); // 当屏首末时间
63
+ const contextmenuKlineTime = ref(null); // 右键点击的那一根K线时间
64
+
65
+ // 指标数据
66
+ const subIndicator = ref("VOL");
67
+ const mainTips = computed(() => {
68
+ const itemData = klineData.value.data[activeIndex.value];
69
+ if (!itemData) {
70
+ return [
71
+ { label: "开", value: "-" },
72
+ { label: "高", value: "-" },
73
+ { label: "低", value: "-" },
74
+ { label: "收", value: "-" },
75
+ { label: "额", value: "-" },
76
+ { label: "涨跌", value: "-" },
77
+ ];
78
+ }
79
+ const result = [
80
+ { label: "开", value: round(itemData[0]) },
81
+ { label: "高", value: round(itemData[3]) },
82
+ { label: "低", value: round(itemData[2]) },
83
+ { label: "收", value: round(itemData[1]) },
84
+ ];
85
+ // 特殊: 交易总额
86
+ itemData[4] !== null && result.push({ label: "额", value: formatValue(itemData[4]) });
87
+ // 特殊: 涨跌幅 公式:收盘价 - 昨收 / (昨收绝对值)
88
+ let diffColor;
89
+ if (itemData[6] > 0) {
90
+ diffColor = "red";
91
+ } else if (itemData[6] < 0) {
92
+ diffColor = "green";
93
+ }
94
+ result.push({ label: "涨跌", value: `${round(itemData[6])}%`, color: diffColor });
95
+ return result;
96
+ });
97
+ const mainIndicatorTips = computed(() => {
98
+ return (
99
+ klineData.value?.mainIndicator?.map((item) => {
100
+ return { label: item.key, value: round(item.data[activeIndex.value]), color: item.color };
101
+ }) || []
102
+ );
103
+ });
104
+ const subIndicatorTips = computed(() => {
105
+ return klineData.value?.subIndicator?.map((item) => ({ label: item.key, color: item.color, value: item.data[activeIndex.value] || "-" })) || [];
106
+ });
107
+
108
+ // 图表: 初始化
109
+ const initChart = () => {
110
+ if (mainChartIns) return;
111
+
112
+ // 主图
113
+ mainChartIns = echarts.init(mainChartRef.value);
114
+ mainChartIns.on(
115
+ "highlight",
116
+ debounce((params) => {
117
+ if (params.dataIndex) {
118
+ activeIndex.value = params.dataIndex;
119
+ } else {
120
+ activeIndex.value = params.batch?.[0].dataIndex ?? -1;
121
+ }
122
+ }, 10)
123
+ );
124
+ mainChartIns.on(
125
+ "datazoom",
126
+ debounce(async () => {
127
+ const { loadCheckCount } = loadKlineConfig;
128
+ const { startValue, endValue } = mainChartIns.getOption()?.dataZoom[0] ?? {};
129
+ // 当前开始索引 < 阈值边界, 触发加载更多数据
130
+ if (isLoadHistory === false && isloadAllHistory === false && startValue < loadCheckCount) await getMoreData("history");
131
+ // 当前结束索引 > 阈值边界, 触发加载更多数据
132
+ if (isLoadNew === false && isloadAllNew === false && endValue > klineData.value.time.length - loadCheckCount) await getMoreData("new");
133
+ getScreenTimeRange();
134
+ })
135
+ );
136
+ mainChartIns.on("globalout", () => {
137
+ const option = mainChartIns.getOption();
138
+ activeIndex.value = option.dataZoom[0].endValue;
139
+ });
140
+ mainChartIns.getZr().on("contextmenu", () => {
141
+ contextmenuKlineTime.value = klineData.value?.time[activeIndex.value];
142
+ });
143
+ window.addEventListener("keydown", handleKeyDownEvent);
144
+
145
+ // 副图
146
+ if (props.userKlineConfig.enable_subChart) {
147
+ subChartIns = echarts.init(subChartRef.value);
148
+ echarts.connect([mainChartIns, subChartIns]);
149
+ }
150
+
151
+ // DOM缩放
152
+ resizeRo = addResizeListener(mainChartRef.value);
153
+ resizeRo.listen(() => {
154
+ requestAnimationFrame(() => {
155
+ mainChartIns.resize();
156
+ subChartIns?.resize();
157
+ sliderChartRef.value?.resize();
158
+ });
159
+ });
160
+ };
161
+ // 图表: 获取数据 [首屏]
162
+ const getMainData = async ({ startTime, endTime }) => {
163
+ // 加载状态初始化
164
+ isLoadHistory = false;
165
+ isloadAllHistory = false;
166
+ isLoadNew = false;
167
+ isloadAllNew = false;
168
+ try {
169
+ loading.value = true;
170
+ const params = {
171
+ varietyCode: props.varietyCode,
172
+ cycle: props.cycle, // 周期
173
+ right: props.rightType, // 复权方式
174
+ contractType: props.varietyStock ? null : props.klineType, // 合约类型
175
+ mainIndicatorList: props.indicatorStore.getIndicatorParams(props.mainIndicator),
176
+ subIndicator: subIndicator.value,
177
+ startTime: dayjs(startTime).subtract(50, "day").format("YYYY-MM-DD HH:mm:ss"), // 确保缩放, 多请求部分数据, 也可以兼容undefined的情况
178
+ endTime: dayjs(endTime).add(50, "day").format("YYYY-MM-DD HH:mm:ss"), // 确保缩放, 多请求部分数据, 也可以兼容undefined的情况
179
+ deleteFirstNumber: 1, // >> 重要: 绩效后端特殊要求传参 <<
180
+ };
181
+ const { body } = await request.post("/middleLayer/kline/getKline", params);
182
+ klineData.value = body ?? { data: [], mainIndicator: [], subIndicator: [], time: [] };
183
+
184
+ // 绘制图表
185
+ const startValue = klineData.value.time.findIndex((item) => new Date(item) >= new Date(startTime));
186
+ const endValue = klineData.value.time.findIndex((item) => new Date(item) >= new Date(endTime));
187
+ draw({
188
+ startValue: startValue === -1 ? 0 : startValue,
189
+ endValue: endValue === -1 ? klineData.value.time.length - 1 : endValue,
190
+ });
191
+ } finally {
192
+ loading.value = false;
193
+ }
194
+ };
195
+ // 图表: 获取数据 [更多]
196
+ const getMoreData = async (type) => {
197
+ const { loadAddCount } = loadKlineConfig;
198
+ switch (type) {
199
+ case "history": {
200
+ isLoadHistory = true;
201
+ const params = {
202
+ varietyCode: props.varietyCode,
203
+ cycle: props.cycle,
204
+ right: props.rightType,
205
+ contractType: props.varietyStock ? null : props.klineType,
206
+ endTime: klineData.value.time[0],
207
+ limit: loadAddCount,
208
+ mainIndicatorList: props.indicatorStore.getIndicatorParams(props.mainIndicator),
209
+ subIndicator: subIndicator.value,
210
+ deleteFirstNumber: 1,
211
+ };
212
+ const { body } = await request.post("/middleLayer/kline/getKline", params);
213
+ // 合并数据
214
+ klineData.value = mergeklineData(body, klineData.value);
215
+ // 判断是否加载完全部的历史数据
216
+ if (body.data.length < loadAddCount) {
217
+ console.log("[K线] 获取更多数据: 左侧已全部获取完毕, 关闭");
218
+ isloadAllHistory = true;
219
+ }
220
+ isLoadHistory = false;
221
+ break;
222
+ }
223
+ case "new": {
224
+ isLoadNew = true;
225
+ const params = {
226
+ varietyCode: props.varietyCode,
227
+ cycle: props.cycle, // 周期
228
+ right: props.rightType, // 复权方式
229
+ contractType: props.varietyStock ? null : props.klineType, // 合约类型
230
+ startTime: klineData.value.time[klineData.value.time.length - 1], // 开始时间
231
+ limit: loadAddCount, // 查询K线数量
232
+ deleteFirstNumber: 1, // >> 重要: 绩效后端特殊要求传参 <<
233
+ mainIndicatorList: props.indicatorStore.getIndicatorParams(props.mainIndicator),
234
+ subIndicator: subIndicator.value,
235
+ };
236
+ const { body } = await request.post("/middleLayer/kline/getKline", params);
237
+ // 合并数据
238
+ klineData.value = mergeklineData(klineData.value, body);
239
+ // 判断是否加载完全部的新数据
240
+ if (body.data.length < loadAddCount) {
241
+ console.log("[K线] 获取更多数据: 右侧已全部获取完毕, 关闭");
242
+ isloadAllNew = true;
243
+ }
244
+ isLoadNew = false;
245
+ break;
246
+ }
247
+ }
248
+
249
+ // 绘制图表 (更新载入更多数据后的定位索引)
250
+ const { startValue, endValue } = mainChartIns.getOption()?.dataZoom[0] ?? {};
251
+ const klineDataTime = mainChartIns.getOption()?.xAxis[0].data;
252
+ const newStartValue = klineData.value.time.findIndex((item) => new Date(item) >= new Date(klineDataTime[startValue]));
253
+ const newEndValue = klineData.value.time.findIndex((item) => new Date(item) >= new Date(klineDataTime[endValue]));
254
+ draw({
255
+ startValue: newStartValue === -1 ? 0 : newStartValue,
256
+ endValue: newEndValue === -1 ? klineData.value.time.length - 1 : newEndValue,
257
+ });
258
+ };
259
+ // 图表: 获取数据 [当屏首末时间范围]
260
+ const getScreenTimeRange = () => {
261
+ if (!klineData.value.time) return (screenTimeRange.value = []);
262
+ const { time } = klineData.value;
263
+ const { startValue, endValue } = mainChartIns.getOption()?.dataZoom[0] ?? {};
264
+ const startTime = time[startValue];
265
+ const endTime = time[endValue];
266
+ if (["6", "7", "8"].includes(props.cycle)) {
267
+ screenTimeRange.value = [dayjs(startTime).format("YYYY-MM-DD"), dayjs(endTime).format("YYYY-MM-DD")];
268
+ } else {
269
+ screenTimeRange.value = [startTime, endTime];
270
+ }
271
+ };
272
+ // 图表: 绘制
273
+ const draw = (params = { startValue: 0, endValue: 0 }) => {
274
+ initChart();
275
+ const { maxValueSpan } = loadKlineConfig;
276
+ const { time, data, mainIndicator } = klineData.value;
277
+
278
+ // 如果超过了最大根数限制, 要求endValue不变, startValue进行变换
279
+ const startValue = params.endValue - params.startValue > maxValueSpan ? params.endValue - maxValueSpan : params.startValue;
280
+ const endValue = params.endValue;
281
+
282
+ // 配置: 指标线
283
+ const indicatorSeries = mainIndicator.map((item) => {
284
+ return {
285
+ name: item.key,
286
+ type: "line",
287
+ silent: true,
288
+ symbol: "none",
289
+ data: item.data,
290
+ lineStyle: {
291
+ width: item.width || 1,
292
+ },
293
+ itemStyle: {
294
+ color: item.color,
295
+ },
296
+ };
297
+ });
298
+ // 配置数据: 净值曲线
299
+ const { netPositionLineData } = handleNetPositionLine(props.netPositionData, props.cycle);
300
+ // 配置数据: 成交点位 + 连线
301
+ const { tradePointData, tradeLineData } = handleMarkPointTradeLog(props.tradeLog, props.cycle, props.sellBuy, time, data);
302
+
303
+ /**
304
+ * @description: 渲染图表
305
+ */
306
+ mainChartIns.setOption(
307
+ {
308
+ animation: false,
309
+ grid: {
310
+ top: "40px",
311
+ left: "60px",
312
+ right: "20px",
313
+ bottom: "20px",
314
+ },
315
+ dataZoom: [
316
+ {
317
+ type: "inside",
318
+ startValue,
319
+ endValue,
320
+ minValueSpan: loadKlineConfig.minValueSpan,
321
+ maxValueSpan: loadKlineConfig.maxValueSpan,
322
+ },
323
+ ],
324
+ tooltip: {
325
+ trigger: "axis",
326
+ appendToBody: true,
327
+ confine: true,
328
+ axisPointer: {
329
+ type: "cross",
330
+ label: {
331
+ rich: {},
332
+ formatter: (data) => {
333
+ const { axisDimension, value } = data;
334
+ if (axisDimension === "x") {
335
+ if (["6", "7", "8"].includes(props.cycle)) {
336
+ return dayjs(value).format("YYYY-MM-DD");
337
+ } else {
338
+ return value;
339
+ }
340
+ } else {
341
+ return String(round(value));
342
+ }
343
+ },
344
+ },
345
+ },
346
+ formatter: (params) => {
347
+ if (!params?.length) return null;
348
+ let body = "";
349
+ params.forEach((item) => {
350
+ // 资产持仓净值
351
+ if (item.seriesName === "netPosition" && item.data !== null) {
352
+ body += `<div>资产持仓净值: ${item.data[1]}</div>`;
353
+ }
354
+ // 成交数据
355
+ if (item.componentSubType === "candlestick") {
356
+ // 买卖点
357
+ if (props.sellBuy === 0) {
358
+ // 买卖点渲染逻辑
359
+ const sellBuy = tradePointData.filter((i) => i?.coord[0] === item.axisValue);
360
+ let buy = 0;
361
+ let sell = 0;
362
+ sellBuy.forEach((i) => {
363
+ const { tradeType, amount } = i.customData;
364
+ if (tradeType === "买") {
365
+ buy += amount;
366
+ }
367
+ if (tradeType === "卖") {
368
+ sell += amount;
369
+ }
370
+ });
371
+ if (buy) body += `<div>买: ${buy}</div>`;
372
+ if (sell) body += `<div>卖: ${sell}</div>`;
373
+ }
374
+ // 开平点
375
+ else {
376
+ // 开平点渲染逻辑
377
+ tradePointData.forEach((i) => {
378
+ if (i?.coord[0] === item.axisValue) {
379
+ const { tradeType, amount, profitAndLoss, openPriceAll, closePriceAll } = i.customData;
380
+ let tooltip = `<div>${tradeType}: ${amount}手</div>`;
381
+ // 开仓-tooltip: 开仓价
382
+ if (tradeType.includes("开")) {
383
+ tooltip += `<div>开仓价: ${(openPriceAll / amount)?.toFixed(2)}</div>`;
384
+ }
385
+ // 平仓-tooltip: 平仓价, 盈亏, 盈亏比率
386
+ if (tradeType.includes("平")) {
387
+ const openPrice = (openPriceAll / amount)?.toFixed(2);
388
+ const closePrice = (closePriceAll / amount)?.toFixed(2);
389
+ tooltip += `<div>平仓价: ${closePrice}</div>`;
390
+ tooltip += `<div>盈亏: ${profitAndLoss?.toFixed(2)}</div>`;
391
+ tooltip += `<div>盈亏比率: ${(((closePrice - openPrice) / openPrice) * 100)?.toFixed(2)} %</div>`;
392
+ }
393
+ body += tooltip;
394
+ }
395
+ });
396
+ }
397
+ }
398
+ });
399
+ if (!body) return null;
400
+ let time = params[0].axisValue;
401
+ if (["6", "7", "8"].includes(props.cycle)) time = dayjs(time).format("YYYY-MM-DD");
402
+ return `
403
+ <div>
404
+ <span style="font-weight: bold;">${time}</span>
405
+ ${body}
406
+ </div>
407
+ `;
408
+ },
409
+ },
410
+ xAxis: {
411
+ show: true,
412
+ type: "category",
413
+ data: time,
414
+ splitLine: {
415
+ show: false,
416
+ },
417
+ axisLabel: {
418
+ formatter: (value) => {
419
+ if (["6", "7", "8"].includes(props.cycle)) {
420
+ return dayjs(value).format("YYYY-MM-DD");
421
+ } else {
422
+ return value;
423
+ }
424
+ },
425
+ },
426
+ },
427
+ yAxis: [
428
+ {
429
+ type: "value",
430
+ axisLine: {
431
+ show: true,
432
+ },
433
+ splitLine: {
434
+ show: true,
435
+ lineStyle: {
436
+ type: "dotted",
437
+ color: "#333",
438
+ },
439
+ },
440
+ min: (value) => {
441
+ const { min, max } = value;
442
+ const interval = Math.abs((max - min) / 10);
443
+ return round(min - interval);
444
+ },
445
+ max: (value) => round(value.max),
446
+ },
447
+ {
448
+ show: false,
449
+ min: (value) => {
450
+ const { min, max } = value;
451
+ const interval = Math.abs((max - min) / 10);
452
+ return min - interval;
453
+ },
454
+ max: (value) => {
455
+ const { min, max } = value;
456
+ const interval = Math.abs((max - min) / 10);
457
+ return max + interval;
458
+ },
459
+ },
460
+ ],
461
+ series: [
462
+ {
463
+ type: "candlestick",
464
+ data,
465
+ markPoint: { data: handleMarkPointOffset([...tradePointData]) },
466
+ markLine: { data: [...tradeLineData] },
467
+ itemStyle: {
468
+ color: "transparent",
469
+ color0: "#00FFFF",
470
+ borderColor: "#FF0000",
471
+ borderColor0: "#00FFFF",
472
+ borderWidth: 1,
473
+ },
474
+ },
475
+ // 指标线
476
+ ...indicatorSeries,
477
+ // 净值曲线
478
+ {
479
+ type: "line",
480
+ name: "netPosition",
481
+ data: netPositionLineData,
482
+ symbol: "none",
483
+ yAxisIndex: 1,
484
+ connectNulls: true,
485
+ itemStyle: {
486
+ color: "#666666",
487
+ },
488
+ lineStyle: {
489
+ width: 2,
490
+ },
491
+ },
492
+ ],
493
+ toolbox: {
494
+ show: false,
495
+ },
496
+ brush: {
497
+ xAxisIndex: "all",
498
+ brushLink: "all",
499
+ transformable: false,
500
+ outOfBrush: {
501
+ colorAlpha: 2,
502
+ },
503
+ brushStyle: {
504
+ color: "rgba(255,255,255,0.1)",
505
+ borderColor: "rgba(255,255,255,0.4)",
506
+ },
507
+ },
508
+ },
509
+ true
510
+ );
511
+ if (props.userKlineConfig.enable_subChart) {
512
+ subChartIns.setOption(getSubOptions(klineData.value, startValue, endValue), true);
513
+ }
514
+
515
+ /**
516
+ * @description: 善后处理
517
+ */
518
+ {
519
+ getScreenTimeRange();
520
+ activeIndex.value = endValue;
521
+ }
522
+
523
+ /**
524
+ * @description: 数据框选: brush
525
+ * 由于brush配置中, 如果有一边的X轴时间不在已载入数据中, 整个都会失效, 所以这里使用的是匹配算法
526
+ */
527
+ {
528
+ const brushAreas = [];
529
+ // 1. 成交范围 (灰白色)
530
+ if (props.userKlineConfig.enable_tradeLogBrush && tradePointData.length) {
531
+ const [startTime, endTime] = [tradePointData[0].coord[0], tradePointData.at(-1).coord[0]];
532
+ const coordRange = normalizeToKlineTimeByMatch(time, [startTime, endTime], props.cycle);
533
+ brushAreas.push({
534
+ brushId: "开平仓范围",
535
+ brushType: "lineX",
536
+ xAxisIndex: 0,
537
+ coordRange,
538
+ });
539
+ }
540
+ // 2. 同步刷选范围 (蓝色)
541
+ if (props.syncBrushRange.cycleList.includes(props.cycle)) {
542
+ const { startTime, endTime } = props.syncBrushRange;
543
+ const coordRange = normalizeToKlineTimeByMatch(time, [startTime, endTime], props.cycle);
544
+ brushAreas.push({
545
+ brushId: "同步刷选范围",
546
+ brushType: "lineX",
547
+ xAxisIndex: 0,
548
+ coordRange,
549
+ brushStyle: {
550
+ fill: "rgba(64, 158, 255, 0.2)",
551
+ stroke: "rgba(64, 158, 255, 0.8)",
552
+ lineWidth: 1,
553
+ },
554
+ });
555
+ }
556
+ // 3. 强制定位高亮 (紫色)
557
+ if (props.positionTime) {
558
+ const [startTime, endTime] = [dayjs(props.positionTime).format("YYYY-MM-DD 00:00:00"), dayjs(props.positionTime).format("YYYY-MM-DD 23:59:59")];
559
+ const coordRange = normalizeToKlineTimeByMatch(time, [startTime, endTime], props.cycle);
560
+ brushAreas.push({
561
+ brushId: "强制定位高亮",
562
+ brushType: "lineX",
563
+ xAxisIndex: 0,
564
+ coordRange,
565
+ brushStyle: {
566
+ fill: "rgba(217, 179, 255, 0.2)",
567
+ stroke: "rgba(217, 179, 255, 0.8)",
568
+ lineWidth: 1,
569
+ },
570
+ });
571
+ }
572
+ mainChartIns.dispatchAction({
573
+ type: "brush",
574
+ areas: brushAreas,
575
+ });
576
+ }
577
+ };
578
+
579
+ // 拖拽轴: 拖拽回调
580
+ const handleSliderChange = (params) => {
581
+ const { startTime, endTime } = params;
582
+ getMainData({ startTime, endTime });
583
+ };
584
+ // 键盘控制 [↑↓←→缩放平移]
585
+ const handleKeyDownEvent = ({ code, ctrlKey }) => {
586
+ if (!(ctrlKey || isHover.value)) return;
587
+ const { xAxis, dataZoom } = mainChartIns.getOption();
588
+ const { data: xAxisData } = xAxis?.[0] ?? { data: [] };
589
+ let { startValue, endValue } = dataZoom?.[0] ?? {};
590
+ if (!xAxisData?.length) return;
591
+ // 键位cb
592
+ switch (code) {
593
+ // ↑ 放大
594
+ case "ArrowUp": {
595
+ if (endValue - startValue < 5) return;
596
+ const diff = Math.floor((endValue - startValue) / 2) + 1;
597
+ startValue = startValue + diff;
598
+ if (endValue - startValue < 5) startValue = endValue - 4;
599
+ break;
600
+ }
601
+ // ↓ 缩小
602
+ case "ArrowDown": {
603
+ const diff = Math.min(500, endValue - startValue);
604
+ startValue = startValue - diff - 1;
605
+ break;
606
+ }
607
+ // ← 左移
608
+ case "ArrowLeft": {
609
+ if (startValue > 0) {
610
+ startValue -= 1;
611
+ endValue -= 1;
612
+ }
613
+ if (activeIndex.value > 0) {
614
+ activeIndex.value -= 1;
615
+ }
616
+ break;
617
+ }
618
+ // → 右移
619
+ case "ArrowRight": {
620
+ if (endValue < xAxisData.length - 1) {
621
+ startValue += 1;
622
+ endValue += 1;
623
+ }
624
+ if (isHover.value && activeIndex.value < xAxisData.length - 1) {
625
+ activeIndex.value += 1;
626
+ }
627
+ break;
628
+ }
629
+ }
630
+ // 1. 更新图表当屏位置
631
+ mainChartIns.dispatchAction({
632
+ type: "dataZoom",
633
+ startValue,
634
+ endValue,
635
+ });
636
+ // 2. 更新图表指示器位置
637
+ mainChartIns.dispatchAction({
638
+ type: "updateAxisPointer",
639
+ seriesIndex: 0,
640
+ dataIndex: isHover.value ? activeIndex.value : null,
641
+ });
642
+ // 3. 更新图表高亮索引
643
+ mainChartIns.dispatchAction({
644
+ type: "highlight",
645
+ dataIndex: isHover.value ? activeIndex.value : endValue,
646
+ });
647
+ };
648
+
649
+ onMounted(() => {
650
+ const [startTime, endTime] = props.initTimeRange;
651
+ getMainData({ startTime, endTime });
652
+ });
653
+ // 监控: [品种, 初始时间范围] => 重加载K线 (缩放初始化)
654
+ watch(
655
+ () => [props.varietyCode, props.initTimeRange],
656
+ () => {
657
+ const [startTime, endTime] = props.initTimeRange;
658
+ getMainData({ startTime, endTime });
659
+ },
660
+ { deep: true }
661
+ );
662
+ // 监控: [主指标, 副指标, 指标配置, 周期选项, 复权选项, 常用选项] => 重加载K线 (缩放不变)
663
+ watch(
664
+ () => [props.mainIndicator, subIndicator.value, props.indicatorStore?.filterIndicator, props.indicatorStore?.customIndicator, props.cycle, props.rightType, props.klineType],
665
+ () => {
666
+ const { startValue, endValue } = mainChartIns.getOption()?.dataZoom[0] ?? {};
667
+ const [startTime, endTime] = [klineData.value.time[startValue], klineData.value.time[endValue]];
668
+ getMainData({ startTime, endTime });
669
+ },
670
+ { deep: true }
671
+ );
672
+ // 监控: [成交点类型, 成交数据, 净值数据] => 重绘K线 (缩放不变)
673
+ watch(
674
+ () => [props.sellBuy, props.tradeLog, props.netPositionData],
675
+ () => {
676
+ const { startValue, endValue } = mainChartIns.getOption()?.dataZoom[0] ?? {};
677
+ draw({ startValue, endValue });
678
+ },
679
+ { deep: true }
680
+ );
681
+ onUnmounted(() => {
682
+ // 解绑
683
+ mainChartIns.off("highlight");
684
+ mainChartIns.off("globalout");
685
+ mainChartIns.off("datazoom");
686
+ mainChartIns.getZr().off("contextmenu");
687
+ window.removeEventListener("keydown", handleKeyDownEvent);
688
+ // 销毁
689
+ mainChartIns.dispose();
690
+ subChartIns?.dispose();
691
+ resizeRo.dispose();
692
+ resizeRo = null;
693
+ });
694
+ defineExpose({
695
+ screenTimeRange,
696
+ contextmenuKlineTime,
697
+ reDraw: () => {
698
+ const { startValue, endValue } = mainChartIns.getOption()?.dataZoom[0] ?? {};
699
+ draw({ startValue, endValue });
700
+ },
701
+ });
702
+ </script>
703
+
704
+ <template>
705
+ <div
706
+ class="kline-plus"
707
+ v-loading="loading"
708
+ @mousemove="isHover = true"
709
+ @mouseout="isHover = false"
710
+ >
711
+ <!-- 主图 -->
712
+ <div class="main-chart">
713
+ <div class="indicator">
714
+ <Tips :data="mainTips" />
715
+ <Tips :data="mainIndicatorTips" />
716
+ </div>
717
+ <template v-if="userKlineConfig.enable_showScreenTimeRange">
718
+ <span class="screen-time-range"> {{ screenTimeRange[0] }} - {{ screenTimeRange[1] }} </span>
719
+ </template>
720
+ <!-- K线 -->
721
+ <div
722
+ ref="mainChartRef"
723
+ class="chart"
724
+ />
725
+ </div>
726
+ <!-- 副图 -->
727
+ <template v-if="userKlineConfig.enable_subChart">
728
+ <div class="sub-chart">
729
+ <div class="indicator">
730
+ <div class="title">
731
+ <span>{{ subIndicator }}</span>
732
+ <el-select
733
+ v-model="subIndicator"
734
+ style="width: 100px; margin-right: 4px; height: 25px"
735
+ size="small"
736
+ class="element-dark"
737
+ popper-class="element-dark"
738
+ >
739
+ <el-option
740
+ v-for="(item, index) in indicatorStore.subIndicatorList"
741
+ :key="index"
742
+ :label="item.label"
743
+ :value="item.value"
744
+ />
745
+ </el-select>
746
+ </div>
747
+ <Tips :data="subIndicatorTips" />
748
+ </div>
749
+ <div
750
+ ref="subChartRef"
751
+ class="chart"
752
+ />
753
+ </div>
754
+ </template>
755
+ <!-- 拖拽轴 -->
756
+ <template v-if="userKlineConfig.enable_sliderChart">
757
+ <div class="slider-chart">
758
+ <SliderChart
759
+ ref="sliderChartRef"
760
+ :screenTimeRange="screenTimeRange"
761
+ :varietyCode="varietyCode"
762
+ :varietyStock="varietyStock"
763
+ :rightType="rightType"
764
+ :klineType="klineType"
765
+ @change="handleSliderChange"
766
+ />
767
+ </div>
768
+ </template>
769
+ <!-- 空数据 -->
770
+ <el-empty
771
+ v-if="!klineData.time.length"
772
+ class="empty"
773
+ description="暂无数据"
774
+ />
775
+ </div>
776
+ </template>
777
+
778
+ <style lang="scss" scoped>
779
+ .kline-plus {
780
+ width: 100%;
781
+ height: 100%;
782
+ position: relative;
783
+ color-scheme: dark;
784
+ display: flex;
785
+ flex-direction: column;
786
+ // 主图
787
+ .main-chart {
788
+ width: 100%;
789
+ height: calc((100% - 40px) * 0.8);
790
+ background-color: black;
791
+ flex: 1;
792
+ .indicator {
793
+ height: 55px;
794
+ box-sizing: border-box;
795
+ padding-left: 10px;
796
+ padding-top: 4px;
797
+ }
798
+ .screen-time-range {
799
+ color: white;
800
+ font-size: 12px;
801
+ line-height: 12px;
802
+ position: absolute;
803
+ right: 10px;
804
+ top: 40px;
805
+ }
806
+ .chart {
807
+ width: 100%;
808
+ height: calc(100% - 55px);
809
+ box-sizing: border-box;
810
+ }
811
+ }
812
+ // 副图
813
+ .sub-chart {
814
+ width: 100%;
815
+ height: calc((100% - 40px) * 0.2);
816
+ .indicator {
817
+ padding-left: 10px;
818
+ display: flex;
819
+ align-items: center;
820
+ .title {
821
+ display: flex;
822
+ align-items: center;
823
+ span {
824
+ font-size: 12px;
825
+ line-height: 24px;
826
+ color: white;
827
+ }
828
+ :deep(.el-select) {
829
+ width: 16px !important;
830
+ height: 16px !important;
831
+ margin-left: 4px;
832
+ .el-select__icon {
833
+ margin-left: 0;
834
+ color: black;
835
+ width: 16px;
836
+ height: 16px;
837
+ }
838
+ .el-select__wrapper {
839
+ border-radius: 50%;
840
+ background: #ccc;
841
+ padding: 0;
842
+ gap: 0;
843
+ min-height: 12px;
844
+ line-height: 12px;
845
+ }
846
+ }
847
+ }
848
+ }
849
+ .chart {
850
+ width: 100%;
851
+ height: calc(100% - 24px);
852
+ }
853
+ }
854
+ // 拖拽轴
855
+ .slider-chart {
856
+ width: 100%;
857
+ height: 40px;
858
+ }
859
+ // 空数据
860
+ .empty {
861
+ position: absolute;
862
+ top: 0;
863
+ left: 0;
864
+ width: 100%;
865
+ height: 100%;
866
+ padding: 0;
867
+ background-color: rgb(0, 0, 0);
868
+ --el-empty-fill-color-0: var(--el-color-black);
869
+ --el-empty-fill-color-1: #4b4b52;
870
+ --el-empty-fill-color-2: #36383d;
871
+ --el-empty-fill-color-3: #1e1e20;
872
+ --el-empty-fill-color-4: #262629;
873
+ --el-empty-fill-color-5: #202124;
874
+ --el-empty-fill-color-6: #212224;
875
+ --el-empty-fill-color-7: #1b1c1f;
876
+ --el-empty-fill-color-8: #1c1d1f;
877
+ --el-empty-fill-color-9: #18181a;
878
+ }
879
+ }
880
+ </style>