index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <template>
  2. <div class="mtp-echats-kline">
  3. <div class="mtp-echats-kline__container main">
  4. <ul class="legend">
  5. <li class="legend-item">开: {{klineDetail ? klineDetail.open : '--'}}</li>
  6. <li class="legend-item">收: {{klineDetail ? klineDetail.close : '--'}}</li>
  7. <li class="legend-item">高: {{klineDetail ? klineDetail.highest : '--'}}</li>
  8. <li class="legend-item">低: {{klineDetail ? klineDetail.lowest : '--'}}</li>
  9. <li class="legend-item">MA5: {{klineDetail ? klineDetail.ma5 : '--'}}</li>
  10. <li class="legend-item">MA10: {{klineDetail ? klineDetail.ma10 : '--'}}</li>
  11. <li class="legend-item">MA15: {{klineDetail ? klineDetail.ma15 : '--'}}</li>
  12. </ul>
  13. <mtp-echarts :option="klineOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="mainReady" />
  14. </div>
  15. <template v-if="showIndicator">
  16. <div class="mtp-echats-kline__container indicator">
  17. <!-- MACD -->
  18. <section class="section" v-if="activeSeriesType === SeriesType.MACD">
  19. <ul class="legend">
  20. <li class="legend-item">MACD: {{macdDetail ? macdDetail.macd : '--'}}</li>
  21. <li class="legend-item">DIF: {{macdDetail ? macdDetail.dif : '--'}}</li>
  22. <li class="legend-item">DEA: {{macdDetail ? macdDetail.dea : '--'}}</li>
  23. </ul>
  24. <mtp-echarts :option="macdOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
  25. </section>
  26. <!-- VOL -->
  27. <section class="section" v-if="activeSeriesType === SeriesType.VOL">
  28. <ul class="legend">
  29. <li class="legend-item">VOL: {{volDetail ? volDetail.vol : '--'}}</li>
  30. </ul>
  31. <mtp-echarts :option="volOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
  32. </section>
  33. <!-- KDJ -->
  34. <section class="section" v-if="activeSeriesType === SeriesType.KDJ">
  35. <ul class="legend">
  36. <li class="legend-item">K: {{kdjDetail ? kdjDetail.k : '--'}}</li>
  37. <li class="legend-item">D: {{kdjDetail ? kdjDetail.d : '--'}}</li>
  38. <li class="legend-item">J: {{kdjDetail ? kdjDetail.j : '--'}}</li>
  39. </ul>
  40. <mtp-echarts :option="kdjOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
  41. </section>
  42. <!-- CCI -->
  43. <section class="section" v-if="activeSeriesType === SeriesType.CCI">
  44. <ul class="legend">
  45. <li class="legend-item">CCI: {{cciDetail ? cciDetail.cci : '--'}}</li>
  46. </ul>
  47. <mtp-echarts :option="cciOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
  48. </section>
  49. </div>
  50. <mtp-tabbar theme="menu" :data="tabs" @change="tabChange" />
  51. </template>
  52. </div>
  53. </template>
  54. <script lang="ts">
  55. import { defineComponent, ref, PropType, watch, computed } from 'vue'
  56. import { QueryHistoryDatas, QueryQuoteDayRsp } from '@/services/go/quote/interface';
  57. import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote';
  58. import { getQuoteDayInfoByCode } from '@/services/bus/goods';
  59. import { throttle } from '@/utils/time'
  60. import { CycleType, SeriesType } from './type'
  61. import { useDataset } from './dataset'
  62. import { useOptions } from './options'
  63. import moment from 'moment';
  64. import * as echarts from 'echarts'
  65. import MtpEcharts from '../echarts-base/index.vue'
  66. import MtpTabbar from '../../tabbar/index.vue'
  67. import { Tabbar } from '../../tabbar/type'
  68. export default defineComponent({
  69. components: {
  70. MtpEcharts,
  71. MtpTabbar,
  72. },
  73. props: {
  74. goodscode: {
  75. type: String,
  76. default: '',
  77. },
  78. // 周期类型
  79. cycleType: {
  80. type: Number as PropType<CycleType>,
  81. default: CycleType.minutes,
  82. },
  83. // 是否显示指标
  84. showIndicator: {
  85. type: Boolean,
  86. default: true,
  87. },
  88. },
  89. setup(props) {
  90. const loading = ref(false),
  91. showEmpty = ref(false),
  92. dataIndex = ref(0), // 当前数据索引值
  93. activeSeriesType = ref(SeriesType.MACD), // 当前选中的指标
  94. chartGroup = new Map<string, echarts.ECharts>(), // 图表联动实例组
  95. quote = ref<QueryQuoteDayRsp>(getQuoteDayInfoByCode(props.goodscode)!); // 商品实时行情
  96. const { klineData, macdData, volData, kdjData, cciData, handleData, calcIndicator } = useDataset();
  97. const { klineOption, macdOption, volOption, kdjOption, cciOption, initOptions, updateOptions } = useOptions(klineData, macdData, volData, kdjData, cciData);
  98. const klineDetail = computed(() => klineData.source[dataIndex.value]);
  99. const macdDetail = computed(() => macdData.source[dataIndex.value]);
  100. const volDetail = computed(() => volData.source[dataIndex.value]);
  101. const kdjDetail = computed(() => kdjData.source[dataIndex.value]);
  102. const cciDetail = computed(() => cciData.source[dataIndex.value]);
  103. const tabs: Tabbar[] = [
  104. { label: 'MACD', value: SeriesType.MACD },
  105. { label: 'VOL', value: SeriesType.VOL },
  106. { label: 'KDJ', value: SeriesType.KDJ },
  107. { label: 'CCI', value: SeriesType.CCI },
  108. ]
  109. const mainReady = (chart: echarts.ECharts) => {
  110. chartGroup.set('main', chart);
  111. initData();
  112. }
  113. const indicatorReady = (chart: echarts.ECharts) => {
  114. chartGroup.delete('indicator');
  115. chartGroup.set('indicator', chart);
  116. echarts.connect([...chartGroup.values()]); // 图表联动
  117. }
  118. // 初始化数据
  119. const initData = () => {
  120. showEmpty.value = false;
  121. loading.value = true;
  122. dataIndex.value = -1;
  123. klineData.source = [];
  124. macdData.source = [];
  125. volData.source = [];
  126. kdjData.source = [];
  127. cciData.source = [];
  128. const params: QueryHistoryDatas = {
  129. cycleType: props.cycleType,
  130. goodsCode: props.goodscode.toUpperCase(),
  131. count: 1440,
  132. };
  133. // 查询历史数据
  134. queryHistoryDatas(params).then((res) => {
  135. if (res.length) {
  136. dataIndex.value = res.length - 1;
  137. // 日期升序排序
  138. const data = res.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf());
  139. handleData(data, () => initOptions());
  140. } else {
  141. showEmpty.value = true;
  142. }
  143. }).catch((err) => {
  144. console.error(err);
  145. showEmpty.value = true;
  146. }).finally(() => {
  147. loading.value = false;
  148. });
  149. }
  150. // 指标切换
  151. const tabChange = (item: Tabbar<SeriesType>) => {
  152. activeSeriesType.value = item.value;
  153. setTimeout(() => {
  154. initOptions();
  155. }, 0);
  156. }
  157. // 获取周期毫秒数
  158. const getCycleMilliseconds = () => {
  159. const milliseconds = 60 * 1000; // 一分钟毫秒数
  160. switch (props.cycleType) {
  161. case CycleType.minutes5: {
  162. return milliseconds * 5;
  163. }
  164. case CycleType.minutes30: {
  165. return milliseconds * 30;
  166. }
  167. case CycleType.minutes60: {
  168. return milliseconds * 60;
  169. }
  170. case CycleType.hours2: {
  171. return milliseconds * 2 * 60;
  172. }
  173. case CycleType.Hours4: {
  174. return milliseconds * 4 * 60;
  175. }
  176. case CycleType.days: {
  177. return milliseconds * 24 * 60;
  178. }
  179. default: {
  180. return milliseconds;
  181. }
  182. }
  183. }
  184. // 更新图表数据
  185. const updateChartData = () => {
  186. const { source } = klineData,
  187. lastIndex = source.length - 1, // 历史行情最后索引位置
  188. cycleMilliseconds = getCycleMilliseconds(),
  189. newTime = moment(quote.value.lasttime), // 实时行情最新时间
  190. last = quote.value.last; // 实时行情最新价
  191. const oldTime = lastIndex === -1 ? newTime : moment(source[lastIndex].date); // 历史行情最后时间
  192. const diffTime = newTime.valueOf() - oldTime.valueOf(); // 计算时间差
  193. if (diffTime > cycleMilliseconds * 2) {
  194. // 时间间隔超过两个周期,重新请求历史数据
  195. } else {
  196. // 判断时间差是否大于周期时间
  197. if (lastIndex === -1 || diffTime > cycleMilliseconds) {
  198. oldTime.add(cycleMilliseconds, 'ms');
  199. const newDate = oldTime.format('YYYY-MM-DD HH:mm:ss');
  200. // 新增K线数据
  201. klineData.source.push({
  202. date: newDate,
  203. open: last,
  204. close: last,
  205. lowest: last,
  206. highest: last,
  207. ma5: '-',
  208. ma10: '-',
  209. ma15: '-',
  210. });
  211. // 新增MACD数据
  212. macdData.source.push({
  213. date: newDate,
  214. ema12: 0,
  215. ema26: 0,
  216. dif: 0,
  217. dea: 0,
  218. macd: 0,
  219. })
  220. // 新增VOL数据
  221. volData.source.push({
  222. date: newDate,
  223. vol: 0,
  224. })
  225. // 新增KDJ数据
  226. kdjData.source.push({
  227. date: newDate,
  228. k: '-',
  229. d: '-',
  230. j: '-',
  231. })
  232. // 新增CCI数据
  233. cciData.source.push({
  234. date: newDate,
  235. cci: '-',
  236. })
  237. } else {
  238. // 更新列表中最后一条记录的数据
  239. const item = source[lastIndex];
  240. if (item.lowest > last) {
  241. item.lowest = last; // 更新最低价
  242. }
  243. if (item.highest < last) {
  244. item.highest = last; // 更新最高价
  245. }
  246. item.close = last; // 更新收盘价
  247. }
  248. // 更新各种指标
  249. calcIndicator(lastIndex === -1 ? 0 : lastIndex);
  250. // 延迟图表更新,减少卡顿
  251. throttle(() => {
  252. updateOptions();
  253. }, 1000)
  254. }
  255. }
  256. // 监听行情推送
  257. watch(() => quote.value.last, () => {
  258. if (!loading.value) {
  259. updateChartData();
  260. }
  261. })
  262. // 监听周期选择变化
  263. watch(() => props.cycleType, () => initData());
  264. return {
  265. SeriesType,
  266. loading,
  267. showEmpty,
  268. dataIndex,
  269. tabs,
  270. activeSeriesType,
  271. klineData,
  272. klineOption,
  273. macdOption,
  274. volOption,
  275. kdjOption,
  276. cciOption,
  277. klineDetail,
  278. macdDetail,
  279. volDetail,
  280. kdjDetail,
  281. cciDetail,
  282. tabChange,
  283. mainReady,
  284. indicatorReady,
  285. }
  286. }
  287. })
  288. </script>
  289. <style lang="less">
  290. @import './index.less';
  291. </style>