|
|
@@ -0,0 +1,316 @@
|
|
|
+<template>
|
|
|
+ <div class="mtp-echats-kline">
|
|
|
+ <div class="mtp-echats-kline__container main">
|
|
|
+ <ul class="legend">
|
|
|
+ <li class="legend-item">开: {{klineDetail ? klineDetail.open : '--'}}</li>
|
|
|
+ <li class="legend-item">收: {{klineDetail ? klineDetail.close : '--'}}</li>
|
|
|
+ <li class="legend-item">高: {{klineDetail ? klineDetail.highest : '--'}}</li>
|
|
|
+ <li class="legend-item">低: {{klineDetail ? klineDetail.lowest : '--'}}</li>
|
|
|
+ <li class="legend-item">MA5: {{klineDetail ? klineDetail.ma5 : '--'}}</li>
|
|
|
+ <li class="legend-item">MA10: {{klineDetail ? klineDetail.ma10 : '--'}}</li>
|
|
|
+ <li class="legend-item">MA15: {{klineDetail ? klineDetail.ma15 : '--'}}</li>
|
|
|
+ </ul>
|
|
|
+ <mtp-echarts :option="klineOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="mainReady" />
|
|
|
+ </div>
|
|
|
+ <template v-if="showIndicator">
|
|
|
+ <div class="mtp-echats-kline__container indicator">
|
|
|
+ <!-- MACD -->
|
|
|
+ <section class="section" v-if="tabs[selectedTab].code === SeriesType.MACD">
|
|
|
+ <ul class="legend">
|
|
|
+ <li class="legend-item">MACD: {{macdDetail ? macdDetail.macd : '--'}}</li>
|
|
|
+ <li class="legend-item">DIF: {{macdDetail ? macdDetail.dif : '--'}}</li>
|
|
|
+ <li class="legend-item">DEA: {{macdDetail ? macdDetail.dea : '--'}}</li>
|
|
|
+ </ul>
|
|
|
+ <mtp-echarts :option="macdOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
|
|
|
+ </section>
|
|
|
+ <!-- VOL -->
|
|
|
+ <section class="section" v-if="tabs[selectedTab].code === SeriesType.VOL">
|
|
|
+ <ul class="legend">
|
|
|
+ <li class="legend-item">VOL: {{volDetail ? volDetail.vol : '--'}}</li>
|
|
|
+ </ul>
|
|
|
+ <mtp-echarts :option="volOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
|
|
|
+ </section>
|
|
|
+ <!-- KDJ -->
|
|
|
+ <section class="section" v-if="tabs[selectedTab].code === SeriesType.KDJ">
|
|
|
+ <ul class="legend">
|
|
|
+ <li class="legend-item">K: {{kdjDetail ? kdjDetail.k : '--'}}</li>
|
|
|
+ <li class="legend-item">D: {{kdjDetail ? kdjDetail.d : '--'}}</li>
|
|
|
+ <li class="legend-item">J: {{kdjDetail ? kdjDetail.j : '--'}}</li>
|
|
|
+ </ul>
|
|
|
+ <mtp-echarts :option="kdjOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
|
|
|
+ </section>
|
|
|
+ <!-- CCI -->
|
|
|
+ <section class="section" v-if="tabs[selectedTab].code === SeriesType.CCI">
|
|
|
+ <ul class="legend">
|
|
|
+ <li class="legend-item">CCI: {{cciDetail ? cciDetail.cci : '--'}}</li>
|
|
|
+ </ul>
|
|
|
+ <mtp-echarts :option="cciOption" :empty="showEmpty" v-model:index="dataIndex" v-model:loading="loading" @ready="indicatorReady" />
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <mtp-tabbar theme="menu" :data="tabs" v-model:active="selectedTab" @change="tabChange" />
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+import { defineComponent, ref, PropType, watch, computed } from 'vue'
|
|
|
+import { QueryHistoryDatas, QueryQuoteDayRsp } from '@/services/go/quote/interface';
|
|
|
+import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote';
|
|
|
+import { getQuoteDayInfoByCode } from '@/services/bus/goods';
|
|
|
+import { throttle } from '@/utils/time'
|
|
|
+import { CycleType, SeriesType } from './type'
|
|
|
+import { useDataset } from './dataset'
|
|
|
+import { useOptions } from './options'
|
|
|
+import moment from 'moment';
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import MtpEcharts from '../echarts-base/index.vue'
|
|
|
+import MtpTabbar from '../../tabbar/index.vue'
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ components: {
|
|
|
+ MtpEcharts,
|
|
|
+ MtpTabbar,
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ goodscode: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ // 周期类型
|
|
|
+ cycleType: {
|
|
|
+ type: Number as PropType<CycleType>,
|
|
|
+ default: CycleType.minutes,
|
|
|
+ },
|
|
|
+ // 是否显示指标
|
|
|
+ showIndicator: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ const loading = ref(false),
|
|
|
+ showEmpty = ref(false),
|
|
|
+ dataIndex = ref(0), // 当前数据索引值
|
|
|
+ selectedTab = ref(0), // 当前选中的标签
|
|
|
+ chartGroup = new Map<string, echarts.ECharts>(), // 图表联动实例组
|
|
|
+ quote = ref<QueryQuoteDayRsp>(getQuoteDayInfoByCode(props.goodscode)!); // 商品实时行情
|
|
|
+
|
|
|
+ const { klineData, macdData, volData, kdjData, cciData, handleData, calcIndicator } = useDataset();
|
|
|
+ const { klineOption, macdOption, volOption, kdjOption, cciOption, initOptions, updateOptions } = useOptions(klineData, macdData, volData, kdjData, cciData);
|
|
|
+ const klineDetail = computed(() => klineData.source[dataIndex.value]);
|
|
|
+ const macdDetail = computed(() => macdData.source[dataIndex.value]);
|
|
|
+ const volDetail = computed(() => volData.source[dataIndex.value]);
|
|
|
+ const kdjDetail = computed(() => kdjData.source[dataIndex.value]);
|
|
|
+ const cciDetail = computed(() => cciData.source[dataIndex.value]);
|
|
|
+
|
|
|
+ const tabs = [
|
|
|
+ { code: SeriesType.MACD, label: 'MACD' },
|
|
|
+ { code: SeriesType.VOL, label: 'VOL' },
|
|
|
+ { code: SeriesType.KDJ, label: 'KDJ' },
|
|
|
+ { code: SeriesType.CCI, label: 'CCI' },
|
|
|
+ ]
|
|
|
+
|
|
|
+ const mainReady = (chart: echarts.ECharts) => {
|
|
|
+ chartGroup.set('main', chart);
|
|
|
+ initData();
|
|
|
+ }
|
|
|
+
|
|
|
+ const indicatorReady = (chart: echarts.ECharts) => {
|
|
|
+ chartGroup.delete('indicator');
|
|
|
+ chartGroup.set('indicator', chart);
|
|
|
+ echarts.connect([...chartGroup.values()]); // 图表联动
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化数据
|
|
|
+ const initData = () => {
|
|
|
+ showEmpty.value = false;
|
|
|
+ loading.value = true;
|
|
|
+ dataIndex.value = -1;
|
|
|
+ klineData.source = [];
|
|
|
+ macdData.source = [];
|
|
|
+ volData.source = [];
|
|
|
+ kdjData.source = [];
|
|
|
+ cciData.source = [];
|
|
|
+
|
|
|
+ const params: QueryHistoryDatas = {
|
|
|
+ cycleType: props.cycleType,
|
|
|
+ goodsCode: props.goodscode.toUpperCase(),
|
|
|
+ count: 1440,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 查询历史数据
|
|
|
+ queryHistoryDatas(params).then((res) => {
|
|
|
+ if (res.length) {
|
|
|
+ dataIndex.value = res.length - 1;
|
|
|
+ // 日期升序排序
|
|
|
+ const data = res.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf());
|
|
|
+ handleData(data);
|
|
|
+ initOptions();
|
|
|
+ } else {
|
|
|
+ showEmpty.value = true;
|
|
|
+ }
|
|
|
+ }).catch((err) => {
|
|
|
+ console.error(err);
|
|
|
+ showEmpty.value = true;
|
|
|
+ }).finally(() => {
|
|
|
+ loading.value = false;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 指标切换
|
|
|
+ const tabChange = (index: number) => {
|
|
|
+ selectedTab.value = index;
|
|
|
+ setTimeout(() => {
|
|
|
+ initOptions();
|
|
|
+ }, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取周期毫秒数
|
|
|
+ const getCycleMilliseconds = () => {
|
|
|
+ const milliseconds = 60 * 1000; // 一分钟毫秒数
|
|
|
+ switch (props.cycleType) {
|
|
|
+ case CycleType.minutes5: {
|
|
|
+ return milliseconds * 5;
|
|
|
+ }
|
|
|
+ case CycleType.minutes30: {
|
|
|
+ return milliseconds * 30;
|
|
|
+ }
|
|
|
+ case CycleType.minutes60: {
|
|
|
+ return milliseconds * 60;
|
|
|
+ }
|
|
|
+ case CycleType.hours2: {
|
|
|
+ return milliseconds * 2 * 60;
|
|
|
+ }
|
|
|
+ case CycleType.Hours4: {
|
|
|
+ return milliseconds * 4 * 60;
|
|
|
+ }
|
|
|
+ case CycleType.days: {
|
|
|
+ return milliseconds * 24 * 60;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ return milliseconds;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新图表数据
|
|
|
+ const updateChartData = () => {
|
|
|
+ const { source } = klineData,
|
|
|
+ lastIndex = source.length - 1, // 历史行情最后索引位置
|
|
|
+ cycleMilliseconds = getCycleMilliseconds(),
|
|
|
+ newTime = moment(quote.value.lasttime), // 实时行情最新时间
|
|
|
+ newPrice = quote.value.last; // 实时行情最新价
|
|
|
+
|
|
|
+ const oldTime = lastIndex === -1 ? newTime : moment(source[lastIndex].date); // 历史行情最后时间
|
|
|
+ const diffTime = newTime.valueOf() - oldTime.valueOf(); // 计算时间差
|
|
|
+
|
|
|
+ if (diffTime > cycleMilliseconds * 2) {
|
|
|
+ // 时间间隔超过两个周期,重新请求历史数据
|
|
|
+ } else {
|
|
|
+ // 判断时间差是否大于周期时间
|
|
|
+ if (lastIndex === -1 || diffTime > cycleMilliseconds) {
|
|
|
+ oldTime.add(cycleMilliseconds, 'ms');
|
|
|
+ const newDate = oldTime.format('YYYY-MM-DD HH:mm:ss');
|
|
|
+
|
|
|
+ // 新增K线数据
|
|
|
+ klineData.source.push({
|
|
|
+ date: newDate,
|
|
|
+ open: newPrice,
|
|
|
+ close: newPrice,
|
|
|
+ lowest: newPrice,
|
|
|
+ highest: newPrice,
|
|
|
+ ma5: '-',
|
|
|
+ ma10: '-',
|
|
|
+ ma15: '-',
|
|
|
+ });
|
|
|
+
|
|
|
+ // 新增MACD数据
|
|
|
+ macdData.source.push({
|
|
|
+ date: newDate,
|
|
|
+ ema12: 0,
|
|
|
+ ema26: 0,
|
|
|
+ dif: 0,
|
|
|
+ dea: 0,
|
|
|
+ macd: 0,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 新增VOL数据
|
|
|
+ volData.source.push({
|
|
|
+ date: newDate,
|
|
|
+ vol: 0,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 新增KDJ数据
|
|
|
+ kdjData.source.push({
|
|
|
+ date: newDate,
|
|
|
+ k: '-',
|
|
|
+ d: '-',
|
|
|
+ j: '-',
|
|
|
+ })
|
|
|
+
|
|
|
+ // 新增CCI数据
|
|
|
+ cciData.source.push({
|
|
|
+ date: newDate,
|
|
|
+ cci: '-',
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 更新列表中最后一条记录的数据
|
|
|
+ const item = source[lastIndex];
|
|
|
+ if (item.lowest > newPrice) {
|
|
|
+ item.lowest = newPrice; // 更新最低价
|
|
|
+ }
|
|
|
+ if (item.highest < newPrice) {
|
|
|
+ item.highest = newPrice; // 更新最高价
|
|
|
+ }
|
|
|
+ item.close = newPrice; // 更新收盘价
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新各种指标
|
|
|
+ calcIndicator(lastIndex === -1 ? 0 : lastIndex);
|
|
|
+
|
|
|
+ // 延迟图表更新,减少卡顿
|
|
|
+ throttle(() => {
|
|
|
+ updateOptions();
|
|
|
+ }, 1000)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听行情推送
|
|
|
+ watch(() => quote.value.last, () => {
|
|
|
+ if (!loading.value) {
|
|
|
+ updateChartData();
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 监听周期选择变化
|
|
|
+ watch(() => props.cycleType, () => initData());
|
|
|
+
|
|
|
+ return {
|
|
|
+ SeriesType,
|
|
|
+ loading,
|
|
|
+ showEmpty,
|
|
|
+ dataIndex,
|
|
|
+ tabs,
|
|
|
+ selectedTab,
|
|
|
+ klineData,
|
|
|
+ klineOption,
|
|
|
+ macdOption,
|
|
|
+ volOption,
|
|
|
+ kdjOption,
|
|
|
+ cciOption,
|
|
|
+ klineDetail,
|
|
|
+ macdDetail,
|
|
|
+ volDetail,
|
|
|
+ kdjDetail,
|
|
|
+ cciDetail,
|
|
|
+ tabChange,
|
|
|
+ mainReady,
|
|
|
+ indicatorReady,
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less">
|
|
|
+@import './index.less';
|
|
|
+</style>
|